Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Commit

Permalink
feat(guardian-signature): implement public hook for guardian signatur…
Browse files Browse the repository at this point in the history
…es (#538)

* feat(guardian-signature): implement public hook for guardian signatures

* feat(guardian-signature): add endpoint for checking guardian signature status

* refactor(signature): improve func name

* fix(guardian-signature): send email request for signature and fix types

* fix: lint and old test

* refactor(signature): slight naming

* feat(guardian-signature): use SendGrid email templates

* fix(guardian-signature): remove old email sendout

* fix(guardian-signature): simplify type check

* fix(guardian-signature): even more simplifed types
  • Loading branch information
AdrianAndersen authored Aug 13, 2024
1 parent 82e09e2 commit 0383e6c
Show file tree
Hide file tree
Showing 14 changed files with 303 additions and 167 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"license": "ISC",
"dependencies": {
"@boklisten/bl-email": "^1.8.2",
"@boklisten/bl-model": "^0.26.2",
"@boklisten/bl-model": "^0.26.5",
"@boklisten/bl-post-office": "^0.5.56",
"@napi-rs/image": "^1.8.0",
"@sendgrid/mail": "^8.1.3",
Expand Down
4 changes: 4 additions & 0 deletions src/bl-error/bl-error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ export class BlErrorHandler {
" selv";
blapiErrorResponse.httpStatus = 409;
break;
case 813:
blapiErrorResponse.msg = "Forsørger har allerede signert";
blapiErrorResponse.httpStatus = 409;
break;
}

return blapiErrorResponse;
Expand Down
64 changes: 56 additions & 8 deletions src/collections/signature/helpers/signature.helper.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
BlError,
Order,
SerializedSignature,
SignatureMetadata,
UserDetail,
} from "@boklisten/bl-model";
import { Transformer } from "@napi-rs/image";

import { SystemUser } from "../../../auth/permission/permission.service";
import { logger } from "../../../logger/logger";
import { BlDocumentStorage } from "../../../storage/blDocumentStorage";
import { Signature } from "../signature.schema";

Expand All @@ -18,20 +21,24 @@ export function serializeSignature(signature: Signature): SerializedSignature {
...rest,
};
}

