Skip to content

Commit

Permalink
Merge pull request #306 from logion-network/feature/secret-recovery
Browse files Browse the repository at this point in the history
Secret Recovery Request
  • Loading branch information
benoitdevos authored May 21, 2024
2 parents 7b39b30 + 1af444b commit cc7a3a2
Show file tree
Hide file tree
Showing 16 changed files with 580 additions and 4 deletions.
29 changes: 29 additions & 0 deletions resources/mail/secret-recovery-requested-legal-officer.pug
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
| logion notification - Secret Recovery request to be reviewed
| Dear #{legalOfficer.userIdentity.firstName} #{legalOfficer.userIdentity.lastName},
|
| 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.
|
| The following user has requested your help to recover a lost secret, please go to your logion application and start the review and approval process:
| __________________________________________________________
|
| Name:
| *******
| #{walletUser.firstName} #{walletUser.lastName}
|
| Email:
| *******
| #{walletUser.email}
|
| Telephone:
| ************
| #{walletUser.phoneNumber}
|
| Address:
| *********
| #{walletUserPostalAddress.line1}
| #{walletUserPostalAddress.line2}
| #{walletUserPostalAddress.postalCode} #{walletUserPostalAddress.city}
| #{walletUserPostalAddress.country}
| __________________________________________________________
|
include /footer.pug
12 changes: 12 additions & 0 deletions resources/mail/secret-recovery-requested-user.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 requested to recover the secret with name #{secretName} linked 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
31 changes: 31 additions & 0 deletions resources/schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -1924,6 +1924,37 @@
}
}
}
},
"CreateSecretRecoveryRequestView": {
"type": "object",
"properties": {
"requesterIdentityLocId": {
"type": "string",
"description": "The id of the identity loc",
"example": "5e4ef4bb-8657-444c-9880-d89e9403fc85"
},
"secretName": {
"type": "string",
"description": "Recoverable secret's name"
},
"challenge": {
"type": "string",
"description": "The challenge"
},
"userIdentity": {
"$ref": "#/components/schemas/UserIdentityView"
},
"userPostalAddress": {
"$ref": "#/components/schemas/PostalAddressView"
}
},
"required": [
"requesterIdentityLocId",
"secretName",
"challenge",
"userIdentity",
"userPostalAddress"
]
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/logion/app.support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { fillInSpec as fillInSpecIdenfy, IdenfyController } from "./controllers/
import { fillInSpec as fillInSpecVote, VoteController } from "./controllers/vote.controller.js";
import { fillInSpec as fillInSpecTokensRecord, TokensRecordController } from "./controllers/records.controller.js";
import { fillInSpec as fillInSpecWorkload, WorkloadController } from "./controllers/workload.controller.js";
import { fillInSpec as fillInSpecSecretRecovery, SecretRecoveryController } from "./controllers/secret_recovery.controller.js";

const { logger } = Log;

Expand Down Expand Up @@ -62,6 +63,7 @@ export function predefinedSpec(spec: OpenAPIV3.Document): OpenAPIV3.Document {
fillInSpecVote(spec);
fillInSpecTokensRecord(spec);
fillInSpecWorkload(spec);
fillInSpecSecretRecovery(spec);

return spec;
}
Expand All @@ -81,7 +83,7 @@ export function buildExpress(expressConfig?: ExpressConfig): Express {
predefinedSpec,
specOutputFileBehavior: SPEC_OUTPUT_FILE_BEHAVIOR.RECREATE,
swaggerDocumentOptions: {

},
alwaysServeDocs: true,
});
Expand Down Expand Up @@ -124,6 +126,7 @@ export function setupApp(expressConfig?: ExpressConfig): Express {
dino.registerController(VoteController);
dino.registerController(TokensRecordController);
dino.registerController(WorkloadController);
dino.registerController(SecretRecoveryController);

dino.dependencyResolver<Container>(AppContainer,
(injector, type) => {
Expand Down
6 changes: 6 additions & 0 deletions src/logion/container/app.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ import { MultiversxService } from "../services/ownership/multiversx.service.js";
import { AstarService, ConnectedAstarService } from "../services/ownership/astar.service.js";
import { WorkloadService } from "../services/workload.service.js";
import { WorkloadController } from "../controllers/workload.controller.js";
import { SecretRecoveryController } from "../controllers/secret_recovery.controller.js";
import { SecretRecoveryRequestRepository } from "../model/secret_recovery.model.js";
import { SecretRecoveryRequestService } from "../services/secret_recovery.service.js";

const container = new Container({ defaultScope: "Singleton", skipBaseClassChecks: true });
configureContainer(container);
Expand Down Expand Up @@ -151,6 +154,8 @@ container.bind(SponsorshipService).toSelf();
container.bind(ConnectedAstarService).toSelf();
container.bind(AstarService).toService(ConnectedAstarService);
container.bind(WorkloadService).toSelf();
container.bind(SecretRecoveryRequestRepository).toSelf();
container.bind(SecretRecoveryRequestService).toSelf();

// Controllers are stateful so they must not be injected with singleton scope
container.bind(LocRequestController).toSelf().inTransientScope();
Expand All @@ -164,5 +169,6 @@ container.bind(IdenfyController).toSelf().inTransientScope();
container.bind(VoteController).toSelf().inTransientScope();
container.bind(TokensRecordController).toSelf().inTransientScope();
container.bind(WorkloadController).toSelf().inTransientScope();
container.bind(SecretRecoveryController).toSelf().inTransientScope();

export { container as AppContainer };
13 changes: 13 additions & 0 deletions src/logion/controllers/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,19 @@ export interface components {
[key: string]: number;
};
};
CreateSecretRecoveryRequestView: {
/**
* @description The id of the identity loc
* @example 5e4ef4bb-8657-444c-9880-d89e9403fc85
*/
requesterIdentityLocId: string;
/** @description Recoverable secret's name */
secretName: string;
/** @description The challenge */
challenge: string;
userIdentity: components["schemas"]["UserIdentityView"];
userPostalAddress: components["schemas"]["PostalAddressView"];
};
};
responses: never;
parameters: never;
Expand Down
138 changes: 138 additions & 0 deletions src/logion/controllers/secret_recovery.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { OpenAPIV3 } from "express-oas-generator";
import {
addTag,
setControllerTag,
requireDefined,
badRequest,
getRequestBody,
getDefaultResponses,
Log
} from "@logion/rest-api-core";
import { injectable } from "inversify";
import { Controller, HttpPost, ApiController, Async, SendsResponse } from "dinoloop";
import { components } from "./components.js";
import { SecretRecoveryRequestFactory, SecretRecoveryRequestDescription } from "../model/secret_recovery.model.js";
import { SecretRecoveryRequestService } from "../services/secret_recovery.service.js";
import { LocRequestRepository } from "../model/locrequest.model.js";
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 { ValidAccountId } from "@logion/node-api";

type CreateSecretRecoveryRequestView = components["schemas"]["CreateSecretRecoveryRequestView"];

const { logger } = Log;

export function fillInSpec(spec: OpenAPIV3.Document): void {
const tagName = 'SecretRecovery';
addTag(spec, {
name: tagName,
description: "Handling of Secret Recovery Requests"
});
setControllerTag(spec, /^\/api\/secret-recovery.*/, tagName);

SecretRecoveryController.createSecretRecoveryRequest(spec);
}

@injectable()
@Controller('/secret-recovery')
export class SecretRecoveryController extends ApiController {

constructor(
private secretRecoveryRequestFactory: SecretRecoveryRequestFactory,
private secretRecoveryRequestService: SecretRecoveryRequestService,
private locRequestRepository: LocRequestRepository,
private directoryService: DirectoryService,
private notificationService: NotificationService,
) {
super();
}

static createSecretRecoveryRequest(spec: OpenAPIV3.Document) {
const operationObject = spec.paths["/api/secret-recovery"].post!;
operationObject.summary = "Creates a new Secret recovery request";
operationObject.description = "This is publicly available";
operationObject.requestBody = getRequestBody({
description: "Secret recovery request creation data",
view: "secret-recovery",
});
operationObject.responses = getDefaultResponses("ProtectionRequestView");
}

@Async()
@HttpPost('')
@SendsResponse()
async createSecretRecoveryRequest(body: CreateSecretRecoveryRequestView) {
const { requesterIdentityLocId, challenge, secretName } = body;
const requesterIdentityLoc = requireDefined(
await this.locRequestRepository.findById(requesterIdentityLocId),
() => badRequest("Identity LOC not found")
);
requireDefined(
requesterIdentityLoc.secrets?.find(secret => secret.name === secretName),
() => badRequest("Secret not found")
)
const userIdentity = {
firstName: body.userIdentity.firstName || "",
lastName: body.userIdentity.lastName || "",
email: body.userIdentity.email || "",
phoneNumber: body.userIdentity.phoneNumber || "",
};
const userPostalAddress = {
line1: body.userPostalAddress.line1 || "",
line2: body.userPostalAddress.line2 || "",
postalCode: body.userPostalAddress.postalCode || "",
city: body.userPostalAddress.city || "",
country: body.userPostalAddress.country || "",
};
const recoveryRequest = this.secretRecoveryRequestFactory.newSecretRecoveryRequest({
requesterIdentityLocId,
legalOfficerAddress: requesterIdentityLoc.getOwner(),
challenge,
secretName,
userIdentity,
userPostalAddress,
createdOn: moment()
})
await this.secretRecoveryRequestService.add(recoveryRequest);
const userPrivateData: UserPrivateData = {
identityLocId: requesterIdentityLocId,
userIdentity,
userPostalAddress,
}
this.notify("WalletUser", "secret-recovery-requested-user", recoveryRequest.getDescription(), requesterIdentityLoc.getOwner(), userPrivateData);
this.notify("LegalOfficer", "secret-recovery-requested-legal-officer", recoveryRequest.getDescription(), requesterIdentityLoc.getOwner(), userPrivateData);
this.response.sendStatus(204);
}

private notify(recipient: NotificationRecipient, templateId: Template, secretRecoveryRequest: SecretRecoveryRequestDescription, legalOfficerAccount: ValidAccountId, userPrivateData: UserPrivateData): void {
this.getNotificationInfo(secretRecoveryRequest, legalOfficerAccount, userPrivateData)
.then(info => {
const to = recipient === "WalletUser" ? info.userEmail : info.legalOfficerEMail
return this.notificationService.notify(to, templateId, info.data)
.catch(reason => logger.warn("Failed to send email '%s' to %s : %s", templateId, to, reason))
})
.catch(reason =>
logger.warn("Failed to retrieve notification info from directory: %s. Mail '%' not sent.", reason, templateId)
)
}

private async getNotificationInfo(secretRecoveryRequest: SecretRecoveryRequestDescription, legalOfficerAccount: ValidAccountId, userPrivateData: UserPrivateData):
Promise<{ legalOfficerEMail: string, userEmail: string | undefined, data: LocalsObject }> {

const legalOfficer = await this.directoryService.get(legalOfficerAccount)
const { userIdentity, userPostalAddress } = userPrivateData;
return {
legalOfficerEMail: legalOfficer.userIdentity.email,
userEmail: userIdentity?.email,
data: {
secretName: secretRecoveryRequest.secretName,
legalOfficer,
walletUser: userIdentity,
walletUserPostalAddress: userPostalAddress
}
}
}
}
2 changes: 1 addition & 1 deletion src/logion/migration/1715669632901-AddSecrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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(`CREATE TABLE "loc_secret" ("request_id" uuid NOT NULL, "name" character varying(255) NOT NULL, "value" character varying(4096) NOT NULL, 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`);
}

