# Guía de Implementación: Autenticación **Versión:** 1.0.0 **Tiempo estimado:** 2-4 horas (adaptación), 8-16 horas (desde cero) **Complejidad:** Media-Alta --- ## Pre-requisitos Antes de implementar, asegurar: - [ ] NestJS configurado con TypeORM - [ ] PostgreSQL con schemas creados - [ ] Variables de entorno configuradas - [ ] Dependencias npm instaladas --- ## Paso 1: Instalar Dependencias ```bash npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt npm install -D @types/passport-jwt @types/bcrypt npm install class-validator class-transformer ``` --- ## Paso 2: Crear DDL de Base de Datos ### 2.1 Schema auth.users ```sql -- Schema: auth (si no existe) CREATE SCHEMA IF NOT EXISTS auth; -- Extensión para UUIDs CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Tabla de usuarios CREATE TABLE auth.users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, encrypted_password TEXT NOT NULL, role VARCHAR(50) DEFAULT 'user', status VARCHAR(50) DEFAULT 'active', email_confirmed_at TIMESTAMPTZ, phone TEXT, phone_confirmed_at TIMESTAMPTZ, is_super_admin BOOLEAN DEFAULT FALSE, banned_until TIMESTAMPTZ, last_sign_in_at TIMESTAMPTZ, raw_user_meta_data JSONB DEFAULT '{}', deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Índices CREATE INDEX idx_auth_users_email ON auth.users(email); CREATE INDEX idx_auth_users_role ON auth.users(role); CREATE INDEX idx_auth_users_status ON auth.users(status); -- Comentarios COMMENT ON TABLE auth.users IS 'Tabla principal de usuarios del sistema'; COMMENT ON COLUMN auth.users.encrypted_password IS 'Password hasheado con bcrypt'; ``` ### 2.2 Schema auth_management.user_sessions ```sql -- Schema: auth_management CREATE SCHEMA IF NOT EXISTS auth_management; -- Tabla de sesiones CREATE TABLE auth_management.user_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, tenant_id UUID, session_token TEXT NOT NULL UNIQUE, refresh_token TEXT NOT NULL, ip_address INET, user_agent TEXT, device_type VARCHAR(50), browser VARCHAR(100), os VARCHAR(100), is_active BOOLEAN DEFAULT TRUE, expires_at TIMESTAMPTZ NOT NULL, last_activity_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Índices CREATE INDEX idx_user_sessions_user_id ON auth_management.user_sessions(user_id); CREATE INDEX idx_user_sessions_refresh_token ON auth_management.user_sessions(refresh_token); CREATE INDEX idx_user_sessions_expires_at ON auth_management.user_sessions(expires_at); COMMENT ON TABLE auth_management.user_sessions IS 'Sesiones activas de usuarios'; ``` ### 2.3 Tabla auth_attempts ```sql CREATE TABLE auth_management.auth_attempts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL, success BOOLEAN NOT NULL, ip_address INET NOT NULL, user_agent TEXT, failure_reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_auth_attempts_email ON auth_management.auth_attempts(email); CREATE INDEX idx_auth_attempts_ip ON auth_management.auth_attempts(ip_address); CREATE INDEX idx_auth_attempts_created_at ON auth_management.auth_attempts(created_at); COMMENT ON TABLE auth_management.auth_attempts IS 'Log de intentos de autenticación'; ``` --- ## Paso 3: Crear Entities ### 3.1 User Entity ```typescript // src/modules/auth/entities/user.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index, } from 'typeorm'; import { Exclude } from 'class-transformer'; @Entity({ schema: 'auth', name: 'users' }) @Index('idx_auth_users_email', ['email']) export class User { @PrimaryGeneratedColumn('uuid') id!: string; @Column({ type: 'text', unique: true }) email!: string; @Column({ type: 'text', name: 'encrypted_password' }) @Exclude() // No serializar en respuestas encrypted_password!: string; @Column({ type: 'varchar', length: 50, default: 'user' }) role!: string; @Column({ type: 'varchar', length: 50, default: 'active' }) status!: string; @Column({ type: 'timestamp with time zone', nullable: true }) email_confirmed_at?: Date; @Column({ type: 'boolean', default: false }) is_super_admin!: boolean; @Column({ type: 'timestamp with time zone', nullable: true }) banned_until?: Date; @Column({ type: 'timestamp with time zone', nullable: true }) last_sign_in_at?: Date; @Column({ type: 'jsonb', default: {} }) raw_user_meta_data!: Record; @Column({ type: 'timestamp with time zone', nullable: true }) deleted_at?: Date; @CreateDateColumn({ type: 'timestamp with time zone' }) created_at!: Date; @UpdateDateColumn({ type: 'timestamp with time zone' }) updated_at!: Date; } ``` ### 3.2 UserSession Entity ```typescript // src/modules/auth/entities/user-session.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, } from 'typeorm'; import { User } from './user.entity'; @Entity({ schema: 'auth_management', name: 'user_sessions' }) export class UserSession { @PrimaryGeneratedColumn('uuid') id!: string; @Column({ type: 'uuid' }) user_id!: string; @Column({ type: 'uuid', nullable: true }) tenant_id?: string; @Column({ type: 'text', unique: true }) session_token!: string; @Column({ type: 'text' }) refresh_token!: string; // Hasheado con SHA256 @Column({ type: 'inet', nullable: true }) ip_address?: string; @Column({ type: 'text', nullable: true }) user_agent?: string; @Column({ type: 'varchar', length: 50, nullable: true }) device_type?: string; @Column({ type: 'varchar', length: 100, nullable: true }) browser?: string; @Column({ type: 'varchar', length: 100, nullable: true }) os?: string; @Column({ type: 'boolean', default: true }) is_active!: boolean; @Column({ type: 'timestamp with time zone' }) expires_at!: Date; @Column({ type: 'timestamp with time zone', default: () => 'NOW()' }) last_activity_at!: Date; @CreateDateColumn({ type: 'timestamp with time zone' }) created_at!: Date; @UpdateDateColumn({ type: 'timestamp with time zone' }) updated_at!: Date; } ``` --- ## Paso 4: Crear DTOs ### 4.1 Login DTO ```typescript // src/modules/auth/dto/login.dto.ts import { ApiProperty } from '@nestjs/swagger'; import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; export class LoginDto { @ApiProperty({ description: 'Email del usuario', example: 'usuario@example.com', }) @IsEmail({}, { message: 'Email inválido' }) @IsNotEmpty({ message: 'Email es requerido' }) email!: string; @ApiProperty({ description: 'Contraseña del usuario', example: 'MySecurePassword123!', minLength: 8, }) @IsString() @MinLength(8, { message: 'Password debe tener al menos 8 caracteres' }) @IsNotEmpty({ message: 'Password es requerido' }) password!: string; } ``` ### 4.2 Register DTO ```typescript // src/modules/auth/dto/register-user.dto.ts import { IsEmail, IsString, MinLength, IsOptional, IsObject } from 'class-validator'; export class RegisterUserDto { @IsEmail({}, { message: 'El email debe ser válido' }) email!: string; @IsString() @MinLength(8, { message: 'La contraseña debe tener al menos 8 caracteres' }) password!: string; @IsObject() @IsOptional() raw_user_meta_data?: Record; @IsString() @IsOptional() first_name?: string; @IsString() @IsOptional() last_name?: string; } ``` --- ## Paso 5: Crear JWT Strategy ```typescript // src/modules/auth/strategies/jwt.strategy.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { ConfigService } from '@nestjs/config'; import { AuthService } from '../services/auth.service'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor( private readonly configService: ConfigService, private readonly authService: AuthService, ) { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: configService.get('JWT_SECRET') || 'dev-secret', }); } async validate(payload: any) { const { sub: userId } = payload; const user = await this.authService.validateUser(userId); if (!user) { throw new UnauthorizedException('Usuario no encontrado o inactivo'); } return { id: user.id, sub: user.id, email: user.email, role: user.role, is_active: !user.deleted_at, email_verified: !!user.email_confirmed_at, }; } } ``` --- ## Paso 6: Crear Guards ### 6.1 JWT Auth Guard ```typescript // src/modules/auth/guards/jwt-auth.guard.ts import { Injectable, ExecutionContext } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { canActivate(context: ExecutionContext) { return super.canActivate(context); } } ``` ### 6.2 Roles Guard ```typescript // src/modules/auth/guards/roles.guard.ts import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; @Injectable() export class RolesGuard implements CanActivate { constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { const requiredRoles = this.reflector.getAllAndOverride('roles', [ context.getHandler(), context.getClass(), ]); if (!requiredRoles || requiredRoles.length === 0) { return true; } const { user } = context.switchToHttp().getRequest(); if (!user) { return false; } return requiredRoles.some((role) => user.role === role); } } ``` ### 6.3 Roles Decorator ```typescript // src/modules/auth/decorators/roles.decorator.ts import { SetMetadata } from '@nestjs/common'; export const Roles = (...roles: string[]) => SetMetadata('roles', roles); ``` --- ## Paso 7: Crear AuthService ```typescript // 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'; import { User } from '../entities/user.entity'; import { UserSession } from '../entities/user-session.entity'; import { LoginDto, RegisterUserDto } from '../dto'; @Injectable() export class AuthService { constructor( @InjectRepository(User) private readonly userRepository: Repository, @InjectRepository(UserSession) private readonly sessionRepository: Repository, private readonly jwtService: JwtService, ) {} async register(dto: RegisterUserDto, ip?: string, userAgent?: string) { // Verificar email único const existing = await this.userRepository.findOne({ where: { email: dto.email }, }); if (existing) { throw new ConflictException('Email ya registrado'); } // Hashear password const hashedPassword = await bcrypt.hash(dto.password, 10); // Crear usuario const user = this.userRepository.create({ email: dto.email, encrypted_password: hashedPassword, role: 'user', raw_user_meta_data: dto.raw_user_meta_data || {}, }); await this.userRepository.save(user); // Generar tokens const tokens = await this.generateTokens(user); // Crear sesión await this.createSession(user.id, tokens.refreshToken, ip, userAgent); return { user: this.toUserResponse(user), ...tokens, }; } async login(dto: LoginDto, ip?: string, userAgent?: string) { const user = await this.userRepository.findOne({ where: { email: dto.email }, }); if (!user) { throw new UnauthorizedException('Credenciales inválidas'); } const isPasswordValid = await bcrypt.compare(dto.password, user.encrypted_password); if (!isPasswordValid) { throw new UnauthorizedException('Credenciales inválidas'); } if (user.deleted_at) { throw new UnauthorizedException('Usuario no activo'); } // Generar tokens const tokens = await this.generateTokens(user); // Crear sesión await this.createSession(user.id, tokens.refreshToken, ip, userAgent); // Actualizar último login user.last_sign_in_at = new Date(); await this.userRepository.save(user); return { user: this.toUserResponse(user), ...tokens, }; } async validateUser(userId: string): Promise { const user = await this.userRepository.findOne({ where: { id: userId }, }); if (user && user.deleted_at) { return null; } return user; } async refreshToken(refreshToken: string) { try { const payload = this.jwtService.verify(refreshToken); const user = await this.validateUser(payload.sub); if (!user) { throw new UnauthorizedException('Usuario no encontrado'); } // Verificar sesión const hashedToken = crypto.createHash('sha256').update(refreshToken).digest('hex'); const session = await this.sessionRepository.findOne({ where: { user_id: user.id, refresh_token: hashedToken }, }); if (!session || new Date() > session.expires_at) { throw new UnauthorizedException('Sesión expirada'); } // Generar nuevos tokens const tokens = await this.generateTokens(user); // Actualizar sesión session.refresh_token = crypto.createHash('sha256').update(tokens.refreshToken).digest('hex'); session.expires_at = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); session.last_activity_at = new Date(); await this.sessionRepository.save(session); return tokens; } catch { throw new UnauthorizedException('Refresh token inválido'); } } private async generateTokens(user: User) { const payload = { sub: user.id, email: user.email, role: user.role }; return { accessToken: this.jwtService.sign(payload, { expiresIn: '15m' }), refreshToken: this.jwtService.sign(payload, { expiresIn: '7d' }), }; } private async createSession(userId: string, refreshToken: string, ip?: string, userAgent?: string) { const hashedRefreshToken = crypto.createHash('sha256').update(refreshToken).digest('hex'); const sessionToken = crypto.randomBytes(32).toString('hex'); const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const session = this.sessionRepository.create({ user_id: userId, session_token: sessionToken, refresh_token: hashedRefreshToken, ip_address: ip || null, user_agent: userAgent || null, expires_at: expiresAt, is_active: true, }); return this.sessionRepository.save(session); } private toUserResponse(user: User) { const { encrypted_password, ...userWithoutPassword } = user; return { ...userWithoutPassword, emailVerified: !!user.email_confirmed_at, isActive: !user.deleted_at, }; } } ``` --- ## Paso 8: Crear AuthModule ```typescript // src/modules/auth/auth.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtModule } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { User } from './entities/user.entity'; import { UserSession } from './entities/user-session.entity'; import { AuthService } from './services/auth.service'; import { AuthController } from './controllers/auth.controller'; import { JwtStrategy } from './strategies/jwt.strategy'; @Module({ imports: [ PassportModule.register({ defaultStrategy: 'jwt' }), JwtModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ secret: configService.get('JWT_SECRET') || 'dev-secret', signOptions: { expiresIn: configService.get('JWT_EXPIRES_IN') || '15m', }, }), inject: [ConfigService], }), TypeOrmModule.forFeature([User, UserSession]), ], controllers: [AuthController], providers: [AuthService, JwtStrategy], exports: [AuthService, JwtModule, PassportModule], }) export class AuthModule {} ``` --- ## Paso 9: Crear AuthController ```typescript // src/modules/auth/controllers/auth.controller.ts import { Controller, Post, Body, Req, UseGuards, Get, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; import { Request } from 'express'; import { AuthService } from '../services/auth.service'; import { LoginDto, RegisterUserDto, RefreshTokenDto } from '../dto'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; @ApiTags('Auth') @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} @Post('register') @ApiOperation({ summary: 'Registrar nuevo usuario' }) async register(@Body() dto: RegisterUserDto, @Req() req: Request) { const ip = req.ip; const userAgent = req.headers['user-agent']; return this.authService.register(dto, ip, userAgent); } @Post('login') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Login con email y password' }) async login(@Body() dto: LoginDto, @Req() req: Request) { const ip = req.ip; const userAgent = req.headers['user-agent']; return this.authService.login(dto, ip, userAgent); } @Post('refresh') @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Renovar access token' }) async refresh(@Body() dto: RefreshTokenDto) { return this.authService.refreshToken(dto.refreshToken); } @Get('me') @UseGuards(JwtAuthGuard) @ApiBearerAuth() @ApiOperation({ summary: 'Obtener usuario actual' }) async me(@Req() req: Request) { return req.user; } } ``` --- ## Checklist de Implementación - [ ] Dependencias npm instaladas - [ ] DDL de base de datos creado - [ ] Entities creados y alineados con DDL - [ ] DTOs con validaciones - [ ] JWT Strategy configurado - [ ] Guards (JWT y Roles) creados - [ ] AuthService con login/register/refresh - [ ] AuthModule exporta servicios necesarios - [ ] AuthController con endpoints - [ ] Variables de entorno configuradas - [ ] Build pasa sin errores - [ ] Tests básicos funcionando --- ## Troubleshooting ### Error: "Cannot find module 'bcrypt'" ```bash npm install bcrypt npm install -D @types/bcrypt ``` ### Error: "JWT secret not configured" Verificar variable de entorno `JWT_SECRET` en `.env` ### Error: "relation auth.users does not exist" Ejecutar DDL para crear tablas antes de iniciar la aplicación --- ## Código de Referencia Ver implementación completa en: - `projects/gamilit/apps/backend/src/modules/auth/` --- **Versión:** 1.0.0 **Sistema:** SIMCO Catálogo