329 lines
11 KiB
JavaScript
329 lines
11 KiB
JavaScript
"use strict";
|
|
/**
|
|
* AuthService - Servicio de Autenticación
|
|
*
|
|
* Gestiona login, logout, refresh tokens y validación de JWT.
|
|
* Implementa patrón multi-tenant con verificación de tenant_id.
|
|
*
|
|
* @module Auth
|
|
*/
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.AuthService = void 0;
|
|
const jwt = __importStar(require("jsonwebtoken"));
|
|
const bcrypt = __importStar(require("bcryptjs"));
|
|
class AuthService {
|
|
userRepository;
|
|
tenantRepository;
|
|
refreshTokenRepository;
|
|
jwtSecret;
|
|
jwtExpiresIn;
|
|
jwtRefreshExpiresIn;
|
|
constructor(userRepository, tenantRepository, refreshTokenRepository) {
|
|
this.userRepository = userRepository;
|
|
this.tenantRepository = tenantRepository;
|
|
this.refreshTokenRepository = refreshTokenRepository;
|
|
this.jwtSecret = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production-minimum-32-chars';
|
|
this.jwtExpiresIn = process.env.JWT_EXPIRES_IN || '1d';
|
|
this.jwtRefreshExpiresIn = process.env.JWT_REFRESH_EXPIRES_IN || '7d';
|
|
}
|
|
/**
|
|
* Login de usuario
|
|
*/
|
|
async login(dto) {
|
|
// Buscar usuario por email
|
|
const user = await this.userRepository.findOne({
|
|
where: { email: dto.email, deletedAt: null },
|
|
relations: ['userRoles', 'userRoles.role'],
|
|
});
|
|
if (!user) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
// Verificar password
|
|
const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash);
|
|
if (!isPasswordValid) {
|
|
throw new Error('Invalid credentials');
|
|
}
|
|
// Verificar que el usuario esté activo
|
|
if (!user.isActive) {
|
|
throw new Error('User is not active');
|
|
}
|
|
// Obtener tenant
|
|
const tenantId = dto.tenantId || user.defaultTenantId;
|
|
if (!tenantId) {
|
|
throw new Error('No tenant specified');
|
|
}
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: tenantId, isActive: true, deletedAt: null },
|
|
});
|
|
if (!tenant) {
|
|
throw new Error('Tenant not found or inactive');
|
|
}
|
|
// Obtener roles del usuario
|
|
const roles = user.userRoles?.map((ur) => ur.role.code) || [];
|
|
// Generar tokens
|
|
const accessToken = this.generateAccessToken(user, tenantId, roles);
|
|
const refreshToken = await this.generateRefreshToken(user.id);
|
|
// Actualizar último login
|
|
await this.userRepository.update(user.id, { lastLoginAt: new Date() });
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn),
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
roles,
|
|
},
|
|
tenant: {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Registro de usuario
|
|
*/
|
|
async register(dto) {
|
|
// Verificar si el email ya existe
|
|
const existingUser = await this.userRepository.findOne({
|
|
where: { email: dto.email },
|
|
});
|
|
if (existingUser) {
|
|
throw new Error('Email already registered');
|
|
}
|
|
// Verificar que el tenant existe
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: dto.tenantId, isActive: true },
|
|
});
|
|
if (!tenant) {
|
|
throw new Error('Tenant not found');
|
|
}
|
|
// Hash del password
|
|
const passwordHash = await bcrypt.hash(dto.password, 12);
|
|
// Crear usuario
|
|
const user = await this.userRepository.save(this.userRepository.create({
|
|
email: dto.email,
|
|
passwordHash,
|
|
firstName: dto.firstName,
|
|
lastName: dto.lastName,
|
|
defaultTenantId: dto.tenantId,
|
|
isActive: true,
|
|
}));
|
|
// Generar tokens (rol default: user)
|
|
const roles = ['user'];
|
|
const accessToken = this.generateAccessToken(user, dto.tenantId, roles);
|
|
const refreshToken = await this.generateRefreshToken(user.id);
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn),
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
roles,
|
|
},
|
|
tenant: {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Refresh de token
|
|
*/
|
|
async refresh(dto) {
|
|
// Validar refresh token
|
|
const validation = this.validateToken(dto.refreshToken, 'refresh');
|
|
if (!validation.valid || !validation.payload) {
|
|
throw new Error('Invalid refresh token');
|
|
}
|
|
// Verificar que el token no está revocado
|
|
const storedToken = await this.refreshTokenRepository.findOne({
|
|
where: { token: dto.refreshToken, revokedAt: null },
|
|
});
|
|
if (!storedToken || storedToken.expiresAt < new Date()) {
|
|
throw new Error('Refresh token expired or revoked');
|
|
}
|
|
// Obtener usuario
|
|
const user = await this.userRepository.findOne({
|
|
where: { id: validation.payload.sub, deletedAt: null },
|
|
relations: ['userRoles', 'userRoles.role'],
|
|
});
|
|
if (!user || !user.isActive) {
|
|
throw new Error('User not found or inactive');
|
|
}
|
|
// Obtener tenant
|
|
const tenant = await this.tenantRepository.findOne({
|
|
where: { id: validation.payload.tenantId, isActive: true },
|
|
});
|
|
if (!tenant) {
|
|
throw new Error('Tenant not found or inactive');
|
|
}
|
|
const roles = user.userRoles?.map((ur) => ur.role.code) || [];
|
|
// Revocar token anterior
|
|
await this.refreshTokenRepository.update(storedToken.id, { revokedAt: new Date() });
|
|
// Generar nuevos tokens
|
|
const accessToken = this.generateAccessToken(user, tenant.id, roles);
|
|
const refreshToken = await this.generateRefreshToken(user.id);
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
expiresIn: this.getExpiresInSeconds(this.jwtExpiresIn),
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
firstName: user.firstName,
|
|
lastName: user.lastName,
|
|
roles,
|
|
},
|
|
tenant: {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
},
|
|
};
|
|
}
|
|
/**
|
|
* Logout - Revocar refresh token
|
|
*/
|
|
async logout(refreshToken) {
|
|
await this.refreshTokenRepository.update({ token: refreshToken }, { revokedAt: new Date() });
|
|
}
|
|
/**
|
|
* Cambiar password
|
|
*/
|
|
async changePassword(userId, dto) {
|
|
const user = await this.userRepository.findOne({
|
|
where: { id: userId },
|
|
});
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
const isCurrentValid = await bcrypt.compare(dto.currentPassword, user.passwordHash);
|
|
if (!isCurrentValid) {
|
|
throw new Error('Current password is incorrect');
|
|
}
|
|
const newPasswordHash = await bcrypt.hash(dto.newPassword, 12);
|
|
await this.userRepository.update(userId, { passwordHash: newPasswordHash });
|
|
// Revocar todos los refresh tokens del usuario
|
|
await this.refreshTokenRepository.update({ userId }, { revokedAt: new Date() });
|
|
}
|
|
/**
|
|
* Validar access token
|
|
*/
|
|
validateAccessToken(token) {
|
|
return this.validateToken(token, 'access');
|
|
}
|
|
/**
|
|
* Validar token
|
|
*/
|
|
validateToken(token, expectedType) {
|
|
try {
|
|
const payload = jwt.verify(token, this.jwtSecret);
|
|
if (payload.type !== expectedType) {
|
|
return { valid: false, error: 'Invalid token type' };
|
|
}
|
|
return { valid: true, payload };
|
|
}
|
|
catch (error) {
|
|
if (error instanceof jwt.TokenExpiredError) {
|
|
return { valid: false, error: 'Token expired' };
|
|
}
|
|
if (error instanceof jwt.JsonWebTokenError) {
|
|
return { valid: false, error: 'Invalid token' };
|
|
}
|
|
return { valid: false, error: 'Token validation failed' };
|
|
}
|
|
}
|
|
/**
|
|
* Generar access token
|
|
*/
|
|
generateAccessToken(user, tenantId, roles) {
|
|
const payload = {
|
|
sub: user.id,
|
|
email: user.email,
|
|
tenantId,
|
|
roles,
|
|
type: 'access',
|
|
};
|
|
return jwt.sign(payload, this.jwtSecret, {
|
|
expiresIn: this.jwtExpiresIn,
|
|
});
|
|
}
|
|
/**
|
|
* Generar refresh token
|
|
*/
|
|
async generateRefreshToken(userId) {
|
|
const payload = {
|
|
sub: userId,
|
|
type: 'refresh',
|
|
};
|
|
const token = jwt.sign(payload, this.jwtSecret, {
|
|
expiresIn: this.jwtRefreshExpiresIn,
|
|
});
|
|
// Almacenar en DB
|
|
const expiresAt = new Date();
|
|
expiresAt.setDate(expiresAt.getDate() + 7); // 7 días
|
|
await this.refreshTokenRepository.save(this.refreshTokenRepository.create({
|
|
userId,
|
|
token,
|
|
expiresAt,
|
|
}));
|
|
return token;
|
|
}
|
|
/**
|
|
* Convertir expiresIn a segundos
|
|
*/
|
|
getExpiresInSeconds(expiresIn) {
|
|
const match = expiresIn.match(/^(\d+)([dhms])$/);
|
|
if (!match)
|
|
return 86400; // default 1 día
|
|
const value = parseInt(match[1]);
|
|
const unit = match[2];
|
|
switch (unit) {
|
|
case 'd': return value * 86400;
|
|
case 'h': return value * 3600;
|
|
case 'm': return value * 60;
|
|
case 's': return value;
|
|
default: return 86400;
|
|
}
|
|
}
|
|
}
|
|
exports.AuthService = AuthService;
|
|
//# sourceMappingURL=auth.service.js.map
|