mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
feat(auth): admin-configurable session lifetime ceiling (#1706)
The 24h session cap from the M-2 audit finding was hard-coded, so the
"Remember Me" checkbox could only control storage location, never
duration. Add session_max_hours setting (default 24, max 720) honoured
at all four token-issuance sites: plain login, 2FA TOTP/email, 2FA
backup, OIDC.
- backend/app/core/auth.py: SESSION_MAX_HOURS_HARD_CEILING + resolver
that clamps to [1h, 720h] and falls back to 24h on missing/blank/
unparseable. DB errors propagate — the login transaction must abort
on a broken DB rather than silently extend or shrink the lifetime.
- backend/app/api/routes/auth.py, mfa.py: all four sites read the
resolved value instead of ACCESS_TOKEN_EXPIRE_MINUTES directly.
- backend/app/schemas/settings.py, routes/settings.py: schema field
with ge=1 le=720 + int coercion in _build_settings_response.
- frontend/src/pages/SettingsPage.tsx: half-width card at top of
Settings -> Users left column with 24h/7d/30d presets, custom input,
and a yellow warning when value > 24h.
- frontend/src/i18n/locales/*.ts: 8 new keys per locale, real
translations in all 11 (en/de/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW).
- backend/tests/integration/test_session_policy.py: 15 tests across
resolver clamping, login JWT exp end-to-end, settings API round-trip.
Already-issued tokens keep their original expiry; the new setting only
affects future logins.
This commit is contained in:
@@ -1155,6 +1155,8 @@ export interface AppSettings {
|
||||
bed_cooled_threshold: number;
|
||||
// Inventory low stock threshold
|
||||
low_stock_threshold: number;
|
||||
// Session policy (#1706) — admin-set ceiling, hours, [1, 720]
|
||||
session_max_hours: number;
|
||||
// User email notifications toggle
|
||||
user_notifications_enabled: boolean;
|
||||
// Default print options
|
||||
|
||||
@@ -2381,6 +2381,17 @@ export default {
|
||||
linkedAccountsDesc: 'Diese externen Identitätsanbieter sind mit deinem Konto verknüpft.',
|
||||
oidcUnlinked: 'Konto getrennt.',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'Sitzungsrichtlinie',
|
||||
description: 'Maximale Sitzungsdauer für neue Benutzeranmeldungen. Bereits ausgegebene Token behalten ihren ursprünglichen Ablauf.',
|
||||
preset24h: '24 Stunden',
|
||||
preset7d: '7 Tage',
|
||||
preset30d: '30 Tage',
|
||||
customHoursLabel: 'Individuelle Sitzungsdauer in Stunden',
|
||||
hoursSuffix: 'Stunden',
|
||||
warning: 'Längere Sitzungen reduzieren den automatischen Abmeldeschutz. Nur für vertrauenswürdige Einzelnutzer-Installationen empfohlen.',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2392,6 +2392,18 @@ export default {
|
||||
oidcUnlinked: 'Account unlinked.',
|
||||
},
|
||||
|
||||
// Session Policy (#1706) — admin-configurable session lifetime ceiling.
|
||||
sessionPolicy: {
|
||||
title: 'Session Policy',
|
||||
description: 'Maximum session lifetime for new user logins. Already-issued tokens keep their original expiry.',
|
||||
preset24h: '24 hours',
|
||||
preset7d: '7 days',
|
||||
preset30d: '30 days',
|
||||
customHoursLabel: 'Custom session lifetime in hours',
|
||||
hoursSuffix: 'hours',
|
||||
warning: 'Longer sessions reduce automatic logout protection. Recommended only for trusted single-user deployments.',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
title: 'SSO / OIDC Providers',
|
||||
|
||||
@@ -2384,6 +2384,17 @@ export default {
|
||||
linkedAccountsDesc: 'Estos proveedores de identidad externos están vinculados a su cuenta.',
|
||||
oidcUnlinked: 'Cuenta desvinculada.',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'Política de sesión',
|
||||
description: 'Duración máxima de sesión para nuevos inicios de sesión. Los tokens ya emitidos conservan su caducidad original.',
|
||||
preset24h: '24 horas',
|
||||
preset7d: '7 días',
|
||||
preset30d: '30 días',
|
||||
customHoursLabel: 'Duración de sesión personalizada en horas',
|
||||
hoursSuffix: 'horas',
|
||||
warning: 'Las sesiones más largas reducen la protección de cierre automático. Recomendado solo para implementaciones de usuario único en entornos de confianza.',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2324,6 +2324,17 @@ export default {
|
||||
linkedAccountsDesc: 'Ces fournisseurs d\'identité externes sont liés à votre compte.',
|
||||
oidcUnlinked: 'Compte dissocié.',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'Politique de session',
|
||||
description: 'Durée maximale des sessions pour les nouvelles connexions utilisateur. Les jetons déjà émis conservent leur expiration d\'origine.',
|
||||
preset24h: '24 heures',
|
||||
preset7d: '7 jours',
|
||||
preset30d: '30 jours',
|
||||
customHoursLabel: 'Durée de session personnalisée en heures',
|
||||
hoursSuffix: 'heures',
|
||||
warning: 'Les sessions plus longues réduisent la protection de déconnexion automatique. Recommandé uniquement pour les déploiements mono-utilisateur de confiance.',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2323,6 +2323,17 @@ export default {
|
||||
linkedAccountsDesc: 'Questi provider di identità esterni sono collegati al tuo account.',
|
||||
oidcUnlinked: 'Account scollegato.',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'Criterio di sessione',
|
||||
description: 'Durata massima della sessione per i nuovi accessi utente. I token già emessi mantengono la scadenza originale.',
|
||||
preset24h: '24 ore',
|
||||
preset7d: '7 giorni',
|
||||
preset30d: '30 giorni',
|
||||
customHoursLabel: 'Durata personalizzata della sessione in ore',
|
||||
hoursSuffix: 'ore',
|
||||
warning: 'Le sessioni più lunghe riducono la protezione di disconnessione automatica. Consigliato solo per installazioni mono-utente attendibili.',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2380,6 +2380,17 @@ export default {
|
||||
linkedAccountsDesc: 'これらの外部IDプロバイダーがあなたのアカウントにリンクされています。',
|
||||
oidcUnlinked: 'アカウントのリンクを解除しました。',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'セッションポリシー',
|
||||
description: '新しいユーザーログインの最大セッション有効期間。すでに発行されたトークンは元の有効期限を保持します。',
|
||||
preset24h: '24時間',
|
||||
preset7d: '7日',
|
||||
preset30d: '30日',
|
||||
customHoursLabel: 'カスタムセッション有効期間(時間)',
|
||||
hoursSuffix: '時間',
|
||||
warning: '長いセッションは自動ログアウト保護を弱めます。信頼できる単一ユーザー環境でのみ推奨されます。',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2238,6 +2238,17 @@ export default {
|
||||
linkedAccountsDesc: '이 외부 ID 제공자가 계정에 연결되어 있습니다.',
|
||||
oidcUnlinked: '계정 연결이 해제되었습니다.'
|
||||
},
|
||||
// 세션 정책 (#1706)
|
||||
sessionPolicy: {
|
||||
title: '세션 정책',
|
||||
description: '신규 사용자 로그인의 최대 세션 수명입니다. 이미 발급된 토큰은 원래 만료 시간을 유지합니다.',
|
||||
preset24h: '24시간',
|
||||
preset7d: '7일',
|
||||
preset30d: '30일',
|
||||
customHoursLabel: '사용자 지정 세션 수명(시간)',
|
||||
hoursSuffix: '시간',
|
||||
warning: '세션이 길어질수록 자동 로그아웃 보호가 약해집니다. 신뢰할 수 있는 단일 사용자 배포에서만 권장됩니다.'
|
||||
},
|
||||
oidc: {
|
||||
title: 'SSO / OIDC 제공자',
|
||||
desc: '외부 ID 제공자를 통해 싱글 사인온을 허용하도록 OpenID Connect 제공자를 설정하세요.',
|
||||
|
||||
@@ -2323,6 +2323,17 @@ export default {
|
||||
linkedAccountsDesc: 'Estes provedores de identidade externos estão vinculados à sua conta.',
|
||||
oidcUnlinked: 'Conta desvinculada.',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'Política de sessão',
|
||||
description: 'Duração máxima da sessão para novos logins de usuário. Tokens já emitidos mantêm sua expiração original.',
|
||||
preset24h: '24 horas',
|
||||
preset7d: '7 dias',
|
||||
preset30d: '30 dias',
|
||||
customHoursLabel: 'Duração personalizada da sessão em horas',
|
||||
hoursSuffix: 'horas',
|
||||
warning: 'Sessões mais longas reduzem a proteção de logout automático. Recomendado apenas para implantações de usuário único confiáveis.',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2384,6 +2384,17 @@ export default {
|
||||
linkedAccountsDesc: 'Bu harici kimlik sağlayıcıları hesabınıza bağlıdır.',
|
||||
oidcUnlinked: 'Hesap bağlantısı kaldırıldı.',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: 'Oturum Politikası',
|
||||
description: 'Yeni kullanıcı girişleri için maksimum oturum süresi. Daha önce verilmiş belirteçler özgün son kullanma tarihlerini korur.',
|
||||
preset24h: '24 saat',
|
||||
preset7d: '7 gün',
|
||||
preset30d: '30 gün',
|
||||
customHoursLabel: 'Özel oturum süresi (saat)',
|
||||
hoursSuffix: 'saat',
|
||||
warning: 'Daha uzun oturumlar otomatik oturum kapatma korumasını azaltır. Yalnızca güvenilir tek kullanıcılı dağıtımlar için önerilir.',
|
||||
},
|
||||
|
||||
// OIDC sağlayıcı ayarları
|
||||
oidc: {
|
||||
|
||||
@@ -2368,6 +2368,17 @@ export default {
|
||||
linkedAccountsDesc: '以下外部身份提供商已与您的账户关联。',
|
||||
oidcUnlinked: '账户已解除关联。',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: '会话策略',
|
||||
description: '新用户登录的最长会话有效期。已颁发的令牌保留其原有的过期时间。',
|
||||
preset24h: '24 小时',
|
||||
preset7d: '7 天',
|
||||
preset30d: '30 天',
|
||||
customHoursLabel: '自定义会话有效期(小时)',
|
||||
hoursSuffix: '小时',
|
||||
warning: '更长的会话会减弱自动注销保护。仅建议在受信任的单用户部署中使用。',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -2368,6 +2368,17 @@ export default {
|
||||
linkedAccountsDesc: '以下外部身份提供者已與您的帳戶連結。',
|
||||
oidcUnlinked: '帳戶已解除連結。',
|
||||
},
|
||||
// Session Policy (#1706)
|
||||
sessionPolicy: {
|
||||
title: '工作階段政策',
|
||||
description: '新使用者登入的最長工作階段有效期。已發行的權杖會保留其原有的到期時間。',
|
||||
preset24h: '24 小時',
|
||||
preset7d: '7 天',
|
||||
preset30d: '30 天',
|
||||
customHoursLabel: '自訂工作階段有效期(小時)',
|
||||
hoursSuffix: '小時',
|
||||
warning: '較長的工作階段會削弱自動登出保護。僅建議在受信任的單一使用者部署中使用。',
|
||||
},
|
||||
|
||||
// OIDC provider settings
|
||||
oidc: {
|
||||
|
||||
@@ -87,6 +87,7 @@ registerSettingsSearch({ labelKey: 'settings.tabs.spoolbuddy', tab: 'spoolbuddy'
|
||||
registerSettingsSearch({ labelKey: 'settings.currentUser', tab: 'users', subTab: 'users', keywords: 'current user profile password change', anchor: 'card-currentuser' });
|
||||
registerSettingsSearch({ labelKey: 'settings.users', tab: 'users', subTab: 'users', keywords: 'users accounts list', anchor: 'card-users' });
|
||||
registerSettingsSearch({ labelKey: 'settings.groups', tab: 'users', subTab: 'users', keywords: 'groups roles permissions administrators operators viewers', anchor: 'card-groups' });
|
||||
registerSettingsSearch({ labelKey: 'settings.sessionPolicy.title', labelFallback: 'Session Policy', tab: 'users', subTab: 'users', keywords: 'session timeout expiry logout remember me jwt token lifetime', anchor: 'card-session-policy' });
|
||||
registerSettingsSearch({ labelKey: 'settings.email.smtpSettings', labelFallback: 'SMTP Configuration', tab: 'users', subTab: 'email', keywords: 'smtp email send server port password auth starttls ssl', anchor: 'card-smtp' });
|
||||
registerSettingsSearch({ labelKey: 'settings.ldap.title', labelFallback: 'LDAP Authentication', tab: 'users', subTab: 'ldap', keywords: 'ldap active directory ad authentication bind dn search base group mapping', anchor: 'card-ldap' });
|
||||
registerSettingsSearch({ labelKey: 'settings.tabs.backup', tab: 'backup', keywords: 'backup github restore download cloud sync profiles archives', anchor: 'card-backup' });
|
||||
@@ -1012,7 +1013,8 @@ export function SettingsPage() {
|
||||
(settings.default_nozzle_offset_cali ?? true) !== (localSettings.default_nozzle_offset_cali ?? true) ||
|
||||
(settings.stagger_group_size ?? 2) !== (localSettings.stagger_group_size ?? 2) ||
|
||||
(settings.stagger_interval_minutes ?? 5) !== (localSettings.stagger_interval_minutes ?? 5) ||
|
||||
(settings.require_plate_clear ?? false) !== (localSettings.require_plate_clear ?? false);
|
||||
(settings.require_plate_clear ?? false) !== (localSettings.require_plate_clear ?? false) ||
|
||||
(settings.session_max_hours ?? 24) !== (localSettings.session_max_hours ?? 24);
|
||||
|
||||
if (!hasChanges) {
|
||||
return;
|
||||
@@ -1099,6 +1101,7 @@ export function SettingsPage() {
|
||||
stagger_group_size: localSettings.stagger_group_size,
|
||||
stagger_interval_minutes: localSettings.stagger_interval_minutes,
|
||||
require_plate_clear: localSettings.require_plate_clear,
|
||||
session_max_hours: localSettings.session_max_hours,
|
||||
};
|
||||
updateMutation.mutate(settingsToSave);
|
||||
}, 500);
|
||||
@@ -5117,8 +5120,73 @@ export function SettingsPage() {
|
||||
|
||||
{authEnabled && (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
||||
{/* Left Column: Current User + User List */}
|
||||
{/* Left Column: Session Policy + Current User + User List */}
|
||||
<div className="space-y-3">
|
||||
{/* Session Policy (#1706) — admin-set ceiling for user session lifetime */}
|
||||
<Card id="card-session-policy">
|
||||
<CardHeader>
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Lock className="w-5 h-5 text-bambu-green" />
|
||||
{t('settings.sessionPolicy.title')}
|
||||
</h3>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-bambu-gray mb-4">
|
||||
{t('settings.sessionPolicy.description')}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-4">
|
||||
{[
|
||||
{ hours: 24, labelKey: 'settings.sessionPolicy.preset24h' },
|
||||
{ hours: 168, labelKey: 'settings.sessionPolicy.preset7d' },
|
||||
{ hours: 720, labelKey: 'settings.sessionPolicy.preset30d' },
|
||||
].map((preset) => {
|
||||
const current = localSettings?.session_max_hours ?? 24;
|
||||
const isActive = current === preset.hours;
|
||||
return (
|
||||
<button
|
||||
key={preset.hours}
|
||||
type="button"
|
||||
onClick={() => updateSetting('session_max_hours', preset.hours)}
|
||||
disabled={authEnabled && !hasPermission('settings:update')}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
isActive
|
||||
? 'bg-bambu-green text-white'
|
||||
: 'bg-bambu-dark-tertiary text-bambu-gray hover:text-white hover:bg-bambu-dark'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{t(preset.labelKey)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={720}
|
||||
value={localSettings?.session_max_hours ?? 24}
|
||||
onChange={(e) => {
|
||||
const raw = parseInt(e.target.value, 10);
|
||||
if (Number.isNaN(raw)) return;
|
||||
updateSetting('session_max_hours', Math.max(1, Math.min(720, raw)));
|
||||
}}
|
||||
disabled={authEnabled && !hasPermission('settings:update')}
|
||||
aria-label={t('settings.sessionPolicy.customHoursLabel')}
|
||||
className="w-20 px-2 py-2 bg-bambu-dark-tertiary text-white text-sm rounded-lg border border-bambu-dark-tertiary focus:border-bambu-green focus:outline-none disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-sm text-bambu-gray">{t('settings.sessionPolicy.hoursSuffix')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{(localSettings?.session_max_hours ?? 24) > 24 && (
|
||||
<div className="flex items-start gap-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
|
||||
<AlertTriangle className="w-4 h-4 text-yellow-400 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-yellow-200">
|
||||
{t('settings.sessionPolicy.warning')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Current User Card */}
|
||||
{user && (
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user