'use client'; import { useEffect, useState, useRef } from 'react'; import Link from 'next/link'; import { Upload, Trash2, Power, PowerOff, AlertTriangle, Loader2, Package, Save, Shield, Lock, LockOpen, Settings } from 'lucide-react'; import type { SettingsPolicy } from '@/lib/admin/types'; import { DEFAULT_POLICY } from '@/lib/admin/types'; interface PluginEntry { id: string; name: string; version: string; author: string; description: string; type: string; enabled: boolean; forceEnabled?: boolean; permissions: string[]; installedAt: string; updatedAt: string; } export default function AdminPluginsPage() { const [plugins, setPlugins] = useState([]); const [loading, setLoading] = useState(true); const [uploading, setUploading] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const fileInputRef = useRef(null); const [policy, setPolicy] = useState({ ...DEFAULT_POLICY }); const [policyDirty, setPolicyDirty] = useState(false); const [savingPolicy, setSavingPolicy] = useState(false); useEffect(() => { fetchPlugins(); fetchPolicy(); }, []); async function fetchPolicy() { try { const res = await fetch('/api/admin/policy'); if (res.ok) { const data = await res.json(); setPolicy(data); } } catch { /* ignore */ } } function togglePluginsEnabled() { setPolicy(prev => ({ ...prev, features: { ...prev.features, pluginsEnabled: !prev.features.pluginsEnabled }, })); setPolicyDirty(true); setMessage(null); } function togglePluginsUploadEnabled() { setPolicy(prev => ({ ...prev, features: { ...prev.features, pluginsUploadEnabled: !prev.features.pluginsUploadEnabled }, })); setPolicyDirty(true); setMessage(null); } async function handleSavePolicy() { setSavingPolicy(true); setMessage(null); try { const res = await fetch('/api/admin/policy', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(policy), }); if (res.ok) { setMessage({ type: 'success', text: 'Plugin policy saved. Users will see changes on next login.' }); setPolicyDirty(false); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Failed to save policy' }); } } catch { setMessage({ type: 'error', text: 'Failed to save policy' }); } finally { setSavingPolicy(false); } } async function fetchPlugins() { setLoading(true); try { const res = await fetch('/api/admin/plugins'); if (res.ok) setPlugins(await res.json()); } finally { setLoading(false); } } async function handleUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setUploading(true); setMessage(null); const formData = new FormData(); formData.append('file', file); try { const res = await fetch('/api/admin/plugins', { method: 'POST', body: formData, }); const data = await res.json(); if (res.ok) { const warnings = data.warnings?.length ? ` (${data.warnings.length} warning(s))` : ''; setMessage({ type: 'success', text: `Plugin "${data.plugin.name}" installed${warnings}` }); await fetchPlugins(); } else { setMessage({ type: 'error', text: data.error || 'Upload failed' }); } } catch { setMessage({ type: 'error', text: 'Upload failed' }); } finally { setUploading(false); if (fileInputRef.current) fileInputRef.current.value = ''; } } async function togglePlugin(id: string, enabled: boolean) { setMessage(null); const res = await fetch('/api/admin/plugins', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, enabled }), }); if (res.ok) { setPlugins(prev => prev.map(p => p.id === id ? { ...p, enabled } : p)); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Update failed' }); } } async function toggleForceEnabled(id: string, forceEnabled: boolean) { setMessage(null); // If force-enabling, also ensure the plugin is enabled const body: Record = { id, forceEnabled }; if (forceEnabled) body.enabled = true; const res = await fetch('/api/admin/plugins', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (res.ok) { setPlugins(prev => prev.map(p => p.id === id ? { ...p, forceEnabled, ...(forceEnabled ? { enabled: true } : {}) } : p)); // Also update policy setPolicy(prev => { const current = prev.forceEnabledPlugins || []; return { ...prev, forceEnabledPlugins: forceEnabled ? [...current.filter(pid => pid !== id), id] : current.filter(pid => pid !== id), }; }); setPolicyDirty(true); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Update failed' }); } } async function forceEnableAll() { setMessage(null); const disabled = plugins.filter(p => !p.enabled); if (disabled.length === 0) { setMessage({ type: 'success', text: 'All plugins are already enabled' }); return; } let failed = 0; for (const p of disabled) { const res = await fetch('/api/admin/plugins', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: p.id, enabled: true }), }); if (!res.ok) failed++; } setPlugins(prev => prev.map(p => failed === 0 ? { ...p, enabled: true } : p)); if (failed === 0) { await fetchPlugins(); setMessage({ type: 'success', text: `All ${disabled.length} plugin(s) enabled` }); } else { await fetchPlugins(); setMessage({ type: 'error', text: `${failed} plugin(s) failed to enable` }); } } async function forceDisableAll() { setMessage(null); const enabled = plugins.filter(p => p.enabled); if (enabled.length === 0) { setMessage({ type: 'success', text: 'All plugins are already disabled' }); return; } let failed = 0; for (const p of enabled) { const res = await fetch('/api/admin/plugins', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: p.id, enabled: false }), }); if (!res.ok) failed++; } if (failed === 0) { await fetchPlugins(); setMessage({ type: 'success', text: `All ${enabled.length} plugin(s) disabled` }); } else { await fetchPlugins(); setMessage({ type: 'error', text: `${failed} plugin(s) failed to disable` }); } } async function deletePlugin(id: string, name: string) { if (!confirm(`Remove plugin "${name}"? This cannot be undone.`)) return; setMessage(null); const res = await fetch('/api/admin/plugins', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }); if (res.ok) { setPlugins(prev => prev.filter(p => p.id !== id)); setMessage({ type: 'success', text: `Plugin "${name}" removed` }); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Delete failed' }); } } if (loading) { return
Loading...
; } const pluginsEnabled = policy.features.pluginsEnabled ?? true; const pluginsUploadEnabled = policy.features.pluginsUploadEnabled ?? true; return (

Plugins

Manage plugins and plugin policy for all users

{policyDirty && ( )}
{message && (
{message.text}
)} {/* Plugin Policy */}

Plugin Policy

Control plugin availability for users

Plugins Enabled

Allow the plugin system to load and run plugins for users

User Plugin Uploads

Allow users to upload plugin ZIP files in Settings

{/* Force enable / disable all */} {plugins.length > 0 && (
Force Enable / Disable All

Bulk toggle all deployed plugins at once

)}
{/* Deployed Plugins */}

Deployed Plugins

Admin-uploaded plugins for all users

{plugins.length === 0 ? (

No plugins installed

Upload a plugin ZIP file to get started

) : (
{plugins.map(plugin => (
{plugin.name} v{plugin.version} {plugin.enabled ? 'Enabled' : 'Disabled'} {plugin.forceEnabled && ( Forced )}
{plugin.description && (

{plugin.description}

)}
by {plugin.author} · {plugin.type} · installed {new Date(plugin.installedAt).toLocaleDateString()}
{plugin.permissions.length > 0 && (
Permissions: {plugin.permissions.join(', ')}
)}
))}
)}
); }