diff --git a/src/api/configureApi.ts b/src/api/configureApi.ts index 6cb0c30d1..88e1a3a80 100644 --- a/src/api/configureApi.ts +++ b/src/api/configureApi.ts @@ -36,6 +36,7 @@ import makeMetricsApiMiddleware from './middleware/metrics'; import { createParticipantsRouter } from './routers/participants/participantsRouter'; import { createSitesRouter } from './routers/sitesRouter'; import { createUsersRouter } from './routers/usersRouter'; +import { API_PARTICIPANT_MEMBER_ROLE_NAME } from './services/kcUsersService'; import { LoggerService } from './services/loggerService'; import { UserService } from './services/userService'; @@ -134,7 +135,7 @@ export function configureAndStartApi(useMetrics: boolean = true, portNumber: num bypassHandlerForPaths( claimCheck((claim: Claim) => { const roles = claim.resource_access?.self_serve_portal_apis?.roles || []; - return roles.includes('api-participant-member'); + return roles.includes(API_PARTICIPANT_MEMBER_ROLE_NAME); }), ...BYPASS_CLAIM_PATHS, ...BYPASS_AUTH_PATHS diff --git a/src/api/controllers/userController.ts b/src/api/controllers/userController.ts index 9bcac13b9..0e6e009dc 100644 --- a/src/api/controllers/userController.ts +++ b/src/api/controllers/userController.ts @@ -16,9 +16,9 @@ import { ParticipantStatus } from '../entities/Participant'; import { getTraceId } from '../helpers/loggingHelpers'; import { getKcAdminClient } from '../keycloakAdminClient'; import { - assignClientRoleToUser, + assignApiParticipantMemberRole, queryUsersByEmail, - sendInviteEmail, + sendInviteEmailToNewUser, } from '../services/kcUsersService'; import { LoggerService } from '../services/loggerService'; import { SelfResendInvitationParser, UserService } from '../services/userService'; @@ -66,7 +66,7 @@ export class UserController { const kcAdminClient = await getKcAdminClient(); const promises = [ req.user!.$query().patch({ acceptedTerms: true }), - assignClientRoleToUser(kcAdminClient, req.user?.email!, 'api-participant-member'), + assignApiParticipantMemberRole(kcAdminClient, req.user?.email!), ]; await Promise.all(promises); res.sendStatus(200); @@ -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); } @@ -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); } diff --git a/src/api/routers/participants/participantsCreation.ts b/src/api/routers/participants/participantsCreation.ts index a78a7f68c..453f0ba00 100644 --- a/src/api/routers/participants/participantsCreation.ts +++ b/src/api/routers/participants/participantsCreation.ts @@ -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'; @@ -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, + assignApiParticipantMemberRole, 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( @@ -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, + user: z.infer, + traceId: string +) { const types = await getParticipantTypesByIds(participantRequest.participantTypes); - const apiRoles = await ApiRole.query().findByIds(participantRequest.apiRoles); let site; if (!participantRequest.siteId) { @@ -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, @@ -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, @@ -172,11 +163,27 @@ export async function createParticipant(req: ParticipantRequest, res: Response) ); // assign proper api access - assignClientRoleToUser(kcAdminClient, user.email, 'api-participant-member'); + await assignApiParticipantMemberRole(kcAdminClient, user.email); // 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); } diff --git a/src/api/routers/participants/participantsRouter.ts b/src/api/routers/participants/participantsRouter.ts index 53c53d4ac..2a6ba7c89 100644 --- a/src/api/routers/participants/participantsRouter.ts +++ b/src/api/routers/participants/participantsRouter.ts @@ -10,7 +10,7 @@ import { ParticipantDTO, 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'; @@ -42,11 +42,7 @@ import { constructAuditTrailObject, performAsyncOperationWithAuditTrail, } from '../../services/auditTrailService'; -import { - assignClientRoleToUser, - createNewUser, - sendInviteEmail, -} from '../../services/kcUsersService'; +import { assignApiParticipantMemberRole } from '../../services/kcUsersService'; import { addSharingParticipants, deleteSharingParticipants, @@ -60,18 +56,14 @@ import { UserParticipantRequest, } 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>; @@ -175,7 +167,7 @@ export function createParticipantsRouter() { await setSiteClientTypes(data); await Promise.all( usersFromParticipant.map((currentUser) => - assignClientRoleToUser(kcAdminClient, currentUser.email, 'api-participant-member') + assignApiParticipantMemberRole(kcAdminClient, currentUser.email) ) ); @@ -210,7 +202,7 @@ export function createParticipantsRouter() { } ); - participantsRouter.put('/', createParticipant); + participantsRouter.put('/', handleCreateParticipant); participantsRouter.use('/:participantId', verifyAndEnrichParticipant); @@ -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); participantsRouter.get( '/:participantId/sharingPermission', diff --git a/src/api/routers/participants/participantsUsers.ts b/src/api/routers/participants/participantsUsers.ts index 98ac115ee..a3603f552 100644 --- a/src/api/routers/participants/participantsUsers.ts +++ b/src/api/routers/participants/participantsUsers.ts @@ -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; + } +} diff --git a/src/api/services/kcUsersService.ts b/src/api/services/kcUsersService.ts index 48d07d01f..f42b9d02c 100644 --- a/src/api/services/kcUsersService.ts +++ b/src/api/services/kcUsersService.ts @@ -4,6 +4,8 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRep import { SSP_KK_API_CLIENT_ID, SSP_KK_SSL_RESOURCE, SSP_WEB_BASE_URL } from '../envars'; +export const API_PARTICIPANT_MEMBER_ROLE_NAME = 'api-participant-member'; + export const queryUsersByEmail = async (kcAdminClient: KeycloakAdminClient, email: string) => { return kcAdminClient.users.find({ email, @@ -11,6 +13,14 @@ export const queryUsersByEmail = async (kcAdminClient: KeycloakAdminClient, emai }); }; +export const doesUserExistInKeycloak = async ( + kcAdminClient: KeycloakAdminClient, + email: string +) => { + const existingKcUser = await queryUsersByEmail(kcAdminClient, email); + return existingKcUser.length > 0; +}; + export const createNewUser = async ( kcAdminClient: KeycloakAdminClient, firstName: string, @@ -30,7 +40,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 ) => { @@ -76,7 +86,7 @@ export const deleteUserByEmail = async (kcAdminClient: KeycloakAdminClient, user }); }; -export const assignClientRoleToUser = async ( +const assignClientRoleToUser = async ( kcAdminClient: KeycloakAdminClient, userEmail: string, roleName: string @@ -101,3 +111,10 @@ export const assignClientRoleToUser = async ( ], }); }; + +export const assignApiParticipantMemberRole = async ( + kcAdminClient: KeycloakAdminClient, + userEmail: string +) => { + await assignClientRoleToUser(kcAdminClient, userEmail, API_PARTICIPANT_MEMBER_ROLE_NAME); +}; diff --git a/src/web/stores/userStore.ts b/src/web/stores/userStore.ts deleted file mode 100644 index 177870ace..000000000 --- a/src/web/stores/userStore.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const userStore = { - username: 'lionellpack@gmail.com', -};