fix(auth): flexibilizar login por email/usuario, asegurar hash admin123 y corregir cookies sobre HTTP
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
\.
|
||||
|
||||
|
||||
@@ -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'.")
|
||||
if not u_obj.preferred_language:
|
||||
u_obj.preferred_language = "es"
|
||||
if not u_obj.theme_preference:
|
||||
|
||||
+8
-4
@@ -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
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user