Security: Passwort-Minimum, Rate Limits, Headers, Passwort-vergessen, email_verified
- Passwort-Minimum 8 Zeichen bei Register + Reset - Rate Limit auf /resend-verification (3/h) und /forgot-password (3/h) - Security-Headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy etc. - email_verified in get_current_user SELECT ergänzt - Forum: create_thread + create_post erfordern email_verified - POST /auth/forgot-password + /auth/reset-password (2h-Token, via support@) - DB-Migration: password_reset_token + password_reset_expires - Frontend: Passwort-vergessen-Modal im Login, Reset-Formular mit Passphrase-Generator - SW by-v576, APP_VER 553
This commit is contained in:
parent
82d6417d09
commit
526ff42215
8 changed files with 232 additions and 4 deletions
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '552'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '553'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.1.4'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
|
||||
|
|
@ -824,6 +824,14 @@ const App = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// Passwort-Reset: #reset-password?token=xxx
|
||||
if (hashPage === 'reset-password' && hashParams.token) {
|
||||
sessionStorage.setItem('by_reset_token', hashParams.token);
|
||||
history.replaceState(null, '', '/');
|
||||
navigate('settings', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// E-Mail-Verifikation: Redirect von /api/auth/verify-email/{token}
|
||||
if (hashParams.verified === '1' || hashParams.verified === 1) {
|
||||
if (state.user) state.user.email_verified = 1;
|
||||
|
|
|
|||
|
|
@ -1239,6 +1239,14 @@ window.Page_settings = (() => {
|
|||
// NICHT EINGELOGGT — Login / Registrierung
|
||||
// ----------------------------------------------------------
|
||||
function _renderAuth(mode) {
|
||||
// Passwort-Reset über Link aus E-Mail
|
||||
const resetToken = sessionStorage.getItem('by_reset_token');
|
||||
if (resetToken) {
|
||||
sessionStorage.removeItem('by_reset_token');
|
||||
_renderResetPassword(resetToken);
|
||||
return;
|
||||
}
|
||||
|
||||
_mode = mode;
|
||||
_container.innerHTML = `
|
||||
<div style="max-width:380px;margin:0 auto;padding:var(--space-6) 0">
|
||||
|
|
@ -1313,6 +1321,13 @@ window.Page_settings = (() => {
|
|||
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2)">
|
||||
Anmelden
|
||||
</button>
|
||||
<p style="text-align:center;margin-top:var(--space-3);font-size:var(--text-xs)">
|
||||
<button type="button" id="forgot-pw-link"
|
||||
class="btn btn-ghost"
|
||||
style="font-size:var(--text-xs);color:var(--c-text-muted);padding:0">
|
||||
Passwort vergessen?
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
|
@ -1414,6 +1429,38 @@ window.Page_settings = (() => {
|
|||
|
||||
function _bindLoginForm() {
|
||||
_bindPwToggle('login-pw', 'login-pw-toggle');
|
||||
|
||||
document.getElementById('forgot-pw-link')?.addEventListener('click', () => {
|
||||
const id = 'forgot-pw-modal';
|
||||
UI.modal.open({
|
||||
title: 'Passwort zurücksetzen',
|
||||
body: `
|
||||
<form id="${id}" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0">
|
||||
Gib deine E-Mail-Adresse ein. Du erhältst einen Link zum Zurücksetzen deines Passworts.
|
||||
</p>
|
||||
<div>
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input class="form-control" id="forgot-pw-email" type="email"
|
||||
placeholder="deine@email.de" autocomplete="email" required>
|
||||
</div>
|
||||
</form>`,
|
||||
footer: `
|
||||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||||
<button class="btn btn-primary" form="${id}" type="submit">Link senden</button>`,
|
||||
});
|
||||
document.getElementById(id)?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = document.querySelector(`[form="${id}"]`);
|
||||
const email = document.getElementById('forgot-pw-email').value.trim();
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.post('/auth/forgot-password', { email });
|
||||
UI.modal.close();
|
||||
UI.toast.success('Falls ein Account existiert, haben wir dir einen Link geschickt.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('auth-form')?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('[type="submit"]');
|
||||
|
|
@ -1610,6 +1657,93 @@ window.Page_settings = (() => {
|
|||
setTimeout(remove, 12000);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PASSWORT ZURÜCKSETZEN
|
||||
// ----------------------------------------------------------
|
||||
function _renderResetPassword(token) {
|
||||
_container.innerHTML = `
|
||||
<div style="max-width:380px;margin:0 auto;padding:var(--space-6) 0">
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<img src="/icons/icon-180.png" alt="Ban Yaro"
|
||||
style="width:72px;height:72px;border-radius:var(--radius-lg);margin-bottom:var(--space-3)">
|
||||
<h1 style="font-size:var(--text-2xl);font-weight:700;margin:0">Neues Passwort</h1>
|
||||
<p style="color:var(--c-text-secondary);margin:var(--space-1) 0 0">
|
||||
Wähle ein sicheres Passwort für deinen Account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="reset-pw-form" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
<div>
|
||||
<label class="form-label">Neues Passwort</label>
|
||||
<div style="position:relative">
|
||||
<input class="form-control" type="password" id="reset-pw-input"
|
||||
placeholder="Mindestens 8 Zeichen" autocomplete="new-password"
|
||||
minlength="8" required style="padding-right:var(--space-10)">
|
||||
<button type="button" id="reset-pw-toggle"
|
||||
class="btn btn-ghost btn-icon"
|
||||
style="position:absolute;right:var(--space-1);top:50%;transform:translateY(-50%);
|
||||
color:var(--c-text-muted);padding:var(--space-2)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#eye"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Hundepassphrase-Generator -->
|
||||
<div style="margin-top:var(--space-2);padding:var(--space-3);
|
||||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||||
border-left:3px solid var(--c-primary)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)">
|
||||
<span style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary);
|
||||
text-transform:uppercase;letter-spacing:.05em">🐾 Passwort-Vorschlag</span>
|
||||
<button type="button" id="reset-gen-new"
|
||||
style="margin-left:auto;font-size:var(--text-xs);color:var(--c-primary);
|
||||
background:none;border:none;cursor:pointer;padding:0;font-weight:600">
|
||||
↺ neu
|
||||
</button>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||
<code id="reset-gen-phrase"
|
||||
style="flex:1;font-size:var(--text-sm);font-weight:700;
|
||||
color:var(--c-text);letter-spacing:.02em;word-break:break-all"></code>
|
||||
<button type="button" id="reset-gen-use"
|
||||
class="btn btn-sm btn-secondary" style="flex-shrink:0">
|
||||
Übernehmen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full">
|
||||
Passwort speichern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
_bindPwToggle('reset-pw-input', 'reset-pw-toggle');
|
||||
|
||||
const phraseEl = document.getElementById('reset-gen-phrase');
|
||||
const pwInput = document.getElementById('reset-pw-input');
|
||||
const _refresh = () => { phraseEl.textContent = _genPassphrase(); };
|
||||
_refresh();
|
||||
document.getElementById('reset-gen-new')?.addEventListener('click', _refresh);
|
||||
document.getElementById('reset-gen-use')?.addEventListener('click', () => {
|
||||
pwInput.value = phraseEl.textContent;
|
||||
pwInput.type = 'text';
|
||||
});
|
||||
|
||||
document.getElementById('reset-pw-form')?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = e.target.querySelector('[type="submit"]');
|
||||
const password = document.getElementById('reset-pw-input').value;
|
||||
await UI.asyncButton(btn, async () => {
|
||||
const res = await API.post('/auth/reset-password', { token, password });
|
||||
if (res?.ok) {
|
||||
UI.toast.success('Passwort geändert! Du kannst dich jetzt anmelden.');
|
||||
_renderAuth('login');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HELPER
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v575';
|
||||
const CACHE_VERSION = 'by-v576';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue