diff --git a/api-gateway/src/api/service/permissions.ts b/api-gateway/src/api/service/permissions.ts index 8a55650ba9..bfbd88da4c 100644 --- a/api-gateway/src/api/service/permissions.ts +++ b/api-gateway/src/api/service/permissions.ts @@ -416,8 +416,8 @@ export class PermissionsApi { role, status, username, - did: { $ne: user.did } }, + currentUsername: user.username, parent: user.parent ? user.parent : user.did, pageIndex, pageSize @@ -470,8 +470,10 @@ export class PermissionsApi { try { const owner = user.parent || user.did; const users = new Users(); - const row = await users.getUserPermissions(username, user.id); - if (!row || row.parent !== owner || row.did === user.did) { + + const row = await users.getUserPermissions(username, owner, user.id); + + if (!row || row.did === user.did) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND); } return row as any; @@ -528,11 +530,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username, user.id); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent, user.id); } catch (error) { await InternalException(error, this.logger, user.id); } - 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 { @@ -618,11 +621,11 @@ export class PermissionsApi { const owner = user.parent || user.did; let target: any; try { - target = await (new Users()).getUserPermissions(username, user.id); + target = await (new Users()).getUserPermissions(username, owner, user.id); } catch (error) { await InternalException(error, this.logger, user.id); } - if (!target || target.parent !== owner) { + if (!target) { throw new HttpException('User does not exist.', HttpStatus.NOT_FOUND) } try { @@ -684,11 +687,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username, user.id); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent, user.id); } catch (error) { await InternalException(error, this.logger, user.id); } - 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 { @@ -750,11 +754,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username, user.id); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent, user.id); } catch (error) { await InternalException(error, this.logger, user.id); } - 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 { @@ -810,11 +815,12 @@ export class PermissionsApi { let row: any; const users = new Users(); try { - row = await users.getUserPermissions(username, user.id); + const parent = user.parent || user.did; + row = await users.getUserPermissions(username, parent, user.id); } catch (error) { await InternalException(error, this.logger, user.id); } - 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/api/service/profile.ts b/api-gateway/src/api/service/profile.ts index bd9c0ead35..dade3182cb 100644 --- a/api-gateway/src/api/service/profile.ts +++ b/api-gateway/src/api/service/profile.ts @@ -2,7 +2,7 @@ import { Permissions, TaskAction } from '@guardian/interfaces'; import { IAuthUser, PinoLogger, RunFunctionAsync } from '@guardian/common'; import { Body, Controller, Get, HttpCode, HttpException, HttpStatus, Param, Post, Put, Req, Response, Query, Delete } from '@nestjs/common'; import { ApiBody, ApiExtraModels, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { CredentialsDTO, DidDocumentDTO, DidDocumentStatusDTO, DidDocumentWithKeyDTO, DidKeyStatusDTO, Examples, InternalServerErrorDTO, PolicyKeyConfigDTO, PolicyKeyDTO, ProfileDTO, TaskDTO, pageHeader } from '#middlewares'; +import { CredentialsDTO, DidDocumentDTO, DidDocumentStatusDTO, DidDocumentWithKeyDTO, DidKeyStatusDTO, Examples, InternalServerErrorDTO, PolicyKeyConfigDTO, PolicyKeyDTO, ProfileDTO, TaskDTO, pageHeader, UserDidDTO } from '#middlewares'; import { Auth, AuthUser } from '#auth'; import { CacheService, getCacheKey, Guardians, InternalException, ServiceError, TaskManager, UseCache } from '#helpers'; import { CACHE, PREFIXES } from '#constants'; @@ -104,6 +104,105 @@ 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 setUserStandardRegistry( + @AuthUser() user: IAuthUser, + @Body() parent: any, + @Req() req + ): Promise { + const { username } = user; + const guardians = new Guardians(); + try { + 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); + } + } + + /** + * 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 addUserStandardRegistry( + @AuthUser() user: IAuthUser, + @Body() parent: any, + @Req() req + ): Promise { + const guardians = new Guardians(); + try { + await guardians.addUserStandardRegistry(user, 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); + } + } + /** * Update user profile (async) */ diff --git a/api-gateway/src/helpers/guardians.ts b/api-gateway/src/helpers/guardians.ts index 6b0346773a..438f1c0dfa 100644 --- a/api-gateway/src/helpers/guardians.ts +++ b/api-gateway/src/helpers/guardians.ts @@ -491,6 +491,24 @@ export class Guardians extends NatsService { return await this.sendMessage(MessageAPI.CREATE_USER_PROFILE_COMMON, { user, username, profile }); } + /** + * Update standart registry + * @param username + * @param standartRegistryDid + */ + public async updateUserStandardRegistry(username: string, standardRegistryDid: string): Promise { + return await this.sendMessage(MessageAPI.USER_UPDATE_STANDARD_REGISTRY, { username, standardRegistryDid }); + } + + /** + * Update standart registry + * @param username + * @param standartRegistryDid + */ + public async addUserStandardRegistry(user: IAuthUser, username: string, standardRegistryDids: string[]): Promise { + return await this.sendMessage(MessageAPI.USER_ADD_STANDARD_REGISTRY, { user, username, standardRegistryDids }); + } + /** * Async create user * @param username diff --git a/api-gateway/src/helpers/users.ts b/api-gateway/src/helpers/users.ts index c06728b675..08e896584f 100644 --- a/api-gateway/src/helpers/users.ts +++ b/api-gateway/src/helpers/users.ts @@ -94,8 +94,8 @@ export class Users extends NatsService { * @param username * @param userId */ - public async getUserPermissions(username: string, userId: string | null): Promise { - return await this.sendMessage(AuthEvents.GET_USER_PERMISSIONS, { username, userId }); + public async getUserPermissions(username: string, parent: string, userId: string | null): Promise { + return await this.sendMessage(AuthEvents.GET_USER_PERMISSIONS, { username, parent, userId }); } /** @@ -516,4 +516,4 @@ export class UsersService { public async generateNewUserTokenBasedOnExternalUserProvider(userProvider: ProviderAuthUser): Promise { return await this.users.generateNewUserTokenBasedOnExternalUserProvider(userProvider); } -} \ No newline at end of file +} diff --git a/api-gateway/src/middlewares/validation/schemas/profiles.dto.ts b/api-gateway/src/middlewares/validation/schemas/profiles.dto.ts index 2cf2d08b00..18f113d37f 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/api-gateway/src/middlewares/validation/schemas/profiles.ts b/api-gateway/src/middlewares/validation/schemas/profiles.ts index 05fec5308f..057aafa3a7 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/account-service.ts b/auth-service/src/api/account-service.ts index fa205fbc60..0b6a306949 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'; import { passwordComplexity, PasswordError } from '#constants'; import { HttpStatus } from '@nestjs/common'; @@ -466,6 +467,7 @@ export class AccountService extends NatsService { this.getMessages(AuthEvents.GET_USER_ACCOUNTS, async (msg: { filters?: any, + currentUsername?: string, parent?: string, pageIndex?: any, pageSize?: any, @@ -477,18 +479,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)) { @@ -504,15 +496,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'], userId); return new MessageError(error); 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/api/parent-permissions-service.ts b/auth-service/src/api/parent-permissions-service.ts new file mode 100644 index 0000000000..cdd5ef9a74 --- /dev/null +++ b/auth-service/src/api/parent-permissions-service.ts @@ -0,0 +1,93 @@ +import { User } from '../entity/user.js'; +import { DatabaseServer, MessageError, MessageResponse, NatsService, PinoLogger, Singleton } from '@guardian/common'; +import { AuthEvents, GenerateUUIDv4 } from '@guardian/interfaces'; +import { ParentPermissions } from '../entity/parent-permissions.js'; +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 b6fe45226d..ee9b8f434b 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 { @@ -106,7 +107,7 @@ export class RoleService extends NatsService { * * @returns {any[]} permissions */ - this.getMessages(AuthEvents.GET_PERMISSIONS, async (msg: {userId: string | null }) => { + this.getMessages(AuthEvents.GET_PERMISSIONS, async (msg: { userId: string | null }) => { const { userId } = msg; try { return new MessageResponse(permissionList); @@ -417,11 +418,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 } }); @@ -457,8 +463,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'], userId); return new MessageError(error); @@ -479,13 +487,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); @@ -500,10 +510,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'], userId); @@ -529,17 +545,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) { @@ -549,7 +570,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)) { @@ -577,10 +598,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'], userId); return new MessageError(error); @@ -592,9 +615,27 @@ export class RoleService extends NatsService { * @param username - username */ this.getMessages(AuthEvents.GET_USER_PERMISSIONS, async (msg: any) => { - const { username, userId } = msg; + const { username, parent, userId } = 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'], userId); diff --git a/auth-service/src/app.ts b/auth-service/src/app.ts index 9d14c49ba6..75a0dc5675 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({ @@ -73,6 +74,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/constants/user.ts b/auth-service/src/constants/user.ts index eb3dfce906..0286f9fee6 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', @@ -39,4 +40,4 @@ export const DB_REQUIRED_PROPS = [ 'hederaAccountId', 'permissions', 'permissionsGroup' -] \ No newline at end of file +] 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/entity/user.ts b/auth-service/src/entity/user.ts index 1ee7cbe9e3..ca7fa473da 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/auth-service/src/migrations/v3-2-1.ts b/auth-service/src/migrations/v3-2-1.ts new file mode 100644 index 0000000000..e0759e670e --- /dev/null +++ b/auth-service/src/migrations/v3-2-1.ts @@ -0,0 +1,36 @@ +import { 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 a665386660..4258c58583 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, diff --git a/common/src/hedera-modules/message/index.ts b/common/src/hedera-modules/message/index.ts index 231da3a3cd..4ba5df668b 100644 --- a/common/src/hedera-modules/message/index.ts +++ b/common/src/hedera-modules/message/index.ts @@ -24,5 +24,6 @@ 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'; +export { UserMessage } from './user-message.js'; export { PolicyDiffMessage } from './policy-diff-message.js'; -export { PolicyActionMessage } from './policy-action-message.js'; \ No newline at end of file +export { PolicyActionMessage } from './policy-action-message.js'; diff --git a/common/src/hedera-modules/message/message-action.ts b/common/src/hedera-modules/message/message-action.ts index 43a69136a7..7104e34861 100644 --- a/common/src/hedera-modules/message/message-action.ts +++ b/common/src/hedera-modules/message/message-action.ts @@ -40,6 +40,7 @@ export enum MessageAction { PublishPolicyLabel = 'publish-policy-label', CreateLabelDocument = 'create-label-document', PublishFormula = 'publish-formula', + AddParent = 'add-parent', PublishPolicyDiff = 'publish-diff', PublishPolicyBackup = 'publish-backup', CreatePolicyAction = 'create-policy-action', @@ -48,4 +49,4 @@ export enum MessageAction { CreatePolicyRequest = 'create-policy-request', UpdatePolicyRequest = 'update-policy-request', ErrorPolicyRequest = 'error-policy-request', -} \ No newline at end of file +} diff --git a/common/src/hedera-modules/message/message-body.interface.ts b/common/src/hedera-modules/message/message-body.interface.ts index cc1204a004..8b0cb94c95 100644 --- a/common/src/hedera-modules/message/message-body.interface.ts +++ b/common/src/hedera-modules/message/message-body.interface.ts @@ -339,6 +339,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 */ diff --git a/common/src/hedera-modules/message/message-server.ts b/common/src/hedera-modules/message/message-server.ts index 3964ef1d16..19374f0be2 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'; import { PolicyDiffMessage } from './policy-diff-message.js'; import { PolicyActionMessage } from './policy-action-message.js'; import { ContractMessage } from './contract-message.js'; @@ -396,6 +397,8 @@ export class MessageServer { case MessageType.Formula: message = FormulaMessage.fromMessageObject(json); break; + case MessageType.User: + message = UserMessage.fromMessageObject(json); case MessageType.PolicyDiff: message = PolicyDiffMessage.fromMessageObject(json); break; diff --git a/common/src/hedera-modules/message/message-type.ts b/common/src/hedera-modules/message/message-type.ts index c6a79e5cd7..85646cd6fa 100644 --- a/common/src/hedera-modules/message/message-type.ts +++ b/common/src/hedera-modules/message/message-type.ts @@ -23,6 +23,7 @@ export enum MessageType { PolicyStatistic = 'Policy-Statistic', PolicyLabel = 'Policy-Label', Formula = 'Formula', + User = 'User', PolicyDiff = 'Policy-Diff', PolicyAction = 'Policy-Action' } 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 7460db7bbd..8604b8cfd8 100644 --- a/common/src/helpers/users.ts +++ b/common/src/helpers/users.ts @@ -267,6 +267,14 @@ export class Users extends NatsService { return await this.sendMessage(AuthEvents.UPDATE_USER_ROLE, { username, userRoles, owner, userId: owner.id }); } + 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/common/src/interfaces/auth.interface.ts b/common/src/interfaces/auth.interface.ts index d90d6502fb..87349f0764 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/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index d3d4238d6e..a329ca103e 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -119,6 +119,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'; @@ -129,6 +131,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'; import { ExternalPoliciesService } from './services/external-policy.service'; import { UserKeysDialog } from './components/user-keys-dialog/user-keys-dialog.component'; @@ -156,6 +163,7 @@ import { UserKeysDialog } from './components/user-keys-dialog/user-keys-dialog.c BrandingComponent, SuggestionsConfigurationComponent, StandardRegistryCardComponent, + StandardRegistryParentCardComponent, NotificationComponent, NotificationsComponent, QrCodeDialogComponent, @@ -176,6 +184,9 @@ import { UserKeysDialog } from './components/user-keys-dialog/user-keys-dialog.c UsersManagementComponent, UsersManagementDetailComponent, WorkerTasksComponent, + AddStandardRegistryDialogComponent, + InfoStandardRegistryDialogComponent, + ActiveStandardRegistryDialogComponent, UserKeysDialog ], exports: [], @@ -214,12 +225,16 @@ import { UserKeysDialog } from './components/user-keys-dialog/user-keys-dialog.c 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..ea8efab793 --- /dev/null +++ b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.html @@ -0,0 +1,35 @@ + + + +
+
+
{{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..9ee2278f13 --- /dev/null +++ b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.scss @@ -0,0 +1,75 @@ + +.sr-card { + position: relative; + + ::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; + padding-bottom: 0; + } + + ::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-policy-count { + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + + i { + position: relative; + top: 2px; + } + } + + .set-active-btn { + margin-left: 8px; + } + + .sr-action button { + height: 28px; + padding: 6px 16px; + } +} + +.dropdown-item { + padding: 8px 16px; + font-size: 15px; + white-space: nowrap; +} 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..ab2f8fd45d --- /dev/null +++ b/frontend/src/app/components/standard-registry-parent-card/standard-registry-parent-card.component.ts @@ -0,0 +1,57 @@ +import {Component, EventEmitter, Input, Output, SimpleChanges, ViewChild} from '@angular/core'; +import {IStandardRegistryResponse} from '@guardian/interfaces'; +import { DialogService } from 'primeng/dynamicdialog'; +import { OverlayPanel } from 'primeng/overlaypanel'; +import { ProfileService } from 'src/app/services/profile.service'; +import { ActiveStandardRegistryDialogComponent } from 'src/app/views/user-profile/active-standard-registry-dialog/active-standard-registry-dialog.component'; + +@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(); + @Output() setActive: EventEmitter = new EventEmitter(); + @Input() activeSr?: IStandardRegistryResponse; + @ViewChild('overlay') overlay!: OverlayPanel; + + constructor(public dialogService: DialogService, private profileService: ProfileService) { + } + + onMoreInfoClick(): void { + this.registrySelected.emit(this.registry.did); + } + + showOverlay(event: MouseEvent) { + if (this.registry.policies?.length) { + this.overlay.toggle(event); + } + } + + onSetActive() { + this.dialogService.open(ActiveStandardRegistryDialogComponent, { + styleClass: 'guardian-dialog', + width: '640px', + modal: true, + showHeader: false, + data: { + title: 'Make active', + currentActive: this.activeSr?.hederaAccountId, + potentialActive: this.registry.hederaAccountId + } + }).onClose.subscribe((data) => { + if(data?.update) { + this.onChangeActiveSr(); + } + }); + } + + onChangeActiveSr() { + this.profileService.selectActiveStandartRegistry(this.registry.did).subscribe(() => { + this.setActive.emit(this.registry.did); + }); + } +} diff --git a/frontend/src/app/services/profile.service.ts b/frontend/src/app/services/profile.service.ts index 59bcd8b290..ee182e2b29 100644 --- a/frontend/src/app/services/profile.service.ts +++ b/frontend/src/app/services/profile.service.ts @@ -75,6 +75,14 @@ export class ProfileService { 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 }); + } + public keys( pageIndex?: number, pageSize?: number diff --git a/frontend/src/app/views/root-profile/root-profile.component.html b/frontend/src/app/views/root-profile/root-profile.component.html index e5763ea4b1..363aacaecf 100644 --- a/frontend/src/app/views/root-profile/root-profile.component.html +++ b/frontend/src/app/views/root-profile/root-profile.component.html @@ -1,19 +1,10 @@
- + - + - +
@@ -80,7 +71,7 @@

Profile

DID Document
+ svgClass="primary-color"> View document
@@ -88,7 +79,7 @@

Profile

VC Document
+ svgClass="primary-color"> View document
@@ -96,12 +87,8 @@

Profile

-
+
Hedera Account
DID Document
DID Document signing keys
@@ -116,84 +103,49 @@

Profile

- +
- +
- +
- +
- +
- +
- +
@@ -225,13 +177,8 @@

Profile

- +
@@ -241,32 +188,20 @@

Profile

-
-
- Invalid DID Document. @@ -284,25 +219,15 @@

Profile

- +
- + Invalid DID Key @@ -311,122 +236,56 @@

Profile

- +
- + - + - +
- + - + - +
- + - +
- + - +
- + - +
@@ -444,12 +303,7 @@

Profile

An error occurred while creating the document.

Please try again later.

-
@@ -460,10 +314,6 @@

Profile

- - + + \ No newline at end of file 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..b1ee4791a3 --- /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 {{potentialActive}}
+
This will deactivate the current active SR {{currentActive}}
+
+
+ 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 65687cc4c7..656132ad5e 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.html +++ b/frontend/src/app/views/user-profile/user-profile.component.html @@ -3,12 +3,8 @@
- +
@@ -16,6 +12,7 @@

Profile

+
@@ -28,6 +25,11 @@

Profile

Decentralized Access Key
+ + +
Standard Registries
+
+
@@ -79,22 +81,18 @@

Profile

DID Document -
- +
+ View document
VC Document -
- +
+ View document
@@ -102,41 +100,29 @@

Profile

- +
- - + +
- -
-
+
\ No newline at end of file diff --git a/frontend/src/app/views/user-profile/user-profile.component.scss b/frontend/src/app/views/user-profile/user-profile.component.scss index 5fb2688463..fbb1f52e7f 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.scss +++ b/frontend/src/app/views/user-profile/user-profile.component.scss @@ -1063,6 +1063,49 @@ mat-radio-button { } } +::ng-deep .ng-star-inserted a { + color: var(--color-grey-5); + font-weight: 600; + font-size: 14px; + text-decoration: none; +} + +::ng-deep .p-tabview-panels { + background-color: inherit; + padding-left: 0; + padding-right: 0; +} + +::ng-deep .p-tabview-nav-container ul.p-tabview-nav { + font-family: Inter, sans-serif; + background-color: inherit; + + a { + background-color: inherit; + font-weight: 600; + font-size: 14px; + line-height: 16px; + } + + .p-highlight a { + color: var(--color-primary); + border-bottom: 2px solid var(--color-primary); + } +} + +.add-registries-button { + display: flex; + justify-content: end; + + ::ng-deep .p-button { + background-color: var(--button-primary-color); + font-size: 14px; + font-weight: 500; + height: 40px; + border-radius: 8px; + } +} + .remote-user-setup { flex-direction: column; } diff --git a/frontend/src/app/views/user-profile/user-profile.component.ts b/frontend/src/app/views/user-profile/user-profile.component.ts index c20b54c599..c58848456c 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.ts +++ b/frontend/src/app/views/user-profile/user-profile.component.ts @@ -19,6 +19,8 @@ import { ValidateIfFieldEqual } from '../../validators/validate-if-field-equal'; import { ChangePasswordComponent } from '../login/change-password/change-password.component'; import { UserKeysDialog } from 'src/app/components/user-keys-dialog/user-keys-dialog.component'; import { CustomConfirmDialogComponent } from 'src/app/modules/common/custom-confirm-dialog/custom-confirm-dialog.component'; +import { AddStandardRegistryDialogComponent } from './add-standard-registry-dialog/add-standard-registry-dialog.component'; +import { InfoStandardRegistryDialogComponent } from './info-standard-registry-dialog/info-standard-registry-dialog.component'; enum OperationMode { None, @@ -82,6 +84,30 @@ export class UserProfileComponent implements OnInit { : this.standardRegistries; } + public get standardRegistriesAsParentList(): IStandardRegistryResponse[] { + const res = this.standardRegistries.length > 0 && this.profile?.parents + ? this.standardRegistries.filter((sr: IStandardRegistryResponse) => this.profile?.parents?.includes(sr.did)) + : []; + return res; + } + + public get potentialStandardRegistryParents(): IStandardRegistryResponse[] { + const res = this.standardRegistries.length + ? this.standardRegistries.filter((sr: IStandardRegistryResponse) => !this.profile?.parents?.includes(sr.did)) + : []; + return res; + } + + public isActiveStandardRegistry(did: string): boolean { + return this.profile?.parent === did; + } + + public get activeSr() { + return this.standardRegistries.length + ? this.standardRegistries.find((sr) => sr.did === this.profile?.parent) + : undefined; + } + public get isFilterButtonDisabled(): boolean { return ( this.filters.policyName.length === 0 && @@ -122,7 +148,7 @@ export class UserProfileComponent implements OnInit { public remoteDidDocumentForm!: UntypedFormControl; public didKeys: any[] = []; - public tab: 'general' | 'keys' = 'general'; + public tab: 'general' | 'keys' | 'srs' = 'general'; public pageIndex: number; public pageSize: number; public pageCount: number; @@ -590,6 +616,36 @@ export class UserProfileComponent implements OnInit { this.selectStandardRegistry(''); } + public selectStandardRegistryShowMore(did: string): void { + const sr = this.standardRegistries.find(sr => sr.did === did); + const activeSr = this.standardRegistries.length + ? this.standardRegistries.find((sr) => sr.did === this.profile?.parent) + : undefined; + if (sr) { + this.dialogService.open(InfoStandardRegistryDialogComponent, { + styleClass: 'guardian-dialog', + width: '720px', + height: '640px', + modal: true, + showHeader: false, + data: { + title: 'Standard Registry Details', + standardRegistry: sr, + activeSr + } + }).onClose.subscribe((data) => { + if (data?.update) { + this.updateActiveSr(data.parent); + } + }); + } + } + + public updateActiveSr(nextActiveSrDid: string) { + this.profile = { ...this.profile, parent: nextActiveSrDid }; + this.cdRef.detectChanges(); + } + public selectStandardRegistry(did: string): void { this.standardRegistryForm.setValue(did); } @@ -892,6 +948,23 @@ export class UserProfileComponent implements OnInit { }); } + public addStandardRegistry() { + this.dialogService.open(AddStandardRegistryDialogComponent, { + styleClass: 'guardian-dialog', + width: '720px', + height: '504px', + modal: true, + showHeader: false, + data: { + title: 'Add Standard Registry', + standardRegistries: this.potentialStandardRegistryParents + } + }).onClose.subscribe((data) => { + if (data?.update) { + this.loadDate(); + } + }); + } public download() { if (this.profile) { const name = this.profile.username; @@ -928,7 +1001,7 @@ export class UserProfileComponent implements OnInit { } public onChangeTab(tab: any) { - this.tab = tab.index === 0 ? 'general' : 'keys'; + this.tab = tab.index === 0 ? 'general' : tab.index === 1 ? 'keys' : 'srs'; this.pageIndex = 0; this.router.navigate([], { queryParams: { tab: this.tab } diff --git a/guardian-service/src/api/profile.service.ts b/guardian-service/src/api/profile.service.ts index fd37f2a741..2936ca1570 100644 --- a/guardian-service/src/api/profile.service.ts +++ b/guardian-service/src/api/profile.service.ts @@ -17,6 +17,11 @@ import { VcHelper, Wallet, Workers, + UserMessage, + Topic, + TopicConfig, + MessageServer, + MessageAction } from '@guardian/common'; import { emptyNotifier, initNotifier } from '../helpers/notifier.js'; import { RestoreDataFromHedera } from '../helpers/restore-data-from-hedera.js'; @@ -118,6 +123,56 @@ export function profileAPI(logger: PinoLogger) { } }); + ApiResponse(MessageAPI.USER_UPDATE_STANDARD_REGISTRY, + async (msg: { username: string, standardRegistryDid: string }) => { + try { + const { username, standardRegistryDid } = msg; + const users = new Users(); + await users.updateUserParent(username, standardRegistryDid); + return new MessageResponse(username); + } catch (error) { + await logger.error(error, ['GUARDIAN_SERVICE']); + console.error(error); + return new MessageError(error, 500); + } + }); + + ApiResponse(MessageAPI.USER_ADD_STANDARD_REGISTRY, + async (msg: { user: IAuthUser, username: string, standardRegistryDids: string[] }) => { + try { + const { user, username, standardRegistryDids } = msg; + const users = new Users(); + + for (const standardRegistryDid of standardRegistryDids) { + await users.addUserParent(username, standardRegistryDid); + const cUser = await users.getUser(username, user.id); + + const row = await new DatabaseServer().findOne(Topic, { + owner: standardRegistryDid, + type: TopicType.UserTopic + }); + const userTopic = await new DatabaseServer().findOne(Topic, { + owner: cUser.did, + type: TopicType.UserTopic + }); + const topicConfig = await TopicConfig.fromObject(row, true, user.id); + + const root = await users.getHederaAccount(cUser.did, user.id); + const messageServer = new MessageServer({ operatorId: root.hederaAccountId, operatorKey: root.hederaAccountKey, signOptions: root.signOptions }); + messageServer.setTopicObject(topicConfig); + const message = new UserMessage(MessageAction.AddParent); + message.setDocument({ ...cUser, topicId: userTopic.topicId }); + await messageServer.sendMessage(message); + } + + return new MessageResponse(standardRegistryDids); + } catch (error) { + await logger.error(error, ['GUARDIAN_SERVICE']); + console.error(error); + return new MessageError(error, 500); + } + }); + ApiResponse(MessageAPI.CREATE_USER_PROFILE_COMMON, async (msg: { user: IAuthUser, @@ -380,6 +435,7 @@ export function profileAPI(logger: PinoLogger) { permissions: user.permissions, did: user.did, parent: user.parent, + parents: user.parents, hederaAccountId: user.hederaAccountId, location: user.location, confirmed: false, diff --git a/interfaces/src/interface/user.interface.ts b/interfaces/src/interface/user.interface.ts index 7cea7f484c..b042321893 100644 --- a/interfaces/src/interface/user.interface.ts +++ b/interfaces/src/interface/user.interface.ts @@ -100,6 +100,10 @@ export interface IUser { */ parent?: string; + /** + * Parent + */ + parents?: string[]; /** * DID document instance */ diff --git a/interfaces/src/type/messages/auth-events.ts b/interfaces/src/type/messages/auth-events.ts index f9ae989a02..faad555146 100644 --- a/interfaces/src/type/messages/auth-events.ts +++ b/interfaces/src/type/messages/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' } diff --git a/interfaces/src/type/messages/message-api.type.ts b/interfaces/src/type/messages/message-api.type.ts index b889db1a6b..23e3a284c5 100644 --- a/interfaces/src/type/messages/message-api.type.ts +++ b/interfaces/src/type/messages/message-api.type.ts @@ -278,7 +278,8 @@ export enum MessageAPI { GET_FORMULA_RELATIONSHIPS = 'GET_FORMULA_RELATIONSHIPS', GET_FORMULAS_DATA = 'GET_FORMULAS_DATA', PUBLISH_FORMULA = 'PUBLISH_FORMULA', - + USER_UPDATE_STANDARD_REGISTRY = 'USER_UPDATE_STANDARD_REGISTRY', + USER_ADD_STANDARD_REGISTRY = 'USER_ADD_STANDARD_REGISTRY', GET_EXTERNAL_POLICY_REQUEST = 'GET_EXTERNAL_POLICY_REQUEST', GET_EXTERNAL_POLICY_REQUESTS = 'GET_EXTERNAL_POLICY_REQUESTS', DELETE_EXTERNAL_POLICY = 'DELETE_EXTERNAL_POLICY', diff --git a/swagger.yaml b/swagger.yaml index 4eafba6207..bea3868635 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -4459,6 +4459,80 @@ paths: summary: Sets Hedera credentials for the user. tags: - profiles + /profiles/parent/select/{username}: + put: + description: '' + operationId: ProfileApi_setUserStandardRegistry + parameters: + - name: username + required: true + in: path + description: The name of the user for whom to update the information. + schema: + example: username + type: string + requestBody: + required: true + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/UserDidDTO' + responses: + '200': + description: Updated. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + security: + - bearer: [] + summary: '' + tags: + - profiles + /profiles/parent/add/{username}: + put: + description: '' + operationId: ProfileApi_addUserStandardRegistry + parameters: + - name: username + required: true + in: path + description: The name of the user for whom to update the information. + schema: + example: username + type: string + requestBody: + required: true + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/UserDidDTO' + responses: + '200': + description: Updated. + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + security: + - bearer: [] + summary: '' + tags: + - profiles /profiles/push/{username}: put: description: >- @@ -17664,8 +17738,15 @@ components: #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 parent: type: string + example: + - >- + #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 + parents: example: >- #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 + type: array + items: + type: string hederaAccountId: type: string example: 0.0.1 @@ -17830,6 +17911,14 @@ components: - entity - hederaAccountId - hederaAccountKey + UserDidDTO: + type: object + properties: + did: + type: string + nullable: false + required: + - did DidDocumentStatusDTO: type: object properties: @@ -19428,8 +19517,15 @@ components: #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 parent: type: string + example: + - >- + #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 + parents: example: >- #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 + type: array + items: + type: string hederaAccountId: type: string example: 0.0.1