feat: use sessionStorage by default for UUID/tokens, add opt-in remember-me with warning

This commit is contained in:
2026-02-08 14:02:46 +01:00
parent 8073003460
commit 5493148551
10 changed files with 95 additions and 11 deletions

View File

@@ -110,6 +110,14 @@ class AuthModal extends HTMLElement {
</div> </div>
` : ''} ` : ''}
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="remember-me" ${auth.getRememberMe() ? 'checked' : ''}>
<span>${t('auth.rememberMe')}</span>
</label>
<p class="field-hint">${t('auth.rememberMeHint')}</p>
</div>
<button type="submit" class="btn btn-primary btn-lg btn-block" ${this.loading ? 'disabled' : ''}> <button type="submit" class="btn btn-primary btn-lg btn-block" ${this.loading ? 'disabled' : ''}>
${this.loading ? t('auth.loggingIn') : t('auth.login')} ${this.loading ? t('auth.loggingIn') : t('auth.login')}
</button> </button>
@@ -247,6 +255,9 @@ class AuthModal extends HTMLElement {
this.error = null this.error = null
this.render() this.render()
const rememberMe = this.querySelector('#remember-me')?.checked || false
auth.setRememberMe(rememberMe)
const result = await auth.login(uuid) const result = await auth.login(uuid)
this.loading = false this.loading = false
@@ -469,6 +480,26 @@ style.textContent = /* css */`
auth-modal .link-btn:hover { auth-modal .link-btn:hover {
color: var(--color-accent-hover); color: var(--color-accent-hover);
} }
auth-modal .checkbox-label {
display: flex;
align-items: center;
gap: var(--space-sm);
cursor: pointer;
font-size: var(--font-size-sm);
}
auth-modal .checkbox-label input {
width: 16px;
height: 16px;
accent-color: var(--color-accent);
}
auth-modal .field-hint {
font-size: var(--font-size-xs);
color: var(--color-text-muted);
margin-top: var(--space-xs);
}
` `
document.head.appendChild(style) document.head.appendChild(style)

View File

@@ -7,6 +7,7 @@
*/ */
import { directus } from './directus.js' import { directus } from './directus.js'
import { setPersist, getPersist } from './directus/client.js'
import { i18n } from '../i18n.js' import { i18n } from '../i18n.js'
const AUTH_DOMAIN = 'dgray.io' const AUTH_DOMAIN = 'dgray.io'
@@ -16,6 +17,9 @@ class AuthService {
this.currentUser = null this.currentUser = null
this.listeners = new Set() this.listeners = new Set()
this.hashCache = new Map() this.hashCache = new Map()
if (localStorage.getItem('dgray_remember') === '1') {
setPersist(true)
}
} }
/** /**
@@ -136,6 +140,8 @@ class AuthService {
this.currentUser = null this.currentUser = null
this.clearStoredUuid() this.clearStoredUuid()
localStorage.removeItem('dgray_remember')
setPersist(false)
this.resetPreferencesToDefaults() this.resetPreferencesToDefaults()
this.notifyListeners() this.notifyListeners()
} }
@@ -227,7 +233,8 @@ class AuthService {
* @param {string} uuid * @param {string} uuid
*/ */
storeUuid(uuid) { storeUuid(uuid) {
localStorage.setItem('dgray_uuid', uuid) const storage = getPersist() ? localStorage : sessionStorage
storage.setItem('dgray_uuid', uuid)
} }
/** /**
@@ -235,16 +242,30 @@ class AuthService {
* @returns {string|null} * @returns {string|null}
*/ */
getStoredUuid() { getStoredUuid() {
return localStorage.getItem('dgray_uuid') return sessionStorage.getItem('dgray_uuid') || localStorage.getItem('dgray_uuid')
} }
/** /**
* Clears stored UUID * Clears stored UUID
*/ */
clearStoredUuid() { clearStoredUuid() {
sessionStorage.removeItem('dgray_uuid')
localStorage.removeItem('dgray_uuid') localStorage.removeItem('dgray_uuid')
} }
setRememberMe(value) {
setPersist(value)
if (value) {
localStorage.setItem('dgray_remember', '1')
} else {
localStorage.removeItem('dgray_remember')
}
}
getRememberMe() {
return localStorage.getItem('dgray_remember') === '1'
}
/** /**
* Subscribe to auth state changes * Subscribe to auth state changes
* @param {Function} callback * @param {Function} callback

View File

@@ -1,5 +1,19 @@
const DIRECTUS_URL = 'https://api.dgray.io' const DIRECTUS_URL = 'https://api.dgray.io'
let _persist = false
export function setPersist(value) {
_persist = value
}
export function getPersist() {
return _persist
}
function _storage() {
return _persist ? localStorage : sessionStorage
}
class DirectusError extends Error { class DirectusError extends Error {
constructor(status, message, data = {}) { constructor(status, message, data = {}) {
super(message) super(message)
@@ -37,13 +51,16 @@ class DirectusClient {
// ── Token Management ── // ── Token Management ──
loadTokens() { loadTokens() {
const stored = localStorage.getItem('dgray_auth') const stored = sessionStorage.getItem('dgray_auth') || localStorage.getItem('dgray_auth')
if (stored) { if (stored) {
try { try {
const { accessToken, refreshToken, expiry } = JSON.parse(stored) const { accessToken, refreshToken, expiry } = JSON.parse(stored)
this.accessToken = accessToken this.accessToken = accessToken
this.refreshToken = refreshToken this.refreshToken = refreshToken
this.tokenExpiry = expiry this.tokenExpiry = expiry
if (localStorage.getItem('dgray_auth')) {
_persist = true
}
this.scheduleTokenRefresh() this.scheduleTokenRefresh()
} catch (e) { } catch (e) {
this.clearTokens() this.clearTokens()
@@ -56,7 +73,7 @@ class DirectusClient {
this.refreshToken = refreshToken this.refreshToken = refreshToken
this.tokenExpiry = Date.now() + (expiresIn * 1000) this.tokenExpiry = Date.now() + (expiresIn * 1000)
localStorage.setItem('dgray_auth', JSON.stringify({ _storage().setItem('dgray_auth', JSON.stringify({
accessToken: this.accessToken, accessToken: this.accessToken,
refreshToken: this.refreshToken, refreshToken: this.refreshToken,
expiry: this.tokenExpiry expiry: this.tokenExpiry
@@ -69,6 +86,7 @@ class DirectusClient {
this.accessToken = null this.accessToken = null
this.refreshToken = null this.refreshToken = null
this.tokenExpiry = null this.tokenExpiry = null
sessionStorage.removeItem('dgray_auth')
localStorage.removeItem('dgray_auth') localStorage.removeItem('dgray_auth')
if (this.refreshTimeout) { if (this.refreshTimeout) {

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Backup herunterladen", "downloadBackup": "Backup herunterladen",
"confirmSaved": "Ich habe meine UUID gespeichert", "confirmSaved": "Ich habe meine UUID gespeichert",
"registrationFailed": "Registrierung fehlgeschlagen", "registrationFailed": "Registrierung fehlgeschlagen",
"loginRequired": "Bitte melde dich an, um fortzufahren" "loginRequired": "Bitte melde dich an, um fortzufahren",
"rememberMe": "Auf diesem Gerät merken",
"rememberMeHint": "Deine UUID wird lokal gespeichert. Nur aktivieren auf vertrauenswürdigen Geräten."
}, },
"favorites": { "favorites": {
"title": "Favoriten", "title": "Favoriten",

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Download Backup", "downloadBackup": "Download Backup",
"confirmSaved": "I have saved my UUID", "confirmSaved": "I have saved my UUID",
"registrationFailed": "Registration failed", "registrationFailed": "Registration failed",
"loginRequired": "Please log in to continue" "loginRequired": "Please log in to continue",
"rememberMe": "Remember me on this device",
"rememberMeHint": "Your UUID will be stored locally. Only enable on trusted devices."
}, },
"favorites": { "favorites": {
"title": "Favorites", "title": "Favorites",

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Descargar copia de seguridad", "downloadBackup": "Descargar copia de seguridad",
"confirmSaved": "He guardado mi UUID", "confirmSaved": "He guardado mi UUID",
"registrationFailed": "Error en el registro", "registrationFailed": "Error en el registro",
"loginRequired": "Inicia sesión para continuar" "loginRequired": "Inicia sesión para continuar",
"rememberMe": "Recordarme en este dispositivo",
"rememberMeHint": "Tu UUID se guardará localmente. Actívalo solo en dispositivos de confianza."
}, },
"favorites": { "favorites": {
"title": "Favoritos", "title": "Favoritos",

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Télécharger la sauvegarde", "downloadBackup": "Télécharger la sauvegarde",
"confirmSaved": "J'ai sauvegardé mon UUID", "confirmSaved": "J'ai sauvegardé mon UUID",
"registrationFailed": "Échec de l'inscription", "registrationFailed": "Échec de l'inscription",
"loginRequired": "Veuillez vous connecter pour continuer" "loginRequired": "Veuillez vous connecter pour continuer",
"rememberMe": "Se souvenir de moi",
"rememberMeHint": "Votre UUID sera stocké localement. N'activez que sur des appareils de confiance."
}, },
"favorites": { "favorites": {
"title": "Favoris", "title": "Favoris",

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Scarica backup", "downloadBackup": "Scarica backup",
"confirmSaved": "Ho salvato il mio UUID", "confirmSaved": "Ho salvato il mio UUID",
"registrationFailed": "Registrazione fallita", "registrationFailed": "Registrazione fallita",
"loginRequired": "Accedi per continuare" "loginRequired": "Accedi per continuare",
"rememberMe": "Ricordami su questo dispositivo",
"rememberMeHint": "Il tuo UUID verrà salvato localmente. Attiva solo su dispositivi affidabili."
}, },
"favorites": { "favorites": {
"title": "Preferiti", "title": "Preferiti",

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Baixar Backup", "downloadBackup": "Baixar Backup",
"confirmSaved": "Eu salvei meu UUID", "confirmSaved": "Eu salvei meu UUID",
"registrationFailed": "Falha no cadastro", "registrationFailed": "Falha no cadastro",
"loginRequired": "Por favor, faça login para continuar" "loginRequired": "Por favor, faça login para continuar",
"rememberMe": "Lembrar neste dispositivo",
"rememberMeHint": "Seu UUID será armazenado localmente. Ative apenas em dispositivos confiáveis."
}, },
"favorites": { "favorites": {
"title": "Favoritos", "title": "Favoritos",

View File

@@ -198,7 +198,9 @@
"downloadBackup": "Скачать резервную копию", "downloadBackup": "Скачать резервную копию",
"confirmSaved": "Я сохранил свой UUID", "confirmSaved": "Я сохранил свой UUID",
"registrationFailed": "Регистрация не удалась", "registrationFailed": "Регистрация не удалась",
"loginRequired": "Войдите, чтобы продолжить" "loginRequired": "Войдите, чтобы продолжить",
"rememberMe": "Запомнить на этом устройстве",
"rememberMeHint": "Ваш UUID будет сохранён локально. Включайте только на доверенных устройствах."
}, },
"favorites": { "favorites": {
"title": "Избранное", "title": "Избранное",