feat(auth): implement staff-triggered password-reset links

This commit is contained in:
Nathnael
2026-07-20 11:20:50 +00:00
parent bed1208dee
commit 6420c72e89
21 changed files with 688 additions and 73 deletions

View File

@@ -1,6 +1,7 @@
import {
Body,
Controller,
Get,
NotFoundException,
Param,
ParseUUIDPipe,
@@ -11,11 +12,14 @@ 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 { CustomerResetService } from "./customer-reset.service";
import {
CustomerResetService,
CustomerResetTarget,
} from "./customer-reset.service";
/**
* Staff-triggered password reset. The customer receives the code and sets their
* own password — staff never see or handle a credential.
* 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")
@@ -23,26 +27,45 @@ import { CustomerResetService } from "./customer-reset.service";
export class CustomerResetController {
constructor(private readonly customerResetService: CustomerResetService) {}
@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 code to a customer's primary contact",
summary: "Send a password-reset link to a customer's primary contact",
})
async resetPassword(
@Param("companyId", ParseUUIDPipe) companyId: string,
@Body() dto: BackofficeResetPasswordDto,
) {
const maskedTarget = await this.customerResetService.sendResetToCustomer(
const sent = await this.customerResetService.sendResetLinkToCustomer(
companyId,
dto.channel,
);
if (!maskedTarget) {
if (!sent) {
throw new NotFoundException(
`No active primary contact with ${dto.channel === "email" ? "an email address" : "a phone number"} for this customer`,
);
}
return { channel: dto.channel, maskedTarget };
return sent;
}
}