diff --git a/dashboard/login.css b/dashboard/login.css new file mode 100644 index 0000000..17443d9 --- /dev/null +++ b/dashboard/login.css @@ -0,0 +1,211 @@ +/* ============================================================ + login.css -- Login / Register page styles + ============================================================ */ + +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: var(--bg-primary); + padding: 2rem; +} + +/* --- Card --- */ +.login-card { + width: 100%; + max-width: 440px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 2.5rem; + animation: fadeIn 0.4s ease; +} + +/* --- Brand header --- */ +.login-brand { + text-align: center; + margin-bottom: 2rem; +} + +.login-brand .logo-icon { + width: 56px; + height: 56px; + background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%); + border-radius: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.6rem; + margin-bottom: 1rem; + box-shadow: 0 4px 20px var(--accent-glow); +} + +.login-brand h1 { + font-family: 'Orbitron', sans-serif; + font-size: 1.5rem; + font-weight: 700; + letter-spacing: 2px; + color: var(--text-primary); + margin-bottom: 0.4rem; +} + +.login-brand h1 span { + color: var(--accent); +} + +.login-brand .slogan { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 400; +} + +/* --- Form panel visibility --- */ +.form-panel { + display: none; +} + +.form-panel.active { + display: block; + animation: fadeIn 0.3s ease; +} + +/* --- Form title --- */ +.form-title { + font-size: 1.15rem; + font-weight: 600; + margin-bottom: 1.5rem; + text-align: center; + color: var(--text-primary); +} + +/* --- Select (dropdown) --- */ +.form-select { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.95rem; + transition: border-color 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a0a0b0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + cursor: pointer; +} + +.form-select:focus { + outline: none; + border-color: var(--accent); +} + +.form-select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +/* --- Submit button (full width) --- */ +.btn-submit { + width: 100%; + padding: 0.85rem; + margin-top: 0.5rem; + font-size: 1rem; +} + +/* --- Toggle link --- */ +.toggle-link { + text-align: center; + margin-top: 1.5rem; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.toggle-link a { + color: var(--accent); + text-decoration: none; + font-weight: 600; + cursor: pointer; + transition: color 0.2s; +} + +.toggle-link a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* --- Alert messages --- */ +.login-alert { + padding: 0.85rem 1rem; + border-radius: 8px; + margin-bottom: 1.25rem; + font-size: 0.9rem; + display: none; + align-items: center; + gap: 0.5rem; + line-height: 1.4; +} + +.login-alert.show { + display: flex; +} + +.login-alert.error { + background: rgba(255, 68, 68, 0.1); + border: 1px solid var(--danger); + color: var(--danger); +} + +.login-alert.success { + background: rgba(0, 214, 143, 0.1); + border: 1px solid var(--success); + color: var(--success); +} + +/* --- Loading spinner on button --- */ +.btn-submit.loading { + pointer-events: none; + opacity: 0.7; +} + +.btn-submit .spinner { + display: none; + width: 18px; + height: 18px; + border: 2px solid rgba(255,255,255,0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +.btn-submit.loading .spinner { + display: inline-block; +} + +.btn-submit.loading .btn-label { + display: none; +} + +/* --- Row layout for two fields side by side --- */ +.form-row { + display: flex; + gap: 1rem; +} + +.form-row .form-group { + flex: 1; +} + +/* --- Responsive --- */ +@media (max-width: 500px) { + .login-card { + padding: 1.75rem 1.5rem; + } + + .form-row { + flex-direction: column; + gap: 0; + } +} diff --git a/dashboard/login.html b/dashboard/login.html new file mode 100644 index 0000000..36ca372 --- /dev/null +++ b/dashboard/login.html @@ -0,0 +1,108 @@ + + + + + + Nexus Autoparts - Iniciar Sesion + + + + + + +
+ +
+
+

NEXUS AUTOPARTS

+

Tu conexion directa con las partes que necesitas

+
+ + + + + +
+

Iniciar Sesion

+
+
+ + +
+
+ + +
+ +
+ +
+ + +
+

Crear Cuenta

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + + + diff --git a/dashboard/login.js b/dashboard/login.js new file mode 100644 index 0000000..415fd84 --- /dev/null +++ b/dashboard/login.js @@ -0,0 +1,227 @@ +/* ============================================================ + login.js -- Login / Register logic for Nexus Autoparts + ============================================================ */ + +(function () { + 'use strict'; + + // ---- DOM refs ---- + const loginPanel = document.getElementById('loginPanel'); + const registerPanel = document.getElementById('registerPanel'); + const loginForm = document.getElementById('loginForm'); + const registerForm = document.getElementById('registerForm'); + const alertBox = document.getElementById('alert'); + + // ---- Role-based redirect map ---- + const ROLE_REDIRECTS = { + ADMIN: '/demo', + OWNER: '/demo', + BODEGA: '/bodega', + TALLER: '/demo', + }; + + // ---- Check existing session on load ---- + (function checkSession() { + const token = localStorage.getItem('access_token'); + const role = localStorage.getItem('user_role'); + if (token && role) { + const dest = ROLE_REDIRECTS[role] || '/index.html'; + window.location.replace(dest); + } + })(); + + // ---- Panel toggling ---- + window.showPanel = function (panel) { + hideAlert(); + if (panel === 'register') { + loginPanel.classList.remove('active'); + registerPanel.classList.add('active'); + } else { + registerPanel.classList.remove('active'); + loginPanel.classList.add('active'); + } + }; + + // ---- Alert helpers ---- + function showAlert(msg, type) { + alertBox.textContent = msg; + alertBox.className = 'login-alert show ' + type; + } + + function hideAlert() { + alertBox.className = 'login-alert'; + alertBox.textContent = ''; + } + + function setLoading(btn, loading) { + btn.classList.toggle('loading', loading); + } + + // ---- Login ---- + loginForm.addEventListener('submit', async function (e) { + e.preventDefault(); + hideAlert(); + + const email = document.getElementById('loginEmail').value.trim(); + const password = document.getElementById('loginPassword').value; + const btn = loginForm.querySelector('.btn-submit'); + + if (!email || !password) { + showAlert('Completa todos los campos.', 'error'); + return; + } + + setLoading(btn, true); + + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + + const data = await res.json(); + + if (!res.ok) { + showAlert(data.error || data.message || 'Credenciales incorrectas.', 'error'); + return; + } + + // Persist tokens & user info + localStorage.setItem('access_token', data.access_token); + localStorage.setItem('refresh_token', data.refresh_token || ''); + localStorage.setItem('user_role', data.role || data.user?.role || ''); + localStorage.setItem('user_name', data.name || data.user?.name || ''); + + const role = (data.role || data.user?.role || '').toUpperCase(); + const dest = ROLE_REDIRECTS[role] || '/index.html'; + window.location.replace(dest); + + } catch (err) { + showAlert('Error de conexion. Intenta de nuevo.', 'error'); + } finally { + setLoading(btn, false); + } + }); + + // ---- Register ---- + registerForm.addEventListener('submit', async function (e) { + e.preventDefault(); + hideAlert(); + + const name = document.getElementById('regName').value.trim(); + const email = document.getElementById('regEmail').value.trim(); + const password = document.getElementById('regPassword').value; + const confirm = document.getElementById('regConfirm').value; + const business_name = document.getElementById('regBusiness').value.trim(); + const phone = document.getElementById('regPhone').value.trim(); + const role = document.getElementById('regRole').value; + const btn = registerForm.querySelector('.btn-submit'); + + // Validations + if (!name || !email || !password || !confirm || !business_name || !phone) { + showAlert('Completa todos los campos.', 'error'); + return; + } + + if (password.length < 8) { + showAlert('La contrasena debe tener al menos 8 caracteres.', 'error'); + return; + } + + if (password !== confirm) { + showAlert('Las contrasenas no coinciden.', 'error'); + return; + } + + setLoading(btn, true); + + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, email, password, role, business_name, phone }), + }); + + const data = await res.json(); + + if (!res.ok) { + showAlert(data.error || data.message || 'Error al crear la cuenta.', 'error'); + return; + } + + showAlert('Cuenta creada. Pendiente de aprobacion por administrador.', 'success'); + registerForm.reset(); + + } catch (err) { + showAlert('Error de conexion. Intenta de nuevo.', 'error'); + } finally { + setLoading(btn, false); + } + }); + + // ================================================================ + // authFetch -- Authenticated fetch wrapper (exported globally) + // ================================================================ + window.authFetch = async function authFetch(url, options = {}) { + const token = localStorage.getItem('access_token'); + if (!token) { + window.location.replace('/login.html'); + return; + } + + const headers = Object.assign({}, options.headers || {}, { + 'Authorization': 'Bearer ' + token, + }); + + let res = await fetch(url, Object.assign({}, options, { headers })); + + // If 401, try refreshing the token once + if (res.status === 401) { + const refreshed = await tryRefreshToken(); + if (refreshed) { + headers['Authorization'] = 'Bearer ' + localStorage.getItem('access_token'); + res = await fetch(url, Object.assign({}, options, { headers })); + } else { + // Refresh failed — log out + logout(); + return; + } + } + + return res; + }; + + async function tryRefreshToken() { + const refreshToken = localStorage.getItem('refresh_token'); + if (!refreshToken) return false; + + try { + const res = await fetch('/api/auth/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + if (!res.ok) return false; + + const data = await res.json(); + localStorage.setItem('access_token', data.access_token); + if (data.refresh_token) { + localStorage.setItem('refresh_token', data.refresh_token); + } + return true; + } catch (e) { + return false; + } + } + + window.logout = function logout() { + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('user_role'); + localStorage.removeItem('user_name'); + window.location.replace('/login.html'); + }; + +})();