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

Refactor some user invite code #514

Merged
merged 8 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/api/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { getKcAdminClient } from '../keycloakAdminClient';
import {
assignClientRoleToUser,
queryUsersByEmail,
sendInviteEmail,
sendInviteEmailToNewUser,
} from '../services/kcUsersService';
import { LoggerService } from '../services/loggerService';
import { SelfResendInvitationParser, UserService } from '../services/userService';
Expand Down Expand Up @@ -85,7 +85,7 @@ export class UserController {
res.sendStatus(200);
}
logger.info(`Resending invitation email for ${email}, keycloak ID ${user[0].id}`);
await sendInviteEmail(kcAdminClient, user[0]);
await sendInviteEmailToNewUser(kcAdminClient, user[0]);
res.sendStatus(200);
}

Expand Down Expand Up @@ -116,7 +116,7 @@ export class UserController {
}

logger.info(`Resending invitation email for ${req.user?.email}, keycloak ID ${user[0].id}`);
await sendInviteEmail(kcAdminClient, user[0]);
await sendInviteEmailToNewUser(kcAdminClient, user[0]);
res.sendStatus(200);
}

Expand Down
55 changes: 31 additions & 24 deletions src/api/routers/participants/participantsCreation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AuditAction, AuditTrailEvents } from '../../entities/AuditTrail';
import {
Participant,
ParticipantCreationPartial,
ParticipantStatus
ParticipantStatus,
} from '../../entities/Participant';
import { User, UserCreationPartial } from '../../entities/User';
import { UserRoleId } from '../../entities/UserRole';
Expand All @@ -16,26 +16,26 @@ import { getKcAdminClient } from '../../keycloakAdminClient';
import { addSite, getSiteList, setSiteClientTypes } from '../../services/adminServiceClient';
import {
mapClientTypesToAdminEnums,
SiteCreationRequest
SiteCreationRequest,
} from '../../services/adminServiceHelpers';
import {
constructAuditTrailObject,
performAsyncOperationWithAuditTrail
performAsyncOperationWithAuditTrail,
} from '../../services/auditTrailService';
import {
assignClientRoleToUser,
createNewUser,
sendInviteEmail
sendInviteEmailToNewUser,
} from '../../services/kcUsersService';
import {
getParticipantTypesByIds,
ParticipantRequest,
sendNewParticipantEmail
sendNewParticipantEmail,
} from '../../services/participantsService';
import { findUserByEmail } from '../../services/usersService';
import {
ParticipantCreationAndApprovalPartial,
ParticipantCreationRequest
ParticipantCreationRequest,
} from './participantClasses';

export async function validateParticipantCreationRequest(
Expand Down Expand Up @@ -95,23 +95,13 @@ const createUserAndAssociatedParticipant = async (
});
};

export async function createParticipant(req: ParticipantRequest, res: Response) {
const participantRequest = ParticipantCreationRequest.parse(req.body);
const traceId = getTraceId(req);

const validationError = await validateParticipantCreationRequest(participantRequest);
if (validationError) {
return res.status(400).send(validationError);
}

const requestingUser = await findUserByEmail(req.auth?.payload?.email as string);
const user = UserCreationPartial.parse({
...req.body,
acceptedTerms: false,
});

async function createParticipant(
email: string,
participantRequest: z.infer<typeof ParticipantCreationRequest>,
user: z.infer<typeof UserCreationPartial>,
traceId: string
) {
const types = await getParticipantTypesByIds(participantRequest.participantTypes);
const apiRoles = await ApiRole.query().findByIds(participantRequest.apiRoles);

let site;
if (!participantRequest.siteId) {
Expand All @@ -127,7 +117,7 @@ export async function createParticipant(req: ParticipantRequest, res: Response)
// existing site. Update client types
setSiteClientTypes({ siteId: participantRequest.siteId, types });
}

const apiRoles = await ApiRole.query().findByIds(participantRequest.apiRoles);
const parsedParticipantRequest = ParticipantCreationAndApprovalPartial.parse({
name: participantRequest.participantName,
types,
Expand All @@ -136,6 +126,7 @@ export async function createParticipant(req: ParticipantRequest, res: Response)
crmAgreementNumber: participantRequest.crmAgreementNumber,
});

const requestingUser = await findUserByEmail(email);
const auditTrailInsertObject = constructAuditTrailObject(
requestingUser!,
AuditTrailEvents.ManageParticipant,
Expand Down Expand Up @@ -175,8 +166,24 @@ export async function createParticipant(req: ParticipantRequest, res: Response)
assignClientRoleToUser(kcAdminClient, user.email, 'api-participant-member');

// send email
await sendInviteEmail(kcAdminClient, newKcUser);
await sendInviteEmailToNewUser(kcAdminClient, newKcUser);
});
}

