feat: add batch source and user operates
This commit is contained in:
+432
-22
@@ -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';
|
'use client';
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ import {
|
|||||||
Video,
|
Video,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { GripVertical } 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 { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
import { AdminConfig, AdminConfigResult } from '@/lib/admin.types';
|
||||||
@@ -301,6 +301,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [selectedUserGroups, setSelectedUserGroups] = useState<string[]>([]);
|
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 [showDeleteUserGroupModal, setShowDeleteUserGroupModal] = useState(false);
|
||||||
const [deletingUserGroup, setDeletingUserGroup] = useState<{
|
const [deletingUserGroup, setDeletingUserGroup] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
@@ -312,6 +315,17 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
// 当前登录用户名
|
// 当前登录用户名
|
||||||
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
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 || [];
|
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域名的辅助函数
|
// 提取URL域名的辅助函数
|
||||||
const extractDomain = (url: string): string => {
|
const extractDomain = (url: string): string => {
|
||||||
try {
|
try {
|
||||||
@@ -636,9 +713,9 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</div>
|
</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'>
|
<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>
|
<tr>
|
||||||
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
<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 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
|
||||||
用户列表
|
用户列表
|
||||||
</h4>
|
</h4>
|
||||||
<button
|
<div className='flex items-center space-x-2'>
|
||||||
onClick={() => {
|
{/* 批量操作按钮 */}
|
||||||
setShowAddUserForm(!showAddUserForm);
|
{selectedUsers.size > 0 && (
|
||||||
if (showChangePasswordForm) {
|
<>
|
||||||
setShowChangePasswordForm(false);
|
<div className='flex items-center space-x-3'>
|
||||||
setChangePasswordUser({ username: '', password: '' });
|
<span className='text-sm text-gray-600 dark:text-gray-400'>
|
||||||
}
|
已选择 {selectedUsers.size} 个用户
|
||||||
}}
|
</span>
|
||||||
className='px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-lg transition-colors'
|
<button
|
||||||
>
|
onClick={() => setShowBatchUserGroupModal(true)}
|
||||||
{showAddUserForm ? '取消' : '添加用户'}
|
className='px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors'
|
||||||
</button>
|
>
|
||||||
|
批量设置用户组
|
||||||
|
</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>
|
</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'>
|
<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>
|
<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
|
<th
|
||||||
scope='col'
|
scope='col'
|
||||||
className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'
|
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}
|
key={user.username}
|
||||||
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors'
|
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'>
|
<td className='px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100'>
|
||||||
{user.username}
|
{user.username}
|
||||||
</td>
|
</td>
|
||||||
@@ -1669,6 +1804,92 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
document.body
|
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
|
<AlertModal
|
||||||
isOpen={alertModal.isOpen}
|
isOpen={alertModal.isOpen}
|
||||||
@@ -1706,6 +1927,29 @@ const VideoSourceConfig = ({
|
|||||||
from: 'config',
|
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 [showValidationModal, setShowValidationModal] = useState(false);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
@@ -1739,6 +1983,8 @@ const VideoSourceConfig = ({
|
|||||||
setSources(config.SourceConfig);
|
setSources(config.SourceConfig);
|
||||||
// 进入时重置 orderChanged
|
// 进入时重置 orderChanged
|
||||||
setOrderChanged(false);
|
setOrderChanged(false);
|
||||||
|
// 重置选择状态
|
||||||
|
setSelectedSources(new Set());
|
||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
@@ -1981,6 +2227,14 @@ const VideoSourceConfig = ({
|
|||||||
>
|
>
|
||||||
<GripVertical size={16} />
|
<GripVertical size={16} />
|
||||||
</td>
|
</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'>
|
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100'>
|
||||||
{source.name}
|
{source.name}
|
||||||
</td>
|
</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) {
|
if (!config) {
|
||||||
return (
|
return (
|
||||||
<div className='text-center text-gray-500 dark:text-gray-400'>
|
<div className='text-center text-gray-500 dark:text-gray-400'>
|
||||||
@@ -2065,6 +2390,35 @@ const VideoSourceConfig = ({
|
|||||||
视频源列表
|
视频源列表
|
||||||
</h4>
|
</h4>
|
||||||
<div className='flex items-center space-x-2'>
|
<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
|
<button
|
||||||
onClick={() => setShowValidationModal(true)}
|
onClick={() => setShowValidationModal(true)}
|
||||||
disabled={isValidating}
|
disabled={isValidating}
|
||||||
@@ -2143,12 +2497,22 @@ const VideoSourceConfig = ({
|
|||||||
</div>
|
</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'>
|
<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>
|
<tr>
|
||||||
<th className='w-8' />
|
<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 className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
名称
|
名称
|
||||||
</th>
|
</th>
|
||||||
@@ -2255,6 +2619,52 @@ const VideoSourceConfig = ({
|
|||||||
timer={alertModal.timer}
|
timer={alertModal.timer}
|
||||||
showConfirm={alertModal.showConfirm}
|
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>
|
</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'>
|
<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>
|
<tr>
|
||||||
<th className='w-8' />
|
<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'>
|
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { db } from '@/lib/db';
|
|||||||
export const runtime = 'nodejs';
|
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 {
|
interface BaseBody {
|
||||||
action?: Action;
|
action?: Action;
|
||||||
@@ -37,7 +37,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const username = authInfo.username;
|
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)) {
|
if (!username || !action || !ACTIONS.includes(action)) {
|
||||||
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -111,6 +111,88 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '该源不可删除' }, { status: 400 });
|
return NextResponse.json({ error: '该源不可删除' }, { status: 400 });
|
||||||
}
|
}
|
||||||
adminConfig.SourceConfig.splice(idx, 1);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case 'sort': {
|
case 'sort': {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const ACTIONS = [
|
|||||||
'updateUserApis',
|
'updateUserApis',
|
||||||
'userGroup',
|
'userGroup',
|
||||||
'updateUserGroups',
|
'updateUserGroups',
|
||||||
|
'batchUpdateUserGroups',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -56,8 +57,8 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户组操作不需要targetUsername
|
// 用户组操作和批量操作不需要targetUsername
|
||||||
if (!targetUsername && action !== 'userGroup') {
|
if (!targetUsername && !['userGroup', 'batchUpdateUserGroups'].includes(action)) {
|
||||||
return NextResponse.json({ error: '缺少目标用户名' }, { status: 400 });
|
return NextResponse.json({ error: '缺少目标用户名' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export async function POST(request: NextRequest) {
|
|||||||
action !== 'updateUserApis' &&
|
action !== 'updateUserApis' &&
|
||||||
action !== 'userGroup' &&
|
action !== 'userGroup' &&
|
||||||
action !== 'updateUserGroups' &&
|
action !== 'updateUserGroups' &&
|
||||||
|
action !== 'batchUpdateUserGroups' &&
|
||||||
username === targetUsername
|
username === targetUsername
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -92,11 +94,11 @@ export async function POST(request: NextRequest) {
|
|||||||
operatorRole = 'admin';
|
operatorRole = 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找目标用户条目(用户组操作不需要)
|
// 查找目标用户条目(用户组操作和批量操作不需要)
|
||||||
let targetEntry: any = null;
|
let targetEntry: any = null;
|
||||||
let isTargetAdmin = false;
|
let isTargetAdmin = false;
|
||||||
|
|
||||||
if (action !== 'userGroup' && targetUsername) {
|
if (!['userGroup', 'batchUpdateUserGroups'].includes(action) && targetUsername) {
|
||||||
targetEntry = adminConfig.UserConfig.Users.find(
|
targetEntry = adminConfig.UserConfig.Users.find(
|
||||||
(u) => u.username === targetUsername
|
(u) => u.username === targetUsername
|
||||||
);
|
);
|
||||||
@@ -419,6 +421,38 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
break;
|
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:
|
default:
|
||||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,15 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,12 +79,15 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,12 +150,15 @@ export async function DELETE(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,15 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,12 +51,15 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,12 +117,15 @@ export async function DELETE(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
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 { searchParams } = new URL(request.url);
|
||||||
const query = searchParams.get('q')?.trim();
|
const query = searchParams.get('q')?.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,15 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,12 +61,15 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,12 +113,15 @@ export async function DELETE(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,15 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
if (config.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const user = config.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,12 +60,15 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const adminConfig = await getConfig();
|
const adminConfig = await getConfig();
|
||||||
if (adminConfig.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = adminConfig.UserConfig.Users.find(
|
const user = adminConfig.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,12 +113,15 @@ export async function DELETE(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const adminConfig = await getConfig();
|
const adminConfig = await getConfig();
|
||||||
if (adminConfig.UserConfig.Users) {
|
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
||||||
// 检查用户是否被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = adminConfig.UserConfig.Users.find(
|
const user = adminConfig.UserConfig.Users.find(
|
||||||
(u) => u.username === authInfo.username
|
(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 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user