Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-2107 Email Field Rework #2701

Merged
merged 15 commits into from
Oct 2, 2024
1 change: 1 addition & 0 deletions express-api/src/controllers/users/usersController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const submitUserAccessRequest = async (req: Request, res: Response) => {
Number(req.body.AgencyId),
req.body.Position,
req.body.Note,
req.body.Email,
);
const config = getConfig();
const user = await userServices.getUser(req.user.preferred_username);
Expand Down
11 changes: 11 additions & 0 deletions express-api/src/services/users/usersServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Agency } from '@/typeorm/Entities/Agency';
import { randomUUID, UUID } from 'crypto';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { UserFiltering } from '@/controllers/users/usersSchema';
import { validateEmail } from '@/utilities/helperFunctions';

interface NormalizedKeycloakUser {
first_name: string;
Expand Down Expand Up @@ -54,10 +55,14 @@ const addKeycloakUserOnHold = async (
agencyId: number,
position: string,
note: string,
email: string,
) => {
if (agencyId == null) {
throw new Error('Null argument.');
}
if (!validateEmail(email)) {
throw new Error('Invalid email.');
}
//Iterating through agencies and roles no longer necessary here?
const normalizedKc = normalizeKeycloakUser(ssoUser);
const systemUser = await AppDataSource.getRepository(User).findOne({
Expand Down Expand Up @@ -207,6 +212,9 @@ const addUser = async (user: User) => {
if (resource) {
throw new ErrorWithCode('Resource already exists.', 409);
}
if (!validateEmail(user.Email)) {
throw new Error('Invalid email.');
}
const retUser = await AppDataSource.getRepository(User).save(user);
return retUser;
};
Expand All @@ -222,6 +230,9 @@ const updateUser = async (user: DeepPartial<User>) => {
if (!resource) {
throw new ErrorWithCode('Resource does not exist.', 404);
}
if (user.Email && !validateEmail(user.Email)) {
throw new Error('Invalid email.');
}
await AppDataSource.getRepository(User).update(user.Id, {
...user,
DisplayName: `${user.LastName}, ${user.FirstName}`,
Expand Down
4 changes: 4 additions & 0 deletions express-api/src/utilities/helperFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Equal, FindOptionsWhere, IsNull, Not, Raw } from 'typeorm';
import { z } from 'zod';

/**
* Special case for PID/PIN matching, as general text comparison is not sufficient.
Expand Down Expand Up @@ -244,3 +245,6 @@ export const toPostgresTimestamp = (date: Date) => {
export const getDaysBetween = (earlierDate: Date, laterDate: Date): number => {
return Math.trunc((laterDate.getTime() - earlierDate.getTime()) / (1000 * 60 * 60 * 24));
};

export const validateEmail = (email: string): boolean =>
z.string().email().safeParse(email).success;
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@ describe('UNIT - User services', () => {
it('should add and return an access request', async () => {
const agencyId = faker.number.int();
//const roleId = faker.string.uuid();
const req = await userServices.addKeycloakUserOnHold(ssoUser, agencyId, '', '');
const req = await userServices.addKeycloakUserOnHold(
ssoUser,
agencyId,
'',
'',
'[email protected]',
);
expect(_usersInsert).toHaveBeenCalledTimes(1);
});
});
Expand Down
13 changes: 13 additions & 0 deletions express-api/tests/unit/utilities/helperFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ILikeWrapper,
TimestampComparisonWrapper,
toPostgresTimestamp,
validateEmail,
} from '@/utilities/helperFunctions';
import { EqualOperator, FindOperator } from 'typeorm';

Expand Down Expand Up @@ -180,4 +181,16 @@ describe('UNIT - helperFunctions', () => {
expect(result.test).toBeUndefined();
});
});

describe('validateEmail', () => {
it('should return true when a valid email is given', () => {
const result = validateEmail('[email protected]');
expect(result).toEqual(true);
});

it('should return false when a invalid email is given', () => {
const result = validateEmail('test@gmaom');
expect(result).toEqual(false);
});
});
});
3 changes: 2 additions & 1 deletion react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"react-router-dom": "6.26.0",
"supercluster": "8.0.1",
"typescript-eslint": "8.6.0",
"use-supercluster": "1.2.0"
"use-supercluster": "1.2.0",
"zod": "3.23.8"
},
"devDependencies": {
"@babel/preset-env": "7.25.2",
Expand Down
12 changes: 10 additions & 2 deletions react-app/src/components/users/UserDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +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';
import { getProvider, validateEmail } from '@/utilities/helperFunctions';

interface IUserDetail {
onClose: () => void;
Expand Down Expand Up @@ -179,7 +179,15 @@ const UserDetail = ({ onClose }: IUserDetail) => {
<TextFormField fullWidth name={'Provider'} label={'Provider'} disabled />
</Grid>
<Grid item xs={6}>
<TextFormField required fullWidth name={'Email'} label={'Email'} />
<TextFormField
required
fullWidth
name={'Email'}
label={'Email'}
rules={{
validate: (value: string) => validateEmail(value) || 'Invalid email.',
}}
/>
</Grid>
<Grid item xs={6}>
<TextFormField required fullWidth name={'FirstName'} label={'First Name'} />
Expand Down
40 changes: 31 additions & 9 deletions react-app/src/pages/AccessRequest.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useMemo } from 'react';
import pendingImage from '@/assets/images/pending.svg';
import { Box, Button, Grid, Paper, Typography } from '@mui/material';
import { Box, Button, Grid, Paper, Tooltip, Typography, useTheme } from '@mui/material';
import AutocompleteFormField from '@/components/form/AutocompleteFormField';
import { useSSO } from '@bcgov/citz-imb-sso-react';
import { FormProvider, useForm } from 'react-hook-form';
Expand All @@ -18,7 +18,8 @@ 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';
import { getProvider, validateEmail } from '@/utilities/helperFunctions';
import InfoIcon from '@mui/icons-material/Info';

interface StatusPageTemplateProps {
blurb: JSX.Element;
Expand All @@ -43,18 +44,21 @@ const RequestForm = ({ submitHandler }: { submitHandler: (d: any) => void }) =>
const keycloak = useSSO();
const agencyOptions = useGroupedAgenciesApi().agencyOptions;
const lookup = useContext(LookupContext);
const theme = useTheme();

const provider = useMemo(
() => getProvider(keycloak.user?.preferred_username, lookup?.data?.Config.bcscIdentifier),
[keycloak.user, lookup],
);

const userIsIdir = provider === 'IDIR';

const formMethods = useForm({
defaultValues: {
Provider: provider,
FirstName: keycloak.user?.first_name,
LastName: keycloak.user?.last_name,
Email: keycloak.user?.email,
FirstName: keycloak.user?.first_name || '',
LastName: keycloak.user?.last_name || '',
Email: userIsIdir ? keycloak.user?.email : '',
Notes: '',
Agency: '',
Position: '',
Expand All @@ -66,7 +70,7 @@ const RequestForm = ({ submitHandler }: { submitHandler: (d: any) => void }) =>
Provider: provider,
FirstName: keycloak.user?.first_name || '',
LastName: keycloak.user?.last_name || '',
Email: keycloak.user?.email || '',
Email: userIsIdir ? keycloak.user?.email : '',
Notes: '',
Agency: '',
Position: '',
Expand All @@ -81,13 +85,31 @@ const RequestForm = ({ submitHandler }: { submitHandler: (d: any) => void }) =>
<TextFormField fullWidth name={'Provider'} label={'Provider'} disabled />
</Grid>
<Grid item xs={6}>
<TextFormField fullWidth name={'Email'} label={'Email'} disabled />
<TextFormField
fullWidth
name={'Email'}
label={'Email'}
disabled={userIsIdir}
required
rules={{
validate: (value: string) => validateEmail(value) || 'Invalid email.',
}}
slotProps={{
input: {
endAdornment: (
<Tooltip title="Avoid entering personal emails">
<InfoIcon fontSize="small" sx={{ color: theme.palette.grey[500] }} />
</Tooltip>
),
},
}}
/>
</Grid>
<Grid item xs={6}>
<TextFormField fullWidth name={'FirstName'} label={'First name'} disabled />
<TextFormField fullWidth name={'FirstName'} label={'First name'} disabled required />
</Grid>
<Grid item xs={6}>
<TextFormField name={'LastName'} fullWidth label={'Last name'} disabled />
<TextFormField name={'LastName'} fullWidth label={'Last name'} disabled required />
</Grid>
<Grid item xs={12}>
<AutocompleteFormField
Expand Down
5 changes: 5 additions & 0 deletions react-app/src/utilities/helperFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { z } from 'zod';

/**
* Pass an array of some arbitrary type and a function that retrieves the key determining uniqueness.
* Returns an new array with any duplicate values omitted.
Expand Down Expand Up @@ -71,3 +73,6 @@ export const getProvider = (username: string, bcscIdentifier?: string) => {
return '';
}
};

export const validateEmail = (email: string): boolean =>
z.string().email().safeParse(email).success;
Loading