export async function handleCreateParticipant(req: ParticipantRequest, res: Response) {
const participantRequest = ParticipantCreationRequest.parse(req.body);
const traceId = getTraceId(req);

const validationError = await validateParticipantCreationRequest(participantRequest);
if (validationError) {
return res.status(400).send(validationError);
}
const user = UserCreationPartial.parse({
...req.body,
acceptedTerms: false,
});
const email = req.auth?.payload?.email as string;
await createParticipant(email, participantRequest, user, traceId);

return res.sendStatus(200);
}
Expand Down
89 changes: 13 additions & 76 deletions src/api/routers/participants/participantsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
Participant,
ParticipantApprovalPartial,
ParticipantDTO,
ParticipantStatus,
ParticipantStatus
} from '../../entities/Participant';
import { UserDTO, UserJobFunction } from '../../entities/User';
import { UserDTO } from '../../entities/User';
import { siteIdNotSetError } from '../../helpers/errorHelpers';
import { getTraceId } from '../../helpers/loggingHelpers';
import { getKcAdminClient } from '../../keycloakAdminClient';
Expand All @@ -26,27 +26,23 @@ import {
renameApiKey,
setSiteClientTypes,
updateApiKeyRoles,
updateKeyPair,
updateKeyPair
} from '../../services/adminServiceClient';
import {
mapAdminApiKeysToApiKeyDTOs,
ParticipantApprovalResponse,
ParticipantApprovalResponse
} from '../../services/adminServiceHelpers';
import {
createdApiKeyToApiKeySecrets,
getApiKey,
getApiRoles,
validateApiRoles,
validateApiRoles
} from '../../services/apiKeyService';
import {
constructAuditTrailObject,
performAsyncOperationWithAuditTrail,
performAsyncOperationWithAuditTrail
} from '../../services/auditTrailService';
import {
assignClientRoleToUser,
createNewUser,
sendInviteEmail,
} from '../../services/kcUsersService';
import { assignClientRoleToUser } from '../../services/kcUsersService';
import {
addSharingParticipants,
deleteSharingParticipants,
Expand All @@ -57,21 +53,17 @@ import {
updateParticipant,
updateParticipantAndTypesAndApiRoles,
UpdateSharingTypes,
UserParticipantRequest,
UserParticipantRequest
alex-yau-ttd marked this conversation as resolved.
Show resolved Hide resolved
} from '../../services/participantsService';
import { getSignedParticipants } from '../../services/signedParticipantsService';
import {
createUserInPortal,
findUserByEmail,
getAllUserFromParticipant,
} from '../../services/usersService';
import { getAllUserFromParticipant } from '../../services/usersService';
import { createBusinessContactsRouter } from '../businessContactsRouter';
import { createParticipantUsersRouter } from '../participantUsersRouter';
import { getParticipantAppNames, setParticipantAppNames } from './participantsAppIds';
import { createParticipant, createParticipantFromRequest } from './participantsCreation';
import { createParticipantFromRequest, handleCreateParticipant } from './participantsCreation';
import { getParticipantDomainNames, setParticipantDomainNames } from './participantsDomainNames';
import { getParticipantKeyPairs } from './participantsKeyPairs';
import { getParticipantUsers } from './participantsUsers';
import { getParticipantUsers, handleInviteUserToParticipant } from './participantsUsers';

export type AvailableParticipantDTO = Required<Pick<ParticipantDTO, 'name' | 'siteId' | 'types'>>;

Expand Down Expand Up @@ -210,7 +202,7 @@ export function createParticipantsRouter() {
}
);

participantsRouter.put('/', createParticipant);
participantsRouter.put('/', handleCreateParticipant);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed because I split the handler into two functions, one of which is called createParticipant


participantsRouter.use('/:participantId', verifyAndEnrichParticipant);

Expand All @@ -220,62 +212,7 @@ export function createParticipantsRouter() {
return res.status(200).json(participant);
});

