Skip to content

Commit

Permalink
Merge pull request #305 from logion-network/feature/recoverable-secrets
Browse files Browse the repository at this point in the history
Add recoverable secrets
  • Loading branch information
gdethier authored May 14, 2024
2 parents 1d811c7 + 38cdd90 commit 4ac9b3e
Show file tree
Hide file tree
Showing 32 changed files with 891 additions and 456 deletions.
12 changes: 12 additions & 0 deletions resources/mail/recoverable-secret-added.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
| logion notification - Recoverable secret added
| Dear #{walletUser.firstName} #{walletUser.lastName},
|
| You receive this message because you just added a recoverable secret with name #{secretName} to your Identity LOC #{loc.id}.
|
| Please note, you can't answer this email. The logion network will never ask you to provide any kind of information or access to a web address through its email notifications.
|
| As a reminder, find below all details of the Legal Officer you'll have to contact in order to recover the secret's value:
|
include /legal-officer-details.pug
|
include /footer.pug
20 changes: 20 additions & 0 deletions resources/schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
"paths": {},
"components": {
"schemas": {
"AddSecretView": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Recoverable secret's name"
},
"value": {
"type": "string",
"description": "Recoverable secret"
}
}
},
"Submittable": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1036,6 +1049,13 @@
},
"collectionParams": {
"$ref": "#/components/schemas/CollectionParamsView"
},
"secrets": {
"type": "array",
"description": "The recoverable secrets attached to this LOC",
"items": {
"$ref": "#/components/schemas/AddSecretView"
}
}
},
"title": "LocRequestView",
Expand Down
7 changes: 4 additions & 3 deletions src/logion/controllers/adapters/locrequestadapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import { IdenfyService } from "../../services/idenfy/idenfy.service.js";
import {
LocRequestAggregateRoot,
LocRequestRepository,
ItemLifecycle,
LocFees,
CollectionParams
} from "../../model/locrequest.model.js";
import { PostalAddress } from "../../model/postaladdress.js";
import { UserIdentity } from "../../model/useridentity.js";
import { components } from "../components.js";
import { VoteRepository, VoteAggregateRoot } from "../../model/vote.model.js";
import { VerifiedIssuerAggregateRoot, VerifiedIssuerSelectionRepository } from "../../model/verifiedissuerselection.model.js";
import { Fees, ValidAccountId, AccountId } from "@logion/node-api";
import { ItemLifecycle } from "src/logion/model/loc_lifecycle.js";
import { LocFees } from "src/logion/model/loc_fees.js";
import { CollectionParams } from "src/logion/model/loc_vos.js";

