'use client'; import { useState, useEffect } from 'react'; import { Lock, User } from 'lucide-react'; import { clearSession, getSession, setSession, type AuthSession } from '@/lib/store/auth-store'; import { resolvePasswordGateState } from '@/lib/auth/password-gate-state'; import { useSubscriptionSync } from '@/lib/hooks/useSubscriptionSync'; import { hasStoredAppSetting, settingsStore } from '@/lib/store/settings-store'; import { useIPTVStore } from '@/lib/store/iptv-store'; type LoginMode = 'none' | 'legacy_password' | 'managed'; function syncIPTVSources(rawValue: string) { const iptvStore = useIPTVStore.getState(); let entries: { name: string; url: string }[] = []; try { const parsed = JSON.parse(rawValue); if (Array.isArray(parsed)) { entries = parsed.filter((item: unknown): item is { name: string; url: string } => { if (!item || typeof item !== 'object') return false; const candidate = item as { name?: unknown; url?: unknown }; return typeof candidate.url === 'string'; }); } } catch { if (rawValue.includes('http')) { const urls = rawValue.split(',').map((value) => value.trim()).filter((value) => value.startsWith('http')); entries = urls.map((url, index) => ({ name: urls.length > 1 ? `直播源 ${index + 1}` : '直播源', url, })); } } iptvStore.syncBuiltinSources(entries); } function syncMergeSources(rawValue: string) { const enabled = rawValue === 'true' || rawValue === '1'; if (!enabled) return; const settings = settingsStore.getSettings(); if (settings.searchDisplayMode !== 'grouped') { settingsStore.saveSettings({ ...settings, searchDisplayMode: 'grouped', }); } } function syncDanmakuApiUrl(rawValue: string) { if (!rawValue || hasStoredAppSetting('danmakuApiUrl')) return; const settings = settingsStore.getSettings(); if (settings.danmakuApiUrl !== rawValue) { settingsStore.saveSettings({ ...settings, danmakuApiUrl: rawValue, }); } } function applyRuntimeConfig(data: { subscriptionSources?: string; iptvSources?: string; mergeSources?: string; danmakuApiUrl?: string; }) { if (data.subscriptionSources) { settingsStore.syncEnvSubscriptions(data.subscriptionSources); } if (data.iptvSources) { syncIPTVSources(data.iptvSources); } if (data.mergeSources) { syncMergeSources(data.mergeSources); } if (data.danmakuApiUrl) { syncDanmakuApiUrl(data.danmakuApiUrl); } } function toAuthSession(session: { accountId: string; profileId: string; username?: string; name: string; role: AuthSession['role']; customPermissions?: AuthSession['customPermissions']; mode?: AuthSession['mode']; }): AuthSession { return { accountId: session.accountId, profileId: session.profileId, username: session.username, name: session.name, role: session.role, customPermissions: session.customPermissions, mode: session.mode, }; } export function PasswordGate({ children, hasAuth: initialHasAuth, }: { children: React.ReactNode; hasAuth: boolean; }) { useSubscriptionSync(); const [isLocked, setIsLocked] = useState(true); const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [isClient, setIsClient] = useState(false); const [persistSession, setPersistSession] = useState(true); const [isValidating, setIsValidating] = useState(false); const [loginMode, setLoginMode] = useState('none'); useEffect(() => { let mounted = true; const init = async () => { const mirroredSession = getSession(); try { const [configRes, sessionRes] = await Promise.all([ fetch('/api/auth'), fetch('/api/auth/session'), ]); if (!configRes.ok) { throw new Error('Failed to fetch auth config'); } const config = await configRes.json(); const sessionStatus = sessionRes.ok ? await sessionRes.json() : { authenticated: false, session: null }; if (!mounted) return; setPersistSession(config.persistSession); setLoginMode(config.loginMode || 'none'); applyRuntimeConfig(config); const serverSession = sessionStatus.authenticated && sessionStatus.session ? toAuthSession(sessionStatus.session) : null; const gateState = resolvePasswordGateState({ hasAuth: !!config.hasAuth, serverSession, mirroredSession, persistSession: config.persistSession, }); if (gateState.action === 'unlock-session') { setSession(gateState.session, gateState.persistSession); setIsLocked(false); setIsClient(true); return; } if (gateState.action === 'unlock-public') { setIsLocked(false); setIsClient(true); return; } if (gateState.clearMirroredSession) { clearSession(); } setIsLocked(true); setIsClient(true); } catch { if (!mounted) return; setIsLocked(initialHasAuth && !mirroredSession); setIsClient(true); } }; init(); return () => { mounted = false; }; }, [initialHasAuth]); const handleUnlock = async (event: React.FormEvent) => { event.preventDefault(); setIsValidating(true); setError(''); try { const response = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: loginMode === 'managed' ? username : undefined, password, }), }); const data = await response.json(); if (response.status === 503) { setError('认证存储暂时不可用,请检查 Upstash Redis 配置'); setIsValidating(false); return; } if (data.valid && data.session) { setSession(toAuthSession(data.session), data.persistSession ?? persistSession); setIsLocked(false); setIsClient(true); setIsValidating(false); return; } } catch { // Ignore network errors and show the same message as invalid credentials. } setError(loginMode === 'managed' ? '用户名或密码错误' : '密码错误'); setIsValidating(false); const form = document.getElementById('password-form'); form?.classList.add('animate-shake'); setTimeout(() => form?.classList.remove('animate-shake'), 500); }; if (!isClient) return null; if (!isLocked) { return <>{children}; } const showManagedFields = loginMode === 'managed'; return (

访问受限

{showManagedFields ? '请输入用户名和密码以继续' : '请输入访问密码以继续'}

{showManagedFields && (
{ setUsername(event.target.value); setError(''); }} placeholder="输入用户名..." className="w-full pl-11 pr-4 py-3 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border 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)]" autoComplete="username" autoFocus />
)}
{ setPassword(event.target.value); setError(''); }} placeholder={showManagedFields ? '输入密码...' : '输入密码...'} 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={!showManagedFields} autoComplete={showManagedFields ? 'current-password' : 'off'} /> {error && (

{error}

)}
); }