Expand Down
14 changes: 14 additions & 0 deletions src/logion/migration/1715933228153-AddSecretRecovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddSecretRecovery1715933228153 implements MigrationInterface {
name = 'AddSecretRecovery1715933228153'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "secret_recovery_request" ("id" uuid NOT NULL, "requester_identity_loc_id" uuid NOT NULL, "legal_officer_address" character varying(255) NOT NULL, "secret_name" character varying(255) NOT NULL, "challenge" character varying(255) NOT NULL, "created_on" TIMESTAMP NOT NULL, "first_name" character varying(255), "last_name" character varying(255), "email" character varying(255), "phone_number" character varying(255), "line1" character varying(255), "line2" character varying(255), "postal_code" character varying(255), "city" character varying(255), "country" character varying(255), CONSTRAINT "PK_secret_recovery_request" PRIMARY KEY ("id"))`);
}

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

}
1 change: 0 additions & 1 deletion src/logion/model/locrequest.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1423,7 +1423,6 @@ export class LocLink extends Child implements HasIndex, Submitted {
}

@Entity("loc_secret")
@Unique([ "requestId", "name" ])
export class RecoverableSecretEntity extends Child {

@PrimaryColumn({ type: "uuid", name: "request_id" })
Expand Down
Loading

0 comments on commit cc7a3a2

Please sign in to comment.