From 29d6f902b6b3bbace81e3f71c7f4fa7b5ef414e5 Mon Sep 17 00:00:00 2001 From: mberlin Date: Wed, 18 Mar 2026 17:47:09 -0300 Subject: [PATCH] Add real user auth (register/login) + admin UI login --- public/admin.html | 164 +++++++++++++++++++++++++++--- src/core/api/auth.controller.ts | 39 +++++-- src/core/auth.guard.ts | 30 ++++-- src/core/auth.module.ts | 8 +- src/core/entities/user.entity.ts | 48 +++++++++ src/core/services/user.service.ts | 34 +++++++ 6 files changed, 295 insertions(+), 28 deletions(-) create mode 100644 src/core/entities/user.entity.ts create mode 100644 src/core/services/user.service.ts diff --git a/public/admin.html b/public/admin.html index 7d6dc37..db40d4d 100644 --- a/public/admin.html +++ b/public/admin.html @@ -34,23 +34,81 @@

Admin Panel

Modular admin UI: cada módulo se registra automáticamente.

- - - +
+

Acceso

+
+ Iniciar sesión
+ + + +
+
+ Registrar usuario
+ + + + + +
+
-
+ diff --git a/src/core/api/auth.controller.ts b/src/core/api/auth.controller.ts index 360583f..721d7d8 100644 --- a/src/core/api/auth.controller.ts +++ b/src/core/api/auth.controller.ts @@ -1,12 +1,39 @@ -import { Controller, Post, Body } from '@nestjs/common'; -import { TokenService } from '../services/token.service'; +import { Body, Controller, Post } from '@nestjs/common'; +import { AuthService } from '../services/auth.service'; +import { UserService } from '../services/user.service'; @Controller('auth') export class AuthController { - constructor(private readonly tokenService: TokenService) {} + constructor( + private readonly authService: AuthService, + private readonly userService: UserService, + ) {} - @Post('token') - createToken(@Body() body: { name?: string; expiresInHours?: number }) { - return this.tokenService.createToken(body.name, body.expiresInHours); + @Post('register') + async register( + @Body() + body: { + tenantId?: string; + name?: string; + email: string; + password: string; + }, + ) { + const tenantId = body.tenantId || process.env.DEFAULT_TENANT_ID || 'default'; + const user = await this.userService.createUser(tenantId, body.email, body.password, body.name); + return { id: user.id, email: user.email, name: user.name }; + } + + @Post('login') + async login( + @Body() body: { tenantId?: string; email: string; password: string }, + ) { + const tenantId = body.tenantId || process.env.DEFAULT_TENANT_ID || 'default'; + const user = await this.userService.validateUser(tenantId, body.email, body.password); + if (!user) { + return { message: 'Invalid credentials' }; + } + const token = this.authService.sign({ sub: user.id, name: user.name, tenantId }); + return { token }; } } diff --git a/src/core/auth.guard.ts b/src/core/auth.guard.ts index 1718b25..a42633c 100644 --- a/src/core/auth.guard.ts +++ b/src/core/auth.guard.ts @@ -1,9 +1,13 @@ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './services/auth.service'; +import { TokenService } from './services/token.service'; @Injectable() export class AuthGuard implements CanActivate { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly tokenService: TokenService, + ) {} canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); @@ -14,18 +18,32 @@ export class AuthGuard implements CanActivate { return true; } + // Allow auth endpoints (login/register) without a token. + if (request.method === 'POST' && request.url?.startsWith('/api/auth/')) { + return true; + } + const authHeader = request.headers['authorization'] || ''; const token = Array.isArray(authHeader) ? authHeader[0] : authHeader; if (!token) { throw new UnauthorizedException(); } - const payload = this.authService.verify(token.replace(/^Bearer\s+/i, '')); - if (!payload) { - throw new UnauthorizedException(); + const raw = token.replace(/^Bearer\s+/i, ''); + + // Try JWT first, then fallback to simple token-based auth. + const payload = this.authService.verify(raw); + if (payload) { + request.user = payload; + return true; } - request.user = payload; - return true; + const validToken = this.tokenService.validate(raw); + if (validToken) { + request.user = { token: raw, name: validToken.name }; + return true; + } + + throw new UnauthorizedException(); } } diff --git a/src/core/auth.module.ts b/src/core/auth.module.ts index 19445b8..a96cecd 100644 --- a/src/core/auth.module.ts +++ b/src/core/auth.module.ts @@ -1,10 +1,14 @@ import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; import { AuthService } from './services/auth.service'; import { TokenService } from './services/token.service'; +import { UserService } from './services/user.service'; import { AuthGuard } from './auth.guard'; +import { UserSchema } from './entities/user.entity'; @Module({ - providers: [AuthService, TokenService, AuthGuard], - exports: [AuthService, TokenService, AuthGuard], + imports: [MikroOrmModule.forFeature([UserSchema])], + providers: [AuthService, TokenService, UserService, AuthGuard], + exports: [AuthService, TokenService, UserService, AuthGuard], }) export class AuthModule {} diff --git a/src/core/entities/user.entity.ts b/src/core/entities/user.entity.ts new file mode 100644 index 0000000..8f84a14 --- /dev/null +++ b/src/core/entities/user.entity.ts @@ -0,0 +1,48 @@ +import { EntitySchema } from '@mikro-orm/core'; +import { v4 as uuidv4 } from 'uuid'; +import { BaseEntity } from './base.entity'; + +export class User extends BaseEntity { + email!: string; + name?: string; + passwordHash!: string; +} + +export const UserSchema = new EntitySchema({ + class: User, + tableName: 'users', + properties: { + id: { + type: 'uuid', + primary: true, + default: uuidv4(), + }, + tenantId: { + type: 'string', + }, + metadata: { + type: 'json', + nullable: true, + }, + createdAt: { + type: 'date', + default: new Date(), + }, + updatedAt: { + type: 'date', + default: new Date(), + }, + email: { + type: 'string', + nullable: false, + }, + name: { + type: 'string', + nullable: true, + }, + passwordHash: { + type: 'string', + nullable: false, + }, + }, +}); diff --git a/src/core/services/user.service.ts b/src/core/services/user.service.ts new file mode 100644 index 0000000..30c2c46 --- /dev/null +++ b/src/core/services/user.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@nestjs/common'; +import { EntityRepository } from '@mikro-orm/core'; +import { InjectRepository } from '@mikro-orm/nestjs'; +import * as bcrypt from 'bcryptjs'; +import { User } from '../entities/user.entity'; + +@Injectable() +export class UserService { + constructor( + @InjectRepository(User) + private readonly userRepository: EntityRepository, + ) {} + + async createUser(tenantId: string, email: string, password: string, name?: string) { + const existing = await this.userRepository.findOne({ tenantId, email }); + if (existing) { + throw new Error('User already exists'); + } + const user = new User(); + user.tenantId = tenantId; + user.email = email; + user.name = name; + user.passwordHash = await bcrypt.hash(password, 10); + await this.userRepository.persistAndFlush(user); + return user; + } + + async validateUser(tenantId: string, email: string, password: string) { + const user = await this.userRepository.findOne({ tenantId, email }); + if (!user) return null; + const isValid = await bcrypt.compare(password, user.passwordHash); + return isValid ? user : null; + } +}