mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
@@ -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]]` 两种格式 | - |
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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'));
|
||||
|
||||
Reference in New Issue
Block a user