export async function deserializeSignature(
serializedSignature: SerializedSignature,
): Promise<Signature> {
const { base64EncodedImage, ...rest } = serializedSignature;

export async function deserializeBase64EncodedImage(
base64EncodedImage: string,
) {
if (!isValidBase64(base64EncodedImage)) {
throw new BlError("Invalid base64").code(701);
}
const image = await new Transformer(Buffer.from(base64EncodedImage, "base64"))
return await new Transformer(Buffer.from(base64EncodedImage, "base64"))
.webp(qualityFactor)
.catch((e) => {
throw new BlError(`Unable to transform to WebP`).code(701).add(e);
});
}

export async function deserializeSignature(
serializedSignature: SerializedSignature,
): Promise<Signature> {
const { base64EncodedImage, ...rest } = serializedSignature;
const image = await deserializeBase64EncodedImage(base64EncodedImage);

return { image, ...rest };
}
Expand All @@ -40,7 +47,8 @@ export async function getValidUserSignature(
userDetail: UserDetail,
signatureStorage: BlDocumentStorage<Signature>,
): Promise<Signature | null> {
const newestSignatureId = userDetail.signatures[0];
const newestSignatureId =
userDetail.signatures[userDetail.signatures.length - 1];
if (newestSignatureId == undefined) return null;

const signature = await signatureStorage.get(newestSignatureId);
Expand Down Expand Up @@ -94,3 +102,43 @@ export function isSignatureExpired(signature: SignatureMetadata): boolean {
function isValidBase64(input: string): boolean {
return Buffer.from(input, "base64").toString("base64") === input;
}

export async function signOrders(
orderStorage: BlDocumentStorage<Order>,
userDetail: UserDetail,
) {
if (!(userDetail.orders && userDetail.orders.length > 0)) {
return;
}
const orders = await orderStorage.getMany(userDetail.orders);
await Promise.all(
orders
.filter((order) => order.pendingSignature)
.map(async (order) => {
return await orderStorage
.update(order.id, { pendingSignature: false }, new SystemUser())
.catch((e) =>
logger.error(
`While processing new signature, unable to update order ${order.id}: ${e}`,
),
);
}),
);
}

export async function isGuardianSignatureRequired(
userDetail: UserDetail,
signatureStorage: BlDocumentStorage<Signature>,
) {
if (!isUnderage(userDetail)) {
return false;
}

const latestValidSignature = await getValidUserSignature(
userDetail,
signatureStorage,
);
return (
latestValidSignature === null || !latestValidSignature.signedByGuardian
);
}
31 changes: 5 additions & 26 deletions src/collections/signature/hooks/signature.post.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import {
UserDetail,
} from "@boklisten/bl-model";

import { SystemUser } from "../../../auth/permission/permission.service";
import { Hook } from "../../../hook/hook";
import { logger } from "../../../logger/logger";
import { BlDocumentStorage } from "../../../storage/blDocumentStorage";
import { BlCollectionName } from "../../bl-collection";
import { orderSchema } from "../../order/order.schema";
Expand All @@ -17,6 +15,7 @@ import {
deserializeSignature,
isUnderage,
serializeSignature,
signOrders,
} from "../helpers/signature.helper";
import { Signature } from "../signature.schema";

Expand Down Expand Up @@ -45,7 +44,7 @@ export class SignaturePostHook extends Hook {
accessToken: AccessToken,
): Promise<Signature> {
const serializedSignature = body;
if (!validateSerialiedSignature(serializedSignature))
if (!validateSerializedSignature(serializedSignature))
throw new BlError("Bad serialized signature").code(701);

const userDetail = await this.userDetailStorage.get(accessToken.details);
Expand All @@ -71,37 +70,17 @@ export class SignaturePostHook extends Hook {
const userDetail = await this.userDetailStorage.get(accessToken.details);
await this.userDetailStorage.update(
userDetail.id,
{ signatures: [writtenSignature.id, ...userDetail.signatures] },
{ signatures: [...userDetail.signatures, writtenSignature.id] },
{ id: accessToken.details, permission: accessToken.permission },
);

await this.signOrders(userDetail);
await signOrders(this.orderStorage, userDetail);

return [serializeSignature(writtenSignature)];
}

private async signOrders(userDetail: UserDetail) {
if (!(userDetail.orders && userDetail.orders.length > 0)) {
return;
}
const orders = await this.orderStorage.getMany(userDetail.orders);
await Promise.all(
orders
.filter((order) => order.pendingSignature)
.map(async (order) => {
return await this.orderStorage
.update(order.id, { pendingSignature: false }, new SystemUser())
.catch((e) =>
logger.error(
`While processing new signature, unable to update order ${order.id}: ${e}`,
),
);
}),
);
}
}

function validateSerialiedSignature(
function validateSerializedSignature(
serializedSignature: unknown,
): serializedSignature is SerializedSignature {
const s = serializedSignature as Partial<SerializedSignature>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { BlapiResponse, BlError, UserDetail } from "@boklisten/bl-model";
import { CheckGuardianSignatureSpec } from "@boklisten/bl-model/signature/serialized-signature";
import { ObjectId } from "mongodb";

import { Operation } from "../../../operation/operation";
import { BlApiRequest } from "../../../request/bl-api-request";
import { BlDocumentStorage } from "../../../storage/blDocumentStorage";
import { BlCollectionName } from "../../bl-collection";
import { userDetailSchema } from "../../user-detail/user-detail.schema";
import {
getValidUserSignature,
isGuardianSignatureRequired,
isUnderage,
} from "../helpers/signature.helper";
import { Signature, signatureSchema } from "../signature.schema";

export class CheckGuardianSignatureOperation implements Operation {
private readonly _userDetailStorage: BlDocumentStorage<UserDetail>;
private readonly _signatureStorage: BlDocumentStorage<Signature>;

constructor(
signatureStorage?: BlDocumentStorage<Signature>,
userDetailStorage?: BlDocumentStorage<UserDetail>,
) {
this._signatureStorage =
signatureStorage ??
new BlDocumentStorage(BlCollectionName.Signatures, signatureSchema);
this._userDetailStorage =
userDetailStorage ??
new BlDocumentStorage(BlCollectionName.UserDetails, userDetailSchema);
}

async run(blApiRequest: BlApiRequest): Promise<BlapiResponse> {
const serializedGuardianSignature = blApiRequest.data;
if (!validateSerializedGuardianSignature(serializedGuardianSignature))
throw new BlError("Bad serialized guardian signature").code(701);

const userDetail = await this._userDetailStorage.get(
serializedGuardianSignature.customerId,
);

if (!isUnderage(userDetail)) {
return new BlapiResponse([
{
message: `${userDetail.name} er myndig, og trenger derfor ikke signatur fra foreldre.`,
guardianSignatureRequired: false,
},
]);
}

if (
!(await isGuardianSignatureRequired(userDetail, this._signatureStorage))
) {
const signature = await getValidUserSignature(
userDetail,
this._signatureStorage,
);
return new BlapiResponse([
{
message: `${signature?.signingName} har allerede signert på vegne av ${userDetail.name}`,
guardianSignatureRequired: false,
},
]);
}

return new BlapiResponse([
{ customerName: userDetail.name, guardianSignatureRequired: true },
]);
}
}

function validateSerializedGuardianSignature(
serializedGuardianSignature: unknown,
): serializedGuardianSignature is CheckGuardianSignatureSpec {
const s = serializedGuardianSignature as Partial<CheckGuardianSignatureSpec>;
return (
s != null &&
typeof s.customerId === "string" &&
ObjectId.isValid(s.customerId)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { BlapiResponse, BlError, Order, UserDetail } from "@boklisten/bl-model";
import { SerializedGuardianSignature } from "@boklisten/bl-model/signature/serialized-signature";
import { ObjectId } from "mongodb";

import { SystemUser } from "../../../auth/permission/permission.service";
import { Operation } from "../../../operation/operation";
import { BlApiRequest } from "../../../request/bl-api-request";
import { BlDocumentStorage } from "../../../storage/blDocumentStorage";
import { BlCollectionName } from "../../bl-collection";
import { orderSchema } from "../../order/order.schema";
import { userDetailSchema } from "../../user-detail/user-detail.schema";
import {
deserializeBase64EncodedImage,
isGuardianSignatureRequired,
serializeSignature,
signOrders,
} from "../helpers/signature.helper";
import { Signature, signatureSchema } from "../signature.schema";

export class GuardianSignatureOperation implements Operation {
private readonly _userDetailStorage: BlDocumentStorage<UserDetail>;
private readonly _orderStorage: BlDocumentStorage<Order>;
private readonly _signatureStorage: BlDocumentStorage<Signature>;

constructor(
signatureStorage?: BlDocumentStorage<Signature>,
orderStorage?: BlDocumentStorage<Order>,
userDetailStorage?: BlDocumentStorage<UserDetail>,
) {
this._signatureStorage =
signatureStorage ??
new BlDocumentStorage(BlCollectionName.Signatures, signatureSchema);
this._orderStorage =
orderStorage ??
new BlDocumentStorage(BlCollectionName.Orders, orderSchema);
this._userDetailStorage =
userDetailStorage ??
new BlDocumentStorage(BlCollectionName.UserDetails, userDetailSchema);
}

async run(blApiRequest: BlApiRequest): Promise<BlapiResponse> {
const serializedGuardianSignature = blApiRequest.data;
if (!validateSerializedGuardianSignature(serializedGuardianSignature))
throw new BlError("Bad serialized guardian signature").code(701);

const userDetail = await this._userDetailStorage.get(
serializedGuardianSignature.customerId,
);

if (
!(await isGuardianSignatureRequired(userDetail, this._signatureStorage))
) {
throw new BlError(
"Valid guardian signature is already present or not needed.",
).code(813);
}

const signatureImage = await deserializeBase64EncodedImage(
serializedGuardianSignature.base64EncodedImage,
);

const writtenSignature = await this._signatureStorage.add(
{
// @ts-expect-error id will be auto-generated
id: null,
image: signatureImage,
signedByGuardian: true,
signingName: serializedGuardianSignature.signingName,
},
new SystemUser(),
);

await this._userDetailStorage.update(
userDetail.id,
{ signatures: [...userDetail.signatures, writtenSignature.id] },
new SystemUser(),
);

await signOrders(this._orderStorage, userDetail);

return new BlapiResponse([serializeSignature(writtenSignature)]);
}
}

function validateSerializedGuardianSignature(
serializedGuardianSignature: unknown,
): serializedGuardianSignature is SerializedGuardianSignature {
const s = serializedGuardianSignature as Partial<SerializedGuardianSignature>;
return (
s != null &&
typeof s.customerId === "string" &&
ObjectId.isValid(s.customerId) &&
typeof s.base64EncodedImage === "string" &&
typeof s.signingName === "string"
);
}
12 changes: 12 additions & 0 deletions src/collections/signature/signature.collection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SignatureGetIdHook } from "./hooks/signature.get-id.hook";
import { SignaturePostHook } from "./hooks/signature.post.hook";
import { CheckGuardianSignatureOperation } from "./operations/check-guardian-signature.operation";
import { GuardianSignatureOperation } from "./operations/guardian-signature.operation";
import { signatureSchema } from "./signature.schema";
import {
BlCollection,
Expand All @@ -23,6 +25,16 @@ export class SignatureCollection implements BlCollection {
restricted: false,
},
hook: new SignaturePostHook(),
operations: [
{
name: "guardian",
operation: new GuardianSignatureOperation(),
},
{
name: "check-guardian-signature",
operation: new CheckGuardianSignatureOperation(),
},
],
},
{
method: "getId",
Expand Down
Loading

0 comments on commit 0383e6c

Please sign in to comment.