'use client'; import { useEffect, useState } from 'react'; import { Save, Loader2, Lock } from 'lucide-react'; import type { SettingsPolicy, FeatureGates } from '@/lib/admin/types'; import { DEFAULT_FEATURE_GATES, DEFAULT_POLICY } from '@/lib/admin/types'; import { apiFetch } from '@/lib/browser-navigation'; // `allMailViewEnabled` is deprecated (folded into `crossAllViewEnabled`, normalized // forward on policy load), so it is hidden from the admin UI. const EXCLUDED_FEATURE_GATES: (keyof FeatureGates)[] = ['pluginsEnabled', 'pluginsUploadEnabled', 'themesEnabled', 'userThemesEnabled', 'allMailViewEnabled']; const FEATURE_GATE_LABELS: Partial> = { sidebarAppsEnabled: { label: 'Sidebar Apps', description: 'Allow custom web apps in navigation rail' }, settingsExportEnabled: { label: 'Settings Export/Import', description: 'Allow users to export and import settings JSON' }, customKeywordsEnabled: { label: 'Custom Keywords', description: 'Allow user-created labels and tags' }, templatesEnabled: { label: 'Email Templates', description: 'Allow email template creation and library' }, calendarEnabled: { label: 'Calendar', description: 'Enable calendar features and views' }, calendarTasksEnabled: { label: 'Calendar Tasks', description: 'Show task panel in calendar view' }, contactsEnabled: { label: 'Contacts', description: 'Enable contacts/address book features' }, smimeEnabled: { label: 'S/MIME', description: 'Enable certificate management and email signing' }, externalContentEnabled: { label: 'External Content', description: 'Allow users to choose external content loading policy' }, debugModeEnabled: { label: 'Debug Mode', description: 'Allow users to enable debug/diagnostic mode' }, folderIconsEnabled: { label: 'Folder Icons', description: 'Allow custom folder icon picker' }, hoverActionsConfigEnabled: { label: 'Hover Actions Config', description: 'Allow users to customize email hover actions' }, filesEnabled: { label: 'Files (WebDAV)', description: 'Enable file storage via WebDAV. WARNING: Large uploads can cause Stalwart/RocksDB instability. Not recommended for production.' }, crossUnreadViewEnabled: { label: 'Unified Mailbox: Unread', description: 'Allow an "Unread" entry in the Unified Mailbox section that lists unread mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' }, crossStarredViewEnabled: { label: 'Unified Mailbox: Starred', description: 'Allow a "Starred" entry in the Unified Mailbox section that lists flagged/starred mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' }, crossAllViewEnabled: { label: 'Unified Mailbox: All Mail', description: 'Allow an "All mail" entry in the Unified Mailbox section that lists all mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' }, unifiedCrossAccountEnabled: { label: 'Unified Mailbox: Cross-account', description: 'Allow users to expand the Unified Mailbox beyond the active account boundary so its lists merge across every logged-in account. When off, the Unified Mailbox stays within the active account and its shared folders.' }, aiAssistantEnabled: { label: 'AI Assistant (preview)', description: 'Show the AI Assistant settings tab. Local (Ollama on the user\'s own machine or this desktop app) is free and unmetered; public (bring-your-own-key) is available too but not yet monitored or metered — see docs/AI-ASSISTANT-CONCEPT.md.' }, }; const RESTRICTABLE_SETTINGS = [ { key: 'fontSize', label: 'Font Size', category: 'Appearance', type: 'enum', allowedValues: ['small', 'medium', 'large'] }, { key: 'density', label: 'Density', category: 'Appearance', type: 'enum', allowedValues: ['compact', 'regular', 'spacious'] }, { key: 'animationsEnabled', label: 'Animations', category: 'Appearance', type: 'boolean' }, { key: 'markAsReadDelay', label: 'Mark as Read Delay', category: 'Email', type: 'number' }, { key: 'deleteAction', label: 'Delete Action', category: 'Email', type: 'enum', allowedValues: ['trash', 'trash-and-read', 'permanent'] }, { key: 'showPreview', label: 'Show Preview', category: 'Email', type: 'boolean' }, { key: 'mailLayout', label: 'Mail Layout', category: 'Email', type: 'enum', allowedValues: ['split', 'focus', 'horizontal'] }, { key: 'emailsPerPage', label: 'Emails Per Page', category: 'Email', type: 'number' }, { key: 'externalContentPolicy', label: 'External Content Policy', category: 'Email', type: 'enum', allowedValues: ['allow', 'block', 'ask'] }, { key: 'sendConfirmation', label: 'Send Confirmation', category: 'Composer', type: 'boolean' }, { key: 'defaultReplyMode', label: 'Default Reply Mode', category: 'Composer', type: 'enum', allowedValues: ['reply', 'reply-all'] }, { key: 'autoSelectReplyIdentity', label: 'Auto-select Reply Identity', category: 'Composer', type: 'boolean' }, { key: 'plainTextMode', label: 'Plain Text Only', category: 'Composer', type: 'boolean' }, { key: 'sessionTimeout', label: 'Session Timeout', category: 'Privacy', type: 'number' }, { key: 'emailNotificationsEnabled', label: 'Email Notifications', category: 'Notifications', type: 'boolean' }, { key: 'calendarNotificationsEnabled', label: 'Calendar Notifications', category: 'Notifications', type: 'boolean' }, { key: 'debugMode', label: 'Debug Mode', category: 'Advanced', type: 'boolean' }, ]; export function PolicyTab() { const [policy, setPolicy] = useState({ ...DEFAULT_POLICY }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [dirty, setDirty] = useState(false); useEffect(() => { fetchPolicy(); }, []); async function fetchPolicy() { setLoading(true); try { const res = await apiFetch('/api/admin/policy'); if (res.ok) { const data = await res.json(); setPolicy(data); } } finally { setLoading(false); } } function toggleFeature(key: keyof FeatureGates) { setPolicy(prev => ({ ...prev, features: { ...prev.features, [key]: !prev.features[key] }, })); setDirty(true); setMessage(null); } function setPushRelayUrl(value: string) { setPolicy(prev => ({ ...prev, pushRelayUrl: value })); setDirty(true); setMessage(null); } function togglePushRelayLocked() { setPolicy(prev => ({ ...prev, pushRelayUrlLocked: !prev.pushRelayUrlLocked })); setDirty(true); setMessage(null); } function toggleLocked(settingKey: string) { setPolicy(prev => { const existing = prev.restrictions[settingKey] || {}; const newRestrictions = { ...prev.restrictions }; if (existing.locked) { delete newRestrictions[settingKey]; } else { newRestrictions[settingKey] = { ...existing, locked: true }; } return { ...prev, restrictions: newRestrictions }; }); setDirty(true); setMessage(null); } function toggleHidden(settingKey: string) { setPolicy(prev => { const existing = prev.restrictions[settingKey] || {}; const newRestrictions = { ...prev.restrictions }; newRestrictions[settingKey] = { ...existing, hidden: !existing.hidden }; if (!newRestrictions[settingKey].hidden && !newRestrictions[settingKey].locked) { delete newRestrictions[settingKey]; } return { ...prev, restrictions: newRestrictions }; }); setDirty(true); setMessage(null); } async function handleSave() { setSaving(true); setMessage(null); const res = await apiFetch('/api/admin/policy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(policy), }); if (res.ok) { setMessage({ type: 'success', text: 'Policy saved. Users will see changes on next login.' }); setDirty(false); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Failed to save' }); } setSaving(false); } if (loading) { return
Loading...
; } const categories = [...new Set(RESTRICTABLE_SETTINGS.map(s => s.category))]; return (

User Policy

Control which features and settings users can access

{dirty && ( )}
{message && (
{message.text}
)}

Feature Gates

Toggle entire features on or off for all users. Plugin and theme gates are on their respective admin pages.

{(Object.keys(DEFAULT_FEATURE_GATES) as (keyof FeatureGates)[]) .filter(key => !EXCLUDED_FEATURE_GATES.includes(key)) .map(key => { const meta = FEATURE_GATE_LABELS[key]; if (!meta) return null; const { label, description } = meta; const enabled = policy.features[key]; return (
{label}

{description}

); })}

Push Relay

Override the Web Push relay URL shown in user notification settings. Leave empty to use the built-in default.

setPushRelayUrl(e.target.value)} placeholder="https://notifications.relay.example.com" className="w-full rounded border border-input bg-background px-3 py-2 text-sm" />
{categories.map(category => (

{category}

{RESTRICTABLE_SETTINGS.filter(s => s.category === category).map(setting => { const restriction = policy.restrictions[setting.key] || {}; return (
{setting.label}
); })}
))}
); }