diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 78aaf53..b1a9c52 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp } from 'lucide-react' +import { CheckCircle, XCircle, RefreshCw, ChevronDown, ChevronUp, Clock } from 'lucide-react' import { Sidebar } from '../components/Sidebar' import type { User } from '../types' @@ -36,8 +36,18 @@ const FIELD_LABELS: Record = { location_id: 'Default Location', } +interface AppRow { + id: number + slug: string + name: string + theme_color: string + category: string | null + active: boolean + max_session_hours: number | null +} + export function AdminSettings({ user }: { user: User }) { - const [tab, setTab] = useState<'integrations' | 'rooms'>('integrations') + const [tab, setTab] = useState<'integrations' | 'rooms' | 'apps'>('integrations') const [integrations, setIntegrations] = useState([]) const [loading, setLoading] = useState(true) @@ -58,7 +68,7 @@ export function AdminSettings({ user }: { user: User }) {

- {(['integrations', 'rooms'] as const).map(t => ( + {(['integrations', 'rooms', 'apps'] as const).map(t => ( ))}
@@ -84,6 +94,7 @@ export function AdminSettings({ user }: { user: User }) { )} {tab === 'rooms' && } + {tab === 'apps' && } ) @@ -426,6 +437,111 @@ function RoomsTab() { ) } +function AppsTab() { + const [apps, setApps] = useState([]) + const [loading, setLoading] = useState(true) + const [values, setValues] = useState>({}) + const [saving, setSaving] = useState>({}) + const [saved, setSaved] = useState>({}) + + useEffect(() => { + fetch('/api/auth/admin/apps', { credentials: 'include' }) + .then(r => r.json()) + .then((data: AppRow[]) => { + const active = data.filter(a => a.active) + setApps(active) + const init: Record = {} + active.forEach(a => { init[a.slug] = a.max_session_hours != null ? String(a.max_session_hours) : '' }) + setValues(init) + }) + .finally(() => setLoading(false)) + }, []) + + async function save(slug: string) { + setSaving(prev => ({ ...prev, [slug]: true })) + setSaved(prev => ({ ...prev, [slug]: false })) + const raw = values[slug].trim() + const max_session_hours = raw === '' ? null : parseInt(raw) + await fetch(`/api/auth/admin/apps/${slug}`, { + method: 'PATCH', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ max_session_hours }), + }) + setSaving(prev => ({ ...prev, [slug]: false })) + setSaved(prev => ({ ...prev, [slug]: true })) + setTimeout(() => setSaved(prev => ({ ...prev, [slug]: false })), 2000) + } + + const byCategory = apps.reduce>((acc, a) => { + const cat = a.category ?? 'Other' + ;(acc[cat] ??= []).push(a) + return acc + }, {}) + + if (loading) return

Loading…

+ + return ( +
+

+ Set a maximum session age per app. Users will be prompted to re-login when the limit is reached, + even if their global session is still valid. Leave blank to use the global default (30 days). +

+ + {Object.entries(byCategory).sort(([a], [b]) => a.localeCompare(b)).map(([cat, catApps]) => ( +
+

+ {cat} +

+
+ {catApps.map(app => ( +
+
+ + {app.name} + +
+ + setValues(prev => ({ ...prev, [app.slug]: e.target.value }))} + onKeyDown={e => e.key === 'Enter' && save(app.slug)} + placeholder="Default (720h)" + style={{ ...inputStyle, width: '130px', textAlign: 'right' }} + /> + hours +
+ +
+ ))} +
+
+ ))} +
+ ) +} + const inputStyle: React.CSSProperties = { background: 'var(--body-bg)', border: '1px solid var(--card-border)', borderRadius: '6px', padding: '0.45rem 0.75rem', fontSize: '0.85rem',