用户组增加权限控制
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user