Add passwordless OTP login and SMS password reset.

Expose login-otp and reset-password so dashboards can finish forgot-password and one-time SMS sign-in, and sync website API docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-05 15:10:06 +03:30
co-authored by Cursor
parent f4295e780c
commit 08f901306c
11 changed files with 314 additions and 41 deletions
+66 -35
View File
@@ -307,27 +307,75 @@ export class AuthService {
}
async verifyOtp(cellNumber: string, code: string) {
if (!this.sms.isEnabled()) {
const user = await this.prisma.user.findUnique({
where: { cellNumber },
const user = await this.consumeOtp(cellNumber, code);
const smsEnabled = this.sms.isEnabled();
if (!user.cellVerifiedAt) {
await this.prisma.user.update({
where: { id: user.id },
data: { cellVerifiedAt: new Date() },
});
}
if (!user) {
throw new UnauthorizedException('Cell number is not registered');
}
return {
enabled: smsEnabled,
verified: true,
message: smsEnabled
? 'Cell number verified successfully'
: 'SMS verification is disabled — cell number marked as verified.',
};
}
if (!user.cellVerifiedAt) {
await this.prisma.user.update({
where: { id: user.id },
data: { cellVerifiedAt: new Date() },
});
}
async loginWithOtp(cellNumber: string, code: string) {
const user = await this.consumeOtp(cellNumber, code);
return {
enabled: false,
verified: true,
message: 'SMS verification is disabled — cell number marked as verified.',
};
await this.prisma.user.update({
where: { id: user.id },
data: {
cellVerifiedAt: user.cellVerifiedAt ?? new Date(),
lastLoginAt: new Date(),
},
});
const authUser = await this.getAuthUser(user.id);
const tokens = await this.issueTokens(authUser);
return {
message: 'Login successful',
user: this.serializeUser(authUser),
...tokens,
};
}
async resetPassword(cellNumber: string, code: string, newPassword: string) {
const user = await this.consumeOtp(cellNumber, code);
const passwordHash = await bcrypt.hash(newPassword, 10);
await this.prisma.user.update({
where: { id: user.id },
data: {
passwordHash,
cellVerifiedAt: user.cellVerifiedAt ?? new Date(),
},
});
return {
message: 'Password reset successfully. You can now sign in with your new password.',
};
}
/** Validates OTP (or skips when SMS is disabled) and returns the active user. */
private async consumeOtp(cellNumber: string, code: string) {
const user = await this.prisma.user.findUnique({
where: { cellNumber },
});
if (!user || !user.isActive) {
throw new UnauthorizedException('Cell number is not registered');
}
if (!this.sms.isEnabled()) {
return user;
}
const storedCode = await this.redis.getOtp(cellNumber);
@@ -335,25 +383,8 @@ export class AuthService {
throw new UnauthorizedException('Invalid or expired verification code');
}
const user = await this.prisma.user.findUnique({
where: { cellNumber },
});
if (!user) {
throw new UnauthorizedException('Cell number is not registered');
}
await this.prisma.user.update({
where: { id: user.id },
data: { cellVerifiedAt: new Date() },
});
await this.redis.deleteOtp(cellNumber);
return {
enabled: true,
verified: true,
message: 'Cell number verified successfully',
};
return user;
}
private async getAuthUser(userId: bigint): Promise<AuthUser> {