feat(auth): "Angemeldet bleiben"-Checkbox auf Login-Screen
Wenn aktiviert, bekommt der JWT-Token statt der üblichen 7 Tage eine Lebensdauer von 180 Tagen. Der Token liegt wie bisher in localStorage, bleibt also bis zum manuellen Löschen / Cookie-Reset gültig. - backend/routers/auth_router.py: LoginRequest.remember_me, längere expires_delta beim Token-Erstellen - index.html: Checkbox unter dem 2FA-Feld - api.js: login() reicht remember_me als 4. Parameter durch - app.js: Wert aus #login-remember lesen und mitschicken - Version v5 → v6
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
import pyotp
|
||||
@@ -11,6 +12,9 @@ import models
|
||||
from auth import create_access_token, get_current_user, get_password_hash, verify_password
|
||||
from database import get_db
|
||||
|
||||
# When "Angemeldet bleiben" is ticked the token lives for half a year.
|
||||
REMEMBER_ME_EXPIRY = timedelta(days=180)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -24,6 +28,7 @@ class LoginRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
totp_code: Optional[str] = None
|
||||
remember_me: Optional[bool] = False
|
||||
|
||||
|
||||
def _user_dict(user: models.User) -> dict:
|
||||
@@ -98,7 +103,8 @@ def login_json(req: LoginRequest, db: Session = Depends(get_db)):
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Ungültiger 2FA-Code",
|
||||
)
|
||||
token = create_access_token({"sub": user.username})
|
||||
expires = REMEMBER_ME_EXPIRY if req.remember_me else None
|
||||
token = create_access_token({"sub": user.username}, expires_delta=expires)
|
||||
return {"access_token": token, "token_type": "bearer", "user": _user_dict(user)}
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<!-- APP_VERSION: update here + version.js on every release -->
|
||||
<title>Calendarr v5</title>
|
||||
<title>Calendarr v6</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="#4285f4" />
|
||||
@@ -73,11 +73,14 @@
|
||||
<label>2FA-Code</label>
|
||||
<input type="text" id="login-totp" placeholder="6-stelliger Code" maxlength="6" inputmode="numeric" autocomplete="one-time-code" />
|
||||
</div>
|
||||
<label class="toggle-label" style="margin-bottom:14px">
|
||||
<input type="checkbox" id="login-remember" /> Angemeldet bleiben
|
||||
</label>
|
||||
<div id="login-error" class="form-error hidden"></div>
|
||||
<button type="submit" class="btn btn-primary btn-full">Anmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v5</button>
|
||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v6</button>
|
||||
</div>
|
||||
|
||||
<!-- ─── MAIN APP ──────────────────────────────────────────── -->
|
||||
@@ -196,7 +199,7 @@
|
||||
<div id="cal-list-items"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v5</button>
|
||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v6</button>
|
||||
</aside>
|
||||
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
||||
|
||||
@@ -232,7 +235,7 @@
|
||||
<input type="hidden" id="ev-start" />
|
||||
<div class="dt-display" id="ev-start-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v5H7z"/></svg>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v6H7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group half">
|
||||
@@ -240,7 +243,7 @@
|
||||
<input type="hidden" id="ev-end" />
|
||||
<div class="dt-display" id="ev-end-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v5H7z"/></svg>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v6H7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +253,7 @@
|
||||
<input type="hidden" id="ev-start-date" />
|
||||
<div class="dt-display" id="ev-start-date-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v5H7z"/></svg>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v6H7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group half">
|
||||
@@ -258,7 +261,7 @@
|
||||
<input type="hidden" id="ev-end-date" />
|
||||
<div class="dt-display" id="ev-end-date-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v5H7z"/></svg>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v6H7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -308,7 +311,7 @@
|
||||
<input type="hidden" id="ev-rec-until" />
|
||||
<div class="dt-display" id="ev-rec-until-display" tabindex="0" role="button">
|
||||
<span class="dt-display-text">—</span>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v5H7z"/></svg>
|
||||
<svg class="dt-display-icon" viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v24a2 2 0 002 2h14a2 2 0 002-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v21zM7 10h5v6H7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -862,7 +865,7 @@
|
||||
<a href="mailto:scarriffleservices@gmail.com">scarriffleservices@gmail.com</a></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="justify-content:space-between;align-items:center">
|
||||
<span style="font-size:12px;color:var(--text-3)">Calendarr v5</span>
|
||||
<span style="font-size:12px;color:var(--text-3)">Calendarr v6</span>
|
||||
<button class="btn btn-ghost" onclick="closeImpressum()">Schliessen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,8 +64,8 @@ export const api = {
|
||||
delete: (path) => request('DELETE', path),
|
||||
upload: (path, form) => uploadRequest(path, form),
|
||||
|
||||
login: (username, password, totp_code = null) =>
|
||||
request('POST', '/auth/login', { username, password, totp_code }),
|
||||
login: (username, password, totp_code = null, remember_me = false) =>
|
||||
request('POST', '/auth/login', { username, password, totp_code, remember_me }),
|
||||
|
||||
setupRequired: () => request('GET', '/auth/setup-required'),
|
||||
setup: (data) => request('POST', '/auth/setup', data),
|
||||
|
||||
@@ -141,11 +141,12 @@ function bindLoginForm() {
|
||||
const username = document.getElementById('login-username').value.trim();
|
||||
const password = document.getElementById('login-password').value;
|
||||
const totpCode = document.getElementById('login-totp')?.value.trim() || null;
|
||||
const remember = document.getElementById('login-remember')?.checked || false;
|
||||
const errEl = document.getElementById('login-error');
|
||||
errEl.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const res = await api.login(username, password, totpCode);
|
||||
const res = await api.login(username, password, totpCode, remember);
|
||||
localStorage.setItem('token', res.access_token);
|
||||
localStorage.setItem('user', JSON.stringify(res.user));
|
||||
await launchApp();
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Increment APP_VERSION with every code change
|
||||
export const APP_VERSION = 'v5';
|
||||
export const APP_VERSION = 'v6';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Calendarr Service Worker
|
||||
// Cache-first for static assets, network-first for /api/* (graceful offline)
|
||||
|
||||
const CACHE_VERSION = 'calendarr-v5';
|
||||
const CACHE_VERSION = 'calendarr-v6';
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
|
||||
Reference in New Issue
Block a user