feat: add user group management

This commit is contained in:
shinya
2025-08-21 13:05:46 +08:00
parent 3535748d28
commit 34ceb3c26a
4 changed files with 733 additions and 55 deletions

View File

@@ -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<string[]>([]);
// 当前登录用户名
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: `
<div class="text-left">
<p class="mb-3">删除用户组 <strong>${groupName}</strong> 将影响所有使用该组的用户,此操作不可恢复!</p>
${affectedCount > 0 ? `
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 mb-3">
<p class="text-sm text-yellow-800 dark:text-yellow-200 font-medium mb-1">
⚠️ 将影响 ${affectedCount} 个用户:
</p>
<p class="text-sm text-yellow-700 dark:text-yellow-300">
${affectedUserNames}
</p>
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-1">
这些用户的用户组将被自动移除
</p>
</div>
` : `
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 mb-3">
<p class="text-sm text-green-800 dark:text-green-200">
✅ 当前没有用户使用此用户组
</p>
</div>
`}
</div>
`,
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) => {
{/* 用户组管理 */}
<div>
<div className='flex items-center justify-between mb-3'>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<button
onClick={() => {
setShowAddUserGroupForm(!showAddUserGroupForm);
if (showEditUserGroupForm) {
setShowEditUserGroupForm(false);
setEditingUserGroup(null);
}
}}
className='px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition-colors'
>
{showAddUserGroupForm ? '取消' : '添加用户组'}
</button>
</div>
{/* 用户组列表 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg max-h-[20rem] overflow-y-auto overflow-x-auto'>
<table className='min-w-full divide-y divide-gray-200 dark:divide-gray-700'>
<thead className='bg-gray-50 dark:bg-gray-900'>
<tr>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th>
<th className='px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</th>
</tr>
</thead>
<tbody className='divide-y divide-gray-200 dark:divide-gray-700'>
{userGroups.map((group) => (
<tr key={group.name} className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors'>
<td className='px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100'>
{group.name}
</td>
<td className='px-6 py-4 whitespace-nowrap'>
<div className='flex items-center space-x-2'>
<span className='text-sm text-gray-900 dark:text-gray-100'>
{group.enabledApis && group.enabledApis.length > 0
? `${group.enabledApis.length} 个源`
: '无限制'}
</span>
</div>
</td>
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
<button
onClick={() => handleStartEditUserGroup(group)}
className='inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors'
>
</button>
<button
onClick={() => handleDeleteUserGroup(group.name)}
className='inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:text-red-200 transition-colors'
>
</button>
</td>
</tr>
))}
{userGroups.length === 0 && (
<tr>
<td colSpan={3} className='px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400'>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* 用户列表 */}
<div>
<div className='flex items-center justify-between mb-3'>
@@ -353,32 +572,55 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
{/* 添加用户表单 */}
{showAddUserForm && (
<div className='mb-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
<div className='flex flex-col sm:flex-row gap-4 sm:gap-3'>
<input
type='text'
placeholder='用户名'
value={newUser.username}
onChange={(e) =>
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'
/>
<input
type='password'
placeholder='密码'
value={newUser.password}
onChange={(e) =>
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'
/>
<button
onClick={handleAddUser}
disabled={!newUser.username || !newUser.password}
className='w-full sm:w-auto px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded-lg transition-colors'
>
</button>
<div className='space-y-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<input
type='text'
placeholder='用户名'
value={newUser.username}
onChange={(e) =>
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'
/>
<input
type='password'
placeholder='密码'
value={newUser.password}
onChange={(e) =>
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'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<select
value={newUser.userGroup}
onChange={(e) =>
setNewUser((prev) => ({ ...prev, userGroup: 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-green-500 focus:border-transparent'
>
<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>
</div>
<div className='flex justify-end'>
<button
onClick={handleAddUser}
disabled={!newUser.username || !newUser.password}
className='px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white rounded-lg transition-colors'
>
</button>
</div>
</div>
</div>
)}
@@ -452,6 +694,12 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
>
</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'
>
</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'
@@ -534,6 +782,51 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
{!user.banned ? '正常' : '已封禁'}
</span>
</td>
<td className='px-6 py-4 whitespace-nowrap'>
<div className='flex items-center space-x-2'>
<span className='text-sm text-gray-900 dark:text-gray-100'>
{user.tags && user.tags.length > 0
? user.tags.join(', ')
: '无用户组'}
</span>
{/* 配置用户组按钮 */}
{(role === 'owner' ||
(role === 'admin' &&
(user.role === 'user' ||
user.username === currentUsername))) && (
<button
onClick={async () => {
const currentGroups = user.tags || [];
const result = await Swal.fire({
title: '配置用户组',
input: 'select',
inputOptions: Object.fromEntries([
['', '无用户组(无限制)'],
...userGroups.map(group => [group.name, `${group.name} (${group.enabledApis && group.enabledApis.length > 0 ? `${group.enabledApis.length} 个源` : '无限制'})`])
]),
inputValue: currentGroups.length > 0 ? currentGroups[0] : '',
inputPlaceholder: '选择用户组',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValidator: (value) => {
if (value === undefined) {
return '请选择用户组';
}
}
});
if (result.isConfirmed && result.value !== undefined) {
const groups = result.value ? [result.value] : [];
handleAssignUserGroup(user.username, groups);
}
}}
className='inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 dark:text-blue-200 transition-colors'
>
</button>
)}
</div>
</td>
<td className='px-6 py-4 whitespace-nowrap'>
<div className='flex items-center space-x-2'>
<span className='text-sm text-gray-900 dark:text-gray-100'>
@@ -752,6 +1045,239 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</div>,
document.body
)}
{/* 添加用户组弹窗 */}
{showAddUserGroupForm && createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'>
<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={() => {
setShowAddUserGroupForm(false);
setNewUserGroup({ name: '', enabledApis: [] });
}}
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='space-y-6'>
{/* 用户组名称 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<input
type='text'
placeholder='请输入用户组名称'
value={newUserGroup.name}
onChange={(e) =>
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'
/>
</div>
{/* 可用视频源 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
</label>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'>
{config?.SourceConfig?.map((source) => (
<label key={source.key} className='flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors'>
<input
type='checkbox'
checked={newUserGroup.enabledApis.includes(source.key)}
onChange={(e) => {
if (e.target.checked) {
setNewUserGroup(prev => ({
...prev,
enabledApis: [...prev.enabledApis, source.key]
}));
} else {
setNewUserGroup(prev => ({
...prev,
enabledApis: prev.enabledApis.filter(api => api !== source.key)
}));
}
}}
className='rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700'
/>
<div className='flex-1 min-w-0'>
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate'>
{source.name}
</div>
{source.api && (
<div className='text-xs text-gray-500 dark:text-gray-400 truncate'>
{extractDomain(source.api)}
</div>
)}
</div>
</label>
))}
</div>
{/* 快速操作按钮 */}
<div className='mt-4 flex space-x-2'>
<button
onClick={() => setNewUserGroup(prev => ({ ...prev, enabledApis: [] }))}
className='px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors'
>
</button>
<button
onClick={() => {
const allApis = config?.SourceConfig?.filter(source => !source.disabled).map(s => s.key) || [];
setNewUserGroup(prev => ({ ...prev, enabledApis: allApis }));
}}
className='px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors'
>
</button>
</div>
</div>
{/* 操作按钮 */}
<div className='flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700'>
<button
onClick={() => {
setShowAddUserGroupForm(false);
setNewUserGroup({ name: '', enabledApis: [] });
}}
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={handleAddUserGroup}
disabled={!newUserGroup.name.trim()}
className='px-6 py-2.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 rounded-lg transition-colors'
>
</button>
</div>
</div>
</div>
</div>
</div>,
document.body
)}
{/* 编辑用户组弹窗 */}
{showEditUserGroupForm && editingUserGroup && createPortal(
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-4xl w-full max-h-[80vh] overflow-y-auto'>
<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'>
- {editingUserGroup.name}
</h3>
<button
onClick={() => {
setShowEditUserGroupForm(false);
setEditingUserGroup(null);
}}
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='space-y-6'>
{/* 可用视频源 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-4'>
</label>
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'>
{config?.SourceConfig?.map((source) => (
<label key={source.key} className='flex items-center space-x-3 p-3 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer transition-colors'>
<input
type='checkbox'
checked={editingUserGroup.enabledApis.includes(source.key)}
onChange={(e) => {
if (e.target.checked) {
setEditingUserGroup(prev => prev ? {
...prev,
enabledApis: [...prev.enabledApis, source.key]
} : null);
} else {
setEditingUserGroup(prev => prev ? {
...prev,
enabledApis: prev.enabledApis.filter(api => api !== source.key)
} : null);
}
}}
className='rounded border-gray-300 text-purple-600 focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700'
/>
<div className='flex-1 min-w-0'>
<div className='text-sm font-medium text-gray-900 dark:text-gray-100 truncate'>
{source.name}
</div>
{source.api && (
<div className='text-xs text-gray-500 dark:text-gray-400 truncate'>
{extractDomain(source.api)}
</div>
)}
</div>
</label>
))}
</div>
{/* 快速操作按钮 */}
<div className='mt-4 flex space-x-2'>
<button
onClick={() => setEditingUserGroup(prev => prev ? { ...prev, enabledApis: [] } : null)}
className='px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors'
>
</button>
<button
onClick={() => {
const allApis = config?.SourceConfig?.filter(source => !source.disabled).map(s => s.key) || [];
setEditingUserGroup(prev => prev ? { ...prev, enabledApis: allApis } : null);
}}
className='px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-400 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-md transition-colors'
>
</button>
</div>
</div>
{/* 操作按钮 */}
<div className='flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-gray-700'>
<button
onClick={() => {
setShowEditUserGroupForm(false);
setEditingUserGroup(null);
}}
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={handleEditUserGroup}
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>
</div>,
document.body
)}
</div>
);
};

View File

@@ -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 });
}

View File

@@ -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: {

View File

@@ -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<number> {
export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
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<string>();
// 遍历用户的所有 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) {