New projects created: - michangarrito (marketplace mobile) - template-saas (SaaS template) - clinica-dental (dental ERP) - clinica-veterinaria (veterinary ERP) Architecture updates: - Move catalog from core/ to shared/ - Add MCP servers structure and templates - Add git management scripts - Update SUBREPOSITORIOS.md with 15 new repos - Update .gitignore for new projects Repository infrastructure: - 4 main repositories - 11 subrepositorios - Gitea remotes configured 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
230 lines
6.3 KiB
TypeScript
230 lines
6.3 KiB
TypeScript
/**
|
|
* AUTH SERVICE - REFERENCE IMPLEMENTATION
|
|
*
|
|
* @description Implementación de referencia para servicio de autenticación JWT.
|
|
* Este archivo muestra los patrones básicos sin dependencias específicas de proyecto.
|
|
*
|
|
* @usage Copiar y adaptar según necesidades del proyecto.
|
|
* @origin gamilit/apps/backend/src/modules/auth/services/auth.service.ts
|
|
*/
|
|
|
|
import { Injectable, UnauthorizedException, ConflictException } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository } from 'typeorm';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import * as bcrypt from 'bcrypt';
|
|
import * as crypto from 'crypto';
|
|
|
|
// Adaptar imports según proyecto
|
|
// import { User, Profile, UserSession } from '../entities';
|
|
// import { RegisterUserDto } from '../dto';
|
|
|
|
/**
|
|
* Constantes de configuración
|
|
* Mover a variables de entorno en producción
|
|
*/
|
|
const BCRYPT_COST = 10;
|
|
const ACCESS_TOKEN_EXPIRES = '15m';
|
|
const REFRESH_TOKEN_EXPIRES = '7d';
|
|
|
|
@Injectable()
|
|
export class AuthService {
|
|
constructor(
|
|
@InjectRepository(User, 'auth')
|
|
private readonly userRepository: Repository<User>,
|
|
|
|
@InjectRepository(Profile, 'auth')
|
|
private readonly profileRepository: Repository<Profile>,
|
|
|
|
@InjectRepository(UserSession, 'auth')
|
|
private readonly sessionRepository: Repository<UserSession>,
|
|
|
|
private readonly jwtService: JwtService,
|
|
) {}
|
|
|
|
/**
|
|
* Registro de nuevo usuario
|
|
*
|
|
* @pattern
|
|
* 1. Validar email único
|
|
* 2. Hashear password (bcrypt)
|
|
* 3. Crear usuario
|
|
* 4. Crear perfil
|
|
* 5. Generar tokens
|
|
*/
|
|
async register(dto: RegisterUserDto): Promise<AuthResponse> {
|
|
// 1. Validar email único
|
|
const existingUser = await this.userRepository.findOne({
|
|
where: { email: dto.email },
|
|
});
|
|
if (existingUser) {
|
|
throw new ConflictException('Email ya registrado');
|
|
}
|
|
|
|
// 2. Hashear password
|
|
const hashedPassword = await bcrypt.hash(dto.password, BCRYPT_COST);
|
|
|
|
// 3. Crear usuario
|
|
const user = this.userRepository.create({
|
|
email: dto.email,
|
|
encrypted_password: hashedPassword,
|
|
role: dto.role || 'user',
|
|
});
|
|
await this.userRepository.save(user);
|
|
|
|
// 4. Crear perfil
|
|
const profile = this.profileRepository.create({
|
|
id: user.id,
|
|
user_id: user.id,
|
|
email: user.email,
|
|
// ...otros campos del perfil
|
|
});
|
|
await this.profileRepository.save(profile);
|
|
|
|
// 5. Generar tokens y crear sesión
|
|
return this.createAuthResponse(user);
|
|
}
|
|
|
|
/**
|
|
* Login de usuario
|
|
*
|
|
* @pattern
|
|
* 1. Buscar usuario por email
|
|
* 2. Validar password
|
|
* 3. Crear sesión
|
|
* 4. Generar tokens
|
|
*/
|
|
async login(email: string, password: string): Promise<AuthResponse> {
|
|
// 1. Buscar usuario
|
|
const user = await this.userRepository.findOne({
|
|
where: { email },
|
|
select: ['id', 'email', 'encrypted_password', 'role'],
|
|
});
|
|
|
|
if (!user) {
|
|
throw new UnauthorizedException('Credenciales inválidas');
|
|
}
|
|
|
|
// 2. Validar password
|
|
const isValid = await bcrypt.compare(password, user.encrypted_password);
|
|
if (!isValid) {
|
|
throw new UnauthorizedException('Credenciales inválidas');
|
|
}
|
|
|
|
// 3-4. Crear sesión y generar tokens
|
|
return this.createAuthResponse(user);
|
|
}
|
|
|
|
/**
|
|
* Refresh de tokens
|
|
*
|
|
* @pattern
|
|
* 1. Buscar sesión por refresh token hash
|
|
* 2. Validar que no esté expirada
|
|
* 3. Generar nuevos tokens
|
|
* 4. Actualizar sesión
|
|
*/
|
|
async refreshToken(refreshToken: string): Promise<AuthResponse> {
|
|
const tokenHash = this.hashToken(refreshToken);
|
|
|
|
const session = await this.sessionRepository.findOne({
|
|
where: { refresh_token: tokenHash },
|
|
relations: ['user'],
|
|
});
|
|
|
|
if (!session || session.is_revoked || new Date() > session.expires_at) {
|
|
throw new UnauthorizedException('Token inválido o expirado');
|
|
}
|
|
|
|
// Generar nuevos tokens
|
|
return this.createAuthResponse(session.user, session);
|
|
}
|
|
|
|
/**
|
|
* Logout
|
|
*
|
|
* @pattern Revocar sesión actual
|
|
*/
|
|
async logout(sessionId: string): Promise<void> {
|
|
await this.sessionRepository.update(sessionId, { is_revoked: true });
|
|
}
|
|
|
|
// ============ HELPERS PRIVADOS ============
|
|
|
|
/**
|
|
* Generar respuesta de autenticación completa
|
|
*/
|
|
private async createAuthResponse(user: User, existingSession?: UserSession): Promise<AuthResponse> {
|
|
const payload = {
|
|
sub: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
};
|
|
|
|
const accessToken = this.jwtService.sign(payload, { expiresIn: ACCESS_TOKEN_EXPIRES });
|
|
const refreshToken = crypto.randomBytes(32).toString('hex');
|
|
|
|
// Crear o actualizar sesión
|
|
if (existingSession) {
|
|
existingSession.refresh_token = this.hashToken(refreshToken);
|
|
existingSession.expires_at = this.calculateExpiry(REFRESH_TOKEN_EXPIRES);
|
|
await this.sessionRepository.save(existingSession);
|
|
} else {
|
|
const session = this.sessionRepository.create({
|
|
user_id: user.id,
|
|
refresh_token: this.hashToken(refreshToken),
|
|
expires_at: this.calculateExpiry(REFRESH_TOKEN_EXPIRES),
|
|
});
|
|
await this.sessionRepository.save(session);
|
|
}
|
|
|
|
return {
|
|
accessToken,
|
|
refreshToken,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
role: user.role,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hash de token para almacenamiento seguro
|
|
*/
|
|
private hashToken(token: string): string {
|
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Calcular fecha de expiración
|
|
*/
|
|
private calculateExpiry(duration: string): Date {
|
|
const match = duration.match(/^(\d+)([mhd])$/);
|
|
if (!match) throw new Error('Invalid duration format');
|
|
|
|
const [, value, unit] = match;
|
|
const multipliers = { m: 60000, h: 3600000, d: 86400000 };
|
|
|
|
return new Date(Date.now() + parseInt(value) * multipliers[unit]);
|
|
}
|
|
}
|
|
|
|
// ============ TIPOS ============
|
|
|
|
interface AuthResponse {
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
user: {
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
};
|
|
}
|
|
|
|
interface RegisterUserDto {
|
|
email: string;
|
|
password: string;
|
|
role?: string;
|
|
}
|