mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-15 08:43:44 +08:00
feat: add environment variable password protection and integrate it into the password gate component.
This commit is contained in:
@@ -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=你的密码`。
|
||||
|
||||
## 🚀 快速部署
|
||||
|
||||
|
||||
+43
-19
@@ -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 }) {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 px-4 bg-[var(--accent-color)] text-white font-bold rounded-[var(--radius-2xl)] hover:translate-y-[-2px] hover:brightness-110 shadow-[var(--shadow-sm)] hover:shadow-[0_4px_8px_var(--shadow-color)] active:translate-y-0 active:scale-[0.98] transition-all duration-200"
|
||||
disabled={loading}
|
||||
className="w-full py-3 px-4 bg-[var(--accent-color)] text-white font-bold rounded-[var(--radius-2xl)] hover:translate-y-[-2px] hover:brightness-110 shadow-[var(--shadow-sm)] hover:shadow-[0_4px_8px_var(--shadow-color)] active:translate-y-0 active:scale-[0.98] transition-all duration-200 disabled:opacity-50"
|
||||
>
|
||||
解锁访问
|
||||
{loading ? '正在验证...' : '解锁访问'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
'use server';
|
||||
|
||||
/**
|
||||
* Verifies if the provided password matches the ACCESS_PASSWORD environment variable.
|
||||
*/
|
||||
export async function verifyEnvPassword(password: string): Promise<boolean> {
|
||||
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<boolean> {
|
||||
return !!process.env.ACCESS_PASSWORD;
|
||||
}
|
||||
Reference in New Issue
Block a user