用户数据结构变更
This commit is contained in:
@@ -213,7 +213,7 @@ const AlertModal = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onConfirm) onConfirm();
|
if (onConfirm) onConfirm();
|
||||||
onClose();
|
// 不要在这里调用onClose,让onConfirm自己决定何时关闭
|
||||||
}}
|
}}
|
||||||
className={buttonStyles.danger}
|
className={buttonStyles.danger}
|
||||||
>
|
>
|
||||||
@@ -422,9 +422,22 @@ interface UserConfigProps {
|
|||||||
config: AdminConfig | null;
|
config: AdminConfig | null;
|
||||||
role: 'owner' | 'admin' | null;
|
role: 'owner' | 'admin' | null;
|
||||||
refreshConfig: () => Promise<void>;
|
refreshConfig: () => Promise<void>;
|
||||||
|
usersV2: Array<{
|
||||||
|
username: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
}> | null;
|
||||||
|
userPage: number;
|
||||||
|
userTotalPages: number;
|
||||||
|
userTotal: number;
|
||||||
|
fetchUsersV2: (page: number) => Promise<void>;
|
||||||
|
userListLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalPages, userTotal, fetchUsersV2, userListLoading }: UserConfigProps) => {
|
||||||
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
const { alertModal, showAlert, hideAlert } = useAlertModal();
|
||||||
const { isLoading, withLoading } = useLoadingState();
|
const { isLoading, withLoading } = useLoadingState();
|
||||||
const [showAddUserForm, setShowAddUserForm] = useState(false);
|
const [showAddUserForm, setShowAddUserForm] = useState(false);
|
||||||
@@ -482,17 +495,30 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
// 当前登录用户名
|
// 当前登录用户名
|
||||||
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
const currentUsername = getAuthInfoFromBrowserCookie()?.username || null;
|
||||||
|
|
||||||
|
// 判断是否有旧版用户数据需要迁移
|
||||||
|
const hasOldUserData = config?.UserConfig?.Users?.filter((u: any) => u.role !== 'owner').length ?? 0 > 0;
|
||||||
|
|
||||||
|
// 使用新版本用户列表(如果可用且没有旧数据),否则使用配置中的用户列表
|
||||||
|
const displayUsers: Array<{
|
||||||
|
username: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned?: boolean;
|
||||||
|
enabledApis?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
created_at?: number;
|
||||||
|
}> = !hasOldUserData && usersV2 ? usersV2 : (config?.UserConfig?.Users || []);
|
||||||
|
|
||||||
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
|
// 使用 useMemo 计算全选状态,避免每次渲染都重新计算
|
||||||
const selectAllUsers = useMemo(() => {
|
const selectAllUsers = useMemo(() => {
|
||||||
const selectableUserCount =
|
const selectableUserCount =
|
||||||
config?.UserConfig?.Users?.filter(
|
displayUsers?.filter(
|
||||||
(user) =>
|
(user) =>
|
||||||
role === 'owner' ||
|
role === 'owner' ||
|
||||||
(role === 'admin' &&
|
(role === 'admin' &&
|
||||||
(user.role === 'user' || user.username === currentUsername))
|
(user.role === 'user' || user.username === currentUsername))
|
||||||
).length || 0;
|
).length || 0;
|
||||||
return selectedUsers.size === selectableUserCount && selectedUsers.size > 0;
|
return selectedUsers.size === selectableUserCount && selectedUsers.size > 0;
|
||||||
}, [selectedUsers.size, config?.UserConfig?.Users, role, currentUsername]);
|
}, [selectedUsers.size, displayUsers, role, currentUsername]);
|
||||||
|
|
||||||
// 获取用户组列表
|
// 获取用户组列表
|
||||||
const userGroups = config?.UserConfig?.Tags || [];
|
const userGroups = config?.UserConfig?.Tags || [];
|
||||||
@@ -878,7 +904,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
throw new Error(data.error || `操作失败: ${res.status}`);
|
throw new Error(data.error || `操作失败: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 成功后刷新配置(无需整页刷新)
|
// 成功后刷新配置和用户列表(refreshConfig 已经是 refreshConfigAndUsers)
|
||||||
await refreshConfig();
|
await refreshConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '操作失败', showAlert);
|
showError(err instanceof Error ? err.message : '操作失败', showAlert);
|
||||||
@@ -916,12 +942,78 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</h4>
|
</h4>
|
||||||
<div className='p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800'>
|
<div className='p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800'>
|
||||||
<div className='text-2xl font-bold text-green-800 dark:text-green-300'>
|
<div className='text-2xl font-bold text-green-800 dark:text-green-300'>
|
||||||
{config.UserConfig.Users.length}
|
{!hasOldUserData && usersV2 ? userTotal : displayUsers.length}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-sm text-green-600 dark:text-green-400'>
|
<div className='text-sm text-green-600 dark:text-green-400'>
|
||||||
总用户数
|
总用户数
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 数据迁移提示 */}
|
||||||
|
{config.UserConfig.Users &&
|
||||||
|
config.UserConfig.Users.filter(u => u.role !== 'owner').length > 0 && (
|
||||||
|
<div className='mt-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800'>
|
||||||
|
<div className='flex items-start justify-between'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<h5 className='text-sm font-medium text-yellow-800 dark:text-yellow-300 mb-1'>
|
||||||
|
检测到旧版用户数据
|
||||||
|
</h5>
|
||||||
|
<p className='text-xs text-yellow-600 dark:text-yellow-400'>
|
||||||
|
建议迁移到新的用户存储结构,以获得更好的性能和安全性。迁移后用户密码将使用SHA256加密。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
showAlert({
|
||||||
|
type: 'warning',
|
||||||
|
title: '确认迁移用户数据',
|
||||||
|
message: '迁移过程中请勿关闭页面。迁移完成后,所有用户密码将使用SHA256加密存储。',
|
||||||
|
showConfirm: true,
|
||||||
|
onConfirm: async () => {
|
||||||
|
hideAlert();
|
||||||
|
await withLoading('migrateUsers', async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/migrate-users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || '迁移失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
showAlert({
|
||||||
|
type: 'success',
|
||||||
|
title: '用户数据迁移成功',
|
||||||
|
message: '所有用户已迁移到新的存储结构',
|
||||||
|
timer: 2000,
|
||||||
|
});
|
||||||
|
await refreshConfig();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('迁移用户数据失败:', error);
|
||||||
|
showAlert({
|
||||||
|
type: 'error',
|
||||||
|
title: '迁移失败',
|
||||||
|
message: error.message || '迁移用户数据时发生错误',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={isLoading('migrateUsers')}
|
||||||
|
className={`ml-4 ${buttonStyles.warning} ${
|
||||||
|
isLoading('migrateUsers') ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isLoading('migrateUsers') ? '迁移中...' : '立即迁移'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 用户组管理 */}
|
{/* 用户组管理 */}
|
||||||
@@ -1195,10 +1287,31 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 用户列表 */}
|
{/* 用户列表 */}
|
||||||
<div
|
<div className='relative'>
|
||||||
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'
|
{config.UserConfig.Users &&
|
||||||
>
|
config.UserConfig.Users.filter(u => u.role !== 'owner').length > 0 && (
|
||||||
|
<div className='absolute inset-0 z-20 backdrop-blur-sm bg-white/30 dark:bg-gray-900/30 rounded-lg flex items-center justify-center'>
|
||||||
|
<div className='bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl border border-yellow-200 dark:border-yellow-800 max-w-md'>
|
||||||
|
<div className='flex items-center gap-3 mb-4'>
|
||||||
|
<AlertTriangle className='w-6 h-6 text-yellow-600 dark:text-yellow-400' />
|
||||||
|
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100'>
|
||||||
|
需要迁移数据
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className='text-sm text-gray-600 dark:text-gray-400 mb-4'>
|
||||||
|
检测到旧版用户数据,请先迁移到新的存储结构后再进行用户管理操作。
|
||||||
|
</p>
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-500'>
|
||||||
|
请在上方的"用户统计"区域点击"立即迁移"按钮完成数据迁移。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<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 sticky top-0 z-10'>
|
<thead className='bg-gray-50 dark:bg-gray-900 sticky top-0 z-10'>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1266,8 +1379,21 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</thead>
|
</thead>
|
||||||
{/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */}
|
{/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const sortedUsers = [...config.UserConfig.Users].sort((a, b) => {
|
// 如果正在加载,显示加载状态
|
||||||
type UserInfo = (typeof config.UserConfig.Users)[number];
|
if (userListLoading) {
|
||||||
|
return (
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={7} className='px-6 py-8 text-center text-gray-500 dark:text-gray-400'>
|
||||||
|
加载中...
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedUsers = [...displayUsers].sort((a, b) => {
|
||||||
|
type UserInfo = (typeof displayUsers)[number];
|
||||||
const priority = (u: UserInfo) => {
|
const priority = (u: UserInfo) => {
|
||||||
if (u.username === currentUsername) return 0;
|
if (u.username === currentUsername) return 0;
|
||||||
if (u.role === 'owner') return 1;
|
if (u.role === 'owner') return 1;
|
||||||
@@ -1507,6 +1633,62 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
})()}
|
})()}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 用户列表分页 */}
|
||||||
|
{!hasOldUserData && usersV2 && userTotalPages > 1 && (
|
||||||
|
<div className='mt-4 flex items-center justify-between px-4'>
|
||||||
|
<div className='text-sm text-gray-600 dark:text-gray-400'>
|
||||||
|
共 {userTotal} 个用户,第 {userPage} / {userTotalPages} 页
|
||||||
|
</div>
|
||||||
|
<div className='flex items-center space-x-2'>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchUsersV2(1)}
|
||||||
|
disabled={userPage === 1}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
userPage === 1
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
首页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchUsersV2(userPage - 1)}
|
||||||
|
disabled={userPage === 1}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
userPage === 1
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchUsersV2(userPage + 1)}
|
||||||
|
disabled={userPage === userTotalPages}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
userPage === userTotalPages
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => fetchUsersV2(userTotalPages)}
|
||||||
|
disabled={userPage === userTotalPages}
|
||||||
|
className={`px-3 py-1 text-sm rounded ${
|
||||||
|
userPage === userTotalPages
|
||||||
|
? 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-600 cursor-not-allowed'
|
||||||
|
: 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
末页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 配置用户采集源权限弹窗 */}
|
{/* 配置用户采集源权限弹窗 */}
|
||||||
@@ -2555,6 +2737,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
message={alertModal.message}
|
message={alertModal.message}
|
||||||
timer={alertModal.timer}
|
timer={alertModal.timer}
|
||||||
showConfirm={alertModal.showConfirm}
|
showConfirm={alertModal.showConfirm}
|
||||||
|
onConfirm={alertModal.onConfirm}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -7758,17 +7941,67 @@ function AdminPageClient() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 新版本用户列表状态
|
||||||
|
const [usersV2, setUsersV2] = useState<Array<{
|
||||||
|
username: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
}> | null>(null);
|
||||||
|
|
||||||
|
// 用户列表分页状态
|
||||||
|
const [userPage, setUserPage] = useState(1);
|
||||||
|
const [userTotalPages, setUserTotalPages] = useState(1);
|
||||||
|
const [userTotal, setUserTotal] = useState(0);
|
||||||
|
const [userListLoading, setUserListLoading] = useState(false);
|
||||||
|
const userLimit = 10;
|
||||||
|
|
||||||
|
// 获取新版本用户列表
|
||||||
|
const fetchUsersV2 = useCallback(async (page: number = 1) => {
|
||||||
|
try {
|
||||||
|
setUserListLoading(true);
|
||||||
|
const response = await fetch(`/api/admin/users?page=${page}&limit=${userLimit}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setUsersV2(data.users);
|
||||||
|
setUserTotalPages(data.totalPages || 1);
|
||||||
|
setUserTotal(data.total || 0);
|
||||||
|
setUserPage(page);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取新版本用户列表失败:', err);
|
||||||
|
} finally {
|
||||||
|
setUserListLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 刷新配置和用户列表
|
||||||
|
const refreshConfigAndUsers = useCallback(async () => {
|
||||||
|
await fetchConfig();
|
||||||
|
await fetchUsersV2();
|
||||||
|
}, [fetchConfig, fetchUsersV2]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 首次加载时显示骨架
|
// 首次加载时显示骨架
|
||||||
fetchConfig(true);
|
fetchConfig(true);
|
||||||
|
// 不再自动获取用户列表,等用户打开用户管理选项卡时再获取
|
||||||
}, [fetchConfig]);
|
}, [fetchConfig]);
|
||||||
|
|
||||||
// 切换标签展开状态
|
// 切换标签展开状态
|
||||||
const toggleTab = (tabKey: string) => {
|
const toggleTab = (tabKey: string) => {
|
||||||
|
const wasExpanded = expandedTabs[tabKey];
|
||||||
|
|
||||||
setExpandedTabs((prev) => ({
|
setExpandedTabs((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[tabKey]: !prev[tabKey],
|
[tabKey]: !prev[tabKey],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 当打开用户管理选项卡时,如果还没有加载用户列表,则加载
|
||||||
|
if (tabKey === 'userConfig' && !wasExpanded && !usersV2) {
|
||||||
|
fetchUsersV2();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新增: 重置配置处理函数
|
// 新增: 重置配置处理函数
|
||||||
@@ -7917,7 +8150,13 @@ function AdminPageClient() {
|
|||||||
<UserConfig
|
<UserConfig
|
||||||
config={config}
|
config={config}
|
||||||
role={role}
|
role={role}
|
||||||
refreshConfig={fetchConfig}
|
refreshConfig={refreshConfigAndUsers}
|
||||||
|
usersV2={usersV2}
|
||||||
|
userPage={userPage}
|
||||||
|
userTotalPages={userTotalPages}
|
||||||
|
userTotal={userTotal}
|
||||||
|
fetchUsersV2={fetchUsersV2}
|
||||||
|
userListLoading={userListLoading}
|
||||||
/>
|
/>
|
||||||
</CollapsibleTab>
|
</CollapsibleTab>
|
||||||
|
|
||||||
@@ -8013,7 +8252,7 @@ function AdminPageClient() {
|
|||||||
isExpanded={expandedTabs.dataMigration}
|
isExpanded={expandedTabs.dataMigration}
|
||||||
onToggle={() => toggleTab('dataMigration')}
|
onToggle={() => toggleTab('dataMigration')}
|
||||||
>
|
>
|
||||||
<DataMigration onRefreshConfig={fetchConfig} />
|
<DataMigration onRefreshConfig={refreshConfigAndUsers} />
|
||||||
</CollapsibleTab>
|
</CollapsibleTab>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,14 +34,31 @@ export async function GET(request: NextRequest) {
|
|||||||
if (username === process.env.USERNAME) {
|
if (username === process.env.USERNAME) {
|
||||||
result.Role = 'owner';
|
result.Role = 'owner';
|
||||||
} else {
|
} else {
|
||||||
const user = config.UserConfig.Users.find((u) => u.username === username);
|
// 优先从新版本获取用户信息
|
||||||
if (user && user.role === 'admin' && !user.banned) {
|
const { db } = await import('@/lib/db');
|
||||||
result.Role = 'admin';
|
const userInfoV2 = await db.getUserInfoV2(username);
|
||||||
|
|
||||||
|
if (userInfoV2) {
|
||||||
|
// 使用新版本用户信息
|
||||||
|
if (userInfoV2.role === 'admin' && !userInfoV2.banned) {
|
||||||
|
result.Role = 'admin';
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '你是管理员吗你就访问?' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return NextResponse.json(
|
// 回退到配置中查找
|
||||||
{ error: '你是管理员吗你就访问?' },
|
const user = config.UserConfig.Users.find((u) => u.username === username);
|
||||||
{ status: 401 }
|
if (user && user.role === 'admin' && !user.banned) {
|
||||||
);
|
result.Role = 'admin';
|
||||||
|
} else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '你是管理员吗你就访问?' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
76
src/app/api/admin/migrate-users/route.ts
Normal file
76
src/app/api/admin/migrate-users/route.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||||
|
if (storageType === 'localstorage') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: '不支持本地存储进行数据迁移',
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有站长可以执行迁移
|
||||||
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
|
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
const adminConfig = await getConfig();
|
||||||
|
|
||||||
|
// 检查是否有需要迁移的用户(排除站长)
|
||||||
|
const usersToMigrate = adminConfig.UserConfig.Users.filter(
|
||||||
|
u => u.role !== 'owner'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!usersToMigrate || usersToMigrate.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '没有需要迁移的用户' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行迁移
|
||||||
|
await db.migrateUsersFromConfig(adminConfig);
|
||||||
|
|
||||||
|
// 迁移完成后,清空配置中的用户列表
|
||||||
|
adminConfig.UserConfig.Users = [];
|
||||||
|
await db.saveAdminConfig(adminConfig);
|
||||||
|
|
||||||
|
// 更新配置缓存
|
||||||
|
const { setCachedConfig } = await import('@/lib/config');
|
||||||
|
await setCachedConfig(adminConfig);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: true, message: '用户数据迁移成功' },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('用户数据迁移失败:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: '用户数据迁移失败',
|
||||||
|
details: (error as Error).message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,24 +85,50 @@ export async function POST(request: NextRequest) {
|
|||||||
if (username === process.env.USERNAME) {
|
if (username === process.env.USERNAME) {
|
||||||
operatorRole = 'owner';
|
operatorRole = 'owner';
|
||||||
} else {
|
} else {
|
||||||
const userEntry = adminConfig.UserConfig.Users.find(
|
// 优先从新版本获取用户信息
|
||||||
(u) => u.username === username
|
const operatorInfo = await db.getUserInfoV2(username);
|
||||||
);
|
if (operatorInfo) {
|
||||||
if (!userEntry || userEntry.role !== 'admin' || userEntry.banned) {
|
if (operatorInfo.role !== 'admin' || operatorInfo.banned) {
|
||||||
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
||||||
|
}
|
||||||
|
operatorRole = 'admin';
|
||||||
|
} else {
|
||||||
|
// 回退到配置中查找
|
||||||
|
const userEntry = adminConfig.UserConfig.Users.find(
|
||||||
|
(u) => u.username === username
|
||||||
|
);
|
||||||
|
if (!userEntry || userEntry.role !== 'admin' || userEntry.banned) {
|
||||||
|
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
||||||
|
}
|
||||||
|
operatorRole = 'admin';
|
||||||
}
|
}
|
||||||
operatorRole = 'admin';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找目标用户条目(用户组操作和批量操作不需要)
|
// 查找目标用户条目(用户组操作和批量操作不需要)
|
||||||
let targetEntry: any = null;
|
let targetEntry: any = null;
|
||||||
let isTargetAdmin = false;
|
let isTargetAdmin = false;
|
||||||
|
let targetUserV2: any = null;
|
||||||
|
|
||||||
if (!['userGroup', 'batchUpdateUserGroups'].includes(action) && 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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 如果配置中没有,从新版本存储中查找
|
||||||
|
if (!targetEntry) {
|
||||||
|
targetUserV2 = await db.getUserInfoV2(targetUsername);
|
||||||
|
if (targetUserV2) {
|
||||||
|
// 构造一个兼容的targetEntry对象
|
||||||
|
targetEntry = {
|
||||||
|
username: targetUsername,
|
||||||
|
role: targetUserV2.role,
|
||||||
|
banned: targetUserV2.banned,
|
||||||
|
tags: targetUserV2.tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
targetEntry &&
|
targetEntry &&
|
||||||
targetEntry.role === 'owner' &&
|
targetEntry.role === 'owner' &&
|
||||||
@@ -120,33 +146,35 @@ export async function POST(request: NextRequest) {
|
|||||||
if (targetEntry) {
|
if (targetEntry) {
|
||||||
return NextResponse.json({ error: '用户已存在' }, { status: 400 });
|
return NextResponse.json({ error: '用户已存在' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
// 检查新版本中是否已存在
|
||||||
|
const existsV2 = await db.checkUserExistV2(targetUsername!);
|
||||||
|
if (existsV2) {
|
||||||
|
return NextResponse.json({ error: '用户已存在' }, { status: 400 });
|
||||||
|
}
|
||||||
if (!targetPassword) {
|
if (!targetPassword) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '缺少目标用户密码' },
|
{ error: '缺少目标用户密码' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await db.registerUser(targetUsername!, targetPassword);
|
|
||||||
|
|
||||||
// 获取用户组信息
|
// 获取用户组信息
|
||||||
const { userGroup } = body as { userGroup?: string };
|
const { userGroup } = body as { userGroup?: string };
|
||||||
|
const tags = userGroup && userGroup.trim() ? [userGroup] : undefined;
|
||||||
|
|
||||||
// 更新配置
|
// 使用新版本创建用户
|
||||||
const newUser: any = {
|
await db.createUserV2(targetUsername!, targetPassword, 'user', tags);
|
||||||
|
|
||||||
|
// 同时在旧版本存储中创建(保持兼容性)
|
||||||
|
await db.registerUser(targetUsername!, targetPassword);
|
||||||
|
|
||||||
|
// 不再更新配置,因为用户已经存储在新版本中
|
||||||
|
// 构造一个虚拟的targetEntry用于后续逻辑
|
||||||
|
targetEntry = {
|
||||||
username: targetUsername!,
|
username: targetUsername!,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
|
tags,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果指定了用户组,添加到tags中
|
|
||||||
if (userGroup && userGroup.trim()) {
|
|
||||||
newUser.tags = [userGroup];
|
|
||||||
}
|
|
||||||
|
|
||||||
adminConfig.UserConfig.Users.push(newUser);
|
|
||||||
targetEntry =
|
|
||||||
adminConfig.UserConfig.Users[
|
|
||||||
adminConfig.UserConfig.Users.length - 1
|
|
||||||
];
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ban': {
|
case 'ban': {
|
||||||
@@ -165,7 +193,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
targetEntry.banned = true;
|
|
||||||
|
// 只更新V2存储
|
||||||
|
await db.updateUserInfoV2(targetUsername!, { banned: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'unban': {
|
case 'unban': {
|
||||||
@@ -183,7 +213,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
targetEntry.banned = false;
|
|
||||||
|
// 只更新V2存储
|
||||||
|
await db.updateUserInfoV2(targetUsername!, { banned: false });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'setAdmin': {
|
case 'setAdmin': {
|
||||||
@@ -205,7 +237,9 @@ export async function POST(request: NextRequest) {
|
|||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
targetEntry.role = 'admin';
|
|
||||||
|
// 只更新V2存储
|
||||||
|
await db.updateUserInfoV2(targetUsername!, { role: 'admin' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'cancelAdmin': {
|
case 'cancelAdmin': {
|
||||||
@@ -227,7 +261,9 @@ export async function POST(request: NextRequest) {
|
|||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
targetEntry.role = 'user';
|
|
||||||
|
// 只更新V2存储
|
||||||
|
await db.updateUserInfoV2(targetUsername!, { role: 'user' });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'changePassword': {
|
case 'changePassword': {
|
||||||
@@ -260,6 +296,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用新版本修改密码(SHA256加密)
|
||||||
|
await db.changePasswordV2(targetUsername!, targetPassword);
|
||||||
|
// 同时更新旧版本(保持兼容性)
|
||||||
await db.changePassword(targetUsername!, targetPassword);
|
await db.changePassword(targetUsername!, targetPassword);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -286,16 +325,11 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只删除V2存储中的用户
|
||||||
|
await db.deleteUserV2(targetUsername!);
|
||||||
|
// 同时删除旧版本(保持兼容性)
|
||||||
await db.deleteUser(targetUsername!);
|
await db.deleteUser(targetUsername!);
|
||||||
|
|
||||||
// 从配置中移除用户
|
|
||||||
const userIndex = adminConfig.UserConfig.Users.findIndex(
|
|
||||||
(u) => u.username === targetUsername
|
|
||||||
);
|
|
||||||
if (userIndex > -1) {
|
|
||||||
adminConfig.UserConfig.Users.splice(userIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'updateUserApis': {
|
case 'updateUserApis': {
|
||||||
@@ -320,13 +354,10 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新用户的采集源权限
|
// 更新V2存储中的采集源权限
|
||||||
if (enabledApis && enabledApis.length > 0) {
|
await db.updateUserInfoV2(targetUsername!, {
|
||||||
targetEntry.enabledApis = enabledApis;
|
enabledApis: enabledApis && enabledApis.length > 0 ? enabledApis : []
|
||||||
} else {
|
});
|
||||||
// 如果为空数组或未提供,则删除该字段,表示无限制
|
|
||||||
delete targetEntry.enabledApis;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -368,19 +399,17 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '用户组不存在' }, { status: 404 });
|
return NextResponse.json({ error: '用户组不存在' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找使用该用户组的所有用户
|
// 查找使用该用户组的所有用户(从V2存储中查找)
|
||||||
const affectedUsers: string[] = [];
|
const affectedUsers = await db.getUsersByTag(groupName);
|
||||||
adminConfig.UserConfig.Users.forEach(user => {
|
|
||||||
if (user.tags && user.tags.includes(groupName)) {
|
// 从用户的tags中移除该用户组
|
||||||
affectedUsers.push(user.username);
|
for (const username of affectedUsers) {
|
||||||
// 从用户的tags中移除该用户组
|
const userInfo = await db.getUserInfoV2(username);
|
||||||
user.tags = user.tags.filter(tag => tag !== groupName);
|
if (userInfo && userInfo.tags) {
|
||||||
// 如果用户没有其他标签了,删除tags字段
|
const newTags = userInfo.tags.filter(tag => tag !== groupName);
|
||||||
if (user.tags.length === 0) {
|
await db.updateUserInfoV2(username, { tags: newTags });
|
||||||
delete user.tags;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// 删除用户组
|
// 删除用户组
|
||||||
adminConfig.UserConfig.Tags.splice(groupIndex, 1);
|
adminConfig.UserConfig.Tags.splice(groupIndex, 1);
|
||||||
@@ -413,10 +442,11 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// 更新用户的用户组
|
// 更新用户的用户组
|
||||||
if (userGroups && userGroups.length > 0) {
|
if (userGroups && userGroups.length > 0) {
|
||||||
targetEntry.tags = userGroups;
|
// 只更新V2存储
|
||||||
|
await db.updateUserInfoV2(targetUsername!, { tags: userGroups });
|
||||||
} else {
|
} else {
|
||||||
// 如果为空数组或未提供,则删除该字段,表示无用户组
|
// 如果为空数组或未提供,则删除该字段,表示无用户组
|
||||||
delete targetEntry.tags;
|
await db.updateUserInfoV2(targetUsername!, { tags: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -431,7 +461,20 @@ export async function POST(request: NextRequest) {
|
|||||||
// 权限检查:站长可批量配置所有人的用户组,管理员只能批量配置普通用户
|
// 权限检查:站长可批量配置所有人的用户组,管理员只能批量配置普通用户
|
||||||
if (operatorRole !== 'owner') {
|
if (operatorRole !== 'owner') {
|
||||||
for (const targetUsername of usernames) {
|
for (const targetUsername of usernames) {
|
||||||
const targetUser = adminConfig.UserConfig.Users.find(u => u.username === targetUsername);
|
// 先从配置中查找
|
||||||
|
let targetUser = adminConfig.UserConfig.Users.find(u => u.username === targetUsername);
|
||||||
|
// 如果配置中没有,从V2存储中查找
|
||||||
|
if (!targetUser) {
|
||||||
|
const userV2 = await db.getUserInfoV2(targetUsername);
|
||||||
|
if (userV2) {
|
||||||
|
targetUser = {
|
||||||
|
username: targetUsername,
|
||||||
|
role: userV2.role,
|
||||||
|
banned: userV2.banned,
|
||||||
|
tags: userV2.tags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if (targetUser && targetUser.role === 'admin' && targetUsername !== username) {
|
if (targetUser && targetUser.role === 'admin' && targetUsername !== username) {
|
||||||
return NextResponse.json({ error: `管理员无法操作其他管理员 ${targetUsername}` }, { status: 400 });
|
return NextResponse.json({ error: `管理员无法操作其他管理员 ${targetUsername}` }, { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -440,14 +483,11 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// 批量更新用户组
|
// 批量更新用户组
|
||||||
for (const targetUsername of usernames) {
|
for (const targetUsername of usernames) {
|
||||||
const targetUser = adminConfig.UserConfig.Users.find(u => u.username === targetUsername);
|
// 只更新V2存储
|
||||||
if (targetUser) {
|
if (userGroups && userGroups.length > 0) {
|
||||||
if (userGroups && userGroups.length > 0) {
|
await db.updateUserInfoV2(targetUsername, { tags: userGroups });
|
||||||
targetUser.tags = userGroups;
|
} else {
|
||||||
} else {
|
await db.updateUserInfoV2(targetUsername, { tags: [] });
|
||||||
// 如果为空数组或未提供,则删除该字段,表示无用户组
|
|
||||||
delete targetUser.tags;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
src/app/api/admin/users/route.ts
Normal file
129
src/app/api/admin/users/route.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||||
|
if (storageType === 'localstorage') {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: '不支持本地存储进行用户列表查询',
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authInfo = getAuthInfoFromCookie(request);
|
||||||
|
if (!authInfo || !authInfo.username) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置
|
||||||
|
const adminConfig = await getConfig();
|
||||||
|
|
||||||
|
// 判定操作者角色
|
||||||
|
let operatorRole: 'owner' | 'admin' | 'user' = 'user';
|
||||||
|
if (authInfo.username === process.env.USERNAME) {
|
||||||
|
operatorRole = 'owner';
|
||||||
|
} else {
|
||||||
|
// 优先从新版本获取用户信息
|
||||||
|
const operatorInfo = await db.getUserInfoV2(authInfo.username);
|
||||||
|
if (operatorInfo) {
|
||||||
|
operatorRole = operatorInfo.role;
|
||||||
|
} else {
|
||||||
|
// 回退到配置中查找
|
||||||
|
const userEntry = adminConfig.UserConfig.Users.find(
|
||||||
|
(u) => u.username === authInfo.username
|
||||||
|
);
|
||||||
|
if (userEntry) {
|
||||||
|
operatorRole = userEntry.role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有站长和管理员可以查看用户列表
|
||||||
|
if (operatorRole !== 'owner' && operatorRole !== 'admin') {
|
||||||
|
return NextResponse.json({ error: '权限不足' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取分页参数
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '10', 10);
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
// 获取用户列表(优先使用新版本)
|
||||||
|
const result = await db.getUserListV2(offset, limit, process.env.USERNAME);
|
||||||
|
|
||||||
|
if (result.users.length > 0) {
|
||||||
|
// 使用新版本数据
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
users: result.users,
|
||||||
|
total: result.total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(result.total / limit),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到配置中的用户列表
|
||||||
|
const configUsers = adminConfig.UserConfig.Users || [];
|
||||||
|
const total = configUsers.length;
|
||||||
|
|
||||||
|
// 排序:站长始终在第一位,其他用户按用户名排序
|
||||||
|
const sortedUsers = [...configUsers].sort((a, b) => {
|
||||||
|
if (a.username === process.env.USERNAME) return -1;
|
||||||
|
if (b.username === process.env.USERNAME) return 1;
|
||||||
|
return a.username.localeCompare(b.username);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const paginatedUsers = sortedUsers.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
// 转换为统一格式
|
||||||
|
const users = paginatedUsers.map((u) => ({
|
||||||
|
username: u.username,
|
||||||
|
role: u.role,
|
||||||
|
banned: u.banned || false,
|
||||||
|
tags: u.tags,
|
||||||
|
created_at: 0, // 配置中没有创建时间
|
||||||
|
}));
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
users,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: '获取用户列表失败',
|
||||||
|
details: (error as Error).message,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,15 +148,41 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否已存在(通过OIDC sub查找)
|
// 检查用户是否已存在(通过OIDC sub查找)
|
||||||
const existingUser = config.UserConfig.Users.find((u: any) => u.oidcSub === oidcSub);
|
// 优先使用新版本查找
|
||||||
|
let username = await db.getUserByOidcSub(oidcSub);
|
||||||
|
let userRole: 'owner' | 'admin' | 'user' = 'user';
|
||||||
|
|
||||||
if (existingUser) {
|
if (username) {
|
||||||
|
// 从新版本获取用户信息
|
||||||
|
const userInfoV2 = await db.getUserInfoV2(username);
|
||||||
|
if (userInfoV2) {
|
||||||
|
userRole = userInfoV2.role;
|
||||||
|
// 检查用户是否被封禁
|
||||||
|
if (userInfoV2.banned) {
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL('/login?error=' + encodeURIComponent('用户被封禁'), origin)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 回退到配置中查找
|
||||||
|
const existingUser = config.UserConfig.Users.find((u: any) => u.oidcSub === oidcSub);
|
||||||
|
if (existingUser) {
|
||||||
|
username = existingUser.username;
|
||||||
|
userRole = existingUser.role || 'user';
|
||||||
|
// 检查用户是否被封禁
|
||||||
|
if (existingUser.banned) {
|
||||||
|
return NextResponse.redirect(
|
||||||
|
new URL('/login?error=' + encodeURIComponent('用户被封禁'), origin)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username) {
|
||||||
// 用户已存在,直接登录
|
// 用户已存在,直接登录
|
||||||
const response = NextResponse.redirect(new URL('/', origin));
|
const response = NextResponse.redirect(new URL('/', origin));
|
||||||
const cookieValue = await generateAuthCookie(
|
const cookieValue = await generateAuthCookie(username, userRole);
|
||||||
existingUser.username,
|
|
||||||
existingUser.role || 'user'
|
|
||||||
);
|
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 7);
|
expires.setDate(expires.getDate() + 7);
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,20 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户名是否已存在
|
// 检查用户名是否已存在(优先使用新版本)
|
||||||
|
let userExists = await db.checkUserExistV2(username);
|
||||||
|
if (!userExists) {
|
||||||
|
// 回退到旧版本检查
|
||||||
|
userExists = await db.checkUserExist(username);
|
||||||
|
}
|
||||||
|
if (userExists) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '用户名已存在' },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置中是否已存在
|
||||||
const existingUser = config.UserConfig.Users.find((u) => u.username === username);
|
const existingUser = config.UserConfig.Users.find((u) => u.username === username);
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -119,9 +132,16 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查OIDC sub是否已被使用
|
// 检查OIDC sub是否已被使用(优先使用新版本)
|
||||||
const existingOIDCUser = config.UserConfig.Users.find((u: any) => u.oidcSub === oidcSession.sub);
|
let existingOIDCUsername = await db.getUserByOidcSub(oidcSession.sub);
|
||||||
if (existingOIDCUser) {
|
if (!existingOIDCUsername) {
|
||||||
|
// 回退到配置中查找
|
||||||
|
const existingOIDCUser = config.UserConfig.Users.find((u: any) => u.oidcSub === oidcSession.sub);
|
||||||
|
if (existingOIDCUser) {
|
||||||
|
existingOIDCUsername = existingOIDCUser.username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (existingOIDCUsername) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '该OIDC账号已被注册' },
|
{ error: '该OIDC账号已被注册' },
|
||||||
{ status: 409 }
|
{ status: 409 }
|
||||||
@@ -132,9 +152,19 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
// 生成随机密码(OIDC用户不需要密码登录)
|
// 生成随机密码(OIDC用户不需要密码登录)
|
||||||
const randomPassword = crypto.randomUUID();
|
const randomPassword = crypto.randomUUID();
|
||||||
|
|
||||||
|
// 获取默认用户组
|
||||||
|
const defaultTags = siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0
|
||||||
|
? siteConfig.DefaultUserTags
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// 使用新版本创建用户(带SHA256加密和OIDC绑定)
|
||||||
|
await db.createUserV2(username, randomPassword, 'user', defaultTags, oidcSession.sub);
|
||||||
|
|
||||||
|
// 同时在旧版本存储中创建(保持兼容性)
|
||||||
await db.registerUser(username, randomPassword);
|
await db.registerUser(username, randomPassword);
|
||||||
|
|
||||||
// 将用户添加到配置中
|
// 将用户添加到配置中(保持兼容性)
|
||||||
const newUser: any = {
|
const newUser: any = {
|
||||||
username: username,
|
username: username,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
@@ -143,8 +173,8 @@ export async function POST(request: NextRequest) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 如果配置了默认用户组,分配给新用户
|
// 如果配置了默认用户组,分配给新用户
|
||||||
if (siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0) {
|
if (defaultTags) {
|
||||||
newUser.tags = siteConfig.DefaultUserTags;
|
newUser.tags = defaultTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.UserConfig.Users.push(newUser);
|
config.UserConfig.Users.push(newUser);
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改密码
|
// 修改密码(只更新V2存储)
|
||||||
await db.changePassword(username, newPassword);
|
await db.changePasswordV2(username, newPassword);
|
||||||
|
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -16,16 +16,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,16 +52,13 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminConfig = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = adminConfig.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,16 +24,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,16 +75,13 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,16 +143,13 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,44 +217,69 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const user = config.UserConfig.Users.find((u) => u.username === username);
|
const user = config.UserConfig.Users.find((u) => u.username === username);
|
||||||
if (user && user.banned) {
|
|
||||||
|
// 优先使用新版本的用户验证
|
||||||
|
let pass = false;
|
||||||
|
let userRole: 'owner' | 'admin' | 'user' = 'user';
|
||||||
|
let isBanned = false;
|
||||||
|
|
||||||
|
// 尝试使用新版本验证
|
||||||
|
const userInfoV2 = await db.getUserInfoV2(username);
|
||||||
|
|
||||||
|
if (userInfoV2) {
|
||||||
|
// 使用新版本验证
|
||||||
|
pass = await db.verifyUserV2(username, password);
|
||||||
|
userRole = userInfoV2.role;
|
||||||
|
isBanned = userInfoV2.banned;
|
||||||
|
} else {
|
||||||
|
// 回退到旧版本验证
|
||||||
|
try {
|
||||||
|
pass = await db.verifyUser(username, password);
|
||||||
|
// 从配置中获取角色和封禁状态
|
||||||
|
if (user) {
|
||||||
|
userRole = user.role;
|
||||||
|
isBanned = user.banned || false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('数据库验证失败', err);
|
||||||
|
return NextResponse.json({ error: '数据库错误' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否被封禁
|
||||||
|
if (isBanned) {
|
||||||
return NextResponse.json({ error: '用户被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验用户密码
|
if (!pass) {
|
||||||
try {
|
return NextResponse.json(
|
||||||
const pass = await db.verifyUser(username, password);
|
{ error: '用户名或密码错误' },
|
||||||
if (!pass) {
|
{ status: 401 }
|
||||||
return NextResponse.json(
|
);
|
||||||
{ error: '用户名或密码错误' },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证成功,设置认证cookie
|
|
||||||
const response = NextResponse.json({ ok: true });
|
|
||||||
const cookieValue = await generateAuthCookie(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
user?.role || 'user',
|
|
||||||
false
|
|
||||||
); // 数据库模式不包含 password
|
|
||||||
const expires = new Date();
|
|
||||||
expires.setDate(expires.getDate() + 7); // 7天过期
|
|
||||||
|
|
||||||
response.cookies.set('auth', cookieValue, {
|
|
||||||
path: '/',
|
|
||||||
expires,
|
|
||||||
sameSite: 'lax', // 改为 lax 以支持 PWA
|
|
||||||
httpOnly: false, // PWA 需要客户端可访问
|
|
||||||
secure: false, // 根据协议自动设置
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('数据库验证失败', err);
|
|
||||||
return NextResponse.json({ error: '数据库错误' }, { status: 500 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证成功,设置认证cookie
|
||||||
|
const response = NextResponse.json({ ok: true });
|
||||||
|
const cookieValue = await generateAuthCookie(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
userRole,
|
||||||
|
false
|
||||||
|
); // 数据库模式不包含 password
|
||||||
|
const expires = new Date();
|
||||||
|
expires.setDate(expires.getDate() + 7); // 7天过期
|
||||||
|
|
||||||
|
response.cookies.set('auth', cookieValue, {
|
||||||
|
path: '/',
|
||||||
|
expires,
|
||||||
|
sameSite: 'lax', // 改为 lax 以支持 PWA
|
||||||
|
httpOnly: false, // PWA 需要客户端可访问
|
||||||
|
secure: false, // 根据协议自动设置
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Cookie已设置`);
|
||||||
|
|
||||||
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录接口异常', error);
|
console.error('登录接口异常', error);
|
||||||
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
return NextResponse.json({ error: '服务器错误' }, { status: 500 });
|
||||||
|
|||||||
@@ -17,16 +17,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,16 +47,13 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,16 +110,13 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,20 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户是否已存在
|
// 检查用户是否已存在(优先使用新版本)
|
||||||
|
let userExists = await db.checkUserExistV2(username);
|
||||||
|
if (!userExists) {
|
||||||
|
// 回退到旧版本检查
|
||||||
|
userExists = await db.checkUserExist(username);
|
||||||
|
}
|
||||||
|
if (userExists) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: '用户名已存在' },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查配置中是否已存在
|
||||||
const existingUser = config.UserConfig.Users.find((u) => u.username === username);
|
const existingUser = config.UserConfig.Users.find((u) => u.username === username);
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -131,24 +144,31 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// 创建用户
|
// 创建用户
|
||||||
try {
|
try {
|
||||||
// 1. 在数据库中创建用户密码
|
// 1. 使用新版本创建用户(带SHA256加密)
|
||||||
|
const defaultTags = siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0
|
||||||
|
? siteConfig.DefaultUserTags
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await db.createUserV2(username, password, 'user', defaultTags);
|
||||||
|
|
||||||
|
// 2. 同时在旧版本存储中创建(保持兼容性)
|
||||||
await db.registerUser(username, password);
|
await db.registerUser(username, password);
|
||||||
|
|
||||||
// 2. 将用户添加到管理员配置的用户列表中
|
// 3. 将用户添加到管理员配置的用户列表中(保持兼容性)
|
||||||
const newUser: any = {
|
const newUser: any = {
|
||||||
username: username,
|
username: username,
|
||||||
role: 'user',
|
role: 'user',
|
||||||
banned: false,
|
banned: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. 如果配置了默认用户组,分配给新用户
|
// 4. 如果配置了默认用户组,分配给新用户
|
||||||
if (siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0) {
|
if (defaultTags) {
|
||||||
newUser.tags = siteConfig.DefaultUserTags;
|
newUser.tags = defaultTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.UserConfig.Users.push(newUser);
|
config.UserConfig.Users.push(newUser);
|
||||||
|
|
||||||
// 4. 保存更新后的配置
|
// 5. 保存更新后的配置
|
||||||
await db.saveAdminConfig(config);
|
await db.saveAdminConfig(config);
|
||||||
|
|
||||||
// 注册成功
|
// 注册成功
|
||||||
|
|||||||
@@ -23,16 +23,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,16 +57,13 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,16 +106,13 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,16 +16,13 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = config.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,16 +56,13 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminConfig = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = adminConfig.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,16 +106,13 @@ export async function DELETE(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminConfig = await getConfig();
|
if (authInfo.username !== process.env.USERNAME) {
|
||||||
if (authInfo.username !== process.env.ADMIN_USERNAME) {
|
|
||||||
// 非站长,检查用户存在或被封禁
|
// 非站长,检查用户存在或被封禁
|
||||||
const user = adminConfig.UserConfig.Users.find(
|
const userInfoV2 = await db.getUserInfoV2(authInfo.username);
|
||||||
(u) => u.username === authInfo.username
|
if (!userInfoV2) {
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
return NextResponse.json({ error: '用户不存在' }, { status: 401 });
|
||||||
}
|
}
|
||||||
if (user.banned) {
|
if (userInfoV2.banned) {
|
||||||
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
return NextResponse.json({ error: '用户已被封禁' }, { status: 401 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,14 +470,15 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
|||||||
return allApiSites;
|
return allApiSites;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userConfig = config.UserConfig.Users.find((u) => u.username === user);
|
// 从V2存储中获取用户信息
|
||||||
if (!userConfig) {
|
const userInfoV2 = await db.getUserInfoV2(user);
|
||||||
|
if (!userInfoV2) {
|
||||||
return allApiSites;
|
return allApiSites;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先根据用户自己的 enabledApis 配置查找
|
// 优先根据用户自己的 enabledApis 配置查找
|
||||||
if (userConfig.enabledApis && userConfig.enabledApis.length > 0) {
|
if (userInfoV2.enabledApis && userInfoV2.enabledApis.length > 0) {
|
||||||
const userApiSitesSet = new Set(userConfig.enabledApis);
|
const userApiSitesSet = new Set(userInfoV2.enabledApis);
|
||||||
return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({
|
return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({
|
||||||
key: s.key,
|
key: s.key,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
@@ -487,11 +488,11 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有 enabledApis 配置,则根据 tags 查找
|
// 如果没有 enabledApis 配置,则根据 tags 查找
|
||||||
if (userConfig.tags && userConfig.tags.length > 0 && config.UserConfig.Tags) {
|
if (userInfoV2.tags && userInfoV2.tags.length > 0 && config.UserConfig.Tags) {
|
||||||
const enabledApisFromTags = new Set<string>();
|
const enabledApisFromTags = new Set<string>();
|
||||||
|
|
||||||
// 遍历用户的所有 tags,收集对应的 enabledApis
|
// 遍历用户的所有 tags,收集对应的 enabledApis
|
||||||
userConfig.tags.forEach(tagName => {
|
userInfoV2.tags.forEach(tagName => {
|
||||||
const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName);
|
const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName);
|
||||||
if (tagConfig && tagConfig.enabledApis) {
|
if (tagConfig && tagConfig.enabledApis) {
|
||||||
tagConfig.enabledApis.forEach(apiKey => enabledApisFromTags.add(apiKey));
|
tagConfig.enabledApis.forEach(apiKey => enabledApisFromTags.add(apiKey));
|
||||||
|
|||||||
193
src/lib/db.ts
193
src/lib/db.ts
@@ -132,7 +132,7 @@ export class DbManager {
|
|||||||
return favorite !== null;
|
return favorite !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 用户相关 ----------
|
// ---------- 用户相关(旧版本,保持兼容) ----------
|
||||||
async registerUser(userName: string, password: string): Promise<void> {
|
async registerUser(userName: string, password: string): Promise<void> {
|
||||||
await this.storage.registerUser(userName, password);
|
await this.storage.registerUser(userName, password);
|
||||||
}
|
}
|
||||||
@@ -154,6 +154,197 @@ export class DbManager {
|
|||||||
await this.storage.deleteUser(userName);
|
await this.storage.deleteUser(userName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 用户相关(新版本) ----------
|
||||||
|
async createUserV2(
|
||||||
|
userName: string,
|
||||||
|
password: string,
|
||||||
|
role: 'owner' | 'admin' | 'user' = 'user',
|
||||||
|
tags?: string[],
|
||||||
|
oidcSub?: string,
|
||||||
|
enabledApis?: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
if (typeof (this.storage as any).createUserV2 === 'function') {
|
||||||
|
await (this.storage as any).createUserV2(userName, password, role, tags, oidcSub, enabledApis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyUserV2(userName: string, password: string): Promise<boolean> {
|
||||||
|
if (typeof (this.storage as any).verifyUserV2 === 'function') {
|
||||||
|
return (this.storage as any).verifyUserV2(userName, password);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserInfoV2(userName: string): Promise<{
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
} | null> {
|
||||||
|
if (typeof (this.storage as any).getUserInfoV2 === 'function') {
|
||||||
|
return (this.storage as any).getUserInfoV2(userName);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserInfoV2(
|
||||||
|
userName: string,
|
||||||
|
updates: {
|
||||||
|
role?: 'owner' | 'admin' | 'user';
|
||||||
|
banned?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
enabledApis?: string[];
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
if (typeof (this.storage as any).updateUserInfoV2 === 'function') {
|
||||||
|
await (this.storage as any).updateUserInfoV2(userName, updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePasswordV2(userName: string, newPassword: string): Promise<void> {
|
||||||
|
if (typeof (this.storage as any).changePasswordV2 === 'function') {
|
||||||
|
await (this.storage as any).changePasswordV2(userName, newPassword);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkUserExistV2(userName: string): Promise<boolean> {
|
||||||
|
if (typeof (this.storage as any).checkUserExistV2 === 'function') {
|
||||||
|
return (this.storage as any).checkUserExistV2(userName);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserByOidcSub(oidcSub: string): Promise<string | null> {
|
||||||
|
if (typeof (this.storage as any).getUserByOidcSub === 'function') {
|
||||||
|
return (this.storage as any).getUserByOidcSub(oidcSub);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserListV2(
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 20,
|
||||||
|
ownerUsername?: string
|
||||||
|
): Promise<{
|
||||||
|
users: Array<{
|
||||||
|
username: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
}>;
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
if (typeof (this.storage as any).getUserListV2 === 'function') {
|
||||||
|
return (this.storage as any).getUserListV2(offset, limit, ownerUsername);
|
||||||
|
}
|
||||||
|
return { users: [], total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUserV2(userName: string): Promise<void> {
|
||||||
|
if (typeof (this.storage as any).deleteUserV2 === 'function') {
|
||||||
|
await (this.storage as any).deleteUserV2(userName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsersByTag(tagName: string): Promise<string[]> {
|
||||||
|
if (typeof (this.storage as any).getUsersByTag === 'function') {
|
||||||
|
return (this.storage as any).getUsersByTag(tagName);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 数据迁移 ----------
|
||||||
|
async migrateUsersFromConfig(adminConfig: AdminConfig): Promise<void> {
|
||||||
|
if (typeof (this.storage as any).createUserV2 !== 'function') {
|
||||||
|
throw new Error('当前存储类型不支持新版用户存储');
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = adminConfig.UserConfig.Users;
|
||||||
|
if (!users || users.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`开始迁移 ${users.length} 个用户...`);
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
try {
|
||||||
|
// 跳过站长(站长使用环境变量认证,不需要迁移)
|
||||||
|
if (user.role === 'owner') {
|
||||||
|
console.log(`跳过站长 ${user.username} 的迁移`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否已经迁移
|
||||||
|
const exists = await this.checkUserExistV2(user.username);
|
||||||
|
if (exists) {
|
||||||
|
console.log(`用户 ${user.username} 已存在,跳过迁移`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取密码
|
||||||
|
let password = '';
|
||||||
|
|
||||||
|
// 如果是OIDC用户,生成随机密码(OIDC用户不需要密码登录)
|
||||||
|
if ((user as any).oidcSub) {
|
||||||
|
password = crypto.randomUUID();
|
||||||
|
console.log(`用户 ${user.username} (OIDC用户) 使用随机密码迁移`);
|
||||||
|
}
|
||||||
|
// 如果是站长,使用环境变量中的密码
|
||||||
|
else if (user.username === process.env.USERNAME && process.env.PASSWORD) {
|
||||||
|
password = process.env.PASSWORD;
|
||||||
|
console.log(`用户 ${user.username} (站长) 使用环境变量密码迁移`);
|
||||||
|
}
|
||||||
|
// 尝试从旧的存储中获取密码
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
if ((this.storage as any).client) {
|
||||||
|
const storedPassword = await (this.storage as any).client.get(`u:${user.username}:pwd`);
|
||||||
|
if (storedPassword) {
|
||||||
|
password = storedPassword;
|
||||||
|
console.log(`用户 ${user.username} 使用旧密码迁移`);
|
||||||
|
} else {
|
||||||
|
// 没有旧密码,使用默认密码
|
||||||
|
password = 'defaultPassword123';
|
||||||
|
console.log(`用户 ${user.username} 没有旧密码,使用默认密码`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
password = 'defaultPassword123';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`获取用户 ${user.username} 的密码失败,使用默认密码`, err);
|
||||||
|
password = 'defaultPassword123';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新用户
|
||||||
|
await this.createUserV2(
|
||||||
|
user.username,
|
||||||
|
password,
|
||||||
|
user.role,
|
||||||
|
user.tags,
|
||||||
|
(user as any).oidcSub,
|
||||||
|
user.enabledApis
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果用户被封禁,更新状态
|
||||||
|
if (user.banned) {
|
||||||
|
await this.updateUserInfoV2(user.username, { banned: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`用户 ${user.username} 迁移成功`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`迁移用户 ${user.username} 失败:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('用户迁移完成');
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 搜索历史 ----------
|
// ---------- 搜索历史 ----------
|
||||||
async getSearchHistory(userName: string): Promise<string[]> {
|
async getSearchHistory(userName: string): Promise<string[]> {
|
||||||
return this.storage.getSearchHistory(userName);
|
return this.storage.getSearchHistory(userName);
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
await this.withRetry(() => this.client.del(this.favKey(userName, key)));
|
await this.withRetry(() => this.client.del(this.favKey(userName, key)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 用户注册 / 登录 ----------
|
// ---------- 用户注册 / 登录(旧版本,保持兼容) ----------
|
||||||
private userPwdKey(user: string) {
|
private userPwdKey(user: string) {
|
||||||
return `u:${user}:pwd`;
|
return `u:${user}:pwd`;
|
||||||
}
|
}
|
||||||
@@ -314,6 +314,256 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 新版用户存储(使用Hash和Sorted Set) ----------
|
||||||
|
private userInfoKey(userName: string) {
|
||||||
|
return `user:${userName}:info`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private userListKey() {
|
||||||
|
return 'user:list';
|
||||||
|
}
|
||||||
|
|
||||||
|
private oidcSubKey(oidcSub: string) {
|
||||||
|
return `oidc:sub:${oidcSub}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHA256加密密码
|
||||||
|
private async hashPassword(password: string): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(password);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新用户(新版本)
|
||||||
|
async createUserV2(
|
||||||
|
userName: string,
|
||||||
|
password: string,
|
||||||
|
role: 'owner' | 'admin' | 'user' = 'user',
|
||||||
|
tags?: string[],
|
||||||
|
oidcSub?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const hashedPassword = await this.hashPassword(password);
|
||||||
|
const createdAt = Date.now();
|
||||||
|
|
||||||
|
// 存储用户信息到Hash
|
||||||
|
const userInfo: Record<string, string> = {
|
||||||
|
role,
|
||||||
|
banned: 'false',
|
||||||
|
password: hashedPassword,
|
||||||
|
created_at: createdAt.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
userInfo.tags = JSON.stringify(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oidcSub) {
|
||||||
|
userInfo.oidcSub = oidcSub;
|
||||||
|
// 创建OIDC映射
|
||||||
|
await this.withRetry(() => this.client.set(this.oidcSubKey(oidcSub), userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.withRetry(() => this.client.hSet(this.userInfoKey(userName), userInfo));
|
||||||
|
|
||||||
|
// 添加到用户列表(Sorted Set,按注册时间排序)
|
||||||
|
await this.withRetry(() => this.client.zAdd(this.userListKey(), {
|
||||||
|
score: createdAt,
|
||||||
|
value: userName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户密码(新版本)
|
||||||
|
async verifyUserV2(userName: string, password: string): Promise<boolean> {
|
||||||
|
const userInfo = await this.withRetry(() =>
|
||||||
|
this.client.hGetAll(this.userInfoKey(userName))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userInfo || !userInfo.password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await this.hashPassword(password);
|
||||||
|
return userInfo.password === hashedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息(新版本)
|
||||||
|
async getUserInfoV2(userName: string): Promise<{
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
created_at: number;
|
||||||
|
} | null> {
|
||||||
|
const userInfo = await this.withRetry(() =>
|
||||||
|
this.client.hGetAll(this.userInfoKey(userName))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userInfo || Object.keys(userInfo).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: (userInfo.role as 'owner' | 'admin' | 'user') || 'user',
|
||||||
|
banned: userInfo.banned === 'true',
|
||||||
|
tags: userInfo.tags ? JSON.parse(userInfo.tags) : undefined,
|
||||||
|
oidcSub: userInfo.oidcSub,
|
||||||
|
created_at: parseInt(userInfo.created_at || '0', 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息(新版本)
|
||||||
|
async updateUserInfoV2(
|
||||||
|
userName: string,
|
||||||
|
updates: {
|
||||||
|
role?: 'owner' | 'admin' | 'user';
|
||||||
|
banned?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
const userInfo: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (updates.role !== undefined) {
|
||||||
|
userInfo.role = updates.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.banned !== undefined) {
|
||||||
|
userInfo.banned = updates.banned ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.tags !== undefined) {
|
||||||
|
if (updates.tags.length > 0) {
|
||||||
|
userInfo.tags = JSON.stringify(updates.tags);
|
||||||
|
} else {
|
||||||
|
// 删除tags字段
|
||||||
|
await this.withRetry(() => this.client.hDel(this.userInfoKey(userName), 'tags'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.oidcSub !== undefined) {
|
||||||
|
const oldInfo = await this.getUserInfoV2(userName);
|
||||||
|
if (oldInfo?.oidcSub && oldInfo.oidcSub !== updates.oidcSub) {
|
||||||
|
// 删除旧的OIDC映射
|
||||||
|
await this.withRetry(() => this.client.del(this.oidcSubKey(oldInfo.oidcSub!)));
|
||||||
|
}
|
||||||
|
userInfo.oidcSub = updates.oidcSub;
|
||||||
|
// 创建新的OIDC映射
|
||||||
|
await this.withRetry(() => this.client.set(this.oidcSubKey(updates.oidcSub!), userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(userInfo).length > 0) {
|
||||||
|
await this.withRetry(() => this.client.hSet(this.userInfoKey(userName), userInfo));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户密码(新版本)
|
||||||
|
async changePasswordV2(userName: string, newPassword: string): Promise<void> {
|
||||||
|
const hashedPassword = await this.hashPassword(newPassword);
|
||||||
|
await this.withRetry(() =>
|
||||||
|
this.client.hSet(this.userInfoKey(userName), 'password', hashedPassword)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否存在(新版本)
|
||||||
|
async checkUserExistV2(userName: string): Promise<boolean> {
|
||||||
|
const exists = await this.withRetry(() =>
|
||||||
|
this.client.exists(this.userInfoKey(userName))
|
||||||
|
);
|
||||||
|
return exists === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过OIDC Sub查找用户名
|
||||||
|
async getUserByOidcSub(oidcSub: string): Promise<string | null> {
|
||||||
|
const userName = await this.withRetry(() =>
|
||||||
|
this.client.get(this.oidcSubKey(oidcSub))
|
||||||
|
);
|
||||||
|
return userName ? ensureString(userName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户列表(分页,新版本)
|
||||||
|
async getUserListV2(
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 20,
|
||||||
|
ownerUsername?: string
|
||||||
|
): Promise<{
|
||||||
|
users: Array<{
|
||||||
|
username: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
created_at: number;
|
||||||
|
}>;
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
// 获取总数
|
||||||
|
const total = await this.withRetry(() => this.client.zCard(this.userListKey()));
|
||||||
|
|
||||||
|
// 获取用户列表(按注册时间升序)
|
||||||
|
const usernames = await this.withRetry(() =>
|
||||||
|
this.client.zRange(this.userListKey(), offset, offset + limit - 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = [];
|
||||||
|
|
||||||
|
// 如果有站长,确保站长始终在第一位
|
||||||
|
if (ownerUsername && offset === 0) {
|
||||||
|
const ownerInfo = await this.getUserInfoV2(ownerUsername);
|
||||||
|
if (ownerInfo) {
|
||||||
|
users.push({
|
||||||
|
username: ownerUsername,
|
||||||
|
role: 'owner' as const,
|
||||||
|
banned: ownerInfo.banned,
|
||||||
|
tags: ownerInfo.tags,
|
||||||
|
created_at: ownerInfo.created_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取其他用户信息
|
||||||
|
for (const username of usernames) {
|
||||||
|
const usernameStr = ensureString(username);
|
||||||
|
// 跳过站长(已经添加)
|
||||||
|
if (ownerUsername && usernameStr === ownerUsername) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await this.getUserInfoV2(usernameStr);
|
||||||
|
if (userInfo) {
|
||||||
|
users.push({
|
||||||
|
username: usernameStr,
|
||||||
|
role: userInfo.role,
|
||||||
|
banned: userInfo.banned,
|
||||||
|
tags: userInfo.tags,
|
||||||
|
created_at: userInfo.created_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { users, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户(新版本)
|
||||||
|
async deleteUserV2(userName: string): Promise<void> {
|
||||||
|
// 获取用户信息
|
||||||
|
const userInfo = await this.getUserInfoV2(userName);
|
||||||
|
|
||||||
|
// 删除OIDC映射
|
||||||
|
if (userInfo?.oidcSub) {
|
||||||
|
await this.withRetry(() => this.client.del(this.oidcSubKey(userInfo.oidcSub!)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户信息Hash
|
||||||
|
await this.withRetry(() => this.client.del(this.userInfoKey(userName)));
|
||||||
|
|
||||||
|
// 从用户列表中移除
|
||||||
|
await this.withRetry(() => this.client.zRem(this.userListKey(), userName));
|
||||||
|
|
||||||
|
// 删除用户的其他数据(播放记录、收藏等)
|
||||||
|
await this.deleteUser(userName);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 搜索历史 ----------
|
// ---------- 搜索历史 ----------
|
||||||
private shKey(user: string) {
|
private shKey(user: string) {
|
||||||
return `u:${user}:sh`; // u:username:sh
|
return `u:${user}:sh`; // u:username:sh
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Redis } from '@upstash/redis';
|
|||||||
|
|
||||||
import { AdminConfig } from './admin.types';
|
import { AdminConfig } from './admin.types';
|
||||||
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
import { Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||||
|
import { userInfoCache } from './user-cache';
|
||||||
|
|
||||||
// 搜索历史最大条数
|
// 搜索历史最大条数
|
||||||
const SEARCH_HISTORY_LIMIT = 20;
|
const SEARCH_HISTORY_LIMIT = 20;
|
||||||
@@ -220,6 +221,366 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- 新版用户存储(使用Hash和Sorted Set) ----------
|
||||||
|
private userInfoKey(userName: string) {
|
||||||
|
return `user:${userName}:info`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private userListKey() {
|
||||||
|
return 'user:list';
|
||||||
|
}
|
||||||
|
|
||||||
|
private oidcSubKey(oidcSub: string) {
|
||||||
|
return `oidc:sub:${oidcSub}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHA256加密密码
|
||||||
|
private async hashPassword(password: string): Promise<string> {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(password);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新用户(新版本)
|
||||||
|
async createUserV2(
|
||||||
|
userName: string,
|
||||||
|
password: string,
|
||||||
|
role: 'owner' | 'admin' | 'user' = 'user',
|
||||||
|
tags?: string[],
|
||||||
|
oidcSub?: string,
|
||||||
|
enabledApis?: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
const hashedPassword = await this.hashPassword(password);
|
||||||
|
const createdAt = Date.now();
|
||||||
|
|
||||||
|
// 存储用户信息到Hash
|
||||||
|
const userInfo: Record<string, any> = {
|
||||||
|
role,
|
||||||
|
banned: false, // 直接使用布尔值
|
||||||
|
password: hashedPassword,
|
||||||
|
created_at: createdAt.toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tags && tags.length > 0) {
|
||||||
|
userInfo.tags = JSON.stringify(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oidcSub) {
|
||||||
|
userInfo.oidcSub = oidcSub;
|
||||||
|
// 创建OIDC映射
|
||||||
|
await withRetry(() => this.client.set(this.oidcSubKey(oidcSub), userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledApis && enabledApis.length > 0) {
|
||||||
|
userInfo.enabledApis = JSON.stringify(enabledApis);
|
||||||
|
}
|
||||||
|
|
||||||
|
await withRetry(() => this.client.hset(this.userInfoKey(userName), userInfo));
|
||||||
|
|
||||||
|
// 添加到用户列表(Sorted Set,按注册时间排序)
|
||||||
|
await withRetry(() => this.client.zadd(this.userListKey(), {
|
||||||
|
score: createdAt,
|
||||||
|
member: userName,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证用户密码(新版本)
|
||||||
|
async verifyUserV2(userName: string, password: string): Promise<boolean> {
|
||||||
|
const userInfo = await withRetry(() =>
|
||||||
|
this.client.hgetall(this.userInfoKey(userName))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userInfo || !userInfo.password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await this.hashPassword(password);
|
||||||
|
return userInfo.password === hashedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息(新版本)
|
||||||
|
async getUserInfoV2(userName: string): Promise<{
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
} | null> {
|
||||||
|
// 先从缓存获取
|
||||||
|
const cached = userInfoCache?.get(userName);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await withRetry(() =>
|
||||||
|
this.client.hgetall(this.userInfoKey(userName))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userInfo || Object.keys(userInfo).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 banned 字段:可能是字符串 'true'/'false' 或布尔值 true/false
|
||||||
|
let banned = false;
|
||||||
|
if (typeof userInfo.banned === 'boolean') {
|
||||||
|
banned = userInfo.banned;
|
||||||
|
} else if (typeof userInfo.banned === 'string') {
|
||||||
|
banned = userInfo.banned === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全解析 tags 字段
|
||||||
|
let tags: string[] | undefined = undefined;
|
||||||
|
if (userInfo.tags) {
|
||||||
|
if (Array.isArray(userInfo.tags)) {
|
||||||
|
tags = userInfo.tags;
|
||||||
|
} else if (typeof userInfo.tags === 'string') {
|
||||||
|
try {
|
||||||
|
tags = JSON.parse(userInfo.tags);
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,可能是单个字符串,转换为数组
|
||||||
|
tags = [userInfo.tags];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全解析 enabledApis 字段
|
||||||
|
let enabledApis: string[] | undefined = undefined;
|
||||||
|
if (userInfo.enabledApis) {
|
||||||
|
if (Array.isArray(userInfo.enabledApis)) {
|
||||||
|
enabledApis = userInfo.enabledApis;
|
||||||
|
} else if (typeof userInfo.enabledApis === 'string') {
|
||||||
|
try {
|
||||||
|
enabledApis = JSON.parse(userInfo.enabledApis);
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,可能是单个字符串,转换为数组
|
||||||
|
enabledApis = [userInfo.enabledApis];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
role: (userInfo.role as 'owner' | 'admin' | 'user') || 'user',
|
||||||
|
banned,
|
||||||
|
tags,
|
||||||
|
oidcSub: userInfo.oidcSub as string | undefined,
|
||||||
|
enabledApis,
|
||||||
|
created_at: parseInt((userInfo.created_at as string) || '0', 10),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 存入缓存
|
||||||
|
userInfoCache?.set(userName, result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息(新版本)
|
||||||
|
async updateUserInfoV2(
|
||||||
|
userName: string,
|
||||||
|
updates: {
|
||||||
|
role?: 'owner' | 'admin' | 'user';
|
||||||
|
banned?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
enabledApis?: string[];
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
const userInfo: Record<string, any> = {};
|
||||||
|
|
||||||
|
if (updates.role !== undefined) {
|
||||||
|
userInfo.role = updates.role;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.banned !== undefined) {
|
||||||
|
// 直接存储布尔值,让 Upstash 自动处理序列化
|
||||||
|
userInfo.banned = updates.banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.tags !== undefined) {
|
||||||
|
if (updates.tags.length > 0) {
|
||||||
|
userInfo.tags = JSON.stringify(updates.tags);
|
||||||
|
} else {
|
||||||
|
// 删除tags字段
|
||||||
|
await withRetry(() => this.client.hdel(this.userInfoKey(userName), 'tags'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.enabledApis !== undefined) {
|
||||||
|
if (updates.enabledApis.length > 0) {
|
||||||
|
userInfo.enabledApis = JSON.stringify(updates.enabledApis);
|
||||||
|
} else {
|
||||||
|
// 删除enabledApis字段
|
||||||
|
await withRetry(() => this.client.hdel(this.userInfoKey(userName), 'enabledApis'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.oidcSub !== undefined) {
|
||||||
|
const oldInfo = await this.getUserInfoV2(userName);
|
||||||
|
if (oldInfo?.oidcSub && oldInfo.oidcSub !== updates.oidcSub) {
|
||||||
|
// 删除旧的OIDC映射
|
||||||
|
await withRetry(() => this.client.del(this.oidcSubKey(oldInfo.oidcSub!)));
|
||||||
|
}
|
||||||
|
userInfo.oidcSub = updates.oidcSub;
|
||||||
|
// 创建新的OIDC映射
|
||||||
|
await withRetry(() => this.client.set(this.oidcSubKey(updates.oidcSub!), userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(userInfo).length > 0) {
|
||||||
|
await withRetry(() => this.client.hset(this.userInfoKey(userName), userInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
userInfoCache?.delete(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改用户密码(新版本)
|
||||||
|
async changePasswordV2(userName: string, newPassword: string): Promise<void> {
|
||||||
|
const hashedPassword = await this.hashPassword(newPassword);
|
||||||
|
await withRetry(() =>
|
||||||
|
this.client.hset(this.userInfoKey(userName), { password: hashedPassword })
|
||||||
|
);
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
userInfoCache?.delete(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户是否存在(新版本)
|
||||||
|
async checkUserExistV2(userName: string): Promise<boolean> {
|
||||||
|
const exists = await withRetry(() =>
|
||||||
|
this.client.exists(this.userInfoKey(userName))
|
||||||
|
);
|
||||||
|
return exists === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过OIDC Sub查找用户名
|
||||||
|
async getUserByOidcSub(oidcSub: string): Promise<string | null> {
|
||||||
|
const userName = await withRetry(() =>
|
||||||
|
this.client.get(this.oidcSubKey(oidcSub))
|
||||||
|
);
|
||||||
|
return userName ? ensureString(userName) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取使用特定用户组的用户列表
|
||||||
|
async getUsersByTag(tagName: string): Promise<string[]> {
|
||||||
|
const affectedUsers: string[] = [];
|
||||||
|
|
||||||
|
// 使用 SCAN 遍历所有用户信息的 key
|
||||||
|
let cursor: number | string = 0;
|
||||||
|
do {
|
||||||
|
const result = await withRetry(() =>
|
||||||
|
this.client.scan(cursor as number, { match: 'user:*:info', count: 100 })
|
||||||
|
);
|
||||||
|
|
||||||
|
cursor = result[0];
|
||||||
|
const keys = result[1];
|
||||||
|
|
||||||
|
// 检查每个用户的 tags
|
||||||
|
for (const key of keys) {
|
||||||
|
const userInfo = await withRetry(() => this.client.hgetall(key));
|
||||||
|
if (userInfo && userInfo.tags) {
|
||||||
|
const tags = JSON.parse(userInfo.tags as string);
|
||||||
|
if (tags.includes(tagName)) {
|
||||||
|
// 从 key 中提取用户名: user:username:info -> username
|
||||||
|
const username = key.replace('user:', '').replace(':info', '');
|
||||||
|
affectedUsers.push(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (typeof cursor === 'number' ? cursor !== 0 : cursor !== '0');
|
||||||
|
|
||||||
|
return affectedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户列表(分页,新版本)
|
||||||
|
async getUserListV2(
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 20,
|
||||||
|
ownerUsername?: string
|
||||||
|
): Promise<{
|
||||||
|
users: Array<{
|
||||||
|
username: string;
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
}>;
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
// 获取总数
|
||||||
|
const total = await withRetry(() => this.client.zcard(this.userListKey()));
|
||||||
|
|
||||||
|
// 获取用户列表(按注册时间升序)
|
||||||
|
const usernames = await withRetry(() =>
|
||||||
|
this.client.zrange(this.userListKey(), offset, offset + limit - 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
const users = [];
|
||||||
|
|
||||||
|
// 如果有站长,确保站长始终在第一位
|
||||||
|
if (ownerUsername && offset === 0) {
|
||||||
|
const ownerInfo = await this.getUserInfoV2(ownerUsername);
|
||||||
|
if (ownerInfo) {
|
||||||
|
users.push({
|
||||||
|
username: ownerUsername,
|
||||||
|
role: 'owner' as const,
|
||||||
|
banned: ownerInfo.banned,
|
||||||
|
tags: ownerInfo.tags,
|
||||||
|
enabledApis: ownerInfo.enabledApis,
|
||||||
|
created_at: ownerInfo.created_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取其他用户信息
|
||||||
|
for (const username of usernames) {
|
||||||
|
const usernameStr = ensureString(username);
|
||||||
|
// 跳过站长(已经添加)
|
||||||
|
if (ownerUsername && usernameStr === ownerUsername) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userInfo = await this.getUserInfoV2(usernameStr);
|
||||||
|
if (userInfo) {
|
||||||
|
users.push({
|
||||||
|
username: usernameStr,
|
||||||
|
role: userInfo.role,
|
||||||
|
banned: userInfo.banned,
|
||||||
|
tags: userInfo.tags,
|
||||||
|
enabledApis: userInfo.enabledApis,
|
||||||
|
created_at: userInfo.created_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { users, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户(新版本)
|
||||||
|
async deleteUserV2(userName: string): Promise<void> {
|
||||||
|
// 获取用户信息
|
||||||
|
const userInfo = await this.getUserInfoV2(userName);
|
||||||
|
|
||||||
|
// 删除OIDC映射
|
||||||
|
if (userInfo?.oidcSub) {
|
||||||
|
await withRetry(() => this.client.del(this.oidcSubKey(userInfo.oidcSub!)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户信息Hash
|
||||||
|
await withRetry(() => this.client.del(this.userInfoKey(userName)));
|
||||||
|
|
||||||
|
// 从用户列表中移除
|
||||||
|
await withRetry(() => this.client.zrem(this.userListKey(), userName));
|
||||||
|
|
||||||
|
// 删除用户的其他数据(播放记录、收藏等)
|
||||||
|
await this.deleteUser(userName);
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
userInfoCache?.delete(userName);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 搜索历史 ----------
|
// ---------- 搜索历史 ----------
|
||||||
private shKey(user: string) {
|
private shKey(user: string) {
|
||||||
return `u:${user}:sh`; // u:username:sh
|
return `u:${user}:sh`; // u:username:sh
|
||||||
|
|||||||
72
src/lib/user-cache.ts
Normal file
72
src/lib/user-cache.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
// 用户信息缓存
|
||||||
|
interface CachedUserInfo {
|
||||||
|
role: 'owner' | 'admin' | 'user';
|
||||||
|
banned: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
|
enabledApis?: string[];
|
||||||
|
created_at: number;
|
||||||
|
cachedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserInfoCache {
|
||||||
|
private cache: Map<string, CachedUserInfo> = new Map();
|
||||||
|
private readonly TTL = 6 * 60 * 60 * 1000; // 6小时过期
|
||||||
|
|
||||||
|
get(username: string): CachedUserInfo | null {
|
||||||
|
const cached = this.cache.get(username);
|
||||||
|
if (!cached) return null;
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (Date.now() - cached.cachedAt > this.TTL) {
|
||||||
|
this.cache.delete(username);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(username: string, userInfo: Omit<CachedUserInfo, 'cachedAt'>): void {
|
||||||
|
this.cache.set(username, {
|
||||||
|
...userInfo,
|
||||||
|
cachedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(username: string): void {
|
||||||
|
this.cache.delete(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理过期的缓存
|
||||||
|
cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const entries = Array.from(this.cache.entries());
|
||||||
|
for (const [username, cached] of entries) {
|
||||||
|
if (now - cached.cachedAt > this.TTL) {
|
||||||
|
this.cache.delete(username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局单例
|
||||||
|
const globalKey = Symbol.for('__MOONTV_USER_INFO_CACHE__');
|
||||||
|
let userInfoCache: UserInfoCache | undefined = (global as any)[globalKey];
|
||||||
|
|
||||||
|
if (!userInfoCache) {
|
||||||
|
userInfoCache = new UserInfoCache();
|
||||||
|
(global as any)[globalKey] = userInfoCache;
|
||||||
|
|
||||||
|
// 每分钟清理一次过期缓存
|
||||||
|
setInterval(() => {
|
||||||
|
userInfoCache?.cleanup();
|
||||||
|
}, 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { userInfoCache };
|
||||||
Reference in New Issue
Block a user