From b540015b7e1ec3b9aad440ad7387da5884117fd9 Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Wed, 23 Sep 2026 14:59:46 -0300 Subject: [PATCH] fix(auth): flexibilizar login por email/usuario, asegurar hash admin123 y corregir cookies sobre HTTP --- backend/app/repositories/user_repository.py | 21 ++++++++++++++++++--- backend/classrooms_db.sql | 2 +- backend/init_db.py | 4 +--- frontend/src/app.js | 12 ++++++++---- frontend/src/routes/auth.js | 16 +++++++++++----- frontend/views/auth/login.html | 11 +++++++---- install.sh | 1 + 7 files changed, 47 insertions(+), 20 deletions(-) diff --git a/backend/app/repositories/user_repository.py b/backend/app/repositories/user_repository.py index 5e066bf..50f524b 100644 --- a/backend/app/repositories/user_repository.py +++ b/backend/app/repositories/user_repository.py @@ -1,5 +1,5 @@ from typing import Optional, List -from sqlalchemy import func +from sqlalchemy import func, or_ from app.models.user import User from app.repositories.base_repository import BaseRepository @@ -10,10 +10,25 @@ class UserRepository(BaseRepository[User]): super().__init__(User) def get_by_email(self, email: str) -> Optional[User]: - """Obtiene un usuario por su dirección de email normalizada.""" + """Obtiene un usuario por su dirección de email normalizada o alias de usuario.""" if not email: return None - return User.query.filter(func.lower(User.email) == email.strip().lower()).first() + clean = email.strip().lower() + user = User.query.filter(func.lower(User.email) == clean).first() + if user: + return user + # Si el usuario ingresó solo el nombre de cuenta (ej. 'admin' o 'bedelia') + if '@' not in clean: + user = User.query.filter( + or_( + func.lower(User.email) == f"{clean}@edu-space.com", + func.lower(User.name) == clean, + func.lower(User.role) == clean.upper() + ) + ).first() + if user: + return user + return None def get_active_users(self) -> List[User]: """Obtiene todos los usuarios con cuenta activa.""" diff --git a/backend/classrooms_db.sql b/backend/classrooms_db.sql index 81344db..dec3134 100644 --- a/backend/classrooms_db.sql +++ b/backend/classrooms_db.sql @@ -1647,7 +1647,7 @@ COPY public.subjects (id, code, name, description, department, credits, active, -- COPY public.users (id, email, password_hash, name, role, is_active, created_at, last_login, preferred_language, theme_preference, role_id) FROM stdin; -1 admin@edu-space.com pbkdf2:sha256:600000$oIjd4T0sIDkJeOcn$129d545ff9af76998ab815cc403f588fa7137a5d0dc78941a531b0c88d776b0a System Administrator ADMIN t 2026-09-03 02:08:29.597255 2026-09-05 01:32:51.778156 es light 1 +1 admin@edu-space.com pbkdf2:sha256:1000000$7UJAkGOl42ZHvDk8$f38ef5fe9b7247692b812abe9199df2d25c015e04b3807267b0f43536817f5c6 System Administrator ADMIN t 2026-09-03 02:08:29.597255 2026-09-05 01:32:51.778156 es light 1 3 carlostellocba@gmail.com pbkdf2:sha256:600000$O0J5tLN2iWVNAZlB$ac8d4b424e4272e1c15703ff53626432f69396877ed3f8be283821a1147c4a05 Carlos Tello Alumno t 2026-09-03 04:42:24.060312 2026-09-03 07:20:39.403354 \N dark 6 \. diff --git a/backend/init_db.py b/backend/init_db.py index 16aaf40..489ed26 100644 --- a/backend/init_db.py +++ b/backend/init_db.py @@ -364,9 +364,7 @@ def init_database(): u_obj.role_id = r_match.id u_obj.role = u_info['role_code'] u_obj.is_active = True - if not u_obj.check_password("admin123"): - u_obj.set_password("admin123") - print(f" [*] Clave de {u_info['email']} restablecida a 'admin123'.") + u_obj.set_password("admin123") if not u_obj.preferred_language: u_obj.preferred_language = "es" if not u_obj.theme_preference: diff --git a/frontend/src/app.js b/frontend/src/app.js index 52ed7fd..4994027 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -40,17 +40,20 @@ const app = express(); const port = process.env.PORT || 3000; app.disable('x-powered-by'); +app.set('trust proxy', 1); -// Blindaje HTTP con Helmet +// Blindaje HTTP con Helmet (compatible con despliegues directos por IP y proxies HTTPS) app.use(helmet({ contentSecurityPolicy: false, - crossOriginEmbedderPolicy: false + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + originAgentCluster: false })); // Rate Limiting para protección contra ataques de fuerza bruta y abuso const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 30, + max: 60, message: 'Demasiados intentos de autenticación. Por favor intente nuevamente en 15 minutos.', standardHeaders: true, legacyHeaders: false @@ -61,7 +64,8 @@ const generalLimiter = rateLimit({ standardHeaders: true, legacyHeaders: false }); -app.use('/auth/login', authLimiter); +app.post('/auth/login', authLimiter); +app.post('/auth/moodle', authLimiter); app.use('/api', generalLimiter); // Configuración de middlewares diff --git a/frontend/src/routes/auth.js b/frontend/src/routes/auth.js index 2c19e3e..1963030 100644 --- a/frontend/src/routes/auth.js +++ b/frontend/src/routes/auth.js @@ -2,6 +2,12 @@ const express = require('express'); const router = express.Router(); const apiClient = require('../services/apiClient'); +const isSecureCookie = (req) => { + if (process.env.COOKIE_SECURE === 'true') return true; + if (process.env.COOKIE_SECURE === 'false') return false; + return Boolean(req.secure || req.protocol === 'https' || req.get('x-forwarded-proto') === 'https'); +}; + router.get('/login', async (req, res) => { let providers = { local: true, google: false, moodle: false }; let google_client_id = ''; @@ -38,7 +44,7 @@ router.post('/login', async (req, res) => { if (token) { res.cookie('auth_token', token, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(req), sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 // 1 día }); @@ -66,7 +72,7 @@ router.post('/moodle', async (req, res) => { if (token) { res.cookie('auth_token', token, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(req), sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 }); @@ -89,7 +95,7 @@ router.post('/google', async (req, res) => { if (token) { res.cookie('auth_token', token, { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(req), sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 }); @@ -130,12 +136,12 @@ router.get('/impersonate/:id', requireAuth, async (req, res) => { if (targetUser) { res.cookie('impersonate_user_id', targetUserId.toString(), { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(req), sameSite: 'lax' }); res.cookie('impersonate_user_data', JSON.stringify(targetUser), { httpOnly: true, - secure: process.env.NODE_ENV === 'production', + secure: isSecureCookie(req), sameSite: 'lax' }); } diff --git a/frontend/views/auth/login.html b/frontend/views/auth/login.html index 325e7c3..6ea59ef 100644 --- a/frontend/views/auth/login.html +++ b/frontend/views/auth/login.html @@ -270,17 +270,20 @@ }); } - // Demo login autofill buttons + // Demo login autofill & instant submit (1 Clic) document.querySelectorAll('.demo-fill-btn').forEach(btn => { - btn.addEventListener('click', function() { + btn.addEventListener('click', function(e) { + e.preventDefault(); const email = this.getAttribute('data-email'); const usrField = document.getElementById('username'); const pwdField = document.getElementById('password'); + const form = document.getElementById('localLoginForm'); if (usrField && pwdField) { usrField.value = email; pwdField.value = 'admin123'; - usrField.classList.add('is-valid'); - setTimeout(() => usrField.classList.remove('is-valid'), 1500); + if (form) { + form.submit(); + } } }); }); diff --git a/install.sh b/install.sh index e0bfe07..085987b 100755 --- a/install.sh +++ b/install.sh @@ -294,6 +294,7 @@ else PORT=3000 FLASK_API_URL=http://127.0.0.1:5000/api/v1 NODE_ENV=production +COOKIE_SECURE=auto FRONTENVEOF chmod 640 "$FRONTEND_ENV" ok "frontend/.env generado con configuración conectada al backend local (puerto 3000)"