Release v1.2.1 — Launch-Ready: Security, E-Mail-Verifikation, Passwort-vergessen, Forum public, Mailing-System
This commit is contained in:
commit
be300bf5f4
11 changed files with 299 additions and 16 deletions
|
|
@ -87,7 +87,7 @@ def get_current_user(
|
|||
user_id = int(payload["sub"])
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number FROM users WHERE id=?",
|
||||
"SELECT id, email, name, rolle, is_premium, is_moderator, is_banned, ban_reason, is_social_media, notes_ki_enabled, breeder_status, is_founder, is_partner, founder_number, email_verified FROM users WHERE id=?",
|
||||
(user_id,)
|
||||
).fetchone()
|
||||
|
||||
|
|
|
|||
|
|
@ -565,6 +565,9 @@ def _migrate(conn_factory):
|
|||
("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"),
|
||||
("users", "founder_number", "INTEGER"),
|
||||
("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"),
|
||||
# Passwort-Zurücksetzen
|
||||
("users", "password_reset_token", "TEXT"),
|
||||
("users", "password_reset_expires", "TEXT"),
|
||||
]
|
||||
with conn_factory() as conn:
|
||||
for table, column, col_type in migrations:
|
||||
|
|
|
|||
|
|
@ -64,6 +64,19 @@ app = FastAPI(
|
|||
redoc_url = None,
|
||||
)
|
||||
|
||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=(self)"
|
||||
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
return response
|
||||
|
||||
app.add_middleware(SecurityHeadersMiddleware)
|
||||
|
||||
|
||||
# Globales File-Upload-Limit (20 MB)
|
||||
_MAX_UPLOAD_BYTES = 20 * 1024 * 1024
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import os
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, Response, Depends
|
||||
|
|
@ -77,6 +78,8 @@ async def register(data: RegisterRequest, response: Response, request: Request):
|
|||
raise HTTPException(400, "Benutzername darf keine Leerzeichen enthalten.")
|
||||
if is_username_blocked(name):
|
||||
raise HTTPException(400, "Dieser Benutzername ist nicht erlaubt.")
|
||||
if len(data.password) < 8:
|
||||
raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein.")
|
||||
|
||||
with db() as conn:
|
||||
if conn.execute("SELECT 1 FROM users WHERE email=?", (data.email,)).fetchone():
|
||||
|
|
@ -247,7 +250,8 @@ async def verify_email(token: str):
|
|||
|
||||
|
||||
@router.post("/resend-verification")
|
||||
async def resend_verification(user=Depends(get_current_user)):
|
||||
async def resend_verification(request: Request, user=Depends(get_current_user)):
|
||||
rl_check(request, max_requests=3, window_seconds=3600, key="resend_verify")
|
||||
with db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT email, name, email_verified FROM users WHERE id=?", (user["id"],)
|
||||
|
|
@ -263,3 +267,65 @@ async def resend_verification(user=Depends(get_current_user)):
|
|||
)
|
||||
_send_verification_email(row["email"], row["name"], token)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class ForgotPasswordRequest(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
class ResetPasswordRequest(BaseModel):
|
||||
token: str
|
||||
password: str
|
||||
|
||||
@router.post("/forgot-password")
|
||||
async def forgot_password(data: ForgotPasswordRequest, request: Request):
|
||||
rl_check(request, max_requests=3, window_seconds=3600, key="forgot_pw")
|
||||
with db() as conn:
|
||||
user = conn.execute(
|
||||
"SELECT id, name FROM users WHERE email=?", (data.email,)
|
||||
).fetchone()
|
||||
# Immer OK zurückgeben — kein User-Enumeration
|
||||
if user:
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires = (datetime.utcnow() + timedelta(hours=2)).isoformat()
|
||||
with db() as conn:
|
||||
conn.execute(
|
||||
"UPDATE users SET password_reset_token=?, password_reset_expires=? WHERE id=?",
|
||||
(token, expires, user["id"])
|
||||
)
|
||||
app_url = os.getenv("APP_URL", "https://banyaro.app")
|
||||
subject = "Ban Yaro — Passwort zurücksetzen"
|
||||
body = (
|
||||
f"Hallo {user['name']},\n\n"
|
||||
"du hast eine Passwort-Zurücksetzen-Anfrage gestellt.\n\n"
|
||||
f"Klicke hier um ein neues Passwort zu setzen:\n"
|
||||
f"{app_url}/#reset-password?token={token}\n\n"
|
||||
"Der Link ist 2 Stunden gültig. Falls du keine Anfrage gestellt hast, ignoriere diese Mail.\n\n"
|
||||
"Viele Grüße,\nDas Ban Yaro Team"
|
||||
)
|
||||
from routes.outreach import _send_smtp
|
||||
try:
|
||||
_send_smtp(data.email, subject, body, "support")
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
||||
if len(data.password) < 8:
|
||||
raise HTTPException(400, "Passwort muss mindestens 8 Zeichen lang sein.")
|
||||
with db() as conn:
|
||||
user = conn.execute(
|
||||
"SELECT id, password_reset_expires FROM users WHERE password_reset_token=?",
|
||||
(data.token,)
|
||||
).fetchone()
|
||||
if not user:
|
||||
raise HTTPException(400, "Ungültiger oder abgelaufener Link.")
|
||||
if user["password_reset_expires"] < datetime.utcnow().isoformat():
|
||||
raise HTTPException(400, "Dieser Link ist abgelaufen. Bitte fordere einen neuen an.")
|
||||
conn.execute(
|
||||
"UPDATE users SET pw_hash=?, password_reset_token=NULL, password_reset_expires=NULL WHERE id=?",
|
||||
(hash_password(data.password), user["id"])
|
||||
)
|
||||
return {"ok": True}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,8 @@ async def list_threads(
|
|||
# ------------------------------------------------------------------
|
||||
@router.post("/threads", status_code=201)
|
||||
async def create_thread(data: ThreadCreate, user=Depends(get_current_user)):
|
||||
if not user.get("email_verified"):
|
||||
raise HTTPException(403, "Bitte bestätige zuerst deine E-Mail-Adresse um im Forum zu schreiben.")
|
||||
if not data.titel.strip():
|
||||
raise HTTPException(400, "Titel darf nicht leer sein.")
|
||||
if not data.text.strip():
|
||||
|
|
@ -304,6 +306,8 @@ async def patch_thread(thread_id: int, data: ThreadPatch, user=Depends(get_curre
|
|||
# ------------------------------------------------------------------
|
||||
@router.post("/threads/{thread_id}/posts", status_code=201)
|
||||
async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current_user)):
|
||||
if not user.get("email_verified"):
|
||||
raise HTTPException(403, "Bitte bestätige zuerst deine E-Mail-Adresse um im Forum zu schreiben.")
|
||||
if not data.text.strip():
|
||||
raise HTTPException(400, "Text darf nicht leer sein.")
|
||||
with db() as conn:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""BAN YARO — Mailing (Admin)"""
|
||||
|
||||
import imaplib
|
||||
import os
|
||||
import smtplib
|
||||
import ssl
|
||||
|
|
@ -9,16 +10,21 @@ from email.utils import formataddr
|
|||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from auth import require_admin
|
||||
from database import db
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter()
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
_SMTP_HOST = os.getenv("SMTP_HOST", "mail.your-server.de")
|
||||
_SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
|
||||
_IMAP_HOST = os.getenv("IMAP_HOST", "mail.your-server.de")
|
||||
_IMAP_PORT = int(os.getenv("IMAP_PORT", "993"))
|
||||
|
||||
_ACCOUNTS = {
|
||||
"partner": {
|
||||
|
|
@ -35,23 +41,73 @@ _ACCOUNTS = {
|
|||
},
|
||||
}
|
||||
|
||||
# Mögliche Namen für den Sent-Ordner (Hetzner/Dovecot)
|
||||
_SENT_CANDIDATES = ["Sent", "Sent Messages", "Sent Items", "INBOX.Sent", "Gesendete Objekte"]
|
||||
|
||||
|
||||
def _imap_save_sent(msg_bytes: bytes, account: str):
|
||||
acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
|
||||
if not acc["user"] or not acc["pass"]:
|
||||
_log.warning("IMAP: Account '%s' nicht konfiguriert, überspringe.", account)
|
||||
return
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
with imaplib.IMAP4_SSL(_IMAP_HOST, _IMAP_PORT, ssl_context=ctx) as imap:
|
||||
imap.login(acc["user"], acc["pass"])
|
||||
_, raw_folders = imap.list()
|
||||
available = [f.decode(errors="replace") for f in (raw_folders or [])]
|
||||
_log.info("IMAP Ordner (%s): %s", account, available)
|
||||
|
||||
# Echten Ordnernamen aus LIST-Antwort extrahieren
|
||||
# Format: '(\Flags) "." INBOX.Sent' → letztes Token
|
||||
folder = None
|
||||
for line in available:
|
||||
name = line.rsplit('"." ', 1)[-1].strip().strip('"')
|
||||
for candidate in _SENT_CANDIDATES:
|
||||
if candidate.lower() in name.lower():
|
||||
folder = name
|
||||
break
|
||||
if folder:
|
||||
break
|
||||
if not folder:
|
||||
folder = "INBOX.Sent"
|
||||
_log.info("IMAP: speichere in Ordner '%s' (%s)", folder, account)
|
||||
|
||||
typ, data = imap.append(
|
||||
folder,
|
||||
r"\Seen",
|
||||
imaplib.Time2Internaldate(datetime.now().timestamp()),
|
||||
msg_bytes,
|
||||
)
|
||||
_log.info("IMAP append: %s %s", typ, data)
|
||||
except Exception as e:
|
||||
_log.error("IMAP Sent-Speicherung fehlgeschlagen (%s): %s", account, e)
|
||||
|
||||
|
||||
def _build_message(to: str, subject: str, body: str, account: str) -> MIMEMultipart:
|
||||
acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = formataddr((acc["name"], acc["from"]))
|
||||
msg["To"] = to
|
||||
msg["Reply-To"] = acc["from"]
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
return msg
|
||||
|
||||
|
||||
def _send_smtp(to: str, subject: str, body: str, account: str = "partner"):
|
||||
acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
|
||||
if not acc["user"] or not acc["pass"]:
|
||||
raise RuntimeError(f"SMTP-Account '{account}' nicht konfiguriert.")
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = formataddr((acc["name"], acc["from"]))
|
||||
msg["To"] = to
|
||||
msg["Reply-To"] = acc["from"]
|
||||
msg.attach(MIMEText(body, "plain", "utf-8"))
|
||||
msg = _build_message(to, subject, body, account)
|
||||
msg_bytes = msg.as_bytes()
|
||||
ctx = ssl.create_default_context()
|
||||
with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s:
|
||||
s.ehlo()
|
||||
s.starttls(context=ctx)
|
||||
s.login(acc["user"], acc["pass"])
|
||||
s.sendmail(acc["from"], [to], msg.as_bytes())
|
||||
s.sendmail(acc["from"], [to], msg_bytes)
|
||||
_imap_save_sent(msg_bytes, account)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -189,4 +189,5 @@
|
|||
<symbol id="certificate" viewBox="0 0 256 256">
|
||||
<path d="M232,86.53V56a16,16,0,0,0-16-16H40A16,16,0,0,0,24,56V184a16,16,0,0,0,16,16H160v24A8,8,0,0,0,172,231l24-13.74L220,231A8,8,0,0,0,232,224V161.47a51.88,51.88,0,0,0,0-74.94ZM128,144H72a8,8,0,0,1,0-16h56a8,8,0,0,1,0,16Zm0-32H72a8,8,0,0,1,0-16h56a8,8,0,0,1,0,16Zm88,98.21-16-9.16a8,8,0,0,0-7.94,0l-16,9.16V172a51.88,51.88,0,0,0,40,0ZM196,160a36,36,0,1,1,36-36A36,36,0,0,1,196,160Z"/>
|
||||
</symbol>
|
||||
<symbol id="envelope-simple" viewBox="0 0 256 256"><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48Zm-8,144H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></symbol></svg>
|
||||
<symbol id="envelope-simple" viewBox="0 0 256 256"><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48Zm-8,144H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></symbol>
|
||||
<symbol id="arrow-bend-up-left" viewBox="0 0 256 256"><path d="M232,200a8,8,0,0,1-16,0,88.1,88.1,0,0,0-88-88H88v40a8,8,0,0,1-13.66,5.66l-48-48a8,8,0,0,1,0-11.32l48-48A8,8,0,0,1,88,56V96h40A104.11,104.11,0,0,1,232,200Z"/></symbol></svg>
|
||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
|
@ -3,8 +3,8 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '552'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.1.4'; // ← semantische Version, wird bei make release gesetzt
|
||||
const APP_VER = '554'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
|
||||
const 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-v577';
|
||||
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
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ services:
|
|||
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
||||
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
||||
- VAPID_CONTACT=mailto:admin@banyaro.app
|
||||
- SMTP_SUPPORT_USER=support@banyaro.de
|
||||
- SMTP_SUPPORT_PASS=Marbled-Drool8-Whacky
|
||||
healthcheck:
|
||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]
|
||||
interval: 30s
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue