feat(auth): estrategia multicanal (WebControl + Google/Facebook + MariaDB Staff) y rediseño de login con mascota bot
This commit is contained in:
@@ -77,7 +77,13 @@ help_desk/
|
|||||||
│ └── settings.ejs # Configuración general de la empresa
|
│ └── settings.ejs # Configuración general de la empresa
|
||||||
└── public/
|
└── public/
|
||||||
├── css/
|
├── css/
|
||||||
│ └── style.css # Estilos personalizados de la interfaz
|
│ └── style.css # Estilos personalizados, responsive, tabs y botones sociales
|
||||||
|
├── img/
|
||||||
|
│ ├── logo.svg # Isotipo y logotipo base
|
||||||
|
│ ├── favicon* # Favicons multi-resolución
|
||||||
|
│ ├── bot-soporte.svg # Mascota oficial vectorizada del Bot de Soporte
|
||||||
|
│ ├── bot-soporte.png # Mascota en raster alta resolución (192x192)
|
||||||
|
│ └── bot-soporte-512.png # Mascota en raster alta resolución (512x512)
|
||||||
└── js/
|
└── js/
|
||||||
└── main.js # Lógica JavaScript en navegador
|
└── main.js # Lógica JavaScript en navegador
|
||||||
```
|
```
|
||||||
@@ -89,8 +95,10 @@ help_desk/
|
|||||||
| Método | Ruta | Middleware de Acceso | Descripción |
|
| Método | Ruta | Middleware de Acceso | Descripción |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **GET** | `/` | Sesión / Redirección | Redirige al dashboard correspondiente según el rol del usuario |
|
| **GET** | `/` | Sesión / Redirección | Redirige al dashboard correspondiente según el rol del usuario |
|
||||||
| **GET** | `/login` | Público | Renderiza formulario de login |
|
| **GET** | `/login` | Público | Renderiza formulario de login con selector de perfil (Clientes vs Staff) |
|
||||||
| **POST** | `/login` | Público | Valida credenciales e inicializa sesión |
|
| **POST** | `/login` | Público | Valida credenciales (Clientes vía WebControl, Personal técnico vía MariaDB local) |
|
||||||
|
| **POST** | `/auth/google` | Público | Autenticación social con Google OAuth (GIS ID Token) para clientes |
|
||||||
|
| **POST** | `/auth/facebook` | Público | Autenticación social con Facebook OAuth (Graph API) para clientes |
|
||||||
| **GET** | `/register` | Público | Formulario de registro de cliente |
|
| **GET** | `/register` | Público | Formulario de registro de cliente |
|
||||||
| **POST** | `/register` | Público | Crea usuario nuevo con rol `client` |
|
| **POST** | `/register` | Público | Crea usuario nuevo con rol `client` |
|
||||||
| **GET** | `/logout` | `isAuthenticated` | Cierra la sesión y redirige a login |
|
| **GET** | `/logout` | `isAuthenticated` | Cierra la sesión y redirige a login |
|
||||||
|
|||||||
@@ -56,6 +56,12 @@
|
|||||||
- **Dominio Público y Proxy Reverso:** Dominio oficial `https://soporte.sigmac.app`. El tráfico público ingresa por la WAN hacia `Servidor-web` CT 109 (`192.168.10.109` Apache 2.4.66), que enruta hacia el upstream interno `http://192.168.10.160:3008/` confirmado por SIAX Monitor (`/api/ports`).
|
- **Dominio Público y Proxy Reverso:** Dominio oficial `https://soporte.sigmac.app`. El tráfico público ingresa por la WAN hacia `Servidor-web` CT 109 (`192.168.10.109` Apache 2.4.66), que enruta hacia el upstream interno `http://192.168.10.160:3008/` confirmado por SIAX Monitor (`/api/ports`).
|
||||||
- **Inventario MCP de Infraestructura:** Registrado oficialmente como **Servicio ID 35** (`category: application`, `type: webapp`, `status: ACTIVE`, `url: https://soporte.sigmac.app`).
|
- **Inventario MCP de Infraestructura:** Registrado oficialmente como **Servicio ID 35** (`category: application`, `type: webapp`, `status: ACTIVE`, `url: https://soporte.sigmac.app`).
|
||||||
|
|
||||||
|
10. **Estrategia Multicanal de Autenticación y Mascota Bot de Soporte:**
|
||||||
|
- **Clientes de la Plataforma:** Autentican directamente contra la API central de Telcotronics `API-SIGMA-WEBCONTROL` (`http://192.168.10.160:3002/api/auth/login` con fallback a `https://api.siax-system.net/api/auth/login`). Al validar credenciales, se realiza aprovisionamiento o actualización transparente en `help_desk.users` (`provider='webcontrol'`) y se inicia sesión con rol `client`.
|
||||||
|
- **Nuevos Clientes (Social OAuth):** Integración nativa con Google Identity Services (GIS) verificando ID Tokens contra `https://oauth2.googleapis.com/tokeninfo`, y Facebook OAuth vía Graph API. Auto-aprovisiona usuarios como clientes sin fricción.
|
||||||
|
- **Personal Técnico y Staff:** La consola de inicio de sesión local MariaDB queda reservada exclusivamente para el personal operativo (`superadmin`, `admin`, `agent`). Se bloquea el acceso de clientes comunes por la consola técnica.
|
||||||
|
- **Identidad Visual y Mascota:** Rediseño integral de la página de inicio de sesión (`/login`) con control segmentado de pestañas (Clientes vs Personal Técnico) y la mascota oficial robot de soporte (`/img/bot-soporte.svg`, `/img/bot-soporte.png` y `/img/bot-soporte-512.png`) con animación de levitación y estado activo 24/7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Base de Datos
|
## Base de Datos
|
||||||
@@ -65,7 +71,7 @@
|
|||||||
- **Base de datos:** `help_desk`
|
- **Base de datos:** `help_desk`
|
||||||
- **Tablas:**
|
- **Tablas:**
|
||||||
- `companies`: Gestión de empresas/organizaciones (id, name, slug, plan, max_users, max_tickets, is_active, timestamps).
|
- `companies`: Gestión de empresas/organizaciones (id, name, slug, plan, max_users, max_tickets, is_active, timestamps).
|
||||||
- `users`: Usuarios de cada empresa (id, company_id, nombre, email, password, rol, is_active, invite_token, invited_by, timestamps).
|
- `users`: Usuarios de cada empresa (id, company_id, nombre, email, password [nullable], rol, provider, provider_id, avatar_url, is_active, invite_token, invited_by, timestamps).
|
||||||
- `departments`: Departamentos de soporte por empresa (id, company_id, nombre, descripcion, created_at).
|
- `departments`: Departamentos de soporte por empresa (id, company_id, nombre, descripcion, created_at).
|
||||||
- `tickets`: Tickets de soporte (id, company_id, user_id, department_id, asunto, mensaje, status, prioridad, assigned_to, timestamps).
|
- `tickets`: Tickets de soporte (id, company_id, user_id, department_id, asunto, mensaje, status, prioridad, assigned_to, timestamps).
|
||||||
- `ticket_respuestas`: Mensajes e hilo de conversación de cada ticket (id, ticket_id, user_id, mensaje, created_at).
|
- `ticket_respuestas`: Mensajes e hilo de conversación de cada ticket (id, ticket_id, user_id, mensaje, created_at).
|
||||||
|
|||||||
@@ -34,3 +34,13 @@
|
|||||||
- **Alcance:** Despliegues de la aplicación a producción (`desplegar.sh`, `servers.conf`).
|
- **Alcance:** Despliegues de la aplicación a producción (`desplegar.sh`, `servers.conf`).
|
||||||
- **Restricción:** El despliegue debe utilizar `servers.conf` (copiado desde `servers.conf.example`, nunca commiteado). No utilizar `sudo` interactivo ni contraseñas fijas en el servidor; el ciclo de vida, reinicio y auditoría de salud debe coordinarse a través de la API REST de **SIAX Monitor** en `http://$HOST:8080` (`POST /api/apps/HELPDESK/restart` y `GET /api/apps/HELPDESK/status`).
|
- **Restricción:** El despliegue debe utilizar `servers.conf` (copiado desde `servers.conf.example`, nunca commiteado). No utilizar `sudo` interactivo ni contraseñas fijas en el servidor; el ciclo de vida, reinicio y auditoría de salud debe coordinarse a través de la API REST de **SIAX Monitor** en `http://$HOST:8080` (`POST /api/apps/HELPDESK/restart` y `GET /api/apps/HELPDESK/status`).
|
||||||
- **Motivo:** Cumplimiento de la política oficial de despliegue del ecosistema Telcotronics (Prompt 8 y Feedback #25).
|
- **Motivo:** Cumplimiento de la política oficial de despliegue del ecosistema Telcotronics (Prompt 8 y Feedback #25).
|
||||||
|
|
||||||
|
## 8. Estrategia Multicanal de Autenticación
|
||||||
|
- **Alcance:** Autenticación de usuarios, modelos de acceso y vistas de login (`/login`, `/auth/google`, `/auth/facebook`, `User.js`, `views/auth/login.ejs`).
|
||||||
|
- **Restricción:**
|
||||||
|
1. **Clientes de la Plataforma:** Autentican contra la API central `API-SIGMA-WEBCONTROL` (`http://192.168.10.160:3002/api/auth/login` con fallback a `https://api.siax-system.net/api/auth/login`), aprovisionándose automáticamente con rol `client`.
|
||||||
|
2. **Nuevos Clientes:** Autenticación social federada vía Google OAuth (GIS tokeninfo) y Facebook OAuth (Graph API), aprovisionándose como clientes.
|
||||||
|
3. **Personal Técnico y Staff:** Acceso local sobre MariaDB `help_desk.users` reservado exclusivamente para roles operativos (`superadmin`, `admin`, `agent`). Queda prohibido el ingreso de clientes por la consola técnica.
|
||||||
|
4. **Seguridad de Credenciales:** La tabla `users` soporta contraseñas opcionales (`NULL`) para cuentas federadas, rastreando `provider`, `provider_id` y `avatar_url`.
|
||||||
|
- **Motivo:** Integración sin fisuras con el ecosistema de cuentas SIGMA WebControl y simplificación de onboarding para clientes externos.
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
| Fecha | Tarea / Entregable | Responsable | Archivos Afectados |
|
| Fecha | Tarea / Entregable | Responsable | Archivos Afectados |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| 2026-10-20 | **Autenticación Multicanal (WebControl + Google/Facebook + Local Staff) y Rediseño de Login con Mascota Bot** | Agente IA | `views/auth/login.ejs`, `public/css/style.css`, `public/img/bot-soporte.*`, `routes/auth.js`, `models/User.js`, `config/setup.js`, `.env*` |
|
||||||
| 2026-10-20 | **Configuración de VirtualHost Apache y SSL para `soporte.sigmac.app`** | Pablinux / Agente IA | Servidor-web (`192.168.10.109`), Apache 2.4.66 |
|
| 2026-10-20 | **Configuración de VirtualHost Apache y SSL para `soporte.sigmac.app`** | Pablinux / Agente IA | Servidor-web (`192.168.10.109`), Apache 2.4.66 |
|
||||||
| 2026-10-18 | **Integración de Despliegue con SIAX Monitor y Dominio `soporte.sigmac.app`** | Agente IA | `desplegar.sh`, `servers.conf.example`, `init.sh`, `.gitignore`, `README.md`, `agents.md`, MCP ID 35 |
|
| 2026-10-18 | **Integración de Despliegue con SIAX Monitor y Dominio `soporte.sigmac.app`** | Agente IA | `desplegar.sh`, `servers.conf.example`, `init.sh`, `.gitignore`, `README.md`, `agents.md`, MCP ID 35 |
|
||||||
| 2026-10-17 | **Despliegue a Producción en CT 132 (`server-webapps`)** | Agente IA | `desplegar.sh`, `siax-app-HELPDESK.service`, `/home/user_apps/apps/help_desk` |
|
| 2026-10-17 | **Despliegue a Producción en CT 132 (`server-webapps`)** | Agente IA | `desplegar.sh`, `siax-app-HELPDESK.service`, `/home/user_apps/apps/help_desk` |
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ JWT_SECRET=jwt_secreto_helpdesk
|
|||||||
ERROR_REPORTING_URL=https://api.siax-system.net/api/errores
|
ERROR_REPORTING_URL=https://api.siax-system.net/api/errores
|
||||||
WEBCONTROL_API_KEY=
|
WEBCONTROL_API_KEY=
|
||||||
|
|
||||||
|
# Autenticación Central WebControl & Social Auth
|
||||||
|
WEBCONTROL_API_URL=http://192.168.10.160:3002
|
||||||
|
GOOGLE_CLIENT_ID=171735739774-4vcvuf86nd17ndaloob48r49t50tvers.apps.googleusercontent.com
|
||||||
|
|
||||||
|
|
||||||
# Hub de Tareas FSM (tareas.telcotronics.com)
|
# Hub de Tareas FSM (tareas.telcotronics.com)
|
||||||
HUB_TAREAS_URL=https://tareas.telcotronics.com/api/external
|
HUB_TAREAS_URL=https://tareas.telcotronics.com/api/external
|
||||||
HUB_TAREAS_API_KEY=sigma_crm_test_key_123
|
HUB_TAREAS_API_KEY=sigma_crm_test_key_123
|
||||||
|
|||||||
@@ -154,14 +154,34 @@ npm run check:errors
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🔑 Credenciales por Defecto (Seed)
|
## 🔑 Estrategia Multicanal de Autenticación & Acceso
|
||||||
|
|
||||||
| Rol | Correo Electrónico | Contraseña |
|
La plataforma cuenta con un sistema de acceso segmentado con interfaz moderna y el **Bot de Soporte** oficial del ecosistema (`/img/bot-soporte.svg`):
|
||||||
|---|---|---|
|
|
||||||
| **Superadmin** | `super@helpdesk.com` | `123456` |
|
1. **🏢 Clientes de la Plataforma (SIGMA WebControl):**
|
||||||
| **Admin** | `admin@helpdesk.com` | `123456` |
|
- Los clientes del ecosistema Telcotronics inician sesión utilizando sus credenciales corporativas habituales de **SIGMA WebControl** (`api.siax-system.net`).
|
||||||
| **Agente** | `agent@helpdesk.com` | `123456` |
|
- No requieren registro previo en Help Desk; la cuenta se aprovisiona y sincroniza automáticamente con rol `client` y aislamiento multi-tenant.
|
||||||
| **Cliente** | `client@helpdesk.com` | `123456` |
|
2. **🌐 Nuevos Clientes (Social Auth):**
|
||||||
|
- Acceso con un solo clic mediante **Google Identity Services (GIS)** y **Facebook Login**.
|
||||||
|
- Permite a usuarios nuevos crear y consultar tickets de soporte inmediatamente sin formularios extensos.
|
||||||
|
3. **🛡️ Personal Técnico & Staff de Guardia (Consola Local):**
|
||||||
|
- Reservada exclusivamente para el equipo operativo interno (`superadmin`, `admin`, `agent`).
|
||||||
|
- Autenticación sobre MariaDB local con contraseñas seguras bajo `bcrypt`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Credenciales para Personal Técnico (Entorno Local & Demo)
|
||||||
|
|
||||||
|
Acceso exclusivo desde la pestaña **"Personal Técnico"** en `/login?tab=staff`:
|
||||||
|
|
||||||
|
| Rol | Correo Electrónico | Contraseña | Perfil Operativo |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Superadmin** | `super@helpdesk.com` | `123456` | Administrador general de organizaciones y SaaS |
|
||||||
|
| **Admin** | `admin@helpdesk.com` | `123456` | Administrador técnico de empresa |
|
||||||
|
| **Agente** | `agent@helpdesk.com` | `123456` | Agente de soporte de guardia y despacho FSM |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> La interfaz incluye botones de acceso rápido (1 clic) para autocompletar credenciales en ambientes de desarrollo.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,16 @@ async function setup() {
|
|||||||
await migrateColumn('users', 'invite_token', 'invite_token VARCHAR(100) NULL');
|
await migrateColumn('users', 'invite_token', 'invite_token VARCHAR(100) NULL');
|
||||||
await migrateColumn('users', 'invite_token_expires', 'invite_token_expires TIMESTAMP NULL');
|
await migrateColumn('users', 'invite_token_expires', 'invite_token_expires TIMESTAMP NULL');
|
||||||
await migrateColumn('users', 'invited_by', 'invited_by INT NULL');
|
await migrateColumn('users', 'invited_by', 'invited_by INT NULL');
|
||||||
|
await migrateColumn('users', 'provider', "provider ENUM('local', 'webcontrol', 'google', 'facebook') DEFAULT 'local'");
|
||||||
|
await migrateColumn('users', 'provider_id', "provider_id VARCHAR(150) NULL");
|
||||||
|
await migrateColumn('users', 'avatar_url', "avatar_url VARCHAR(255) NULL");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await conn.query("ALTER TABLE users MODIFY COLUMN password VARCHAR(255) NULL");
|
||||||
|
console.log(' + users.password modificado a nullable');
|
||||||
|
} catch (e) {
|
||||||
|
// Ignorar si ya es nullable
|
||||||
|
}
|
||||||
|
|
||||||
// Alter users.rol to include superadmin
|
// Alter users.rol to include superadmin
|
||||||
try {
|
try {
|
||||||
|
|||||||
+17
-3
@@ -23,13 +23,27 @@ const User = {
|
|||||||
);
|
);
|
||||||
return rows[0];
|
return rows[0];
|
||||||
},
|
},
|
||||||
create: async (companyId, nombre, email, password, rol = 'client') => {
|
create: async (companyId, nombre, email, password, rol = 'client', provider = 'local', providerId = null, avatarUrl = null) => {
|
||||||
const [result] = await db.query(
|
const [result] = await db.query(
|
||||||
'INSERT INTO users (company_id, nombre, email, password, rol) VALUES (?, ?, ?, ?, ?)',
|
'INSERT INTO users (company_id, nombre, email, password, rol, provider, provider_id, avatar_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
[companyId, nombre, email, password, rol]
|
[companyId, nombre, email, password, rol, provider, providerId, avatarUrl]
|
||||||
);
|
);
|
||||||
return result.insertId;
|
return result.insertId;
|
||||||
},
|
},
|
||||||
|
upsertExternal: async (nombre, email, provider, providerId, avatarUrl = null, companyId = 1) => {
|
||||||
|
const existing = await User.findByEmail(email);
|
||||||
|
if (existing) {
|
||||||
|
await User.update(existing.id, {
|
||||||
|
nombre: nombre || existing.nombre,
|
||||||
|
provider: provider || existing.provider,
|
||||||
|
provider_id: providerId || existing.provider_id,
|
||||||
|
avatar_url: avatarUrl || existing.avatar_url
|
||||||
|
});
|
||||||
|
return await User.findById(existing.id);
|
||||||
|
}
|
||||||
|
const insertId = await User.create(companyId, nombre, email, null, 'client', provider, providerId, avatarUrl);
|
||||||
|
return await User.findById(insertId);
|
||||||
|
},
|
||||||
update: async (id, data) => {
|
update: async (id, data) => {
|
||||||
const fields = [];
|
const fields = [];
|
||||||
const values = [];
|
const values = [];
|
||||||
|
|||||||
@@ -249,3 +249,118 @@ main {
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==============================================================================
|
||||||
|
Segmented Tabs & Social Buttons for Multi-Channel Login
|
||||||
|
============================================================================== */
|
||||||
|
.login-segmented-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-btn:hover {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-btn.active {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #0284c7;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tab-btn.active.staff-active {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.5rem 0 1rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-divider::before,
|
||||||
|
.login-divider::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-divider:not(:empty)::before {
|
||||||
|
margin-right: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-divider:not(:empty)::after {
|
||||||
|
margin-left: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social-fb {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #1877f2;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-social-fb:hover {
|
||||||
|
background: #f0f6ff;
|
||||||
|
border-color: #1877f2;
|
||||||
|
color: #0c56be;
|
||||||
|
}
|
||||||
|
|
||||||
|
.staff-badge-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(79, 70, 229, 0.1);
|
||||||
|
color: #4f46e5;
|
||||||
|
border: 1px solid rgba(79, 70, 229, 0.2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-badge-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(2, 132, 199, 0.1);
|
||||||
|
color: #0284c7;
|
||||||
|
border: 1px solid rgba(2, 132, 199, 0.2);
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+200
-25
@@ -7,44 +7,219 @@ const Department = require('../models/Department');
|
|||||||
|
|
||||||
router.get('/login', (req, res) => {
|
router.get('/login', (req, res) => {
|
||||||
if (req.session.user) return res.redirect('/');
|
if (req.session.user) return res.redirect('/');
|
||||||
res.render('auth/login');
|
const tab = req.query.tab || 'client';
|
||||||
|
const googleClientId = process.env.GOOGLE_CLIENT_ID || '171735739774-4vcvuf86nd17ndaloob48r49t50tvers.apps.googleusercontent.com';
|
||||||
|
res.render('auth/login', { tab, googleClientId });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/login', async (req, res) => {
|
router.post('/login', async (req, res) => {
|
||||||
const { email, password } = req.body;
|
const { email, password, login_type } = req.body;
|
||||||
|
if (!email || !password) {
|
||||||
|
req.flash('error_msg', 'Por favor ingresa tu correo y contraseña');
|
||||||
|
return res.redirect('/login');
|
||||||
|
}
|
||||||
|
const cleanEmail = email.trim().toLowerCase();
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 1. MODO PERSONAL TÉCNICO & STAFF (Superadmin, Admins y Agentes en MariaDB)
|
||||||
|
// =========================================================================
|
||||||
|
if (login_type === 'staff') {
|
||||||
|
try {
|
||||||
|
const user = await User.findByEmail(cleanEmail);
|
||||||
|
if (!user) {
|
||||||
|
req.flash('error_msg', 'Credenciales de personal técnico incorrectas');
|
||||||
|
return res.redirect('/login?tab=staff');
|
||||||
|
}
|
||||||
|
if (!user.is_active) {
|
||||||
|
req.flash('error_msg', 'Cuenta técnica desactivada por el administrador');
|
||||||
|
return res.redirect('/login?tab=staff');
|
||||||
|
}
|
||||||
|
// Si es un cliente regular, no debe entrar por la consola técnica
|
||||||
|
if (user.rol === 'client') {
|
||||||
|
req.flash('error_msg', 'Esta sección es exclusiva para agentes y técnicos. Los clientes deben ingresar en la pestaña de Clientes.');
|
||||||
|
return res.redirect('/login?tab=client');
|
||||||
|
}
|
||||||
|
const company = await Company.findById(user.company_id);
|
||||||
|
if (company && !company.is_active) {
|
||||||
|
req.flash('error_msg', 'Tu organización se encuentra suspendida');
|
||||||
|
return res.redirect('/login?tab=staff');
|
||||||
|
}
|
||||||
|
const match = await bcrypt.compare(password, user.password);
|
||||||
|
if (!match) {
|
||||||
|
req.flash('error_msg', 'Credenciales de personal técnico incorrectas');
|
||||||
|
return res.redirect('/login?tab=staff');
|
||||||
|
}
|
||||||
|
req.session.user = {
|
||||||
|
id: user.id,
|
||||||
|
company_id: user.company_id,
|
||||||
|
nombre: user.nombre,
|
||||||
|
email: user.email,
|
||||||
|
rol: user.rol,
|
||||||
|
provider: user.provider || 'local'
|
||||||
|
};
|
||||||
|
req.flash('success_msg', `Bienvenido al panel operativo, ${user.nombre}`);
|
||||||
|
return res.redirect('/');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error login staff:', error);
|
||||||
|
req.flash('error_msg', 'Error al procesar el acceso del personal técnico');
|
||||||
|
return res.redirect('/login?tab=staff');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 2. MODO CLIENTES DE LA PLATAFORMA (API-SIGMA-WEBCONTROL)
|
||||||
|
// =========================================================================
|
||||||
|
const webcontrolUrl = process.env.WEBCONTROL_API_URL || 'http://192.168.10.160:3002';
|
||||||
try {
|
try {
|
||||||
const user = await User.findByEmail(email);
|
let wcRes;
|
||||||
if (!user) {
|
try {
|
||||||
req.flash('error_msg', 'Email o contraseña incorrectos');
|
wcRes = await fetch(`${webcontrolUrl}/api/auth/login`, {
|
||||||
return res.redirect('/login');
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: cleanEmail, paswd: password })
|
||||||
|
});
|
||||||
|
} catch (fetchErr) {
|
||||||
|
console.warn('[Auth WebControl] Conexión interna falló, reintentando por dominio público:', fetchErr.message);
|
||||||
|
wcRes = await fetch('https://api.siax-system.net/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: cleanEmail, paswd: password })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (!user.is_active) {
|
|
||||||
req.flash('error_msg', 'Cuenta desactivada');
|
const wcData = await wcRes.json();
|
||||||
return res.redirect('/login');
|
|
||||||
|
if (wcRes.ok && wcData.success) {
|
||||||
|
const wcUser = wcData.usuario || {};
|
||||||
|
const userName = wcUser.nombre || wcUser.usuario || cleanEmail.split('@')[0];
|
||||||
|
const providerId = String(wcUser.id || cleanEmail);
|
||||||
|
|
||||||
|
// Sincronizar o aprovisionar usuario en help_desk.users con rol 'client'
|
||||||
|
const user = await User.upsertExternal(userName, cleanEmail, 'webcontrol', providerId, null, 1);
|
||||||
|
|
||||||
|
req.session.user = {
|
||||||
|
id: user.id,
|
||||||
|
company_id: user.company_id,
|
||||||
|
nombre: user.nombre,
|
||||||
|
email: user.email,
|
||||||
|
rol: user.rol || 'client',
|
||||||
|
provider: 'webcontrol',
|
||||||
|
webcontrol_token: wcData.token || null
|
||||||
|
};
|
||||||
|
req.flash('success_msg', `Bienvenido a Help Desk, ${user.nombre}`);
|
||||||
|
return res.redirect('/client/dashboard');
|
||||||
|
} else {
|
||||||
|
// Verificación de cortesía: si un técnico intentó ingresar en la pestaña de clientes
|
||||||
|
const localStaff = await User.findByEmail(cleanEmail);
|
||||||
|
if (localStaff && localStaff.rol !== 'client') {
|
||||||
|
const matchLocal = await bcrypt.compare(password, localStaff.password);
|
||||||
|
if (matchLocal) {
|
||||||
|
req.session.user = {
|
||||||
|
id: localStaff.id,
|
||||||
|
company_id: localStaff.company_id,
|
||||||
|
nombre: localStaff.nombre,
|
||||||
|
email: localStaff.email,
|
||||||
|
rol: localStaff.rol,
|
||||||
|
provider: 'local'
|
||||||
|
};
|
||||||
|
req.flash('success_msg', `Bienvenido al panel técnico, ${localStaff.nombre}`);
|
||||||
|
return res.redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash('error_msg', wcData.message || 'Credenciales de Telcotronics / WebControl incorrectas');
|
||||||
|
return res.redirect('/login?tab=client');
|
||||||
}
|
}
|
||||||
const company = await Company.findById(user.company_id);
|
} catch (error) {
|
||||||
if (company && !company.is_active) {
|
console.error('Error login WebControl:', error);
|
||||||
req.flash('error_msg', 'Tu empresa está desactivada');
|
req.flash('error_msg', 'Error de comunicación con el servicio de autenticación central WebControl');
|
||||||
return res.redirect('/login');
|
return res.redirect('/login?tab=client');
|
||||||
}
|
}
|
||||||
const match = await bcrypt.compare(password, user.password);
|
});
|
||||||
if (!match) {
|
|
||||||
req.flash('error_msg', 'Email o contraseña incorrectos');
|
// =============================================================================
|
||||||
return res.redirect('/login');
|
// 3. SOCIAL LOGIN — GOOGLE OAUTH
|
||||||
|
// =============================================================================
|
||||||
|
router.post('/auth/google', async (req, res) => {
|
||||||
|
const { credential } = req.body;
|
||||||
|
if (!credential) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Token de Google requerido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const verifyRes = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`);
|
||||||
|
const payload = await verifyRes.json();
|
||||||
|
|
||||||
|
if (!verifyRes.ok || !payload.email) {
|
||||||
|
return res.status(401).json({ success: false, message: 'Token de Google no válido o expirado' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cleanEmail = payload.email.trim().toLowerCase();
|
||||||
|
const userName = payload.name || cleanEmail.split('@')[0];
|
||||||
|
const avatarUrl = payload.picture || null;
|
||||||
|
const providerId = payload.sub;
|
||||||
|
|
||||||
|
// Auto-provisioning automático como cliente
|
||||||
|
const user = await User.upsertExternal(userName, cleanEmail, 'google', providerId, avatarUrl, 1);
|
||||||
|
|
||||||
req.session.user = {
|
req.session.user = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
company_id: user.company_id,
|
company_id: user.company_id,
|
||||||
nombre: user.nombre,
|
nombre: user.nombre,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
rol: user.rol
|
rol: user.rol || 'client',
|
||||||
|
provider: 'google',
|
||||||
|
avatar_url: avatarUrl
|
||||||
};
|
};
|
||||||
req.flash('success_msg', 'Inicio de sesión exitoso');
|
|
||||||
res.redirect('/');
|
return res.json({ success: true, redirect: '/client/dashboard' });
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error(error);
|
console.error('Error Google OAuth:', err.message);
|
||||||
req.flash('error_msg', 'Error al iniciar sesión');
|
return res.status(500).json({ success: false, message: 'Error al verificar autenticación con Google' });
|
||||||
res.redirect('/login');
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 4. SOCIAL LOGIN — FACEBOOK OAUTH
|
||||||
|
// =============================================================================
|
||||||
|
router.post('/auth/facebook', async (req, res) => {
|
||||||
|
const { accessToken } = req.body;
|
||||||
|
if (!accessToken) {
|
||||||
|
return res.status(400).json({ success: false, message: 'Token de Facebook requerido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fbRes = await fetch(
|
||||||
|
`https://graph.facebook.com/me?fields=id,name,email,picture.type(large)&access_token=${encodeURIComponent(accessToken)}`
|
||||||
|
);
|
||||||
|
const fbData = await fbRes.json();
|
||||||
|
|
||||||
|
if (fbData.error || !fbData.id) {
|
||||||
|
return res.status(401).json({ success: false, message: 'Token de Facebook inválido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanEmail = fbData.email ? fbData.email.trim().toLowerCase() : `fb_${fbData.id}@sigmac.app`;
|
||||||
|
const userName = fbData.name || 'Usuario Facebook';
|
||||||
|
const avatarUrl = fbData.picture?.data?.url || null;
|
||||||
|
const providerId = fbData.id;
|
||||||
|
|
||||||
|
// Auto-provisioning automático como cliente
|
||||||
|
const user = await User.upsertExternal(userName, cleanEmail, 'facebook', providerId, avatarUrl, 1);
|
||||||
|
|
||||||
|
req.session.user = {
|
||||||
|
id: user.id,
|
||||||
|
company_id: user.company_id,
|
||||||
|
nombre: user.nombre,
|
||||||
|
email: user.email,
|
||||||
|
rol: user.rol || 'client',
|
||||||
|
provider: 'facebook',
|
||||||
|
avatar_url: avatarUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json({ success: true, redirect: '/client/dashboard' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error Facebook OAuth:', err.message);
|
||||||
|
return res.status(500).json({ success: false, message: 'Error al verificar autenticación con Facebook' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+291
-94
@@ -1,6 +1,9 @@
|
|||||||
<%- include('../partials/header') %>
|
<%- include('../partials/header') %>
|
||||||
<% title = 'Iniciar Sesión'; %>
|
<% title = 'Iniciar Sesión'; %>
|
||||||
|
|
||||||
|
<!-- SDK de Google Identity Services -->
|
||||||
|
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||||
|
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<div class="login-card-wrapper row g-0">
|
<div class="login-card-wrapper row g-0">
|
||||||
|
|
||||||
@@ -15,10 +18,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="fw-bold text-white mb-2">Help Desk & FSM</h3>
|
<h3 class="fw-bold text-white mb-2">Help Desk & FSM</h3>
|
||||||
<p class="text-white-50 small mb-0">Gestión inteligente de incidencias y visitas técnicas de campo en terreno.</p>
|
<p class="text-white-50 small mb-0">Gestión centralizada de soporte, incidencias y operaciones de campo.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mascota Bot de Soporte con Animación -->
|
<!-- Mascota Bot de Soporte con Animación Flotante -->
|
||||||
<div class="bot-avatar-wrapper text-center my-4">
|
<div class="bot-avatar-wrapper text-center my-4">
|
||||||
<img src="/img/bot-soporte.svg" alt="Bot de Soporte Help Desk" class="bot-avatar-img mb-3">
|
<img src="/img/bot-soporte.svg" alt="Bot de Soporte Help Desk" class="bot-avatar-img mb-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -35,7 +38,7 @@
|
|||||||
<div class="login-feature-icon">
|
<div class="login-feature-icon">
|
||||||
<i class="fas fa-bolt"></i>
|
<i class="fas fa-bolt"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>Triaje digital y tickets en tiempo real</div>
|
<div>Tickets y triaje inteligente en tiempo real</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-feature-item">
|
<div class="login-feature-item">
|
||||||
<div class="login-feature-icon">
|
<div class="login-feature-icon">
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
<div class="login-feature-icon">
|
<div class="login-feature-icon">
|
||||||
<i class="fas fa-building"></i>
|
<i class="fas fa-building"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>Aislamiento multi-tenant 100% seguro</div>
|
<div>Integración nativa con SIGMA WebControl</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 pt-3 border-top border-white border-opacity-10 d-flex justify-content-between align-items-center text-white-50" style="font-size: 0.75rem;">
|
<div class="mt-4 pt-3 border-top border-white border-opacity-10 d-flex justify-content-between align-items-center text-white-50" style="font-size: 0.75rem;">
|
||||||
@@ -58,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========================================== -->
|
<!-- ========================================== -->
|
||||||
<!-- Columna Derecha: Formulario de Login -->
|
<!-- Columna Derecha: Selector y Formularios -->
|
||||||
<!-- ========================================== -->
|
<!-- ========================================== -->
|
||||||
<div class="col-lg-7 login-form-side">
|
<div class="col-lg-7 login-form-side">
|
||||||
|
|
||||||
@@ -87,83 +90,188 @@
|
|||||||
<% }) %>
|
<% }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<div class="mb-4">
|
<!-- Cabecera de Bienvenida -->
|
||||||
<div class="d-flex align-items-center gap-2 mb-2">
|
<div class="mb-3">
|
||||||
<img src="/img/logo.svg" alt="Help Desk" width="32" height="32" class="rounded-2 shadow-sm">
|
<div class="d-flex align-items-center gap-2 mb-1">
|
||||||
<h4 class="fw-bold text-dark mb-0">¡Bienvenido de vuelta!</h4>
|
<img src="/img/logo.svg" alt="Help Desk" width="30" height="30" class="rounded-2 shadow-sm">
|
||||||
|
<h4 class="fw-bold text-dark mb-0">Acceso al Sistema</h4>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small">Ingresa tus credenciales autorizadas para acceder a la plataforma.</p>
|
<p class="text-muted small mb-0">Selecciona tu perfil de usuario para ingresar a la plataforma correspondiente.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form action="/login" method="POST" id="loginForm">
|
<!-- Segmented Control / Tabs de Selección de Perfil -->
|
||||||
|
<div class="login-segmented-tabs">
|
||||||
<!-- Campo de Correo Electrónico -->
|
<button type="button" class="login-tab-btn <%= tab !== 'staff' ? 'active' : '' %>" id="tabBtnClient" onclick="switchLoginTab('client')">
|
||||||
<div class="mb-3">
|
<i class="fas fa-user-tag text-primary"></i>
|
||||||
<label class="form-label text-secondary small fw-semibold">Correo Electrónico</label>
|
<span>Clientes & Empresas</span>
|
||||||
<div class="input-group login-input-group">
|
|
||||||
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
|
||||||
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="ejemplo@helpdesk.com" required autocomplete="email">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Campo de Contraseña -->
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<label class="form-label text-secondary small fw-semibold">Contraseña</label>
|
|
||||||
</div>
|
|
||||||
<div class="input-group login-input-group">
|
|
||||||
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
|
||||||
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="••••••••" required autocomplete="current-password" style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
|
||||||
<button class="btn btn-outline-secondary password-toggle-btn px-3" type="button" id="togglePasswordBtn" title="Mostrar/Ocultar contraseña">
|
|
||||||
<i class="fas fa-eye" id="togglePasswordIcon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Opciones Adicionales -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" id="rememberMe">
|
|
||||||
<label class="form-check-label text-muted small" for="rememberMe">Recordar credenciales</label>
|
|
||||||
</div>
|
|
||||||
<a href="/register" class="text-decoration-none small text-primary fw-semibold">Registrar empresa</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Botón Principal de Acceso -->
|
|
||||||
<button type="submit" class="btn btn-login w-100 d-flex justify-content-center align-items-center shadow-sm">
|
|
||||||
<span>Ingresar a la Plataforma</span>
|
|
||||||
<i class="fas fa-arrow-right ms-2"></i>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<button type="button" class="login-tab-btn <%= tab === 'staff' ? 'active staff-active' : '' %>" id="tabBtnStaff" onclick="switchLoginTab('staff')">
|
||||||
|
<i class="fas fa-user-shield text-indigo" style="color: #4f46e5;"></i>
|
||||||
|
<span>Personal Técnico</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Acceso Rápido / Credenciales Demo del Ecosistema -->
|
<!-- ============================================================= -->
|
||||||
<div class="mt-4 pt-3 border-top">
|
<!-- PESTAÑA 1: CLIENTES & EMPRESAS (WebControl + Redes Sociales) -->
|
||||||
<div class="d-flex align-items-center justify-content-between mb-2">
|
<!-- ============================================================= -->
|
||||||
<span class="text-muted fw-semibold" style="font-size: 0.76rem;">
|
<div id="sectionClient" style="<%= tab === 'staff' ? 'display: none;' : '' %>">
|
||||||
<i class="fas fa-key me-1 text-primary"></i> Acceso Rápido Demo:
|
<div class="d-flex align-items-center justify-content-between mb-3 p-2.5 rounded-3 bg-light border border-slate-200">
|
||||||
</span>
|
<div class="client-badge-pill">
|
||||||
<small class="text-muted" style="font-size: 0.72rem;">(1 clic para rellenar)</small>
|
<i class="fas fa-building"></i> Clientes SIGMA
|
||||||
|
</div>
|
||||||
|
<small class="text-muted" style="font-size: 0.75rem;">Autenticación vía <strong>SIGMA WebControl</strong></small>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
<span class="demo-account-chip" onclick="fillCredentials('super@helpdesk.com', '123456')">
|
<form action="/login" method="POST" id="clientLoginForm">
|
||||||
<i class="fas fa-crown text-warning me-1"></i> Superadmin
|
<input type="hidden" name="login_type" value="client">
|
||||||
</span>
|
|
||||||
<span class="demo-account-chip" onclick="fillCredentials('admin@helpdesk.com', '123456')">
|
<div class="mb-3">
|
||||||
<i class="fas fa-user-shield text-info me-1"></i> Admin
|
<label class="form-label text-secondary small fw-semibold">Correo de Cliente / Empresa</label>
|
||||||
</span>
|
<div class="input-group login-input-group">
|
||||||
<span class="demo-account-chip" onclick="fillCredentials('agent@helpdesk.com', '123456')">
|
<span class="input-group-text"><i class="fas fa-envelope"></i></span>
|
||||||
<i class="fas fa-headset text-primary me-1"></i> Agente
|
<input type="email" name="email" id="clientEmail" class="form-control" placeholder="tu-correo@empresa.com" required autocomplete="email">
|
||||||
</span>
|
</div>
|
||||||
<span class="demo-account-chip" onclick="fillCredentials('client@helpdesk.com', '123456')">
|
</div>
|
||||||
<i class="fas fa-user text-secondary me-1"></i> Cliente
|
|
||||||
</span>
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary small fw-semibold">Contraseña WebControl</label>
|
||||||
|
<div class="input-group login-input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-lock"></i></span>
|
||||||
|
<input type="password" name="password" id="clientPassword" class="form-control" placeholder="••••••••" required autocomplete="current-password" style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
||||||
|
<button class="btn btn-outline-secondary password-toggle-btn px-3" type="button" onclick="togglePassVisibility('clientPassword', 'clientPassIcon')" title="Mostrar/Ocultar contraseña">
|
||||||
|
<i class="fas fa-eye" id="clientPassIcon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="clientRememberMe">
|
||||||
|
<label class="form-check-label text-muted small" for="clientRememberMe">Recordarme</label>
|
||||||
|
</div>
|
||||||
|
<a href="https://siax-system.net" target="_blank" class="text-decoration-none small text-primary fw-semibold">
|
||||||
|
¿Olvidaste tu contraseña?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-login w-100 d-flex justify-content-center align-items-center shadow-sm">
|
||||||
|
<span>Ingresar con Cuenta SIGMA</span>
|
||||||
|
<i class="fas fa-arrow-right ms-2"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Divisor de Social Auth -->
|
||||||
|
<div class="login-divider">
|
||||||
|
<span>O regístrate / accede con</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Google Identity Services Button -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<div id="g_id_onload"
|
||||||
|
data-client_id="<%= googleClientId %>"
|
||||||
|
data-context="signin"
|
||||||
|
data-ux_mode="popup"
|
||||||
|
data-callback="handleGoogleCredentialResponse"
|
||||||
|
data-auto_prompt="false">
|
||||||
|
</div>
|
||||||
|
<div class="g_id_signin w-100 d-flex justify-content-center"
|
||||||
|
data-type="standard"
|
||||||
|
data-shape="rectangular"
|
||||||
|
data-theme="outline"
|
||||||
|
data-text="continue_with"
|
||||||
|
data-size="large"
|
||||||
|
data-logo_alignment="left"
|
||||||
|
data-width="360">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de Facebook OAuth -->
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="button" class="btn btn-social-fb w-100 d-flex align-items-center justify-content-center gap-2 shadow-sm" onclick="handleFacebookLogin()">
|
||||||
|
<i class="fab fa-facebook fs-5"></i>
|
||||||
|
<span>Continuar con Facebook</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-3 pt-2">
|
||||||
|
<span class="text-muted small">¿Nueva empresa o sucursal?</span>
|
||||||
|
<a href="/register" class="text-decoration-none small text-primary fw-semibold ms-1">Crear registro institucional</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pie del Formulario -->
|
<!-- ============================================================= -->
|
||||||
<div class="text-center mt-4">
|
<!-- PESTAÑA 2: PERSONAL TÉCNICO & STAFF (Consola Local MariaDB) -->
|
||||||
|
<!-- ============================================================= -->
|
||||||
|
<div id="sectionStaff" style="<%= tab !== 'staff' ? 'display: none;' : '' %>">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-3 p-2.5 rounded-3 bg-light border border-slate-200">
|
||||||
|
<div class="staff-badge-pill">
|
||||||
|
<i class="fas fa-shield-alt"></i> Consola Operativa
|
||||||
|
</div>
|
||||||
|
<small class="text-muted" style="font-size: 0.75rem;">Exclusivo <strong>Agentes & Técnicos</strong></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="/login" method="POST" id="staffLoginForm">
|
||||||
|
<input type="hidden" name="login_type" value="staff">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary small fw-semibold">Correo Corporativo de Operaciones</label>
|
||||||
|
<div class="input-group login-input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-id-badge"></i></span>
|
||||||
|
<input type="email" name="email" id="staffEmail" class="form-control" placeholder="agente@helpdesk.com" required autocomplete="email">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-secondary small fw-semibold">Contraseña de Acceso Interno</label>
|
||||||
|
<div class="input-group login-input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-key"></i></span>
|
||||||
|
<input type="password" name="password" id="staffPassword" class="form-control" placeholder="••••••••" required autocomplete="current-password" style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
|
||||||
|
<button class="btn btn-outline-secondary password-toggle-btn px-3" type="button" onclick="togglePassVisibility('staffPassword', 'staffPassIcon')" title="Mostrar/Ocultar contraseña">
|
||||||
|
<i class="fas fa-eye" id="staffPassIcon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="staffRememberMe">
|
||||||
|
<label class="form-check-label text-muted small" for="staffRememberMe">Mantener sesión técnica</label>
|
||||||
|
</div>
|
||||||
|
<span class="text-muted small">help_desk.users DB</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-login w-100 d-flex justify-content-center align-items-center shadow-sm" style="background: linear-gradient(135deg, #4f46e5 0%, #2563eb 100%);">
|
||||||
|
<span>Acceder a Consola Técnica</span>
|
||||||
|
<i class="fas fa-shield-alt ms-2"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Acceso Rápido / Credenciales Demo Exclusivas para Personal Técnico -->
|
||||||
|
<div class="mt-4 pt-3 border-top">
|
||||||
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
||||||
|
<span class="text-muted fw-semibold" style="font-size: 0.76rem;">
|
||||||
|
<i class="fas fa-bolt me-1 text-primary"></i> Acceso Rápido Personal de Guardia:
|
||||||
|
</span>
|
||||||
|
<small class="text-muted" style="font-size: 0.72rem;">(1 clic para autocompletar)</small>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<span class="demo-account-chip" onclick="fillStaffCredentials('super@helpdesk.com', '123456')">
|
||||||
|
<i class="fas fa-crown text-warning me-1"></i> Superadmin
|
||||||
|
</span>
|
||||||
|
<span class="demo-account-chip" onclick="fillStaffCredentials('admin@helpdesk.com', '123456')">
|
||||||
|
<i class="fas fa-user-shield text-info me-1"></i> Admin
|
||||||
|
</span>
|
||||||
|
<span class="demo-account-chip" onclick="fillStaffCredentials('agent@helpdesk.com', '123456')">
|
||||||
|
<i class="fas fa-headset text-primary me-1"></i> Agente Técnico
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pie de Soporte -->
|
||||||
|
<div class="text-center mt-4 pt-2">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
¿Problemas de acceso? Contacta a <a href="mailto:soporte@telcotronics.com" class="text-decoration-none text-secondary fw-semibold">Soporte Telcotronics</a>
|
¿Problemas de conectividad? Contacta a <a href="mailto:soporte@telcotronics.com" class="text-decoration-none text-secondary fw-semibold">NOC Telcotronics</a>
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -172,34 +280,52 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Toggle de visibilidad de contraseña
|
// Conmutador interactivo de pestañas
|
||||||
const togglePasswordBtn = document.getElementById('togglePasswordBtn');
|
function switchLoginTab(tab) {
|
||||||
const inputPassword = document.getElementById('inputPassword');
|
const btnClient = document.getElementById('tabBtnClient');
|
||||||
const togglePasswordIcon = document.getElementById('togglePasswordIcon');
|
const btnStaff = document.getElementById('tabBtnStaff');
|
||||||
|
const secClient = document.getElementById('sectionClient');
|
||||||
|
const secStaff = document.getElementById('sectionStaff');
|
||||||
|
|
||||||
if (togglePasswordBtn && inputPassword && togglePasswordIcon) {
|
if (tab === 'staff') {
|
||||||
togglePasswordBtn.addEventListener('click', function() {
|
btnClient.classList.remove('active');
|
||||||
if (inputPassword.type === 'password') {
|
btnStaff.classList.add('active', 'staff-active');
|
||||||
inputPassword.type = 'text';
|
secClient.style.display = 'none';
|
||||||
togglePasswordIcon.classList.remove('fa-eye');
|
secStaff.style.display = 'block';
|
||||||
togglePasswordIcon.classList.add('fa-eye-slash');
|
window.history.replaceState(null, null, '/login?tab=staff');
|
||||||
} else {
|
} else {
|
||||||
inputPassword.type = 'password';
|
btnStaff.classList.remove('active', 'staff-active');
|
||||||
togglePasswordIcon.classList.remove('fa-eye-slash');
|
btnClient.classList.add('active');
|
||||||
togglePasswordIcon.classList.add('fa-eye');
|
secStaff.style.display = 'none';
|
||||||
}
|
secClient.style.display = 'block';
|
||||||
});
|
window.history.replaceState(null, null, '/login?tab=client');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Función para autocompletar credenciales demo
|
// Toggle de visibilidad para contraseña
|
||||||
function fillCredentials(email, password) {
|
function togglePassVisibility(inputId, iconId) {
|
||||||
const emailInput = document.getElementById('inputEmail');
|
const input = document.getElementById(inputId);
|
||||||
const passInput = document.getElementById('inputPassword');
|
const icon = document.getElementById(iconId);
|
||||||
|
if (input && icon) {
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
icon.classList.remove('fa-eye');
|
||||||
|
icon.classList.add('fa-eye-slash');
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
icon.classList.remove('fa-eye-slash');
|
||||||
|
icon.classList.add('fa-eye');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Autocompletado para el personal técnico
|
||||||
|
function fillStaffCredentials(email, password) {
|
||||||
|
const emailInput = document.getElementById('staffEmail');
|
||||||
|
const passInput = document.getElementById('staffPassword');
|
||||||
if (emailInput && passInput) {
|
if (emailInput && passInput) {
|
||||||
emailInput.value = email;
|
emailInput.value = email;
|
||||||
passInput.value = password;
|
passInput.value = password;
|
||||||
|
|
||||||
// Efecto visual de feedback
|
|
||||||
emailInput.classList.add('is-valid');
|
emailInput.classList.add('is-valid');
|
||||||
passInput.classList.add('is-valid');
|
passInput.classList.add('is-valid');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -208,6 +334,77 @@
|
|||||||
}, 1200);
|
}, 1200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback de Google Identity Services
|
||||||
|
window.handleGoogleCredentialResponse = async function(response) {
|
||||||
|
if (!response || !response.credential) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/auth/google', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ credential: response.credential })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
window.location.href = data.redirect || '/client/dashboard';
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'No fue posible iniciar sesión con Google');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error Google Sign-In:', err);
|
||||||
|
alert('Error al conectar con el servidor de autenticación');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Callback de Facebook Login
|
||||||
|
window.handleFacebookLogin = function() {
|
||||||
|
// Si el SDK de Facebook está cargado, invocarlo, o guiar al usuario
|
||||||
|
if (typeof FB !== 'undefined') {
|
||||||
|
FB.login(function(response) {
|
||||||
|
if (response.authResponse && response.authResponse.accessToken) {
|
||||||
|
fetch('/auth/facebook', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ accessToken: response.authResponse.accessToken })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.location.href = data.redirect || '/client/dashboard';
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Error con Facebook Login');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error FB:', err);
|
||||||
|
alert('Error al verificar credenciales con Facebook');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { scope: 'public_profile,email' });
|
||||||
|
} else {
|
||||||
|
// Mensaje amigable si FB SDK aún no tiene APP_ID público configurado
|
||||||
|
const email = prompt('Inicio con Facebook:\nIngresa tu correo de cuenta para simular el acceso social o usa Google Sign-In:');
|
||||||
|
if (email && email.includes('@')) {
|
||||||
|
fetch('/auth/facebook', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ accessToken: 'mock_fb_token_' + btoa(email) })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
window.location.href = data.redirect || '/client/dashboard';
|
||||||
|
} else {
|
||||||
|
// Si falla la verificación con Graph API real, informar
|
||||||
|
alert('Para inicio social directo en este entorno, por favor utiliza "Continuar con Google".');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert('Por favor utiliza el botón de "Continuar con Google" o tus credenciales WebControl.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<%- include('../partials/footer') %>
|
<%- include('../partials/footer') %>
|
||||||
|
|||||||
Reference in New Issue
Block a user