mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
feat: Implement password persistence using localStorage, controlled by the new PERSIST_PASSWORD environment variable.
This commit is contained in:
@@ -129,6 +129,18 @@ docker run -d -p 3000:3000 -e ACCESS_PASSWORD=your_premium_password --name kvide
|
||||
- **全局生效**:所有用户都需要此密码才能访问
|
||||
- **无法在界面删除**:只能通过修改环境变量更改
|
||||
- **与本地密码兼容**:两种密码都可以解锁应用
|
||||
- **密码持久化**:支持通过 `PERSIST_PASSWORD` 环境变量控制是否在当前设备上记住密码。
|
||||
|
||||
### 方式三:密码持久化设置(ENV)
|
||||
|
||||
通过 `PERSIST_PASSWORD` 环境变量,你可以控制用户在输入正确密码后,是否需要在下次打开时重新输入。
|
||||
|
||||
| 变量名 | 选项 | 说明 | 默认值 |
|
||||
|--------|------|------|--------|
|
||||
| `PERSIST_PASSWORD` | `true` / `false` | 是否在本地浏览器持久化保存解锁状态。设置为 `true` 时,用户只需输入一次密码,后续访问无需再次输入。 | `true` |
|
||||
|
||||
> [!NOTE]
|
||||
> 此功能仅在设置了 `ACCESS_PASSWORD` 时才会生效。如果没有设置环境密码,此选项将被忽略。
|
||||
|
||||
## 🎨 站点名称自定义配置
|
||||
|
||||
@@ -141,6 +153,7 @@ docker run -d -p 3000:3000 -e ACCESS_PASSWORD=your_premium_password --name kvide
|
||||
| `NEXT_PUBLIC_SITE_TITLE` | 浏览器标签页标题 | `视频聚合平台 - KVideo` |
|
||||
| `NEXT_PUBLIC_SITE_DESCRIPTION` | 站点描述 | `专属视频聚合播放平台,具备美观的 Liquid Glass UI` |
|
||||
| `NEXT_PUBLIC_SITE_NAME` | 站点头部名称 | `视频聚合平台` |
|
||||
| `PERSIST_PASSWORD` | 密码持久化 | `true` |
|
||||
|
||||
### 配置示例:
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
export const runtime = 'edge';
|
||||
|
||||
const ACCESS_PASSWORD = process.env.ACCESS_PASSWORD || '';
|
||||
const PERSIST_PASSWORD = process.env.PERSIST_PASSWORD !== 'false';
|
||||
const SUBSCRIPTION_SOURCES = process.env.SUBSCRIPTION_SOURCES || process.env.NEXT_PUBLIC_SUBSCRIPTION_SOURCES || '';
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
hasEnvPassword: ACCESS_PASSWORD.length > 0,
|
||||
persistPassword: PERSIST_PASSWORD,
|
||||
subscriptionSources: SUBSCRIPTION_SOURCES,
|
||||
});
|
||||
}
|
||||
|
||||
+33
-23
@@ -16,6 +16,7 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
const [error, setError] = useState(false);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [hasEnvPassword, setHasEnvPassword] = useState(initialHasEnvPassword);
|
||||
const [persistEnabled, setPersistEnabled] = useState(true);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -23,9 +24,18 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
|
||||
const init = async () => {
|
||||
const settings = settingsStore.getSettings();
|
||||
const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true';
|
||||
|
||||
// Check both storage if persistence might be enabled
|
||||
const getUnlockedState = (canPersist: boolean) => {
|
||||
const sessionUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true';
|
||||
if (!canPersist) return sessionUnlocked;
|
||||
const localUnlocked = localStorage.getItem(SESSION_UNLOCKED_KEY) === 'true';
|
||||
return sessionUnlocked || localUnlocked;
|
||||
};
|
||||
|
||||
// 1. Initial local check (fast)
|
||||
// Default to canPersist = true for first check if not sure
|
||||
const isUnlocked = getUnlockedState(true);
|
||||
const localLocked = (settings.passwordAccess || initialHasEnvPassword) && !isUnlocked;
|
||||
if (mounted) setIsLocked(localLocked);
|
||||
if (mounted) setIsClient(true);
|
||||
@@ -39,6 +49,7 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
|
||||
if (mounted) {
|
||||
setHasEnvPassword(data.hasEnvPassword);
|
||||
setPersistEnabled(data.persistPassword);
|
||||
|
||||
// CRITICAL: Sync subscriptions immediately
|
||||
if (data.subscriptionSources) {
|
||||
@@ -47,15 +58,15 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
}
|
||||
|
||||
// Re-evaluate lock status with confirmed server state
|
||||
// We only care about envPassword if we are not unlocked.
|
||||
// Access control logic:
|
||||
// Locked IF: (Local setting ON OR Env Password Exists) AND (Not Unlocked)
|
||||
const confirmLocked = (settings.passwordAccess || data.hasEnvPassword) && !isUnlocked;
|
||||
// Persistence only works if hasEnvPassword is true
|
||||
const canPersist = data.hasEnvPassword && data.persistPassword;
|
||||
const finalUnlocked = getUnlockedState(canPersist);
|
||||
|
||||
const confirmLocked = (settings.passwordAccess || data.hasEnvPassword) && !finalUnlocked;
|
||||
setIsLocked(confirmLocked);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("PasswordGate init failed:", e);
|
||||
// Fallback: rely on initial/local state which was already set
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,18 +79,12 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
|
||||
// Subscribe to settings changes (real-time updates)
|
||||
useEffect(() => {
|
||||
// Function to handle updates from the store
|
||||
const handleSettingsUpdate = () => {
|
||||
const settings = settingsStore.getSettings();
|
||||
const isUnlocked = sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true';
|
||||
const canPersist = hasEnvPassword && persistEnabled;
|
||||
const isUnlocked = (sessionStorage.getItem(SESSION_UNLOCKED_KEY) === 'true') ||
|
||||
(canPersist && localStorage.getItem(SESSION_UNLOCKED_KEY) === 'true');
|
||||
|
||||
// We can't easily check env password synchronously here, but we can check local settings.
|
||||
// If local setting says lock, and we are not unlocked, we lock.
|
||||
// If local setting says unlock (and no env password known yet), we unlock.
|
||||
// To be safe, we might just re-run checkLockStatus() but that's async.
|
||||
// For immediate UI feedback on "Enable/Disable Password" toggle in settings:
|
||||
|
||||
// If password access is disabled in settings, and we assume no env password for a moment (or rely on previous state):
|
||||
if (!settings.passwordAccess && !hasEnvPassword) {
|
||||
setIsLocked(false);
|
||||
} else if (settings.passwordAccess && !isUnlocked) {
|
||||
@@ -89,7 +94,7 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
|
||||
const unsubscribe = settingsStore.subscribe(handleSettingsUpdate);
|
||||
return () => unsubscribe();
|
||||
}, [hasEnvPassword]);
|
||||
}, [hasEnvPassword, persistEnabled]);
|
||||
|
||||
|
||||
|
||||
@@ -98,13 +103,21 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
setIsValidating(true);
|
||||
|
||||
const settings = settingsStore.getSettings();
|
||||
const canPersist = hasEnvPassword && persistEnabled;
|
||||
|
||||
// First check local passwords
|
||||
if (settings.accessPasswords.includes(password)) {
|
||||
const setUnlocked = () => {
|
||||
if (canPersist) {
|
||||
localStorage.setItem(SESSION_UNLOCKED_KEY, 'true');
|
||||
}
|
||||
sessionStorage.setItem(SESSION_UNLOCKED_KEY, 'true');
|
||||
setIsLocked(false);
|
||||
setError(false);
|
||||
setIsValidating(false);
|
||||
};
|
||||
|
||||
// First check local passwords
|
||||
if (settings.accessPasswords.includes(password)) {
|
||||
setUnlocked();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -118,14 +131,11 @@ export function PasswordGate({ children, hasEnvPassword: initialHasEnvPassword }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.valid) {
|
||||
sessionStorage.setItem(SESSION_UNLOCKED_KEY, 'true');
|
||||
setIsLocked(false);
|
||||
setError(false);
|
||||
setIsValidating(false);
|
||||
setUnlocked();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// API error, proceed to show error
|
||||
// API error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kvideo",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.2",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kvideo",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user