export type UserPrivateData = {
identityLocId: string | undefined,
Expand Down Expand Up @@ -122,6 +122,7 @@ export class LocRequestAdapter {
sponsorshipId: locDescription.sponsorshipId?.toString(),
fees: this.toLocFeesView(locDescription.fees),
collectionParams: this.toCollectionParamsView(locDescription.collectionParams),
secrets: request.getSecrets(viewer),
};
const voidInfo = request.getVoidInfo();
if(voidInfo !== null) {
Expand Down
2 changes: 1 addition & 1 deletion src/logion/controllers/collection.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { OpenAPIV3 } from "express-oas-generator";
import moment from "moment";
import {
LocRequestRepository,
FileDescription,
LocRequestAggregateRoot,
LocFileDelivered
} from "../model/locrequest.model.js";
Expand Down Expand Up @@ -49,6 +48,7 @@ import { OwnershipCheckService } from "../services/ownershipcheck.service.js";
import { RestrictedDeliveryService } from "../services/restricteddelivery.service.js";
import { downloadAndClean } from "../lib/http.js";
import { LocRequestService } from "../services/locrequest.service.js";
import { FileDescription } from "../model/loc_items.js";

type CollectionItemView = components["schemas"]["CollectionItemView"];
type CollectionItemsView = components["schemas"]["CollectionItemsView"];
Expand Down
8 changes: 8 additions & 0 deletions src/logion/controllers/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export type webhooks = Record<string, never>;

export interface components {
schemas: {
AddSecretView: {
/** @description Recoverable secret's name */
name?: string;
/** @description Recoverable secret */
value?: string;
};
Submittable: {
/** @description The address of the submitter */
submitter?: components["schemas"]["SupportedAccountId"];
Expand Down Expand Up @@ -555,6 +561,8 @@ export interface components {
sponsorshipId?: string;
fees?: components["schemas"]["LocFeesView"];
collectionParams?: components["schemas"]["CollectionParamsView"];
/** @description The recoverable secrets attached to this LOC */
secrets?: components["schemas"]["AddSecretView"][];
};
/**
* LocPublicView
Expand Down
102 changes: 92 additions & 10 deletions src/logion/controllers/locrequest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,8 @@ import { components } from "./components.js";
import {
LocRequestRepository,
LocRequestFactory,
LocRequestDescription,
LocRequestAggregateRoot,
FetchLocRequestsSpecification,
LocRequestDecision,
FileDescription,
MetadataItemParams,
FileParams,
StoredFile,
LinkParams,
SubmissionType,
LocFees,
CollectionParams
} from "../model/locrequest.model.js";
import {
getRequestBody,
Expand Down Expand Up @@ -56,6 +46,10 @@ import { SponsorshipService } from "../services/sponsorship.service.js";
import { Hash } from "../lib/crypto/hashing.js";
import { toBigInt } from "../lib/convert.js";
import { LocalsObject } from "pug";
import { SubmissionType } from "../model/loc_lifecycle.js";
import { CollectionParams, LocRequestDecision, LocRequestDescription } from "../model/loc_vos.js";
import { LocFees } from "../model/loc_fees.js";
import { FileDescription, FileParams, LinkParams, MetadataItemParams, StoredFile } from "../model/loc_items.js";

const { logger } = Log;

Expand Down Expand Up @@ -110,6 +104,8 @@ export function fillInSpec(spec: OpenAPIV3.Document): void {
LocRequestController.submitLocRequest(spec);
LocRequestController.cancelLocRequest(spec);
LocRequestController.reworkLocRequest(spec);
LocRequestController.addSecret(spec);
LocRequestController.removeSecret(spec);
}

type CreateLocRequestView = components["schemas"]["CreateLocRequestView"];
Expand All @@ -131,6 +127,7 @@ type CloseView = components["schemas"]["CloseView"];
type OpenView = components["schemas"]["OpenView"];
type LocFeesView = components["schemas"]["LocFeesView"];
type CollectionParamsView = components["schemas"]["CollectionParamsView"];
type AddSecretView = components["schemas"]["AddSecretView"];

@injectable()
@Controller('/loc-request')
Expand Down Expand Up @@ -1567,4 +1564,89 @@ export class LocRequestController extends ApiController {
}
}
}

static addSecret(spec: OpenAPIV3.Document) {
const operationObject = spec.paths["/api/loc-request/{requestId}/secrets"].post!;
operationObject.summary = "Creates a new recoverable secret";
operationObject.description = "The authenticated user must be the LOC requester";
operationObject.requestBody = getRequestBody({
description: "Secret creation data",
view: "AddSecretView",
});
operationObject.responses = getDefaultResponsesNoContent();
setPathParameters(operationObject, {
'requestId': "The ID of the LOC",
});
}

@Async()
@HttpPost('/:requestId/secrets')
@SendsResponse()
async addSecret(body: AddSecretView, requestId: string) {
const authenticatedUser = await this.authenticationService.authenticatedUser(this.request);

const name = requireDefined(body.name, () => badRequest("Missing recoverable secret name"));
const value = requireDefined(body.value, () => badRequest("Missing recoverable secret value"));
const request = await this.locRequestService.update(requestId, async request => {
if(!request.isRequester(authenticatedUser.validAccountId)) {
throw unauthorized("Only requester can add recoverable secrets");
}
try {
request.addSecret(name, value);
} catch(e) {
if(e instanceof Error) {
throw badRequest(e.message);
} else {
throw e;
}
}
});

const description = request.getDescription();
const userIdentity = requireDefined(description.userIdentity);
this.getNotificationInfo(description, userIdentity)
.then(info => this.notificationService.notify(info.legalOfficerEMail, "recoverable-secret-added", {
...info.data,
secretName: name,
}))
.catch(error => logger.error(error));

this.response.sendStatus(204);
}

static removeSecret(spec: OpenAPIV3.Document) {
const operationObject = spec.paths["/api/loc-request/{requestId}/secrets/{secretName}"].delete!;
operationObject.summary = "Removes an existing recoverable secret";
operationObject.description = "The authenticated user must be the LOC requester";
operationObject.responses = getDefaultResponsesNoContent();
setPathParameters(operationObject, {
'requestId': "The ID of the LOC",
'secretName': "The secret's name"
});
}

@Async()
@HttpDelete('/:requestId/secrets/:secretName')
@SendsResponse()
async removeSecret(_body: never, requestId: string, secretName: string) {
const authenticatedUser = await this.authenticationService.authenticatedUser(this.request);

const decodedSecretName = decodeURIComponent(secretName);
await this.locRequestService.update(requestId, async request => {
if(!request.isRequester(authenticatedUser.validAccountId)) {
throw unauthorized("Only requester can add recoverable secrets");
}
try {
request.removeSecret(decodedSecretName);
} catch(e) {
if(e instanceof Error) {
throw badRequest(e.message);
} else {
throw e;
}
}
});

this.response.sendStatus(204);
}
}
16 changes: 16 additions & 0 deletions src/logion/migration/1715669632901-AddSecrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddSecrets1715669632901 implements MigrationInterface {
name = 'AddSecrets1715669632901'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "loc_secret" ("request_id" uuid NOT NULL, "name" character varying(255) NOT NULL, "value" character varying(4096) NOT NULL, CONSTRAINT "UQ_loc_secret_request_id_name" UNIQUE ("request_id", "name"), CONSTRAINT "PK_loc_secret" PRIMARY KEY ("request_id", "name"))`);
await queryRunner.query(`ALTER TABLE "loc_secret" ADD CONSTRAINT "FK_loc_secret_request_id" FOREIGN KEY ("request_id") REFERENCES "loc_request"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "loc_secret" DROP CONSTRAINT "FK_loc_secret_request_id"`);
await queryRunner.query(`DROP TABLE "loc_secret"`);
}

}
20 changes: 20 additions & 0 deletions src/logion/model/idenfy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Column } from "typeorm";

import { IdenfyVerificationStatus } from "../services/idenfy/idenfy.types";

export type EmbeddableIdenfyVerificationStatus = 'PENDING' | IdenfyVerificationStatus;

export class EmbeddableIdenfyVerification {

@Column("varchar", { name: "idenfy_auth_token", length: 40, nullable: true })
authToken?: string | null;

@Column("varchar", { name: "idenfy_scan_ref", length: 40, nullable: true })
scanRef?: string | null;

@Column("varchar", { name: "idenfy_status", length: 255, nullable: true })
status?: EmbeddableIdenfyVerificationStatus | null;

@Column("text", { name: "idenfy_callback_payload", nullable: true })
callbackPayload?: string | null;
}
48 changes: 48 additions & 0 deletions src/logion/model/loc_fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Column } from "typeorm";

import { AMOUNT_PRECISION } from "./fees.js";
import { toBigInt } from "../lib/convert.js";

export interface LocFees {
readonly valueFee?: bigint;
readonly legalFee?: bigint;
readonly collectionItemFee?: bigint;
readonly tokensRecordFee?: bigint;
}

export class EmbeddableLocFees {

@Column("numeric", { name: "value_fee", precision: AMOUNT_PRECISION, nullable: true })
valueFee?: string | null;

@Column("numeric", { name: "legal_fee", precision: AMOUNT_PRECISION, nullable: true })
legalFee?: string | null;

@Column("numeric", { name: "collection_item_fee", precision: AMOUNT_PRECISION, nullable: true })
collectionItemFee?: string | null;

@Column("numeric", { name: "tokens_record_fee", precision: AMOUNT_PRECISION, nullable: true })
tokensRecordFee?: string | null;

static from(fees: LocFees | undefined): EmbeddableLocFees | undefined {
if(fees) {
const embeddable = new EmbeddableLocFees();
embeddable.valueFee = fees.valueFee?.toString();
embeddable.legalFee = fees.legalFee?.toString();
embeddable.collectionItemFee = fees.collectionItemFee?.toString();
embeddable.tokensRecordFee = fees.tokensRecordFee?.toString();
return embeddable;
} else {
return undefined;
}
}

to(): LocFees {
return {
valueFee: toBigInt(this.valueFee),
legalFee: toBigInt(this.legalFee),
collectionItemFee: toBigInt(this.collectionItemFee),
tokensRecordFee: toBigInt(this.tokensRecordFee),
}
}
}
Loading

0 comments on commit 4ac9b3e

Please sign in to comment.