diff --git a/api-gateway/src/api/service/account.ts b/api-gateway/src/api/service/account.ts index e20d031e36..0b75541561 100644 --- a/api-gateway/src/api/service/account.ts +++ b/api-gateway/src/api/service/account.ts @@ -1,8 +1,8 @@ -import { IAuthUser, NotificationHelper, PinoLogger } from '@guardian/common'; -import { Permissions, PolicyStatus, SchemaEntity, UserRole } from '@guardian/interfaces'; +import { IAuthUser, NotificationHelper, PinoLogger, RunFunctionAsync } from '@guardian/common'; +import { Permissions, PolicyStatus, SchemaEntity, TaskAction, UserRole } from '@guardian/interfaces'; import { ClientProxy } from '@nestjs/microservices'; import { Body, Controller, Get, Headers, HttpCode, HttpException, HttpStatus, Inject, Post, Req } from '@nestjs/common'; -import { ApiBearerAuth, ApiBody, ApiConflictResponse, ApiCreatedResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, ApiUnprocessableEntityResponse, getSchemaPath } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiAcceptedResponse, ApiBody, ApiConflictResponse, ApiCreatedResponse, ApiExtraModels, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse, ApiUnprocessableEntityResponse, getSchemaPath } from '@nestjs/swagger'; import { AccessTokenRequestDTO, AccessTokenResponseDTO, @@ -16,7 +16,7 @@ import { Examples, InternalServerErrorDTO, LoginUserDTO, - RegisterUserDTO, + OnboardingDTO, RegisterUserDTO, TaskDTO, StandardRegistryAccountDTO, UnauthorizedErrorDTO, UnprocessableEntityErrorDTO, @@ -31,7 +31,7 @@ import { OTPStatusResponseDTO } from '#middlewares'; import { Auth, AuthUser, checkPermission } from '#auth'; -import { EntityOwner, Guardians, InternalException, PolicyEngine, UseCache, Users } from '#helpers'; +import { EntityOwner, Guardians, InternalException, PolicyEngine, ServiceError, TaskManager, UseCache, Users } from '#helpers'; import { PolicyListResponse } from '../../entities/policy'; import { StandardRegistryAccountResponse } from '../../entities/account'; import { ApplicationEnvironment } from '../../environment.js'; @@ -167,11 +167,7 @@ export class AccountApi { if (!parentUser) { throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED); } - try { - await checkPermission(UserRole.STANDARD_REGISTRY)(parentUser); - } catch (error) { - await InternalException(error, this.logger, parentUser?.id); - } + await checkPermission(UserRole.STANDARD_REGISTRY)(parentUser); } try { const { role, username, password } = body; @@ -191,6 +187,164 @@ export class AccountApi { } } + /** + * Registers and fully onboards a new user in a single async call. + * + * @param body.username - Username for the new account + * @param body.password - Password + * @param body.password_confirmation - Must match password + * @param body.role - UserRole.USER or UserRole.STANDARD_REGISTRY + * @param body.hederaAccountId - Optional. Auto-generated from operator if omitted + * @param body.hederaAccountKey - Required when hederaAccountId is provided + * @param body.parent - Optional. Standard Registry username/DID (USER role only) + * @param body.vcDocument - Optional. VC subject to publish during setup + * @param body.didDocument - Optional. Custom DID document; auto-generated if omitted + * @param body.didKeys - Optional. Keys for the custom DID document + * @param body.useFireblocksSigning - Optional. Use Fireblocks instead of local key + * @param body.fireblocksConfig - Optional. Fireblocks configuration + * + * @returns TaskDTO — poll GET /tasks/:taskId for result + */ + @Post('/push/onboard') + @ApiOperation({ + summary: 'Registers and fully onboards a new user account.', + description: + 'Creates a user account, ' + + 'Hedera account, DID, and cryptographic keys on behalf of the user. ' + + 'If hederaAccountId / hederaAccountKey are omitted the platform generates them. ', + }) + @ApiBody({ + description: 'Registration and optional Hedera / DID credentials.', + type: OnboardingDTO, + }) + @ApiAcceptedResponse({ + description: 'Task created — poll for completion.', + type: TaskDTO, + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO, + }) + @ApiExtraModels(OnboardingDTO, TaskDTO, InternalServerErrorDTO) + @HttpCode(HttpStatus.ACCEPTED) + async registerAndOnboard( + @Body() body: OnboardingDTO, + @Req() req: any, + ): Promise { + const users = new Users(); + let parentUser: IAuthUser | null = null; + + const HEDERA_ACCOUNT_ID_REGEX = /^\d+\.\d+\.\d+$/; + const HEDERA_ACCOUNT_KEY_REGEX = + /^(?:302e020100300506032b657004220420[0-9a-fA-F]{64}|[0-9a-fA-F]{64})$/; + + if (!ApplicationEnvironment.demoMode) { + const authHeader = req.headers.authorization; + const token = authHeader?.split(' ')[1]; + try { + parentUser = await users.getUserByToken(token) as IAuthUser; + } catch (_) { + parentUser = null; + } + if (!parentUser) { + throw new HttpException('UNAUTHORIZED', HttpStatus.UNAUTHORIZED); + } + await checkPermission(UserRole.STANDARD_REGISTRY)(parentUser); + } + + if (body.hederaAccountId || body.hederaAccountKey) { + if (!body.hederaAccountId || !HEDERA_ACCOUNT_ID_REGEX.test(body.hederaAccountId)) { + throw new HttpException( + 'Invalid hederaAccountId format. Expected format: 0.0.XXXXX', + HttpStatus.UNPROCESSABLE_ENTITY, + ); + } + if (!body.hederaAccountKey || !HEDERA_ACCOUNT_KEY_REGEX.test(body.hederaAccountKey)) { + throw new HttpException( + 'Invalid hederaAccountKey format. Expected a valid Hedera private key', + HttpStatus.UNPROCESSABLE_ENTITY, + ); + } + } + + if (body.useFireblocksSigning === true && !body.fireblocksConfig) { + throw new HttpException( + 'fireblocksConfig is required when useFireblocksSigning is true', + HttpStatus.UNPROCESSABLE_ENTITY, + ); + } + + // USER role must have a Standard Registry parent + if (body.role === UserRole.USER) { + if (!body.parent?.trim()) { + throw new HttpException( + 'parent (Standard Registry username) is required for USER role accounts', + HttpStatus.BAD_REQUEST, + ); + } + const parentSR = await users.getUser(body.parent.trim(), parentUser?.id ?? null); + if (!parentSR) { + throw new HttpException( + `Standard Registry '${body.parent}' not found`, + HttpStatus.BAD_REQUEST, + ); + } + if (parentSR.role !== UserRole.STANDARD_REGISTRY) { + throw new HttpException( + `'${body.parent}' is not a Standard Registry`, + HttpStatus.BAD_REQUEST, + ); + } + body.parent = parentSR.did; + } + + const existingUser = await users.getUser(body.username, parentUser?.id ?? null); + if (existingUser) { + throw new HttpException( + `Username '${body.username}' is already taken`, + HttpStatus.CONFLICT, + ); + } + + if (body.hederaAccountId) { + const existingHederaUser = await users.getUserByAccount(body.hederaAccountId); + if (existingHederaUser) { + throw new HttpException( + `Hedera account '${body.hederaAccountId}' is already associated with an existing registration`, + HttpStatus.CONFLICT, + ); + } + } + + const taskManager = new TaskManager(); + const task = taskManager.start(TaskAction.ONBOARD_USER, parentUser?.id ?? null); + + // After the task completes, transfer ownership to the newly created user + taskManager.registerCallback(task, async (completedTask) => { + if (completedTask.result?.username) { + try { + const users = new Users(); + const newUser = await users.getUser(completedTask.result.username, parentUser?.id ?? null); + if (newUser?.id) { + taskManager.transferOwnership(task.taskId, newUser.id); + } + } catch (_) { + // Non-fatal — ownership transfer best-effort + } + } + }); + + RunFunctionAsync(async () => { + const guardians = new Guardians(); + await guardians.onboardUserAsync(parentUser, body, task); + }, async (error) => { + await this.logger.error(error, ['API_GATEWAY'], parentUser?.id); + taskManager.addError(task.taskId, { code: error.code || 500, message: error.message }); + }); + + return task; + } + /** * Login */ diff --git a/api-gateway/src/api/service/task.ts b/api-gateway/src/api/service/task.ts index 568cd9b476..280ab7a189 100644 --- a/api-gateway/src/api/service/task.ts +++ b/api-gateway/src/api/service/task.ts @@ -1,6 +1,6 @@ import { IAuthUser, PinoLogger } from '@guardian/common'; -import { Controller, Get, HttpCode, HttpStatus, Param } from '@nestjs/common'; -import { ApiTags, ApiParam, ApiOperation, ApiExtraModels, ApiOkResponse, ApiInternalServerErrorResponse } from '@nestjs/swagger'; +import { Controller, Get, HttpCode, HttpStatus, HttpException, Param } from '@nestjs/common'; +import { ApiTags, ApiParam, ApiOperation, ApiExtraModels, ApiOkResponse, ApiInternalServerErrorResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'; import { AuthUser, Auth } from '#auth'; import { Examples, InternalServerErrorDTO, TaskStatusDTO } from '#middlewares'; import { InternalException, TaskManager } from '#helpers'; @@ -60,4 +60,51 @@ export class TaskApi { await InternalException(error, this.logger, user.id); } } + + /** + * Get user onboard task status + */ + @Get('/onboard/:taskId') + @ApiOperation({ + summary: 'Returns task status of user onboarding by Id without authentication.', + description: + 'Returns task status of user onboarding by Id. No Bearer token required.', + }) + @ApiParam({ + name: 'taskId', + type: String, + description: 'Task Id returned by the initiating endpoint', + required: true, + example: Examples.UUID, + }) + @ApiOkResponse({ + description: 'Successful operation.', + type: TaskStatusDTO, + }) + @ApiUnauthorizedResponse({ + description: 'Task exists but is not an onboarding task.', + type: InternalServerErrorDTO, + }) + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO, + }) + @ApiExtraModels(TaskStatusDTO, InternalServerErrorDTO) + @HttpCode(HttpStatus.OK) + async getTaskStatus( + @Param('taskId') taskId: string, + ): Promise { + try { + const taskManager = new TaskManager(); + return taskManager.getOnboardingTask(taskId); + } catch (error) { + if (error?.code === 'TASK_NOT_ONBOARDING') { + throw new HttpException( + 'Unauthorized: this API only exposes onboarding tasks.', + HttpStatus.UNAUTHORIZED, + ); + } + await InternalException(error, this.logger, null); + } + } } diff --git a/api-gateway/src/helpers/guardians.ts b/api-gateway/src/helpers/guardians.ts index 87823e9a1f..ea91ead7d9 100644 --- a/api-gateway/src/helpers/guardians.ts +++ b/api-gateway/src/helpers/guardians.ts @@ -57,7 +57,8 @@ import { PolicyPreviewDTO, ProfileDTO, PolicyKeyDTO, - ToolVersionDTO + ToolVersionDTO, + OnboardingDTO } from '#middlewares'; /** @@ -559,6 +560,20 @@ export class Guardians extends NatsService { return await this.sendMessage(MessageAPI.CREATE_USER_PROFILE_COMMON_ASYNC, { user, username, profile, task }); } + /** + * Onboard a new user in a single async call. + * @param parentUser - the authenticated parent (Standard Registry) or null in demo mode + * @param payload - OnboardingDTO fields + * @param task - task tracking object + */ + public async onboardUserAsync( + parentUser: IAuthUser | null, + payload: OnboardingDTO, + task: NewTask + ): Promise { + return await this.sendMessage(MessageAPI.ONBOARD_USER_ASYNC, { parentUser, payload, task }); + } + /** * Restore user profile async * @param username diff --git a/api-gateway/src/helpers/task-manager.ts b/api-gateway/src/helpers/task-manager.ts index 4a98041edc..9793465405 100644 --- a/api-gateway/src/helpers/task-manager.ts +++ b/api-gateway/src/helpers/task-manager.ts @@ -70,6 +70,7 @@ export class TaskManager { [TaskAction.PREVIEW_SCHEMA_MESSAGE, 4], [TaskAction.CREATE_RANDOM_KEY, 3], [TaskAction.CONNECT_USER, 9], + [TaskAction.ONBOARD_USER, 9], [TaskAction.PREVIEW_POLICY_MESSAGE, 4], [TaskAction.CREATE_TOKEN, 4], [TaskAction.ASSOCIATE_TOKEN, 4], @@ -337,6 +338,47 @@ export class TaskManager { } } + /** + * Transfer task ownership to a different user. + * @param taskId + * @param newUserId + * @returns {void} + */ + public transferOwnership(taskId: string, newUserId: string): void { + const task = this.tasks[taskId]; + if (task) { + task.userId = newUserId; + } + } + + /** + * Return a sanitized onboarding task status by taskId + * @param taskId + * @returns {object} - task data + */ + public getOnboardingTask(taskId: string): object | undefined { + const task = this.tasks[taskId]; + if (!task) { + return undefined; + } + if (task.action !== TaskAction.ONBOARD_USER) { + const err: any = new Error('This API only exposes onboarding tasks.'); + err.code = 'TASK_NOT_ONBOARDING'; + throw err; + } + // Strip sensitive result fields + return { + taskId: task.taskId, + action: task.action, + expectation: task.expectation, + completed: task.result != null, + failed: task.error != null, + error: task.error + ? { message: task.error.message ?? 'Task failed' } + : null, + }; + } + /** * Return expectation for task * @param action diff --git a/api-gateway/src/middlewares/validation/schemas/accounts.ts b/api-gateway/src/middlewares/validation/schemas/accounts.ts index 6c48f61def..89b11e8c72 100644 --- a/api-gateway/src/middlewares/validation/schemas/accounts.ts +++ b/api-gateway/src/middlewares/validation/schemas/accounts.ts @@ -8,12 +8,14 @@ import { IsNotEmpty, IsNumber, IsOptional, - IsString + IsString, + ValidateNested } from 'class-validator'; import { UserRole } from '@guardian/interfaces'; import { Expose, Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; import { Match } from '../../../helpers/decorators/match.validator.js'; +import { DidDocumentDTO, DidKeyDTO, FireblocksConfigDTO, SubjectDTO } from './profiles.js'; export class PermissionGroupResponseDTO { @ApiProperty({ @@ -411,6 +413,58 @@ export class UserAccountDTO { did?: string; } +export class OnboardingDTO extends RegisterUserDTO { + @ApiProperty({ + required: false, + description: 'Hedera account ID (e.g. 0.0.12345). Auto-generated from the operator account if omitted.', + example: '0.0.12345' + }) + @IsOptional() + @IsString() + hederaAccountId?: string; + + @ApiProperty({ + required: false, + description: 'Hedera account private key. Required when hederaAccountId is provided; auto-generated if omitted.', + }) + @IsOptional() + @IsString() + hederaAccountKey?: string; + + @ApiProperty({ + required: false, + description: 'Standard Registry username or DID. Required for USER role accounts to link them to their registry.', + example: 'registry_username' + }) + @IsOptional() + @IsString() + parent?: string; + + @ApiProperty({ required: false, description: 'VC document to publish during profile setup.', type: () => SubjectDTO }) + @IsOptional() + vcDocument?: SubjectDTO; + + @ApiProperty({ required: false, description: 'Pre-created DID document. Auto-generated if omitted.', type: () => DidDocumentDTO }) + @IsOptional() + didDocument?: DidDocumentDTO; + + @ApiProperty({ required: false, description: 'Private keys for the DID document methods.', isArray: true, type: () => DidKeyDTO }) + @IsOptional() + @IsArray() + didKeys?: DidKeyDTO[]; + + @ApiProperty({ required: false, default: false, description: 'Use Fireblocks signing instead of local key.' }) + @IsOptional() + @IsBoolean() + useFireblocksSigning?: boolean; + + @ApiProperty({ required: false, type: () => FireblocksConfigDTO, description: 'Fireblocks configuration (required when useFireblocksSigning is true).' }) + @IsOptional() + @ValidateNested() + @Type(() => FireblocksConfigDTO) + fireblocksConfig?: FireblocksConfigDTO; +} + export class CredentialSubjectDTO { @ApiProperty() geography: string; diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 695857feb0..305670eca5 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -828,6 +828,7 @@ * [User Session](guardian/users/user-operations/account-apis/user-session.md) * [User Login](guardian/users/user-operations/account-apis/user-login.md) * [Registering new account](guardian/users/user-operations/account-apis/registering-new-account.md) + * [Unified User Onboarding](guardian/users/user-operations/account-apis/unified-onboarding.md) * [Returns all Standard Registries](guardian/users/user-operations/account-apis/returns-all-root-authorities.md) * [Returns Access Token](guardian/users/user-operations/account-apis/returns-access-token.md) * [Profile APIs](guardian/users/user-operations/profile-apis/README.md) diff --git a/docs/guardian/users/user-operations/account-apis/unified-onboarding.md b/docs/guardian/users/user-operations/account-apis/unified-onboarding.md new file mode 100644 index 0000000000..34f3e3ffa5 --- /dev/null +++ b/docs/guardian/users/user-operations/account-apis/unified-onboarding.md @@ -0,0 +1,119 @@ +# Unified User Onboarding + +**`POST /accounts/push/onboard`** + +Registers a new user account and fully sets up their Hedera account, DID, and cryptographic keys in a single async call. Returns a `taskId` immediately — poll `GET /tasks/onboard/{taskId}` for progress. + +**Authentication:** Bearer token required for non-demo mode (`Authorization: Bearer `). Only a Standard Registry user may onboard new accounts outside of demo mode. + +--- + +## Request + +### Request Body + +```json +{ + "username": "example_user", + "password": "examplePassword123", + "password_confirmation": "examplePassword123", + "role": "USER", + "parent": "example_registry" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `username` | string | Yes | Username for the new account | +| `password` | string | Yes | Account password | +| `password_confirmation` | string | Yes | Must match `password` | +| `role` | string | Yes | `STANDARD_REGISTRY` or `USER` | +| `parent` | string | Required for `USER` role | Standard Registry username. Links the `USER` account to their registry | +| `hederaAccountId` | string | No | Hedera account ID (e.g. `0.0.4532001`). Auto-generated if omitted | +| `hederaAccountKey` | string | No | Hedera account private key (DER encoded). Required when `hederaAccountId` is provided | +| `vcDocument` | object | No | VC credential subject to publish during setup | +| `didDocument` | object | No | Custom DID document. Auto-generated if omitted | +| `didKeys` | array | No | Private keys for the custom DID document methods | +| `useFireblocksSigning` | boolean | No | Use Fireblocks instead of local key signing | +| `fireblocksConfig` | object | No | Fireblocks configuration. Required when `useFireblocksSigning` is `true` | + +--- + +## Response + +### Success Response + +**Status:** `202 Accepted` + +```json +{ + "taskId": "63e3e5e8-a01b-3c00-1234-abcd5678ef90", + "expectation": 11, + "action": "Onboard user" +} +``` + +### Error Responses + +| Status | Description | +|--------|-------------| +| `400 Bad Request` | Passwords don't match, missing required fields, or `parent` not found for `USER` role | +| `401 Unauthorized` | Caller is not authenticated (non-demo mode) | +| `403 Forbidden` | Caller does not have Standard Registry role | +| `409 Conflict` | Username already exists | +| `422 Unprocessable Entity` | `hederaAccountId` provided without `hederaAccountKey` | +| `500 Internal Server Error` | Unexpected server failure | + +--- + +# Polling Onboarding Task Status + +**`GET /tasks/onboard/{taskId}`** + +Returns the current status of an onboarding task. No authentication required. Restricted to tasks started by `POST /accounts/push/onboard` — any other task type returns `401 Unauthorized`. + +**Authentication:** None + +--- + +## Request + +### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `taskId` | string | Yes | Task ID returned by `POST /accounts/push/onboard` | + +--- + +## Response + +### Success Response + +**Status:** `200 OK` + +```json +{ + "taskId": "63e3e5e8-a01b-3c00-1234-abcd5678ef90", + "action": "Onboard user", + "expectation": 11, + "completed": false, + "failed": false, + "error": null +} +``` + +| Field | Description | +|-------|-------------| +| `completed` | `true` when the task has finished successfully | +| `failed` | `true` when the task has failed | +| `error` | Error message object when `failed` is `true`, otherwise `null` | + +> Sensitive credentials (`publicKey`, `hederaAccountId`, `did`) are never returned by this endpoint. Once `completed` is `true`, the new user must log in and call `GET /tasks/{taskId}` with their own Bearer token to retrieve their full credentials. + +### Error Responses + +| Status | Description | +|--------|-------------| +| `401 Unauthorized` | `taskId` belongs to a non-onboarding task | +| `500 Internal Server Error` | Unexpected server failure | diff --git a/guardian-service/src/api/helpers/profile-helper.ts b/guardian-service/src/api/helpers/profile-helper.ts index e3a8cd1fe4..02f0ba96fe 100644 --- a/guardian-service/src/api/helpers/profile-helper.ts +++ b/guardian-service/src/api/helpers/profile-helper.ts @@ -79,6 +79,20 @@ export interface IDidKey { key: string } +export interface IOnboardingPayload { + username: string; + password: string; + role: UserRole; + hederaAccountId?: string; + hederaAccountKey?: string; + parent?: string; + vcDocument?: any; + didDocument?: any; + didKeys?: IDidKey[]; + useFireblocksSigning?: boolean; + fireblocksConfig?: IFireblocksConfig; +} + /** * Get global topic */ diff --git a/guardian-service/src/api/profile.service.ts b/guardian-service/src/api/profile.service.ts index c2a432a4e6..0a7430183b 100644 --- a/guardian-service/src/api/profile.service.ts +++ b/guardian-service/src/api/profile.service.ts @@ -1,4 +1,4 @@ -import { DidDocumentStatus, MessageAPI, SchemaEntity, TopicType, WorkerTaskType } from '@guardian/interfaces'; +import { DidDocumentStatus, LocationType, MessageAPI, SchemaEntity, TopicType, UserRole, WorkerTaskType } from '@guardian/interfaces'; import { ApiResponse } from '../api/helpers/api-response.js'; import { CommonDidDocument, @@ -14,6 +14,7 @@ import { NewNotifier, PinoLogger, RunFunctionAsync, + SecretManager, Users, VcHelper, Wallet, @@ -23,7 +24,7 @@ import { RestoreDataFromHedera } from '../helpers/restore-data-from-hedera.js'; import { Controller, Module } from '@nestjs/common'; import { ClientsModule, Transport } from '@nestjs/microservices'; import { AccountId, PrivateKey } from '@hiero-ledger/sdk'; -import { setupUserProfile, validateCommonDid } from './helpers/profile-helper.js'; +import { ICredentials, IFireblocksConfig, IOnboardingPayload, setupUserProfile, validateCommonDid } from './helpers/profile-helper.js'; @Controller() export class ProfileController { @@ -168,6 +169,120 @@ export function profileAPI(logger: PinoLogger) { return new MessageResponse(task); }); + ApiResponse(MessageAPI.ONBOARD_USER_ASYNC, + async (msg: { + parentUser: IAuthUser | null, + payload: IOnboardingPayload, + task: any + }) => { + const { parentUser, payload, task } = msg; + const notifier = await NewNotifier.create(task); + const logId: string | null = parentUser?.id?.toString() ?? null; + + RunFunctionAsync(async () => { + const STEP_CREATE_HEDERA = 'Create Hedera account'; + const STEP_REGISTER = 'Register user'; + + let hederaAccountId: string = payload.hederaAccountId ?? null; + let hederaAccountKey: string = payload.hederaAccountKey ?? null; + + if (!hederaAccountId) { + notifier.addStep(STEP_CREATE_HEDERA); + } + notifier.addStep(STEP_REGISTER); + notifier.start(); + + if (!hederaAccountId) { + notifier.startStep(STEP_CREATE_HEDERA); + + const secretManager = SecretManager.New(); + const { OPERATOR_ID, OPERATOR_KEY } = await secretManager.getSecrets('keys/operator'); + + const rawBalance = payload.role === UserRole.STANDARD_REGISTRY + ? parseInt(process.env.INITIAL_STANDARD_REGISTRY_BALANCE, 10) + : parseInt(process.env.INITIAL_BALANCE, 10); + const initialBalance: number = Number.isFinite(rawBalance) ? rawBalance : null; + + const workers = new Workers(); + const treasury = await workers.addNonRetryableTask({ + type: WorkerTaskType.CREATE_ACCOUNT, + data: { + operatorId: OPERATOR_ID, + operatorKey: OPERATOR_KEY, + initialBalance, + payload: { userId: logId } + } + }, { + priority: 20, + attempts: 0, + userId: logId, + interception: logId, + registerCallback: true + }); + + hederaAccountId = treasury.id; + hederaAccountKey = treasury.key; + notifier.completeStep(STEP_CREATE_HEDERA); + } + + notifier.startStep(STEP_REGISTER); + const users = new Users(); + await users.registerNewUser( + payload.username, + payload.password, + payload.role, + logId + ); + notifier.completeStep(STEP_REGISTER); + + const profile: ICredentials = { + type: LocationType.LOCAL, + entity: payload.role === UserRole.STANDARD_REGISTRY + ? SchemaEntity.STANDARD_REGISTRY + : SchemaEntity.USER, + parent: payload.parent ?? parentUser?.did ?? null, + hederaAccountId, + hederaAccountKey, + vcDocument: payload.vcDocument ?? null, + didDocument: payload.didDocument ?? null, + didKeys: payload.didKeys ?? [], + useFireblocksSigning: payload.useFireblocksSigning ?? false, + // cast is safe: upstream guard ensures fireblocksConfig is present when useFireblocksSigning is true + fireblocksConfig: payload.fireblocksConfig as IFireblocksConfig, + // topicId is populated by setupUserProfile; not known at construction time + topicId: undefined as unknown as string, + }; + + const did = await setupUserProfile({ + username: payload.username, + profile, + logger, + notifier, + logId + }); + + let publicKey: string | null = null; + try { + publicKey = PrivateKey.fromString(hederaAccountKey).publicKey.toString(); + } catch (_) { + publicKey = null; + } + + notifier.result({ + username: payload.username, + role: payload.role, + did, + hederaAccountId, + publicKey + }); + }, async (error) => { + await logger.error(error, ['GUARDIAN_SERVICE'], logId); + notifier.fail(error); + }); + + return new MessageResponse(task); + }); + ApiResponse(MessageAPI.RESTORE_USER_PROFILE_COMMON_ASYNC, async (msg: { user: IAuthUser, diff --git a/interfaces/src/type/messages/message-api.type.ts b/interfaces/src/type/messages/message-api.type.ts index 7f7a2d6840..568bd5bb2e 100644 --- a/interfaces/src/type/messages/message-api.type.ts +++ b/interfaces/src/type/messages/message-api.type.ts @@ -74,6 +74,7 @@ export enum MessageAPI { CREATE_USER_PROFILE_COMMON = 'CREATE_USER_PROFILE_COMMON', CREATE_USER_PROFILE_COMMON_ASYNC = 'CREATE_USER_PROFILE_COMMON_ASYNC', RESTORE_USER_PROFILE_COMMON_ASYNC = 'RESTORE_USER_PROFILE_COMMON_ASYNC', + ONBOARD_USER_ASYNC = 'ONBOARD_USER_ASYNC', GET_USER_PROFILE = 'GET_USER_PROFILE', GET_USER_BALANCE = 'GET_USER_BALANCE', GET_USER_BALANCE_REST = 'GET_USER_BALANCE_REST', diff --git a/interfaces/src/type/task-action.type.ts b/interfaces/src/type/task-action.type.ts index a1c3dcb43c..23d2d94dd0 100644 --- a/interfaces/src/type/task-action.type.ts +++ b/interfaces/src/type/task-action.type.ts @@ -27,6 +27,7 @@ export enum TaskAction { DELETE_POLICIES = 'Delete policies', CLONE_POLICY = 'Clone policy', RESTORE_USER_PROFILE = 'Restore user profile', + ONBOARD_USER = 'Onboard user', GET_USER_TOPICS = 'Get user topics', CREATE_TOOL = 'Create tool', PUBLISH_TOOL = 'Publish tool',