Certo Fabio β€” ora che tutto il backend e il frontend sono finalmente coerenti, ti preparo un README tecnico completo, scritto come si deve, chiaro, professionale e utile sia per te che per chiunque metterΓ  mano al progetto in futuro. È pensato per essere messo direttamente in README.md alla radice del progetto. --- πŸ“Έ PhotoGallery β€” README Tecnico Completo Una piattaforma completa per la gestione di foto multi‑utente, con: - autenticazione JWT - scansione automatica delle cartelle - thumbnails generate lato server - sincronizzazione incrementale - WebSocket real‑time - frontend modulare e reattivo - mappa globale stile Google Photos - pannello info EXIF + geolocalizzazione - bottom sheet multi‑foto - modal fullscreen con navigazione --- πŸš€ 1. Architettura generale ` backend/ server.js api_v1/ scanner/ config.js ... db/ knex.js routes/ photos.js public/ index.html login.html css/ js/ auth.js login.js logout.js config.js api.js state.js sync.js gallery.js modal.js infoPanel.js mapGlobal.js bottomSheet.js optionsSheet.js ` --- πŸ” 2. Autenticazione Login POST /auth/login Richiede: `json { "email": "...", "password": "..." } ` Risponde: `json { "token": "JWT...", "name": "Fabio" } ` Il token contiene: `json { "id": 1, "email": "fabio@example.com", "name": "Fabio", "exp": 1234567890 } ` Logout POST /auth/logout Il token viene inserito in denylist. Middleware Tutte le rotte (eccetto /auth/*) richiedono: ` Authorization: Bearer ` Il middleware aggiunge automaticamente: ` req.query.user = [req.user.name, "Common"] ` Quindi il frontend non deve mai passare user=. --- πŸ—‚οΈ 3. Configurazione GET /config restituisce: `json { "baseUrl": "https://…", "pathFull": false, "galleryRefreshSeconds": 30 } ` Il frontend usa: - baseUrl per costruire URL assoluti - pathFull per capire se i path sono giΓ  completi - galleryRefreshSeconds per il polling --- πŸ–ΌοΈ 4. Foto: API principali Tutte le foto GET /photos?user[]=Fabio&user[]=Common (aggiunto automaticamente dal middleware) Foto per ID GET /photos/byIds?id=123 Cambiamenti incrementali GET /photos/changes?since= Restituisce: `json { "changes": [ { "photoid": 123, "changetype": "added", "timestamp": "..." }, { "photoid": 456, "changetype": "removed", "timestamp": "..." } ] } ` --- πŸ”„ 5. Sincronizzazione Il frontend implementa: Full load - chiamato al primo avvio - scarica tutte le foto - salva in localStorage - aggiorna lastSync Incremental sync - usa /photos/changes - applica solo differenze - aggiorna la gallery - aggiorna lastSync WebSocket real‑time Il server invia: ` { type: "added", id } { type: "removed", id } { type: "add_dir", folder } { type: "del_dir", folder } ` Il frontend aggiorna: - stato locale - gallery - mappa --- πŸ—ΊοΈ 6. Mappa globale Basata su Leaflet + MarkerCluster. - clustering automatico - collage thumbnails nei cluster - click cluster β†’ bottom sheet - click foto β†’ modal - refresh automatico quando cambia la gallery --- πŸͺŸ 7. Modal foto/video FunzionalitΓ : - navigazione ← β†’ - click ai bordi - tastiera - preload Β±3 foto - supporto video (mp4/webm/mov) - pannello info integrato --- ℹ️ 8. Info Panel Mostra: - nome - data - dimensioni - peso - MIME - cartella - EXIF GPS - reverse geocoding (se presente) - mappa dedicata Auto‑refresh quando cambi foto nel modal. --- πŸ“š 9. Bottom Sheet Usato per: - mostrare gruppi di foto (cluster mappa) - navigazione rapida - apertura modal --- βš™οΈ 10. Options Sheet Gestisce: - ordinamento (asc/desc) - raggruppamento (auto/day/month/year) - filtri (folder/location/type) Aggiorna la gallery in tempo reale. --- 🧠 11. Stato locale Gestito in state.js: - localPhotos[] - lastSync - cache in localStorage - funzioni: add/remove/update --- 🧩 12. Costruzione URL media Tutto passa da: `js mediaUrlsFromPhoto(photo) ` Restituisce: `js { original, preview } ` Supporta: - path relativi - path assoluti - baseUrl dinamico - cartella utente --- πŸ§ͺ 13. Test end‑to‑end consigliato 1. Vai su /login 2. Inserisci email/password 3. Redirect a / 4. Full load β†’ gallery appare 5. Apri una foto β†’ modal 6. Apri info β†’ mappa + EXIF 7. Apri mappa globale β†’ cluster 8. Click cluster β†’ bottom sheet 9. Click foto β†’ modal 10. Cancella una foto dal filesystem β†’ WS β†’ frontend aggiorna 11. Logout β†’ redirect a login --- πŸ› οΈ 14. Requisiti - Node.js 18+ - SQLite3 - ffmpeg (per thumbnails video) - ImageMagick (per thumbnails foto) --- ▢️ 15. Avvio `bash npm install node server.js ` Server su: ` http://localhost:4000 ` --- 🎯 16. TODO / miglioramenti futuri - virtualizzazione gallery (migliaia di foto) - ricerca per testo - timeline orizzontale - upload drag&drop - editing EXIF - preferiti / album --- πŸ’¬ 17. Contatti Progetto sviluppato da Fabio Backend + frontend completamente integrati. --- Perfetto Fabio β€” ti preparo il diagramma architetturale completo, chiaro, leggibile e fedele al tuo backend e al tuo frontend attuale. Niente fronzoli, niente astrazioni inutili: solo ciΓ² che serve davvero per capire come gira tutto. --- 🧩 DIAGRAMMA ARCHITETTURALE β€” PhotoGallery System 1) Vista generale (High‑Level) ` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FRONTEND β”‚ β”‚ (public/, JS modules) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ HTTP + WS β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BACKEND β”‚ β”‚ (Node + Express) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ DB queries β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SQLite DB β”‚ β”‚ (tabella photos) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ` --- 2) Dettaglio Frontend (moduli JS) ` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FRONTEND β”‚ β”‚ β”‚ β”‚ index.html / login.html β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ auth.js ←→ login.js ←→ logout.js β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ config.js β†’ api.js β†’ state.js β†’ sync.js β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ └── WebSocket β”‚ β”‚ β–Ό β–Ό β”‚ β”‚ gallery.js ←→ modal.js ←→ infoPanel.js β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β”‚ β”‚ mapGlobal.js ←→ bottomSheet.js ←→ optionsSheet.js β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ` Flusso principale: 1. auth.js controlla token β†’ se valido, mostra app 2. config.js carica /config 3. sync.js fa full load β†’ incremental sync β†’ WebSocket 4. state.js mantiene foto locali 5. gallery.js renderizza 6. modal.js apre foto/video 7. infoPanel.js mostra EXIF + mappa 8. mapGlobal.js mostra mappa globale 9. bottomSheet.js mostra strip foto 10. optionsSheet.js gestisce filtri/ordinamento --- 3) Dettaglio Backend (server.js) ` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BACKEND β”‚ β”‚ β”‚ β”‚ server.js β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ STATIC: serve public/ β”‚ β”‚ β”œβ”€β”€ /config β”‚ β”‚ β”œβ”€β”€ /auth/login β”‚ β”‚ β”œβ”€β”€ /auth/logout β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ JWT middleware β”‚ β”‚ β”‚ β”œβ”€β”€ verify token β”‚ β”‚ β”‚ β”œβ”€β”€ denylist β”‚ β”‚ β”‚ └── req.user = { id, email, name } β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ GET middleware (user filtering) β”‚ β”‚ β”‚ └── req.query.user = [req.user.name, "Common"] β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ /scan (Admin β†’ tutti, User β†’ solo se stesso) β”‚ β”‚ β”œβ”€β”€ /apiv1/autoscan (ADD, DEL, ADDDIR, DELDIR) β”‚ β”‚ β”‚ └── WebSocket broadcast β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ /photos (router SQLite) β”‚ β”‚ β”‚ β”œβ”€β”€ /photos β”‚ β”‚ β”‚ β”œβ”€β”€ /photos/byIds β”‚ β”‚ β”‚ └── /photos/changes β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ /files (serve file statici sicuri) β”‚ β”‚ β”œβ”€β”€ /initDB /initDBuser β”‚ β”‚ β”‚ β”‚ β”‚ └── ws-server.js (WebSocket) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ` --- 4) Flusso completo di una foto (end‑to‑end) ` [1] File aggiunto nel filesystem β”‚ β–Ό [2] scan_auto β†’ type="ADD" β”‚ β”œβ”€β”€ scanFile() β”œβ”€β”€ scanPhotoSingle() β”œβ”€β”€ INSERT in DB β”œβ”€β”€ genera thumbnails └── WS: { type:"added", id } β”‚ β–Ό [3] Frontend riceve WS β”‚ β”œβ”€β”€ getPhotoById(id) β”œβ”€β”€ addPhotoLocal() β”œβ”€β”€ refreshGallery() └── redrawPhotoMarkers() ` --- 5) Flusso login β†’ gallery ` login.html β”‚ β–Ό login.js β†’ AppAuth.login(email, password) β”‚ β–Ό POST /auth/login β”‚ β–Ό token JWT salvato β”‚ β–Ό redirect β†’ / β”‚ β–Ό index.html β”‚ β–Ό auth.js.isLoggedIn() β†’ OK β”‚ β–Ό config.js β†’ GET /config β”‚ β–Ό sync.js.fullLoad() β”‚ β–Ό GET /photos β”‚ β–Ό state.js.setLocalPhotos() β”‚ β–Ό gallery.js.renderGallery() β”‚ β–Ό mapGlobal.js.redrawPhotoMarkers() ` --- 6) Diagramma WebSocket ` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” WS β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Backend β”‚ ───────────→ β”‚ Frontend β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ added β†’ { id } β”‚ │────────────────────────────────▢│ addPhotoLocal() β”‚ β”‚ refreshGallery() β”‚ β”‚ redrawPhotoMarkers() β”‚ β”‚ β”‚ removed β†’ { id } β”‚ │────────────────────────────────▢│ removePhotoLocal() β”‚ β”‚ refreshGallery() β”‚ β”‚ redrawPhotoMarkers() β”‚ β”‚ β”‚ adddir / deldir β”‚ │────────────────────────────────▢│ incrementalSync() ` --- 7) Diagramma Database ` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ photos β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ id (PK) β”‚ β”‚ user β”‚ β”‚ name β”‚ β”‚ path β”‚ β”‚ cartella β”‚ β”‚ mime_type β”‚ β”‚ width β”‚ β”‚ height β”‚ β”‚ size_bytes β”‚ β”‚ taken_at β”‚ β”‚ gps_lat β”‚ β”‚ gps_lng β”‚ β”‚ gps_alt β”‚ β”‚ location_json β”‚ β”‚ created_at β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ` --- 8) Diagramma URL media ` BASE_URL/photos//// Esempi: original: https://server/photos/Fabio/original/2024/IMG_001.jpg thumbs: https://server/photos/Fabio/thumbs/2024/IMG001thub2.jpg ` --- 9) Diagramma dei moduli JS (dipendenze) ` auth.js ↑ login.js β†’ logout.js ↑ index.html ↓ config.js β†’ api.js β†’ state.js β†’ sync.js β†’ WebSocket ↓ ↓ ↓ gallery.js ← modal.js ← infoPanel.js ↓ mapGlobal.js ← bottomSheet.js ← optionsSheet.js ` --- Utente Browser/Frontend Backend (Express) DB (SQLite) β”‚ β”‚ β”‚ β”‚ β”‚ 1. Apre /login β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 2. Inserisce email/password β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ POST /auth/login β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 3. Verifica utente β”‚ β”‚ β”‚ β”‚ bcrypt.compare() β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 4. Genera JWT β”‚ β”‚ β”‚ β”‚ createToken() β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 5. Riceve token β”‚ β”‚ β”‚ ◀────────────────────────────────── β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 6. Salva token (localStorage) β”‚ β”‚ β”‚ β”‚ 7. Redirect β†’ / β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 8. GET /config β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 9. Riceve config β”‚ β”‚ β”‚ ◀────────────────────────────────── β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 10. Full Sync β”‚ β”‚ β”‚ β”‚ GET /photos β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ 11. Middleware JWT β”‚ β”‚ β”‚ β”‚ req.user = {name,...} β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 12. Filtra per utente β”‚ β”‚ β”‚ β”‚ req.query.user=[Fabio,Common] β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ SELECT * FROM photos WHERE user IN (...) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 13. Riceve lista foto β”‚ β”‚ β”‚ ◀────────────────────────────────── β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 14. state.setLocalPhotos() β”‚ β”‚ β”‚ β”‚ 15. gallery.render() β”‚ β”‚ β”‚ β”‚ 16. mapGlobal.redrawMarkers() β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 17. Apre WebSocket β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ ws-server β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 18. Incremental Sync (polling) β”‚ β”‚ β”‚ β”‚ GET /photos/changes?since=... β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 19. Riceve changes β”‚ β”‚ β”‚ ◀────────────────────────────────── β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 20. Applica differenze β”‚ β”‚ β”‚ β”‚ state.add/remove/update β”‚ β”‚ β”‚ β”‚ gallery.refresh() β”‚ β”‚ β”‚ β”‚ mapGlobal.redrawMarkers() β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 21. Evento reale: file aggiunto β”‚ β”‚ β”‚ β”‚ β”‚ scan_auto β†’ ADD β”‚ β”‚ β”‚ β”‚ INSERT in DB β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 22. WS broadcast β”‚ β”‚ ◀────────────────────────────────── {type:"added", id} β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 23. getPhotoById(id) β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ /photos/byIds β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 24. Riceve foto β”‚ β”‚ β”‚ ◀────────────────────────────────── β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 25. state.addPhotoLocal() β”‚ β”‚ β”‚ β”‚ 26. gallery.refresh() β”‚ β”‚ β”‚ β”‚ 27. mapGlobal.redrawMarkers() β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 28. Clic su foto β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ modal.open(photo) β”‚ β”‚ β”‚ β”‚ infoPanel.render(photo) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 29. Logout β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ AppAuth.logout() β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Άβ”‚ POST /auth/logout β”‚ β”‚ β”‚ β”‚ addToDenylist(token) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ 30. clearTokens() β”‚ β”‚ β”‚ β”‚ 31. redirect β†’ /login β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