diff --git a/apps/api-gateway/src/authz/guards/org-roles.guard.ts b/apps/api-gateway/src/authz/guards/org-roles.guard.ts index b4e71fb26..b9018781b 100644 --- a/apps/api-gateway/src/authz/guards/org-roles.guard.ts +++ b/apps/api-gateway/src/authz/guards/org-roles.guard.ts @@ -1,7 +1,5 @@ import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException, Logger } from '@nestjs/common'; -import { HttpException } from '@nestjs/common'; -import { HttpStatus } from '@nestjs/common'; import { Injectable } from '@nestjs/common'; import { OrgRoles } from 'libs/org-roles/enums'; import { ROLES_KEY } from '../decorators/roles.decorator'; @@ -34,15 +32,24 @@ export class OrgRolesGuard implements CanActivate { const orgId = req.params.orgId || req.query.orgId || req.body.orgId; - if (!orgId) { - throw new BadRequestException(ResponseMessages.organisation.error.orgIdIsRequired); - } + if (orgId) { if (!isValidUUID(orgId)) { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); - } - - if (orgId) { + } + + const orgDetails = user.resource_access[orgId]; + + if (orgDetails) { + const orgRoles: string[] = orgDetails.roles; + const roleAccess = requiredRoles.some((role) => orgRoles.includes(role)); + + if (!roleAccess) { + throw new ForbiddenException(ResponseMessages.organisation.error.roleNotMatch, { cause: new Error(), description: ResponseMessages.errorMessages.forbidden }); + } + return roleAccess; + } + const specificOrg = user.userOrgRoles.find((orgDetails) => { if (!orgDetails.orgId) { return false; @@ -78,7 +85,7 @@ export class OrgRolesGuard implements CanActivate { return false; } else { - throw new HttpException('organization is required', HttpStatus.BAD_REQUEST); + throw new BadRequestException('organization is required'); } // Sending user friendly message if a user attempts to access an API that is inaccessible to their role diff --git a/apps/api-gateway/src/authz/guards/user-access-guard.ts b/apps/api-gateway/src/authz/guards/user-access-guard.ts new file mode 100644 index 000000000..ea53f63fc --- /dev/null +++ b/apps/api-gateway/src/authz/guards/user-access-guard.ts @@ -0,0 +1,16 @@ +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class UserAccessGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + + const { user } = request; + + if (user.hasOwnProperty('client_id')) { + throw new UnauthorizedException('You do not have access'); + } + return true; + } +} diff --git a/apps/api-gateway/src/organization/organization.controller.ts b/apps/api-gateway/src/organization/organization.controller.ts index 20635a1f9..39e4e4047 100644 --- a/apps/api-gateway/src/organization/organization.controller.ts +++ b/apps/api-gateway/src/organization/organization.controller.ts @@ -95,7 +95,7 @@ export class OrganizationController { * @returns get organization roles */ - @Get('/roles') + @Get('/:orgId/roles') @ApiOperation({ summary: 'Fetch org-roles details', description: 'Fetch org-roles details' @@ -103,9 +103,9 @@ export class OrganizationController { @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() - async getOrgRoles(@Res() res: Response): Promise { + async getOrgRoles(@Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @Res() res: Response): Promise { - const orgRoles = await this.organizationService.getOrgRoles(); + const orgRoles = await this.organizationService.getOrgRoles(orgId.trim()); const finalResponse: IResponse = { statusCode: HttpStatus.OK, @@ -327,7 +327,11 @@ export class OrganizationController { @UseGuards(AuthGuard('jwt')) @ApiBearerAuth() async createOrganization(@Body() createOrgDto: CreateOrganizationDto, @Res() res: Response, @User() reqUser: user): Promise { - const orgData = await this.organizationService.createOrganization(createOrgDto, reqUser.id); + + // eslint-disable-next-line prefer-destructuring + const keycloakUserId = reqUser.keycloakUserId; + + const orgData = await this.organizationService.createOrganization(createOrgDto, reqUser.id, keycloakUserId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.organisation.success.create, @@ -350,7 +354,11 @@ export class OrganizationController { @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiBearerAuth() async createOrgCredentials(@Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise { - const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id); + + // eslint-disable-next-line prefer-destructuring + const keycloakUserId = reqUser.keycloakUserId; + + const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id, keycloakUserId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.organisation.success.orgCredentials, @@ -381,6 +389,28 @@ export class OrganizationController { return res.status(HttpStatus.OK).json(finalResponse); } + @Post('/register-org-map-users') + @ApiOperation({ + summary: 'Register client and map users', + description: 'Register client and map users' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.PLATFORM_ADMIN) + @ApiBearerAuth() + @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) + async registerOrgsMapUsers(@Res() res: Response): Promise { + + await this.organizationService.registerOrgsMapUsers(); + + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: 'Organization client created and users mapped to client' + }; + + return res.status(HttpStatus.CREATED).json(finalResponse); + + } + @Post('/:orgId/invitations') @ApiOperation({ summary: 'Create organization invitation', @@ -482,6 +512,7 @@ export class OrganizationController { @ApiOperation({ summary: 'Delete Organization Client Credentials', description: 'Delete Organization Client Credentials' }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() + @ApiExcludeEndpoint() @UseGuards(AuthGuard('jwt')) async deleteOrgClientCredentials(@Param('orgId') orgId: string, @Res() res: Response): Promise { diff --git a/apps/api-gateway/src/organization/organization.service.ts b/apps/api-gateway/src/organization/organization.service.ts index 1e4d377d8..0baf88fa0 100644 --- a/apps/api-gateway/src/organization/organization.service.ts +++ b/apps/api-gateway/src/organization/organization.service.ts @@ -6,7 +6,6 @@ import { CreateOrganizationDto } from './dtos/create-organization-dto'; import { BulkSendInvitationDto } from './dtos/send-invitation.dto'; import { UpdateUserRolesDto } from './dtos/update-user-roles.dto'; import { UpdateOrganizationDto } from './dtos/update-organization-dto'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { organisation } from '@prisma/client'; import { IGetOrgById, IGetOrganization } from 'apps/organization/interfaces/organization.interface'; import { IOrgUsers } from 'apps/user/interfaces/user.interface'; @@ -14,6 +13,7 @@ import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganization import { ClientCredentialsDto } from './dtos/client-credentials.dto'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Injectable() export class OrganizationService extends BaseService { @@ -26,8 +26,8 @@ export class OrganizationService extends BaseService { * @param createOrgDto * @returns Organization creation Success */ - async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise { - const payload = { createOrgDto, userId }; + async createOrganization(createOrgDto: CreateOrganizationDto, userId: string, keycloakUserId: string): Promise { + const payload = { createOrgDto, userId, keycloakUserId }; return this.sendNatsMessage(this.serviceProxy, 'create-organization', payload); } @@ -37,8 +37,8 @@ export class OrganizationService extends BaseService { * @param userId * @returns Orgnization client credentials */ - async createOrgCredentials(orgId: string, userId: string): Promise { - const payload = { orgId, userId }; + async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise { + const payload = { orgId, userId, keycloakUserId }; return this.sendNatsMessage(this.serviceProxy, 'create-org-credentials', payload); } @@ -133,8 +133,8 @@ export class OrganizationService extends BaseService { * @returns get organization roles */ - async getOrgRoles(): Promise { - const payload = {}; + async getOrgRoles(orgId: string): Promise { + const payload = {orgId}; return this.sendNatsMessage(this.serviceProxy, 'get-org-roles', payload); } @@ -148,6 +148,11 @@ export class OrganizationService extends BaseService { return this.sendNatsMessage(this.serviceProxy, 'send-invitation', payload); } + async registerOrgsMapUsers(): Promise { + const payload = {}; + return this.sendNatsMessage(this.serviceProxy, 'register-orgs-users-map', payload); + } + /** * * @param updateUserDto diff --git a/apps/api-gateway/src/user/user.controller.ts b/apps/api-gateway/src/user/user.controller.ts index 8e9ee72a4..5e85bbfbc 100644 --- a/apps/api-gateway/src/user/user.controller.ts +++ b/apps/api-gateway/src/user/user.controller.ts @@ -50,6 +50,7 @@ import { OrgRoles } from 'libs/org-roles/enums'; import { AwsService } from '@credebl/aws/aws.service'; import { PaginationDto } from '@credebl/common/dtos/pagination.dto'; import { CreateCertificateDto } from './dto/share-certificate.dto'; +import { UserAccessGuard } from '../authz/guards/user-access-guard'; @UseFilters(CustomExceptionFilter) @Controller('users') @@ -132,7 +133,7 @@ export class UserController { summary: 'Fetch login user details', description: 'Fetch login user details' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async getProfile(@User() reqUser: user, @Res() res: Response): Promise { const userData = await this.userService.getProfile(reqUser.id); @@ -154,7 +155,7 @@ export class UserController { summary: 'Get all platform and ecosystem settings', description: 'Get all platform and ecosystem settings' }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() async getPlatformSettings(@Res() res: Response): Promise { @@ -174,7 +175,7 @@ export class UserController { summary: 'users activity', description: 'Fetch users activity' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'limit', required: true }) async getUserActivities( @@ -201,7 +202,7 @@ export class UserController { summary: 'organization invitations', description: 'Fetch organization invitations' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() @ApiQuery({ name: 'pageNumber', @@ -293,7 +294,7 @@ export class UserController { summary: 'accept/reject organization invitation', description: 'Accept or Reject organization invitations' }) - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) @ApiBearerAuth() async acceptRejectInvitaion( @Body() acceptRejectInvitation: AcceptRejectInvitationDto, @@ -349,7 +350,7 @@ export class UserController { }) @ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto }) @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) async updateUserProfile( @Body() updateUserProfileDto: UpdateUserProfileDto, @User() reqUser: user, @@ -375,7 +376,7 @@ export class UserController { @ApiOperation({ summary: 'Store user password details', description: 'Store user password details' }) @ApiExcludeEndpoint() @ApiBearerAuth() - @UseGuards(AuthGuard('jwt')) + @UseGuards(AuthGuard('jwt'), UserAccessGuard) async addPasskey( @Body() userInfo: AddPasskeyDetailsDto, @@ -403,7 +404,7 @@ export class UserController { summary: 'Update platform and ecosystem settings', description: 'Update platform and ecosystem settings' }) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard, UserAccessGuard) @Roles(OrgRoles.PLATFORM_ADMIN) @ApiBearerAuth() async updatePlatformSettings( diff --git a/apps/organization/dtos/update-invitation.dt.ts b/apps/organization/dtos/update-invitation.dt.ts index 1d80b08d4..21e345858 100644 --- a/apps/organization/dtos/update-invitation.dt.ts +++ b/apps/organization/dtos/update-invitation.dt.ts @@ -5,5 +5,6 @@ export class UpdateInvitationDto { orgId: string; status: Invitation; userId: string; + keycloakUserId: string; email: string; } \ No newline at end of file diff --git a/apps/organization/repositories/organization.repository.ts b/apps/organization/repositories/organization.repository.ts index 741d8bd09..e2464aab6 100644 --- a/apps/organization/repositories/organization.repository.ts +++ b/apps/organization/repositories/organization.repository.ts @@ -492,6 +492,38 @@ export class OrganizationRepository { } } + async getUnregisteredClientOrgs(): Promise { + try { + const recordsWithNullIdpId = await this.prisma.organisation.findMany({ + where: { + idpId: null + }, + include: { + userOrgRoles: { + include: { + user: { + select: { + email: true, + username: true, + id: true, + keycloakUserId: true, + isEmailVerified: true + } + }, + orgRole: true + } + } + } + }); + + return recordsWithNullIdpId; + + } catch (error) { + this.logger.error(`error: ${JSON.stringify(error)}`); + throw error; + } + } + /** * * @param queryObject diff --git a/apps/organization/src/organization.controller.ts b/apps/organization/src/organization.controller.ts index 87b1c45ea..245f4a8e8 100644 --- a/apps/organization/src/organization.controller.ts +++ b/apps/organization/src/organization.controller.ts @@ -8,9 +8,9 @@ import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; import { IGetOrgById, IGetOrganization, IUpdateOrganization, Payload } from '../interfaces/organization.interface'; import { organisation } from '@prisma/client'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; import { IOrgCredentials, IOrganizationInvitations, IOrganization, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Controller() export class OrganizationController { @@ -24,8 +24,8 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'create-organization' }) - async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string }): Promise { - return this.organizationService.createOrganization(payload.createOrgDto, payload.userId); + async createOrganization(@Body() payload: { createOrgDto: CreateOrganizationDto; userId: string, keycloakUserId: string }): Promise { + return this.organizationService.createOrganization(payload.createOrgDto, payload.userId, payload.keycloakUserId); } /** @@ -34,8 +34,8 @@ export class OrganizationController { * @returns organization client credentials */ @MessagePattern({ cmd: 'create-org-credentials' }) - async createOrgCredentials(@Body() payload: { orgId: string; userId: string }): Promise { - return this.organizationService.createOrgCredentials(payload.orgId); + async createOrgCredentials(@Body() payload: { orgId: string; userId: string, keycloakUserId: string }): Promise { + return this.organizationService.createOrgCredentials(payload.orgId, payload.userId, payload.keycloakUserId); } /** @@ -113,8 +113,13 @@ export class OrganizationController { */ @MessagePattern({ cmd: 'get-org-roles' }) - async getOrgRoles(): Promise { - return this.organizationService.getOrgRoles(); + async getOrgRoles(payload: {orgId: string}): Promise { + return this.organizationService.getOrgRoles(payload.orgId); + } + + @MessagePattern({ cmd: 'register-orgs-users-map' }) + async registerOrgsMapUsers(): Promise { + return this.organizationService.registerOrgsMapUsers(); } /** diff --git a/apps/organization/src/organization.service.ts b/apps/organization/src/organization.service.ts index 0d3154c44..b5012d426 100644 --- a/apps/organization/src/organization.service.ts +++ b/apps/organization/src/organization.service.ts @@ -1,6 +1,16 @@ /* eslint-disable prefer-destructuring */ -import { organisation, user } from '@prisma/client'; -import { Injectable, Logger, ConflictException, InternalServerErrorException, HttpException, BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; +// eslint-disable-next-line camelcase +import { org_invitations, organisation, user } from '@prisma/client'; +import { + Injectable, + Logger, + ConflictException, + InternalServerErrorException, + HttpException, + BadRequestException, + ForbiddenException, + UnauthorizedException +} from '@nestjs/common'; import { PrismaService } from '@credebl/prisma-service'; import { CommonService } from '@credebl/common'; import { OrganizationRepository } from '../repositories/organization.repository'; @@ -17,7 +27,7 @@ import { CreateOrganizationDto } from '../dtos/create-organization.dto'; import { BulkSendInvitationDto } from '../dtos/send-invitation.dto'; import { UpdateInvitationDto } from '../dtos/update-invitation.dt'; import { Invitation, OrgAgentType, transition } from '@credebl/enum/enum'; -import { IGetOrgById, IGetOrganization, IUpdateOrganization, IOrgAgent, IClientCredentials, ICreateConnectionUrl } from '../interfaces/organization.interface'; +import { IGetOrgById, IGetOrganization, IUpdateOrganization, IOrgAgent, IClientCredentials, ICreateConnectionUrl, IOrgRole } from '../interfaces/organization.interface'; import { UserActivityService } from '@credebl/user-activity'; import { CommonConstants } from '@credebl/common/common.constant'; import { ClientRegistrationService } from '@credebl/client-registration/client-registration.service'; @@ -25,11 +35,16 @@ import { map } from 'rxjs/operators'; import { Cache } from 'cache-manager'; import { AwsService } from '@credebl/aws'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { IOrgRoles } from 'libs/org-roles/interfaces/org-roles.interface'; -import { IOrgCredentials, IOrganization, IOrganizationInvitations, IOrganizationDashboard } from '@credebl/common/interfaces/organization.interface'; +import { + IOrgCredentials, + IOrganization, + IOrganizationInvitations, + IOrganizationDashboard +} from '@credebl/common/interfaces/organization.interface'; import { ClientCredentialTokenPayloadDto } from '@credebl/client-registration/dtos/client-credential-token-payload.dto'; import { IAccessTokenData } from '@credebl/common/interfaces/interface'; +import { IClientRoles } from '@credebl/client-registration/interfaces/client.interface'; @Injectable() export class OrganizationService { constructor( @@ -44,7 +59,7 @@ export class OrganizationService { private readonly logger: Logger, @Inject(CACHE_MANAGER) private cacheService: Cache, private readonly clientRegistrationService: ClientRegistrationService - ) { } + ) {} /** * @@ -53,9 +68,12 @@ export class OrganizationService { */ // eslint-disable-next-line camelcase - async createOrganization(createOrgDto: CreateOrganizationDto, userId: string): Promise { + async createOrganization( + createOrgDto: CreateOrganizationDto, + userId: string, + keycloakUserId: string + ): Promise { try { - const organizationExist = await this.organizationRepository.checkOrganizationNameExist(createOrgDto.name); if (organizationExist) { @@ -80,7 +98,7 @@ export class OrganizationService { } else { createOrgDto.logo = ''; } - + const organizationDetails = await this.organizationRepository.createOrganization(createOrgDto); // To return selective object data @@ -89,18 +107,47 @@ export class OrganizationService { delete organizationDetails.orgSlug; delete organizationDetails.website; - const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); + try { + const orgCredentials = await this.registerToKeycloak( + organizationDetails.name, + organizationDetails.id, + keycloakUserId, + userId, + false + ); + + const { clientId, idpId } = orgCredentials; + + const updateOrgData = { + clientId, + idpId + }; + + const updatedOrg = await this.organizationRepository.updateOrganizationById( + updateOrgData, + organizationDetails.id + ); + + if (!updatedOrg) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); + } + } catch (error) { + this.logger.error(`Error In creating client : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client'); + } if (createOrgDto.notificationWebhook) { await this.storeOrgWebhookEndpoint(organizationDetails.id, createOrgDto.notificationWebhook); } - await this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, organizationDetails.id); + await this.userActivityService.createActivity( + userId, + organizationDetails.id, + `${organizationDetails.name} organization created`, + 'Get started with inviting users to join organization' + ); - await this.userActivityService.createActivity(userId, organizationDetails.id, `${organizationDetails.name} organization created`, 'Get started with inviting users to join organization'); - return organizationDetails; - } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -108,28 +155,57 @@ export class OrganizationService { } /** - * - * @param orgId + * + * @param orgId * @returns organization client credentials */ - async createOrgCredentials(orgId: string): Promise { + async createOrgCredentials(orgId: string, userId: string, keycloakUserId: string): Promise { try { - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); if (!organizationDetails) { throw new ConflictException(ResponseMessages.organisation.error.orgNotFound); } - const orgCredentials = await this.registerToKeycloak(organizationDetails.name, organizationDetails.id); - - const {clientId, clientSecret, idpId} = orgCredentials; + let updateOrgData = {}; + let generatedClientSecret = ''; - const updateOrgData = { - clientId, - clientSecret: this.maskString(clientSecret), - idpId - }; + if (organizationDetails.idpId) { + const token = await this.clientRegistrationService.getManagementToken(); + + generatedClientSecret = await this.clientRegistrationService.generateClientSecret( + organizationDetails.idpId, + token + ); + + updateOrgData = { + clientSecret: this.maskString(generatedClientSecret) + }; + } else { + + try { + const orgCredentials = await this.registerToKeycloak( + organizationDetails.name, + organizationDetails.id, + keycloakUserId, + userId, + true + ); + + const { clientId, idpId, clientSecret } = orgCredentials; + + generatedClientSecret = clientSecret; + + updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + } catch (error) { + this.logger.error(`Error In creating client : ${JSON.stringify(error)}`); + throw new InternalServerErrorException('Unable to create client'); + } + } const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); @@ -137,8 +213,11 @@ export class OrganizationService { throw new InternalServerErrorException(ResponseMessages.organisation.error.credentialsNotUpdate); } - return orgCredentials; - + return { + idpId: updatedOrg.idpId, + clientId: updatedOrg.clientId, + clientSecret: generatedClientSecret + }; } catch (error) { this.logger.error(`In createOrgCredentials : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -147,46 +226,95 @@ export class OrganizationService { /** * Register the organization to keycloak - * @param orgName - * @param orgId + * @param orgName + * @param orgId * @returns client credentials */ - async registerToKeycloak(orgName: string, orgId: string): Promise { - const token = await this.clientRegistrationService.getManagementToken(); - return this.clientRegistrationService.createClient(orgName, orgId, token); - } + async registerToKeycloak( + orgName: string, + orgId: string, + keycloakUserId: string, + userId: string, + shouldUpdateRole: boolean + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const orgDetails = await this.clientRegistrationService.createClient(orgName, orgId, token); + + const orgRolesList = [OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER]; + + for (const role of orgRolesList) { + await this.clientRegistrationService.createClientRole(orgDetails.idpId, token, role, role); + } + + const ownerRoleClient = await this.clientRegistrationService.getClientSpecificRoles( + orgDetails.idpId, + token, + OrgRoles.OWNER + ); + + const payload = [ + { + id: ownerRoleClient.id, + name: ownerRoleClient.name + } + ]; + + const ownerRoleData = await this.orgRoleService.getRole(OrgRoles.OWNER); + if (!shouldUpdateRole) { + + await Promise.all([ + this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload), + this.userOrgRoleService.createUserOrgRole(userId, ownerRoleData.id, orgId, ownerRoleClient.id) + ]); + + } else { + const roleIdList = [ + { + roleId: ownerRoleData.id, + idpRoleId: ownerRoleClient.id + } + ]; + + await Promise.all([ + this.clientRegistrationService.createUserClientRole(orgDetails.idpId, token, keycloakUserId, payload), + this.userOrgRoleService.deleteOrgRoles(userId, orgId), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIdList) + ]); + } + + return orgDetails; + } async deleteClientCredentials(orgId: string): Promise { - const token = await this.clientRegistrationService.getManagementToken(); + const token = await this.clientRegistrationService.getManagementToken(); - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - if (!organizationDetails) { - throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); - } + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } - try { - await this.clientRegistrationService.deleteClient(organizationDetails.idpId, token); - const updateOrgData = { - clientId: null, - clientSecret: null, - idpId: null - }; - - await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); - - } catch (error) { - throw new InternalServerErrorException('Unable to delete client credentails'); - } + try { + await this.clientRegistrationService.deleteClient(organizationDetails.idpId, token); + const updateOrgData = { + clientId: null, + clientSecret: null, + idpId: null + }; + + await this.organizationRepository.updateOrganizationById(updateOrgData, orgId); + } catch (error) { + throw new InternalServerErrorException('Unable to delete client credentails'); + } - return ResponseMessages.organisation.success.deleteCredentials; + return ResponseMessages.organisation.success.deleteCredentials; } /** * Mask string and display last 5 characters - * @param inputString - * @returns + * @param inputString + * @returns */ maskString(inputString: string): string { if (5 <= inputString.length) { @@ -202,20 +330,20 @@ export class OrganizationService { return inputString; } } - - async isValidBase64 (value: string): Promise { + + async isValidBase64(value: string): Promise { try { if (!value || 'string' !== typeof value) { return false; } - + const base64Regex = /^data:image\/([a-zA-Z]*);base64,([^\"]*)$/; const matches = value.match(base64Regex); return Boolean(matches) && 3 === matches.length; } catch (error) { return false; } - }; + } async uploadFileToS3(orgLogo: string): Promise { try { @@ -234,8 +362,8 @@ export class OrganizationService { this.logger.error(`In getting imageUrl : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } - } - + } + /** * * @param orgName @@ -268,7 +396,7 @@ export class OrganizationService { const orgSlug = await this.createOrgSlug(updateOrgDto.name); updateOrgDto.orgSlug = orgSlug; updateOrgDto.userId = userId; - + if (await this.isValidBase64(updateOrgDto.logo)) { const imageUrl = await this.uploadFileToS3(updateOrgDto.logo); updateOrgDto.logo = imageUrl; @@ -328,9 +456,13 @@ export class OrganizationService { * @returns Get created organizations details */ - async getOrganizations(userId: string, pageNumber: number, pageSize: number, search: string): Promise { + async getOrganizations( + userId: string, + pageNumber: number, + pageSize: number, + search: string + ): Promise { try { - const query = { userOrgRoles: { some: { userId } @@ -345,14 +477,8 @@ export class OrganizationService { userId }; - const getOrgs = await this.organizationRepository.getOrganizations( - query, - filterOptions, - pageNumber, - pageSize - ); + const getOrgs = await this.organizationRepository.getOrganizations(query, filterOptions, pageNumber, pageSize); return getOrgs; - } catch (error) { this.logger.error(`In fetch getOrganizations : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -364,7 +490,6 @@ export class OrganizationService { return this.authenticateClientKeycloak(clientId, clientSecret); } - async authenticateClientKeycloak(clientId: string, clientSecret: string): Promise { try { @@ -396,7 +521,6 @@ export class OrganizationService { async getPublicOrganizations(pageNumber: number, pageSize: number, search: string): Promise { try { - const query = { publicProfile: true, OR: [ @@ -407,13 +531,7 @@ export class OrganizationService { const filterOptions = {}; - return this.organizationRepository.getOrganizations( - query, - filterOptions, - pageNumber, - pageSize - ); - + return this.organizationRepository.getOrganizations(query, filterOptions, pageNumber, pageSize); } catch (error) { this.logger.error(`In fetch getPublicOrganizations : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -423,7 +541,6 @@ export class OrganizationService { async getPublicProfile(payload: { orgSlug: string }): Promise { const { orgSlug } = payload; try { - const query = { orgSlug, publicProfile: true @@ -437,7 +554,6 @@ export class OrganizationService { const credDefs = await this.organizationRepository.getCredDefByOrg(organizationDetails.id); organizationDetails['credential_definitions'] = credDefs; return organizationDetails; - } catch (error) { this.logger.error(`get user: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -452,7 +568,6 @@ export class OrganizationService { async getOrganization(orgId: string): Promise { try { - const query = { id: orgId }; @@ -471,13 +586,23 @@ export class OrganizationService { * @returns Get created invitation details */ - async getInvitationsByOrgId(orgId: string, pageNumber: number, pageSize: number, search: string): Promise { + async getInvitationsByOrgId( + orgId: string, + pageNumber: number, + pageSize: number, + search: string + ): Promise { try { - const getOrganization = await this.organizationRepository.getInvitationsByOrgId(orgId, pageNumber, pageSize, search); + const getOrganization = await this.organizationRepository.getInvitationsByOrgId( + orgId, + pageNumber, + pageSize, + search + ); for await (const item of getOrganization['invitations']) { const getOrgRoles = await this.orgRoleService.getOrgRolesByIds(item['orgRoles']); (item['orgRoles'] as object) = getOrgRoles; - }; + } return getOrganization; } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); @@ -490,10 +615,25 @@ export class OrganizationService { * @returns organization roles */ - - async getOrgRoles(): Promise { + async getOrgRoles(orgId: string): Promise { try { - return this.orgRoleService.getOrgRoles(); + if (!orgId) { + throw new BadRequestException(ResponseMessages.organisation.error.orgIdIsRequired); + } + + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + + if (!organizationDetails.idpId) { + return this.orgRoleService.getOrgRoles(); + } + + const token = await this.clientRegistrationService.getManagementToken(); + + return this.clientRegistrationService.getAllClientRoles(organizationDetails.idpId, token); } catch (error) { this.logger.error(`In getOrgRoles : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -505,12 +645,8 @@ export class OrganizationService { * @param email * @returns */ - async checkInvitationExist( - email: string, - orgId: string - ): Promise { + async checkInvitationExist(email: string, orgId: string): Promise { try { - const query = { email, orgId @@ -541,19 +677,14 @@ export class OrganizationService { } } - /** - * - * @Body sendInvitationDto - * @returns createInvitation - */ - - - async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + async createInvitationByOrgRoles( + bulkInvitationDto: BulkSendInvitationDto, + userEmail: string, + userId: string, + orgName: string + ): Promise { const { invitations, orgId } = bulkInvitationDto; - try { - const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - for (const invitation of invitations) { const { orgRoleId, email } = invitation; @@ -575,14 +706,111 @@ export class OrganizationService { await this.organizationRepository.createSendInvitation(email, String(orgId), String(userId), orgRoleId); try { - await this.sendInviteEmailTemplate(email, organizationDetails.name, orgRolesDetails, firstName, isUserExist); + await this.sendInviteEmailTemplate(email, orgName, orgRolesDetails, firstName, isUserExist); } catch (error) { throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); } } + } + } + + async createInvitationByClientRoles( + bulkInvitationDto: BulkSendInvitationDto, + userEmail: string, + userId: string, + orgName: string, + idpId: string + ): Promise { + const { invitations, orgId } = bulkInvitationDto; + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + const orgRoles = await this.orgRoleService.getOrgRoles(); + + for (const invitation of invitations) { + const { orgRoleId, email } = invitation; + + const isUserExist = await this.checkUserExistInPlatform(email); + + const userData = await this.getUserFirstName(userEmail); + + const { firstName } = userData; + + const matchedRoles = clientRolesList + .filter((role) => orgRoleId.includes(role.id.trim())) + .map((role) => role.name); + + if (orgRoleId.length !== matchedRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); + } + + const filteredOrgRoles = orgRoles.filter((role) => matchedRoles.includes(role.name.trim())); + + const isInvitationExist = await this.checkInvitationExist(email, orgId); + + if (!isInvitationExist && userEmail !== invitation.email) { + + await this.organizationRepository.createSendInvitation( + email, + String(orgId), + String(userId), + filteredOrgRoles.map((role) => role.id) + ); + + try { + await this.sendInviteEmailTemplate( + email, + orgName, + filteredOrgRoles, + firstName, + isUserExist + ); + } catch (error) { + throw new InternalServerErrorException(ResponseMessages.user.error.emailSend); + } + } + } + } + + /** + * + * @Body sendInvitationDto + * @returns createInvitation + */ + + async createInvitation(bulkInvitationDto: BulkSendInvitationDto, userId: string, userEmail: string): Promise { + const { orgId } = bulkInvitationDto; + try { + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); + } + + if (!organizationDetails.idpId) { + await this.createInvitationByOrgRoles( + bulkInvitationDto, + userEmail, + userId, + organizationDetails.name + ); + } else { + await this.createInvitationByClientRoles( + bulkInvitationDto, + userEmail, + userId, + organizationDetails.name, + organizationDetails.idpId + ); } - await this.userActivityService.createActivity(userId, organizationDetails.id, `Invitations sent for ${organizationDetails.name}`, 'Get started with user role management once invitations accepted'); + + await this.userActivityService.createActivity( + userId, + organizationDetails.id, + `Invitations sent for ${organizationDetails.name}`, + 'Get started with user role management once invitations accepted' + ); return ResponseMessages.organisation.success.createInvitation; } catch (error) { this.logger.error(`In send Invitation : ${JSON.stringify(error)}`); @@ -602,7 +830,7 @@ export class OrganizationService { email: string, orgName: string, orgRolesDetails: object[], - firstName:string, + firstName: string, isUserExist: boolean ): Promise { const platformConfigData = await this.prisma.platform_config.findMany(); @@ -613,7 +841,13 @@ export class OrganizationService { emailData.emailTo = email; emailData.emailSubject = `Invitation to join “${orgName}” on CREDEBL`; - emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate(email, orgName, orgRolesDetails, firstName, isUserExist); + emailData.emailHtml = await urlEmailTemplate.sendInviteEmailTemplate( + email, + orgName, + orgRolesDetails, + firstName, + isUserExist + ); //Email is sent to user for the verification through emailData const isEmailSent = await sendEmail(emailData); @@ -648,7 +882,7 @@ export class OrganizationService { const pattern = { cmd: 'get-user-by-mail' }; const payload = { email: userEmail }; - const userData = await this.organizationServiceProxy + const userData = await this.organizationServiceProxy .send(pattern, payload) .toPromise() .catch((error) => { @@ -660,12 +894,38 @@ export class OrganizationService { }, error.status ); - }); - return userData; - } - + }); + return userData; + } + + async getUserUserId(userId: string): Promise { + const pattern = { cmd: 'get-user-by-user-id' }; + // const payload = { id: userId }; + + const userData = await this.organizationServiceProxy + .send(pattern, userId) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.status, + error: error.error, + message: error.message + }, + error.status + ); + }); + return userData; + } - async fetchUserInvitation(email: string, status: string, pageNumber: number, pageSize: number, search = ''): Promise { + async fetchUserInvitation( + email: string, + status: string, + pageNumber: number, + pageSize: number, + search = '' + ): Promise { try { return this.organizationRepository.getAllOrgInvitations(email, status, pageNumber, pageSize, search); } catch (error) { @@ -674,6 +934,49 @@ export class OrganizationService { } } + async updateClientInvitation( + // eslint-disable-next-line camelcase + invitation: org_invitations, + idpId: string, + userId: string, + keycloakUserId: string, + orgId: string, + status: string + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + + const orgRoles = await this.orgRoleService.getOrgRolesByIds(invitation.orgRoles); + + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = orgRoles.map((orgRole: IOrgRole) => { + let roleObj: { roleId: string; name: string; idpRoleId: string} = null; + + for (let index = 0; index < clientRolesList.length; index++) { + if (clientRolesList[index].name === orgRole.name) { + roleObj = { + roleId: orgRole.id, + name: orgRole.name, + idpRoleId: clientRolesList[index].id + }; + break; + } + } + + return roleObj; + }); + + const data = { + status + }; + + await Promise.all([ + this.organizationRepository.updateOrgInvitation(invitation.id, data), + this.clientRegistrationService.createUserClientRole(idpId, token, keycloakUserId, rolesPayload.map(role => ({id: role.idpRoleId, name: role.name}))), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + } + /** * * @param payload @@ -681,10 +984,10 @@ export class OrganizationService { */ async updateOrgInvitation(payload: UpdateInvitationDto): Promise { try { - const { orgId, status, invitationId, userId } = payload; + const { orgId, status, invitationId, userId, keycloakUserId, email } = payload; const invitation = await this.organizationRepository.getInvitationById(String(invitationId)); - if (!invitation) { + if (!invitation || (invitation && invitation.email !== email)) { throw new NotFoundException(ResponseMessages.user.error.invitationNotFound); } @@ -692,32 +995,112 @@ export class OrganizationService { throw new NotFoundException(ResponseMessages.user.error.invalidOrgId); } + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); + + if (!organizationDetails) { + throw new ConflictException(ResponseMessages.organisation.error.orgNotFound); + } + const invitationStatus = invitation.status as Invitation; if (!transition(invitationStatus, payload.status)) { - throw new BadRequestException(`${ResponseMessages.user.error.invitationStatusUpdateInvalid} ${invitation.status}`); + throw new BadRequestException( + `${ResponseMessages.user.error.invitationStatusUpdateInvalid} ${invitation.status}` + ); } const data = { status }; - await this.organizationRepository.updateOrgInvitation(invitationId, data); - if (status === Invitation.REJECTED) { + await this.organizationRepository.updateOrgInvitation(invitationId, data); return ResponseMessages.user.success.invitationReject; } - for (const roleId of invitation.orgRoles) { - await this.userOrgRoleService.createUserOrgRole(userId, roleId, orgId); + + if (organizationDetails.idpId) { + await this.updateClientInvitation(invitation, organizationDetails.idpId, userId, keycloakUserId, orgId, status); + } else { + await this.organizationRepository.updateOrgInvitation(invitationId, data); + + for (const roleId of invitation.orgRoles) { + await this.userOrgRoleService.createUserOrgRole(userId, roleId, orgId); + } } return ResponseMessages.user.success.invitationAccept; - } catch (error) { this.logger.error(`In updateOrgInvitation : ${error}`); throw new RpcException(error.response ? error.response : error); } } + async updateUserClientRoles( + // eslint-disable-next-line camelcase + roleIds: string[], + idpId: string, + userId: string, + orgId: string + ): Promise { + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles( + idpId, + token + ); + const orgRoles = await this.orgRoleService.getOrgRoles(); + + const matchedClientRoles = clientRolesList.filter((role) => roleIds.includes(role.id.trim())); + + if (roleIds.length !== matchedClientRoles.length) { + throw new NotFoundException(ResponseMessages.organisation.error.orgRoleIdNotFound); + } + + const rolesPayload: { roleId: string; name: string; idpRoleId: string }[] = matchedClientRoles.map( + (clientRole: IClientRoles) => { + let roleObj: { roleId: string; name: string; idpRoleId: string } = null; + + for (let index = 0; index < orgRoles.length; index++) { + if (orgRoles[index].name === clientRole.name) { + roleObj = { + roleId: orgRoles[index].id, + name: orgRoles[index].name, + idpRoleId: clientRole.id + }; + break; + } + } + + return roleObj; + } + ); + + const userData = await this.getUserUserId(userId); + + const [, deletedUserRoleRecords] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles( + idpId, + token, + userData.keycloakUserId + ), + this.userOrgRoleService.deleteOrgRoles(userId, orgId) + ]); + + if (0 === deletedUserRoleRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); + } + + const [, isUserRoleUpdated] = await Promise.all([ + this.clientRegistrationService.createUserClientRole( + idpId, + token, + userData.keycloakUserId, + rolesPayload.map((role) => ({ id: role.idpRoleId, name: role.name })) + ), + this.userOrgRoleService.updateUserOrgRole(userId, orgId, rolesPayload) + ]); + + return isUserRoleUpdated; + } + /** * * @param orgId @@ -727,26 +1110,45 @@ export class OrganizationService { */ async updateUserRoles(orgId: string, roleIds: string[], userId: string): Promise { try { - const isUserExistForOrg = await this.userOrgRoleService.checkUserOrgExist(userId, orgId); if (!isUserExistForOrg) { throw new NotFoundException(ResponseMessages.organisation.error.userNotFound); } - const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); + const organizationDetails = await this.organizationRepository.getOrganizationDetails(orgId); - if (isRolesExist && 0 === isRolesExist.length) { - throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound); } - const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); + if (!organizationDetails.idpId) { + const isRolesExist = await this.orgRoleService.getOrgRolesByIds(roleIds); - if (0 === deleteUserRecords['count']) { - throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); - } + if (isRolesExist && 0 === isRolesExist.length) { + throw new NotFoundException(ResponseMessages.organisation.error.rolesNotExist); + } + + const deleteUserRecords = await this.userOrgRoleService.deleteOrgRoles(userId, orgId); + + if (0 === deleteUserRecords['count']) { + throw new InternalServerErrorException(ResponseMessages.organisation.error.updateUserRoles); + } + + for (const role of roleIds) { + this.userOrgRoleService.createUserOrgRole(userId, role, orgId); + } + + return true; + } else { - return this.userOrgRoleService.updateUserOrgRole(userId, orgId, roleIds); + return this.updateUserClientRoles( + roleIds, + organizationDetails.idpId, + userId, + organizationDetails.id + ); + } } catch (error) { this.logger.error(`Error in updateUserRoles: ${JSON.stringify(error)}`); @@ -756,7 +1158,7 @@ export class OrganizationService { async getOrgDashboard(orgId: string): Promise { try { - return this.organizationRepository.getOrgDashboard(orgId); + return this.organizationRepository.getOrgDashboard(orgId); } catch (error) { this.logger.error(`In create organization : ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); @@ -792,7 +1194,6 @@ export class OrganizationService { } } - async getOrgOwner(orgId: string): Promise { try { const orgDetails = await this.organizationRepository.getOrganizationOwnerDetails(orgId, OrgRoles.OWNER); @@ -803,7 +1204,6 @@ export class OrganizationService { } } - async deleteOrganization(orgId: string): Promise { try { const getAgent = await this.organizationRepository.getAgentEndPoint(orgId); @@ -815,7 +1215,6 @@ export class OrganizationService { let url; if (getAgent.orgAgentTypeId === OrgAgentType.DEDICATED) { url = `${getAgent.agentEndPoint}${CommonConstants.URL_DELETE_WALLET}`; - } else if (getAgent.orgAgentTypeId === OrgAgentType.SHARED) { url = `${getAgent.agentEndPoint}${CommonConstants.URL_DELETE_SHARED_WALLET}`.replace('#', getAgent.tenantId); } @@ -827,14 +1226,12 @@ export class OrganizationService { const deleteWallet = await this._deleteWallet(payload); if (deleteWallet) { - const orgDelete = await this.organizationRepository.deleteOrg(orgId); if (false === orgDelete) { throw new NotFoundException(ResponseMessages.organisation.error.deleteOrg); } } - return true; } catch (error) { this.logger.error(`delete organization: ${JSON.stringify(error)}`); @@ -853,18 +1250,20 @@ export class OrganizationService { return this.organizationServiceProxy .send(pattern, payload) .pipe( - map((response) => ( - { + map((response) => ({ response })) - ).toPromise() - .catch(error => { + ) + .toPromise() + .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( { status: error.statusCode, error: error.message - }, error.error); + }, + error.error + ); }); } catch (error) { this.logger.error(`[_deleteWallet] - error in delete wallet : ${JSON.stringify(error)}`); @@ -872,7 +1271,6 @@ export class OrganizationService { } } - async _getOrgAgentApiKey(orgId: string): Promise { const pattern = { cmd: 'get-org-agent-api-key' }; const payload = { orgId }; @@ -883,10 +1281,112 @@ export class OrganizationService { return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ + throw new HttpException( + { status: error.status, error: error.message - }, error.status); + }, + error.status + ); + } + } + + async registerOrgsMapUsers(): Promise { + + try { + + const unregisteredOrgsList = await this.organizationRepository.getUnregisteredClientOrgs(); + + if (!unregisteredOrgsList || 0 === unregisteredOrgsList.length) { + throw new NotFoundException('Unregistered client organizations not found'); + } + + for (const org of unregisteredOrgsList) { + const userOrgRoles = 0 < org['userOrgRoles'].length && org['userOrgRoles']; + + const ownerUserList = 0 < org['userOrgRoles'].length + && userOrgRoles.filter(userOrgRole => userOrgRole.orgRole.name === OrgRoles.OWNER); + + const ownerUser = 0 < ownerUserList.length && ownerUserList[0].user; + + const orgObj = { + id: org.id, + idpId: org.idpId, + name: org.name, + ownerId: ownerUser.id, + ownerEmail: ownerUser.email, + ownerKeycloakId: ownerUser.keycloakUserId + }; + + if (orgObj.ownerKeycloakId) { + const orgCredentials = await this.registerToKeycloak( + orgObj.name, + orgObj.id, + orgObj.ownerKeycloakId, + orgObj.ownerId, + true + ); + + const { clientId, idpId, clientSecret } = orgCredentials; + + const updateOrgData = { + clientId, + clientSecret: this.maskString(clientSecret), + idpId + }; + + const updatedOrg = await this.organizationRepository.updateOrganizationById(updateOrgData, orgObj.id); + + this.logger.log(`updatedOrg::`, updatedOrg); + + const usersToRegisterList = userOrgRoles.filter(userOrgRole => null !== userOrgRole.user.keycloakUserId); + + const token = await this.clientRegistrationService.getManagementToken(); + const clientRolesList = await this.clientRegistrationService.getAllClientRoles(idpId, token); + + const deletedUserDetails: string[] = []; + for (const userRole of usersToRegisterList) { + const user = userRole.user; + + const matchedClientRoles = clientRolesList.filter((role) => userRole.orgRole.name === role.name) + .map(clientRole => ({roleId: userRole.orgRole.id, idpRoleId: clientRole.id, name: clientRole.name})); + + if (!deletedUserDetails.includes(user.id)) { + const [, deletedUserRoleRecords] = await Promise.all([ + this.clientRegistrationService.deleteUserClientRoles(idpId, token, user.keycloakUserId), + this.userOrgRoleService.deleteOrgRoles(user.id, orgObj.id) + ]); + + this.logger.log(`deletedUserRoleRecords::`, deletedUserRoleRecords); + + deletedUserDetails.push(user.id); + } + + + await Promise.all([ + this.clientRegistrationService.createUserClientRole( + idpId, + token, + user.keycloakUserId, + matchedClientRoles.map((role) => ({ id: role.idpRoleId, name: role.name })) + ), + this.userOrgRoleService.updateUserOrgRole( + user.id, + orgObj.id, + matchedClientRoles.map((role) => ({ roleId: role.roleId, idpRoleId: role.idpRoleId })) + ) + ]); + this.logger.log(`Organization client created and users mapped to roles`); + + } + } + } + + return ''; + } catch (error) { + this.logger.error(`Error in registerOrgsMapUsers: ${JSON.stringify(error)}`); + throw new RpcException(error.response ? error.response : error); + } } @@ -931,10 +1431,13 @@ export class OrganizationService { return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); } } -} \ No newline at end of file +} diff --git a/apps/user/interfaces/user.interface.ts b/apps/user/interfaces/user.interface.ts index 7eaa65d52..503370357 100644 --- a/apps/user/interfaces/user.interface.ts +++ b/apps/user/interfaces/user.interface.ts @@ -5,6 +5,7 @@ export interface IUsersProfile { firstName?: string; lastName?: string; supabaseUserId?: string; + keycloakUserId?: string; userOrgRoles?: IUserOrgRole[]; } diff --git a/apps/user/repositories/user.repository.ts b/apps/user/repositories/user.repository.ts index f49d1cc79..95337baab 100644 --- a/apps/user/repositories/user.repository.ts +++ b/apps/user/repositories/user.repository.ts @@ -223,6 +223,7 @@ export class UserRepository { clientId: true, clientSecret: true, supabaseUserId: true, + keycloakUserId: true, userOrgRoles: { include: { orgRole: true, @@ -271,6 +272,7 @@ export class UserRepository { profileImg: true, publicProfile: true, supabaseUserId: true, + keycloakUserId: true, isEmailVerified: true, userOrgRoles: { select:{ diff --git a/apps/user/src/user.controller.ts b/apps/user/src/user.controller.ts index 35574e26e..802b7b150 100644 --- a/apps/user/src/user.controller.ts +++ b/apps/user/src/user.controller.ts @@ -90,6 +90,12 @@ export class UserController { async findUserByEmail(payload: { email }): Promise { return this.userService.findUserByEmail(payload); } + + @MessagePattern({ cmd: 'get-user-by-user-id' }) + async findUserByUserId(id: string): Promise { + return this.userService.findUserByUserId(id); + } + /** * @param credentialId * @returns User credentials diff --git a/apps/user/src/user.service.ts b/apps/user/src/user.service.ts index ef9c036fe..dbbac2214 100644 --- a/apps/user/src/user.service.ts +++ b/apps/user/src/user.service.ts @@ -271,8 +271,21 @@ export class UserService { keycloakDetails.keycloakUserId.toString() ); - const holderRoleData = await this.orgRoleService.getRole(OrgRoles.HOLDER); - await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderRoleData.id); + const realmRoles = await this.clientRegistrationService.getAllRealmRoles(token); + + const holderRole = realmRoles.filter(role => role.name === OrgRoles.HOLDER); + const holderRoleData = 0 < holderRole.length && holderRole[0]; + + const payload = [ + { + id: holderRoleData.id, + name: holderRoleData.name + } + ]; + + await this.clientRegistrationService.createUserHolderRole(token, keycloakDetails.keycloakUserId.toString(), payload); + const holderOrgRole = await this.orgRoleService.getRole(OrgRoles.HOLDER); + await this.userOrgRoleService.createUserOrgRole(userDetails.id, holderOrgRole.id, null, holderRoleData.id); return ResponseMessages.user.success.signUpUser; } catch (error) { @@ -503,6 +516,11 @@ export class UserService { } } + findUserByUserId(id: string): Promise { + return this.userRepository.getUserById(id); + + } + async resetPassword(resetPasswordDto: IUserResetPassword): Promise { const { email, oldPassword, newPassword } = resetPasswordDto; @@ -788,7 +806,7 @@ export class UserService { async acceptRejectInvitations(acceptRejectInvitation: AcceptRejectInvitationDto, userId: string): Promise { try { const userData = await this.userRepository.getUserById(userId); - return this.fetchInvitationsStatus(acceptRejectInvitation, userId, userData.email); + return this.fetchInvitationsStatus(acceptRejectInvitation, userData.keycloakUserId, userData.email, userId); } catch (error) { this.logger.error(`acceptRejectInvitations: ${error}`); throw new RpcException(error.response ? error.response : error); @@ -893,15 +911,16 @@ export class UserService { */ async fetchInvitationsStatus( acceptRejectInvitation: AcceptRejectInvitationDto, - userId: string, - email: string + keycloakUserId: string, + email: string, + userId: string ): Promise { try { const pattern = { cmd: 'update-invitation-status' }; const { orgId, invitationId, status } = acceptRejectInvitation; - const payload = { userId, orgId, invitationId, status, email }; + const payload = { userId, keycloakUserId, orgId, invitationId, status, email }; const invitationsData = await this.userServiceProxy .send(pattern, payload) diff --git a/libs/client-registration/src/client-registration.service.ts b/libs/client-registration/src/client-registration.service.ts index d5fed0c93..ea9fdbb78 100644 --- a/libs/client-registration/src/client-registration.service.ts +++ b/libs/client-registration/src/client-registration.service.ts @@ -18,6 +18,7 @@ import { userTokenPayloadDto } from './dtos/userTokenPayloadDto'; import { KeycloakUserRegistrationDto } from 'apps/user/dtos/keycloak-register.dto'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ResponseService } from '@credebl/response'; +import { IClientRoles } from './interfaces/client.interface'; @Injectable() export class ClientRegistrationService { @@ -186,7 +187,6 @@ export class ClientRegistrationService { } } - async getManagementTokenForMobile() { try { const payload = new ClientCredentialTokenPayloadDto(); @@ -276,6 +276,205 @@ export class ClientRegistrationService { } + async createUserClientRole( + idpId: string, + token: string, + userId: string, + payload: object[] + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId, idpId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createUserClientRolesResponse ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User client role is assigned'; + } + + async deleteUserClientRoles( + idpId: string, + token: string, + userId: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpDelete( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId, idpId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `deleteUserClientRoles ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User client role is deleted'; + } + + async createUserHolderRole( + token: string, + userId: string, + payload: object[] + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientUserRoleURL(realmName, userId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createUserHolderRole ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'User holder role is assigned'; + } + + async getAllClientRoles( + idpId: string, + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const clientRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getAllClientRoles ${JSON.stringify( + clientRolesResponse + )}` + ); + + return clientRolesResponse; + } + + async getClientSpecificRoles( + idpId: string, + token: string, + roleName: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const clientRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId, roleName), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getClientSpecificRoles ${JSON.stringify( + clientRolesResponse + )}` + ); + + return clientRolesResponse; + } + + async getAllRealmRoles( + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const realmRolesResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetRealmRoleURL(realmName), + this.getAuthHeader(token) + ); + + this.logger.debug( + `getAllRealmRoles ${JSON.stringify( + realmRolesResponse + )}` + ); + + return realmRolesResponse; + } + + + async createClientRole( + idpId: string, + token: string, + name: string, + description: string + ): Promise { + + const payload = { + clientRole: true, + name, + description + }; + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientRolesResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientRoleURL(realmName, idpId), + payload, + this.getAuthHeader(token) + ); + + this.logger.debug( + `createClientRolesResponse ${JSON.stringify( + createClientRolesResponse + )}` + ); + + return 'Client role is created'; + + } + + async generateClientSecret( + idpId: string, + token: string + ): Promise { + + const realmName = process.env.KEYCLOAK_REALM; + + const createClientSercretResponse = await this.commonService.httpPost( + await this.keycloakUrlService.GetClientSecretURL(realmName, idpId), + {}, + this.getAuthHeader(token) + ); + + this.logger.debug( + `ClientRegistrationService create realm client secret ${JSON.stringify( + createClientSercretResponse + )}` + ); + + const getClientSercretResponse = await this.commonService.httpGet( + await this.keycloakUrlService.GetClientSecretURL(realmName, idpId), + this.getAuthHeader(token) + ); + this.logger.debug( + `ClientRegistrationService get client secret ${JSON.stringify( + getClientSercretResponse + )}` + ); + this.logger.log(`${getClientSercretResponse.value}`); + const clientSecret = getClientSercretResponse.value; + + return clientSecret; + + } + async createClient( orgName: string, orgId: string, @@ -323,20 +522,7 @@ export class ClientRegistrationService { publicClient: false, frontchannelLogout: false, fullScopeAllowed: false, - nodeReRegistrationTimeout: 0, - defaultClientScopes: [ - 'web-origins', - 'role_list', - 'profile', - 'roles', - 'email' - ], - optionalClientScopes: [ - 'address', - 'phone', - 'offline_access', - 'microprofile-jwt' - ] + nodeReRegistrationTimeout: 0 }; const createClientResponse = await this.commonService.httpPost( diff --git a/libs/client-registration/src/interfaces/client.interface.ts b/libs/client-registration/src/interfaces/client.interface.ts new file mode 100644 index 000000000..59e78a52c --- /dev/null +++ b/libs/client-registration/src/interfaces/client.interface.ts @@ -0,0 +1,8 @@ +export interface IClientRoles { + id: string + name: string + description?: string + composite?: boolean + clientRole?: boolean + containerId?: string +} \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index f574d0612..02c885350 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -40,7 +40,7 @@ export const ResponseMessages = { verifyMail: 'Please verify your email', invalidCredentials: 'Invalid Credentials', registerFido: 'Please complete your fido registration', - invitationNotFound: 'Invitation not found', + invitationNotFound: 'Invitation not found for this user', invitationAlreadyAccepted:'Organization invitation already accepted', invitationAlreadyRejected:'Organization invitation already rejected', invalidInvitationStatus: 'Invalid invitation status', @@ -91,7 +91,7 @@ export const ResponseMessages = { rolesNotExist: 'Provided roles not exists in the platform', orgProfile: 'Organization profile not found', userNotFound: 'User not found for the given organization', - orgRoleIdNotFound:'Provided roles not exists in the platform', + orgRoleIdNotFound:'Provided roles not exists for this organization', updateUserRoles: 'Unable to update user roles', deleteOrg: 'Organization not found', deleteOrgInvitation: 'Organization does not have access to delete this invitation', diff --git a/libs/keycloak-url/src/keycloak-url.service.ts b/libs/keycloak-url/src/keycloak-url.service.ts index cb766e604..d10c9f5f8 100644 --- a/libs/keycloak-url/src/keycloak-url.service.ts +++ b/libs/keycloak-url/src/keycloak-url.service.ts @@ -71,6 +71,48 @@ export class KeycloakUrlService { return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/client-secret`; } + async GetClientRoleURL( + realm: string, + clientid: string, + roleName = '' + ):Promise { + + if ('' === roleName) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/roles`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/clients/${clientid}/roles/${roleName}`; + + } + + async GetRealmRoleURL( + realm: string, + roleName = '' + ):Promise { + + if ('' === roleName) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/roles`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/roles/${roleName}`; + + } + + async GetClientUserRoleURL( + realm: string, + userId: string, + clientId?: string + ):Promise { + + if (clientId) { + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userId}/role-mappings/clients/${clientId}`; + } + + return `${process.env.KEYCLOAK_DOMAIN}admin/realms/${realm}/users/${userId}/role-mappings/realm`; + + } + + async GetClientIdpURL( realm: string, idp: string diff --git a/libs/org-roles/interfaces/org-roles.interface.ts b/libs/org-roles/interfaces/org-roles.interface.ts index b170d93af..ec63e63c1 100644 --- a/libs/org-roles/interfaces/org-roles.interface.ts +++ b/libs/org-roles/interfaces/org-roles.interface.ts @@ -2,8 +2,8 @@ export interface IOrgRoles { id: string; name: string; description: string; - createDateTime: Date; - createdBy: string; - lastChangedDateTime: Date; - lastChangedBy: string; + createDateTime?: Date; + createdBy?: string; + lastChangedDateTime?: Date; + lastChangedBy?: string; } \ No newline at end of file diff --git a/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql b/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql new file mode 100644 index 000000000..4d6fed26f --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20240220121359_user_org_idproleid/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "user_org_roles" ADD COLUMN "idpRoleId" UUID; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 714d724d6..28dcfbf44 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -84,6 +84,7 @@ model user_org_roles { userId String @db.Uuid orgRoleId String @db.Uuid orgId String? @db.Uuid + idpRoleId String? @db.Uuid organisation organisation? @relation(fields: [orgId], references: [id]) orgRole org_roles @relation(fields: [orgRoleId], references: [id]) user user @relation(fields: [userId], references: [id]) @@ -484,4 +485,4 @@ model notification { lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6) lastChangedBy String @default("1") deletedAt DateTime? @db.Timestamp(6) -} \ No newline at end of file +} diff --git a/libs/user-org-roles/repositories/index.ts b/libs/user-org-roles/repositories/index.ts index 618c6d50d..5458796cd 100644 --- a/libs/user-org-roles/repositories/index.ts +++ b/libs/user-org-roles/repositories/index.ts @@ -18,13 +18,14 @@ export class UserOrgRolesRepository { * @returns user details */ // eslint-disable-next-line camelcase - async createUserOrgRole(userId: string, roleId: string, orgId?: string): Promise { + async createUserOrgRole(userId: string, roleId: string, orgId?: string, idpRoleId?: string): Promise { try { const data: { orgRole: { connect: { id: string } }; user: { connect: { id: string } }; organisation?: { connect: { id: string } }; + idpRoleId?: string } = { orgRole: { connect: { id: roleId } }, user: { connect: { id: userId } } @@ -34,6 +35,10 @@ export class UserOrgRolesRepository { data.organisation = { connect: { id: orgId } }; } + if (idpRoleId) { + data.idpRoleId = idpRoleId; + } + const saveResponse = await this.prisma.user_org_roles.create({ data }); diff --git a/libs/user-org-roles/src/user-org-roles.service.ts b/libs/user-org-roles/src/user-org-roles.service.ts index e5677dc19..0b87cd9cf 100644 --- a/libs/user-org-roles/src/user-org-roles.service.ts +++ b/libs/user-org-roles/src/user-org-roles.service.ts @@ -13,8 +13,8 @@ export class UserOrgRolesService { * @returns user details */ // eslint-disable-next-line camelcase - async createUserOrgRole(userId: string, roleId: string, orgId?: string): Promise { - return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId); + async createUserOrgRole(userId: string, roleId: string, orgId?: string, idpRoleId?: string): Promise { + return this.userOrgRoleRepository.createUserOrgRole(userId, roleId, orgId, idpRoleId); } @@ -46,10 +46,14 @@ export class UserOrgRolesService { * @param roleIds * @returns */ - async updateUserOrgRole(userId: string, orgId: string, roleIds: string[]): Promise { + async updateUserOrgRole( + userId: string, + orgId: string, + roleIdList: {roleId: string, idpRoleId: string}[] + ): Promise { - for (const role of roleIds) { - this.userOrgRoleRepository.createUserOrgRole(userId, role, orgId); + for (const roleData of roleIdList) { + this.userOrgRoleRepository.createUserOrgRole(userId, roleData.roleId, orgId, roleData.idpRoleId); } return true;