mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 09:30:59 +00:00
feat: enhance booking management with shipping line support and cargo handling improvements
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { ExternalProfile } from "../companies/entities/external-profile.entity";
|
||||
@@ -11,9 +10,10 @@ import { ResetChannel } from "./dto/forgot-password.dto";
|
||||
import {
|
||||
ForgotPasswordService,
|
||||
RESET_LINK_TTL_MS,
|
||||
type ResetTicket,
|
||||
} from "./forgot-password.service";
|
||||
import { maskOtpTarget } from "./mask-target.util";
|
||||
import { isDomesticPhone } from "../otp/otp.service";
|
||||
import { isDomesticPhone, type OtpTarget } from "../otp/otp.service";
|
||||
|
||||
/** The account a staff-triggered reset would land on. */
|
||||
export interface CustomerResetTarget {
|
||||
@@ -116,6 +116,22 @@ export class CustomerResetService {
|
||||
channel: ResetChannel,
|
||||
options?: { scope?: string; allowWithoutCredential?: boolean },
|
||||
): Promise<SentResetLink | null> {
|
||||
const sent = await this.sendResetLinkToUserOnChannels(userId, [channel], options);
|
||||
return sent[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* One ticket, several channels. Minting retires every earlier ticket for the
|
||||
* user (`mintResetTicket`), so sending email and SMS as two separate mints
|
||||
* makes the first link dead on arrival — the same link must go to both.
|
||||
* Returns one entry per channel that was actually sent (unreachable channels
|
||||
* are skipped, not errors).
|
||||
*/
|
||||
async sendResetLinkToUserOnChannels(
|
||||
userId: string,
|
||||
channels: ResetChannel[],
|
||||
options?: { scope?: string; allowWithoutCredential?: boolean },
|
||||
): Promise<SentResetLink[]> {
|
||||
const user = options?.allowWithoutCredential
|
||||
? await this.forgotPasswordService.resolveActivatableUserById(userId)
|
||||
: await this.forgotPasswordService.resolveActiveUserById(userId);
|
||||
@@ -128,51 +144,58 @@ export class CustomerResetService {
|
||||
: " (or has no active credential — pass allowWithoutCredential for first-time activation)"
|
||||
}`,
|
||||
);
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.deliverResetLink(user, user.id, channel, options?.scope);
|
||||
// Mint once, before any send: a failed send leaves an unused ticket that
|
||||
// simply expires, whereas sending a link before the ticket exists would
|
||||
// hand the customer a URL that is dead on arrival.
|
||||
let ticket: ResetTicket | null = null;
|
||||
const sent: SentResetLink[] = [];
|
||||
for (const channel of channels) {
|
||||
const target = this.forgotPasswordService.targetFor(user, channel);
|
||||
if (!target) continue;
|
||||
// The gateway silently drops foreign numbers — treat like a missing phone
|
||||
// rather than reporting "link sent" for a message that will never arrive.
|
||||
// The backoffice disables the channel up front via `phoneIsDomestic`; this
|
||||
// guards direct API calls.
|
||||
if (channel === "phone" && target.phone && !isDomesticPhone(target.phone)) {
|
||||
this.logger.warn(
|
||||
`Staff reset via SMS refused for user ${userId} — non-domestic phone`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
ticket ??= await this.forgotPasswordService.mintResetTicket(
|
||||
user.id,
|
||||
RESET_LINK_TTL_MS,
|
||||
);
|
||||
const result = await this.deliverResetLink(
|
||||
target,
|
||||
user.id,
|
||||
channel,
|
||||
ticket,
|
||||
options?.scope,
|
||||
);
|
||||
if (result) sent.push(result);
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared tail: target selection → SMS reachability → mint → send → report.
|
||||
* Callers have already resolved `user` to an active account.
|
||||
* Shared tail: send the already-minted ticket to a resolved target → report.
|
||||
*/
|
||||
private async deliverResetLink(
|
||||
user: User,
|
||||
target: OtpTarget,
|
||||
userId: string,
|
||||
channel: ResetChannel,
|
||||
ticket: ResetTicket,
|
||||
scope?: string,
|
||||
): Promise<SentResetLink | null> {
|
||||
this.logger.log(
|
||||
`Staff-triggered shipping line ${"link"}`,
|
||||
);
|
||||
const target = this.forgotPasswordService.targetFor(user, channel);
|
||||
if (!target) return null;
|
||||
|
||||
// A foreign number is unreachable by the domestic-only SMS gateway — treat
|
||||
// it like a missing phone rather than reporting "link sent" for a message
|
||||
// that will never arrive. The backoffice disables the channel up front via
|
||||
// `phoneIsDomestic`; this guards direct API calls.
|
||||
if (channel === "phone" && target.phone && !isDomesticPhone(target.phone)) {
|
||||
this.logger.warn(
|
||||
`Staff reset via SMS refused for user ${userId} — non-domestic phone`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mint first, send second: a failed send leaves an unused ticket that simply
|
||||
// expires, whereas sending a link before the ticket exists would hand the
|
||||
// customer a URL that is dead on arrival.
|
||||
const ticket = await this.forgotPasswordService.mintResetTicket(
|
||||
userId,
|
||||
RESET_LINK_TTL_MS,
|
||||
);
|
||||
const link = this.buildResetLink(ticket.userId, ticket.verificationCode);
|
||||
const expiresAt = new Date(Date.now() + RESET_LINK_TTL_MS);
|
||||
this.logger.log(
|
||||
`Staff-triggered shipping line ${link}`,
|
||||
);
|
||||
|
||||
const { queued } = target.email
|
||||
? await this.emailClient.sendEmail({
|
||||
|
||||
@@ -212,7 +212,9 @@ export class ForgotPasswordService {
|
||||
* is the proof).
|
||||
*/
|
||||
async mintResetTicket(userId: string, ttlMs: number): Promise<ResetTicket> {
|
||||
const code = randomBytes(24).toString("base64url");
|
||||
// Hex, not base64url: the token rides in an SMS, and the GSM-7 alphabet has
|
||||
// no "_" — gateways substitute a space and the link arrives broken.
|
||||
const code = randomBytes(24).toString("hex");
|
||||
const verificationCode = await hashPassword(code);
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
|
||||
Reference in New Issue
Block a user