From 6907134c5e5acc8ac55f32edf90c5362810cb453 Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Wed, 23 Sep 2026 16:42:11 -0300 Subject: [PATCH] fix(bff): agregar endpoint /settings/demo-mode y proxy /api/v1 en Express Node.js y Flask API --- backend/app/routes/api/admin.py | 71 +++++++++++++++++++ backend/app/routes/api/auth.py | 4 +- frontend/src/app.js | 25 +++++++ frontend/src/routes/admin.js | 58 ++++++++++++++- frontend/src/routes/auth.js | 13 +++- .../views/admin/settings/global_config.html | 9 ++- 6 files changed, 173 insertions(+), 7 deletions(-) diff --git a/backend/app/routes/api/admin.py b/backend/app/routes/api/admin.py index 1e6748b..a90cca6 100644 --- a/backend/app/routes/api/admin.py +++ b/backend/app/routes/api/admin.py @@ -1941,6 +1941,77 @@ def update_auth_providers(): }), 200 +@api_admin_bp.route('/settings/demo-mode', methods=['GET', 'POST']) +@jwt_required +def toggle_demo_mode_api(): + """Habilita o deshabilita el Acceso Rápido Demo y las cuentas default (Bedelía, Docente, Alumno) vía API REST""" + from app.models.setting import SystemSetting + from app.models.user import User + from app.models.audit_log import AuditLog + from sqlalchemy import func + + acting_user = getattr(g, 'jwt_user', None) + if not (acting_user and (acting_user.is_admin() or getattr(acting_user, 'role', '').upper() == 'ADMIN')): + return jsonify({'error': 'Forbidden', 'message': 'Solo administradores pueden modificar el modo demo.'}), 403 + + demo_emails = ['bedelia@edu-space.com', 'docente@edu-space.com', 'alumno@edu-space.com'] + + if request.method == 'GET': + demo_access_enabled = SystemSetting.get_value('demo_access_enabled', 'true') in ['true', 'True', '1', True] + demo_users = User.query.filter(func.lower(User.email).in_(demo_emails)).all() + demo_accounts_active = any(u.is_active for u in demo_users) + return jsonify({ + 'status': 'success', + 'demo_access_enabled': demo_access_enabled, + 'demo_accounts_active': demo_accounts_active + }), 200 + + data = request.get_json(silent=True) or request.form.to_dict() or {} + if 'enabled' in data: + target_state = data['enabled'] in [True, 'true', '1', 'on'] + else: + current_state = SystemSetting.get_value('demo_access_enabled', 'true') in ['true', 'True', '1', True] + target_state = not current_state + + SystemSetting.set_value( + 'demo_access_enabled', + 'true' if target_state else 'false', + 'Acceso Rápido Demo en pantalla de Login', + category='security' + ) + + demo_users = User.query.filter(func.lower(User.email).in_(demo_emails)).all() + for u in demo_users: + u.is_active = target_state + db.session.commit() + + action_name = 'ENABLE_DEMO_ACCOUNTS' if target_state else 'DISABLE_DEMO_ACCOUNTS' + msg_detail = ( + 'Acceso Rápido Demo habilitado y cuentas Bedelía, Docente y Alumno activadas' + if target_state else + 'Acceso Rápido Demo deshabilitado y cuentas default (Bedelía, Docente, Alumno) desactivadas' + ) + try: + audit = AuditLog( + user_id=acting_user.id, + user_email=acting_user.email, + action=action_name, + module='settings', + details=msg_detail + ) + db.session.add(audit) + db.session.commit() + except Exception: + pass + + return jsonify({ + 'status': 'success', + 'message': msg_detail, + 'demo_access_enabled': target_state, + 'demo_accounts_active': target_state + }), 200 + + @api_admin_bp.route('/settings/google-oauth', methods=['POST']) @jwt_required def update_google_oauth_settings(): diff --git a/backend/app/routes/api/auth.py b/backend/app/routes/api/auth.py index c03e495..c48fadc 100644 --- a/backend/app/routes/api/auth.py +++ b/backend/app/routes/api/auth.py @@ -37,6 +37,7 @@ def get_auth_providers(): google_val = SystemSetting.get_value('auth_google_enabled', 'false').lower() in ('true', '1') moodle_val = SystemSetting.get_value('auth_moodle_enabled', 'false').lower() in ('true', '1') google_client_id = SystemSetting.get_value('google_client_id', '') + demo_access_enabled = SystemSetting.get_value('demo_access_enabled', 'true').lower() in ('true', '1') return jsonify({ 'status': 'success', @@ -45,7 +46,8 @@ def get_auth_providers(): 'google': google_val, 'moodle': moodle_val }, - 'google_client_id': google_client_id + 'google_client_id': google_client_id, + 'demo_access_enabled': demo_access_enabled }), 200 diff --git a/frontend/src/app.js b/frontend/src/app.js index d9eb846..7fa8b74 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -229,6 +229,31 @@ const buildingsRoutes = require('./routes/buildings'); const adminRoutes = require('./routes/admin'); const mySubjectsRoutes = require('./routes/my_subjects'); +// Proxy transparente hacia la API REST de Flask para llamadas AJAX frontend +app.use('/api/v1', async (req, res) => { + try { + const token = req.cookies.auth_token; + const targetUrl = `${process.env.FLASK_API_URL || 'http://localhost:5000/api/v1'}${req.url}`; + const headers = { ...req.headers }; + delete headers.host; + if (token && !headers.authorization) { + headers.authorization = `Bearer ${token}`; + } + const resp = await axios({ + method: req.method, + url: targetUrl, + data: req.body, + params: req.query, + headers, + validateStatus: () => true + }); + return res.status(resp.status).json(resp.data); + } catch (err) { + console.error('API v1 proxy error:', err.message); + return res.status(500).json({ error: 'ProxyError', message: 'Error de comunicación con el backend API: ' + err.message }); + } +}); + app.use('/', indexRoutes); app.use('/auth', authRoutes); app.use('/dashboard', dashboardRoutes); diff --git a/frontend/src/routes/admin.js b/frontend/src/routes/admin.js index 71be236..dcee70d 100644 --- a/frontend/src/routes/admin.js +++ b/frontend/src/routes/admin.js @@ -1307,22 +1307,76 @@ const handleGlobalSettings = async (req, res) => { google_oauth: { allowed_domains: 'unicaba.edu.ar', callback_url: '/auth/google/callback' }, moodle: { server_url: 'http://10.0.0.207/moodle', auto_sync_enabled: true, sync_interval_minutes: 15 } }; + let demo_access_enabled = true; + let demo_accounts_active = true; try { const resp = await req.apiClient.get('/admin/settings/all'); - if (resp.data && resp.data.data) { + if (resp.data && resp.data.settings) { + settings = resp.data.settings; + } else if (resp.data && resp.data.data) { settings = resp.data.data; } } catch (e) { console.warn('Global settings fetch notice:', e.message); } + try { + const demoResp = await req.apiClient.get('/settings/demo-mode'); + if (demoResp.data) { + if (demoResp.data.demo_access_enabled !== undefined) { + demo_access_enabled = Boolean(demoResp.data.demo_access_enabled); + } + if (demoResp.data.demo_accounts_active !== undefined) { + demo_accounts_active = Boolean(demoResp.data.demo_accounts_active); + } + } + } catch (e) { + console.warn('Demo mode status fetch notice:', e.message); + } + res.render('admin/settings/global_config', { title: 'Configuración Global del Sistema - Admin Edu-Space', - settings + settings, + demo_access_enabled, + demo_accounts_active }); }; router.get(['/settings', '/global-config', '/config'], handleGlobalSettings); +// Endpoint específico para alternar modo demo y cuentas default +router.post(['/settings/demo-mode', '/demo-mode'], async (req, res) => { + try { + const resp = await req.apiClient.post('/settings/demo-mode', req.body); + return res.status(resp.status || 200).json(resp.data); + } catch (err) { + console.error('Error in POST /admin/settings/demo-mode:', err.response?.data || err.message); + return res.status(err.response?.status || 500).json(err.response?.data || { + status: 'error', + message: 'Error al comunicarse con el servicio de configuración: ' + (err.response?.data?.message || err.message) + }); + } +}); + +// Proxy para cualquier subruta de configuración (smtp, google-oauth, moodle, etc.) +router.all('/settings/*', async (req, res) => { + try { + const subPath = req.params[0]; + const resp = await req.apiClient({ + method: req.method, + url: `/settings/${subPath}`, + data: req.body, + params: req.query + }); + return res.status(resp.status).json(resp.data); + } catch (err) { + console.error(`Error in /admin/settings/${req.params[0]}:`, err.response?.data || err.message); + return res.status(err.response?.status || 500).json(err.response?.data || { + status: 'error', + message: err.response?.data?.message || err.message + }); + } +}); + module.exports = router; diff --git a/frontend/src/routes/auth.js b/frontend/src/routes/auth.js index ddd1007..04e8255 100644 --- a/frontend/src/routes/auth.js +++ b/frontend/src/routes/auth.js @@ -11,11 +11,17 @@ const isSecureCookie = (req) => { router.get('/login', async (req, res) => { let providers = { local: true, google: false, moodle: false }; let google_client_id = ''; + let demo_access_enabled = true; try { const resp = await apiClient.get('/auth/providers'); - if (resp.data && resp.data.providers) { - providers = resp.data.providers; + if (resp.data) { + if (resp.data.providers) { + providers = resp.data.providers; + } google_client_id = resp.data.google_client_id || ''; + if (resp.data.demo_access_enabled !== undefined) { + demo_access_enabled = Boolean(resp.data.demo_access_enabled); + } } } catch (e) { // Fallback default @@ -24,7 +30,8 @@ router.get('/login', async (req, res) => { res.render('auth/login', { error: req.query.error || null, providers, - google_client_id + google_client_id, + demo_access_enabled }); }); diff --git a/frontend/views/admin/settings/global_config.html b/frontend/views/admin/settings/global_config.html index 27c7951..eb41ca2 100644 --- a/frontend/views/admin/settings/global_config.html +++ b/frontend/views/admin/settings/global_config.html @@ -624,7 +624,14 @@ async function toggleDemoMode() { }, body: JSON.stringify({ enabled: targetEnable }) }); - const data = await res.json(); + const contentType = res.headers.get('content-type') || ''; + let data = {}; + if (contentType.includes('application/json')) { + data = await res.json(); + } else { + const rawText = await res.text(); + throw new Error(res.ok ? 'Respuesta no válida' : `Error del servidor (${res.status})`); + } if (res.ok) { showAlert(data.message, targetEnable ? 'info' : 'success'); if (targetEnable) {