mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 03:10:54 +00:00
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.
91 lines
2.5 KiB
TypeScript
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;
|
|
}
|
|
}
|