276 lines
11 KiB
JavaScript
276 lines
11 KiB
JavaScript
"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
|