Files
TREK/client/src/pages/admin/AdminSettingsTab.tsx
T
Maurice 7471976c9a feat(auth): passkey enrolment, login button + admin settings UI
PasskeysSection in account settings (add/rename/remove with a current-password step-up), a 'Sign in with a passkey' button on the login page, the admin enable + RP-ID/origins controls, and a per-user admin reset action.
2026-06-05 18:46:23 +02:00

513 lines
27 KiB
TypeScript

import React from 'react'
import { adminApi, authApi } from '../../api/client'
import { getApiErrorMessage } from '../../types'
import { Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, Sun, RefreshCw, AlertTriangle } from 'lucide-react'
import type { TranslationFn } from '../../types'
import type { useAdmin } from './useAdmin'
interface AdminSettingsTabProps {
admin: ReturnType<typeof useAdmin>
t: TranslationFn
}
// "Settings" admin tab: auth methods, require-MFA, allowed file types, API keys,
// OIDC config and the danger zone. Pure layout around the useAdmin hook.
export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): React.ReactElement {
const {
toast,
setPlacesPhotosEnabled, setPlacesAutocompleteEnabled, setPlacesDetailsEnabled,
placesPhotosEnabled, setPlacesPhotosEnabledState,
placesAutocompleteEnabled, setPlacesAutocompleteEnabledState,
placesDetailsEnabled, setPlacesDetailsEnabledState,
oidcConfig, setOidcConfig, savingOidc, setSavingOidc,
passwordLogin, setPasswordLogin, passwordRegistration, setPasswordRegistration,
oidcLogin, setOidcLogin, oidcRegistration, setOidcRegistration,
envOverrideOidcOnly, oidcConfigured, requireMfa,
passkeyLogin, setPasskeyLogin, passkeyConfigured,
webauthnRpId, setWebauthnRpId, webauthnOrigins, setWebauthnOrigins, savingWebauthn, handleSaveWebauthn,
allowedFileTypes, setAllowedFileTypes, savingFileTypes, setSavingFileTypes,
mapsKey, setMapsKey, showKeys, savingKeys, validating, validation,
setShowRotateJwtModal,
handleToggleAuthSetting, handleToggleRequireMfa,
toggleKey, handleSaveApiKeys, handleValidateKey,
} = admin
return (
<div className="space-y-6">
{/* Authentication Methods */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.authMethods')}</h2>
</div>
<div className="p-6 space-y-5">
{envOverrideOidcOnly && (
<p className="text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
{t('admin.envOverrideHint')}
</p>
)}
{/* Password Login */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.passwordLogin')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.passwordLoginHint')}</p>
</div>
<button
disabled={envOverrideOidcOnly || (!passwordLogin && !oidcLogin)}
onClick={() => handleToggleAuthSetting('password_login', !passwordLogin, setPasswordLogin)}
title={!passwordLogin && !oidcLogin ? t('admin.lockoutWarning') : undefined}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${passwordLogin ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: passwordLogin ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
{/* Password Registration */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.passwordRegistration')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.passwordRegistrationHint')}</p>
</div>
<button
disabled={envOverrideOidcOnly}
onClick={() => handleToggleAuthSetting('password_registration', !passwordRegistration, setPasswordRegistration)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${passwordRegistration ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: passwordRegistration ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
{/* SSO Login (only when OIDC configured) */}
{oidcConfigured && (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.oidcLogin')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.oidcLoginHint')}</p>
</div>
<button
disabled={!passwordLogin && oidcLogin}
onClick={() => handleToggleAuthSetting('oidc_login', !oidcLogin, setOidcLogin)}
title={!passwordLogin && oidcLogin ? t('admin.lockoutWarning') : undefined}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${oidcLogin ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: oidcLogin ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
)}
{/* SSO Registration (only when OIDC configured) */}
{oidcConfigured && (
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.oidcRegistration')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.oidcRegistrationHint')}</p>
</div>
<button
onClick={() => handleToggleAuthSetting('oidc_registration', !oidcRegistration, setOidcRegistration)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${oidcRegistration ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: oidcRegistration ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
)}
</div>
</div>
{/* Passkey (WebAuthn) login */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.passkey.title')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('admin.passkey.cardHint')}</p>
</div>
<div className="p-6 space-y-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.passkey.login')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.passkey.loginHint')}</p>
</div>
<button
type="button"
onClick={() => handleToggleAuthSetting('passkey_login', !passkeyLogin, setPasskeyLogin)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${passkeyLogin ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: passkeyLogin ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
{passkeyLogin && !passkeyConfigured && (
<p className="flex items-start gap-2 text-xs text-amber-600 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
{t('admin.passkey.notConfigured')}
</p>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{t('admin.passkey.rpId')}</label>
<p className="text-xs text-slate-400 mb-1.5">{t('admin.passkey.rpIdHint')}</p>
<input
type="text"
value={webauthnRpId}
onChange={e => setWebauthnRpId(e.target.value)}
placeholder="trek.example.org"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">{t('admin.passkey.origins')}</label>
<p className="text-xs text-slate-400 mb-1.5">{t('admin.passkey.originsHint')}</p>
<input
type="text"
value={webauthnOrigins}
onChange={e => setWebauthnOrigins(e.target.value)}
placeholder="https://trek.example.org"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<button
type="button"
onClick={handleSaveWebauthn}
disabled={savingWebauthn}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:opacity-50"
>
{savingWebauthn ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
{t('common.save')}
</button>
</div>
</div>
{/* Require 2FA for all users */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.requireMfa')}</h2>
</div>
<div className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.requireMfa')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.requireMfaHint')}</p>
</div>
<button
type="button"
onClick={() => handleToggleRequireMfa(!requireMfa)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${requireMfa ? 'bg-content' : 'bg-edge'}`}
>
<span
className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200"
style={{ transform: requireMfa ? 'translateX(20px)' : 'translateX(0)' }}
/>
</button>
</div>
</div>
</div>
{/* Allowed File Types */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.fileTypes')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('admin.fileTypesHint')}</p>
</div>
<div className="p-6">
<input
type="text"
value={allowedFileTypes}
onChange={e => setAllowedFileTypes(e.target.value)}
placeholder="jpg,png,pdf,doc,docx,xls,xlsx,txt,csv"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
<p className="text-xs text-slate-400 mt-2">{t('admin.fileTypesFormat')}</p>
<button
onClick={async () => {
setSavingFileTypes(true)
try {
await authApi.updateAppSettings({ allowed_file_types: allowedFileTypes })
toast.success(t('admin.fileTypesSaved'))
} catch { toast.error(t('common.error')) }
finally { setSavingFileTypes(false) }
}}
disabled={savingFileTypes}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:bg-slate-400 mt-3"
>
{savingFileTypes ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-4 h-4" />}
{t('common.save')}
</button>
</div>
</div>
{/* API Keys */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.apiKeys')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('admin.apiKeysHint')}</p>
</div>
<div className="p-6 space-y-4">
{/* Google Maps Key */}
<div>
<label className="flex items-center gap-2 text-sm font-medium text-slate-700 mb-1.5">
{t('admin.mapsKey')}
<span className="text-[9px] font-medium px-1.5 py-px rounded-full bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">{t('admin.recommended')}</span>
</label>
<div className="flex gap-2">
<div className="relative flex-1">
<input
type={showKeys.maps ? 'text' : 'password'}
value={mapsKey}
onChange={e => setMapsKey(e.target.value)}
placeholder={t('settings.keyPlaceholder')}
className="w-full pr-10 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
<button
type="button"
onClick={() => toggleKey('maps')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
>
{showKeys.maps ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
<button
onClick={() => handleValidateKey('maps')}
disabled={!mapsKey || validating.maps}
className="px-3 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-1.5"
>
{validating.maps ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : validation.maps === true ? (
<CheckCircle className="w-4 h-4 text-emerald-500" />
) : validation.maps === false ? (
<XCircle className="w-4 h-4 text-red-500" />
) : null}
{t('admin.validateKey')}
</button>
</div>
<p className="text-xs text-slate-400 mt-1">{t('admin.mapsKeyHintLong')}</p>
{validation.maps === true && (
<p className="text-xs text-emerald-600 mt-1 flex items-center gap-1">
<span className="w-2 h-2 bg-emerald-500 rounded-full inline-block"></span>
{t('admin.keyValid')}
</p>
)}
{validation.maps === false && (
<p className="text-xs text-red-500 mt-1 flex items-center gap-1">
<span className="w-2 h-2 bg-red-500 rounded-full inline-block"></span>
{t('admin.keyInvalid')}
</p>
)}
</div>
{/* Place Photos Toggle */}
<div className="flex items-center justify-between gap-4 py-3 border-t border-slate-100">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.placesPhotos.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesPhotos.subtitle')}</p>
</div>
<button
onClick={async () => {
const next = !placesPhotosEnabled
setPlacesPhotosEnabledState(next)
setPlacesPhotosEnabled(next)
try { await adminApi.updatePlacesPhotos(next) } catch { setPlacesPhotosEnabledState(!next); setPlacesPhotosEnabled(!next) }
}}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesPhotosEnabled ? 'bg-content' : 'bg-edge'}`}
>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesPhotosEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button>
</div>
{/* Place Autocomplete Toggle */}
<div className="flex items-center justify-between gap-4 py-3 border-t border-slate-100">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.placesAutocomplete.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesAutocomplete.subtitle')}</p>
</div>
<button
onClick={async () => {
const next = !placesAutocompleteEnabled
setPlacesAutocompleteEnabledState(next)
setPlacesAutocompleteEnabled(next)
try { await adminApi.updatePlacesAutocomplete(next) } catch { setPlacesAutocompleteEnabledState(!next); setPlacesAutocompleteEnabled(!next) }
}}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesAutocompleteEnabled ? 'bg-content' : 'bg-edge'}`}
>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesAutocompleteEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button>
</div>
{/* Place Details Toggle */}
<div className="flex items-center justify-between gap-4 py-3 border-t border-slate-100">
<div>
<p className="text-sm font-medium text-slate-700">{t('admin.placesDetails.title')}</p>
<p className="text-xs text-slate-400 mt-0.5">{t('admin.placesDetails.subtitle')}</p>
</div>
<button
onClick={async () => {
const next = !placesDetailsEnabled
setPlacesDetailsEnabledState(next)
setPlacesDetailsEnabled(next)
try { await adminApi.updatePlacesDetails(next) } catch { setPlacesDetailsEnabledState(!next); setPlacesDetailsEnabled(!next) }
}}
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${placesDetailsEnabled ? 'bg-content' : 'bg-edge'}`}
>
<span className="absolute left-0.5 h-5 w-5 rounded-full bg-white transition-transform duration-200" style={{ transform: placesDetailsEnabled ? 'translateX(20px)' : 'translateX(0)' }} />
</button>
</div>
{/* Open-Meteo Weather Info */}
<div className="rounded-lg border border-emerald-200 bg-emerald-50 dark:bg-emerald-950/30 dark:border-emerald-800 overflow-hidden">
<div className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg bg-emerald-500 flex items-center justify-center flex-shrink-0">
<Sun className="w-3.5 h-3.5 text-white" />
</div>
<span className="text-sm font-semibold text-emerald-900 dark:text-emerald-200">{t('admin.weather.title')}</span>
</div>
<span className="text-[10px] font-medium px-2 py-0.5 rounded-full bg-emerald-200 dark:bg-emerald-800 text-emerald-800 dark:text-emerald-200">{t('admin.weather.badge')}</span>
</div>
<div className="px-4 pb-3">
<p className="text-xs text-emerald-800 dark:text-emerald-300 leading-relaxed">{t('admin.weather.description')}</p>
<p className="text-[11px] text-emerald-600 dark:text-emerald-400 mt-1.5 leading-relaxed">{t('admin.weather.locationHint')}</p>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="rounded-md bg-white dark:bg-emerald-900/40 px-3 py-2 border border-emerald-100 dark:border-emerald-800">
<p className="text-xs font-semibold text-emerald-900 dark:text-emerald-200">{t('admin.weather.forecast')}</p>
<p className="text-[11px] text-emerald-600 dark:text-emerald-400 mt-0.5">{t('admin.weather.forecastDesc')}</p>
</div>
<div className="rounded-md bg-white dark:bg-emerald-900/40 px-3 py-2 border border-emerald-100 dark:border-emerald-800">
<p className="text-xs font-semibold text-emerald-900 dark:text-emerald-200">{t('admin.weather.climate')}</p>
<p className="text-[11px] text-emerald-600 dark:text-emerald-400 mt-0.5">{t('admin.weather.climateDesc')}</p>
</div>
<div className="rounded-md bg-white dark:bg-emerald-900/40 px-3 py-2 border border-emerald-100 dark:border-emerald-800">
<p className="text-xs font-semibold text-emerald-900 dark:text-emerald-200">{t('admin.weather.requests')}</p>
<p className="text-[11px] text-emerald-600 dark:text-emerald-400 mt-0.5">{t('admin.weather.requestsDesc')}</p>
</div>
</div>
</div>
</div>
<button
onClick={handleSaveApiKeys}
disabled={savingKeys}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:bg-slate-400"
>
{savingKeys ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-4 h-4" />}
{t('common.save')}
</button>
</div>
</div>
{/* OIDC / SSO Configuration */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100">
<h2 className="font-semibold text-slate-900">{t('admin.oidcTitle')}</h2>
<p className="text-xs text-slate-400 mt-1">{t('admin.oidcSubtitle')}</p>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('admin.oidcDisplayName')}</label>
<input
type="text"
value={oidcConfig.display_name}
onChange={e => setOidcConfig(c => ({ ...c, display_name: e.target.value }))}
placeholder='z.B. Google, Authentik, Keycloak'
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">{t('admin.oidcIssuer')}</label>
<input
type="url"
value={oidcConfig.issuer}
onChange={e => setOidcConfig(c => ({ ...c, issuer: e.target.value }))}
placeholder='https://accounts.google.com'
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
<p className="text-xs text-slate-400 mt-1">{t('admin.oidcIssuerHint')}</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Discovery URL <span className="text-slate-400 font-normal">(optional)</span></label>
<input
type="url"
value={oidcConfig.discovery_url}
onChange={e => setOidcConfig(c => ({ ...c, discovery_url: e.target.value }))}
placeholder='https://auth.example.com/application/o/trek/.well-known/openid-configuration'
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
<p className="text-xs text-slate-400 mt-1">Override the auto-constructed discovery URL. Required for providers like Authentik where the endpoint is not at <code className="bg-slate-100 px-1 rounded">{'<issuer>/.well-known/openid-configuration'}</code>.</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Client ID</label>
<input
type="text"
value={oidcConfig.client_id}
onChange={e => setOidcConfig(c => ({ ...c, client_id: e.target.value }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1.5">Client Secret</label>
<input
type="password"
value={oidcConfig.client_secret}
onChange={e => setOidcConfig(c => ({ ...c, client_secret: e.target.value }))}
placeholder={oidcConfig.client_secret_set ? '••••••••' : ''}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent"
/>
</div>
<button
onClick={async () => {
setSavingOidc(true)
try {
const payload: Record<string, unknown> = { issuer: oidcConfig.issuer, client_id: oidcConfig.client_id, display_name: oidcConfig.display_name, discovery_url: oidcConfig.discovery_url }
if (oidcConfig.client_secret) payload.client_secret = oidcConfig.client_secret
await adminApi.updateOidc(payload)
toast.success(t('admin.oidcSaved'))
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('common.error')))
} finally {
setSavingOidc(false)
}
}}
disabled={savingOidc}
className="flex items-center gap-2 px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-700 disabled:bg-slate-400"
>
{savingOidc ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <Save className="w-4 h-4" />}
{t('common.save')}
</button>
</div>
</div>
{/* Danger Zone */}
<div className="bg-white rounded-xl border border-red-200 overflow-hidden">
<div className="px-6 py-4 border-b border-red-100 bg-red-50">
<h2 className="font-semibold text-red-700 flex items-center gap-2">
<AlertTriangle className="w-4 h-4" />
Danger Zone
</h2>
</div>
<div className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-700">Rotate JWT Secret</p>
<p className="text-xs text-slate-400 mt-0.5">Generate a new JWT signing secret. All active sessions will be invalidated immediately.</p>
</div>
<button
onClick={() => setShowRotateJwtModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors"
>
<RefreshCw className="w-4 h-4" />
Rotate
</button>
</div>
</div>
</div>
</div>
)
}