Files
parsshop-back/dist/modules/auth/auth.service.js
2026-03-26 13:58:07 +03:00

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