diff --git a/Backend/app-talen-backend/README.md b/Backend/app-talen-backend/README.md index 2f97f63..7f6016c 100644 --- a/Backend/app-talen-backend/README.md +++ b/Backend/app-talen-backend/README.md @@ -97,12 +97,20 @@ DB_DATABASE=talent_db TYPEORM_SYNC=true JWT_SECRET=change-this-secret JWT_EXPIRES_IN=7d +SEED_DEFAULT_ADMIN=true +DEFAULT_ADMIN_EMAIL=admin@talen.local +DEFAULT_ADMIN_PASSWORD=AdminTemp2026! +DEFAULT_ADMIN_FORCE_SYNC=false ``` `TYPEORM_SYNC=true` permite que TypeORM cree o sincronice tablas automaticamente durante desarrollo. Para produccion se recomienda usar migraciones y dejarlo en `false`. `JWT_EXPIRES_IN=7d` define que los tokens de autenticacion expiran a los 7 dias. +Cuando `SEED_DEFAULT_ADMIN=true`, al iniciar la API se crea automaticamente un usuario ADMIN en la base de datos si no existe. Los datos se configuran con `DEFAULT_ADMIN_EMAIL` y `DEFAULT_ADMIN_PASSWORD`. + +Si `DEFAULT_ADMIN_FORCE_SYNC=true`, en cada inicio se vuelve a sincronizar ese usuario (rol ADMIN y password hasheada a partir de `DEFAULT_ADMIN_PASSWORD`). + ## Autenticacion Se implementa autenticacion con JWT en el modulo `auth`. diff --git a/Backend/app-talen-backend/docker-compose.yml b/Backend/app-talen-backend/docker-compose.yml index 558b9b5..857fc4d 100644 --- a/Backend/app-talen-backend/docker-compose.yml +++ b/Backend/app-talen-backend/docker-compose.yml @@ -14,6 +14,10 @@ services: TYPEORM_SYNC: ${TYPEORM_SYNC:-false} JWT_SECRET: ${JWT_SECRET} JWT_EXPIRES_IN: ${JWT_EXPIRES_IN} + SEED_DEFAULT_ADMIN: ${SEED_DEFAULT_ADMIN:-true} + DEFAULT_ADMIN_EMAIL: ${DEFAULT_ADMIN_EMAIL:-admin@talen.local} + DEFAULT_ADMIN_PASSWORD: ${DEFAULT_ADMIN_PASSWORD:-AdminTemp2026!} + DEFAULT_ADMIN_FORCE_SYNC: ${DEFAULT_ADMIN_FORCE_SYNC:-false} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} ports: diff --git a/Backend/app-talen-backend/src/modules/auth/application/admin-bootstrap.service.ts b/Backend/app-talen-backend/src/modules/auth/application/admin-bootstrap.service.ts new file mode 100644 index 0000000..8c13c55 --- /dev/null +++ b/Backend/app-talen-backend/src/modules/auth/application/admin-bootstrap.service.ts @@ -0,0 +1,90 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { InjectRepository } from '@nestjs/typeorm'; +import * as bcrypt from 'bcrypt'; +import { QueryFailedError } from 'typeorm'; +import { Repository } from 'typeorm'; +import { UserRole } from '../../users/domain/user-role.enum'; +import { User } from '../../users/infrastructure/entities/user.entity'; + +@Injectable() +export class AdminBootstrapService implements OnModuleInit { + private readonly logger = new Logger(AdminBootstrapService.name); + + constructor( + @InjectRepository(User) + private readonly usersRepository: Repository, + private readonly configService: ConfigService, + ) {} + + async onModuleInit(): Promise { + const seedEnabled = + this.configService.get('SEED_DEFAULT_ADMIN', 'true') === 'true'; + + if (!seedEnabled) { + this.logger.log('Default admin seed disabled (SEED_DEFAULT_ADMIN=false)'); + return; + } + + const email = this.configService + .get('DEFAULT_ADMIN_EMAIL', 'admin@talen.local') + .trim() + .toLowerCase(); + const plainPassword = this.configService.get( + 'DEFAULT_ADMIN_PASSWORD', + 'AdminTemp2026!', + ); + const forcePasswordSync = + this.configService.get('DEFAULT_ADMIN_FORCE_SYNC', 'false') === + 'true'; + + if (!email || !plainPassword) { + this.logger.warn( + 'Default admin seed skipped: missing DEFAULT_ADMIN_EMAIL or DEFAULT_ADMIN_PASSWORD', + ); + return; + } + + const existingUser = await this.usersRepository.findOne({ + where: { email }, + }); + + if (!existingUser) { + const hashedPassword = await bcrypt.hash(plainPassword, 10); + const user = this.usersRepository.create({ + email, + password: hashedPassword, + role: UserRole.ADMIN, + }); + try { + await this.usersRepository.save(user); + } catch (error) { + // If multiple instances boot at the same time, another instance may create the user first. + if (!this.isUniqueViolation(error)) { + throw error; + } + } + this.logger.log(`Default admin created: ${email}`); + return; + } + + if (!forcePasswordSync) { + this.logger.log(`Default admin already exists: ${email}`); + return; + } + + existingUser.password = await bcrypt.hash(plainPassword, 10); + existingUser.role = UserRole.ADMIN; + await this.usersRepository.save(existingUser); + this.logger.log(`Default admin synchronized: ${email}`); + } + + private isUniqueViolation(error: unknown): boolean { + if (!(error instanceof QueryFailedError)) { + return false; + } + + const code = (error as QueryFailedError & { code?: string }).code; + return code === '23505'; + } +} diff --git a/Backend/app-talen-backend/src/modules/auth/auth.module.ts b/Backend/app-talen-backend/src/modules/auth/auth.module.ts index 9a37013..faaa3ba 100644 --- a/Backend/app-talen-backend/src/modules/auth/auth.module.ts +++ b/Backend/app-talen-backend/src/modules/auth/auth.module.ts @@ -13,6 +13,7 @@ import { AuthController } from './infrastructure/auth.controller'; import { LinkedInStrategy } from './infrastructure/strategies/linkedin.strategy'; import { GoogleStrategy } from './infrastructure/strategies/google.strategy'; import { MailModule } from '../mail/mail.module'; +import { AdminBootstrapService } from './application/admin-bootstrap.service'; @Module({ imports: [ @@ -50,6 +51,7 @@ import { MailModule } from '../mail/mail.module'; LinkedInAuthGuard, LinkedInStrategy, GoogleStrategy, + AdminBootstrapService, ], exports: [JwtModule, JwtAuthGuard], }) diff --git a/Frontend/appTalenFront/vercel.json b/Frontend/appTalenFront/vercel.json new file mode 100644 index 0000000..1323cda --- /dev/null +++ b/Frontend/appTalenFront/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..1323cda --- /dev/null +++ b/vercel.json @@ -0,0 +1,8 @@ +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +}