diff --git a/app/api/auth/route.ts b/app/api/auth/route.ts new file mode 100644 index 0000000..f831e8f --- /dev/null +++ b/app/api/auth/route.ts @@ -0,0 +1,107 @@ +/** + * Auth API Route + * Handles authentication with role-based accounts + */ + +import { NextRequest, NextResponse } from 'next/server'; + +export const runtime = 'edge'; + +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ''; +const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || ''; +const ACCOUNTS = process.env.ACCOUNTS || ''; +const PERSIST_SESSION = process.env.PERSIST_SESSION !== 'false'; // default true +const SUBSCRIPTION_SOURCES = process.env.SUBSCRIPTION_SOURCES || process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES || ''; + +// Backward compat: ACCESS_PASSWORD acts as ADMIN_PASSWORD if ADMIN_PASSWORD is not set +const effectiveAdminPassword = ADMIN_PASSWORD || ACCESS_PASSWORD; + +interface AccountEntry { + password: string; + name: string; + role: 'admin' | 'viewer'; +} + +function parseAccounts(): AccountEntry[] { + if (!ACCOUNTS) return []; + + return ACCOUNTS.split(',') + .map(entry => entry.trim()) + .filter(entry => entry.length > 0) + .map(entry => { + const parts = entry.split(':'); + if (parts.length < 2) return null; + const [password, name, role] = parts; + return { + password: password.trim(), + name: name.trim(), + role: (role?.trim() === 'admin' ? 'admin' : 'viewer') as 'admin' | 'viewer', + }; + }) + .filter((a): a is AccountEntry => a !== null && a.password.length > 0 && a.name.length > 0); +} + +/** + * Generate a deterministic profileId from password using SHA-256. + * Uses a salt to avoid rainbow table attacks. + */ +async function generateProfileId(password: string): Promise { + const salt = 'kvideo-profile-salt-v1'; + const data = new TextEncoder().encode(password + salt); + const hash = await crypto.subtle.digest('SHA-256', data); + const hashArray = Array.from(new Uint8Array(hash)); + // Use first 8 bytes (16 hex chars) for a compact but unique ID + return hashArray.slice(0, 8).map(b => b.toString(16).padStart(2, '0')).join(''); +} + +export async function GET() { + const hasAuth = !!(effectiveAdminPassword || ACCOUNTS); + + return NextResponse.json({ + hasAuth, + persistSession: PERSIST_SESSION, + subscriptionSources: SUBSCRIPTION_SOURCES, + }); +} + +export async function POST(request: NextRequest) { + try { + const { password } = await request.json(); + + if (!password || typeof password !== 'string') { + return NextResponse.json({ valid: false, message: 'Password required' }, { status: 400 }); + } + + // 1. Check admin password + if (effectiveAdminPassword && password === effectiveAdminPassword) { + const profileId = await generateProfileId(password); + return NextResponse.json({ + valid: true, + name: '管理员', + role: 'admin', + profileId, + persistSession: PERSIST_SESSION, + }); + } + + // 2. Check ACCOUNTS entries + const accounts = parseAccounts(); + for (const account of accounts) { + if (password === account.password) { + const profileId = await generateProfileId(password); + return NextResponse.json({ + valid: true, + name: account.name, + role: account.role, + profileId, + persistSession: PERSIST_SESSION, + }); + } + } + + // 3. No match + return NextResponse.json({ valid: false }); + } catch { + return NextResponse.json({ valid: false, message: 'Invalid request' }, { status: 400 }); + } +} diff --git a/app/api/config/route.ts b/app/api/config/route.ts index dbbbe21..57bc4c5 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -1,45 +1,17 @@ /** - * Config API Route - * Exposes configuration status (never actual values) to the client + * Config API Route (Simplified) + * Only returns non-auth configuration now. + * Auth has moved to /api/auth. */ -import { NextRequest, NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; export const runtime = 'edge'; -const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || ''; -const SETTINGS_PASSWORD = process.env.SETTINGS_PASSWORD || ''; -const PERSIST_PASSWORD = process.env.PERSIST_PASSWORD !== 'false'; const SUBSCRIPTION_SOURCES = process.env.SUBSCRIPTION_SOURCES || process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES || ''; export async function GET() { return NextResponse.json({ - hasEnvPassword: ACCESS_PASSWORD.length > 0, - hasEnvSettingsPassword: SETTINGS_PASSWORD.length > 0, - persistPassword: PERSIST_PASSWORD, subscriptionSources: SUBSCRIPTION_SOURCES, }); } - -export async function POST(request: NextRequest) { - try { - const { password, type } = await request.json(); - - if (type === 'settings') { - if (!SETTINGS_PASSWORD) { - return NextResponse.json({ valid: false, message: 'No env settings password set' }); - } - const valid = password === SETTINGS_PASSWORD; - return NextResponse.json({ valid }); - } - - if (!ACCESS_PASSWORD) { - return NextResponse.json({ valid: false, message: 'No env password set' }); - } - - const valid = password === ACCESS_PASSWORD; - return NextResponse.json({ valid }); - } catch { - return NextResponse.json({ valid: false, message: 'Invalid request' }, { status: 400 }); - } -} diff --git a/app/layout.tsx b/app/layout.tsx index d8ed1e0..bdc8f34 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -95,7 +95,7 @@ export default function RootLayout({ suppressHydrationWarning > - + {children} diff --git a/app/premium/settings/page.tsx b/app/premium/settings/page.tsx index d2a6139..3cb96a2 100644 --- a/app/premium/settings/page.tsx +++ b/app/premium/settings/page.tsx @@ -4,7 +4,7 @@ import { AddSourceModal } from '@/components/settings/AddSourceModal'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { PremiumSourceSettings } from '@/components/settings/PremiumSourceSettings'; import { SettingsHeader } from '@/components/settings/SettingsHeader'; -import { SettingsPasswordGate } from '@/components/SettingsPasswordGate'; +import { AdminGate } from '@/components/AdminGate'; import { usePremiumSettingsPage } from './hooks/usePremiumSettingsPage'; import Link from 'next/link'; @@ -24,7 +24,7 @@ export default function PremiumSettingsPage() { } = usePremiumSettingsPage(); return ( - +
{/* Custom Header for Secret Settings */} @@ -83,6 +83,6 @@ export default function PremiumSettingsPage() { onCancel={() => setIsRestoreDefaultsDialogOpen(false)} />
- + ); } diff --git a/app/settings/hooks/useSettingsPage.ts b/app/settings/hooks/useSettingsPage.ts index a37e9b4..b5861e9 100644 --- a/app/settings/hooks/useSettingsPage.ts +++ b/app/settings/hooks/useSettingsPage.ts @@ -19,14 +19,6 @@ export function useSettingsPage() { const [isRestoreDefaultsDialogOpen, setIsRestoreDefaultsDialogOpen] = useState(false); const [editingSource, setEditingSource] = useState(null); - const [passwordAccess, setPasswordAccess] = useState(false); - const [accessPasswords, setAccessPasswords] = useState([]); - const [envPasswordSet, setEnvPasswordSet] = useState(false); - - const [settingsPasswordEnabled, setSettingsPasswordEnabled] = useState(false); - const [settingsPasswords, setSettingsPasswords] = useState([]); - const [envSettingsPasswordSet, setEnvSettingsPasswordSet] = useState(false); - // Display settings const [realtimeLatency, setRealtimeLatency] = useState(false); const [searchDisplayMode, setSearchDisplayMode] = useState('normal'); @@ -39,27 +31,11 @@ export function useSettingsPage() { setSources(settings.sources || []); setSubscriptions(settings.subscriptions || []); setSortBy(settings.sortBy); - setPasswordAccess(settings.passwordAccess); - setAccessPasswords(settings.accessPasswords); - setSettingsPasswordEnabled(settings.settingsPasswordEnabled); - setSettingsPasswords(settings.settingsPasswords); setRealtimeLatency(settings.realtimeLatency); setSearchDisplayMode(settings.searchDisplayMode); setFullscreenType(settings.fullscreenType); setProxyMode(settings.proxyMode); setRememberScrollPosition(settings.rememberScrollPosition); - - // Fetch env password status - fetch('/api/config') - .then(res => res.json()) - .then(data => { - setEnvPasswordSet(data.hasEnvPassword); - setEnvSettingsPasswordSet(data.hasEnvSettingsPassword); - }) - .catch(() => { - setEnvPasswordSet(false); - setEnvSettingsPasswordSet(false); - }); }, []); const handleSourcesChange = (newSources: VideoSource[]) => { @@ -70,10 +46,6 @@ export function useSettingsPage() { sources: newSources, sortBy, subscriptions, - searchHistory: true, - watchHistory: true, - passwordAccess, - accessPasswords }); }; @@ -98,85 +70,6 @@ export function useSettingsPage() { ...currentSettings, sources, sortBy: newSort, - searchHistory: true, - watchHistory: true, - passwordAccess, - accessPasswords - }); - }; - - const handlePasswordToggle = (enabled: boolean) => { - setPasswordAccess(enabled); - const currentSettings = settingsStore.getSettings(); - settingsStore.saveSettings({ - ...currentSettings, - sources, - sortBy, - searchHistory: true, - watchHistory: true, - passwordAccess: enabled, - accessPasswords - }); - }; - - const handleAddPassword = (password: string) => { - const updated = [...accessPasswords, password]; - setAccessPasswords(updated); - const currentSettings = settingsStore.getSettings(); - settingsStore.saveSettings({ - ...currentSettings, - sources, - sortBy, - searchHistory: true, - watchHistory: true, - passwordAccess, - accessPasswords: updated - }); - }; - - const handleRemovePassword = (password: string) => { - const updated = accessPasswords.filter(p => p !== password); - setAccessPasswords(updated); - const currentSettings = settingsStore.getSettings(); - settingsStore.saveSettings({ - ...currentSettings, - sources, - sortBy, - searchHistory: true, - watchHistory: true, - passwordAccess, - accessPasswords: updated - }); - }; - - const handleSettingsPasswordToggle = (enabled: boolean) => { - setSettingsPasswordEnabled(enabled); - const currentSettings = settingsStore.getSettings(); - settingsStore.saveSettings({ - ...currentSettings, - settingsPasswordEnabled: enabled, - }); - }; - - const handleAddSettingsPassword = (password: string) => { - const updated = [...settingsPasswords, password]; - setSettingsPasswords(updated); - const currentSettings = settingsStore.getSettings(); - settingsStore.saveSettings({ - ...currentSettings, - settingsPasswordEnabled, - settingsPasswords: updated, - }); - }; - - const handleRemoveSettingsPassword = (password: string) => { - const updated = settingsPasswords.filter(p => p !== password); - setSettingsPasswords(updated); - const currentSettings = settingsStore.getSettings(); - settingsStore.saveSettings({ - ...currentSettings, - settingsPasswordEnabled, - settingsPasswords: updated, }); }; @@ -199,8 +92,6 @@ export function useSettingsPage() { setSources(settings.sources); setSortBy(settings.sortBy); setSubscriptions(settings.subscriptions || []); - setPasswordAccess(settings.passwordAccess); - setAccessPasswords(settings.accessPasswords); // Reload to apply changes setTimeout(() => window.location.reload(), 1000); @@ -371,12 +262,6 @@ export function useSettingsPage() { sources, subscriptions, sortBy, - passwordAccess, - accessPasswords, - envPasswordSet, - settingsPasswordEnabled, - settingsPasswords, - envSettingsPasswordSet, realtimeLatency, searchDisplayMode, isAddModalOpen, @@ -393,18 +278,12 @@ export function useSettingsPage() { handleSourcesChange, handleAddSource, handleSortChange, - handlePasswordToggle, - handleAddPassword, - handleRemovePassword, - handleSettingsPasswordToggle, - handleAddSettingsPassword, - handleRemoveSettingsPassword, handleExport, - handleImportFile, // Renamed from handleImport - handleImportLink, // New - handleAddSubscription, // New - handleRemoveSubscription, // New - handleRefreshSubscription, // New + handleImportFile, + handleImportLink, + handleAddSubscription, + handleRemoveSubscription, + handleRefreshSubscription, handleRestoreDefaults, handleResetAll, editingSource, diff --git a/app/settings/page.tsx b/app/settings/page.tsx index d37e27d..b33de34 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,6 +1,5 @@ 'use client'; -import { Suspense } from 'react'; import { AddSourceModal } from '@/components/settings/AddSourceModal'; import { ExportModal } from '@/components/settings/ExportModal'; import { ImportModal } from '@/components/settings/ImportModal'; @@ -8,24 +7,17 @@ import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { SourceSettings } from '@/components/settings/SourceSettings'; import { SortSettings } from '@/components/settings/SortSettings'; import { DataSettings } from '@/components/settings/DataSettings'; -import { PasswordSettings } from '@/components/settings/PasswordSettings'; -import { SettingsPasswordSettings } from '@/components/settings/SettingsPasswordSettings'; +import { AccountSettings } from '@/components/settings/AccountSettings'; import { DisplaySettings } from '@/components/settings/DisplaySettings'; import { PlayerSettings } from '@/components/settings/PlayerSettings'; import { SettingsHeader } from '@/components/settings/SettingsHeader'; -import { SettingsPasswordGate } from '@/components/SettingsPasswordGate'; +import { AdminGate } from '@/components/AdminGate'; import { useSettingsPage } from './hooks/useSettingsPage'; export default function SettingsPage() { const { sources, sortBy, - passwordAccess, - accessPasswords, - envPasswordSet, - settingsPasswordEnabled, - settingsPasswords, - envSettingsPasswordSet, realtimeLatency, searchDisplayMode, fullscreenType, @@ -42,12 +34,6 @@ export default function SettingsPage() { handleSourcesChange, handleAddSource, handleSortChange, - handlePasswordToggle, - handleAddPassword, - handleRemovePassword, - handleSettingsPasswordToggle, - handleAddSettingsPassword, - handleRemoveSettingsPassword, handleExport, handleImportFile, handleImportLink, @@ -70,12 +56,15 @@ export default function SettingsPage() { } = useSettingsPage(); return ( - +
{/* Header */} + {/* Account Settings */} + + {/* Player Settings */} - {/* Password Settings */} - - - {/* Settings Password Protection */} - - {/* Display Settings */}
- + ); } - diff --git a/components/AdminGate.tsx b/components/AdminGate.tsx new file mode 100644 index 0000000..0cbbb2b --- /dev/null +++ b/components/AdminGate.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { isAdmin, getSession } from '@/lib/store/auth-store'; +import { ShieldAlert, User } from 'lucide-react'; + +function ViewerNotice() { + const session = getSession(); + + return ( +
+
+
+
+ +
+ +
+

权限不足

+

仅管理员可修改设置

+
+ + {session && ( +
+ + 当前用户:{session.name} + + 观众 + +
+ )} + + + 返回首页 + +
+
+
+ ); +} + +export function AdminGate({ children, fallback }: { children: React.ReactNode; fallback?: React.ReactNode }) { + if (!isAdmin()) return fallback || ; + return <>{children}; +} diff --git a/components/PasswordGate.tsx b/components/PasswordGate.tsx index a9039b3..b509ee0 100644 --- a/components/PasswordGate.tsx +++ b/components/PasswordGate.tsx @@ -1,13 +1,12 @@ 'use client'; import { useState, useEffect } from 'react'; -import { settingsStore } from '@/lib/store/settings-store'; +import { getSession, setSession } from '@/lib/store/auth-store'; import { useSubscriptionSync } from '@/lib/hooks/useSubscriptionSync'; +import { settingsStore } from '@/lib/store/settings-store'; import { Lock } from 'lucide-react'; -const SESSION_UNLOCKED_KEY = 'kvideo-unlocked'; - -export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }: { children: React.ReactNode, hasEnvPassword: boolean }) { +export function PasswordGate({ children, hasAuth: initialHasAuth }: { children: React.ReactNode, hasAuth: boolean }) { // Enable background subscription syncing globally useSubscriptionSync(); @@ -15,58 +14,43 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword } const [password, setPassword] = useState(''); const [error, setError] = useState(false); const [isClient, setIsClient] = useState(false); - const [hasEnvPassword, setHasEnvPassword] = useState(initialHasEnvPassword); - const [persistEnabled, setPersistEnabled] = useState(true); + const [hasAuth, setHasAuth] = useState(initialHasAuth); + const [persistSession, setPersistSession] = useState(true); const [isValidating, setIsValidating] = useState(false); useEffect(() => { let mounted = true; const init = async () => { - const settings = settingsStore.getSettings(); + // Check if already has a valid session + const session = getSession(); + const isAuthenticated = !!session; - // Check both storage if persistence might be enabled - const getUnlockedState = (canPersist: boolean) => { - const sessionUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; - if (!canPersist) return sessionUnlocked; - const localUnlocked = localStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; - return sessionUnlocked || localUnlocked; - }; + // Initial fast check + const localLocked = initialHasAuth && !isAuthenticated; + if (mounted) { + setIsLocked(localLocked); + setIsClient(true); + } - // 1. Initial local check (fast) - // Determine if the app SHOULD be protected - const isProtected = (settings.passwordAccess && settings.accessPasswords.length > 0) || initialHasEnvPassword; - - // Default to canPersist = true for first check if not sure - const isUnlocked = getUnlockedState(true); - const localLocked = isProtected && !isUnlocked; - if (mounted) setIsLocked(localLocked); - if (mounted) setIsClient(true); - - // 2. Fetch remote config & sync + // Fetch remote config & sync try { - const res = await fetch('/api/config'); - if (!res.ok) throw new Error('Failed to fetch config'); + const res = await fetch('/api/auth'); + if (!res.ok) throw new Error('Failed to fetch auth config'); const data = await res.json(); if (mounted) { - setHasEnvPassword(data.hasEnvPassword); - setPersistEnabled(data.persistPassword); + setHasAuth(data.hasAuth); + setPersistSession(data.persistSession); - // CRITICAL: Sync subscriptions immediately + // Sync subscriptions if (data.subscriptionSources) { - console.log('Syncing env subscriptions:', data.subscriptionSources); settingsStore.syncEnvSubscriptions(data.subscriptionSources); } // Re-evaluate lock status with confirmed server state - // Persistence only works if hasEnvPassword is true - const canPersist = data.hasEnvPassword && data.persistPassword; - const finalUnlocked = getUnlockedState(canPersist); - - const isProtectedNow = (settings.passwordAccess && settings.accessPasswords.length > 0) || data.hasEnvPassword; - const confirmLocked = isProtectedNow && !finalUnlocked; + const confirmLocked = data.hasAuth && !isAuthenticated; setIsLocked(confirmLocked); } } catch (e) { @@ -76,73 +60,34 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword } init(); - return () => { - mounted = false; - }; - }, [initialHasEnvPassword]); - - // Subscribe to settings changes (real-time updates) - useEffect(() => { - const handleSettingsUpdate = () => { - const settings = settingsStore.getSettings(); - const canPersist = hasEnvPassword && persistEnabled; - const isUnlocked = (sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true') || - (canPersist && localStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'); - - const isProtected = (settings.passwordAccess && settings.accessPasswords.length > 0) || hasEnvPassword; - - if (!isProtected) { - setIsLocked(false); - } else if (!isUnlocked) { - setIsLocked(true); - } - }; - - const unsubscribe = settingsStore.subscribe(handleSettingsUpdate); - return () => unsubscribe(); - }, [hasEnvPassword, persistEnabled]); - - + return () => { mounted = false; }; + }, [initialHasAuth]); const handleUnlock = async (e: React.FormEvent) => { e.preventDefault(); setIsValidating(true); - const settings = settingsStore.getSettings(); - const canPersist = hasEnvPassword && persistEnabled; + try { + const res = await fetch('/api/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + const data = await res.json(); - const setUnlocked = () => { - if (canPersist) { - localStorage.setItem(SESSION_UNLOCKED_KEY, 'true'); - } - sessionStorage.setItem(SESSION_UNLOCKED_KEY, 'true'); - setIsLocked(false); - setError(false); - setIsValidating(false); - }; - - // First check local passwords - if (settings.accessPasswords.includes(password)) { - setUnlocked(); - return; - } - - // Then check env password via API - if (hasEnvPassword) { - try { - const res = await fetch('/api/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password }), - }); - const data = await res.json(); - if (data.valid) { - setUnlocked(); - return; - } - } catch { - // API error + if (data.valid) { + setSession({ + profileId: data.profileId, + name: data.name, + role: data.role, + }, data.persistSession ?? persistSession); + + // Reload to re-initialize stores with profiled keys + window.location.reload(); + return; } + } catch { + // API error } // Password didn't match @@ -199,9 +144,10 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
diff --git a/components/SettingsPasswordGate.tsx b/components/SettingsPasswordGate.tsx deleted file mode 100644 index 3011dc0..0000000 --- a/components/SettingsPasswordGate.tsx +++ /dev/null @@ -1,182 +0,0 @@ -'use client'; - -import { useState, useEffect } from 'react'; -import { settingsStore } from '@/lib/store/settings-store'; -import { Lock } from 'lucide-react'; - -const SESSION_KEY = 'kvideo-settings-unlocked'; - -export function SettingsPasswordGate({ children }: { children: React.ReactNode }) { - const [isLocked, setIsLocked] = useState(true); - const [password, setPassword] = useState(''); - const [error, setError] = useState(false); - const [isClient, setIsClient] = useState(false); - const [hasEnvSettingsPassword, setHasEnvSettingsPassword] = useState(false); - const [isValidating, setIsValidating] = useState(false); - - useEffect(() => { - let mounted = true; - - const init = async () => { - const settings = settingsStore.getSettings(); - const sessionUnlocked = sessionStorage.getItem(SESSION_KEY) === 'true'; - - const isProtected = (settings.settingsPasswordEnabled && settings.settingsPasswords.length > 0); - const localLocked = isProtected && !sessionUnlocked; - - if (mounted) { - setIsLocked(localLocked); - setIsClient(true); - } - - // Fetch remote config for env settings password - try { - const res = await fetch('/api/config'); - if (!res.ok) throw new Error('Failed to fetch config'); - const data = await res.json(); - - if (mounted) { - setHasEnvSettingsPassword(data.hasEnvSettingsPassword); - - const isProtectedNow = (settings.settingsPasswordEnabled && settings.settingsPasswords.length > 0) || data.hasEnvSettingsPassword; - setIsLocked(isProtectedNow && !sessionUnlocked); - } - } catch (e) { - console.error('SettingsPasswordGate init failed:', e); - } - }; - - init(); - - return () => { mounted = false; }; - }, []); - - // Subscribe to settings changes (auto-unlock if all passwords removed) - useEffect(() => { - const handleSettingsUpdate = () => { - const settings = settingsStore.getSettings(); - const sessionUnlocked = sessionStorage.getItem(SESSION_KEY) === 'true'; - const isProtected = (settings.settingsPasswordEnabled && settings.settingsPasswords.length > 0) || hasEnvSettingsPassword; - - if (!isProtected) { - setIsLocked(false); - } else if (!sessionUnlocked) { - setIsLocked(true); - } - }; - - const unsubscribe = settingsStore.subscribe(handleSettingsUpdate); - return () => unsubscribe(); - }, [hasEnvSettingsPassword]); - - const handleUnlock = async (e: React.FormEvent) => { - e.preventDefault(); - setIsValidating(true); - - const settings = settingsStore.getSettings(); - - const setUnlocked = () => { - sessionStorage.setItem(SESSION_KEY, 'true'); - setIsLocked(false); - setError(false); - setIsValidating(false); - }; - - // Check local settings passwords first - if (settings.settingsPasswords.includes(password)) { - setUnlocked(); - return; - } - - // Then check env password via API - if (hasEnvSettingsPassword) { - try { - const res = await fetch('/api/config', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password, type: 'settings' }), - }); - const data = await res.json(); - if (data.valid) { - setUnlocked(); - return; - } - } catch { - // API error - } - } - - // Password didn't match - setError(true); - setIsValidating(false); - const form = document.getElementById('settings-password-form'); - form?.classList.add('animate-shake'); - setTimeout(() => form?.classList.remove('animate-shake'), 500); - }; - - if (!isClient) return null; - - if (!isLocked) { - return <>{children}; - } - - return ( -
-
-
-
- -
- -
-

设置已锁定

-

请输入设置密码以继续

-
- -
-
- { - setPassword(e.target.value); - setError(false); - }} - placeholder="输入密码..." - className={`w-full px-4 py-3 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border ${error ? 'border-red-500' : 'border-[var(--glass-border)]' - } focus:outline-none focus:border-[var(--accent-color)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)] transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) text-[var(--text-color)] placeholder-[var(--text-color-secondary)]`} - autoFocus - /> - {error && ( -

- 密码错误 -

- )} -
- - -
-
-
- -
- ); -} diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx index 1d839c2..d7c5031 100644 --- a/components/layout/Navbar.tsx +++ b/components/layout/Navbar.tsx @@ -1,8 +1,13 @@ +'use client'; + +import { useState, useEffect } from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ThemeSwitcher } from '@/components/ThemeSwitcher'; import { Icons } from '@/components/ui/Icon'; import { siteConfig } from '@/lib/config/site-config'; +import { getSession, clearSession, type AuthSession } from '@/lib/store/auth-store'; +import { LogOut } from 'lucide-react'; interface NavbarProps { onReset: () => void; @@ -11,6 +16,16 @@ interface NavbarProps { export function Navbar({ onReset, isPremiumMode = false }: NavbarProps) { const settingsHref = isPremiumMode ? '/premium/settings' : '/settings'; + const [session, setSessionState] = useState(null); + + useEffect(() => { + setSessionState(getSession()); + }, []); + + const handleLogout = () => { + clearSession(); + window.location.reload(); + }; return (