const invitationParser = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string(),
jobFunction: z.nativeEnum(UserJobFunction),
});

participantsRouter.post(
'/:participantId/invite',
async (req: UserParticipantRequest, res: Response) => {
try {
const { participant, user } = req;
const { firstName, lastName, email, jobFunction } = invitationParser.parse(req.body);
const traceId = getTraceId(req);
// TODO: UID2-3878 - support user belonging to multiple participants by not 400ing here if the user already exists.
const existingUser = await findUserByEmail(email);
if (existingUser) {
return res.status(400).send('Error inviting user');
}
const kcAdminClient = await getKcAdminClient();
const auditTrailInsertObject = constructAuditTrailObject(
user!,
AuditTrailEvents.ManageTeamMembers,
{
action: AuditAction.Add,
firstName,
lastName,
email,
jobFunction,
},
participant!.id
);

await performAsyncOperationWithAuditTrail(auditTrailInsertObject, traceId, async () => {
const newUser = await createNewUser(kcAdminClient, firstName, lastName, email);
await createUserInPortal(
{
email,
jobFunction,
firstName,
lastName,
},
participant!.id
);
await sendInviteEmail(kcAdminClient, newUser);
});

return res.sendStatus(201);
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).send(err.issues);
}
throw err;
}
}
);
participantsRouter.post('/:participantId/invite', handleInviteUserToParticipant);
Copy link
Contributor Author

@alex-yau-ttd alex-yau-ttd Sep 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extracted this out - functionality is the same (will be changed in a subsequent PR)


participantsRouter.get(
'/:participantId/sharingPermission',
Expand Down
72 changes: 70 additions & 2 deletions src/api/routers/participants/participantsUsers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,78 @@
import { Response } from 'express';
import { z } from 'zod';

import { ParticipantRequest } from '../../services/participantsService';
import { getAllUserFromParticipant } from '../../services/usersService';
import { AuditAction, AuditTrailEvents } from '../../entities/AuditTrail';
import { UserJobFunction } from '../../entities/User';
import { getTraceId } from '../../helpers/loggingHelpers';
import { getKcAdminClient } from '../../keycloakAdminClient';
import {
constructAuditTrailObject,
performAsyncOperationWithAuditTrail,
} from '../../services/auditTrailService';
import { createNewUser, sendInviteEmailToNewUser } from '../../services/kcUsersService';
import { ParticipantRequest, UserParticipantRequest } from '../../services/participantsService';
import {
createUserInPortal,
findUserByEmail,
getAllUserFromParticipant,
} from '../../services/usersService';

export async function getParticipantUsers(req: ParticipantRequest, res: Response) {
const { participant } = req;
const users = await getAllUserFromParticipant(participant!);
return res.status(200).json(users);
}

const invitationParser = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string(),
jobFunction: z.nativeEnum(UserJobFunction),
});

export async function handleInviteUserToParticipant(req: UserParticipantRequest, res: Response) {
try {
const { participant, user } = req;
const { firstName, lastName, email, jobFunction } = invitationParser.parse(req.body);
const traceId = getTraceId(req);
// TODO: UID2-3878 - support user belonging to multiple participants by not 400ing here if the user already exists.
const existingUser = await findUserByEmail(email);
if (existingUser) {
return res.status(400).send('Error inviting user');
}
const kcAdminClient = await getKcAdminClient();
const auditTrailInsertObject = constructAuditTrailObject(
user!,
AuditTrailEvents.ManageTeamMembers,
{
action: AuditAction.Add,
firstName,
lastName,
email,
jobFunction,
},
participant!.id
);

await performAsyncOperationWithAuditTrail(auditTrailInsertObject, traceId, async () => {
const newUser = await createNewUser(kcAdminClient, firstName, lastName, email);
await createUserInPortal(
{
email,
jobFunction,
firstName,
lastName,
},
participant!.id
);
await sendInviteEmailToNewUser(kcAdminClient, newUser);
});

return res.sendStatus(201);
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).send(err.issues);
}
throw err;
}
}
2 changes: 1 addition & 1 deletion src/api/services/kcUsersService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const createNewUser = async (
};

const logoutUrl = new URL('logout', SSP_WEB_BASE_URL).href;
export const sendInviteEmail = async (
export const sendInviteEmailToNewUser = async (
kcAdminClient: KeycloakAdminClient,
user: UserRepresentation
) => {
Expand Down
Loading