'use client'; import { useState, useEffect } from 'react'; import { getSession, setSession } from '@/lib/store/auth-store'; import { useSubscriptionSync } from '@/lib/hooks/useSubscriptionSync'; import { hasStoredAppSetting, settingsStore } from '@/lib/store/settings-store'; import { useIPTVStore } from '@/lib/store/iptv-store'; import { Lock } from 'lucide-react'; /** * Sync IPTV sources from environment variable. * Format: JSON array [{name, url}] or comma-separated URLs. */ function syncIPTVSources(rawValue: string) { const iptvStore = useIPTVStore.getState(); let entries: { name: string; url: string }[] = []; // Try JSON try { const parsed = JSON.parse(rawValue); if (Array.isArray(parsed)) { entries = parsed.filter((item: any) => item && typeof item.url === 'string'); } } catch { // Try comma-separated URLs if (rawValue.includes('http')) { const urls = rawValue.split(',').map(u => u.trim()).filter(u => u.startsWith('http')); entries = urls.map((url, i) => ({ name: urls.length > 1 ? `直播源 ${i + 1}` : '直播源', url, })); } } iptvStore.syncBuiltinSources(entries); } /** * Sync merge sources setting from environment variable. * Value: 'true' or '1' to enable grouped display mode. */ 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); } } export function PasswordGate({ children, hasAuth: initialHasAuth }: { children: React.ReactNode, hasAuth: boolean }) { // Enable background subscription syncing globally useSubscriptionSync(); const [isLocked, setIsLocked] = useState(true); const [password, setPassword] = useState(''); const [error, setError] = useState(false); const [isClient, setIsClient] = useState(false); const [hasAuth, setHasAuth] = useState(initialHasAuth); const [persistSession, setPersistSession] = useState(true); const [isValidating, setIsValidating] = useState(false); useEffect(() => { let mounted = true; const init = async () => { // Check if already has a valid session const session = getSession(); const isAuthenticated = !!session; // Initial fast check const localLocked = initialHasAuth && !isAuthenticated; if (mounted) { setIsLocked(localLocked); setIsClient(true); } // Fetch remote config & sync try { const res = await fetch('/api/auth'); if (!res.ok) throw new Error('Failed to fetch auth config'); const data = await res.json(); if (mounted) { setHasAuth(data.hasAuth); setPersistSession(data.persistSession); applyRuntimeConfig(data); // Re-evaluate lock status with confirmed server state const confirmLocked = data.hasAuth && !isAuthenticated; setIsLocked(confirmLocked); } } catch (e) { console.error("PasswordGate init failed:", e); } }; init(); return () => { mounted = false; }; }, [initialHasAuth]); const handleUnlock = async (e: React.FormEvent) => { e.preventDefault(); setIsValidating(true); try { const res = await fetch('/api/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password }), }); const data = await res.json(); if (data.valid) { applyRuntimeConfig(data); setSession({ profileId: data.profileId, name: data.name, role: data.role, customPermissions: data.customPermissions, }, data.persistSession ?? persistSession); // Reload to re-initialize stores with profiled keys window.location.reload(); return; } } catch { // API error } // Password didn't match setError(true); setIsValidating(false); const form = document.getElementById('password-form'); form?.classList.add('animate-shake'); setTimeout(() => form?.classList.remove('animate-shake'), 500); }; if (!isClient) return null; // Prevent hydration mismatch if (!isLocked) { return <>{children}>; } return (