'use client'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Info, LogOut, Shield } from 'lucide-react'; import { clearSession, getSession, type Permission, type Role } from '@/lib/store/auth-store'; import { SettingsSection } from './SettingsSection'; import { Icons } from '@/components/ui/Icon'; import { ALL_PERMISSIONS, ROLE_PERMISSIONS } from '@/lib/auth/permissions'; type LoginMode = 'none' | 'legacy_password' | 'managed'; interface AccountInfo { id: string; username: string; name: string; role: Role; customPermissions: Permission[]; createdAt: number; updatedAt: number; } interface EditableAccount { id?: string; username: string; name: string; role: Role; customPermissions: Permission[]; password: string; isNew?: boolean; markedForDeletion?: boolean; } interface LegacyConfigEntry { password: string; name: string; role: Role; customPermissions: Permission[]; } const PERMISSION_LABELS: Record = { source_management: '视频源管理', account_management: '账户管理', danmaku_api: '弹幕 API', data_management: '数据管理', player_settings: '播放器设置', danmaku_appearance: '弹幕外观', view_settings: '显示设置', iptv_access: 'IPTV 访问', iptv_source_management: 'IPTV 自定义源管理', iptv_builtin_sources: 'IPTV 内置源', }; function buildEditableAccounts(accounts: AccountInfo[]): EditableAccount[] { return accounts.map((account) => ({ id: account.id, username: account.username, name: account.name, role: account.role, customPermissions: account.customPermissions, password: '', })); } function arraysEqual(left: Permission[], right: Permission[]): boolean { if (left.length !== right.length) return false; const sortedLeft = [...left].sort(); const sortedRight = [...right].sort(); return sortedLeft.every((value, index) => value === sortedRight[index]); } async function logoutAndReload() { try { await fetch('/api/auth/session', { method: 'DELETE' }); } catch { // Best-effort logout: clear the local mirror even if the request fails. } clearSession(); window.location.reload(); } export function AccountSettings() { const [session, setSessionState] = useState>(null); const [hasAuth, setHasAuth] = useState(false); const [loginMode, setLoginMode] = useState('none'); const [accounts, setAccounts] = useState([]); const [draftAccounts, setDraftAccounts] = useState([]); const [loadingAccounts, setLoadingAccounts] = useState(false); const [saveError, setSaveError] = useState(''); const [saveSuccess, setSaveSuccess] = useState(''); const [isSaving, setIsSaving] = useState(false); const [isDirty, setIsDirty] = useState(false); const [showLegacyConfig, setShowLegacyConfig] = useState(false); const [legacyEntries, setLegacyEntries] = useState([]); const canManageAccounts = session?.role === 'super_admin'; const isManagedMode = loginMode === 'managed'; const fetchAccounts = useCallback(async () => { if (!canManageAccounts) return; setLoadingAccounts(true); setSaveError(''); try { const response = await fetch('/api/auth/accounts'); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Failed to load accounts'); } const nextAccounts = (data.accounts || []) as AccountInfo[]; setAccounts(nextAccounts); setDraftAccounts(buildEditableAccounts(nextAccounts)); setIsDirty(false); } catch (error) { setSaveError(error instanceof Error ? error.message : 'Failed to load accounts'); } finally { setLoadingAccounts(false); } }, [canManageAccounts]); useEffect(() => { setSessionState(getSession()); fetch('/api/auth') .then((response) => response.json()) .then((data) => { setHasAuth(!!data.hasAuth); setLoginMode(data.loginMode || 'none'); }) .catch(() => { // Ignore config failures and keep the conservative default. }); }, []); useEffect(() => { if (!canManageAccounts) return; fetchAccounts(); }, [canManageAccounts, fetchAccounts, loginMode]); const currentDraftAccounts = useMemo( () => draftAccounts.filter((account) => !account.markedForDeletion), [draftAccounts] ); const addDraftAccount = () => { setDraftAccounts((current) => [ ...current, { username: '', name: '', role: 'viewer', customPermissions: [], password: '', isNew: true, }, ]); setIsDirty(true); setSaveSuccess(''); }; const updateDraftAccount = (index: number, patch: Partial) => { setDraftAccounts((current) => current.map((account, accountIndex) => { if (accountIndex !== index) return account; return { ...account, ...patch, }; })); setIsDirty(true); setSaveSuccess(''); }; const toggleDraftPermission = (index: number, permission: Permission) => { setDraftAccounts((current) => current.map((account, accountIndex) => { if (accountIndex !== index) return account; const nextPermissions = account.customPermissions.includes(permission) ? account.customPermissions.filter((value) => value !== permission) : [...account.customPermissions, permission]; return { ...account, customPermissions: nextPermissions, }; })); setIsDirty(true); setSaveSuccess(''); }; const removeDraftAccount = (index: number) => { setDraftAccounts((current) => current.flatMap((account, accountIndex) => { if (accountIndex !== index) return [account]; if (account.isNew) return []; return [{ ...account, markedForDeletion: true }]; })); setIsDirty(true); setSaveSuccess(''); }; const restoreDrafts = () => { setDraftAccounts(buildEditableAccounts(accounts)); setIsDirty(false); setSaveError(''); setSaveSuccess(''); }; const saveManagedAccounts = async () => { setIsSaving(true); setSaveError(''); setSaveSuccess(''); const originalById = new Map(accounts.map((account) => [account.id, account])); try { for (const draft of draftAccounts) { if (draft.markedForDeletion && draft.id) { const response = await fetch(`/api/auth/accounts/${draft.id}`, { method: 'DELETE', }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `Failed to delete ${draft.name}`); } } } for (const draft of draftAccounts) { if (draft.markedForDeletion) continue; if (draft.isNew) { const response = await fetch('/api/auth/accounts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: draft.username, name: draft.name, password: draft.password, role: draft.role, customPermissions: draft.customPermissions, }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `Failed to create ${draft.name || draft.username}`); } continue; } if (!draft.id) continue; const original = originalById.get(draft.id); if (!original) continue; const patch: Record = {}; if (draft.name !== original.name) patch.name = draft.name; if (draft.role !== original.role) patch.role = draft.role; if (!arraysEqual(draft.customPermissions, original.customPermissions)) { patch.customPermissions = draft.customPermissions; } if (draft.password) patch.password = draft.password; if (Object.keys(patch).length === 0) continue; const response = await fetch(`/api/auth/accounts/${draft.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || `Failed to update ${draft.name}`); } } await fetchAccounts(); setSaveSuccess('账户修改已保存'); } catch (error) { setSaveError(error instanceof Error ? error.message : 'Failed to save accounts'); } finally { setIsSaving(false); } }; const addLegacyEntry = () => { setLegacyEntries((current) => [ ...current, { password: '', name: '', role: 'viewer', customPermissions: [] }, ]); }; const updateLegacyEntry = (index: number, patch: Partial) => { setLegacyEntries((current) => current.map((entry, entryIndex) => { if (entryIndex !== index) return entry; return { ...entry, ...patch, }; })); }; const toggleLegacyPermission = (index: number, permission: Permission) => { setLegacyEntries((current) => current.map((entry, entryIndex) => { if (entryIndex !== index) return entry; const nextPermissions = entry.customPermissions.includes(permission) ? entry.customPermissions.filter((value) => value !== permission) : [...entry.customPermissions, permission]; return { ...entry, customPermissions: nextPermissions, }; })); }; const removeLegacyEntry = (index: number) => { setLegacyEntries((current) => current.filter((_, entryIndex) => entryIndex !== index)); }; const generatedLegacyAccounts = useMemo(() => { return legacyEntries .filter((entry) => entry.password.trim() && entry.name.trim()) .map((entry) => { let value = `${entry.password.trim()}:${entry.name.trim()}`; if (entry.role !== 'viewer' || entry.customPermissions.length > 0) { value += `:${entry.role}`; } if (entry.customPermissions.length > 0) { value += `:${entry.customPermissions.join('|')}`; } return value; }) .join(','); }, [legacyEntries]); if (!hasAuth && !session) return null; return (
{session && (
{session.name.charAt(0)}

{session.name}

{session.role === 'super_admin' ? '超级管理员' : session.role === 'admin' ? '管理员' : '观众'} {session.username && ( @{session.username} )} {session.mode && ( {session.mode === 'managed' ? '托管账户模式' : '环境变量模式'} )}
)}

当前登录模式: {isManagedMode ? 'Redis 托管账户' : loginMode === 'legacy_password' ? '环境变量密码登录' : '未启用'}

{isManagedMode ? (

托管模式下由超级管理员直接在此页面管理账户,修改会立即写入服务端存储。

) : (

环境变量模式下可继续使用 ADMIN_PASSWORDACCOUNTS 配置访问控制。

)}
{isManagedMode ? ( canManageAccounts ? (

账户列表

支持新增、改权限、重置密码和删除账户。只有点击保存才会提交修改。

{saveError && (
{saveError}
)} {saveSuccess && (
{saveSuccess}
)} {loadingAccounts ? (
正在加载账户...
) : (
{currentDraftAccounts.map((account, index) => { const extraPermissions = ALL_PERMISSIONS.filter((permission) => !ROLE_PERMISSIONS[account.role].includes(permission)); const isCurrentAccount = session?.accountId === account.id; return (
{account.isNew ? '新账户' : account.name || account.username || '未命名账户'} {account.role === 'super_admin' ? '超级管理员' : account.role === 'admin' ? '管理员' : '观众'} {isCurrentAccount && ( 当前账户 )}
{extraPermissions.length > 0 && (
额外权限
{extraPermissions.map((permission) => { const checked = account.customPermissions.includes(permission); return ( ); })}
)}
); })}
)}
) : (
当前模式已启用托管账户,但只有超级管理员可以查看和修改账户列表。
) ) : ( canManageAccounts && (

环境变量账户配置

兼容旧部署模式。新增或修改后,把生成的 ACCOUNTS 值同步到部署环境。

{accounts.length > 0 && (
{accounts.map((account) => (
{account.name.charAt(0)}
{account.name}

@{account.username}

{account.role === 'super_admin' ? '超级管理员' : account.role === 'admin' ? '管理员' : '观众'}
))}
)} {showLegacyConfig && (
{legacyEntries.map((entry, index) => { const extraPermissions = ALL_PERMISSIONS.filter((permission) => !ROLE_PERMISSIONS[entry.role].includes(permission)); return (
updateLegacyEntry(index, { password: event.target.value })} className="px-3 py-2 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] text-sm text-[var(--text-color)] focus:outline-none focus:border-[var(--accent-color)]" /> updateLegacyEntry(index, { name: event.target.value })} className="px-3 py-2 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)] text-sm text-[var(--text-color)] focus:outline-none focus:border-[var(--accent-color)]" />
{extraPermissions.length > 0 && (
{extraPermissions.map((permission) => ( ))}
)}
); })} {generatedLegacyAccounts && (
{generatedLegacyAccounts}
)}
)}
) )}
); }