fix(auth): flexibilizar login por email/usuario, asegurar hash admin123 y corregir cookies sobre HTTP

This commit is contained in:
2026-09-23 14:59:46 -03:00
parent bddd382c96
commit b540015b7e
7 changed files with 47 additions and 20 deletions
+18 -3
View File
@@ -1,5 +1,5 @@
from typing import Optional, List from typing import Optional, List
from sqlalchemy import func from sqlalchemy import func, or_
from app.models.user import User from app.models.user import User
from app.repositories.base_repository import BaseRepository from app.repositories.base_repository import BaseRepository
@@ -10,10 +10,25 @@ class UserRepository(BaseRepository[User]):
super().__init__(User) super().__init__(User)
def get_by_email(self, email: str) -> Optional[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: if not email:
return None 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]: def get_active_users(self) -> List[User]:
"""Obtiene todos los usuarios con cuenta activa.""" """Obtiene todos los usuarios con cuenta activa."""
+1 -1
View File
@@ -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; 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 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
\. \.
+1 -3
View File
@@ -364,9 +364,7 @@ def init_database():
u_obj.role_id = r_match.id u_obj.role_id = r_match.id
u_obj.role = u_info['role_code'] u_obj.role = u_info['role_code']
u_obj.is_active = True u_obj.is_active = True
if not u_obj.check_password("admin123"): u_obj.set_password("admin123")
u_obj.set_password("admin123")
print(f" [*] Clave de {u_info['email']} restablecida a 'admin123'.")
if not u_obj.preferred_language: if not u_obj.preferred_language:
u_obj.preferred_language = "es" u_obj.preferred_language = "es"
if not u_obj.theme_preference: if not u_obj.theme_preference:
+8 -4
View File
@@ -40,17 +40,20 @@ const app = express();
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
app.disable('x-powered-by'); 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({ app.use(helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
crossOriginEmbedderPolicy: false crossOriginEmbedderPolicy: false,
crossOriginOpenerPolicy: false,
originAgentCluster: false
})); }));
// Rate Limiting para protección contra ataques de fuerza bruta y abuso // Rate Limiting para protección contra ataques de fuerza bruta y abuso
const authLimiter = rateLimit({ const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 30, max: 60,
message: 'Demasiados intentos de autenticación. Por favor intente nuevamente en 15 minutos.', message: 'Demasiados intentos de autenticación. Por favor intente nuevamente en 15 minutos.',
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
@@ -61,7 +64,8 @@ const generalLimiter = rateLimit({
standardHeaders: true, standardHeaders: true,
legacyHeaders: false legacyHeaders: false
}); });
app.use('/auth/login', authLimiter); app.post('/auth/login', authLimiter);
app.post('/auth/moodle', authLimiter);
app.use('/api', generalLimiter); app.use('/api', generalLimiter);
// Configuración de middlewares // Configuración de middlewares
+11 -5
View File
@@ -2,6 +2,12 @@ const express = require('express');
const router = express.Router(); const router = express.Router();
const apiClient = require('../services/apiClient'); 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) => { router.get('/login', async (req, res) => {
let providers = { local: true, google: false, moodle: false }; let providers = { local: true, google: false, moodle: false };
let google_client_id = ''; let google_client_id = '';
@@ -38,7 +44,7 @@ router.post('/login', async (req, res) => {
if (token) { if (token) {
res.cookie('auth_token', token, { res.cookie('auth_token', token, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: isSecureCookie(req),
sameSite: 'lax', sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 // 1 día maxAge: 24 * 60 * 60 * 1000 // 1 día
}); });
@@ -66,7 +72,7 @@ router.post('/moodle', async (req, res) => {
if (token) { if (token) {
res.cookie('auth_token', token, { res.cookie('auth_token', token, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: isSecureCookie(req),
sameSite: 'lax', sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 maxAge: 24 * 60 * 60 * 1000
}); });
@@ -89,7 +95,7 @@ router.post('/google', async (req, res) => {
if (token) { if (token) {
res.cookie('auth_token', token, { res.cookie('auth_token', token, {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: isSecureCookie(req),
sameSite: 'lax', sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 maxAge: 24 * 60 * 60 * 1000
}); });
@@ -130,12 +136,12 @@ router.get('/impersonate/:id', requireAuth, async (req, res) => {
if (targetUser) { if (targetUser) {
res.cookie('impersonate_user_id', targetUserId.toString(), { res.cookie('impersonate_user_id', targetUserId.toString(), {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: isSecureCookie(req),
sameSite: 'lax' sameSite: 'lax'
}); });
res.cookie('impersonate_user_data', JSON.stringify(targetUser), { res.cookie('impersonate_user_data', JSON.stringify(targetUser), {
httpOnly: true, httpOnly: true,
secure: process.env.NODE_ENV === 'production', secure: isSecureCookie(req),
sameSite: 'lax' sameSite: 'lax'
}); });
} }
+7 -4
View File
@@ -270,17 +270,20 @@
}); });
} }
// Demo login autofill buttons // Demo login autofill & instant submit (1 Clic)
document.querySelectorAll('.demo-fill-btn').forEach(btn => { 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 email = this.getAttribute('data-email');
const usrField = document.getElementById('username'); const usrField = document.getElementById('username');
const pwdField = document.getElementById('password'); const pwdField = document.getElementById('password');
const form = document.getElementById('localLoginForm');
if (usrField && pwdField) { if (usrField && pwdField) {
usrField.value = email; usrField.value = email;
pwdField.value = 'admin123'; pwdField.value = 'admin123';
usrField.classList.add('is-valid'); if (form) {
setTimeout(() => usrField.classList.remove('is-valid'), 1500); form.submit();
}
} }
}); });
}); });
+1
View File
@@ -294,6 +294,7 @@ else
PORT=3000 PORT=3000
FLASK_API_URL=http://127.0.0.1:5000/api/v1 FLASK_API_URL=http://127.0.0.1:5000/api/v1
NODE_ENV=production NODE_ENV=production
COOKIE_SECURE=auto
FRONTENVEOF FRONTENVEOF
chmod 640 "$FRONTEND_ENV" chmod 640 "$FRONTEND_ENV"
ok "frontend/.env generado con configuración conectada al backend local (puerto 3000)" ok "frontend/.env generado con configuración conectada al backend local (puerto 3000)"