Add password login, set-password, business images migration, and user serialization.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,10 +16,17 @@ import {
|
||||
isValidIranMobile,
|
||||
normalizeIranMobile,
|
||||
} from '../../common/utils/iran-mobile.util';
|
||||
import {
|
||||
hashPassword,
|
||||
verifyPassword,
|
||||
} from '../../common/utils/password.util';
|
||||
import { serializeUser } from '../../common/utils/user-serialize.util';
|
||||
import { AuthOtpEntity } from '../../entities/auth-otp.entity';
|
||||
import { UserEntity, UserRole } from '../../entities/user.entity';
|
||||
import { JwtPayload } from './auth.types';
|
||||
import { LoginPasswordDto } from './dto/login-password.dto';
|
||||
import { RequestOtpDto } from './dto/request-otp.dto';
|
||||
import { SetPasswordDto } from './dto/set-password.dto';
|
||||
import { VerifyOtpDto } from './dto/verify-otp.dto';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@@ -28,9 +35,9 @@ type RequestOtpResponse = {
|
||||
otpCode?: string;
|
||||
};
|
||||
|
||||
type VerifyOtpResponse = {
|
||||
type AuthSessionResponse = {
|
||||
accessToken: string;
|
||||
user: Pick<UserEntity, 'id' | 'mobile' | 'fullName' | 'role' | 'isActive'>;
|
||||
user: ReturnType<typeof serializeUser>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -113,7 +120,7 @@ export class AuthService {
|
||||
return response;
|
||||
}
|
||||
|
||||
async verifyOtp(dto: VerifyOtpDto): Promise<VerifyOtpResponse> {
|
||||
async verifyOtp(dto: VerifyOtpDto): Promise<AuthSessionResponse> {
|
||||
const mobile = this.normalizeMobile(dto.mobile);
|
||||
const code = String(dto.code ?? '').trim();
|
||||
|
||||
@@ -148,24 +155,94 @@ export class AuthService {
|
||||
await this.otps.save(otp);
|
||||
|
||||
const user = await this.findOrCreateCustomer(mobile);
|
||||
const payload: JwtPayload = {
|
||||
sub: user.id,
|
||||
mobile: user.mobile,
|
||||
role: user.role,
|
||||
};
|
||||
|
||||
return {
|
||||
accessToken: await this.jwt.signAsync(payload),
|
||||
user: this.serializeUser(user),
|
||||
};
|
||||
return this.issueSession(user);
|
||||
}
|
||||
|
||||
async loginWithPassword(dto: LoginPasswordDto): Promise<AuthSessionResponse> {
|
||||
const mobile = this.normalizeMobile(dto.mobile);
|
||||
const user = await this.users
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('user.mobile = :mobile', { mobile })
|
||||
.getOne();
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException('Invalid mobile or password');
|
||||
}
|
||||
if (!user.passwordHash) {
|
||||
throw new UnauthorizedException(
|
||||
'Password is not set. Sign in with OTP first, then set a password in your profile.',
|
||||
);
|
||||
}
|
||||
|
||||
const valid = await verifyPassword(dto.password, user.passwordHash);
|
||||
if (!valid) {
|
||||
throw new UnauthorizedException('Invalid mobile or password');
|
||||
}
|
||||
|
||||
return this.issueSession(user);
|
||||
}
|
||||
|
||||
async setPassword(userId: string, dto: SetPasswordDto) {
|
||||
if (dto.password !== dto.confirmPassword) {
|
||||
throw new BadRequestException('Password confirmation does not match');
|
||||
}
|
||||
|
||||
const user = await this.users
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('user.id = :id', { id: userId })
|
||||
.getOne();
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
}
|
||||
|
||||
if (user.passwordHash) {
|
||||
if (!dto.currentPassword) {
|
||||
throw new BadRequestException('Current password is required');
|
||||
}
|
||||
const valid = await verifyPassword(dto.currentPassword, user.passwordHash);
|
||||
if (!valid) {
|
||||
throw new UnauthorizedException('Current password is incorrect');
|
||||
}
|
||||
}
|
||||
|
||||
user.passwordHash = await hashPassword(dto.password);
|
||||
const saved = await this.users.save(user);
|
||||
return serializeUser(saved);
|
||||
}
|
||||
|
||||
async me(userId: string) {
|
||||
const user = await this.users.findOne({ where: { id: userId } });
|
||||
const user = await this.users
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('user.id = :id', { id: userId })
|
||||
.getOne();
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
}
|
||||
return this.serializeUser(user);
|
||||
return serializeUser(user);
|
||||
}
|
||||
|
||||
private async issueSession(user: UserEntity): Promise<AuthSessionResponse> {
|
||||
const full = await this.users
|
||||
.createQueryBuilder('user')
|
||||
.addSelect('user.passwordHash')
|
||||
.where('user.id = :id', { id: user.id })
|
||||
.getOne();
|
||||
const profile = full ?? user;
|
||||
|
||||
const payload: JwtPayload = {
|
||||
sub: profile.id,
|
||||
mobile: profile.mobile,
|
||||
role: profile.role,
|
||||
};
|
||||
return {
|
||||
accessToken: await this.jwt.signAsync(payload),
|
||||
user: serializeUser(profile),
|
||||
};
|
||||
}
|
||||
|
||||
private async findOrCreateCustomer(mobile: string): Promise<UserEntity> {
|
||||
@@ -193,16 +270,6 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
private serializeUser(user: UserEntity) {
|
||||
return {
|
||||
id: user.id,
|
||||
mobile: user.mobile,
|
||||
fullName: user.fullName,
|
||||
role: user.role,
|
||||
isActive: user.isActive,
|
||||
};
|
||||
}
|
||||
|
||||
private normalizeMobile(mobile: string): string {
|
||||
const normalized = normalizeIranMobile(mobile);
|
||||
if (!isValidIranMobile(normalized)) {
|
||||
|
||||
Reference in New Issue
Block a user