Einige kleine verbesserungen #1
@@ -1,3 +1,4 @@
|
|||||||
|
from datetime import timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pyotp
|
import pyotp
|
||||||
@@ -11,6 +12,9 @@ import models
|
|||||||
from auth import create_access_token, get_current_user, get_password_hash, verify_password
|
from auth import create_access_token, get_current_user, get_password_hash, verify_password
|
||||||
from database import get_db
|
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()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +28,7 @@ class LoginRequest(BaseModel):
|
|||||||
username: str
|
username: str
|
||||||
password: str
|
password: str
|
||||||
totp_code: Optional[str] = None
|
totp_code: Optional[str] = None
|
||||||
|
remember_me: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
def _user_dict(user: models.User) -> dict:
|
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,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Ungültiger 2FA-Code",
|
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)}
|
return {"access_token": token, "token_type": "bearer", "user": _user_dict(user)}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<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" />
|
<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 -->
|
<!-- 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="icon" type="image/svg+xml" href="/static/favicon.svg" />
|
||||||
<link rel="manifest" href="/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<meta name="theme-color" content="#4285f4" />
|
<meta name="theme-color" content="#4285f4" />
|
||||||
@@ -73,11 +73,14 @@
|
|||||||
<label>2FA-Code</label>
|
<label>2FA-Code</label>
|
||||||
<input type="text" id="login-totp" placeholder="6-stelliger Code" maxlength="6" inputmode="numeric" autocomplete="one-time-code" />
|
<input type="text" id="login-totp" placeholder="6-stelliger Code" maxlength="6" inputmode="numeric" autocomplete="one-time-code" />
|
||||||
</div>
|
</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>
|
<div id="login-error" class="form-error hidden"></div>
|
||||||
<button type="submit" class="btn btn-primary btn-full">Anmelden</button>
|
<button type="submit" class="btn btn-primary btn-full">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v5</button>
|
<button class="impressum-link" onclick="openImpressum()">© 2026 Scarriffleservices · v6</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ─── MAIN APP ──────────────────────────────────────────── -->
|
<!-- ─── MAIN APP ──────────────────────────────────────────── -->
|
||||||
@@ -196,7 +199,7 @@
|
|||||||
<div id="cal-list-items"></div>
|
<div id="cal-list-items"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v5</button>
|
<button class="sidebar-copyright" onclick="openImpressum()">© 2026 Scarriffleservices · v6</button>
|
||||||
</aside>
|
</aside>
|
||||||
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
|
||||||
|
|
||||||
@@ -232,7 +235,7 @@
|
|||||||
<input type="hidden" id="ev-start" />
|
<input type="hidden" id="ev-start" />
|
||||||
<div class="dt-display" id="ev-start-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-start-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<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>
|
</div>
|
||||||
<div class="form-group half">
|
<div class="form-group half">
|
||||||
@@ -240,7 +243,7 @@
|
|||||||
<input type="hidden" id="ev-end" />
|
<input type="hidden" id="ev-end" />
|
||||||
<div class="dt-display" id="ev-end-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-end-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,7 +253,7 @@
|
|||||||
<input type="hidden" id="ev-start-date" />
|
<input type="hidden" id="ev-start-date" />
|
||||||
<div class="dt-display" id="ev-start-date-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-start-date-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<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>
|
</div>
|
||||||
<div class="form-group half">
|
<div class="form-group half">
|
||||||
@@ -258,7 +261,7 @@
|
|||||||
<input type="hidden" id="ev-end-date" />
|
<input type="hidden" id="ev-end-date" />
|
||||||
<div class="dt-display" id="ev-end-date-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-end-date-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,7 +311,7 @@
|
|||||||
<input type="hidden" id="ev-rec-until" />
|
<input type="hidden" id="ev-rec-until" />
|
||||||
<div class="dt-display" id="ev-rec-until-display" tabindex="0" role="button">
|
<div class="dt-display" id="ev-rec-until-display" tabindex="0" role="button">
|
||||||
<span class="dt-display-text">—</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -862,7 +865,7 @@
|
|||||||
<a href="mailto:scarriffleservices@gmail.com">scarriffleservices@gmail.com</a></p>
|
<a href="mailto:scarriffleservices@gmail.com">scarriffleservices@gmail.com</a></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer" style="justify-content:space-between;align-items:center">
|
<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>
|
<button class="btn btn-ghost" onclick="closeImpressum()">Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export const api = {
|
|||||||
delete: (path) => request('DELETE', path),
|
delete: (path) => request('DELETE', path),
|
||||||
upload: (path, form) => uploadRequest(path, form),
|
upload: (path, form) => uploadRequest(path, form),
|
||||||
|
|
||||||
login: (username, password, totp_code = null) =>
|
login: (username, password, totp_code = null, remember_me = false) =>
|
||||||
request('POST', '/auth/login', { username, password, totp_code }),
|
request('POST', '/auth/login', { username, password, totp_code, remember_me }),
|
||||||
|
|
||||||
setupRequired: () => request('GET', '/auth/setup-required'),
|
setupRequired: () => request('GET', '/auth/setup-required'),
|
||||||
setup: (data) => request('POST', '/auth/setup', data),
|
setup: (data) => request('POST', '/auth/setup', data),
|
||||||
|
|||||||
@@ -141,11 +141,12 @@ function bindLoginForm() {
|
|||||||
const username = document.getElementById('login-username').value.trim();
|
const username = document.getElementById('login-username').value.trim();
|
||||||
const password = document.getElementById('login-password').value;
|
const password = document.getElementById('login-password').value;
|
||||||
const totpCode = document.getElementById('login-totp')?.value.trim() || null;
|
const totpCode = document.getElementById('login-totp')?.value.trim() || null;
|
||||||
|
const remember = document.getElementById('login-remember')?.checked || false;
|
||||||
const errEl = document.getElementById('login-error');
|
const errEl = document.getElementById('login-error');
|
||||||
errEl.classList.add('hidden');
|
errEl.classList.add('hidden');
|
||||||
|
|
||||||
try {
|
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('token', res.access_token);
|
||||||
localStorage.setItem('user', JSON.stringify(res.user));
|
localStorage.setItem('user', JSON.stringify(res.user));
|
||||||
await launchApp();
|
await launchApp();
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// Increment APP_VERSION with every code change
|
// Increment APP_VERSION with every code change
|
||||||
export const APP_VERSION = 'v5';
|
export const APP_VERSION = 'v6';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Calendarr Service Worker
|
// Calendarr Service Worker
|
||||||
// Cache-first for static assets, network-first for /api/* (graceful offline)
|
// Cache-first for static assets, network-first for /api/* (graceful offline)
|
||||||
|
|
||||||
const CACHE_VERSION = 'calendarr-v5';
|
const CACHE_VERSION = 'calendarr-v6';
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|||||||
Reference in New Issue
Block a user