Files
edr-platform/apps/edr-freight-api/src/modules/auth/customer-reset.controller.ts
Nathnael 3af3017b86 feat(backoffice): add an Account tab with the customer's portal logins
The detail page showed the company's business contact details but not the
credentials anyone actually signs in with, and the two drift apart
routinely — so "the customer says they can't log in" was unanswerable
from this screen.

Adds `GET /backoffice/customers/:companyId/accounts`, joining each
external profile to its IAM account, primary contact first. Deliberately
not filtered to active accounts: a suspended or never-activated login is
exactly the case being looked into. The user query selects columns
explicitly — the entity's relations include credentials and sessions, and
this response reaches a browser.

Rendered as cards rather than a table: it is a handful of rows of
mostly-optional fields, which a table renders as a field of dashes.
"Password never set" is called out on its own, being the usual answer to
"they never got in", and a profile whose IAM user is gone reads as a red
fault rather than an inactive status.
2026-08-27 09:30:24 +00:00

91 lines
2.5 KiB
TypeScript

import {
Body,
Controller,
Get,
NotFoundException,
Param,
ParseUUIDPipe,
Post,
} from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import { BookingStaff } from "../../common/booking-guards";
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
import { BackofficeResetPasswordDto } from "./dto/forgot-password.dto";
import {
CustomerAccount,
CustomerAccountsService,
} from "./customer-accounts.service";
import {
CustomerResetService,
CustomerResetTarget,
} from "./customer-reset.service";
/**
* Staff-triggered password reset. The customer receives a single-use link and
* sets their own password — staff never see or handle a credential.
*/
@ApiTags("backoffice")
@Controller("backoffice/customers")
@ApiBearerAuth()
export class CustomerResetController {
constructor(
private readonly customerResetService: CustomerResetService,
private readonly customerAccountsService: CustomerAccountsService,
) {}
@Get(":companyId/accounts")
@BookingStaff(FREIGHT_PERMS.customers.view)
@ApiOperation({
summary:
"The portal login accounts belonging to a customer, primary contact first",
})
async accounts(
@Param("companyId", ParseUUIDPipe) companyId: string,
): Promise<CustomerAccount[]> {
return this.customerAccountsService.listForCompany(companyId);
}
@Get(":companyId/reset-target")
@BookingStaff(FREIGHT_PERMS.customers.resetPassword)
@ApiOperation({
summary: "The primary contact's IAM account a reset link would be sent to",
})
async resetTarget(
@Param("companyId", ParseUUIDPipe) companyId: string,
): Promise<CustomerResetTarget> {
const target = await this.customerResetService.getResetTarget(companyId);
if (!target) {
throw new NotFoundException(
"This customer has no active primary-contact account to reset",
);
}
return target;
}
@Post(":companyId/reset-password")
@BookingStaff(FREIGHT_PERMS.customers.resetPassword)
@ApiOperation({
summary: "Send a password-reset link to a customer's primary contact",
})
async resetPassword(
@Param("companyId", ParseUUIDPipe) companyId: string,
@Body() dto: BackofficeResetPasswordDto,
) {
const sent = await this.customerResetService.sendResetLinkToCustomer(
companyId,
dto.channel,
);
if (!sent) {
throw new NotFoundException(
`No active primary contact with ${dto.channel === "email" ? "an email address" : "a phone number"} for this customer`,
);
}
return sent;
}
}