Spaces:
Sleeping
Sleeping
germansango01 commited on
Commit ·
6ebcfa2
1
Parent(s): cbd824d
Validate logout flow whit backend
Browse files- frontend/index.html +12 -7
- frontend/src/api.js +15 -2
- frontend/src/app.js +104 -10
- frontend/src/style.css +16 -0
frontend/index.html
CHANGED
|
@@ -345,32 +345,37 @@
|
|
| 345 |
</div>
|
| 346 |
<div class="modal-body">
|
| 347 |
<!-- Login Form -->
|
| 348 |
-
<form class="modal-form active" id="form-login">
|
| 349 |
<div class="form-group">
|
| 350 |
<label for="login-email">Correo electrónico</label>
|
| 351 |
-
<input type="
|
|
|
|
| 352 |
</div>
|
| 353 |
<div class="form-group">
|
| 354 |
<label for="login-password">Contraseña</label>
|
| 355 |
-
<input type="password" id="login-password" placeholder="••••••••"
|
|
|
|
| 356 |
</div>
|
| 357 |
<div class="form-error" id="login-error"></div>
|
| 358 |
<button type="submit" class="modal-submit">Entrar</button>
|
| 359 |
</form>
|
| 360 |
|
| 361 |
<!-- Register Form -->
|
| 362 |
-
<form class="modal-form" id="form-register">
|
| 363 |
<div class="form-group">
|
| 364 |
<label for="register-email">Correo electrónico</label>
|
| 365 |
-
<input type="
|
|
|
|
| 366 |
</div>
|
| 367 |
<div class="form-group">
|
| 368 |
<label for="register-password">Contraseña</label>
|
| 369 |
-
<input type="password" id="register-password" placeholder="Mínimo 8 caracteres"
|
|
|
|
| 370 |
</div>
|
| 371 |
<div class="form-group">
|
| 372 |
<label for="register-password-confirm">Confirmar contraseña</label>
|
| 373 |
-
<input type="password" id="register-password-confirm" placeholder="Repite la contraseña"
|
|
|
|
| 374 |
</div>
|
| 375 |
<div class="form-error" id="register-error"></div>
|
| 376 |
<button type="submit" class="modal-submit">Crear cuenta</button>
|
|
|
|
| 345 |
</div>
|
| 346 |
<div class="modal-body">
|
| 347 |
<!-- Login Form -->
|
| 348 |
+
<form class="modal-form active" id="form-login" novalidate>
|
| 349 |
<div class="form-group">
|
| 350 |
<label for="login-email">Correo electrónico</label>
|
| 351 |
+
<input type="text" id="login-email" placeholder="usuario@ejemplo.com" autocomplete="email" />
|
| 352 |
+
<small class="field-error" id="login-email-error"></small>
|
| 353 |
</div>
|
| 354 |
<div class="form-group">
|
| 355 |
<label for="login-password">Contraseña</label>
|
| 356 |
+
<input type="password" id="login-password" placeholder="••••••••" autocomplete="current-password" />
|
| 357 |
+
<small class="field-error" id="login-password-error"></small>
|
| 358 |
</div>
|
| 359 |
<div class="form-error" id="login-error"></div>
|
| 360 |
<button type="submit" class="modal-submit">Entrar</button>
|
| 361 |
</form>
|
| 362 |
|
| 363 |
<!-- Register Form -->
|
| 364 |
+
<form class="modal-form" id="form-register" novalidate>
|
| 365 |
<div class="form-group">
|
| 366 |
<label for="register-email">Correo electrónico</label>
|
| 367 |
+
<input type="text" id="register-email" placeholder="usuario@ejemplo.com" autocomplete="email" />
|
| 368 |
+
<small class="field-error" id="register-email-error"></small>
|
| 369 |
</div>
|
| 370 |
<div class="form-group">
|
| 371 |
<label for="register-password">Contraseña</label>
|
| 372 |
+
<input type="password" id="register-password" placeholder="Mínimo 8 caracteres" autocomplete="new-password" />
|
| 373 |
+
<small class="field-error" id="register-password-error"></small>
|
| 374 |
</div>
|
| 375 |
<div class="form-group">
|
| 376 |
<label for="register-password-confirm">Confirmar contraseña</label>
|
| 377 |
+
<input type="password" id="register-password-confirm" placeholder="Repite la contraseña" autocomplete="new-password" />
|
| 378 |
+
<small class="field-error" id="register-password-confirm-error"></small>
|
| 379 |
</div>
|
| 380 |
<div class="form-error" id="register-error"></div>
|
| 381 |
<button type="submit" class="modal-submit">Crear cuenta</button>
|
frontend/src/api.js
CHANGED
|
@@ -65,8 +65,21 @@ export async function register(email, password) {
|
|
| 65 |
return body
|
| 66 |
}
|
| 67 |
|
| 68 |
-
export function logout() {
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
export async function getMe() {
|
|
|
|
| 65 |
return body
|
| 66 |
}
|
| 67 |
|
| 68 |
+
export async function logout() {
|
| 69 |
+
const token = getToken()
|
| 70 |
+
try {
|
| 71 |
+
if (token) {
|
| 72 |
+
await fetch(`${BASE}/auth/logout`, {
|
| 73 |
+
method: 'POST',
|
| 74 |
+
headers: {
|
| 75 |
+
'Content-Type': 'application/json',
|
| 76 |
+
Authorization: `Bearer ${token}`,
|
| 77 |
+
},
|
| 78 |
+
})
|
| 79 |
+
}
|
| 80 |
+
} finally {
|
| 81 |
+
clearToken()
|
| 82 |
+
}
|
| 83 |
}
|
| 84 |
|
| 85 |
export async function getMe() {
|
frontend/src/app.js
CHANGED
|
@@ -32,6 +32,36 @@ import * as map from './map.js'
|
|
| 32 |
import * as simulator from './simulator.js'
|
| 33 |
import { extractFilterOptions, filterMarkets } from './filters.js'
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
/* ─── Estado global ─── */
|
| 36 |
let state = {
|
| 37 |
view: 'dashboard',
|
|
@@ -361,8 +391,8 @@ function updateAuthButton() {
|
|
| 361 |
if (btn) {
|
| 362 |
if (authed) {
|
| 363 |
btn.textContent = 'Salir'
|
| 364 |
-
btn.onclick = () => {
|
| 365 |
-
api.logout()
|
| 366 |
updateAuthButton()
|
| 367 |
location.reload()
|
| 368 |
}
|
|
@@ -376,8 +406,8 @@ function updateAuthButton() {
|
|
| 376 |
indicator.classList.toggle('logged-in', authed)
|
| 377 |
indicator.title = authed ? 'Salir' : 'Entrar'
|
| 378 |
indicator.onclick = authed
|
| 379 |
-
? () => {
|
| 380 |
-
api.logout()
|
| 381 |
updateAuthButton()
|
| 382 |
location.reload()
|
| 383 |
}
|
|
@@ -390,6 +420,28 @@ async function handleLogin(e) {
|
|
| 390 |
const email = document.getElementById('login-email').value.trim()
|
| 391 |
const password = document.getElementById('login-password').value
|
| 392 |
const errorEl = document.getElementById('login-error')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
try {
|
| 394 |
await api.login(email, password)
|
| 395 |
closeAuthModal()
|
|
@@ -400,6 +452,12 @@ async function handleLogin(e) {
|
|
| 400 |
}
|
| 401 |
}
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
async function handleRegister(e) {
|
| 404 |
e.preventDefault()
|
| 405 |
const email = document.getElementById('register-email').value.trim()
|
|
@@ -407,12 +465,34 @@ async function handleRegister(e) {
|
|
| 407 |
const confirm = document.getElementById('register-password-confirm').value
|
| 408 |
const errorEl = document.getElementById('register-error')
|
| 409 |
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
}
|
| 414 |
-
if (password
|
| 415 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
return
|
| 417 |
}
|
| 418 |
|
|
@@ -422,10 +502,22 @@ async function handleRegister(e) {
|
|
| 422 |
updateAuthButton()
|
| 423 |
await initAppData()
|
| 424 |
} catch (err) {
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
}
|
| 427 |
}
|
| 428 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
async function ensureAuth() {
|
| 430 |
if (!api.isAuthenticated()) return false
|
| 431 |
try {
|
|
@@ -1187,7 +1279,9 @@ export async function init() {
|
|
| 1187 |
tab.addEventListener('click', () => switchAuthTab(tab.dataset.tab))
|
| 1188 |
})
|
| 1189 |
document.getElementById('form-login')?.addEventListener('submit', handleLogin)
|
|
|
|
| 1190 |
document.getElementById('form-register')?.addEventListener('submit', handleRegister)
|
|
|
|
| 1191 |
document.getElementById('auth-modal')?.addEventListener('click', (e) => {
|
| 1192 |
if (e.target.id === 'auth-modal') closeAuthModal()
|
| 1193 |
})
|
|
|
|
| 32 |
import * as simulator from './simulator.js'
|
| 33 |
import { extractFilterOptions, filterMarkets } from './filters.js'
|
| 34 |
|
| 35 |
+
/* ─── Helpers de validación de formularios ─── */
|
| 36 |
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
| 37 |
+
|
| 38 |
+
function isValidEmail(value) {
|
| 39 |
+
return EMAIL_RE.test(value.trim())
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
function showFieldError(inputId, msg) {
|
| 43 |
+
const input = document.getElementById(inputId)
|
| 44 |
+
const errorEl = document.getElementById(`${inputId}-error`)
|
| 45 |
+
if (input) input.classList.add('input-invalid')
|
| 46 |
+
if (errorEl) errorEl.textContent = msg
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
function clearFieldError(inputId) {
|
| 50 |
+
const input = document.getElementById(inputId)
|
| 51 |
+
const errorEl = document.getElementById(`${inputId}-error`)
|
| 52 |
+
if (input) input.classList.remove('input-invalid')
|
| 53 |
+
if (errorEl) errorEl.textContent = ''
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
function clearAllFieldErrors(...inputIds) {
|
| 57 |
+
inputIds.forEach(clearFieldError)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
function focusFirstInvalid(form) {
|
| 61 |
+
const invalid = form.querySelector('.input-invalid')
|
| 62 |
+
if (invalid) invalid.focus()
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
/* ─── Estado global ─── */
|
| 66 |
let state = {
|
| 67 |
view: 'dashboard',
|
|
|
|
| 391 |
if (btn) {
|
| 392 |
if (authed) {
|
| 393 |
btn.textContent = 'Salir'
|
| 394 |
+
btn.onclick = async () => {
|
| 395 |
+
await api.logout()
|
| 396 |
updateAuthButton()
|
| 397 |
location.reload()
|
| 398 |
}
|
|
|
|
| 406 |
indicator.classList.toggle('logged-in', authed)
|
| 407 |
indicator.title = authed ? 'Salir' : 'Entrar'
|
| 408 |
indicator.onclick = authed
|
| 409 |
+
? async () => {
|
| 410 |
+
await api.logout()
|
| 411 |
updateAuthButton()
|
| 412 |
location.reload()
|
| 413 |
}
|
|
|
|
| 420 |
const email = document.getElementById('login-email').value.trim()
|
| 421 |
const password = document.getElementById('login-password').value
|
| 422 |
const errorEl = document.getElementById('login-error')
|
| 423 |
+
|
| 424 |
+
clearAllFieldErrors('login-email', 'login-password')
|
| 425 |
+
errorEl.textContent = ''
|
| 426 |
+
|
| 427 |
+
let valid = true
|
| 428 |
+
if (!email) {
|
| 429 |
+
showFieldError('login-email', 'Introduce tu correo electrónico.')
|
| 430 |
+
valid = false
|
| 431 |
+
} else if (!isValidEmail(email)) {
|
| 432 |
+
showFieldError('login-email', 'El formato del correo no es válido.')
|
| 433 |
+
valid = false
|
| 434 |
+
}
|
| 435 |
+
if (!password) {
|
| 436 |
+
showFieldError('login-password', 'Introduce tu contraseña.')
|
| 437 |
+
valid = false
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
if (!valid) {
|
| 441 |
+
focusFirstInvalid(e.target)
|
| 442 |
+
return
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
try {
|
| 446 |
await api.login(email, password)
|
| 447 |
closeAuthModal()
|
|
|
|
| 452 |
}
|
| 453 |
}
|
| 454 |
|
| 455 |
+
function attachLoginInputListeners() {
|
| 456 |
+
;['login-email', 'login-password'].forEach((id) => {
|
| 457 |
+
document.getElementById(id)?.addEventListener('input', () => clearFieldError(id))
|
| 458 |
+
})
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
async function handleRegister(e) {
|
| 462 |
e.preventDefault()
|
| 463 |
const email = document.getElementById('register-email').value.trim()
|
|
|
|
| 465 |
const confirm = document.getElementById('register-password-confirm').value
|
| 466 |
const errorEl = document.getElementById('register-error')
|
| 467 |
|
| 468 |
+
clearAllFieldErrors('register-email', 'register-password', 'register-password-confirm')
|
| 469 |
+
errorEl.textContent = ''
|
| 470 |
+
|
| 471 |
+
let valid = true
|
| 472 |
+
if (!email) {
|
| 473 |
+
showFieldError('register-email', 'Introduce tu correo electrónico.')
|
| 474 |
+
valid = false
|
| 475 |
+
} else if (!isValidEmail(email)) {
|
| 476 |
+
showFieldError('register-email', 'El formato del correo no es válido.')
|
| 477 |
+
valid = false
|
| 478 |
}
|
| 479 |
+
if (!password) {
|
| 480 |
+
showFieldError('register-password', 'Introduce una contraseña.')
|
| 481 |
+
valid = false
|
| 482 |
+
} else if (password.length < 8) {
|
| 483 |
+
showFieldError('register-password', 'La contraseña debe tener al menos 8 caracteres.')
|
| 484 |
+
valid = false
|
| 485 |
+
}
|
| 486 |
+
if (!confirm) {
|
| 487 |
+
showFieldError('register-password-confirm', 'Confirma tu contraseña.')
|
| 488 |
+
valid = false
|
| 489 |
+
} else if (confirm !== password) {
|
| 490 |
+
showFieldError('register-password-confirm', 'Las contraseñas no coinciden.')
|
| 491 |
+
valid = false
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
if (!valid) {
|
| 495 |
+
focusFirstInvalid(e.target)
|
| 496 |
return
|
| 497 |
}
|
| 498 |
|
|
|
|
| 502 |
updateAuthButton()
|
| 503 |
await initAppData()
|
| 504 |
} catch (err) {
|
| 505 |
+
const isEmailTaken = err.message?.includes('EMAIL_EXISTS') || err.message?.includes('409')
|
| 506 |
+
if (isEmailTaken) {
|
| 507 |
+
showFieldError('register-email', 'Este correo ya está registrado.')
|
| 508 |
+
focusFirstInvalid(e.target)
|
| 509 |
+
} else {
|
| 510 |
+
errorEl.textContent = 'Error al registrar. Inténtalo de nuevo.'
|
| 511 |
+
}
|
| 512 |
}
|
| 513 |
}
|
| 514 |
|
| 515 |
+
function attachRegisterInputListeners() {
|
| 516 |
+
;['register-email', 'register-password', 'register-password-confirm'].forEach((id) => {
|
| 517 |
+
document.getElementById(id)?.addEventListener('input', () => clearFieldError(id))
|
| 518 |
+
})
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
async function ensureAuth() {
|
| 522 |
if (!api.isAuthenticated()) return false
|
| 523 |
try {
|
|
|
|
| 1279 |
tab.addEventListener('click', () => switchAuthTab(tab.dataset.tab))
|
| 1280 |
})
|
| 1281 |
document.getElementById('form-login')?.addEventListener('submit', handleLogin)
|
| 1282 |
+
attachLoginInputListeners()
|
| 1283 |
document.getElementById('form-register')?.addEventListener('submit', handleRegister)
|
| 1284 |
+
attachRegisterInputListeners()
|
| 1285 |
document.getElementById('auth-modal')?.addEventListener('click', (e) => {
|
| 1286 |
if (e.target.id === 'auth-modal') closeAuthModal()
|
| 1287 |
})
|
frontend/src/style.css
CHANGED
|
@@ -1367,6 +1367,22 @@ td {
|
|
| 1367 |
min-height: 18px;
|
| 1368 |
}
|
| 1369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1370 |
.modal-submit {
|
| 1371 |
background: #2563eb;
|
| 1372 |
border: none;
|
|
|
|
| 1367 |
min-height: 18px;
|
| 1368 |
}
|
| 1369 |
|
| 1370 |
+
.field-error {
|
| 1371 |
+
display: block;
|
| 1372 |
+
font-size: 0.8125rem;
|
| 1373 |
+
color: var(--red);
|
| 1374 |
+
margin-top: 4px;
|
| 1375 |
+
min-height: 16px;
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
.form-group input.input-invalid {
|
| 1379 |
+
border-color: var(--red);
|
| 1380 |
+
}
|
| 1381 |
+
|
| 1382 |
+
.form-group input.input-invalid:focus {
|
| 1383 |
+
border-color: var(--red);
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
.modal-submit {
|
| 1387 |
background: #2563eb;
|
| 1388 |
border: none;
|