diff --git a/express-api/src/constants/config.ts b/express-api/src/constants/config.ts index 0af352bc14..3c9e67c459 100644 --- a/express-api/src/constants/config.ts +++ b/express-api/src/constants/config.ts @@ -32,6 +32,9 @@ const config = { title: 'PIMS', uri: process.env.FRONTEND_URL, }, + keycloak: { + client_id: process.env.SSO_CLIENT_ID, + }, }; const getConfig = () => { diff --git a/express-api/src/controllers/lookup/lookupController.ts b/express-api/src/controllers/lookup/lookupController.ts index ed69c82faa..8783d8cc7c 100644 --- a/express-api/src/controllers/lookup/lookupController.ts +++ b/express-api/src/controllers/lookup/lookupController.ts @@ -380,6 +380,7 @@ export const lookupAll = async (req: Request, res: Response) => { ), Config: { contactEmail: cfg.contact.toEmail, + bcscIdentifier: cfg.keycloak.client_id, }, }; return res.status(200).send(returnObj); diff --git a/express-api/src/routes/lookup.swagger.yaml b/express-api/src/routes/lookup.swagger.yaml index 8b9d437389..ad28db3d96 100644 --- a/express-api/src/routes/lookup.swagger.yaml +++ b/express-api/src/routes/lookup.swagger.yaml @@ -385,6 +385,8 @@ components: contactEmail: type: string example: email@gov.bc.ca + bcscIdentifier: + type: string ConstructionTypes: type: array items: diff --git a/express-api/src/typeorm/Entities/User.ts b/express-api/src/typeorm/Entities/User.ts index 7da1c54dfb..a84409aa73 100644 --- a/express-api/src/typeorm/Entities/User.ts +++ b/express-api/src/typeorm/Entities/User.ts @@ -66,7 +66,8 @@ export class User extends BaseEntity { @Column({ type: 'timestamp', nullable: true }) ApprovedOn: Date; - @Column({ type: 'uuid', nullable: true }) + // Using varchar instead of uuid because some providers use characters outside [0-9a-f] + @Column({ type: 'character varying', nullable: true, length: 36 }) KeycloakUserId: string; // Agency Relations diff --git a/express-api/src/typeorm/Migrations/1726615038425-KeycloakUserIdType.ts b/express-api/src/typeorm/Migrations/1726615038425-KeycloakUserIdType.ts new file mode 100644 index 0000000000..d28e38b2c4 --- /dev/null +++ b/express-api/src/typeorm/Migrations/1726615038425-KeycloakUserIdType.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class KeycloakUserIdType1726615038425 implements MigrationInterface { + name = 'KeycloakUserIdType1726615038425'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN keycloak_user_id TYPE CHARACTER VARYING(36) USING keycloak_user_id::TEXT;`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN keycloak_user_id TYPE UUID USING keycloak_user_id::UUID;`, + ); + } +} diff --git a/react-app/src/components/users/UserDetail.tsx b/react-app/src/components/users/UserDetail.tsx index 7d46436d8b..1ddc08aa75 100644 --- a/react-app/src/components/users/UserDetail.tsx +++ b/react-app/src/components/users/UserDetail.tsx @@ -17,6 +17,7 @@ import { useParams } from 'react-router-dom'; import useDataSubmitter from '@/hooks/useDataSubmitter'; import { Role, Roles } from '@/constants/roles'; import { LookupContext } from '@/contexts/lookupContext'; +import { getProvider } from '@/utilities/helperFunctions'; interface IUserDetail { onClose: () => void; @@ -50,8 +51,13 @@ const UserDetail = ({ onClose }: IUserDetail) => { Role: lookupData?.Roles?.find((role) => role.Id === data?.RoleId), }; + const provider = useMemo( + () => getProvider(data?.Username, lookupData?.Config.bcscIdentifier), + [data], + ); + const userProfileData = { - Provider: data?.Username.includes('idir') ? 'IDIR' : 'BCeID', + Provider: provider, Email: data?.Email, FirstName: data?.FirstName, LastName: data?.LastName, @@ -102,7 +108,7 @@ const UserDetail = ({ onClose }: IUserDetail) => { useEffect(() => { profileFormMethods.reset({ - Provider: data?.Username.includes('idir') ? 'IDIR' : 'BCeID', + Provider: provider, Email: userProfileData.Email, FirstName: userProfileData.FirstName, LastName: userProfileData.LastName, diff --git a/react-app/src/components/users/UsersTable.tsx b/react-app/src/components/users/UsersTable.tsx index a6e21738f1..9ed8e1d75d 100644 --- a/react-app/src/components/users/UsersTable.tsx +++ b/react-app/src/components/users/UsersTable.tsx @@ -11,6 +11,7 @@ import { Agency } from '@/hooks/api/useAgencyApi'; import { User } from '@/hooks/api/useUsersApi'; import { LookupContext } from '@/contexts/lookupContext'; import { Role } from '@/constants/roles'; +import { getProvider } from '@/utilities/helperFunctions'; const CustomMenuItem = (props: PropsWithChildren & { value: string }) => { const theme = useTheme(); @@ -175,17 +176,7 @@ const UsersTable = (props: IUsersTable) => { field: 'Username', headerName: 'Provider', width: 125, - valueGetter: (value) => { - const username: string = value; - if (username && !username.includes('@')) return undefined; - const provider = username.split('@').at(1); - switch (provider) { - case 'idir': - return 'IDIR'; - default: - return 'BCeID'; - } - }, + valueGetter: (value) => getProvider(value, lookup?.data?.Config.bcscIdentifier), }, { field: 'Agency', diff --git a/react-app/src/hooks/api/useLookupApi.ts b/react-app/src/hooks/api/useLookupApi.ts index be6bb6cb82..3969b3142e 100644 --- a/react-app/src/hooks/api/useLookupApi.ts +++ b/react-app/src/hooks/api/useLookupApi.ts @@ -60,6 +60,7 @@ export interface LookupAll { RegionalDistricts: Partial[]; Config: { contactEmail: string; + bcscIdentifier?: string; }; } diff --git a/react-app/src/pages/AccessRequest.tsx b/react-app/src/pages/AccessRequest.tsx index c2a16e863d..965dd2f149 100644 --- a/react-app/src/pages/AccessRequest.tsx +++ b/react-app/src/pages/AccessRequest.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from 'react'; +import React, { useContext, useEffect, useMemo } from 'react'; import pendingImage from '@/assets/images/pending.svg'; import { Box, Button, Grid, Paper, Typography } from '@mui/material'; import AutocompleteFormField from '@/components/form/AutocompleteFormField'; @@ -18,6 +18,7 @@ import TextFormField from '@/components/form/TextFormField'; import { useGroupedAgenciesApi } from '@/hooks/api/useGroupedAgenciesApi'; import { SnackBarContext } from '@/contexts/snackbarContext'; import { LookupContext } from '@/contexts/lookupContext'; +import { getProvider } from '@/utilities/helperFunctions'; interface StatusPageTemplateProps { blurb: JSX.Element; @@ -41,10 +42,16 @@ const StatusPageTemplate = (props: StatusPageTemplateProps) => { const RequestForm = ({ submitHandler }: { submitHandler: (d: any) => void }) => { const keycloak = useSSO(); const agencyOptions = useGroupedAgenciesApi().agencyOptions; + const lookup = useContext(LookupContext); + + const provider = useMemo( + () => getProvider(keycloak.user?.preferred_username, lookup?.data?.Config.bcscIdentifier), + [keycloak.user, lookup], + ); const formMethods = useForm({ defaultValues: { - UserName: keycloak.user?.username, + Provider: provider, FirstName: keycloak.user?.first_name, LastName: keycloak.user?.last_name, Email: keycloak.user?.email, @@ -54,12 +61,24 @@ const RequestForm = ({ submitHandler }: { submitHandler: (d: any) => void }) => }, }); + useEffect(() => { + formMethods.reset({ + Provider: provider, + FirstName: keycloak.user?.first_name || '', + LastName: keycloak.user?.last_name || '', + Email: keycloak.user?.email || '', + Notes: '', + Agency: '', + Position: '', + }); + }, [provider, keycloak.user]); + return ( <> - + diff --git a/react-app/src/utilities/helperFunctions.ts b/react-app/src/utilities/helperFunctions.ts index 4392bd9f34..d56adbfd01 100644 --- a/react-app/src/utilities/helperFunctions.ts +++ b/react-app/src/utilities/helperFunctions.ts @@ -51,3 +51,23 @@ export const getValueByNestedKey = >(obj: T, key: } return result; }; + +/** + * Returns the provider based on the username and optional BCSC identifier. + * @param username The username to check for provider information. + * @param bcscIdentifier The optional BCSC identifier to check for BCSC provider. + * @returns The provider name ('IDIR', 'BCeID', 'BCSC') based on the username or an empty string if no match. + */ +export const getProvider = (username: string, bcscIdentifier?: string) => { + if (!username) return ''; + switch (true) { + case username.includes('idir'): + return 'IDIR'; + case username.includes('bceid'): + return 'BCeID'; + case username.includes(bcscIdentifier): + return 'BC Services Card'; + default: + return ''; + } +};