"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthService = void 0; const common_1 = require("@nestjs/common"); const config_1 = require("@nestjs/config"); const typeorm_1 = require("@nestjs/typeorm"); const jwt_1 = require("@nestjs/jwt"); const bcrypt = require("bcryptjs"); const typeorm_2 = require("typeorm"); const auth_otp_entity_1 = require("./entities/auth-otp.entity"); const user_session_entity_1 = require("./entities/user-session.entity"); const user_level_enum_1 = require("../users/enums/user-level.enum"); const user_role_enum_1 = require("../users/enums/user-role.enum"); const users_service_1 = require("../users/users.service"); const sms_service_1 = require("./sms.service"); let AuthService = class AuthService { usersService; jwtService; configService; smsService; authOtpsRepository; userSessionsRepository; constructor(usersService, jwtService, configService, smsService, authOtpsRepository, userSessionsRepository) { this.usersService = usersService; this.jwtService = jwtService; this.configService = configService; this.smsService = smsService; this.authOtpsRepository = authOtpsRepository; this.userSessionsRepository = userSessionsRepository; } async requestOtp(phone, fullName) { const user = await this.usersService.findOrCreateByPhone(phone, fullName); const otpCode = this.generateOtp(); const ttlSeconds = this.configService.get('otp.ttlSeconds', 120); const otp = this.authOtpsRepository.create({ phone: user.phone, codeHash: await bcrypt.hash(otpCode, 10), purpose: 'login', expiresAt: new Date(Date.now() + ttlSeconds * 1000), attemptCount: 0, }); await this.authOtpsRepository.save(otp); const smsSent = await this.smsService.sendOtp(phone, otpCode); return { message: 'OTP generated successfully', expiresInSeconds: ttlSeconds, phone, smsSent, otpPreview: this.configService.get('app.nodeEnv') === 'development' ? otpCode : undefined, }; } async registerWithPassword(dto) { const existingPhone = await this.usersService.findByPhone(dto.phone); if (existingPhone) { throw new common_1.BadRequestException('Phone already exists'); } const existingUsername = await this.usersService.findByUsername(dto.username); if (existingUsername) { throw new common_1.BadRequestException('Username already exists'); } const savedUser = await this.usersService.create({ phone: dto.phone, username: dto.username, fullName: dto.fullName ?? dto.username, passwordHash: await bcrypt.hash(dto.password, 10), isVerified: true, role: user_role_enum_1.UserRole.USER, }); const tokens = await this.issueTokens(savedUser); await this.storeRefreshToken(savedUser, tokens.refreshToken); return tokens; } async loginWithPassword(dto) { const user = await this.usersService.findByUsername(dto.username); if (!user?.passwordHash) { throw new common_1.UnauthorizedException('Invalid username or password'); } const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash); if (!isPasswordValid) { throw new common_1.UnauthorizedException('Invalid username or password'); } const tokens = await this.issueTokens(user); await this.storeRefreshToken(user, tokens.refreshToken); return tokens; } async verifyOtp(phone, otp) { const user = await this.usersService.findByPhone(phone); const otpRecord = await this.authOtpsRepository.findOne({ where: { phone, purpose: 'login', usedAt: (0, typeorm_2.IsNull)() }, order: { createdAt: 'DESC' }, }); if (!user || !otpRecord) { throw new common_1.UnauthorizedException('OTP not requested'); } if (otpRecord.expiresAt.getTime() < Date.now()) { throw new common_1.UnauthorizedException('OTP expired'); } const isOtpValid = await bcrypt.compare(otp, otpRecord.codeHash); if (!isOtpValid) { otpRecord.attemptCount += 1; await this.authOtpsRepository.save(otpRecord); throw new common_1.BadRequestException('Invalid OTP'); } user.isVerified = true; otpRecord.usedAt = new Date(); await Promise.all([ this.usersService.save(user), this.authOtpsRepository.save(otpRecord), ]); const tokens = await this.issueTokens(user); await this.storeRefreshToken(user, tokens.refreshToken); return tokens; } async refreshToken(refreshToken) { const payload = await this.jwtService.verifyAsync(refreshToken, { secret: this.configService.getOrThrow('jwt.secret'), }); if (payload.type !== 'refresh') { throw new common_1.UnauthorizedException('Invalid token type'); } const user = await this.usersService.findByPhone(payload.phone); if (!user) { throw new common_1.UnauthorizedException('Refresh token not found'); } const sessions = await this.userSessionsRepository.find({ where: { user: { id: user.id }, revokedAt: (0, typeorm_2.IsNull)(), }, relations: { user: true }, order: { createdAt: 'DESC' }, }); const validSession = await this.findMatchingSession(sessions, refreshToken); if (!validSession || validSession.expiresAt.getTime() < Date.now()) { throw new common_1.UnauthorizedException('Invalid refresh token'); } validSession.revokedAt = new Date(); await this.userSessionsRepository.save(validSession); const tokens = await this.issueTokens(user); await this.storeRefreshToken(user, tokens.refreshToken); return tokens; } async logout(userId) { const user = await this.findUserById(userId); await this.userSessionsRepository .createQueryBuilder() .update(user_session_entity_1.UserSession) .set({ revokedAt: new Date() }) .where('userId = :userId', { userId: user.id }) .andWhere('revoked_at IS NULL') .execute(); return { message: 'Logged out successfully' }; } async issueTokens(user) { const currentLevel = user.loyaltyProfile?.currentLevel ?? user_level_enum_1.UserLevel.BRONZE; const accessPayload = { sub: user.id, phone: user.phone, role: user.role, level: currentLevel, permissions: this.resolvePermissions(user), type: 'access', }; const refreshPayload = { ...accessPayload, type: 'refresh', }; const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(accessPayload, { secret: this.configService.getOrThrow('jwt.secret'), expiresIn: this.configService.getOrThrow('jwt.accessTtl'), }), this.jwtService.signAsync(refreshPayload, { secret: this.configService.getOrThrow('jwt.secret'), expiresIn: this.configService.getOrThrow('jwt.refreshTtl'), }), ]); return { accessToken, refreshToken, user: { id: user.id, phone: user.phone, fullName: user.fullName, role: user.role, level: currentLevel, }, }; } async storeRefreshToken(user, refreshToken) { const refreshTtl = this.configService.getOrThrow('jwt.refreshTtl'); const session = this.userSessionsRepository.create({ user, refreshTokenHash: await bcrypt.hash(refreshToken, 10), expiresAt: new Date(Date.now() + this.parseDurationToMs(refreshTtl)), }); await this.userSessionsRepository.save(session); } generateOtp() { return Math.floor(100000 + Math.random() * 900000).toString(); } resolvePermissions(user) { if (user.role === user_role_enum_1.UserRole.ADMIN) { return [ 'products.manage', 'categories.manage', 'brands.manage', 'users.manage', 'media.manage', ]; } if (user.role === user_role_enum_1.UserRole.AGENT) { return ['orders.assist', 'customers.assist']; } return ['profile.read']; } async findUserById(userId) { const user = await this.usersService.findById(userId); if (!user) { throw new common_1.UnauthorizedException('User not found'); } return user; } async findMatchingSession(sessions, refreshToken) { for (const session of sessions) { const isValid = await bcrypt.compare(refreshToken, session.refreshTokenHash); if (isValid) { return session; } } return null; } parseDurationToMs(value) { const match = /^(\d+)(ms|s|m|h|d)$/i.exec(value); if (!match) { throw new common_1.BadRequestException(`Unsupported duration format: ${value}`); } const amount = Number(match[1]); const unit = match[2].toLowerCase(); const unitMap = { ms: 1, s: 1000, m: 60 * 1000, h: 60 * 60 * 1000, d: 24 * 60 * 60 * 1000, }; return amount * unitMap[unit]; } }; exports.AuthService = AuthService; exports.AuthService = AuthService = __decorate([ (0, common_1.Injectable)(), __param(4, (0, typeorm_1.InjectRepository)(auth_otp_entity_1.AuthOtp)), __param(5, (0, typeorm_1.InjectRepository)(user_session_entity_1.UserSession)), __metadata("design:paramtypes", [users_service_1.UsersService, jwt_1.JwtService, config_1.ConfigService, sms_service_1.SmsService, typeorm_2.Repository, typeorm_2.Repository]) ], AuthService); //# sourceMappingURL=auth.service.js.map