Merge pull request #192 from edison0817/dev1

增加强制开启托管账户的环境变量
This commit is contained in:
Kuek Hao Yang
2026-07-08 21:37:02 +08:00
committed by GitHub
4 changed files with 51 additions and 6 deletions
+3
View File
@@ -269,6 +269,7 @@ KVideo 现在支持两套认证模式:
- 配置 `AUTH_SECRET`
- 配置 `UPSTASH_REDIS_REST_URL`
- 配置 `UPSTASH_REDIS_REST_TOKEN`
- (可选)配置 `MANAGED_AUTH_ENABLED=true` 强制优先使用托管账户模式
启用后:
@@ -277,6 +278,7 @@ KVideo 现在支持两套认证模式:
- 超级管理员可在设置页直接管理账户和权限
- 配置同步、历史、收藏等跨设备数据会按登录账户自动隔离
> **强制优先托管模式**:设置 `MANAGED_AUTH_ENABLED=true` 只会改变认证模式选择,不会跳过 `AUTH_SECRET` 和 Redis 检查。缺少 `AUTH_SECRET`、`UPSTASH_REDIS_REST_URL` 或 `UPSTASH_REDIS_REST_TOKEN` 时,托管账户模式仍不会启用。
首次启用时,如果 Redis 里还没有账户,会自动使用 `ADMIN_PASSWORD``ACCOUNTS` 作为引导种子创建首批托管账户。
### 方式二:单管理员密码(环境变量模式)
@@ -680,6 +682,7 @@ docker run -e PORT=8080 -p 8080:8080 --name kvideo kuekhaoyang/kvideo:latest
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `AUTH_SECRET` | 托管账户模式的会话签名密钥;启用 Redis 托管账户时必填 | - |
| `MANAGED_AUTH_ENABLED` | 设为 `true` 时强制优先使用托管账户模式;仍要求 `AUTH_SECRET` 和 Upstash Redis 配置完整 | - |
| `ADMIN_PASSWORD` | 管理员密码;环境变量模式直接生效,也可作为托管模式首次引导的超级管理员种子 | - |
| `ACCESS_PASSWORD` | 访问密码(向后兼容,等同于 `ADMIN_PASSWORD` | - |
| `ACCOUNTS` | 多账户配置;支持 `密码:名称[:角色[:权限1\|权限2]]``用户名:密码:名称[:角色[:权限1\|权限2]]` 两种格式 | - |
+20
View File
@@ -5,6 +5,8 @@ import {
type Role,
} from '@/lib/auth/permissions';
export type LoginMode = 'none' | 'legacy_password' | 'managed';
export interface SeedAccountInput {
username: string;
password: string;
@@ -36,6 +38,24 @@ export interface SessionPayload {
iat: number;
}
export function resolveLoginMode({
managedAccountCount,
managedAuthEnabled,
managedAuthForced,
legacyAuthConfigured,
}: {
managedAccountCount: number;
managedAuthEnabled: boolean;
managedAuthForced: boolean;
legacyAuthConfigured: boolean;
}): LoginMode {
if (managedAccountCount > 0 || (managedAuthForced && managedAuthEnabled)) {
return 'managed';
}
return legacyAuthConfigured ? 'legacy_password' : 'none';
}
const PBKDF2_ITERATIONS = 120_000;
const PBKDF2_KEY_BYTES = 32;
const SESSION_TOKEN_VERSION = 'v1';
+11 -6
View File
@@ -7,9 +7,11 @@ import {
hashPassword,
normalizeUsername,
parseBootstrapAccounts,
resolveLoginMode,
signSessionPayload,
verifyPassword,
verifySessionToken,
type LoginMode,
type SeedAccountInput,
type SessionPayload,
type StoredAccountRecord,
@@ -22,7 +24,7 @@ import {
type Role,
} from '@/lib/auth/permissions';
export type LoginMode = 'none' | 'legacy_password' | 'managed';
export type { LoginMode };
export interface ServerAuthSession {
accountId: string;
@@ -79,6 +81,7 @@ const IPTV_SOURCES = process.env.IPTV_SOURCES || process.env.NEXT_PUBLIC_IPTV_SO
const MERGE_SOURCES = process.env.MERGE_SOURCES || process.env.NEXT_PUBLIC_MERGE_SOURCES || '';
const DANMAKU_API_URL = process.env.DANMAKU_API_URL || process.env.NEXT_PUBLIC_DANMAKU_API_URL || '';
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
const MANAGED_AUTH_FORCED = process.env.MANAGED_AUTH_ENABLED === 'true';
const effectiveAdminPassword = ADMIN_PASSWORD || ACCESS_PASSWORD;
@@ -216,12 +219,14 @@ function getPublicRuntimeConfig(): Omit<PublicAuthConfig, 'hasAuth' | 'hasPremiu
}
export async function getPublicAuthConfig(): Promise<PublicAuthConfig> {
const managedAuthEnabled = isManagedAuthEnabled();
const managedAccountCount = await getManagedAccountCount();
const loginMode: LoginMode = managedAccountCount > 0
? 'managed'
: isLegacyAuthConfigured()
? 'legacy_password'
: 'none';
const loginMode = resolveLoginMode({
managedAccountCount,
managedAuthEnabled,
managedAuthForced: MANAGED_AUTH_FORCED,
legacyAuthConfigured: isLegacyAuthConfigured(),
});
return {
hasAuth: loginMode !== 'none',
+17
View File
@@ -4,6 +4,7 @@ import {
createStoredAccount,
hashPassword,
parseBootstrapAccounts,
resolveLoginMode,
signSessionPayload,
verifyPassword,
verifySessionToken,
@@ -78,6 +79,22 @@ test('createStoredAccount stores hashed password and normalized permissions', as
assert.equal(await verifyPassword('secret', account.passwordSalt, account.passwordHash), true);
});
test('MANAGED_AUTH_ENABLED does not bypass managed auth hard dependencies', () => {
assert.equal(resolveLoginMode({
managedAccountCount: 0,
managedAuthEnabled: false,
managedAuthForced: true,
legacyAuthConfigured: true,
}), 'legacy_password');
assert.equal(resolveLoginMode({
managedAccountCount: 0,
managedAuthEnabled: true,
managedAuthForced: true,
legacyAuthConfigured: true,
}), 'managed');
});
test('resolvePermissions applies role defaults and IPTV management inheritance', () => {
const viewerPermissions = resolvePermissions('viewer', ['iptv_access']);
assert.ok(viewerPermissions.includes('iptv_access'));