diff --git a/README.md b/README.md
index c6b6d72..2d9d48e 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,27 @@
- **Service Worker**:离线缓存和智能预加载
- **Server Components**:优化首屏加载性能
- **Client Components**:复杂交互和状态管理
+### 🔒 访问控制
+
+- **环境级密码**:通过设置环境变量启用全局访问密码
+- **永久授权**:输入一次密码即可永久访问,无需重复验证
+- **本地管理**:支持在应用设置中添加多个自定义访问密码
+
+---
+
+## ⚙️ 环境配置
+
+如果需要为你的 KVideo 实例添加访问控制,可以设置以下环境变量:
+
+| 变量名 | 必填 | 默认值 | 说明 |
+|------|------|------|------|
+| `ACCESS_PASSWORD` | 否 | - | 设置后,所有访问者必须输入此密码才能使用应用。 |
+
+### 如何设置环境变量
+
+- **Vercel**: 在项目设置 -> Environment Variables 中添加 `ACCESS_PASSWORD`。
+- **Docker**: 使用 `-e ACCESS_PASSWORD=你的密码` 运行容器,或在 `docker-compose.yml` 中配置。
+- **本地开发**: 在项目根目录创建 `.env.local` 文件并添加 `ACCESS_PASSWORD=你的密码`。
## 🚀 快速部署
diff --git a/components/PasswordGate.tsx b/components/PasswordGate.tsx
index c506969..a2a5ee0 100644
--- a/components/PasswordGate.tsx
+++ b/components/PasswordGate.tsx
@@ -3,12 +3,14 @@
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 SESSION_UNLOCKED_KEY = 'kvideo-unlocked';
+const ACCESS_GRANTED_KEY = 'kvideo-access-granted';
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);
@@ -17,38 +19,59 @@ export function PasswordGate({ children }: { children: React.ReactNode }) {
checkLockStatus();
}, []);
- const checkLockStatus = () => {
- const settings = settingsStore.getSettings();
- if (!settings.passwordAccess) {
+ const checkLockStatus = async () => {
+ const isAccessGranted = localStorage.getItem(ACCESS_GRANTED_KEY) === 'true';
+ if (isAccessGranted) {
setIsLocked(false);
+ setLoading(false);
return;
}
- const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true';
- if (isUnlocked) {
+ const envRequired = await isEnvPasswordRequired();
+ const settings = settingsStore.getSettings();
+
+ if (!envRequired && !settings.passwordAccess) {
setIsLocked(false);
} else {
setIsLocked(true);
}
+ setLoading(false);
};
- const handleUnlock = (e: React.FormEvent) => {
+ const handleUnlock = async (e: React.FormEvent) => {
e.preventDefault();
- const settings = settingsStore.getSettings();
- if (settings.accessPasswords.includes(password)) {
- sessionStorage.setItem(SESSION_UNLOCKED_KEY, 'true');
+ setLoading(true);
+
+ // Check against ENV password first
+ const isEnvValid = await verifyEnvPassword(password);
+ if (isEnvValid) {
+ localStorage.setItem(ACCESS_GRANTED_KEY, 'true');
setIsLocked(false);
setError(false);
- } else {
- setError(true);
- // Shake animation trigger
- const form = document.getElementById('password-form');
- form?.classList.add('animate-shake');
- setTimeout(() => form?.classList.remove('animate-shake'), 500);
+ setLoading(false);
+ return;
}
+
+ // Check against local settings passwords
+ const settings = settingsStore.getSettings();
+ if (settings.accessPasswords.includes(password)) {
+ localStorage.setItem(ACCESS_GRANTED_KEY, 'true');
+ setIsLocked(false);
+ setError(false);
+ setLoading(false);
+ return;
+ }
+
+ // 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) return null; // Prevent hydration mismatch
+ if (!isClient || loading) return null; // Prevent hydration mismatch and show nothing while checking
if (!isLocked) {
return <>{children}>;
@@ -94,9 +117,10 @@ export function PasswordGate({ children }: { children: React.ReactNode }) {
diff --git a/lib/actions/auth.ts b/lib/actions/auth.ts
new file mode 100644
index 0000000..eed07cc
--- /dev/null
+++ b/lib/actions/auth.ts
@@ -0,0 +1,17 @@
+'use server';
+
+/**
+ * Verifies if the provided password matches the ACCESS_PASSWORD environment variable.
+ */
+export async function verifyEnvPassword(password: string): Promise {
+ const accessPassword = process.env.ACCESS_PASSWORD;
+ if (!accessPassword) return false;
+ return password === accessPassword;
+}
+
+/**
+ * Checks if a password is required by checking the presence of ACCESS_PASSWORD in the environment.
+ */
+export async function isEnvPasswordRequired(): Promise {
+ return !!process.env.ACCESS_PASSWORD;
+}