germansango01 commited on
Commit
6ebcfa2
·
1 Parent(s): cbd824d

Validate logout flow whit backend

Browse files
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="email" id="login-email" placeholder="usuario@ejemplo.com" required />
 
352
  </div>
353
  <div class="form-group">
354
  <label for="login-password">Contraseña</label>
355
- <input type="password" id="login-password" placeholder="••••••••" required />
 
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="email" id="register-email" placeholder="usuario@ejemplo.com" required />
 
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" required minlength="8" />
 
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" required />
 
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
- clearToken()
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- if (password !== confirm) {
411
- errorEl.textContent = 'Las contraseñas no coinciden.'
412
- return
 
 
 
 
 
 
 
413
  }
414
- if (password.length < 8) {
415
- errorEl.textContent = 'La contraseña debe tener al menos 8 caracteres.'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  return
417
  }
418
 
@@ -422,10 +502,22 @@ async function handleRegister(e) {
422
  updateAuthButton()
423
  await initAppData()
424
  } catch (err) {
425
- errorEl.textContent = 'Error al registrar. El correo podría estar en uso.'
 
 
 
 
 
 
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;