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:
co-authored by
Cursor
parent
f4295e780c
commit
08f901306c
+66
-35
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user