mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 05:30:55 +00:00
85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import { Body, Controller, Logger, Post } from "@nestjs/common";
|
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
|
import { Public } from "@edr/api-common";
|
|
|
|
import {
|
|
ForgotPasswordRequestDto,
|
|
ForgotPasswordVerifyDto,
|
|
ResolveResetLinkDto,
|
|
} from "./dto/forgot-password.dto";
|
|
import {
|
|
ForgotPasswordService,
|
|
ResetLinkAccount,
|
|
ResetTicket,
|
|
} from "./forgot-password.service";
|
|
|
|
/**
|
|
* Freight-owned reset flow. IAM ships a `forgot-password` route, but it only
|
|
* ever SMSes a magic link (no email channel, and it needs `FE_BASE_URL`, which
|
|
* this API does not set). These routes drive freight's own email-or-phone OTP
|
|
* service instead, then hand back a ticket for IAM's public `set-password`.
|
|
*/
|
|
@ApiTags("auth")
|
|
@Controller("auth")
|
|
@Public()
|
|
export class ForgotPasswordController {
|
|
private readonly logger = new Logger(ForgotPasswordController.name);
|
|
|
|
constructor(private readonly forgotPasswordService: ForgotPasswordService) {}
|
|
|
|
@Post("forgot-password/request")
|
|
@ApiOperation({
|
|
summary: "Send a password-reset code to the account's email AND phone",
|
|
description:
|
|
"One code, delivered over every contact the account has; either delivery " +
|
|
"verifies it. Always reports success — an unknown, inactive, or contactless " +
|
|
"account is indistinguishable from a real one, so this cannot be used to " +
|
|
"enumerate accounts.",
|
|
})
|
|
async request(@Body() dto: ForgotPasswordRequestDto): Promise<{ success: true }> {
|
|
const user = await this.forgotPasswordService.resolveActiveUser(dto.identifier);
|
|
|
|
if (user) {
|
|
try {
|
|
await this.forgotPasswordService.requestReset(user);
|
|
} catch (error) {
|
|
// A delivery failure must not change the response shape either — log it
|
|
// and let the caller sit on the OTP screen.
|
|
this.logger.error(
|
|
`Reset code delivery failed for user ${user.id}: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}`,
|
|
error instanceof Error ? error.stack : undefined,
|
|
);
|
|
}
|
|
} else {
|
|
this.logger.log("Reset requested for an unknown or inactive account");
|
|
}
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
@Post("forgot-password/verify")
|
|
@ApiOperation({
|
|
summary: "Exchange a valid reset code for a single-use set-password ticket",
|
|
description:
|
|
"The returned { userId, verificationCode } is the body for PATCH /api/auth/set-password, " +
|
|
"alongside the same identifier and the new password.",
|
|
})
|
|
verify(@Body() dto: ForgotPasswordVerifyDto): Promise<ResetTicket> {
|
|
return this.forgotPasswordService.verifyAndMintTicket(dto.identifier, dto.otp);
|
|
}
|
|
|
|
@Post("forgot-password/resolve-link")
|
|
@ApiOperation({
|
|
summary: "Validate a staff-issued reset link and return its set-password ticket",
|
|
description:
|
|
"Takes the link's uid/token pair. The returned { userId, identifier, verificationCode } " +
|
|
"is the body for PATCH /api/auth/set-password, so the customer never types an identifier. " +
|
|
"A bad or expired link is rejected here rather than after the password is typed.",
|
|
})
|
|
resolveLink(@Body() dto: ResolveResetLinkDto): Promise<ResetLinkAccount> {
|
|
return this.forgotPasswordService.resolveResetLink(dto.userId, dto.token);
|
|
}
|
|
}
|