From f46cfd4f6485bf9b0490e04ac28ae1077a7d5abb Mon Sep 17 00:00:00 2001 From: EnvBsh Date: Tue, 8 Apr 2025 13:47:38 +0200 Subject: [PATCH 1/5] added endpoints for add/select user parent --- api-gateway/src/api/service/profile.ts | 94 ++++++++++++++++++- api-gateway/src/helpers/guardians.ts | 19 ++++ .../validation/schemas/profiles.ts | 5 + auth-service/src/api/auth.interface.ts | 4 + auth-service/src/constants/user.ts | 3 +- auth-service/src/entity/user.ts | 5 + common/src/interfaces/auth.interface.ts | 5 + guardian-service/src/api/profile.service.ts | 82 +++++++++++----- .../src/type/messages/message-api.type.ts | 2 + 9 files changed, 195 insertions(+), 24 deletions(-) diff --git a/api-gateway/src/api/service/profile.ts b/api-gateway/src/api/service/profile.ts index 55d2127c8a..a859f24bd2 100644 --- a/api-gateway/src/api/service/profile.ts +++ b/api-gateway/src/api/service/profile.ts @@ -2,7 +2,7 @@ import { DidDocumentStatus, Permissions, SchemaEntity, TaskAction, TopicType } f import { IAuthUser, PinoLogger, RunFunctionAsync } from '@guardian/common'; import { Body, Controller, Get, HttpCode, HttpException, HttpStatus, Param, Post, Put, Req } from '@nestjs/common'; import { ApiBody, ApiExtraModels, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; -import { CredentialsDTO, DidDocumentDTO, DidDocumentStatusDTO, DidDocumentWithKeyDTO, DidKeyStatusDTO, InternalServerErrorDTO, ProfileDTO, TaskDTO } from '#middlewares'; +import { CredentialsDTO, DidDocumentDTO, DidDocumentStatusDTO, DidDocumentWithKeyDTO, DidKeyStatusDTO, InternalServerErrorDTO, ProfileDTO, TaskDTO, UserDidDTO } from '#middlewares'; import { Auth, AuthUser } from '#auth'; import { CacheService, getCacheKey, Guardians, InternalException, ServiceError, TaskManager, UseCache } from '#helpers'; import {CACHE, PREFIXES} from '#constants'; @@ -164,6 +164,98 @@ export class ProfileApi { await this.cacheService.invalidate(getCacheKey([req.url, ...invalidedCacheTags], user)) } + + /** + * Update user parent + */ + @Put('/parent/select/:username') + @Auth( + //Permissions.PROFILES_USER_UPDATE, + ) + @ApiOperation({ + summary: '', + description: '' + }) + @ApiParam({ + name: 'username', + type: String, + description: 'The name of the user for whom to update the information.', + required: true, + example: 'username' + }) + @ApiBody({ + description: '', + required: true, + type: UserDidDTO + }) + @ApiOkResponse({ + description: 'Updated.', + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO + }) + @ApiExtraModels(UserDidDTO, InternalServerErrorDTO) + @HttpCode(HttpStatus.NO_CONTENT) + async setUserStandartRegistry( + @AuthUser() user: IAuthUser, + @Body() parent: any, + @Req() req + ): Promise { + const { username } = user; + const guardians = new Guardians(); + try { + await guardians.updateUserStandartRegistry(username, parent.did); + } catch (error) { + throw new HttpException(error.message, HttpStatus.UNAUTHORIZED); + } + } + + /** + * Add user standart registry + */ + @Put('/parent/add/:username') + @Auth( + //Permissions.PROFILES_USER_UPDATE, + ) + @ApiOperation({ + summary: '', + description: '' + }) + @ApiParam({ + name: 'username', + type: String, + description: 'The name of the user for whom to update the information.', + required: true, + example: 'username' + }) + @ApiBody({ + description: '', + required: true, + type: UserDidDTO + }) + @ApiOkResponse({ + description: 'Updated.', + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO + }) + @ApiExtraModels(UserDidDTO, InternalServerErrorDTO) + @HttpCode(HttpStatus.NO_CONTENT) + async addUserStandartRegistry( + @AuthUser() user: IAuthUser, + @Body() parent: any, + @Req() req + ): Promise { + const guardians = new Guardians(); + try { + await guardians.addUserStandartRegistry(user.username, parent.did); + } catch (error) { + throw new HttpException(error.message, HttpStatus.UNAUTHORIZED); + } + } + /** * Update user profile (async) */ diff --git a/api-gateway/src/helpers/guardians.ts b/api-gateway/src/helpers/guardians.ts index 6da3f5e027..907ff51a16 100644 --- a/api-gateway/src/helpers/guardians.ts +++ b/api-gateway/src/helpers/guardians.ts @@ -500,6 +500,25 @@ export class Guardians extends NatsService { return await this.sendMessage(MessageAPI.CREATE_USER_PROFILE_COMMON, { username, profile }); } + + /** + * Update standart registry + * @param username + * @param standartRegistryDid + */ + public async updateUserStandartRegistry(username: string, standartRegistryDid: string): Promise { + return await this.sendMessage(MessageAPI.USER_UPDATE_STANDART_REGISTRY, { username, standartRegistryDid }); + } + + /** + * Update standart registry + * @param username + * @param standartRegistryDid + */ + public async addUserStandartRegistry(username: string, standartRegistryDid: string): Promise { + return await this.sendMessage(MessageAPI.USER_ADD_STANDART_REGISTRY, { username, standartRegistryDid }); + } + /** * Async create user * @param username diff --git a/api-gateway/src/middlewares/validation/schemas/profiles.ts b/api-gateway/src/middlewares/validation/schemas/profiles.ts index a8f5b093e9..51f421d919 100644 --- a/api-gateway/src/middlewares/validation/schemas/profiles.ts +++ b/api-gateway/src/middlewares/validation/schemas/profiles.ts @@ -21,6 +21,11 @@ export class DidKeyDTO { key: string; } +export class UserDidDTO { + @ApiProperty({ type: 'string', nullable: false, required: true }) + did: string; +} + export class DidDocumentDTO { @ApiProperty({ type: 'string', nullable: false, required: true }) id: string; diff --git a/auth-service/src/api/auth.interface.ts b/auth-service/src/api/auth.interface.ts index 2d9efe1aab..37d1de8964 100644 --- a/auth-service/src/api/auth.interface.ts +++ b/auth-service/src/api/auth.interface.ts @@ -28,6 +28,10 @@ export interface IAuthUser { * Parent */ parent?: string; + /** + * Parents + */ + parents?: string[]; /** * login expire date */ diff --git a/auth-service/src/constants/user.ts b/auth-service/src/constants/user.ts index fd1e241719..43a4202e7f 100644 --- a/auth-service/src/constants/user.ts +++ b/auth-service/src/constants/user.ts @@ -5,6 +5,7 @@ export const REQUIRED_PROPS = { USER_NAME: 'username', DID: 'did', PARENT: 'parent', + PARENTS: 'parents', HEDERA_ACCOUNT_ID: 'hederaAccountId', ROLE: 'role', POLICY_ROLES: 'policyRoles', @@ -38,4 +39,4 @@ export const DB_REQUIRED_PROPS = [ 'hederaAccountId', 'permissions', 'permissionsGroup' -] \ No newline at end of file +] diff --git a/auth-service/src/entity/user.ts b/auth-service/src/entity/user.ts index d2f33647b5..701af6aecd 100644 --- a/auth-service/src/entity/user.ts +++ b/auth-service/src/entity/user.ts @@ -45,6 +45,11 @@ export class User extends BaseEntity implements IUser { @Property({ nullable: true }) parent?: string; + /** + * Parents user + */ + @Property({ nullable: true }) + parents?: string[]; /** * Wallet token */ diff --git a/common/src/interfaces/auth.interface.ts b/common/src/interfaces/auth.interface.ts index bf0ad200ef..46e50673e0 100644 --- a/common/src/interfaces/auth.interface.ts +++ b/common/src/interfaces/auth.interface.ts @@ -26,6 +26,11 @@ export interface IAuthUser { * Parent user DID */ parent?: string; + + /** + * Parents user list of DID + */ + parents?: string; /** * Hedera account id */ diff --git a/guardian-service/src/api/profile.service.ts b/guardian-service/src/api/profile.service.ts index c3d1f055e2..a1e7ced82e 100644 --- a/guardian-service/src/api/profile.service.ts +++ b/guardian-service/src/api/profile.service.ts @@ -127,6 +127,7 @@ async function setupUserProfile( await users.updateCurrentUser(username, { did, parent: profile.parent, + parents: [profile.parent], hederaAccountId: profile.hederaAccountId, useFireblocksSigning: profile.useFireblocksSigning }); @@ -278,28 +279,20 @@ async function createUserProfile( const dataBaseServer = new DatabaseServer(); - if (parent) { - topicConfig = await TopicConfig.fromObject( - await dataBaseServer.findOne(Topic, { - owner: parent, - type: TopicType.UserTopic - }), true); - } - if (!topicConfig) { - notifier.info('Create user topic'); - logger.info('Create User Topic', ['GUARDIAN_SERVICE']); - const topicHelper = new TopicHelper(hederaAccountId, hederaAccountKey, signOptions); - topicConfig = await topicHelper.create({ - type: TopicType.UserTopic, - name: TopicType.UserTopic, - description: TopicType.UserTopic, - owner: null, - policyId: null, - policyUUID: null - }); - await topicHelper.oneWayLink(topicConfig, globalTopic, user.id.toString()); - newTopic = await dataBaseServer.save(Topic, topicConfig.toObject()); - } + notifier.info('Create user topic'); + logger.info('Create User Topic', ['GUARDIAN_SERVICE']); + const topicHelper = new TopicHelper(hederaAccountId, hederaAccountKey, signOptions); + topicConfig = await topicHelper.create({ + type: TopicType.UserTopic, + name: TopicType.UserTopic, + description: TopicType.UserTopic, + owner: null, + policyId: null, + policyUUID: null + }); + await topicHelper.oneWayLink(topicConfig, globalTopic, user.id.toString()); + newTopic = await dataBaseServer.save(Topic, topicConfig.toObject()); + messageServer.setTopicObject(topicConfig); // ------------------------ // Resolve topic --> @@ -694,6 +687,51 @@ export function profileAPI(logger: PinoLogger) { } }); + ApiResponse(MessageAPI.USER_UPDATE_STANDART_REGISTRY, + async (msg: { username: string, standartRegistryDid: string }) => { + try { + const { username, standartRegistryDid } = msg; + const users = new Users(); + const user = await users.getUser(username); + + if (!user.parents?.length || !user.parents.includes(standartRegistryDid)) { + return new MessageError("The Standard Registry DID is not included in the user's parents", 403); + } + + await users.updateCurrentUser(username, { + parent: standartRegistryDid, + }); + //notifier + } catch (error) { + await logger.error(error, ['GUARDIAN_SERVICE']); + console.error(error); + return new MessageError(error, 500); + } + }); + + ApiResponse(MessageAPI.USER_ADD_STANDART_REGISTRY, + async (msg: { username: string, standartRegistryDid: string }) => { + try { + const { username, standartRegistryDid } = msg; + const users = new Users(); + const user = await users.getUser(username); + + if (user.parents?.includes(standartRegistryDid)) { + return new MessageError("The Standard Registry DID has already been included in the user's parents", 403); + } + + await users.updateCurrentUser(username, { + parents: [...(user.parents || []), standartRegistryDid], + }); + //notifier + } catch (error) { + await logger.error(error, ['GUARDIAN_SERVICE']); + console.error(error); + return new MessageError(error, 500); + } + }); + + ApiResponse(MessageAPI.CREATE_USER_PROFILE_COMMON, async (msg: { username: string, profile: any }) => { try { diff --git a/interfaces/src/type/messages/message-api.type.ts b/interfaces/src/type/messages/message-api.type.ts index e52f849523..5728b43b4a 100644 --- a/interfaces/src/type/messages/message-api.type.ts +++ b/interfaces/src/type/messages/message-api.type.ts @@ -274,6 +274,8 @@ export enum MessageAPI { GET_FORMULA_RELATIONSHIPS = 'GET_FORMULA_RELATIONSHIPS', GET_FORMULAS_DATA = 'GET_FORMULAS_DATA', PUBLISH_FORMULA = 'PUBLISH_FORMULA', + USER_UPDATE_STANDART_REGISTRY = 'USER_UPDATE_STANDART_REGISTRY', + USER_ADD_STANDART_REGISTRY = 'USER_ADD_STANDART_REGISTRY', } /** From ebd098140c2ea11ea1877732c1b9df1f8a5ae7d3 Mon Sep 17 00:00:00 2001 From: EnvBsh Date: Fri, 11 Apr 2025 15:50:44 +0200 Subject: [PATCH 2/5] Manage user permissions using new parentPermissions table; Migration; Messaging for new parent creation --- api-gateway/src/api/service/permissions.ts | 31 ++-- api-gateway/src/helpers/users.ts | 4 +- auth-service/src/api/account-service.ts | 45 +++--- .../src/api/parent-permissions-service.ts | 96 +++++++++++ auth-service/src/api/role-service.ts | 86 +++++++--- auth-service/src/app.ts | 4 + auth-service/src/entity/parent-permissions.ts | 33 ++++ auth-service/src/migrations/v3-2-1.ts | 36 +++++ auth-service/src/utils/user.ts | 19 ++- common/src/hedera-modules/message/index.ts | 3 +- .../hedera-modules/message/message-action.ts | 3 +- .../message/message-body.interface.ts | 21 ++- .../hedera-modules/message/message-server.ts | 4 + .../hedera-modules/message/message-type.ts | 3 +- .../hedera-modules/message/user-message.ts | 149 ++++++++++++++++++ common/src/helpers/users.ts | 8 + guardian-service/src/api/profile.service.ts | 40 ++--- interfaces/src/type/auth-events.ts | 4 +- 18 files changed, 507 insertions(+), 82 deletions(-) create mode 100644 auth-service/src/api/parent-permissions-service.ts create mode 100644 auth-service/src/entity/parent-permissions.ts create mode 100644 auth-service/src/migrations/v3-2-1.ts create mode 100644 common/src/hedera-modules/message/user-message.ts diff --git a/api-gateway/src/api/service/permissions.ts b/api-gateway/src/api/service/permissions.ts index 4f6b3b5414..39f8b650c8 100644 --- a/api-gateway/src/api/service/permissions.ts +++ b/api-gateway/src/api/service/permissions.ts @@ -414,8 +414,8 @@ export class PermissionsApi { role, status, username, - did: { $ne: user.did } }, + currentUsername: user.username, parent: user.parent ? user.parent : user.did, pageIndex, pageSize @@ -468,8 +468,9 @@ export class PermissionsApi { try { const owner = user.parent || user.did; const users = new Users(); - const row = await users.getUserPermissions(username); - if (!row || row.parent !== owner || row.did === user.did) { + const row = await users.getUserPermissions(username, owner); + + if (!row || row.did === user.did) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND); } return row as any; @@ -526,11 +527,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent); } catch (error) { await InternalException(error, this.logger); } - if (!row || row.parent !== user.did || row.did === user.did) { + if (!row || row.did === user.did) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND) } try { @@ -616,11 +618,11 @@ export class PermissionsApi { const owner = user.parent || user.did; let target: any; try { - target = await (new Users()).getUserPermissions(username); + target = await (new Users()).getUserPermissions(username, owner); } catch (error) { await InternalException(error, this.logger); } - if (!target || target.parent !== owner) { + if (!target) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND) } try { @@ -681,11 +683,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent); } catch (error) { await InternalException(error, this.logger); } - if (!row || row.parent !== user.did || row.did === user.did) { + if (!row || row.did === user.did) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND) } try { @@ -746,11 +749,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent); } catch (error) { await InternalException(error, this.logger); } - if (!row || row.parent !== user.parent || row.did === user.did) { + if (!row || row.did === user.did) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND) } try { @@ -806,11 +810,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent); } catch (error) { await InternalException(error, this.logger); } - if (!row || row.parent !== user.parent || row.did === user.did) { + if (!row || row.did === user.did) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND) } try { diff --git a/api-gateway/src/helpers/users.ts b/api-gateway/src/helpers/users.ts index 224974b169..62a2d11cf5 100644 --- a/api-gateway/src/helpers/users.ts +++ b/api-gateway/src/helpers/users.ts @@ -92,8 +92,8 @@ export class Users extends NatsService { * Return user by username * @param username */ - public async getUserPermissions(username: string): Promise { - return await this.sendMessage(AuthEvents.GET_USER_PERMISSIONS, { username }); + public async getUserPermissions(username: string, parent: string): Promise { + return await this.sendMessage(AuthEvents.GET_USER_PERMISSIONS, { username, parent }); } /** diff --git a/auth-service/src/api/account-service.ts b/auth-service/src/api/account-service.ts index 6a65069ae1..a218dcc578 100644 --- a/auth-service/src/api/account-service.ts +++ b/auth-service/src/api/account-service.ts @@ -20,6 +20,7 @@ import { UserRole } from '@guardian/interfaces'; import { UserUtils, UserPassword, PasswordType, UserAccessTokenService, UserProp } from '#utils'; +import { ParentPermissions } from '../entity/parent-permissions.js'; /** * Account service @@ -436,6 +437,7 @@ export class AccountService extends NatsService { this.getMessages(AuthEvents.GET_USER_ACCOUNTS, async (msg: { filters?: any, + currentUsername?: string, parent?: string, pageIndex?: any, pageSize?: any @@ -445,18 +447,8 @@ export class AccountService extends NatsService { return new MessageError('Invalid load users parameter'); } - const { filters, pageIndex, pageSize, parent } = msg; - const otherOptions: any = { - fields: [ - 'username', - 'did', - 'hederaAccountId', - 'role', - 'permissionsGroup', - 'permissions', - 'template' - ] - }; + const { filters, currentUsername, pageIndex, pageSize, parent } = msg; + const otherOptions: any = {}; const _pageSize = parseInt(pageSize, 10); const _pageIndex = parseInt(pageIndex, 10); if (Number.isInteger(_pageSize) && Number.isInteger(_pageIndex)) { @@ -472,15 +464,28 @@ export class AccountService extends NatsService { if (filters.role) { options['permissionsGroup.roleId'] = filters.role; } - if (filters.username) { - options.username = { $regex: '.*' + filters.username + '.*' }; - } - if (filters.did) { - options.did = filters.did; - } + options.username = { + ...(filters.username && { $regex: '.*' + filters.username + '.*' }), + $ne: currentUsername + }; + } + const [items, count] = await new DatabaseServer().findAndCount(ParentPermissions, options, otherOptions); + const resultUsers = []; + for (const item of items) { + const user = await new DatabaseServer().findOne(User, { username: item.username }, { + fields: [ + 'username', + 'did', + 'hederaAccountId', + 'role', + 'permissionsGroup', + 'permissions', + 'template' + ] + }); + resultUsers.push({...user, permissions: item.permissions, permissionsGroup: item.permissionsGroup}); } - const [items, count] = await new DatabaseServer().findAndCount(User, options, otherOptions); - return new MessageResponse({ items, count }); + return new MessageResponse({ items: resultUsers, count }); } catch (error) { await logger.error(error, ['GUARDIAN_SERVICE']); return new MessageError(error); diff --git a/auth-service/src/api/parent-permissions-service.ts b/auth-service/src/api/parent-permissions-service.ts new file mode 100644 index 0000000000..d378938be9 --- /dev/null +++ b/auth-service/src/api/parent-permissions-service.ts @@ -0,0 +1,96 @@ +import { User } from '../entity/user.js'; +import { DatabaseServer, MessageError, MessageResponse, NatsService, PinoLogger, Singleton } from '@guardian/common'; +import { AuthEvents, GenerateUUIDv4, IGetGlobalApplicationKey, IGetKeyMessage, IGetKeyResponse, IGroup, ISetGlobalApplicationKey, ISetKeyMessage, WalletEvents } from '@guardian/interfaces'; +import { IVault } from '../vaults/index.js'; +import { ParentPermissions } from '../entity/parent-permissions.js'; +import { permission } from 'process'; +import { UserProp, UserUtils } from '#utils'; +import { getDefaultRole } from './role-service.js'; + +/** + * Parent permissions service + */ +@Singleton +export class ParentPermissionsService extends NatsService { + + /** + * Message queue name + */ + public messageQueueName = 'parent-permissions-queue'; + + /** + * Reply subject + * @private + */ + public replySubject = 'parent-permissions-queue-reply-' + GenerateUUIDv4(); + + + /** + * Register listeners + */ + registerListeners(logger: PinoLogger): void { + this.getMessages(AuthEvents.ADD_USER_PARENT, + async (msg: { username: string, parent: string }) => { + const { username, parent } = msg; + try { + const entityRepository = new DatabaseServer(); + const user = await UserUtils.getUser({ username }, UserProp.RAW); + + if(user.parents?.includes(parent)) { + throw new Error("The Standard Registry DID is already included in the user's parents"); + } + + if(!user.parents) { + user.parents = []; + } + + user.parents.push(parent); + + await entityRepository.update(User, null, user); + + const defaultRole = await getDefaultRole(parent); + let permissions = []; + let permissionsGroup = []; + if (defaultRole) { + permissionsGroup = [{ + uuid: defaultRole.uuid, + roleId: defaultRole.id, + roleName: defaultRole.name, + owner: parent + }]; + permissions = defaultRole.permissions; + } + + const row = entityRepository.create(ParentPermissions, { + username, + parent, + permissionsGroup, + permissions + }); + await entityRepository.save(ParentPermissions, row); + + return new MessageResponse(user); + } catch (error) { + await logger.error(error, ['AUTH_SERVICE']); + return new MessageError(error); + } + }); + this.getMessages(AuthEvents.UPDATE_USER_PARENT, + async (msg: { username: string, parent: string }) => { + const { username, parent } = msg; + try { + const user = await UserUtils.getUser({ username }, UserProp.RAW); + if(!user.parents?.includes(parent)) { + throw new Error("The Standard Registry DID is not included in the user's parents"); + } + user.parent = parent; + await UserUtils.updateUserPermissions(user); + + return new MessageResponse(user); + } catch (error) { + await logger.error(error, ['AUTH_SERVICE']); + return new MessageError(error); + } + }); + } +} diff --git a/auth-service/src/api/role-service.ts b/auth-service/src/api/role-service.ts index 339826994c..ed4d38b3f3 100644 --- a/auth-service/src/api/role-service.ts +++ b/auth-service/src/api/role-service.ts @@ -3,6 +3,7 @@ import { AuthEvents, GenerateUUIDv4, IGroup, IOwner, PermissionsArray } from '@g import { DynamicRole } from '../entity/dynamic-role.js'; import { User } from '../entity/user.js'; import { UserProp, UserUtils } from '#utils'; +import { ParentPermissions } from '../entity/parent-permissions.js'; const permissionList = PermissionsArray.filter((p) => !p.disabled).map((p) => { return { @@ -407,11 +408,16 @@ export class RoleService extends NatsService { } const { username, userRoles, owner } = msg; - const target = await UserUtils.getUser({ username, parent: owner.creator }, UserProp.RAW); - if (!target) { + const user = await UserUtils.getUser({ username, parents: owner.owner }, UserProp.RAW); + if (!user) { return new MessageError('User does not exist'); } + const target = await entityRepository.findOne(ParentPermissions, { + username: user.username, + parent: owner.owner, + }); + const roleMap = new Map(); const permissions = new Set(); const roles = await entityRepository.find(DynamicRole, { id: { $in: userRoles } }); @@ -447,8 +453,10 @@ export class RoleService extends NatsService { }); } target.permissions = Array.from(permissions); - const result = await entityRepository.update(User, null, target); - return new MessageResponse(UserUtils.updateUserFields(result, UserProp.REQUIRED)); + await entityRepository.update(ParentPermissions, null, target); + await UserUtils.updateUserPermissions(user); + + return new MessageResponse(UserUtils.updateUserFields(user, UserProp.REQUIRED)); } catch (error) { await logger.error(error, ['GUARDIAN_SERVICE']); return new MessageError(error); @@ -468,13 +476,15 @@ export class RoleService extends NatsService { const entityRepository = new DatabaseServer(); const { owner } = msg; - const users = await UserUtils.getUsers({ parent: owner }, UserProp.RAW); + const parentPermissions = await entityRepository.find(ParentPermissions, { + parent: owner, + }); const roleMap = new Map(); - for (const user of users) { + for (const target of parentPermissions) { const permissionsGroup: IGroup[] = []; const permissions = new Set(); - if (user.permissionsGroup) { - for (const group of user.permissionsGroup) { + if (target.permissionsGroup) { + for (const group of target.permissionsGroup) { if (!roleMap.has(group.roleId)) { const row = await entityRepository.findOne(DynamicRole, { id: group.roleId }); roleMap.set(group.roleId, row); @@ -489,10 +499,16 @@ export class RoleService extends NatsService { } } } - user.permissionsGroup = permissionsGroup; - user.permissions = Array.from(permissions); - await entityRepository.update(User, null, user); + target.permissionsGroup = permissionsGroup; + target.permissions = Array.from(permissions); + await entityRepository.update(ParentPermissions, null, target); } + + const users = await UserUtils.getUsers({ parent: owner }, UserProp.RAW); + for (const user of users) { + await UserUtils.updateUserPermissions(user); + } + return new MessageResponse(UserUtils.updateUsersFields(users, UserProp.REQUIRED)); } catch (error) { await logger.error(error, ['GUARDIAN_SERVICE']); @@ -517,17 +533,22 @@ export class RoleService extends NatsService { } const { username, userRoles, owner } = msg; - const user = await UserUtils.getUser({ did: owner.creator }, UserProp.RAW); - const target = await UserUtils.getUser({ username }, UserProp.RAW); + const userSender = await UserUtils.getUser({ did: owner.creator }, UserProp.RAW); + const userReceiver = await UserUtils.getUser({ username }, UserProp.RAW); + + const userPermissions = await entityRepository.findOne(ParentPermissions, { + username: userReceiver.username, + parent: userSender.parent, + }); - if (!user || !target) { + if (!userSender || !userReceiver) { return new MessageError('User does not exist'); } //Old const othersRoles = new Map(); - target.permissionsGroup = target.permissionsGroup || []; - for (const group of target.permissionsGroup) { + userPermissions.permissionsGroup = userPermissions.permissionsGroup || []; + for (const group of userPermissions.permissionsGroup) { if (group.owner !== owner.creator) { const role = await entityRepository.findOne(DynamicRole, { id: group.roleId }); if (role) { @@ -537,7 +558,7 @@ export class RoleService extends NatsService { } //New - const ownRoles = user.permissionsGroup?.map((g) => g.roleId) || []; + const ownRoles = userSender.permissionsGroup?.map((g) => g.roleId) || []; const roles = await entityRepository.find(DynamicRole, { id: { $in: userRoles } }); for (const role of roles) { if (ownRoles.includes(role.id)) { @@ -565,10 +586,12 @@ export class RoleService extends NatsService { } } - target.permissionsGroup = permissionsGroup; - target.permissions = Array.from(permissions); - await entityRepository.update(User, null, target); - return new MessageResponse(UserUtils.updateUserFields(target, UserProp.REQUIRED)); + userPermissions.permissionsGroup = permissionsGroup; + userPermissions.permissions = Array.from(permissions); + await entityRepository.update(ParentPermissions, null, userPermissions); + await UserUtils.updateUserPermissions(userReceiver); + + return new MessageResponse(UserUtils.updateUserFields(userReceiver, UserProp.REQUIRED)); } catch (error) { await logger.error(error, ['GUARDIAN_SERVICE']); return new MessageError(error); @@ -580,9 +603,26 @@ export class RoleService extends NatsService { * @param username - username */ this.getMessages(AuthEvents.GET_USER_PERMISSIONS, async (msg: any) => { - const { username } = msg; + const { username, parent } = msg; try { - const user = await UserUtils.getUser({ username }, UserProp.REQUIRED) + const user = await UserUtils.getUser({ username, parents: parent }, UserProp.REQUIRED); + + if(!user) { + return new MessageResponse(null); + } + + const parentPermissions = await new DatabaseServer().findOne(ParentPermissions, { + username, + parent + }); + + if(!parentPermissions) { + return new MessageResponse(null); + } + + user.permissions = parentPermissions.permissions; + user.permissionsGroup = parentPermissions.permissionsGroup; + return new MessageResponse(user); } catch (error) { await logger.error(error, ['AUTH_SERVICE']); diff --git a/auth-service/src/app.ts b/auth-service/src/app.ts index 401024edcb..e19f1c8169 100644 --- a/auth-service/src/app.ts +++ b/auth-service/src/app.ts @@ -14,6 +14,7 @@ import { MeecoAuthService } from './api/meeco-service.js'; import { ApplicationEnvironment } from './environment.js'; import { RoleService } from './api/role-service.js'; import { DEFAULT_MONGO } from '#constants'; +import { ParentPermissionsService } from './api/parent-permissions-service.js'; Promise.all([ Migration({ @@ -68,6 +69,9 @@ Promise.all([ await new RoleService().setConnection(cn).init(); new RoleService().registerListeners(logger); + await new ParentPermissionsService().setConnection(cn).init(); + new ParentPermissionsService().registerListeners(logger); + const validator = new ValidateConfiguration(); if (parseInt(process.env.MEECO_AUTH_PROVIDER_ACTIVE, 10)) { diff --git a/auth-service/src/entity/parent-permissions.ts b/auth-service/src/entity/parent-permissions.ts new file mode 100644 index 0000000000..39fe16521d --- /dev/null +++ b/auth-service/src/entity/parent-permissions.ts @@ -0,0 +1,33 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { IGroup } from '@guardian/interfaces'; +import { BaseEntity } from '@guardian/common'; + +/** + * ParentPermissions collection + */ +@Entity() +export class ParentPermissions extends BaseEntity { + /** + * User DID + */ + @Property({ nullable: true }) + username: string; + + /** + * Parent user + */ + @Property({ nullable: true }) + parent: string; + + /** + * Group name + */ + @Property({ nullable: true }) + permissionsGroup?: IGroup[]; + + /** + * Permissions + */ + @Property({ nullable: true }) + permissions?: string[]; +} diff --git a/auth-service/src/migrations/v3-2-1.ts b/auth-service/src/migrations/v3-2-1.ts new file mode 100644 index 0000000000..cfcb2778ce --- /dev/null +++ b/auth-service/src/migrations/v3-2-1.ts @@ -0,0 +1,36 @@ +import { DefaultRoles, GenerateUUIDv4, UserRole } from '@guardian/interfaces'; +import { Migration } from '@mikro-orm/migrations-mongodb'; + +/** + * Migration to version 2.25.1 + */ +export class ReleaseMigration extends Migration { + /** + * Up migration + */ + async up(): Promise { + await this.setDefaultRoles(); + } + + /** + * Change document state format + */ + async setDefaultRoles() { + const parentPermissionsCollection = this.getCollection('ParentPermissions'); + const userCollection = this.getCollection('User'); + const users = userCollection.find({ role: UserRole.USER }, { session: this.ctx }); + while (await users.hasNext()) { + const user = await users.next(); + if(user.parent && !user.parents) { + user.parents = [user.parent]; + + await parentPermissionsCollection.insertOne({ + username: user.username, + parent: user.parent, + permissionsGroup: user.permissionsGroup, + permissions: user.permissions + }, { session: this.ctx }); + } + } + } +} diff --git a/auth-service/src/utils/user.ts b/auth-service/src/utils/user.ts index ca7b63a5d0..f6a0cdd1b5 100644 --- a/auth-service/src/utils/user.ts +++ b/auth-service/src/utils/user.ts @@ -11,6 +11,7 @@ import { USER_REQUIRED_PROPS, USER_KEYS_PROPS } from '#constants'; import { User } from '../entity/user.js'; import { DynamicRole } from '../entity/dynamic-role.js'; import { DatabaseServer } from '@guardian/common'; +import { ParentPermissions } from '../entity/parent-permissions.js'; export enum UserProp { RAW = 'RAW', @@ -73,6 +74,22 @@ export class UserUtils { return users.map((user) => UserUtils.updateUserFields(user, prop)); } + public static async updateUserPermissions(user: User): Promise { + const entityRepository = new DatabaseServer(); + const permissionsRow = await entityRepository.findOne(ParentPermissions, { + username: user.username, + parent: user.parent, + }); + + if(permissionsRow) { + user.permissions = permissionsRow.permissions; + user.permissionsGroup = permissionsRow.permissionsGroup; + } + + await new DatabaseServer().update(User, null, user); + return user; + } + public static async createNewUser(user: { username: string, role: UserRole, @@ -137,4 +154,4 @@ export class UserUtils { const users = await new DatabaseServer().find(User, filters); return UserUtils.updateUsersFields(users, prop); } -} \ No newline at end of file +} diff --git a/common/src/hedera-modules/message/index.ts b/common/src/hedera-modules/message/index.ts index 15226ac62c..f26896d9ce 100644 --- a/common/src/hedera-modules/message/index.ts +++ b/common/src/hedera-modules/message/index.ts @@ -23,4 +23,5 @@ export { StatisticMessage } from './statistic-message.js'; export { StatisticAssessmentMessage } from './statistic-assessment-message.js'; export { LabelMessage } from './label-message.js'; export { LabelDocumentMessage } from './label-document-message.js'; -export { FormulaMessage } from './formula-message.js'; \ No newline at end of file +export { FormulaMessage } from './formula-message.js'; +export { UserMessage } from './user-message.js'; diff --git a/common/src/hedera-modules/message/message-action.ts b/common/src/hedera-modules/message/message-action.ts index daadbf8221..912e7dbb8a 100644 --- a/common/src/hedera-modules/message/message-action.ts +++ b/common/src/hedera-modules/message/message-action.ts @@ -40,4 +40,5 @@ export enum MessageAction { PublishPolicyLabel = 'publish-policy-label', CreateLabelDocument = 'create-label-document', PublishFormula = 'publish-formula', -} \ No newline at end of file + AddParent = 'add-parent', +} diff --git a/common/src/hedera-modules/message/message-body.interface.ts b/common/src/hedera-modules/message/message-body.interface.ts index a8a8e02584..dc5dc255b8 100644 --- a/common/src/hedera-modules/message/message-body.interface.ts +++ b/common/src/hedera-modules/message/message-body.interface.ts @@ -299,6 +299,25 @@ export interface RegistrationMessageBody extends MessageBody { attributes: { [x: string]: string } | undefined; } +export interface UserMessageBody extends MessageBody { + /** + * DID + */ + did: string; + /** + * Topic ID + */ + topicId: string; + /** + * Language + */ + lang: string; + /** + * Attributes + */ + attributes: { [x: string]: string } | undefined; +} + /** * Token message body */ @@ -738,4 +757,4 @@ export interface LabelDocumentMessageBody extends MessageBody { * Definition */ definition: string; -} \ No newline at end of file +} diff --git a/common/src/hedera-modules/message/message-server.ts b/common/src/hedera-modules/message/message-server.ts index bdd9a62603..0076343f34 100644 --- a/common/src/hedera-modules/message/message-server.ts +++ b/common/src/hedera-modules/message/message-server.ts @@ -26,6 +26,7 @@ import { UserPermissionsMessage } from './user-permissions-message.js'; import { StatisticMessage } from './statistic-message.js'; import { LabelMessage } from './label-message.js'; import { FormulaMessage } from './formula-message.js'; +import { UserMessage } from './user-message.js'; /** * Message server @@ -319,6 +320,9 @@ export class MessageServer { case MessageType.Formula: message = FormulaMessage.fromMessageObject(json); break; + case MessageType.User: + message = UserMessage.fromMessageObject(json); + break; // Default schemas case 'schema-document': message = SchemaMessage.fromMessageObject(json); diff --git a/common/src/hedera-modules/message/message-type.ts b/common/src/hedera-modules/message/message-type.ts index f6fb25210b..44cbcb28e3 100644 --- a/common/src/hedera-modules/message/message-type.ts +++ b/common/src/hedera-modules/message/message-type.ts @@ -22,5 +22,6 @@ export enum MessageType { UserPermissions = 'User-Permissions', PolicyStatistic = 'Policy-Statistic', PolicyLabel = 'Policy-Label', - Formula = 'Formula' + Formula = 'Formula', + User = 'User' } diff --git a/common/src/hedera-modules/message/user-message.ts b/common/src/hedera-modules/message/user-message.ts new file mode 100644 index 0000000000..351e6c77cf --- /dev/null +++ b/common/src/hedera-modules/message/user-message.ts @@ -0,0 +1,149 @@ +import { Message } from './message.js'; +import { IURL } from './url.interface.js'; +import { MessageAction } from './message-action.js'; +import { MessageType } from './message-type.js'; +import { UserMessageBody } from './message-body.interface.js'; + +/** + * Registration message + */ +export class UserMessage extends Message { + /** + * DID + */ + public did: string; + /** + * Topic ID + */ + declare public topicId: string; + /** + * Language + */ + declare public lang: string; + /** + * Attributes + */ + public attributes: { [x: string]: string } | undefined; + + /** + * Registrant topicId + */ + public registrantTopicId: string; + + constructor(action: MessageAction) { + super(action, MessageType.User); + } + + /** + * Set document + * @param did + * @param topicId + * @param attributes + */ + public setDocument(user: any): void { + this.did = user.did; + this.registrantTopicId = user.topicId; + this.lang = 'en-US'; + this.attributes = {}; + } + + /** + * To message object + */ + public override toMessageObject(): UserMessageBody { + return { + id: this._id, + status: null, + type: this.type, + action: this.action, + lang: this.lang, + did: this.did, + topicId: this.registrantTopicId, + attributes: this.attributes + } + } + + /** + * To documents + */ + public async toDocuments(): Promise { + return []; + } + + /** + * Load documents + * @param documents + */ + public loadDocuments(documents: string[]): UserMessage { + return this; + } + + /** + * From message + * @param message + */ + public static fromMessage(message: string): UserMessage { + if (!message) { + throw new Error('Message Object is empty'); + } + + const json = JSON.parse(message); + return UserMessage.fromMessageObject(json); + } + + /** + * From message object + * @param json + */ + public static fromMessageObject(json: UserMessageBody): UserMessage { + if (!json) { + throw new Error('JSON Object is empty'); + } + + if (json.type !== MessageType.User) { + throw new Error('Invalid message type'); + } + + let message = new UserMessage(json.action); + message = Message._fromMessageObject(message, json); + message._id = json.id; + message._status = json.status; + message.did = json.did; + message.registrantTopicId = json.topicId + message.lang = json.lang; + message.attributes = json.attributes || {}; + return message; + } + + /** + * Validate + */ + public override validate(): boolean { + return true; + } + + /** + * Get URLs + */ + public getUrls(): IURL[] { + return []; + } + + /** + * To JSON + */ + public override toJson(): any { + const result = super.toJson(); + result.did = this.did; + result.registrantTopicId = this.registrantTopicId; + result.attributes = this.attributes; + return result; + } + + /** + * Get User DID + */ + public override getOwner(): string { + return this.did; + } +} diff --git a/common/src/helpers/users.ts b/common/src/helpers/users.ts index 151bd8449c..827c6fcd9f 100644 --- a/common/src/helpers/users.ts +++ b/common/src/helpers/users.ts @@ -241,6 +241,14 @@ export class Users extends NatsService { return await this.sendMessage(AuthEvents.UPDATE_USER_ROLE, { username, userRoles, owner }); } + public async addUserParent(username: string, parent: string) { + return await this.sendMessage(AuthEvents.ADD_USER_PARENT, { username, parent }); + } + + public async updateUserParent(username: string, parent: string) { + return await this.sendMessage(AuthEvents.UPDATE_USER_PARENT, { username, parent }); + } + /** * Get hedera account * @param did diff --git a/guardian-service/src/api/profile.service.ts b/guardian-service/src/api/profile.service.ts index a1e7ced82e..eb879a197b 100644 --- a/guardian-service/src/api/profile.service.ts +++ b/guardian-service/src/api/profile.service.ts @@ -28,6 +28,7 @@ import { VCMessage, Wallet, Workers, + UserMessage } from '@guardian/common'; import { emptyNotifier, initNotifier, INotifier } from '../helpers/notifier.js'; import { RestoreDataFromHedera } from '../helpers/restore-data-from-hedera.js'; @@ -89,7 +90,6 @@ async function getGlobalTopic(): Promise { return null; } } - /** * Set up user profile * @param username @@ -692,16 +692,8 @@ export function profileAPI(logger: PinoLogger) { try { const { username, standartRegistryDid } = msg; const users = new Users(); - const user = await users.getUser(username); - - if (!user.parents?.length || !user.parents.includes(standartRegistryDid)) { - return new MessageError("The Standard Registry DID is not included in the user's parents", 403); - } - - await users.updateCurrentUser(username, { - parent: standartRegistryDid, - }); - //notifier + await users.updateUserParent(username, standartRegistryDid); + return new MessageResponse(username); } catch (error) { await logger.error(error, ['GUARDIAN_SERVICE']); console.error(error); @@ -714,16 +706,28 @@ export function profileAPI(logger: PinoLogger) { try { const { username, standartRegistryDid } = msg; const users = new Users(); - const user = await users.getUser(username); - if (user.parents?.includes(standartRegistryDid)) { - return new MessageError("The Standard Registry DID has already been included in the user's parents", 403); - } + await users.addUserParent(username, standartRegistryDid); + const user = await users.getUser(username); - await users.updateCurrentUser(username, { - parents: [...(user.parents || []), standartRegistryDid], + const row = await new DatabaseServer().findOne(Topic, { + owner: standartRegistryDid, + type: TopicType.UserTopic + }); + const userTopic = await new DatabaseServer().findOne(Topic, { + owner: user.did, + type: TopicType.UserTopic }); - //notifier + const topicConfig = await TopicConfig.fromObject(row, true); + + const root = await users.getHederaAccount(user.did); + const messageServer = new MessageServer(root.hederaAccountId, root.hederaAccountKey, root.signOptions); + messageServer.setTopicObject(topicConfig); + + const message = new UserMessage(MessageAction.AddParent); + message.setDocument({...user, topicId: userTopic.topicId}); + await messageServer.sendMessage(message); + return new MessageResponse(user); } catch (error) { await logger.error(error, ['GUARDIAN_SERVICE']); console.error(error); diff --git a/interfaces/src/type/auth-events.ts b/interfaces/src/type/auth-events.ts index f9ae989a02..faad555146 100644 --- a/interfaces/src/type/auth-events.ts +++ b/interfaces/src/type/auth-events.ts @@ -38,5 +38,7 @@ export enum AuthEvents { SET_DEFAULT_USER_ROLE = 'SET_DEFAULT_USER_ROLE', DELEGATE_USER_ROLE = 'DELEGATE_USER_ROLE', GET_USER_PERMISSIONS = 'GET_USER_PERMISSIONS', - CHANGE_USER_PASSWORD = 'CHANGE_USER_PASSWORD' + CHANGE_USER_PASSWORD = 'CHANGE_USER_PASSWORD', + UPDATE_USER_PARENT = 'UPDATE_USER_PARENT', + ADD_USER_PARENT = 'ADD_USER_PARENT' } From 33b1f8f2faed99c895c0623a9b47831dbc1bcf25 Mon Sep 17 00:00:00 2001 From: EnvBsh Date: Mon, 21 Apr 2025 22:32:33 +0200 Subject: [PATCH 3/5] standard registry switcher UI --- api-gateway/src/api/service/profile.ts | 17 +- api-gateway/src/helpers/guardians.ts | 8 +- .../validation/schemas/profiles.dto.ts | 12 +- common/src/interfaces/auth.interface.ts | 2 +- frontend/src/app/app.module.ts | 19 +- ...andard-registry-parent-card.component.html | 21 + ...andard-registry-parent-card.component.scss | 67 +++ ...standard-registry-parent-card.component.ts | 20 + frontend/src/app/services/profile.service.ts | 8 + ...ve-standard-registry-dialog.component.html | 33 ++ ...ve-standard-registry-dialog.component.scss | 7 + ...tive-standard-registry-dialog.component.ts | 41 ++ ...dd-standard-registry-dialog.component.html | 68 ++++ ...dd-standard-registry-dialog.component.scss | 56 +++ .../add-standard-registry-dialog.component.ts | 57 +++ ...fo-standard-registry-dialog.component.html | 49 +++ ...fo-standard-registry-dialog.component.scss | 75 ++++ ...info-standard-registry-dialog.component.ts | 104 +++++ .../user-profile/user-profile.component.html | 385 +++++++----------- .../user-profile/user-profile.component.scss | 43 ++ .../user-profile/user-profile.component.ts | 64 +++ guardian-service/src/api/profile.service.ts | 57 +-- interfaces/src/interface/user.interface.ts | 4 + .../src/type/messages/message-api.type.ts | 4 +- 24 files changed, 952 insertions(+), 269 deletions(-) create mode 100644 frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.html create mode 100644 frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.scss create mode 100644 frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.ts create mode 100644 frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.html create mode 100644 frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.scss create mode 100644 frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.ts create mode 100644 frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.html create mode 100644 frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.scss create mode 100644 frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.ts create mode 100644 frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.html create mode 100644 frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.scss create mode 100644 frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.ts diff --git a/api-gateway/src/api/service/profile.ts b/api-gateway/src/api/service/profile.ts index a859f24bd2..2d93ac4c47 100644 --- a/api-gateway/src/api/service/profile.ts +++ b/api-gateway/src/api/service/profile.ts @@ -98,6 +98,7 @@ export class ProfileApi { permissions: user.permissions, did: user.did, parent: user.parent, + parents: user.parents, hederaAccountId: user.hederaAccountId, confirmed: !!(didDocument && didDocument.status === DidDocumentStatus.CREATE), failed: !!(didDocument && didDocument.status === DidDocumentStatus.FAILED), @@ -197,7 +198,7 @@ export class ProfileApi { }) @ApiExtraModels(UserDidDTO, InternalServerErrorDTO) @HttpCode(HttpStatus.NO_CONTENT) - async setUserStandartRegistry( + async setUserStandardRegistry( @AuthUser() user: IAuthUser, @Body() parent: any, @Req() req @@ -205,7 +206,11 @@ export class ProfileApi { const { username } = user; const guardians = new Guardians(); try { - await guardians.updateUserStandartRegistry(username, parent.did); + await guardians.updateUserStandardRegistry(username, parent.did); + + const invalidedCacheTags = [`/${PREFIXES.PROFILES}/${user.username}`]; + + await this.cacheService.invalidate(getCacheKey([req.url, ...invalidedCacheTags], user)) } catch (error) { throw new HttpException(error.message, HttpStatus.UNAUTHORIZED); } @@ -243,14 +248,18 @@ export class ProfileApi { }) @ApiExtraModels(UserDidDTO, InternalServerErrorDTO) @HttpCode(HttpStatus.NO_CONTENT) - async addUserStandartRegistry( + async addUserStandardRegistry( @AuthUser() user: IAuthUser, @Body() parent: any, @Req() req ): Promise { const guardians = new Guardians(); try { - await guardians.addUserStandartRegistry(user.username, parent.did); + await guardians.addUserStandardRegistry(user.username, parent.did); + + const invalidedCacheTags = [`/${PREFIXES.PROFILES}/${user.username}`]; + + await this.cacheService.invalidate(getCacheKey([req.url, ...invalidedCacheTags], user)) } catch (error) { throw new HttpException(error.message, HttpStatus.UNAUTHORIZED); } diff --git a/api-gateway/src/helpers/guardians.ts b/api-gateway/src/helpers/guardians.ts index 907ff51a16..d312e277b3 100644 --- a/api-gateway/src/helpers/guardians.ts +++ b/api-gateway/src/helpers/guardians.ts @@ -506,8 +506,8 @@ export class Guardians extends NatsService { * @param username * @param standartRegistryDid */ - public async updateUserStandartRegistry(username: string, standartRegistryDid: string): Promise { - return await this.sendMessage(MessageAPI.USER_UPDATE_STANDART_REGISTRY, { username, standartRegistryDid }); + public async updateUserStandardRegistry(username: string, standardRegistryDid: string): Promise { + return await this.sendMessage(MessageAPI.USER_UPDATE_STANDARD_REGISTRY, { username, standardRegistryDid }); } /** @@ -515,8 +515,8 @@ export class Guardians extends NatsService { * @param username * @param standartRegistryDid */ - public async addUserStandartRegistry(username: string, standartRegistryDid: string): Promise { - return await this.sendMessage(MessageAPI.USER_ADD_STANDART_REGISTRY, { username, standartRegistryDid }); + public async addUserStandardRegistry(username: string, standardRegistryDids: string[]): Promise { + return await this.sendMessage(MessageAPI.USER_ADD_STANDARD_REGISTRY, { username, standardRegistryDids }); } /** diff --git a/api-gateway/src/middlewares/validation/schemas/profiles.dto.ts b/api-gateway/src/middlewares/validation/schemas/profiles.dto.ts index 3d75e4b930..92fce72061 100644 --- a/api-gateway/src/middlewares/validation/schemas/profiles.dto.ts +++ b/api-gateway/src/middlewares/validation/schemas/profiles.dto.ts @@ -53,12 +53,22 @@ export class UserDTO implements IUser { @ApiProperty({ type: 'string', required: false, - example: Examples.DID + example: [Examples.DID] }) @IsOptional() @IsString() parent?: string; + @ApiProperty({ + type: 'string', + required: false, + isArray: true, + example: Examples.DID + }) + @IsOptional() + @IsString() + parents?: string[]; + @ApiProperty({ type: 'string', required: false, diff --git a/common/src/interfaces/auth.interface.ts b/common/src/interfaces/auth.interface.ts index 46e50673e0..da17b64019 100644 --- a/common/src/interfaces/auth.interface.ts +++ b/common/src/interfaces/auth.interface.ts @@ -30,7 +30,7 @@ export interface IAuthUser { /** * Parents user list of DID */ - parents?: string; + parents?: string[]; /** * Hedera account id */ diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 831a0a91a7..b427b91e60 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -120,6 +120,8 @@ import { ForgotPasswordDialogComponent } from './views/login/forgot-password-dia import { MultiSelectModule } from 'primeng/multiselect'; import { RadioButtonModule } from 'primeng/radiobutton'; import { CalendarModule } from 'primeng/calendar'; +import { CardModule } from 'primeng/card'; +import { ChipModule } from 'primeng/chip'; import { InputTextareaModule } from 'primeng/inputtextarea'; import { ContractEngineModule } from './modules/contract-engine/contract-engine.module'; import { ProjectComparisonService } from './services/project-comparison.service'; @@ -130,6 +132,11 @@ import '../prototypes/date-prototype'; import { OnlyForDemoDirective } from './directives/onlyfordemo.directive'; import { UseWithServiceDirective } from './directives/use-with-service.directive'; import { WorkerTasksComponent } from './views/worker-tasks/worker-tasks.component'; +import { AddStandardRegistryDialogComponent } from './views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component'; +import { StandardRegistryParentCardComponent } from './components/standard-registry-parent-card/standard-registry-parent-card.component'; +import { InfoStandardRegistryDialogComponent } from './views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component'; +import { InputSwitchModule } from 'primeng/inputswitch'; +import { ActiveStandardRegistryDialogComponent } from './views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component'; @NgModule({ declarations: [ @@ -155,6 +162,7 @@ import { WorkerTasksComponent } from './views/worker-tasks/worker-tasks.componen BrandingComponent, SuggestionsConfigurationComponent, StandardRegistryCardComponent, + StandardRegistryParentCardComponent, NotificationComponent, NotificationsComponent, QrCodeDialogComponent, @@ -175,7 +183,10 @@ import { WorkerTasksComponent } from './views/worker-tasks/worker-tasks.componen RolesViewComponent, UsersManagementComponent, UsersManagementDetailComponent, - WorkerTasksComponent + WorkerTasksComponent, + AddStandardRegistryDialogComponent, + InfoStandardRegistryDialogComponent, + ActiveStandardRegistryDialogComponent ], exports: [], bootstrap: [AppComponent], @@ -213,12 +224,16 @@ import { WorkerTasksComponent } from './views/worker-tasks/worker-tasks.componen MultiSelectModule, RadioButtonModule, CalendarModule, + CardModule, + ChipModule, InputTextareaModule, ContractEngineModule, ProjectComparisonModule, DndModule, CheckboxModule, - AngularSvgIconModule.forRoot()], + InputSwitchModule, + AngularSvgIconModule.forRoot(), + ], providers: [ WebSocketService, AuthService, diff --git a/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.html b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.html new file mode 100644 index 0000000000..680fcaae58 --- /dev/null +++ b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.html @@ -0,0 +1,21 @@ + + + +
+
+
{{registry.username}}
+
+ {{registry.policies.length}} policies + +
+
+ +
+
+ + +
+
diff --git a/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.scss b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.scss new file mode 100644 index 0000000000..1f7b7fbe51 --- /dev/null +++ b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.scss @@ -0,0 +1,67 @@ + +.sr-card { + ::ng-deep .p-card { + border-radius: 8px; + border: 1px solid var(--color-grey-3); + box-shadow: none; + } + + ::ng-deep .p-card-content { + flex-direction: column; + display: flex; + gap: 16px; + } + + ::ng-deep .p-chip { + cursor: pointer; + border-radius: 6px; + font-size: 14px; + } + + ::ng-deep p-chip.active .p-chip { + color: var(--color-accent-green-1); + background-color: var(--color-accent-green-2); + } + + ::ng-deep p-chip.inactive .p-chip { + color: var(--color-grey-6); + background-color: var(--color-grey-2); + } + + .sr-main-info { + font-size: 12px; + font-weight: 500; + display: flex; + justify-content: space-between; + margin-bottom: 8px; + } + + .sr-account-id { + font-family: Poppins; + font-weight: 600; + font-size: 16px; + line-height: 20px; + } + + .sr-action { + ::ng-deep .p-button { + color: var(--color-primary); + font-size: 14px; + font-weight: 500; + background-color: #ffffff; + border: 1px solid var(--color-primary); + border-radius: 6px; + } + } + + .sr-policy-count { + display: flex; + align-items: center; + gap: 8px; + + i { + position: relative; + top: 2px; + } + } +} diff --git a/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.ts b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.ts new file mode 100644 index 0000000000..b0260fb11c --- /dev/null +++ b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.ts @@ -0,0 +1,20 @@ +import {Component, EventEmitter, Input, Output, SimpleChanges, ViewChild} from '@angular/core'; +import {IStandardRegistryResponse} from '@guardian/interfaces'; + +@Component({ + selector: 'app-standard-registry-parent-card', + templateUrl: './standard-registry-parent-card.component.html', + styleUrls: ['./standard-registry-parent-card.component.scss'], +}) +export class StandardRegistryParentCardComponent { + @Input() registry!: IStandardRegistryResponse; + @Input() active: boolean; + @Output() registrySelected: EventEmitter = new EventEmitter(); + + constructor() { + } + + onMoreInfoClick(): void { + this.registrySelected.emit(this.registry.did); + } +} diff --git a/frontend/src/app/services/profile.service.ts b/frontend/src/app/services/profile.service.ts index 828e43a6db..6fb533558a 100644 --- a/frontend/src/app/services/profile.service.ts +++ b/frontend/src/app/services/profile.service.ts @@ -48,4 +48,12 @@ export class ProfileService { public validateDIDKeys(document: any, keys: any): Observable { return this.http.post(`${this.url}/did-keys/validate`, { document, keys }); } + + public addStandartRegistriesAsParent(standardRegistryDids: string[]): Observable { + return this.http.put(`${this.url}/parent/add/${encodeURIComponent(this.auth.getUsername())}`, { did: standardRegistryDids }); + } + + public selectActiveStandartRegistry(standardRegistryDids: string): Observable { + return this.http.put(`${this.url}/parent/select/${encodeURIComponent(this.auth.getUsername())}`, { did: standardRegistryDids }); + } } diff --git a/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.html b/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.html new file mode 100644 index 0000000000..c3fc59e791 --- /dev/null +++ b/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.html @@ -0,0 +1,33 @@ +
+
+
{{title}}
+
+
+ +
+
+
+
+
Please confirm that you want to activate SR {{currentActive}}
+
This will deactivate the current active SR {{potentialActive}}
+
+
+ diff --git a/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.scss b/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.scss new file mode 100644 index 0000000000..e8169054b8 --- /dev/null +++ b/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.scss @@ -0,0 +1,7 @@ +.active-standart-registry { + margin-bottom: 32px; +} + +.active-sr-btn { + margin-left: 16px; +} diff --git a/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.ts b/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.ts new file mode 100644 index 0000000000..8ce4e3185f --- /dev/null +++ b/frontend/src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit } from '@angular/core'; +import { IStandardRegistryResponse } from '@guardian/interfaces'; +import { DialogService, DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ProfileService } from 'src/app/services/profile.service'; + +@Component({ + selector: 'active-standard-registry-dialog', + templateUrl: './active-standard-registry-dialog.component.html', + styleUrls: ['./active-standard-registry-dialog.component.scss'] +}) +export class ActiveStandardRegistryDialogComponent implements OnInit { + public currentActive: string + public potentialActive: string + public title: string = '' + + constructor(private dialogRef: DynamicDialogRef, + private dialogConfig: DynamicDialogConfig, + private profileService: ProfileService, + public dialogService: DialogService) { + } + + ngOnInit(): void { + const { + title, + currentActive, + potentialActive + } = this.dialogConfig.data; + + this.title = title; + this.currentActive = currentActive; + this.potentialActive = potentialActive; + } + + onClose() { + this.dialogRef.close({ update: false }); + } + + onSubmit() { + this.dialogRef.close({ update: true }); + } +} diff --git a/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.html b/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.html new file mode 100644 index 0000000000..83212895b2 --- /dev/null +++ b/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.html @@ -0,0 +1,68 @@ +
+
+
{{title}}
+
+
+ +
+
+
+
+
+
+
+
+ + + + + Select standard registry + + + +
+ {{ item.username }} +
+
+
+ +
{{ selectedItems.length }} Selected
+
+
+
+
+
+

Selected Standard Registries

+

{{sr.username}} + {{ sr.policies.length ? sr.policies.length + ' policies' : 'No policies' }} +

+
+
+
+ diff --git a/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.scss b/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.scss new file mode 100644 index 0000000000..31ee4ee5bf --- /dev/null +++ b/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.scss @@ -0,0 +1,56 @@ +.dialog-body { + pointer-events: auto; + user-select: text; + height: calc(100% - 150px); + overflow: hidden; + + ::ng-deep .p-multiselect { + border-radius: 8px; + width: 100% !important; + } + + ::ng-deep .p-multiselect.p-focus { + box-shadow: none !important; + border-color: inherit !important; + } + + .selected-sr { + font-size: 14px; + color: var(--color-grey-black-2); + } + + .dd-label { + font-weight: 500; + font-size: 12px; + color: var(--color-grey-black-1); + margin-bottom: 6px; + display: inline-block; + } +} + +.add-sr-btn { + margin-left: 16px; +} + +.policies-count { + color: var(--color-grey-6); + font-weight: 400; + font-size: 14px; + margin-left: 10px; +} + +.loading { + background: #fff; + opacity: 0.3; + position: absolute; + z-index: 99; + top: 0; + left: 0; + bottom: 0; + right: 0; + display: flex; + align-items: center; + justify-items: center; + justify-content: center; + align-content: center; +} diff --git a/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.ts b/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.ts new file mode 100644 index 0000000000..f3105c5537 --- /dev/null +++ b/frontend/src/app/views/user-profile/add-standard-registry-dialog/add-standard-registry-dialog.component.ts @@ -0,0 +1,57 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { IStandardRegistryResponse } from '@guardian/interfaces'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ProfileService } from 'src/app/services/profile.service'; + +@Component({ + selector: 'add-standard-registry-dialog', + templateUrl: './add-standard-registry-dialog.component.html', + styleUrls: ['./add-standard-registry-dialog.component.scss'] +}) +export class AddStandardRegistryDialogComponent implements OnInit { + public standardRegistries: IStandardRegistryResponse[] + public title: string = '' + public loading: boolean = false + selectedStandardRegistryDids: string[] = [] + forgotPasswordFormGroup: UntypedFormGroup = new UntypedFormGroup({ + username: new UntypedFormControl(''), + }); + + constructor(private dialogRef: DynamicDialogRef, private dialogConfig: DynamicDialogConfig, private profileService: ProfileService) { + } + + get selectedStandardRegistryObj() { + return this.standardRegistries.filter(sr => this.selectedStandardRegistryDids.includes(sr.did)); + } + + getFormattedSrs() { + return this.standardRegistries.map((sr) => { + return { + label: sr.username, + value: sr.did + } + }) + } + + ngOnInit(): void { + const { + title, + standardRegistries + } = this.dialogConfig.data; + + this.title = title; + this.standardRegistries = standardRegistries; + } + + onClose() { + this.dialogRef.close({ update: false}); + } + + onAddStandardRegistries() { + this.loading = true; + this.profileService.addStandartRegistriesAsParent(this.selectedStandardRegistryDids).subscribe(() => { + this.dialogRef.close({ update: true}); + }); + } +} diff --git a/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.html b/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.html new file mode 100644 index 0000000000..04976961c6 --- /dev/null +++ b/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.html @@ -0,0 +1,49 @@ +
+
+
{{title}}
+
+
+ +
+
+
+
+
+
+ {{standardRegistry.username}} +
+

{{standardRegistry.hederaAccountId}}

+
+
+
Set as Active
+
+ +
+
+
+
+ Standard registry DID +
+
+ {{standardRegistry.did}} +
+
+
+
+
{{ field.name }}
+
{{ field.value }}
+
+
+
+
+ diff --git a/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.scss b/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.scss new file mode 100644 index 0000000000..c89edff9ac --- /dev/null +++ b/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.scss @@ -0,0 +1,75 @@ +.dialog-body { + height: calc(100% - 150px); +} + +.dialog-header { + padding-bottom: 32px; +} + +.info-standart-registry { + .content { + margin-bottom: 64px; + } + + ::ng-deep .p-inputswitch.p-inputswitch-checked .p-inputswitch-slider { + background-color: var(--color-primary); + } + ::ng-deep p-inputswitch { + height: 24px; + } + ::ng-deep .p-inputswitch-slider { + height: 24px; + box-shadow: none !important; + } + + h3 { + margin: 0; + font-weight: 600; + font-size: 16px; + line-height: 20px; + + font-family: Poppins; + letter-spacing: 0%; + + } + .content { + display: flex; + flex-direction: column; + gap: 24px; + } + + .default-label { + font-weight: 500; + margin-bottom: 8px; + font-weight: 500; + font-size: 12px; + line-height: 14px; + } + + .registry-row.double { + display: flex; + gap: 16px; + + .registry-row__label { + width: 164px; + } + } + + .registry-row__label { + font-family: Inter; + text-transform: uppercase; + font-weight: 500; + font-size: 12px; + line-height: 14px; + color: var(--color-grey-6); + margin-bottom: 8px; + } + .registry-row__value { + font-family: Inter; + color: var(--color-grey-black-2); + font-weight: 400; + font-size: 12px; + line-height: 14px; + + } +} diff --git a/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.ts b/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.ts new file mode 100644 index 0000000000..e723b4464d --- /dev/null +++ b/frontend/src/app/views/user-profile/info-standard-registry-dialog/info-standard-registry-dialog.component.ts @@ -0,0 +1,104 @@ +import { Component, OnInit, SimpleChanges } from '@angular/core'; +import { IStandardRegistryResponse } from '@guardian/interfaces'; +import { DialogService, DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ProfileService } from 'src/app/services/profile.service'; +import { ActiveStandardRegistryDialogComponent } from '../active-standard-registry-dialog/active-standard-registry-dialog.component'; + +@Component({ + selector: 'info-standard-registry-dialog', + templateUrl: './info-standard-registry-dialog.component.html', + styleUrls: ['./info-standard-registry-dialog.component.scss'] +}) +export class InfoStandardRegistryDialogComponent implements OnInit { + public standardRegistry: IStandardRegistryResponse + public activeSr: IStandardRegistryResponse + public title: string = '' + public isActive: boolean = false + public disabledSwitcher: boolean = false + + private ignoreFields: string[] = ['@context', 'id', 'type']; + public groupedFields: { name: string; value: any }[][]; + + constructor(private dialogRef: DynamicDialogRef, + private dialogConfig: DynamicDialogConfig, + private profileService: ProfileService, + public dialogService: DialogService) { + } + + ngOnInit(): void { + const { + title, + standardRegistry, + activeSr + } = this.dialogConfig.data; + + this.title = title; + this.activeSr = activeSr; + this.standardRegistry = standardRegistry; + + this.isActive = standardRegistry.did === activeSr.did; + this.disabledSwitcher = this.isActive; + + const fields = []; + if (this.standardRegistry?.vcDocument?.document) { + let cs: any = this.standardRegistry.vcDocument.document.credentialSubject; + if (Array.isArray(cs)) { + cs = cs[0]; + } + + if (cs) { + for (const [name, value] of Object.entries(cs)) { + if ( + !this.ignoreFields.includes(name) && + value && + typeof value !== 'function' && + typeof value !== 'object' + ) { + fields.push({name, value}); + } + } + + this.groupedFields = this.chunk(fields, 2); + } + } + } + + onClose() { + this.dialogRef.close({ update: false }); + } + + onToggleChange() { + this.dialogService.open(ActiveStandardRegistryDialogComponent, { + styleClass: 'guardian-dialog', + width: '640px', + modal: true, + showHeader: false, + data: { + title: 'Make active', + currentActive: this.activeSr.hederaAccountId, + potentialActive: this.standardRegistry.hederaAccountId + } + }).onClose.subscribe((data) => { + if(data?.update) { + this.onChangeActiveSr(); + } else { + this.isActive = !this.isActive; + } + }); + } + + onChangeActiveSr() { + this.profileService.selectActiveStandartRegistry(this.standardRegistry.did).subscribe(() => { + this.dialogRef.close({ update: true, parent: this.standardRegistry.did }); + }); + } + + chunk(arr: T[], size: number): T[][] { + const result: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; + } + +} diff --git a/frontend/src/app/views/user-profile/user-profile.component.html b/frontend/src/app/views/user-profile/user-profile.component.html index 1117d00524..06bfaf6271 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.html +++ b/frontend/src/app/views/user-profile/user-profile.component.html @@ -3,86 +3,122 @@
- +

Profile

-