用户组增加权限控制

This commit is contained in:
mtvpls
2026-04-22 16:22:17 +08:00
parent 2734de0c85
commit 0916e4ff08
61 changed files with 617 additions and 165 deletions
+1 -6
View File
@@ -77,6 +77,7 @@ export interface AdminConfig {
Tags?: {
name: string;
enabledApis: string[];
permissions?: string[];
}[];
};
SourceConfig: {
@@ -189,8 +190,6 @@ export interface AdminConfig {
EnableVideoCardEntry: boolean; // VideoCard入口开关
EnablePlayPageEntry: boolean; // 播放页入口开关
EnableAIComments: boolean; // AI评论生成开关
// 权限控制
AllowRegularUsers: boolean; // 是否允许普通用户使用AI问片(关闭后仅站长和管理员可用)
// 高级设置
Temperature?: number; // AI温度参数(0-2),默认0.7
MaxTokens?: number; // 最大回复token数,默认1000
@@ -276,10 +275,6 @@ export interface AdminConfig {
BaseUrl?: string; // lxserver 地址
Token?: string; // lxserver x-user-token
ProxyEnabled?: boolean; // 是否走 stream 代理
// 兼容旧代码的遗留字段(待删除)
TuneHubEnabled?: boolean;
TuneHubBaseUrl?: string;
TuneHubApiKey?: string;
OpenListCacheEnabled?: boolean;
OpenListCacheURL?: string;
OpenListCacheUsername?: string;
+36
View File
@@ -0,0 +1,36 @@
export const FEATURE_PERMISSION_OPTIONS = [
{ key: 'private_library', label: '私人影库', description: 'OpenList 私人影库访问' },
{ key: 'emby', label: 'Emby', description: 'Emby 私人媒体库访问' },
{ key: 'xiaoya', label: '小雅', description: '小雅媒体库访问' },
{ key: 'ai_ask', label: 'AI问片', description: 'AI 问片与影视问答' },
{ key: 'netdisk_search', label: '网盘搜索', description: 'Pansou 网盘资源搜索' },
{ key: 'magnet_search', label: '磁链搜索', description: '动漫/磁链搜索' },
{ key: 'magnet_save_private_library', label: '磁链保存影库', description: '磁链保存到私人影库' },
{ key: 'netdisk_transfer', label: '网盘转存', description: '夸克网盘转存' },
{ key: 'netdisk_temp_play', label: '临时播放', description: '网盘资源临时播放' },
{ key: 'live', label: '电视直播', description: '电视直播频道观看' },
{ key: 'web_live', label: '网络直播', description: '网络直播观看' },
{ key: 'music', label: '音乐', description: '音乐视听功能' },
{ key: 'manga', label: '漫画展馆', description: '漫画搜索、阅读与书架' },
] as const;
export type FeaturePermissionKey = (typeof FEATURE_PERMISSION_OPTIONS)[number]['key'];
export const ALL_FEATURE_PERMISSION_KEYS = FEATURE_PERMISSION_OPTIONS.map(
(item) => item.key
) as FeaturePermissionKey[];
export function sanitizeFeaturePermissions(
permissions?: string[] | null
): FeaturePermissionKey[] {
if (!Array.isArray(permissions)) return [];
const allowed = new Set<FeaturePermissionKey>(ALL_FEATURE_PERMISSION_KEYS);
return Array.from(
new Set(
permissions.filter(
(item): item is FeaturePermissionKey =>
typeof item === 'string' && allowed.has(item as FeaturePermissionKey)
)
)
);
}
+6
View File
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { db } from '@/lib/db';
import { hasFeaturePermission } from '@/lib/permissions';
export async function getMusicV2Username(request: NextRequest): Promise<string | null> {
const authInfo = getAuthInfoFromCookie(request);
@@ -14,6 +15,11 @@ export async function getMusicV2Username(request: NextRequest): Promise<string |
}
}
const allowed = await hasFeaturePermission(authInfo.username, 'music');
if (!allowed) {
return null;
}
return authInfo.username;
}
+101
View File
@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig } from '@/lib/config';
import { db } from '@/lib/db';
import {
ALL_FEATURE_PERMISSION_KEYS,
type FeaturePermissionKey,
sanitizeFeaturePermissions,
} from '@/lib/feature-permissions';
export type FeatureAccessMap = Record<FeaturePermissionKey, boolean>;
export function createEmptyFeatureAccessMap(): FeatureAccessMap {
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
acc[key] = false;
return acc;
}, {} as FeatureAccessMap);
}
function isPrivilegedRole(role?: string) {
return role === 'owner' || role === 'admin';
}
async function getUserFeatureAccessMap(username: string): Promise<FeatureAccessMap> {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as FeatureAccessMap);
}
const userInfo = await db.getUserInfoV2(username);
if (!userInfo || userInfo.banned) {
return createEmptyFeatureAccessMap();
}
if (username === process.env.USERNAME || isPrivilegedRole(userInfo.role)) {
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as FeatureAccessMap);
}
const config = await getConfig();
const tags = Array.isArray(userInfo.tags) ? userInfo.tags : [];
// 兼容旧用户:未分配用户组时,默认拥有全部功能权限
if (tags.length === 0) {
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
acc[key] = true;
return acc;
}, {} as FeatureAccessMap);
}
const allowedPermissions = new Set<FeaturePermissionKey>();
tags.forEach((tagName) => {
const group = config.UserConfig.Tags?.find((item) => item.name === tagName);
sanitizeFeaturePermissions(group?.permissions).forEach((permission) =>
allowedPermissions.add(permission)
);
});
return ALL_FEATURE_PERMISSION_KEYS.reduce((acc, key) => {
acc[key] = allowedPermissions.has(key);
return acc;
}, {} as FeatureAccessMap);
}
export async function getUserFeatureAccess(username?: string | null): Promise<FeatureAccessMap> {
if (!username) return createEmptyFeatureAccessMap();
return getUserFeatureAccessMap(username);
}
export async function hasFeaturePermission(
username: string,
permission: FeaturePermissionKey
): Promise<boolean> {
const accessMap = await getUserFeatureAccessMap(username);
return accessMap[permission] === true;
}
export async function requireFeaturePermission(
request: NextRequest,
permission: FeaturePermissionKey,
errorMessage = '无权限访问该功能'
): Promise<{ username: string } | NextResponse> {
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo?.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const allowed = await hasFeaturePermission(authInfo.username, permission);
if (!allowed) {
return NextResponse.json({ error: errorMessage }, { status: 403 });
}
return { username: authInfo.username };
}