From 3093f40e00aff7691f6c644461de583b5bad851e Mon Sep 17 00:00:00 2001 From: Brandon Lee Kitajchuk Date: Wed, 26 Oct 2022 09:21:25 -0700 Subject: [PATCH] Feature: Multi-user: Changes to initial admin login processing / implement JWT (#140) * Add simple JWT parsing function * Implement user action for global store --- cypress/fixtures/userLogin.json | 6 ++++-- src/api/types.ts | 21 +++++++++++++++++++-- src/containers/internal/navi/index.tsx | 9 +++++---- src/router/auth.tsx | 20 ++++++++++++++++++++ src/store/actions.ts | 8 +++++--- src/test/index.tsx | 3 ++- 6 files changed, 55 insertions(+), 12 deletions(-) diff --git a/cypress/fixtures/userLogin.json b/cypress/fixtures/userLogin.json index 7fa2907..bfdc6e0 100644 --- a/cypress/fixtures/userLogin.json +++ b/cypress/fixtures/userLogin.json @@ -1,5 +1,7 @@ { - "token": "327a2d17-ce9b-45a7-b0ff-a556536d27fb", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3NpZCI6IjFjNTc4MWQyLTY5MGItNDIwYy1iZDUzLTVkN2Y1NjMwMDVjOCIsInNjb3BlIjoiYWRtaW4iLCJmb3JjZV9jaGFuZ2UiOnRydWUsInBlcm1pc3Npb25zIjpbIlBST1ZJU0lPTl9VU0VSUyIsIlBST1ZJU0lPTl9TRVJWSUNFUyIsIlZJRVdfT05MWSJdLCJpYXQiOjE2NjY3OTgzMTEsImV4cCI6MTY2NjgwMTkxMX0.ZV3KnRit8WGpipfiiMAZ2AVLQ25csWje1-K6hdqxktE", + "scope": "admin", "user_sid": "78131ad5-f041-4d5d-821c-47b2d8c6d015", - "force_change": false + "force_change": false, + "permissions": ["VIEW_ONLY", "PROVISION_SERVICES", "PROVISION_USERS"] } diff --git a/src/api/types.ts b/src/api/types.ts index e32ae06..6658243 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -10,6 +10,22 @@ export type IpType = "ip" | "fqdn" | "fqdn-top-level" | "invalid"; export type LimitCategories = "api_rate" | "voice_call_session" | "device"; +/** User roles / permissions */ + +export type UserScopes = "admin" | "service_provider" | "account"; + +export type UserPermissions = + | "VIEW_ONLY" + | "PROVISION_SERVICES" + | "PROVISION_USERS"; + +// We'll want something like this for actual permissions implementation... +// export enum UserPermissions { +// VIEW_ONLY = 0, +// PROVISION_SERVICES = 1, +// PROVISION_USERS = 2, +// } + /** Status codes */ export enum StatusCodes { @@ -90,12 +106,13 @@ export interface PasswordSettings { /** API responses/payloads */ export interface User { + scope: UserScopes; user_sid: string; + permissions: UserPermissions[]; } -export interface UserLogin { +export interface UserLogin extends User { token: string; - user_sid: string; force_change: boolean; } diff --git a/src/containers/internal/navi/index.tsx b/src/containers/internal/navi/index.tsx index 297032b..140ef4a 100644 --- a/src/containers/internal/navi/index.tsx +++ b/src/containers/internal/navi/index.tsx @@ -108,6 +108,7 @@ export const Navi = ({ /** Fetch service providers */ useEffect(() => { + dispatch({ type: "user" }); dispatch({ type: "serviceProviders" }); }, []); @@ -166,12 +167,12 @@ export const Navi = ({ - {/* Until we have the APIs this initial UI is not accessible */} - {user && ( + {/* Intentionally hiding this as we will need to work this out as we go... */} + {/* ACL component will need to be updated for new user scope/permissions handling */} + {false && user && (
- {/* Seed should be user sid when we have the APIs for it... */} { localStorage.setItem(storageKey, token); }; +/** + * Decode data from a JWT + * https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library + */ +export const parseJwt = (token: string) => { + const base64Url = token.split(".")[1]; + const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); + const jsonPayload = decodeURIComponent( + window + .atob(base64) + .split("") + .map((c) => { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); + + return JSON.parse(jsonPayload); +}; + /** * Provider hook that creates auth object and handles state */ diff --git a/src/store/actions.ts b/src/store/actions.ts index 6006358..5657f63 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,5 +1,6 @@ -import { getUser, getServiceProviders } from "src/api"; +import { getServiceProviders } from "src/api"; import { sortLocaleName } from "src/utils"; +import { getToken, parseJwt } from "src/router/auth"; import type { State, Action } from "./types"; import type { ServiceProvider, User } from "src/api/types"; @@ -61,8 +62,9 @@ export const currentServiceProviderAction = ( }; export const userAsyncAction = async (): Promise => { - const response = await getUser("user_sid"); - return response.json; + const token = getToken(); + const { user_sid, permissions, scope } = parseJwt(token); + return { user_sid, permissions, scope }; }; export const serviceProvidersAsyncAction = async (): Promise< diff --git a/src/test/index.tsx b/src/test/index.tsx index b17f66d..a904478 100644 --- a/src/test/index.tsx +++ b/src/test/index.tsx @@ -6,6 +6,7 @@ import { AuthContext } from "src/router/auth"; import { MSG_SOMETHING_WRONG } from "src/constants"; import type { AuthStateContext } from "src/router/auth"; +import type { UserLogin } from "src/api/types"; import userLogin from "../../cypress/fixtures/userLogin.json"; @@ -19,7 +20,7 @@ type LayoutProviderProps = TestProviderProps & { }; export const signinError = () => Promise.reject(MSG_SOMETHING_WRONG); -export const signinSuccess = () => Promise.resolve(userLogin); +export const signinSuccess = () => Promise.resolve(userLogin as UserLogin); export const signout = () => undefined; export const authProps: AuthStateContext = { token: "",