diff --git a/.env.sample b/.env.sample index ab71c43..0b70aa4 100644 --- a/.env.sample +++ b/.env.sample @@ -6,7 +6,6 @@ JWT_ISSUER=12D3KooWDCuGU7WY3VaWjBS1E44x4EnmTgK3HRxWFqYG3dqXDfP1 # Replace this v JWT_TTL_SEC=3600 # TTL of issued JWT tokens OWNER=5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice HEALTH_TOKEN="change-me-please" -DIRECTORY_URL=http://localhost:8090 BASE_URL=http://localhost:8080 # E-mail diff --git a/resources/mail/README.md b/resources/mail/README.md index 350caf4..49d9bd4 100644 --- a/resources/mail/README.md +++ b/resources/mail/README.md @@ -9,9 +9,8 @@ contain only variables, which are replaced at runtime with their respective valu All possible variables are available (for copy/paste) in this template: [all-documented-vars.pug](all-documented-vars.pug) ### Legal Officer - legalOfficer.address + legalOfficer.account.address legalOfficer.additionalDetails - legalOfficer.node legalOfficer.userIdentity.firstName legalOfficer.userIdentity.lastName diff --git a/resources/mail/all-documented-vars.pug b/resources/mail/all-documented-vars.pug index 5c06c40..67ce3b2 100644 --- a/resources/mail/all-documented-vars.pug +++ b/resources/mail/all-documented-vars.pug @@ -3,7 +3,6 @@ | === Legal Officer === | #{legalOfficer.address}; | #{legalOfficer.additionalDetails}; -| #{legalOfficer.node}; | | #{legalOfficer.userIdentity.firstName}; | #{legalOfficer.userIdentity.lastName}; @@ -17,26 +16,8 @@ | #{legalOfficer.postalAddress.city}; | #{legalOfficer.postalAddress.country}; | -| === Other Legal Officer === -| #{otherLegalOfficer.address}; -| #{otherLegalOfficer.additionalDetails}; -| #{otherLegalOfficer.node}; -| -| #{otherLegalOfficer.userIdentity.firstName}; -| #{otherLegalOfficer.userIdentity.lastName}; -| #{otherLegalOfficer.userIdentity.email}; -| #{otherLegalOfficer.userIdentity.phoneNumber}; -| -| #{otherLegalOfficer.postalAddress.company}; -| #{otherLegalOfficer.postalAddress.line1}; -| #{otherLegalOfficer.postalAddress.line2}; -| #{otherLegalOfficer.postalAddress.postalCode}; -| #{otherLegalOfficer.postalAddress.city}; -| #{otherLegalOfficer.postalAddress.country}; -| | === Account Recovery === | #{recovery.requesterAddress.address}; -| #{recovery.otherLegalOfficerAddress}; | #{recovery.addressToRecover}; | #{recovery.createdOn}; | diff --git a/resources/mail/legal-officer-details.pug b/resources/mail/legal-officer-details.pug index bbfc759..4233d3c 100644 --- a/resources/mail/legal-officer-details.pug +++ b/resources/mail/legal-officer-details.pug @@ -8,7 +8,7 @@ | | Identification key: | ******************** -| #{legalOfficer.address} +| #{legalOfficer.account.address} | | Email: | ******* diff --git a/resources/schemas.json b/resources/schemas.json index d2ff659..bcf1e27 100644 --- a/resources/schemas.json +++ b/resources/schemas.json @@ -2049,6 +2049,70 @@ "description": "The ID of the Secret Recovery" } } + }, + "FetchLegalOfficersView": { + "type": "object", + "properties": { + "legalOfficers": { + "type": "array", + "description": "All the legal officers", + "items": { + "$ref": "#/components/schemas/LegalOfficerView" + } + } + }, + "title": "FetchLegalOfficersView", + "description": "The fetched Legal Officers" + }, + "LegalOfficerView": { + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "The SS58 address of the legal officer" + }, + "userIdentity": { + "$ref": "#/components/schemas/UserIdentityView" + }, + "postalAddress": { + "$ref": "#/components/schemas/LegalOfficerPostalAddressView" + }, + "additionalDetails": { + "type": "string", + "description": "Any additional public info" + } + }, + "title": "LegalOfficerView", + "description": "The Legal Officer" + }, + "CreateOrUpdateLegalOfficerView": { + "type": "object", + "properties": { + "userIdentity": { + "$ref": "#/components/schemas/UserIdentityView" + }, + "postalAddress": { + "$ref": "#/components/schemas/LegalOfficerPostalAddressView" + }, + "additionalDetails": { + "type": "string", + "description": "Any additional public info" + } + }, + "title": "CreateOrUpdateLegalOfficerView", + "description": "The Legal Officer info to created or updated" + }, + "LegalOfficerPostalAddressView": { + "type": "object", + "allOf": [{ + "$ref": "#/components/schemas/PostalAddressView" + }], + "properties": { + "company": { + "type": "string", + "description": "The company of the Legal Officer" + } + } } } } diff --git a/src/logion/app.support.ts b/src/logion/app.support.ts index 7c59978..3e49fad 100644 --- a/src/logion/app.support.ts +++ b/src/logion/app.support.ts @@ -26,6 +26,7 @@ import { fillInSpec as fillInSpecTokensRecord, TokensRecordController } from "./ import { fillInSpec as fillInSpecWorkload, WorkloadController } from "./controllers/workload.controller.js"; import { fillInSpec as fillInSpecSecretRecovery, SecretRecoveryController } from "./controllers/secret_recovery.controller.js"; import { fillInSpec as fillInSpecRecovery, RecoveryController } from "./controllers/recovery.controller.js"; +import { fillInSpec as fillInSpecLegalOfficer, LegalOfficerController } from "./controllers/legalofficer.controller.js"; const { logger } = Log; @@ -66,6 +67,7 @@ export function predefinedSpec(spec: OpenAPIV3.Document): OpenAPIV3.Document { fillInSpecWorkload(spec); fillInSpecSecretRecovery(spec); fillInSpecRecovery(spec); + fillInSpecLegalOfficer(spec); return spec; } @@ -130,6 +132,7 @@ export function setupApp(expressConfig?: ExpressConfig): Express { dino.registerController(WorkloadController); dino.registerController(SecretRecoveryController); dino.registerController(RecoveryController); + dino.registerController(LegalOfficerController); dino.dependencyResolver(AppContainer, (injector, type) => { diff --git a/src/logion/container/app.container.ts b/src/logion/container/app.container.ts index 3dc45b5..c93a138 100644 --- a/src/logion/container/app.container.ts +++ b/src/logion/container/app.container.ts @@ -20,7 +20,9 @@ import { TransactionController } from '../controllers/transaction.controller.js' import { CollectionRepository, CollectionFactory } from "../model/collection.model.js"; import { NotificationService } from "../services/notification.service.js"; import { MailService } from "../services/mail.service.js"; -import { DirectoryService } from "../services/directory.service.js"; +import { LegalOfficerService, TransactionalLegalOfficerService } from "../services/legalOfficerService.js"; +import { LegalOfficerController } from "../controllers/legalofficer.controller.js"; +import { LegalOfficerRepository, LegalOfficerFactory } from "../model/legalofficer.model.js"; import { VaultTransferRequestController } from '../controllers/vaulttransferrequest.controller.js'; import { VaultTransferRequestFactory, VaultTransferRequestRepository } from '../model/vaulttransferrequest.model.js'; import { LoFileFactory, LoFileRepository } from "../model/lofile.model.js"; @@ -96,7 +98,10 @@ container.bind(CollectionFactory).toSelf() container.bind(LogionNodeCollectionService).toSelf(); container.bind(NotificationService).toSelf() container.bind(MailService).toSelf() -container.bind(DirectoryService).toSelf() +container.bind(LegalOfficerService).toService(TransactionalLegalOfficerService); +container.bind(TransactionalLegalOfficerService).toSelf(); +container.bind(LegalOfficerFactory).toSelf(); +container.bind(LegalOfficerRepository).toSelf(); container.bind(VaultTransferRequestRepository).toSelf(); container.bind(VaultTransferRequestFactory).toSelf(); container.bind(LoFileFactory).toSelf(); @@ -177,5 +182,6 @@ container.bind(TokensRecordController).toSelf().inTransientScope(); container.bind(WorkloadController).toSelf().inTransientScope(); container.bind(SecretRecoveryController).toSelf().inTransientScope(); container.bind(RecoveryController).toSelf().inTransientScope(); +container.bind(LegalOfficerController).toSelf().inTransientScope(); export { container as AppContainer }; diff --git a/src/logion/controllers/account_recovery.controller.ts b/src/logion/controllers/account_recovery.controller.ts index 52cf145..88ca55c 100644 --- a/src/logion/controllers/account_recovery.controller.ts +++ b/src/logion/controllers/account_recovery.controller.ts @@ -25,7 +25,7 @@ import { } from '../model/account_recovery.model.js'; import { components } from './components.js'; import { NotificationService, Template, NotificationRecipient } from "../services/notification.service.js"; -import { DirectoryService } from "../services/directory.service.js"; +import { LegalOfficerService } from "../services/legalOfficerService.js"; import { AccountRecoveryRequestService } from '../services/accountrecoveryrequest.service.js'; import { LocalsObject } from 'pug'; import { LocRequestAdapter, UserPrivateData } from "./adapters/locrequestadapter.js"; @@ -91,7 +91,7 @@ export class AccountRecoveryController extends ApiController { private accountRecoveryRequestFactory: AccountRecoveryRequestFactory, private authenticationService: AuthenticationService, private notificationService: NotificationService, - private directoryService: DirectoryService, + private legalOfficerService: LegalOfficerService, private accountRecoveryRequestService: AccountRecoveryRequestService, private locRequestAdapter: LocRequestAdapter, private locRequestRepository: LocRequestRepository, @@ -114,7 +114,7 @@ export class AccountRecoveryController extends ApiController { @HttpPost('') async createRequest(body: CreateAccountRecoveryRequestView): Promise { const requester = await this.authenticationService.authenticatedUser(this.request); - const legalOfficerAddress = await this.directoryService.requireLegalOfficerAddressOnNode(body.legalOfficerAddress); + const legalOfficerAddress = await this.legalOfficerService.requireLegalOfficerAddressOnNode(body.legalOfficerAddress); const requesterIdentityLoc = requireDefined(body.requesterIdentityLoc); const request = await this.accountRecoveryRequestFactory.newAccountRecoveryRequest({ id: uuid(), @@ -314,8 +314,7 @@ export class AccountRecoveryController extends ApiController { private async getNotificationInfo(request: AccountRecoveryRequestDescription, userPrivateData?: UserPrivateData, decision?: LegalOfficerDecisionDescription): Promise<{ legalOfficerEMail: string, userEmail: string | undefined, data: LocalsObject }> { - const legalOfficer = await this.directoryService.get(request.legalOfficerAddress) - const otherLegalOfficer = await this.directoryService.get(request.otherLegalOfficerAddress) + const legalOfficer = await this.legalOfficerService.get(request.legalOfficerAddress) const { userIdentity, userPostalAddress } = userPrivateData ? userPrivateData : await this.locRequestAdapter.getUserPrivateData(request.requesterIdentityLocId) return { legalOfficerEMail: legalOfficer.userIdentity.email, @@ -323,7 +322,6 @@ export class AccountRecoveryController extends ApiController { data: { recovery: { ...request, decision }, legalOfficer, - otherLegalOfficer, walletUser: userIdentity, walletUserPostalAddress: userPostalAddress } diff --git a/src/logion/controllers/components.ts b/src/logion/controllers/components.ts index 6f3b158..f63364e 100644 --- a/src/logion/controllers/components.ts +++ b/src/logion/controllers/components.ts @@ -1086,6 +1086,40 @@ export interface components { /** @description The ID of the Secret Recovery */ id?: string; }; + /** + * FetchLegalOfficersView + * @description The fetched Legal Officers + */ + FetchLegalOfficersView: { + /** @description All the legal officers */ + legalOfficers?: components["schemas"]["LegalOfficerView"][]; + }; + /** + * LegalOfficerView + * @description The Legal Officer + */ + LegalOfficerView: { + /** @description The SS58 address of the legal officer */ + address?: string; + userIdentity?: components["schemas"]["UserIdentityView"]; + postalAddress?: components["schemas"]["LegalOfficerPostalAddressView"]; + /** @description Any additional public info */ + additionalDetails?: string; + }; + /** + * CreateOrUpdateLegalOfficerView + * @description The Legal Officer info to created or updated + */ + CreateOrUpdateLegalOfficerView: { + userIdentity?: components["schemas"]["UserIdentityView"]; + postalAddress?: components["schemas"]["LegalOfficerPostalAddressView"]; + /** @description Any additional public info */ + additionalDetails?: string; + }; + LegalOfficerPostalAddressView: { + /** @description The company of the Legal Officer */ + company?: string; + } & components["schemas"]["PostalAddressView"]; }; responses: never; parameters: never; diff --git a/src/logion/controllers/legalofficer.controller.ts b/src/logion/controllers/legalofficer.controller.ts new file mode 100644 index 0000000..1c5954f --- /dev/null +++ b/src/logion/controllers/legalofficer.controller.ts @@ -0,0 +1,156 @@ +import { + addTag, + setControllerTag, + getDefaultResponses, + setPathParameters, + getRequestBody, + AuthenticationService, + requireDefined, + badRequest +} from "@logion/rest-api-core"; +import { OpenAPIV3 } from "openapi-types"; +import { injectable } from "inversify"; +import { Controller, ApiController, HttpGet, Async, HttpPut } from "dinoloop"; +import { components } from "./components.js"; +import { + LegalOfficerRepository, + LegalOfficerDescription, + LegalOfficerFactory, +} from "../model/legalofficer.model.js"; +import { ValidAccountId } from "@logion/node-api"; +import { LegalOfficerService } from "../services/legalOfficerService.js"; + +export function fillInSpec(spec: OpenAPIV3.Document): void { + const tagName = 'Legal Officers'; + addTag(spec, { + name: tagName, + description: "Retrieval and Management of Legal Officers details" + }); + setControllerTag(spec, /^\/api\/legal-officer.*/, tagName); + + LegalOfficerController.fetchLegalOfficers(spec); + LegalOfficerController.getLegalOfficer(spec); + LegalOfficerController.createOrUpdateLegalOfficer(spec); +} + +type LegalOfficerView = components["schemas"]["LegalOfficerView"] +type FetchLegalOfficersView = components["schemas"]["FetchLegalOfficersView"] +type CreateOrUpdateLegalOfficerView = components["schemas"]["CreateOrUpdateLegalOfficerView"] + +@injectable() +@Controller('/legal-officer') +export class LegalOfficerController extends ApiController { + + constructor( + private legalOfficerRepository: LegalOfficerRepository, + private legalOfficerFactory: LegalOfficerFactory, + private authenticationService: AuthenticationService, + private legalOfficerService: LegalOfficerService, + ) { + super(); + } + + static fetchLegalOfficers(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/legal-officer"].get!; + operationObject.summary = "Gets the list of all legal officers hosted on this node"; + operationObject.description = "No authentication required."; + operationObject.responses = getDefaultResponses("FetchLegalOfficersView"); + } + + @HttpGet('') + @Async() + async fetchLegalOfficers(): Promise { + const legalOfficers = await this.legalOfficerRepository.findAll(); + return { legalOfficers: legalOfficers.map(legalOfficer => legalOfficer.getDescription()).map(this.toView) } + } + + static getLegalOfficer(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/legal-officer/{address}"].get!; + operationObject.summary = "Gets the details of one legal officer"; + operationObject.description = "No authentication required."; + operationObject.responses = getDefaultResponses("LegalOfficerView"); + setPathParameters(operationObject, { + address: "The Polkadot address of the expected Legal Officer" + }) + } + + @HttpGet('/:address') + @Async() + async getLegalOfficer(address: string): Promise { + const account = ValidAccountId.polkadot(address); + const legalOfficer = await this.legalOfficerRepository.findByAccount(account); + if (legalOfficer) { + return this.toView(legalOfficer.getDescription()); + } else { + throw badRequest("No legal officer with given address"); + } + } + + private toView(description: LegalOfficerDescription): LegalOfficerView { + const userIdentity = description.userIdentity; + const postalAddress = description.postalAddress; + return { + address: description.account.address, + userIdentity: { + firstName: userIdentity.firstName, + lastName: userIdentity.lastName, + email: userIdentity.email, + phoneNumber: userIdentity.phoneNumber, + }, + postalAddress: { + company: postalAddress.company, + line1: postalAddress.line1, + line2: postalAddress.line2, + postalCode: postalAddress.postalCode, + city: postalAddress.city, + country: postalAddress.country, + }, + additionalDetails: description.additionalDetails, + } + } + + static createOrUpdateLegalOfficer(spec: OpenAPIV3.Document) { + const operationObject = spec.paths["/api/legal-officer"].put!; + operationObject.summary = "Creates or updates the details of a legal officer"; + operationObject.description = "The authenticated user must be Legal Officer hosted on this node"; + operationObject.requestBody = getRequestBody({ + description: "Legal Officer details to be created/updated", + view: "CreateOrUpdateLegalOfficerView" + }) + operationObject.responses = getDefaultResponses("LegalOfficerView"); + setPathParameters(operationObject, { + address: "The Polkadot address of the expected Legal Officer" + }) + } + + @HttpPut('') + @Async() + async createOrUpdateLegalOfficer(createOrUpdate: CreateOrUpdateLegalOfficerView): Promise { + const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); + const account = (await authenticatedUser.requireLegalOfficerOnNode()).validAccountId; + const userIdentity = requireDefined(createOrUpdate.userIdentity); + const postalAddress = requireDefined(createOrUpdate.postalAddress); + const description: LegalOfficerDescription = { + account, + userIdentity: { + firstName: userIdentity.firstName || "", + lastName: userIdentity.lastName || "", + email: userIdentity.email || "", + phoneNumber: userIdentity.phoneNumber || "", + }, + postalAddress: { + company: postalAddress.company || "", + line1: postalAddress.line1 || "", + line2: postalAddress.line2 || "", + postalCode: postalAddress.postalCode || "", + city: postalAddress.city || "", + country: postalAddress.country || "", + }, + additionalDetails: createOrUpdate.additionalDetails || "", + } + const legalOfficer = this.legalOfficerFactory.newLegalOfficer(description); + await this.legalOfficerService.createOrUpdateLegalOfficer(legalOfficer); + + return this.toView(legalOfficer.getDescription()); + } +} diff --git a/src/logion/controllers/locrequest.controller.ts b/src/logion/controllers/locrequest.controller.ts index 1535bc2..1b39257 100644 --- a/src/logion/controllers/locrequest.controller.ts +++ b/src/logion/controllers/locrequest.controller.ts @@ -33,7 +33,7 @@ import { UserIdentity } from "../model/useridentity.js"; import { FileStorageService } from "../services/file.storage.service.js"; import { ForbiddenException } from "dinoloop/modules/builtin/exceptions/exceptions.js"; import { NotificationService, Template, NotificationRecipient } from "../services/notification.service.js"; -import { DirectoryService } from "../services/directory.service.js"; +import { LegalOfficerService } from "../services/legalOfficerService.js"; import { CollectionRepository } from "../model/collection.model.js"; import { getUploadedFile } from "./fileupload.js"; import { PostalAddress } from "../model/postaladdress.js"; @@ -140,7 +140,7 @@ export class LocRequestController extends ApiController { private collectionRepository: CollectionRepository, private fileStorageService: FileStorageService, private notificationService: NotificationService, - private directoryService: DirectoryService, + private legalOfficerService: LegalOfficerService, private locRequestAdapter: LocRequestAdapter, private locRequestService: LocRequestService, private locAuthorizationService: LocAuthorizationService, @@ -164,7 +164,7 @@ export class LocRequestController extends ApiController { @Async() async createLocRequest(createLocRequestView: CreateLocRequestView): Promise { const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); - const ownerAddress = await this.directoryService.requireLegalOfficerAddressOnNode(createLocRequestView.ownerAddress); + const ownerAddress = await this.legalOfficerService.requireLegalOfficerAddressOnNode(createLocRequestView.ownerAddress); const locType = requireDefined(createLocRequestView.locType); const requesterAddress = !authenticatedUser.validAccountId.equals(ownerAddress) ? authenticatedUser.validAccountId : @@ -276,7 +276,7 @@ export class LocRequestController extends ApiController { @Async() async createOpenLoc(openLocView: OpenLocView): Promise { const authenticatedUser = await this.authenticationService.authenticatedUser(this.request); - const ownerAddress = await this.directoryService.requireLegalOfficerAddressOnNode(openLocView.ownerAddress); + const ownerAddress = await this.legalOfficerService.requireLegalOfficerAddressOnNode(openLocView.ownerAddress); const locType = requireDefined(openLocView.locType); const requesterAddress = authenticatedUser.validAccountId; const requesterIdentityLoc = await this.locRequestRepository.getValidPolkadotIdentityLoc(requesterAddress, ownerAddress); @@ -1555,7 +1555,7 @@ export class LocRequestController extends ApiController { private async getNotificationInfo(loc: LocRequestDescription, userIdentity: UserIdentity, decision?: LocRequestDecision): Promise<{ legalOfficerEMail: string, data: LocalsObject }> { - const legalOfficer = await this.directoryService.get(loc.ownerAddress); + const legalOfficer = await this.legalOfficerService.get(loc.ownerAddress); return { legalOfficerEMail: legalOfficer.userIdentity.email, data: { diff --git a/src/logion/controllers/secret_recovery.controller.ts b/src/logion/controllers/secret_recovery.controller.ts index 84c7876..e1232bb 100644 --- a/src/logion/controllers/secret_recovery.controller.ts +++ b/src/logion/controllers/secret_recovery.controller.ts @@ -22,7 +22,7 @@ import moment from "moment"; import { NotificationRecipient, Template, NotificationService } from "../services/notification.service.js"; import { UserPrivateData } from "./adapters/locrequestadapter.js"; import { LocalsObject } from "pug"; -import { DirectoryService } from "../services/directory.service.js"; +import { LegalOfficerService } from "../services/legalOfficerService.js"; import { UUID, ValidAccountId } from "@logion/node-api"; import { LegalOfficerDecisionDescription } from "../model/decision.js"; import { EMPTY_POSTAL_ADDRESS } from "../model/postaladdress.js"; @@ -62,7 +62,7 @@ export class SecretRecoveryController extends ApiController { private secretRecoveryRequestService: SecretRecoveryRequestService, private secretRecoveryRequestRepository: SecretRecoveryRequestRepository, private locRequestRepository: LocRequestRepository, - private directoryService: DirectoryService, + private legalOfficerService: LegalOfficerService, private notificationService: NotificationService, private authenticationService: AuthenticationService, ) { @@ -142,7 +142,7 @@ export class SecretRecoveryController extends ApiController { private async getNotificationInfo(secretRecoveryRequest: SecretRecoveryRequestDescription, legalOfficerAccount: ValidAccountId, userPrivateData: UserPrivateData, decision?: LegalOfficerDecisionDescription): Promise<{ legalOfficerEMail: string, userEmail: string | undefined, data: LocalsObject }> { - const legalOfficer = await this.directoryService.get(legalOfficerAccount) + const legalOfficer = await this.legalOfficerService.get(legalOfficerAccount) const { userIdentity, userPostalAddress } = userPrivateData; return { legalOfficerEMail: legalOfficer.userIdentity.email, diff --git a/src/logion/controllers/vaulttransferrequest.controller.ts b/src/logion/controllers/vaulttransferrequest.controller.ts index 6bbff5a..e94af3b 100644 --- a/src/logion/controllers/vaulttransferrequest.controller.ts +++ b/src/logion/controllers/vaulttransferrequest.controller.ts @@ -25,7 +25,7 @@ import { } from '../model/vaulttransferrequest.model.js'; import { components } from './components.js'; import { NotificationService } from "../services/notification.service.js"; -import { DirectoryService } from "../services/directory.service.js"; +import { LegalOfficerService } from "../services/legalOfficerService.js"; import { AccountRecoveryRequestDescription, AccountRecoveryRepository } from '../model/account_recovery.model.js'; import { VaultTransferRequestService } from '../services/vaulttransferrequest.service.js'; import { LocalsObject } from 'pug'; @@ -71,7 +71,7 @@ export class VaultTransferRequestController extends ApiController { private vaultTransferRequestFactory: VaultTransferRequestFactory, private authenticationService: AuthenticationService, private notificationService: NotificationService, - private directoryService: DirectoryService, + private legalOfficerService: LegalOfficerService, private accountRecoveryRepository: AccountRecoveryRepository, private vaultTransferRequestService: VaultTransferRequestService, private polkadotService: PolkadotService, @@ -96,7 +96,7 @@ export class VaultTransferRequestController extends ApiController { async createVaultTransferRequest(body: CreateVaultTransferRequestView): Promise { const origin = ValidAccountId.polkadot(requireDefined(body.origin, () => badRequest("Missing origin"))); const destination = ValidAccountId.polkadot(requireDefined(body.destination, () => badRequest("Missing destination"))); - const legalOfficerAddress = await this.directoryService.requireLegalOfficerAddressOnNode(body.legalOfficerAddress); + const legalOfficerAddress = await this.legalOfficerService.requireLegalOfficerAddressOnNode(body.legalOfficerAddress); const userData = await this.userAuthorizedAndProtected(origin, legalOfficerAddress); const request = this.vaultTransferRequestFactory.newVaultTransferRequest({ @@ -204,7 +204,7 @@ export class VaultTransferRequestController extends ApiController { decision?: VaultTransferRequestDecision ): Promise<{ legalOfficerEmail: string, userEmail: string | undefined, data: LocalsObject }> { - const legalOfficer = await this.directoryService.get(vaultTransfer.legalOfficerAddress); + const legalOfficer = await this.legalOfficerService.get(vaultTransfer.legalOfficerAddress); const { userIdentity, userPostalAddress } = userPrivateData; return { legalOfficerEmail: legalOfficer.userIdentity.email, diff --git a/src/logion/migration/1718188396630-AddLegalOfficers.ts b/src/logion/migration/1718188396630-AddLegalOfficers.ts new file mode 100644 index 0000000..7158131 --- /dev/null +++ b/src/logion/migration/1718188396630-AddLegalOfficers.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddLegalOfficers1718188396630 implements MigrationInterface { + name = 'AddLegalOfficers1718188396630' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "legal_officer" ("address" character varying NOT NULL, "first_name" character varying(255) NOT NULL, "last_name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "phone_number" character varying(255) NOT NULL, "company" character varying(255), "line1" character varying(255) NOT NULL, "line2" character varying(255), "postal_code" character varying(255) NOT NULL, "city" character varying(255) NOT NULL, "country" character varying(255) NOT NULL, "additional_details" character varying(255), CONSTRAINT "PK_legal_officer" PRIMARY KEY ("address"))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "legal_officer"`); + } + +} diff --git a/src/logion/model/legalofficer.model.ts b/src/logion/model/legalofficer.model.ts index f7062f2..42de015 100644 --- a/src/logion/model/legalofficer.model.ts +++ b/src/logion/model/legalofficer.model.ts @@ -1,21 +1,136 @@ import { PostalAddress } from "./postaladdress.js"; import { UserIdentity } from "./useridentity.js"; import { ValidAccountId } from "@logion/node-api"; +import { appDataSource } from "@logion/rest-api-core"; +import { Entity, Column, PrimaryColumn, Repository } from "typeorm"; +import { injectable } from "inversify"; +import { DB_SS58_PREFIX } from "./supportedaccountid.model.js"; interface LegalOfficerPostalAddress extends PostalAddress { readonly company: string } -export interface LegalOfficer { - readonly address: string; - readonly userIdentity: UserIdentity - readonly postalAddress: LegalOfficerPostalAddress - readonly additionalDetails: string - readonly node: string -} - export interface LegalOfficerSettingId { id: string, legalOfficer: ValidAccountId, } +@Entity("legal_officer") +export class LegalOfficerAggregateRoot { + + @PrimaryColumn() + address?: string; + + @Column({ length: 255, name: "first_name" }) + firstName?: string; + + @Column({ length: 255, name: "last_name" }) + lastName?: string; + + @Column({ length: 255 }) + email?: string; + + @Column({ length: 255, name: "phone_number" }) + phoneNumber?: string; + + @Column({ length: 255, nullable: true }) + company?: string; + + @Column({ length: 255 }) + line1?: string; + + @Column({ length: 255, nullable: true }) + line2?: string; + + @Column({ length: 255, name: "postal_code" }) + postalCode?: string; + + @Column({ length: 255 }) + city?: string; + + @Column({ length: 255 }) + country?: string; + + @Column({ length: 255, name: "additional_details", nullable: true }) + additionalDetails?: string; + + getDescription(): LegalOfficerDescription { + return { + account: ValidAccountId.polkadot(this.address!), + userIdentity: { + firstName: this.firstName || "", + lastName: this.lastName || "", + email: this.email || "", + phoneNumber: this.phoneNumber || "", + }, + postalAddress: { + company: this.company || "", + line1: this.line1 || "", + line2: this.line2 || "", + postalCode: this.postalCode || "", + city: this.city || "", + country: this.country || "", + }, + additionalDetails: this.additionalDetails || "", + } + } +} + +export interface LegalOfficerDescription { + readonly account: ValidAccountId; + readonly userIdentity: UserIdentity; + readonly postalAddress: LegalOfficerPostalAddress; + readonly additionalDetails: string; +} + +@injectable() +export class LegalOfficerFactory { + + newLegalOfficer(description: LegalOfficerDescription): LegalOfficerAggregateRoot { + const legalOfficer = new LegalOfficerAggregateRoot(); + legalOfficer.address = description.account.getAddress(DB_SS58_PREFIX); + + const userIdentity = description.userIdentity; + legalOfficer.firstName = userIdentity.firstName; + legalOfficer.lastName = userIdentity.lastName; + legalOfficer.email = userIdentity.email; + legalOfficer.phoneNumber = userIdentity.phoneNumber; + + const postalAddress = description.postalAddress + legalOfficer.company = postalAddress.company; + legalOfficer.line1 = postalAddress.line1; + legalOfficer.line2 = postalAddress.line2; + legalOfficer.postalCode = postalAddress.postalCode; + legalOfficer.city = postalAddress.city; + legalOfficer.country = postalAddress.country; + + legalOfficer.additionalDetails = description.additionalDetails; + + return legalOfficer; + } +} + +@injectable() +export class LegalOfficerRepository { + + constructor() { + this.repository = appDataSource.getRepository(LegalOfficerAggregateRoot); + } + + readonly repository: Repository + + public findByAccount(address: ValidAccountId): Promise { + return this.repository.findOneBy({ address: address.getAddress(DB_SS58_PREFIX) }); + } + + public findAll(): Promise { + return this.repository.find(); + } + + public async save(root: LegalOfficerAggregateRoot): Promise { + await this.repository.save(root); + } + +} + + diff --git a/src/logion/services/accountrecoverysynchronization.service.ts b/src/logion/services/accountrecoverysynchronization.service.ts index 8217c4d..90117cd 100644 --- a/src/logion/services/accountrecoverysynchronization.service.ts +++ b/src/logion/services/accountrecoverysynchronization.service.ts @@ -5,7 +5,7 @@ import { AccountRecoveryRepository, FetchAccountRecoveryRequestsSpecification } import { Adapters, ValidAccountId } from '@logion/node-api'; import { JsonExtrinsic, toString } from "./types/responses/Extrinsic.js"; import { AccountRecoveryRequestService as AccountRecoveryService } from './accountrecoveryrequest.service.js'; -import { DirectoryService } from "./directory.service.js"; +import { LegalOfficerService } from "./legalOfficerService.js"; const { logger } = Log; @@ -15,7 +15,7 @@ export class AccountRecoverySynchronizer { constructor( private accountRecoveryRepository: AccountRecoveryRepository, private accountRecoveryService: AccountRecoveryService, - private directoryService: DirectoryService, + private legalOfficerService: LegalOfficerService, ) { } @@ -30,7 +30,7 @@ export class AccountRecoverySynchronizer { const legalOfficerAddresses = Adapters.asArray(extrinsic.call.args['legal_officers']).map(address => Adapters.asString(address)); for (const legalOfficerAddress of legalOfficerAddresses) { const legalOfficer = ValidAccountId.polkadot(legalOfficerAddress); - if (await this.directoryService.isLegalOfficerAddressOnNode(legalOfficer)) { + if (await this.legalOfficerService.isLegalOfficerAddressOnNode(legalOfficer)) { const signer = extrinsic.signer!; const requests = await this.accountRecoveryRepository.findBy(new FetchAccountRecoveryRequestsSpecification({ expectedRequesterAddress: ValidAccountId.polkadot(signer), diff --git a/src/logion/services/directory.service.ts b/src/logion/services/directory.service.ts deleted file mode 100644 index 08df01b..0000000 --- a/src/logion/services/directory.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { injectable } from "inversify"; -import { LegalOfficer } from "../model/legalofficer.model.js"; -import axios, { AxiosInstance } from "axios"; -import { badRequest, AuthenticationSystemFactory } from "@logion/rest-api-core"; -import { AuthorityService } from "@logion/authenticator"; -import { ValidAccountId } from "@logion/node-api"; - -@injectable() -export class DirectoryService { - - private readonly axios: AxiosInstance; - private readonly authorityService: Promise; - - constructor(private authenticationSystemFactory: AuthenticationSystemFactory) { - if (process.env.DIRECTORY_URL === undefined) { - throw Error("Please set var DIRECTORY_URL"); - } - this.axios = axios.create({ baseURL: process.env.DIRECTORY_URL }); - const authenticationSystem = this.authenticationSystemFactory.authenticationSystem(); - this.authorityService = authenticationSystem.then(system => system.authorityService); - } - - async get(account: ValidAccountId): Promise { - return await this.axios.get(`/api/legal-officer/${ account.address }`) - .then(response => response.data); - } - - async requireLegalOfficerAddressOnNode(address: string | undefined): Promise { - if (!address) { - throw badRequest("Missing Legal Officer address") - } - const account = ValidAccountId.polkadot(address); - if (await this.isLegalOfficerAddressOnNode(account)) { - return account; - } else { - throw badRequest(`Address ${ address } is not the one of a Legal Officer on this node.`) - } - } - - async isLegalOfficerAddressOnNode(account: ValidAccountId): Promise { - const authorityService = await this.authorityService; - return (await authorityService.isLegalOfficerOnNode(account)) - } -} diff --git a/src/logion/services/legalOfficerService.ts b/src/logion/services/legalOfficerService.ts new file mode 100644 index 0000000..d173d5e --- /dev/null +++ b/src/logion/services/legalOfficerService.ts @@ -0,0 +1,78 @@ +import { injectable } from "inversify"; +import { + LegalOfficerDescription, + LegalOfficerRepository, + LegalOfficerAggregateRoot +} from "../model/legalofficer.model.js"; +import { badRequest, AuthenticationSystemFactory, requireDefined, DefaultTransactional } from "@logion/rest-api-core"; +import { AuthorityService } from "@logion/authenticator"; +import { ValidAccountId } from "@logion/node-api"; + +export abstract class LegalOfficerService { + + private readonly authorityService: Promise; + + constructor( + private authenticationSystemFactory: AuthenticationSystemFactory, + private legalOfficerRepository: LegalOfficerRepository, +) { + const authenticationSystem = this.authenticationSystemFactory.authenticationSystem(); + this.authorityService = authenticationSystem.then(system => system.authorityService); + } + + async createOrUpdateLegalOfficer(legalOfficer: LegalOfficerAggregateRoot): Promise { + await this.legalOfficerRepository.save(legalOfficer); + } + + async get(account: ValidAccountId): Promise { + const legalOfficer = requireDefined( + await this.legalOfficerRepository.findByAccount(account), + () => new Error(`Cannot find legal officer ${ account.address } in local database`) + ); + return legalOfficer.getDescription() + } + + async requireLegalOfficerAddressOnNode(address: string | undefined): Promise { + if (!address) { + throw badRequest("Missing Legal Officer address") + } + const account = ValidAccountId.polkadot(address); + if (await this.isLegalOfficerAddressOnNode(account)) { + return account; + } else { + throw badRequest(`Address ${ address } is not the one of a Legal Officer on this node.`) + } + } + + async isLegalOfficerAddressOnNode(account: ValidAccountId): Promise { + const authorityService = await this.authorityService; + return (await authorityService.isLegalOfficerOnNode(account)) + } +} + +@injectable() +export class TransactionalLegalOfficerService extends LegalOfficerService { + + constructor( + authenticationSystemFactory: AuthenticationSystemFactory, + legalOfficerRepository: LegalOfficerRepository + ) { + super(authenticationSystemFactory, legalOfficerRepository); + } + + @DefaultTransactional() + async createOrUpdateLegalOfficer(legalOfficer: LegalOfficerAggregateRoot): Promise { + return super.createOrUpdateLegalOfficer(legalOfficer); + } +} + +@injectable() +export class NonTransactionalLegalOfficerService extends LegalOfficerService { + + constructor( + authenticationSystemFactory: AuthenticationSystemFactory, + legalOfficerRepository: LegalOfficerRepository + ) { + super(authenticationSystemFactory, legalOfficerRepository); + } +} diff --git a/src/logion/services/locsynchronization.service.ts b/src/logion/services/locsynchronization.service.ts index 39c7117..f65314d 100644 --- a/src/logion/services/locsynchronization.service.ts +++ b/src/logion/services/locsynchronization.service.ts @@ -9,7 +9,7 @@ import { LocRequestService } from './locrequest.service.js'; import { CollectionService } from './collection.service.js'; import { UserIdentity } from '../model/useridentity.js'; import { NotificationService } from './notification.service.js'; -import { DirectoryService } from './directory.service.js'; +import { LegalOfficerService } from './legalOfficerService.js'; import { VerifiedIssuerSelectionService } from './verifiedissuerselection.service.js'; import { TokensRecordService } from './tokensrecord.service.js'; import { EMPTY_ITEMS, LocItems } from '../model/loc_items.js'; @@ -24,7 +24,7 @@ export class LocSynchronizer { private locRequestService: LocRequestService, private collectionService: CollectionService, private notificationService: NotificationService, - private directoryService: DirectoryService, + private legalOfficerService: LegalOfficerService, private verifiedIssuerSelectionService: VerifiedIssuerSelectionService, private tokensRecordService: TokensRecordService, ) {} @@ -226,7 +226,7 @@ export class LocSynchronizer { private async notifyIssuerNominatedDismissed(extrinsic: JsonExtrinsic) { const nominated = extrinsic.call.method === "nominateIssuer"; const legalOfficerAddress = ValidAccountId.polkadot(requireDefined(extrinsic.signer)); - if(await this.directoryService.isLegalOfficerAddressOnNode(legalOfficerAddress)) { + if(await this.legalOfficerService.isLegalOfficerAddressOnNode(legalOfficerAddress)) { const issuerAccount = ValidAccountId.polkadot(Adapters.asString(extrinsic.call.args["issuer"])); const identityLoc = await this.getIssuerIdentityLoc(legalOfficerAddress, issuerAccount); logger.info("Handling nomination/dismissal of issuer %s", issuerAccount.address); @@ -262,7 +262,7 @@ export class LocSynchronizer { }) { const { legalOfficerAddress, nominated, issuer } = args; try { - const legalOfficer = await this.directoryService.get(legalOfficerAddress); + const legalOfficer = await this.legalOfficerService.get(legalOfficerAddress); const data = { legalOfficer, walletUser: issuer, @@ -279,7 +279,7 @@ export class LocSynchronizer { private async handleIssuerSelectedUnselected(extrinsic: JsonExtrinsic) { const legalOfficerAddress = ValidAccountId.polkadot(requireDefined(extrinsic.signer)); - if(await this.directoryService.isLegalOfficerAddressOnNode(legalOfficerAddress)) { + if(await this.legalOfficerService.isLegalOfficerAddressOnNode(legalOfficerAddress)) { const issuerAccount = ValidAccountId.polkadot(Adapters.asString(extrinsic.call.args["issuer"])); const identityLoc = await this.getIssuerIdentityLoc(legalOfficerAddress, issuerAccount); const selected = extrinsic.call.args["selected"] as boolean; @@ -305,7 +305,7 @@ export class LocSynchronizer { }) { const { legalOfficerAddress, selected, locRequest, issuer } = args; try { - const legalOfficer = await this.directoryService.get(legalOfficerAddress); + const legalOfficer = await this.legalOfficerService.get(legalOfficerAddress); const data = { legalOfficer, walletUser: issuer, diff --git a/test/helpers/addresses.ts b/test/helpers/addresses.ts index 19e16de..d481d8a 100644 --- a/test/helpers/addresses.ts +++ b/test/helpers/addresses.ts @@ -1,4 +1,5 @@ import { ValidAccountId } from "@logion/node-api"; +import { LegalOfficerDescription } from "../../src/logion/model/legalofficer.model"; // Note to developers: Addresses are encoded // using a different custom prefix: 0 (= Polkadot) @@ -14,3 +15,60 @@ export const CHARLY = "14Gjs1TD93gnwEBfDMHoCgsuf1s2TVKUP6Z1qKmAZnZ8cW5q"; export const ALICE_ACCOUNT = ValidAccountId.polkadot(ALICE); export const BOB_ACCOUNT = ValidAccountId.polkadot(BOB); export const CHARLY_ACCOUNT = ValidAccountId.polkadot(CHARLY); + +export const LEGAL_OFFICERS: LegalOfficerDescription[] = [ + { + account: ALICE_ACCOUNT, + userIdentity: { + firstName: "Alice", + lastName: "Alice", + email: "alice@logion.network", + phoneNumber: "+32 498 00 00 00", + }, + postalAddress: { + company: "MODERO", + line1: "Huissier de Justice Etterbeek", + line2: "Rue Beckers 17", + postalCode: "1040", + city: "Etterbeek", + country: "Belgique" + }, + additionalDetails: "", + }, + { + account: BOB_ACCOUNT, + userIdentity: { + firstName: "Bob", + lastName: "Bob", + email: "bob@logion.network", + phoneNumber: "+33 4 00 00 00 00", + }, + postalAddress: { + company: "SELARL ADRASTEE", + line1: "Gare des Brotteaux", + line2: "14, place Jules Ferry", + postalCode: "69006", + city: "Lyon", + country: "France" + }, + additionalDetails: "", + }, + { + account: CHARLY_ACCOUNT, + userIdentity: { + firstName: "Charlie", + lastName: "Charlie", + email: "charlie@logion.network", + phoneNumber: "+33 2 00 00 00 00", + }, + postalAddress: { + company: "AUXILIA CONSEILS 18", + line1: "Huissiers de Justice associƩs", + line2: "7 rue Jean Francois Champollion Parc Comitec", + postalCode: "18000", + city: "Bourges", + country: "France" + }, + additionalDetails: "", + } +] diff --git a/test/integration/migration/migration.spec.ts b/test/integration/migration/migration.spec.ts index c6fa5ec..a9158e6 100644 --- a/test/integration/migration/migration.spec.ts +++ b/test/integration/migration/migration.spec.ts @@ -3,7 +3,7 @@ const { connect, disconnect, queryRunner, runAllMigrations, revertAllMigrations describe('Migration', () => { - const NUM_OF_TABLES = 24; + const NUM_OF_TABLES = 25; jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; beforeEach(async () => { diff --git a/test/unit/controllers/account_recovery.controller.spec.ts b/test/unit/controllers/account_recovery.controller.spec.ts index 5a7e90f..24c6ed4 100644 --- a/test/unit/controllers/account_recovery.controller.spec.ts +++ b/test/unit/controllers/account_recovery.controller.spec.ts @@ -15,7 +15,7 @@ import { ALICE, BOB, BOB_ACCOUNT, ALICE_ACCOUNT } from '../../helpers/addresses. import { AccountRecoveryController } from '../../../src/logion/controllers/account_recovery.controller.js'; import { NotificationService, Template } from "../../../src/logion/services/notification.service.js"; import moment from "moment"; -import { DirectoryService } from "../../../src/logion/services/directory.service.js"; +import { LegalOfficerService } from "../../../src/logion/services/legalOfficerService.js"; import { notifiedLegalOfficer } from "../services/notification-test-data.js"; import { UserIdentity } from '../../../src/logion/model/useridentity.js'; import { PostalAddress } from '../../../src/logion/model/postaladdress.js'; @@ -96,7 +96,7 @@ function mockRecoveryRequestModel(container: Container, addressToRecover: ValidA root.setup(instance => instance.requesterIdentityLocId) .returns(identityLoc.id) - + factory.setup(instance => instance.newAccountRecoveryRequest( It.Is(params => { return params.addressToRecover !== null @@ -105,7 +105,7 @@ function mockRecoveryRequestModel(container: Container, addressToRecover: ValidA }))) .returns(Promise.resolve(root.object())); container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); - mockNotificationAndDirectoryService(container); + mockNotificationAndLegalOfficerService(container); container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); @@ -253,7 +253,7 @@ function mockModelForFetch(container: Container): void { const factory = new Mock(); container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); - mockNotificationAndDirectoryService(container) + mockNotificationAndLegalOfficerService(container) container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); @@ -288,7 +288,7 @@ function mockModelForReview(container: Container): void { const factory = new Mock(); container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); - mockNotificationAndDirectoryService(container) + mockNotificationAndLegalOfficerService(container) container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); @@ -356,7 +356,7 @@ function mockModelForAccept(container: Container, verifies: boolean): void { const factory = new Mock(); container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); - mockNotificationAndDirectoryService(container); + mockNotificationAndLegalOfficerService(container); container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); @@ -471,28 +471,28 @@ function mockModelForReject(container: Container, verifies: boolean): void { const factory = new Mock(); container.bind(AccountRecoveryRequestFactory).toConstantValue(factory.object()); - mockNotificationAndDirectoryService(container); + mockNotificationAndLegalOfficerService(container); container.bind(AccountRecoveryRequestService).toConstantValue(new NonTransactionalAccountRecoveryRequestService(repository.object())); container.bind(LocRequestAdapter).toConstantValue(mockLocRequestAdapter()); container.bind(LocRequestRepository).toConstantValue(mockLocRequestRepository()); } -function mockNotificationAndDirectoryService(container: Container) { +function mockNotificationAndLegalOfficerService(container: Container) { notificationService = new Mock(); notificationService .setup(instance => instance.notify(It.IsAny(), It.IsAny