diff --git a/README.md b/README.md index 6f96cc3..c6b6d72 100644 --- a/README.md +++ b/README.md @@ -109,31 +109,6 @@ - **Service Worker**:离线缓存和智能预加载 - **Server Components**:优化首屏加载性能 - **Client Components**:复杂交互和状态管理 -### 🔒 访问控制 - -KVideo 提供两个层级的访问保护: - -1. **全局访问保护 (Global)**: 通过设置环境变量 `ACCESS_PASSWORD` 启用。所有访问该实例的用户都必须输入此密码。适用于保护私有部署的实例。 -2. **本地设备锁 (Local)**: 在应用设置中手动开启。仅在当前浏览器/设备生效,用于防止他人直接操作你的设备。 - -> [!TIP] -> **永久授权**: 为了更好的平衡安全与体验,无论哪种密码,只要在当前设备输入正确一次,即可实现永久访问,无需每次进入都重新输入。 - ---- - -## ⚙️ 环境配置 (全局保护) - -如果需要为你的 KVideo 实例添加**全局访问密码**,请设置以下环境变量: - -| 变量名 | 必填 | 默认值 | 说明 | -|------|------|------|------| -| `ACCESS_PASSWORD` | 否 | - | 设置后,任何设备访问该实例都需要先进行身份验证。 | - -### 如何设置环境变量 - -- **Vercel**: 在项目设置 -> Environment Variables 中添加 `ACCESS_PASSWORD`。 -- **Docker**: 使用 `-e ACCESS_PASSWORD=你的密码` 运行容器,或在 `docker-compose.yml` 中配置。 -- **本地开发**: 在项目根目录创建 `.env.local` 文件并添加 `ACCESS_PASSWORD=你的密码`。 ## 🚀 快速部署 diff --git a/app/layout.tsx b/app/layout.tsx index 4d8235c..95b76d5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/ThemeProvider"; +import { Analytics } from "@vercel/analytics/react"; import { ServiceWorkerRegister } from "@/components/ServiceWorkerRegister"; import { PasswordGate } from "@/components/PasswordGate"; @@ -24,8 +25,6 @@ export const metadata: Metadata = { }, }; -export const runtime = 'edge'; - export default function RootLayout({ children, }: Readonly<{ @@ -41,6 +40,7 @@ export default function RootLayout({ {children} + diff --git a/app/page.tsx b/app/page.tsx index b76b80e..27d40ff 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,5 @@ 'use client'; -export const dynamic = 'force-dynamic'; - import { Suspense } from 'react'; import { SearchForm } from '@/components/search/SearchForm'; import { NoResults } from '@/components/search/NoResults'; diff --git a/app/settings/hooks/useSettingsPage.ts b/app/settings/hooks/useSettingsPage.ts index 9a9ef92..333bc47 100644 --- a/app/settings/hooks/useSettingsPage.ts +++ b/app/settings/hooks/useSettingsPage.ts @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react'; import { settingsStore, getDefaultSources, type SortOption } from '@/lib/store/settings-store'; import type { VideoSource } from '@/lib/types'; -import { isEnvPasswordRequired } from '@/lib/actions/auth'; export function useSettingsPage() { const [sources, setSources] = useState([]); @@ -15,7 +14,6 @@ export function useSettingsPage() { const [passwordAccess, setPasswordAccess] = useState(false); const [accessPasswords, setAccessPasswords] = useState([]); - const [envPasswordSet, setEnvPasswordSet] = useState(false); useEffect(() => { const settings = settingsStore.getSettings(); @@ -23,8 +21,6 @@ export function useSettingsPage() { setSortBy(settings.sortBy); setPasswordAccess(settings.passwordAccess); setAccessPasswords(settings.accessPasswords); - - isEnvPasswordRequired().then(setEnvPasswordSet); }, []); const handleSourcesChange = (newSources: VideoSource[]) => { @@ -166,6 +162,5 @@ export function useSettingsPage() { handleResetAll, editingSource, handleEditSource, - envPasswordSet, }; } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 923b90b..a1b9360 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -41,7 +41,6 @@ export default function SettingsPage() { editingSource, handleEditSource, setEditingSource, - envPasswordSet, } = useSettingsPage(); return ( @@ -57,7 +56,6 @@ export default function SettingsPage() { onToggle={handlePasswordToggle} onAdd={handleAddPassword} onRemove={handleRemovePassword} - envPasswordSet={envPasswordSet} /> {/* Source Management */} diff --git a/app/styles/base.css b/app/styles/base.css index 12efe4c..d3ba92f 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -11,22 +11,6 @@ html { scroll-behavior: auto; } -/* Accessibility & UX */ -button, -label, -a, -select, -summary, -[role="button"], -.clickable { - cursor: pointer; -} - -button:disabled, -[role="button"]:disabled { - cursor: not-allowed; -} - /* Focus Styles */ *:focus-visible { outline: 2px solid var(--accent-color); diff --git a/components/PasswordGate.tsx b/components/PasswordGate.tsx index a2a5ee0..c506969 100644 --- a/components/PasswordGate.tsx +++ b/components/PasswordGate.tsx @@ -3,14 +3,12 @@ import { useState, useEffect } from 'react'; import { settingsStore } from '@/lib/store/settings-store'; import { Lock } from 'lucide-react'; -import { verifyEnvPassword, isEnvPasswordRequired } from '@/lib/actions/auth'; -const ACCESS_GRANTED_KEY = 'kvideo-access-granted'; +const SESSION_UNLOCKED_KEY = 'kvideo-unlocked'; export function PasswordGate({ children }: { children: React.ReactNode }) { const [isLocked, setIsLocked] = useState(true); const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [isClient, setIsClient] = useState(false); @@ -19,59 +17,38 @@ export function PasswordGate({ children }: { children: React.ReactNode }) { checkLockStatus(); }, []); - const checkLockStatus = async () => { - const isAccessGranted = localStorage.getItem(ACCESS_GRANTED_KEY) === 'true'; - if (isAccessGranted) { + const checkLockStatus = () => { + const settings = settingsStore.getSettings(); + if (!settings.passwordAccess) { setIsLocked(false); - setLoading(false); return; } - const envRequired = await isEnvPasswordRequired(); - const settings = settingsStore.getSettings(); - - if (!envRequired && !settings.passwordAccess) { + const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true'; + if (isUnlocked) { setIsLocked(false); } else { setIsLocked(true); } - setLoading(false); }; - const handleUnlock = async (e: React.FormEvent) => { + const handleUnlock = (e: React.FormEvent) => { e.preventDefault(); - setLoading(true); - - // Check against ENV password first - const isEnvValid = await verifyEnvPassword(password); - if (isEnvValid) { - localStorage.setItem(ACCESS_GRANTED_KEY, 'true'); - setIsLocked(false); - setError(false); - setLoading(false); - return; - } - - // Check against local settings passwords const settings = settingsStore.getSettings(); if (settings.accessPasswords.includes(password)) { - localStorage.setItem(ACCESS_GRANTED_KEY, 'true'); + sessionStorage.setItem(SESSION_UNLOCKED_KEY, 'true'); setIsLocked(false); setError(false); - setLoading(false); - return; + } else { + setError(true); + // Shake animation trigger + const form = document.getElementById('password-form'); + form?.classList.add('animate-shake'); + setTimeout(() => form?.classList.remove('animate-shake'), 500); } - - // Invalid password - setError(true); - setLoading(false); - // Shake animation trigger - const form = document.getElementById('password-form'); - form?.classList.add('animate-shake'); - setTimeout(() => form?.classList.remove('animate-shake'), 500); }; - if (!isClient || loading) return null; // Prevent hydration mismatch and show nothing while checking + if (!isClient) return null; // Prevent hydration mismatch if (!isLocked) { return <>{children}; @@ -117,10 +94,9 @@ export function PasswordGate({ children }: { children: React.ReactNode }) { diff --git a/components/settings/PasswordSettings.tsx b/components/settings/PasswordSettings.tsx index 701a81e..7db988a 100644 --- a/components/settings/PasswordSettings.tsx +++ b/components/settings/PasswordSettings.tsx @@ -10,7 +10,6 @@ interface PasswordSettingsProps { onToggle: (enabled: boolean) => void; onAdd: (password: string) => void; onRemove: (password: string) => void; - envPasswordSet?: boolean; } export function PasswordSettings({ @@ -19,7 +18,6 @@ export function PasswordSettings({ onToggle, onAdd, onRemove, - envPasswordSet, }: PasswordSettingsProps) { const [newPassword, setNewPassword] = useState(''); const [error, setError] = useState(''); @@ -40,7 +38,7 @@ export function PasswordSettings({ }; return ( - +