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 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."""
|
||||||
|
|||||||
@@ -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
|
||||||
\.
|
\.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
Reference in New Issue
Block a user