diff --git a/README.md b/README.md index 5db4ea5..1d37622 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ The package installation can be done in two ways: ## How to use the package ```javascript! -const { TuitioClient, fetch, invalidate } = require("@flare/tuitio-client"); +const { TuitioClient, fetch } = require("@flare/tuitio-client"); const TuitioClient = require("@flare/tuitio-client"); -const type { TuitioToken, TuitioAuthenticationResult, TuitioState } = require("@flare/tuitio-client"); +const type { TuitioLoginResult, TuitioLogoutResult, TuitioState } = require("@flare/tuitio-client"); ``` ```javascript! -import { TuitioClient, fetch, invalidate } from "@flare/tuitio-client"; +import { TuitioClient, fetch } from "@flare/tuitio-client"; import TuitioClient from "@flare/tuitio-client"; -import type { TuitioToken, TuitioAuthenticationResult, TuitioState } from "@flare/tuitio-client"; +import type { TuitioLoginResult, TuitioLogoutResult, TuitioState } from "@flare/tuitio-client"; ``` ## Unit testing @@ -37,4 +37,5 @@ All tests in the package can be executed by running: `npm test`. 1.0.1 - Export Tuitio types 1.0.2 - Validate that Tuitio's URL parameter is a valid URL 1.0.3 - Added LICENSE file -1.0.4 - TuitioState's token property can be null +1.0.4 - TuitioState's token property can be null +1.1.0 - In this version, the account logout method and the latest changes published by Tuitio were implemented. diff --git a/package-lock.json b/package-lock.json index 747faaf..029dd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "@flare/tuitio-client", - "version": "1.0.4", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@flare/tuitio-client", - "version": "1.0.4", - "license": "ISC", + "version": "1.1.0", + "license": "MIT", "dependencies": { "@flare/js-utils": "^1.0.3", "axios": "^1.3.2" diff --git a/package.json b/package.json index f2f0ddb..ff37b08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@flare/tuitio-client", - "version": "1.0.4", + "version": "1.1.0", "description": "Tuitio client is an npm package written in typescript that facilitates the integration of a javascript application with Tuitio.", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/__tests__/TuitioClient.test.ts b/src/__tests__/TuitioClient.test.ts index 378db08..d113ba4 100644 --- a/src/__tests__/TuitioClient.test.ts +++ b/src/__tests__/TuitioClient.test.ts @@ -2,19 +2,24 @@ import { getUrlTemplates } from "../config"; import { TuitioClient } from "../client"; -import type { TuitioAuthenticationResult, TuitioToken } from "../client"; +import type { TuitioLoginResult } from "../client"; test("Get url templates", () => { const result = getUrlTemplates("https://test.com/api"); - expect(result).toHaveProperty("authentication"); - expect(result.authentication).toContain("https://test.com/api"); - expect(result.authentication).toContain("{username}"); - expect(result.authentication).toContain("{password}"); + expect(result).toHaveProperty("login"); + expect(result).toHaveProperty("logout"); + expect(result.login).toContain("https://test.com/api"); + expect(result.login).toContain("{username}"); + expect(result.login).toContain("{password}"); + expect(result.logout).toContain("{token}"); }); test("Tuitio client initialization", () => { - const client = new TuitioClient("https://test.com/api"); - expect(client.tuitioUrl).toBe("https://test.com/api"); + const tuitioUrl = "https://test.com/api"; + const client = new TuitioClient(tuitioUrl); + expect(client.tuitioUrl).toBe(tuitioUrl); + expect(client.urlTemplates.login.startsWith(tuitioUrl)).toBe(true); + expect(client.urlTemplates.logout.startsWith(tuitioUrl)).toBe(true); }); test("Tuitio client initialization with null URL", () => { @@ -35,18 +40,17 @@ test("Tuitio client initialization with invalid URL", () => { test("Tuitio client fake authentication", async () => { const client = new TuitioClient("https://test.com/api"); - const spy = jest.spyOn(client, "authenticate").mockImplementation(async (userName, password) => { - const token = { raw: `mock-${userName}-${password}`, validFrom: new Date("10/02/2023"), validUntil: new Date("11/02/2023") }; - const authResult = { token, status: "_MOCK_" }; + const spy = jest.spyOn(client, "login").mockImplementation(async (userName, password) => { + const token = `mock-${userName}-${password}`; + const authResult = { result: { token, expiresIn: 600000 }, error: null }; return authResult; }); - const result = await client.authenticate("user", "pass"); + const loginResult = await client.login("user", "pass"); - expect(result.token.raw).toBe("mock-user-pass"); - expect(result.token.validFrom).toStrictEqual(new Date("10/02/2023")); - expect(result.token.validUntil).toStrictEqual(new Date("11/02/2023")); - expect(result.status).toBe("_MOCK_"); + expect(loginResult.result.token).toBe("mock-user-pass"); + expect(loginResult.result.expiresIn).toBe(600000); + expect(loginResult.error).toBeNull; spy.mockRestore(); }); diff --git a/src/__tests__/TuitioAuthentication.test.ts b/src/__tests__/TuitioLogin.test.ts similarity index 57% rename from src/__tests__/TuitioAuthentication.test.ts rename to src/__tests__/TuitioLogin.test.ts index 3e48ff4..22a1ab7 100644 --- a/src/__tests__/TuitioAuthentication.test.ts +++ b/src/__tests__/TuitioLogin.test.ts @@ -5,34 +5,32 @@ // Copyright (c) 2023 Tudor Stanciu import axios from "axios"; -import { TuitioClient, fetch, invalidate } from "../client"; +import { TuitioClient, fetch } from "../client"; jest.mock("axios"); -test("Tuitio client authentication", async () => { +test("Tuitio client: successfully account login", async () => { (axios.request as jest.Mock).mockResolvedValue({ data: { - token: { raw: "mock-user-pass", validFrom: new Date("10/02/2023"), validUntil: new Date("11/02/2023") }, - status: "_MOCK_" + result: { + token: "token-mock", + expiresIn: 600000 + }, + error: null } }); const client = new TuitioClient("https://test.com/api"); - const result = await client.authenticate("user", "pass"); + const loginResult = await client.login("user", "pass"); - expect(result.token.raw).toBe("mock-user-pass"); - expect(result.token.validFrom).toStrictEqual(new Date("10/02/2023")); - expect(result.token.validUntil).toStrictEqual(new Date("11/02/2023")); - expect(result.status).toBe("_MOCK_"); + expect(loginResult.result.token).toBe("token-mock"); + expect(loginResult.result.expiresIn).toBe(600000); + expect(loginResult.error).toBeNull(); const storage = fetch(); - expect(storage.token?.raw).toBe("mock-user-pass"); - expect(storage.userName).toBe("user"); - - invalidate(); - const emptyStorage = fetch(); - expect(emptyStorage.token).toBeNull(); - expect(emptyStorage.userName).toBeNull(); + expect(storage).toBeDefined(); + expect(storage?.token).toBe("token-mock"); + expect(storage?.userName).toBe("user"); }); test("Tuitio client failed authentication", async () => { @@ -46,7 +44,7 @@ test("Tuitio client failed authentication", async () => { const client = new TuitioClient("https://test.com/api"); try { - await client.authenticate("user", "pass"); + await client.login("user", "pass"); } catch (error: any) { expect(error.title).toBe("Internal server error"); expect(error.message).toBe("Internal server error"); @@ -64,7 +62,7 @@ test("Tuitio client failed authentication with unhandled exception", async () => const client = new TuitioClient("https://test.com/api"); try { - await client.authenticate("user", "pass"); + await client.login("user", "pass"); } catch (error: any) { expect(error.response.status).toBe(500); expect(error.response.error.message).toBe("Internal server error"); diff --git a/src/__tests__/TuitioLogout.test.ts b/src/__tests__/TuitioLogout.test.ts new file mode 100644 index 0000000..389b7ce --- /dev/null +++ b/src/__tests__/TuitioLogout.test.ts @@ -0,0 +1,32 @@ +/** + * @jest-environment jsdom + */ + +// Copyright (c) 2023 Tudor Stanciu + +import axios from "axios"; +import { TuitioClient, fetch } from "../client"; + +jest.mock("axios"); + +test("Tuitio client: successfully account logout", async () => { + (axios.request as jest.Mock).mockResolvedValue({ + data: { + result: { userId: 0, userName: "tuitio.user", logoutDate: new Date() }, + error: null + } + }); + + const client = new TuitioClient("https://test.com/api"); + const logoutResult = await client.logout("token-mock"); + + expect(logoutResult.result.userId).toBe(0); + expect(logoutResult.result.userName).toBe("tuitio.user"); + expect(logoutResult.error).toBeNull(); + + const storage = fetch(); + expect(storage).toBeDefined(); + expect(storage.token).toBeNull(); + expect(storage.validUntil).toBeNull(); + expect(storage.userName).toBeNull(); +}); diff --git a/src/__tests__/TuitioStorage.test.ts b/src/__tests__/TuitioStorage.test.ts index 45904ad..cc83994 100644 --- a/src/__tests__/TuitioStorage.test.ts +++ b/src/__tests__/TuitioStorage.test.ts @@ -4,14 +4,11 @@ // Copyright (c) 2023 Tudor Stanciu -import { fetch, invalidate } from "../client"; +import { fetch } from "../client"; test("Tuitio empty storage", () => { const result = fetch(); expect(result.token).toBeNull(); + expect(result.validUntil).toBeNull(); expect(result.userName).toBeNull(); }); - -test("Tuitio storage invalidation", () => { - expect(invalidate()).toBe(void 0); -}); diff --git a/src/client.ts b/src/client.ts index 30a1960..2663491 100644 --- a/src/client.ts +++ b/src/client.ts @@ -5,6 +5,7 @@ import { localStorage } from "@flare/js-utils"; import { storageKeys } from "./constants"; import { getUrlTemplates } from "./config"; import { isValidURL } from "./utils"; +import type { UrlTemplates } from "./config"; const { setItem, getItem, removeItem } = localStorage; @@ -24,11 +25,12 @@ async function request(url: string, method: string) { } } -export type TuitioToken = { raw: string; validFrom: Date; validUntil: Date }; -export type TuitioAuthenticationResult = { token: TuitioToken; status: string }; +export type TuitioLoginResult = { result: { token: string; expiresIn: number }; error: string | null }; +export type TuitioLogoutResult = { result: { userId: number; userName: string; logoutDate: Date }; error: string }; class TuitioClient { tuitioUrl: string; + urlTemplates: UrlTemplates; constructor(tuitioUrl: string | null) { if (tuitioUrl === null || tuitioUrl === "" || isValidURL(tuitioUrl) === false) { @@ -36,40 +38,48 @@ class TuitioClient { } this.tuitioUrl = tuitioUrl; + this.urlTemplates = getUrlTemplates(this.tuitioUrl); } - async authenticate(userName: string, password: string): Promise { - const templates = getUrlTemplates(this.tuitioUrl); - const url = templates.authentication.replace("{username}", userName).replace("{password}", password); + async login(userName: string, password: string): Promise { + const url = this.urlTemplates.login.replace("{username}", userName).replace("{password}", password); const response = await request(url, "post"); - if (response.token) { - setItem(storageKeys.TOKEN, response.token); - setItem(storageKeys.USER, userName); + if (!response.error) { + const currentDate = new Date(); + const expiresIn = response.result.expiresIn - 10000; //10 seconds of safety margin + const validUntil = new Date(currentDate.getTime() + expiresIn); + const authData = { token: response.result.token, validUntil, userName }; + setItem(storageKeys.AUTH_DATA, authData); } return response; } + + async logout(token: string): Promise { + const url = this.urlTemplates.logout.replace("{token}", token); + const response = await request(url, "post"); + if (!response.error) { + removeItem(storageKeys.AUTH_DATA); + } + return response; + } } -const invalidate = (): void => { - const token = getItem(storageKeys.TOKEN); - if (token) { - removeItem(storageKeys.TOKEN); - removeItem(storageKeys.USER); - } -}; - -export type TuitioState = { token: TuitioToken | null; userName: string }; +export type TuitioState = { token: string | null; validUntil: Date | null; userName: string | null }; +const defaultTuitioState: TuitioState = { token: null, validUntil: null, userName: null }; const fetch = (): TuitioState => { - const data = { - token: getItem(storageKeys.TOKEN), - userName: getItem(storageKeys.USER) + const authData = getItem(storageKeys.AUTH_DATA); + if (!authData) return defaultTuitioState; + const { token, validUntil, userName } = authData; + const data: TuitioState = { + token, + validUntil, + userName }; - return data; }; -export { TuitioClient, invalidate, fetch }; +export { TuitioClient, fetch }; export default TuitioClient; diff --git a/src/config.ts b/src/config.ts index 73070cc..6e57115 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,12 +2,14 @@ import { combineUrls } from "./utils"; -type UrlTemplates = { - authentication: string; +export type UrlTemplates = { + login: string; + logout: string; }; const getUrlTemplates = (tuitioUrl: string): UrlTemplates => ({ - authentication: combineUrls(tuitioUrl, "/identity/authenticate?UserName={username}&Password={password}") + login: combineUrls(tuitioUrl, "/account/login?UserName={username}&Password={password}"), + logout: combineUrls(tuitioUrl, "/account/logout?Token={token}") }); export { getUrlTemplates }; diff --git a/src/constants.ts b/src/constants.ts index d7ebcab..10ffe3c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,5 @@ // Copyright (c) 2023 Tudor Stanciu -export const storageKeys: { TOKEN: string; USER: string } = { - TOKEN: "TUITIO_AUTHORIZATION_TOKEN", - USER: "TUITIO_USER_NAME" +export const storageKeys: { AUTH_DATA: string } = { + AUTH_DATA: "tuitio.auth.data" }; diff --git a/src/index.ts b/src/index.ts index c2b304e..98fc5a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,8 @@ // Copyright (c) 2023 Tudor Stanciu -import { TuitioClient, invalidate, fetch } from "./client"; -import type { TuitioToken, TuitioAuthenticationResult, TuitioState } from "./client"; +import { TuitioClient, fetch } from "./client"; +import type { TuitioLoginResult, TuitioLogoutResult, TuitioState } from "./client"; -export { TuitioClient, invalidate, fetch }; -export type { TuitioToken, TuitioAuthenticationResult, TuitioState }; +export { TuitioClient, fetch }; +export type { TuitioLoginResult, TuitioLogoutResult, TuitioState }; export default TuitioClient;