feat: add batch source and user operates

This commit is contained in:
shinya
2025-08-22 22:44:15 +08:00
parent 8979bd8936
commit 3b6908942c
8 changed files with 626 additions and 74 deletions

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion,react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any, no-console, @typescript-eslint/no-non-null-assertion,react-hooks/exhaustive-deps,@typescript-eslint/no-empty-function */
'use client';
@@ -37,7 +37,7 @@ import {
Video,
} from 'lucide-react';
import { GripVertical } from 'lucide-react';
import { Suspense, useCallback, useEffect, useState } from 'react';
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
@@ -301,6 +301,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
tags?: string[];
} | null>(null);
const [selectedUserGroups, setSelectedUserGroups] = useState<string[]>([]);
const [selectedUsers, setSelectedUsers] = useState<Set<string>>(new Set());
const [showBatchUserGroupModal, setShowBatchUserGroupModal] = useState(false);
const [selectedUserGroup, setSelectedUserGroup] = useState<string>('');
const [showDeleteUserGroupModal, setShowDeleteUserGroupModal] = useState(false);
const [deletingUserGroup, setDeletingUserGroup] = useState<{
name: string;
@@ -312,6 +315,17 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
// 当前登录用户名
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
const selectAllUsers = useMemo(() => {
const selectableUserCount = config?.UserConfig?.Users?.filter(user =>
(role === 'owner' ||
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername)))
).length || 0;
return selectedUsers.size === selectableUserCount && selectedUsers.size > 0;
}, [selectedUsers.size, config?.UserConfig?.Users, role, currentUsername]);
// 获取用户组列表
const userGroups = config?.UserConfig?.Tags || [];
@@ -498,6 +512,69 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
}
};
// 处理用户选择
const handleSelectUser = useCallback((username: string, checked: boolean) => {
setSelectedUsers(prev => {
const newSelectedUsers = new Set(prev);
if (checked) {
newSelectedUsers.add(username);
} else {
newSelectedUsers.delete(username);
}
return newSelectedUsers;
});
}, []);
const handleSelectAllUsers = useCallback((checked: boolean) => {
if (checked) {
// 只选择自己有权限操作的用户
const selectableUsernames = config?.UserConfig?.Users?.filter(user =>
(role === 'owner' ||
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername)))
).map(u => u.username) || [];
setSelectedUsers(new Set(selectableUsernames));
} else {
setSelectedUsers(new Set());
}
}, [config?.UserConfig?.Users, role, currentUsername]);
// 批量设置用户组
const handleBatchSetUserGroup = async (userGroup: string) => {
if (selectedUsers.size === 0) return;
try {
const res = await fetch('/api/admin/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'batchUpdateUserGroups',
usernames: Array.from(selectedUsers),
userGroups: userGroup === '' ? [] : [userGroup],
}),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `操作失败: ${res.status}`);
}
const userCount = selectedUsers.size;
setSelectedUsers(new Set());
setShowBatchUserGroupModal(false);
setSelectedUserGroup('');
showSuccess(`已为 ${userCount} 个用户设置用户组: ${userGroup}`, showAlert);
// 刷新配置
await refreshConfig();
} catch (err) {
showError('批量设置用户组失败', showAlert);
}
};
// 提取URL域名的辅助函数
const extractDomain = (url: string): string => {
try {
@@ -636,9 +713,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</div>
{/* 用户组列表 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[20rem] overflow-y-auto overflow-x-auto'>
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[20rem] overflow-y-auto overflow-x-auto relative'>
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
<thead className='bg-gray-50 dark:bg-gray-900'>
<thead className='bg-gray-50 dark:bg-gray-900 sticky top-0 z-10'>
<tr>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
@@ -700,18 +777,37 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<button
onClick={() => {
setShowAddUserForm(!showAddUserForm);
if (showChangePasswordForm) {
setShowChangePasswordForm(false);
setChangePasswordUser({ username: '', password: '' });
}
}}
className='px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors'
>
{showAddUserForm ? '取消' : '添加用户'}
</button>
<div className='flex items-center space-x-2'>
{/* 批量操作按钮 */}
{selectedUsers.size > 0 && (
<>
<div className='flex items-center space-x-3'>
<span className='text-sm text-gray-600 dark:text-gray-400'>
{selectedUsers.size}
</span>
<button
onClick={() => setShowBatchUserGroupModal(true)}
className='px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors'
>
</button>
</div>
<div className='w-px h-6 bg-gray-300 dark:bg-gray-600'></div>
</>
)}
<button
onClick={() => {
setShowAddUserForm(!showAddUserForm);
if (showChangePasswordForm) {
setShowChangePasswordForm(false);
setChangePasswordUser({ username: '', password: '' });
}
}}
className='px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors'
>
{showAddUserForm ? '取消' : '添加用户'}
</button>
</div>
</div>
{/* 添加用户表单 */}
@@ -817,10 +913,33 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
)}
{/* 用户列表 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto'>
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table="user-list">
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
<thead className='bg-gray-50 dark:bg-gray-900'>
<thead className='bg-gray-50 dark:bg-gray-900 sticky top-0 z-10'>
<tr>
<th className='w-4' />
<th className='w-10 px-1 py-3 text-center'>
{(() => {
// 检查是否有权限操作任何用户
const hasAnyPermission = config?.UserConfig?.Users?.some(user =>
(role === 'owner' ||
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername)))
);
return hasAnyPermission ? (
<input
type='checkbox'
checked={selectAllUsers}
onChange={(e) => handleSelectAllUsers(e.target.checked)}
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
) : (
<div className='w-4 h-4' />
);
})()}
</th>
<th
scope='col'
className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'
@@ -898,6 +1017,22 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
key={user.username}
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors'
>
<td className='w-4' />
<td className='w-10 px-1 py-3 text-center'>
{(role === 'owner' ||
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername))) ? (
<input
type='checkbox'
checked={selectedUsers.has(user.username)}
onChange={(e) => handleSelectUser(user.username, e.target.checked)}
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
) : (
<div className='w-4 h-4' />
)}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100'>
{user.username}
</td>
@@ -1669,6 +1804,92 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
document.body
)}
{/* 批量设置用户组弹窗 */}
{showBatchUserGroupModal && createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick={() => {
setShowBatchUserGroupModal(false);
setSelectedUserGroup('');
}}>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full' onClick={(e) => e.stopPropagation()}>
<div className='p-6'>
<div className='flex items-center justify-between mb-6'>
<h3 className='text-xl font-semibold text-gray-900 dark:text-gray-100'>
</h3>
<button
onClick={() => {
setShowBatchUserGroupModal(false);
setSelectedUserGroup('');
}}
className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
<svg className='w-6 h-6' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M6 18L18 6M6 6l12 12' />
</svg>
</button>
</div>
<div className='mb-6'>
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-4'>
<div className='flex items-center space-x-2 mb-2'>
<svg className='w-5 h-5 text-blue-600 dark:text-blue-400' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' />
</svg>
<span className='text-sm font-medium text-blue-800 dark:text-blue-300'>
</span>
</div>
<p className='text-sm text-blue-700 dark:text-blue-400'>
<strong>{selectedUsers.size} </strong> "无用户组"
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<select
onChange={(e) => setSelectedUserGroup(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 transition-colors'
value={selectedUserGroup}
>
<option value=''></option>
{userGroups.map((group) => (
<option key={group.name} value={group.name}>
{group.name} {group.enabledApis && group.enabledApis.length > 0 ? `(${group.enabledApis.length} 个源)` : ''}
</option>
))}
</select>
<p className='mt-2 text-xs text-gray-500 dark:text-gray-400'>
"无用户组"访
</p>
</div>
</div>
{/* 操作按钮 */}
<div className='flex justify-end space-x-3'>
<button
onClick={() => {
setShowBatchUserGroupModal(false);
setSelectedUserGroup('');
}}
className='px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors'
>
</button>
<button
onClick={() => handleBatchSetUserGroup(selectedUserGroup)}
className='px-6 py-2.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors'
>
</button>
</div>
</div>
</div>
</div>,
document.body
)}
{/* 通用弹窗组件 */}
<AlertModal
isOpen={alertModal.isOpen}
@@ -1706,6 +1927,29 @@ const VideoSourceConfig = ({
from: 'config',
});
// 批量操作相关状态
const [selectedSources, setSelectedSources] = useState<Set<string>>(new Set());
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
const selectAll = useMemo(() => {
return selectedSources.size === sources.length && selectedSources.size > 0;
}, [selectedSources.size, sources.length]);
// 确认弹窗状态
const [confirmModal, setConfirmModal] = useState<{
isOpen: boolean;
title: string;
message: string;
onConfirm: () => void;
onCancel: () => void;
}>({
isOpen: false,
title: '',
message: '',
onConfirm: () => { },
onCancel: () => { }
});
// 有效性检测相关状态
const [showValidationModal, setShowValidationModal] = useState(false);
const [searchKeyword, setSearchKeyword] = useState('');
@@ -1739,6 +1983,8 @@ const VideoSourceConfig = ({
setSources(config.SourceConfig);
// 进入时重置 orderChanged
setOrderChanged(false);
// 重置选择状态
setSelectedSources(new Set());
}
}, [config]);
@@ -1981,6 +2227,14 @@ const VideoSourceConfig = ({
>
<GripVertical size={16} />
</td>
<td className='px-2 py-4 text-center'>
<input
type='checkbox'
checked={selectedSources.has(source.key)}
onChange={(e) => handleSelectSource(source.key, e.target.checked)}
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100'>
{source.name}
</td>
@@ -2049,6 +2303,77 @@ const VideoSourceConfig = ({
);
};
// 全选/取消全选
const handleSelectAll = useCallback((checked: boolean) => {
if (checked) {
const allKeys = sources.map(s => s.key);
setSelectedSources(new Set(allKeys));
} else {
setSelectedSources(new Set());
}
}, [sources]);
// 单个选择
const handleSelectSource = useCallback((key: string, checked: boolean) => {
setSelectedSources(prev => {
const newSelected = new Set(prev);
if (checked) {
newSelected.add(key);
} else {
newSelected.delete(key);
}
return newSelected;
});
}, []);
// 批量操作
const handleBatchOperation = async (action: 'batch_enable' | 'batch_disable' | 'batch_delete') => {
if (selectedSources.size === 0) {
showAlert({ type: 'warning', title: '请先选择要操作的视频源', message: '请选择至少一个视频源' });
return;
}
const keys = Array.from(selectedSources);
let confirmMessage = '';
let actionName = '';
switch (action) {
case 'batch_enable':
confirmMessage = `确定要启用选中的 ${keys.length} 个视频源吗?`;
actionName = '批量启用';
break;
case 'batch_disable':
confirmMessage = `确定要禁用选中的 ${keys.length} 个视频源吗?`;
actionName = '批量禁用';
break;
case 'batch_delete':
confirmMessage = `确定要删除选中的 ${keys.length} 个视频源吗?此操作不可恢复!`;
actionName = '批量删除';
break;
}
// 显示确认弹窗
setConfirmModal({
isOpen: true,
title: '确认操作',
message: confirmMessage,
onConfirm: async () => {
try {
await callSourceApi({ action, keys });
showAlert({ type: 'success', title: `${actionName}成功`, message: `${actionName}${keys.length} 个视频源`, timer: 2000 });
// 重置选择状态
setSelectedSources(new Set());
} catch (err) {
showAlert({ type: 'error', title: `${actionName}失败`, message: err instanceof Error ? err.message : '操作失败' });
}
setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } });
},
onCancel: () => {
setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: () => { }, onCancel: () => { } });
}
});
};
if (!config) {
return (
<div className='text-center text-gray-500 dark:text-gray-400'>
@@ -2065,6 +2390,35 @@ const VideoSourceConfig = ({
</h4>
<div className='flex items-center space-x-2'>
{/* 批量操作按钮 */}
{selectedSources.size > 0 && (
<>
<div className='flex items-center space-x-3'>
<span className='text-sm text-gray-600 dark:text-gray-400'>
{selectedSources.size}
</span>
<button
onClick={() => handleBatchOperation('batch_enable')}
className='px-3 py-1 text-sm bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors'
>
</button>
<button
onClick={() => handleBatchOperation('batch_disable')}
className='px-3 py-1 text-sm bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg transition-colors'
>
</button>
<button
onClick={() => handleBatchOperation('batch_delete')}
className='px-3 py-1 text-sm bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors'
>
</button>
</div>
<div className='w-px h-6 bg-gray-300 dark:bg-gray-600'></div>
</>
)}
<button
onClick={() => setShowValidationModal(true)}
disabled={isValidating}
@@ -2143,12 +2497,22 @@ const VideoSourceConfig = ({
</div>
)}
{/* 视频源表格 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto'>
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative' data-table="source-list">
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
<thead className='bg-gray-50 dark:bg-gray-900'>
<thead className='bg-gray-50 dark:bg-gray-900 sticky top-0 z-10'>
<tr>
<th className='w-8' />
<th className='w-12 px-2 py-3 text-center'>
<input
type='checkbox'
checked={selectAll}
onChange={(e) => handleSelectAll(e.target.checked)}
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th>
@@ -2255,6 +2619,52 @@ const VideoSourceConfig = ({
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
{/* 批量操作确认弹窗 */}
{confirmModal.isOpen && createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4' onClick={confirmModal.onCancel}>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full' onClick={(e) => e.stopPropagation()}>
<div className='p-6'>
<div className='flex items-center justify-between mb-4'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100'>
{confirmModal.title}
</h3>
<button
onClick={confirmModal.onCancel}
className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors'
>
<svg className='w-5 h-5' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M6 18L18 6M6 6l12 12' />
</svg>
</button>
</div>
<div className='mb-6'>
<p className='text-sm text-gray-600 dark:text-gray-400'>
{confirmModal.message}
</p>
</div>
{/* 操作按钮 */}
<div className='flex justify-end space-x-3'>
<button
onClick={confirmModal.onCancel}
className='px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors'
>
</button>
<button
onClick={confirmModal.onConfirm}
className='px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors'
>
</button>
</div>
</div>
</div>
</div>,
document.body
)}
</div>
);
};
@@ -2535,9 +2945,9 @@ const CategoryConfig = ({
)}
{/* 分类表格 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto'>
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[28rem] overflow-y-auto overflow-x-auto relative'>
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
<thead className='bg-gray-50 dark:bg-gray-900'>
<thead className='bg-gray-50 dark:bg-gray-900 sticky top-0 z-10'>
<tr>
<th className='w-8' />
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>

View File

@@ -9,7 +9,7 @@ import { db } from '@/lib/db';
export const runtime = 'nodejs';
// 支持的操作类型
type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort';
type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort' | 'batch_disable' | 'batch_enable' | 'batch_delete';
interface BaseBody {
action?: Action;
@@ -37,7 +37,7 @@ export async function POST(request: NextRequest) {
const username = authInfo.username;
// 基础校验
const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort'];
const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort', 'batch_disable', 'batch_enable', 'batch_delete'];
if (!username || !action || !ACTIONS.includes(action)) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -111,6 +111,88 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: '该源不可删除' }, { status: 400 });
}
adminConfig.SourceConfig.splice(idx, 1);
// 检查并清理用户组和用户的权限数组
// 清理用户组权限
if (adminConfig.UserConfig.Tags) {
adminConfig.UserConfig.Tags.forEach(tag => {
if (tag.enabledApis) {
tag.enabledApis = tag.enabledApis.filter(api => api !== key);
}
});
}
// 清理用户权限
adminConfig.UserConfig.Users.forEach(user => {
if (user.enabledApis) {
user.enabledApis = user.enabledApis.filter(api => api !== key);
}
});
break;
}
case 'batch_disable': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys) || keys.length === 0) {
return NextResponse.json({ error: '缺少 keys 参数或为空' }, { status: 400 });
}
keys.forEach(key => {
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
if (entry) {
entry.disabled = true;
}
});
break;
}
case 'batch_enable': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys) || keys.length === 0) {
return NextResponse.json({ error: '缺少 keys 参数或为空' }, { status: 400 });
}
keys.forEach(key => {
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
if (entry) {
entry.disabled = false;
}
});
break;
}
case 'batch_delete': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys) || keys.length === 0) {
return NextResponse.json({ error: '缺少 keys 参数或为空' }, { status: 400 });
}
// 过滤掉 from=config 的源,但不报错
const keysToDelete = keys.filter(key => {
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
return entry && entry.from !== 'config';
});
// 批量删除
keysToDelete.forEach(key => {
const idx = adminConfig.SourceConfig.findIndex((s) => s.key === key);
if (idx !== -1) {
adminConfig.SourceConfig.splice(idx, 1);
}
});
// 检查并清理用户组和用户的权限数组
if (keysToDelete.length > 0) {
// 清理用户组权限
if (adminConfig.UserConfig.Tags) {
adminConfig.UserConfig.Tags.forEach(tag => {
if (tag.enabledApis) {
tag.enabledApis = tag.enabledApis.filter(api => !keysToDelete.includes(api));
}
});
}
// 清理用户权限
adminConfig.UserConfig.Users.forEach(user => {
if (user.enabledApis) {
user.enabledApis = user.enabledApis.filter(api => !keysToDelete.includes(api));
}
});
}
break;
}
case 'sort': {

View File

@@ -20,6 +20,7 @@ const ACTIONS = [
'updateUserApis',
'userGroup',
'updateUserGroups',
'batchUpdateUserGroups',
] as const;
export async function POST(request: NextRequest) {
@@ -56,8 +57,8 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
// 用户组操作不需要targetUsername
if (!targetUsername && action !== 'userGroup') {
// 用户组操作和批量操作不需要targetUsername
if (!targetUsername && !['userGroup', 'batchUpdateUserGroups'].includes(action)) {
return NextResponse.json({ error: '缺少目标用户名' }, { status: 400 });
}
@@ -67,6 +68,7 @@ export async function POST(request: NextRequest) {
action !== 'updateUserApis' &&
action !== 'userGroup' &&
action !== 'updateUserGroups' &&
action !== 'batchUpdateUserGroups' &&
username === targetUsername
) {
return NextResponse.json(
@@ -92,11 +94,11 @@ export async function POST(request: NextRequest) {
operatorRole = 'admin';
}
// 查找目标用户条目(用户组操作不需要)
// 查找目标用户条目(用户组操作和批量操作不需要)
let targetEntry: any = null;
let isTargetAdmin = false;
if (action !== 'userGroup' && targetUsername) {
if (!['userGroup', 'batchUpdateUserGroups'].includes(action) && targetUsername) {
targetEntry = adminConfig.UserConfig.Users.find(
(u) => u.username === targetUsername
);
@@ -419,6 +421,38 @@ export async function POST(request: NextRequest) {
break;
}
case 'batchUpdateUserGroups': {
const { usernames, userGroups } = body as { usernames: string[]; userGroups: string[] };
if (!usernames || !Array.isArray(usernames) || usernames.length === 0) {
return NextResponse.json({ error: '缺少用户名列表' }, { status: 400 });
}
// 权限检查:站长可批量配置所有人的用户组,管理员只能批量配置普通用户
if (operatorRole !== 'owner') {
for (const targetUsername of usernames) {
const targetUser = adminConfig.UserConfig.Users.find(u => u.username === targetUsername);
if (targetUser && targetUser.role === 'admin' && targetUsername !== username) {
return NextResponse.json({ error: `管理员无法操作其他管理员 ${targetUsername}` }, { status: 400 });
}
}
}
// 批量更新用户组
for (const targetUsername of usernames) {
const targetUser = adminConfig.UserConfig.Users.find(u => u.username === targetUsername);
if (targetUser) {
if (userGroups && userGroups.length > 0) {
targetUser.tags = userGroups;
} else {
// 如果为空数组或未提供,则删除该字段,表示无用户组
delete targetUser.tags;
}
}
}
break;
}
default:
return NextResponse.json({ error: '未知操作' }, { status: 400 });
}

View File

@@ -25,12 +25,15 @@ export async function GET(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -76,12 +79,15 @@ export async function POST(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -144,12 +150,15 @@ export async function DELETE(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}

View File

@@ -18,12 +18,15 @@ export async function GET(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -48,12 +51,15 @@ export async function POST(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -111,12 +117,15 @@ export async function DELETE(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}

View File

@@ -17,16 +17,6 @@ export async function GET(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
const { searchParams } = new URL(request.url);
const query = searchParams.get('q')?.trim();

View File

@@ -24,12 +24,15 @@ export async function GET(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -58,12 +61,15 @@ export async function POST(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -107,12 +113,15 @@ export async function DELETE(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}

View File

@@ -17,12 +17,15 @@ export async function GET(request: NextRequest) {
}
const config = await getConfig();
if (config.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = config.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -57,12 +60,15 @@ export async function POST(request: NextRequest) {
}
const adminConfig = await getConfig();
if (adminConfig.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = adminConfig.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}
@@ -107,12 +113,15 @@ export async function DELETE(request: NextRequest) {
}
const adminConfig = await getConfig();
if (adminConfig.UserConfig.Users) {
// 检查用户是否被封禁
if (authInfo.username !== process.env.ADMIN_USERNAME) {
// 非站长,检查用户存在或被封禁
const user = adminConfig.UserConfig.Users.find(
(u) => u.username === authInfo.username
);
if (user && user.banned) {
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
}
if (user.banned) {
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
}
}