diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1bda760..2d13d95 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -138,28 +138,166 @@ interface UserConfigProps { const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const [showAddUserForm, setShowAddUserForm] = useState(false); const [showChangePasswordForm, setShowChangePasswordForm] = useState(false); + const [showAddUserGroupForm, setShowAddUserGroupForm] = useState(false); + const [showEditUserGroupForm, setShowEditUserGroupForm] = useState(false); const [newUser, setNewUser] = useState({ username: '', password: '', + userGroup: '', // 新增用户组字段 }); const [changePasswordUser, setChangePasswordUser] = useState({ username: '', password: '', }); + const [newUserGroup, setNewUserGroup] = useState({ + name: '', + enabledApis: [] as string[], + }); + const [editingUserGroup, setEditingUserGroup] = useState<{ + name: string; + enabledApis: string[]; + } | null>(null); const [showConfigureApisModal, setShowConfigureApisModal] = useState(false); const [selectedUser, setSelectedUser] = useState<{ username: string; role: 'user' | 'admin' | 'owner'; enabledApis?: string[]; + tags?: string[]; } | null>(null); const [selectedApis, setSelectedApis] = useState([]); // 当前登录用户名 const currentUsername = getAuthInfoFromBrowserCookie()?.username || null; + // 获取用户组列表 + const userGroups = config?.UserConfig?.Tags || []; + // 处理用户组相关操作 + const handleUserGroupAction = async ( + action: 'add' | 'edit' | 'delete', + groupName: string, + enabledApis?: string[] + ) => { + try { + const res = await fetch('/api/admin/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + action: 'userGroup', + groupAction: action, + groupName, + enabledApis, + }), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || `操作失败: ${res.status}`); + } + await refreshConfig(); + + if (action === 'add') { + setNewUserGroup({ name: '', enabledApis: [] }); + setShowAddUserGroupForm(false); + } else if (action === 'edit') { + setEditingUserGroup(null); + setShowEditUserGroupForm(false); + } + + showSuccess(action === 'add' ? '用户组添加成功' : action === 'edit' ? '用户组更新成功' : '用户组删除成功'); + } catch (err) { + showError(err instanceof Error ? err.message : '操作失败'); + } + }; + + const handleAddUserGroup = () => { + if (!newUserGroup.name.trim()) return; + handleUserGroupAction('add', newUserGroup.name, newUserGroup.enabledApis); + }; + + const handleEditUserGroup = () => { + if (!editingUserGroup?.name.trim()) return; + handleUserGroupAction('edit', editingUserGroup.name, editingUserGroup.enabledApis); + }; + + const handleDeleteUserGroup = async (groupName: string) => { + // 计算会受影响的用户数量 + const affectedUsers = config?.UserConfig?.Users?.filter(user => + user.tags && user.tags.includes(groupName) + ) || []; + + const affectedCount = affectedUsers.length; + const affectedUserNames = affectedUsers.map(u => u.username).join(', '); + + const { isConfirmed } = await Swal.fire({ + title: '确认删除用户组', + html: ` +
+

删除用户组 ${groupName} 将影响所有使用该组的用户,此操作不可恢复!

+ ${affectedCount > 0 ? ` +
+

+ ⚠️ 将影响 ${affectedCount} 个用户: +

+

+ ${affectedUserNames} +

+

+ 这些用户的用户组将被自动移除 +

+
+ ` : ` +
+

+ ✅ 当前没有用户使用此用户组 +

+
+ `} +
+ `, + icon: 'warning', + showCancelButton: true, + confirmButtonText: '确认删除', + cancelButtonText: '取消', + confirmButtonColor: '#dc2626', + }); + + if (!isConfirmed) return; + + await handleUserGroupAction('delete', groupName); + }; + + const handleStartEditUserGroup = (group: { name: string; enabledApis: string[] }) => { + setEditingUserGroup({ ...group }); + setShowEditUserGroupForm(true); + setShowAddUserGroupForm(false); + }; + + // 为用户分配用户组 + const handleAssignUserGroup = async (username: string, userGroups: string[]) => { + try { + const res = await fetch('/api/admin/user', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + targetUsername: username, + action: 'updateUserGroups', + userGroups, + }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || `操作失败: ${res.status}`); + } + + await refreshConfig(); + showSuccess('用户组分配成功'); + } catch (err) { + showError(err instanceof Error ? err.message : '操作失败'); + } + }; const handleBanUser = async (uname: string) => { await handleUserAction('ban', uname); @@ -179,8 +317,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { const handleAddUser = async () => { if (!newUser.username || !newUser.password) return; - await handleUserAction('add', newUser.username, newUser.password); - setNewUser({ username: '', password: '' }); + await handleUserAction('add', newUser.username, newUser.password, newUser.userGroup); + setNewUser({ username: '', password: '', userGroup: '' }); setShowAddUserForm(false); }; @@ -278,7 +416,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { | 'changePassword' | 'deleteUser', targetUsername: string, - targetPassword?: string + targetPassword?: string, + userGroup?: string ) => { try { const res = await fetch('/api/admin/user', { @@ -287,6 +426,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { body: JSON.stringify({ targetUsername, ...(targetPassword ? { targetPassword } : {}), + ...(userGroup ? { userGroup } : {}), action, }), }); @@ -330,6 +470,85 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { + {/* 用户组管理 */} +
+
+

+ 用户组管理 +

+ +
+ + {/* 用户组列表 */} +
+ + + + + + + + + + {userGroups.map((group) => ( + + + + + + ))} + {userGroups.length === 0 && ( + + + + )} + +
+ 用户组名称 + + 可用视频源 + + 操作 +
+ {group.name} + +
+ + {group.enabledApis && group.enabledApis.length > 0 + ? `${group.enabledApis.length} 个源` + : '无限制'} + +
+
+ + +
+ 暂无用户组,请添加用户组来管理用户权限 +
+
+
+ {/* 用户列表 */}
@@ -353,32 +572,55 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { {/* 添加用户表单 */} {showAddUserForm && (
-
- - setNewUser((prev) => ({ ...prev, username: e.target.value })) - } - className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' - /> - - setNewUser((prev) => ({ ...prev, password: e.target.value })) - } - className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' - /> - +
+
+ + setNewUser((prev) => ({ ...prev, username: e.target.value })) + } + className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> + + setNewUser((prev) => ({ ...prev, password: e.target.value })) + } + className='px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent' + /> +
+
+ + +
+
+ +
)} @@ -452,6 +694,12 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { > 状态 + + 用户组 + { {!user.banned ? '正常' : '已封禁'} + +
+ + {user.tags && user.tags.length > 0 + ? user.tags.join(', ') + : '无用户组'} + + {/* 配置用户组按钮 */} + {(role === 'owner' || + (role === 'admin' && + (user.role === 'user' || + user.username === currentUsername))) && ( + + )} +
+
@@ -752,6 +1045,239 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
, document.body )} + + {/* 添加用户组弹窗 */} + {showAddUserGroupForm && createPortal( +
+
+
+
+

+ 添加新用户组 +

+ +
+ +
+ {/* 用户组名称 */} +
+ + + setNewUserGroup((prev) => ({ ...prev, name: e.target.value })) + } + className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent' + /> +
+ + {/* 可用视频源 */} +
+ +
+ {config?.SourceConfig?.map((source) => ( + + ))} +
+ + {/* 快速操作按钮 */} +
+ + +
+
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+
, + document.body + )} + + {/* 编辑用户组弹窗 */} + {showEditUserGroupForm && editingUserGroup && createPortal( +
+
+
+
+

+ 编辑用户组 - {editingUserGroup.name} +

+ +
+ +
+ {/* 可用视频源 */} +
+ +
+ {config?.SourceConfig?.map((source) => ( + + ))} +
+ + {/* 快速操作按钮 */} +
+ + +
+
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+
, + document.body + )}
); }; diff --git a/src/app/api/admin/user/route.ts b/src/app/api/admin/user/route.ts index ff5c695..4632eb6 100644 --- a/src/app/api/admin/user/route.ts +++ b/src/app/api/admin/user/route.ts @@ -18,6 +18,8 @@ const ACTIONS = [ 'changePassword', 'deleteUser', 'updateUserApis', + 'userGroup', + 'updateUserGroups', ] as const; export async function POST(request: NextRequest) { @@ -54,7 +56,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '参数格式错误' }, { status: 400 }); } - if (!targetUsername) { + // 用户组操作不需要targetUsername + if (!targetUsername && action !== 'userGroup') { return NextResponse.json({ error: '缺少目标用户名' }, { status: 400 }); } @@ -62,6 +65,8 @@ export async function POST(request: NextRequest) { action !== 'changePassword' && action !== 'deleteUser' && action !== 'updateUserApis' && + action !== 'userGroup' && + action !== 'updateUserGroups' && username === targetUsername ) { return NextResponse.json( @@ -87,22 +92,27 @@ export async function POST(request: NextRequest) { operatorRole = 'admin'; } - // 查找目标用户条目 - let targetEntry = adminConfig.UserConfig.Users.find( - (u) => u.username === targetUsername - ); + // 查找目标用户条目(用户组操作不需要) + let targetEntry: any = null; + let isTargetAdmin = false; - if ( - targetEntry && - targetEntry.role === 'owner' && - action !== 'changePassword' - ) { - return NextResponse.json({ error: '无法操作站长' }, { status: 400 }); + if (action !== 'userGroup' && targetUsername) { + targetEntry = adminConfig.UserConfig.Users.find( + (u) => u.username === targetUsername + ); + + if ( + targetEntry && + targetEntry.role === 'owner' && + action !== 'changePassword' + ) { + return NextResponse.json({ error: '无法操作站长' }, { status: 400 }); + } + + // 权限校验逻辑 + isTargetAdmin = targetEntry?.role === 'admin'; } - // 权限校验逻辑 - const isTargetAdmin = targetEntry?.role === 'admin'; - switch (action) { case 'add': { if (targetEntry) { @@ -115,11 +125,22 @@ export async function POST(request: NextRequest) { ); } await db.registerUser(targetUsername!, targetPassword); + + // 获取用户组信息 + const { userGroup } = body as { userGroup?: string }; + // 更新配置 - adminConfig.UserConfig.Users.push({ + const newUser: any = { username: targetUsername!, role: 'user', - }); + }; + + // 如果指定了用户组,添加到tags中 + if (userGroup && userGroup.trim()) { + newUser.tags = [userGroup]; + } + + adminConfig.UserConfig.Users.push(newUser); targetEntry = adminConfig.UserConfig.Users[ adminConfig.UserConfig.Users.length - 1 @@ -307,6 +328,97 @@ export async function POST(request: NextRequest) { break; } + case 'userGroup': { + // 用户组管理操作 + const { groupAction, groupName, enabledApis } = body as { + groupAction: 'add' | 'edit' | 'delete'; + groupName: string; + enabledApis?: string[]; + }; + + if (!adminConfig.UserConfig.Tags) { + adminConfig.UserConfig.Tags = []; + } + + switch (groupAction) { + case 'add': { + // 检查用户组是否已存在 + if (adminConfig.UserConfig.Tags.find(t => t.name === groupName)) { + return NextResponse.json({ error: '用户组已存在' }, { status: 400 }); + } + adminConfig.UserConfig.Tags.push({ + name: groupName, + enabledApis: enabledApis || [], + }); + break; + } + case 'edit': { + const groupIndex = adminConfig.UserConfig.Tags.findIndex(t => t.name === groupName); + if (groupIndex === -1) { + return NextResponse.json({ error: '用户组不存在' }, { status: 404 }); + } + adminConfig.UserConfig.Tags[groupIndex].enabledApis = enabledApis || []; + break; + } + case 'delete': { + const groupIndex = adminConfig.UserConfig.Tags.findIndex(t => t.name === groupName); + if (groupIndex === -1) { + return NextResponse.json({ error: '用户组不存在' }, { status: 404 }); + } + + // 查找使用该用户组的所有用户 + const affectedUsers: string[] = []; + adminConfig.UserConfig.Users.forEach(user => { + if (user.tags && user.tags.includes(groupName)) { + affectedUsers.push(user.username); + // 从用户的tags中移除该用户组 + user.tags = user.tags.filter(tag => tag !== groupName); + // 如果用户没有其他标签了,删除tags字段 + if (user.tags.length === 0) { + delete user.tags; + } + } + }); + + // 删除用户组 + adminConfig.UserConfig.Tags.splice(groupIndex, 1); + + // 记录删除操作的影响 + console.log(`删除用户组 "${groupName}",影响用户: ${affectedUsers.length > 0 ? affectedUsers.join(', ') : '无'}`); + + break; + } + default: + return NextResponse.json({ error: '未知的用户组操作' }, { status: 400 }); + } + break; + } + case 'updateUserGroups': { + if (!targetEntry) { + return NextResponse.json({ error: '目标用户不存在' }, { status: 404 }); + } + + const { userGroups } = body as { userGroups: string[] }; + + // 权限检查:站长可配置所有人的用户组,管理员可配置普通用户和自己的用户组 + if ( + isTargetAdmin && + operatorRole !== 'owner' && + username !== targetUsername + ) { + return NextResponse.json({ error: '仅站长可配置其他管理员的用户组' }, { status: 400 }); + } + + // 更新用户的用户组 + if (userGroups && userGroups.length > 0) { + targetEntry.tags = userGroups; + } else { + // 如果为空数组或未提供,则删除该字段,表示无用户组 + delete targetEntry.tags; + } + + break; + } default: return NextResponse.json({ error: '未知操作' }, { status: 400 }); } diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 0c762e9..4cc2aaa 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -22,7 +22,12 @@ export interface AdminConfig { username: string; role: 'user' | 'admin' | 'owner'; banned?: boolean; - enabledApis?: string[]; // 为空则允许全部 + enabledApis?: string[]; // 优先级高于tags限制 + tags?: string[]; // 多 tags 取并集限制 + }[]; + Tags?: { + name: string; + enabledApis: string[]; }[]; }; SourceConfig: { diff --git a/src/lib/config.ts b/src/lib/config.ts index c1cc706..0e04dd1 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -289,6 +289,7 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig { role: 'owner', banned: false, enabledApis: originOwnerCfg?.enabledApis || undefined, + tags: originOwnerCfg?.tags || undefined, }); // 采集源去重 @@ -338,17 +339,51 @@ export async function getCacheTime(): Promise { export async function getAvailableApiSites(user?: string): Promise { const config = await getConfig(); const allApiSites = config.SourceConfig.filter((s) => !s.disabled); - const userApiSites = user ? config.UserConfig.Users.find((u) => u.username === user)?.enabledApis || [] : []; - if (userApiSites.length === 0) { + + if (!user) { return allApiSites; } - const userApiSitesSet = new Set(userApiSites); - return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({ - key: s.key, - name: s.name, - api: s.api, - detail: s.detail, - })); + + const userConfig = config.UserConfig.Users.find((u) => u.username === user); + if (!userConfig) { + return allApiSites; + } + + // 优先根据用户自己的 enabledApis 配置查找 + if (userConfig.enabledApis && userConfig.enabledApis.length > 0) { + const userApiSitesSet = new Set(userConfig.enabledApis); + return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({ + key: s.key, + name: s.name, + api: s.api, + detail: s.detail, + })); + } + + // 如果没有 enabledApis 配置,则根据 tags 查找 + if (userConfig.tags && userConfig.tags.length > 0 && config.UserConfig.Tags) { + const enabledApisFromTags = new Set(); + + // 遍历用户的所有 tags,收集对应的 enabledApis + userConfig.tags.forEach(tagName => { + const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName); + if (tagConfig && tagConfig.enabledApis) { + tagConfig.enabledApis.forEach(apiKey => enabledApisFromTags.add(apiKey)); + } + }); + + if (enabledApisFromTags.size > 0) { + return allApiSites.filter((s) => enabledApisFromTags.has(s.key)).map((s) => ({ + key: s.key, + name: s.name, + api: s.api, + detail: s.detail, + })); + } + } + + // 如果都没有配置,返回所有可用的 API 站点 + return allApiSites; } export async function setCachedConfig(config: AdminConfig) {