From d63192ef7a671694e1dba5d0378ec194df932a8b Mon Sep 17 00:00:00 2001 From: mtvpls Date: Wed, 24 Dec 2025 00:24:50 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=95=B0=E6=8D=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 267 ++++++++++++- src/app/api/admin/config/route.ts | 31 +- src/app/api/admin/migrate-users/route.ts | 76 ++++ src/app/api/admin/user/route.ts | 164 +++++--- src/app/api/admin/users/route.ts | 129 +++++++ src/app/api/auth/oidc/callback/route.ts | 38 +- .../api/auth/oidc/complete-register/route.ts | 44 ++- src/app/api/change-password/route.ts | 4 +- src/app/api/danmaku-filter/route.ts | 22 +- src/app/api/favorites/route.ts | 33 +- src/app/api/login/route.ts | 93 +++-- src/app/api/playrecords/route.ts | 33 +- src/app/api/register/route.ts | 34 +- src/app/api/searchhistory/route.ts | 33 +- src/app/api/skipconfigs/route.ts | 33 +- src/lib/config.ts | 13 +- src/lib/db.ts | 193 +++++++++- src/lib/redis-base.db.ts | 252 +++++++++++- src/lib/upstash.db.ts | 361 ++++++++++++++++++ src/lib/user-cache.ts | 72 ++++ 20 files changed, 1680 insertions(+), 245 deletions(-) create mode 100644 src/app/api/admin/migrate-users/route.ts create mode 100644 src/app/api/admin/users/route.ts create mode 100644 src/lib/user-cache.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 9bc8a82..5cdfb98 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -213,7 +213,7 @@ const AlertModal = ({ + + + )} {/* 用户组管理 */} @@ -1195,10 +1287,31 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { )} {/* 用户列表 */} -
+
+ {/* 迁移遮罩层 */} + {config.UserConfig.Users && + config.UserConfig.Users.filter(u => u.role !== 'owner').length > 0 && ( +
+
+
+ +

+ 需要迁移数据 +

+
+

+ 检测到旧版用户数据,请先迁移到新的存储结构后再进行用户管理操作。 +

+

+ 请在上方的"用户统计"区域点击"立即迁移"按钮完成数据迁移。 +

+
+
+ )} +
@@ -1266,8 +1379,21 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { {/* 按规则排序用户:自己 -> 站长(若非自己) -> 管理员 -> 其他 */} {(() => { - const sortedUsers = [...config.UserConfig.Users].sort((a, b) => { - type UserInfo = (typeof config.UserConfig.Users)[number]; + // 如果正在加载,显示加载状态 + if (userListLoading) { + return ( + + + + + + ); + } + + const sortedUsers = [...displayUsers].sort((a, b) => { + type UserInfo = (typeof displayUsers)[number]; const priority = (u: UserInfo) => { if (u.username === currentUsername) return 0; if (u.role === 'owner') return 1; @@ -1507,6 +1633,62 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { })()}
+ 加载中... +
+ + {/* 用户列表分页 */} + {!hasOldUserData && usersV2 && userTotalPages > 1 && ( +
+
+ 共 {userTotal} 个用户,第 {userPage} / {userTotalPages} 页 +
+
+ + + + +
+
+ )} +
{/* 配置用户采集源权限弹窗 */} @@ -2555,6 +2737,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => { message={alertModal.message} timer={alertModal.timer} showConfirm={alertModal.showConfirm} + onConfirm={alertModal.onConfirm} /> ); @@ -7758,17 +7941,67 @@ function AdminPageClient() { } }, []); + // 新版本用户列表状态 + const [usersV2, setUsersV2] = useState | 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(() => { // 首次加载时显示骨架 fetchConfig(true); + // 不再自动获取用户列表,等用户打开用户管理选项卡时再获取 }, [fetchConfig]); // 切换标签展开状态 const toggleTab = (tabKey: string) => { + const wasExpanded = expandedTabs[tabKey]; + setExpandedTabs((prev) => ({ ...prev, [tabKey]: !prev[tabKey], })); + + // 当打开用户管理选项卡时,如果还没有加载用户列表,则加载 + if (tabKey === 'userConfig' && !wasExpanded && !usersV2) { + fetchUsersV2(); + } }; // 新增: 重置配置处理函数 @@ -7917,7 +8150,13 @@ function AdminPageClient() { @@ -8013,7 +8252,7 @@ function AdminPageClient() { isExpanded={expandedTabs.dataMigration} onToggle={() => toggleTab('dataMigration')} > - + )} diff --git a/src/app/api/admin/config/route.ts b/src/app/api/admin/config/route.ts index 7544700..8fbe7eb 100644 --- a/src/app/api/admin/config/route.ts +++ b/src/app/api/admin/config/route.ts @@ -34,14 +34,31 @@ export async function GET(request: NextRequest) { if (username === process.env.USERNAME) { result.Role = 'owner'; } else { - const user = config.UserConfig.Users.find((u) => u.username === username); - if (user && user.role === 'admin' && !user.banned) { - result.Role = 'admin'; + // 优先从新版本获取用户信息 + const { db } = await import('@/lib/db'); + 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 { - return NextResponse.json( - { error: '你是管理员吗你就访问?' }, - { status: 401 } - ); + // 回退到配置中查找 + const user = config.UserConfig.Users.find((u) => u.username === username); + if (user && user.role === 'admin' && !user.banned) { + result.Role = 'admin'; + } else { + return NextResponse.json( + { error: '你是管理员吗你就访问?' }, + { status: 401 } + ); + } } } diff --git a/src/app/api/admin/migrate-users/route.ts b/src/app/api/admin/migrate-users/route.ts new file mode 100644 index 0000000..59922c8 --- /dev/null +++ b/src/app/api/admin/migrate-users/route.ts @@ -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 } + ); + } +} diff --git a/src/app/api/admin/user/route.ts b/src/app/api/admin/user/route.ts index 9473f25..bc07401 100644 --- a/src/app/api/admin/user/route.ts +++ b/src/app/api/admin/user/route.ts @@ -85,24 +85,50 @@ export async function POST(request: NextRequest) { if (username === process.env.USERNAME) { operatorRole = 'owner'; } else { - const userEntry = adminConfig.UserConfig.Users.find( - (u) => u.username === username - ); - if (!userEntry || userEntry.role !== 'admin' || userEntry.banned) { - return NextResponse.json({ error: '权限不足' }, { status: 401 }); + // 优先从新版本获取用户信息 + const operatorInfo = await db.getUserInfoV2(username); + if (operatorInfo) { + if (operatorInfo.role !== 'admin' || operatorInfo.banned) { + 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 isTargetAdmin = false; + let targetUserV2: any = null; if (!['userGroup', 'batchUpdateUserGroups'].includes(action) && targetUsername) { + // 先从配置中查找 targetEntry = adminConfig.UserConfig.Users.find( (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 ( targetEntry && targetEntry.role === 'owner' && @@ -120,33 +146,35 @@ export async function POST(request: NextRequest) { if (targetEntry) { return NextResponse.json({ error: '用户已存在' }, { status: 400 }); } + // 检查新版本中是否已存在 + const existsV2 = await db.checkUserExistV2(targetUsername!); + if (existsV2) { + return NextResponse.json({ error: '用户已存在' }, { status: 400 }); + } if (!targetPassword) { return NextResponse.json( { error: '缺少目标用户密码' }, { status: 400 } ); } - await db.registerUser(targetUsername!, targetPassword); // 获取用户组信息 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!, 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; } case 'ban': { @@ -165,7 +193,9 @@ export async function POST(request: NextRequest) { ); } } - targetEntry.banned = true; + + // 只更新V2存储 + await db.updateUserInfoV2(targetUsername!, { banned: true }); break; } case 'unban': { @@ -183,7 +213,9 @@ export async function POST(request: NextRequest) { ); } } - targetEntry.banned = false; + + // 只更新V2存储 + await db.updateUserInfoV2(targetUsername!, { banned: false }); break; } case 'setAdmin': { @@ -205,7 +237,9 @@ export async function POST(request: NextRequest) { { status: 401 } ); } - targetEntry.role = 'admin'; + + // 只更新V2存储 + await db.updateUserInfoV2(targetUsername!, { role: 'admin' }); break; } case 'cancelAdmin': { @@ -227,7 +261,9 @@ export async function POST(request: NextRequest) { { status: 401 } ); } - targetEntry.role = 'user'; + + // 只更新V2存储 + await db.updateUserInfoV2(targetUsername!, { role: 'user' }); break; } case 'changePassword': { @@ -260,6 +296,9 @@ export async function POST(request: NextRequest) { ); } + // 使用新版本修改密码(SHA256加密) + await db.changePasswordV2(targetUsername!, targetPassword); + // 同时更新旧版本(保持兼容性) await db.changePassword(targetUsername!, targetPassword); break; } @@ -286,16 +325,11 @@ export async function POST(request: NextRequest) { ); } + // 只删除V2存储中的用户 + await db.deleteUserV2(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; } case 'updateUserApis': { @@ -320,13 +354,10 @@ export async function POST(request: NextRequest) { ); } - // 更新用户的采集源权限 - if (enabledApis && enabledApis.length > 0) { - targetEntry.enabledApis = enabledApis; - } else { - // 如果为空数组或未提供,则删除该字段,表示无限制 - delete targetEntry.enabledApis; - } + // 更新V2存储中的采集源权限 + await db.updateUserInfoV2(targetUsername!, { + enabledApis: enabledApis && enabledApis.length > 0 ? enabledApis : [] + }); break; } @@ -368,19 +399,17 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '用户组不存在' }, { status: 404 }); } - // 查找使用该用户组的所有用户 - const affectedUsers: string[] = []; - adminConfig.UserConfig.Users.forEach(user => { - if (user.tags && user.tags.includes(groupName)) { - affectedUsers.push(user.username); - // 从用户的tags中移除该用户组 - user.tags = user.tags.filter(tag => tag !== groupName); - // 如果用户没有其他标签了,删除tags字段 - if (user.tags.length === 0) { - delete user.tags; - } + // 查找使用该用户组的所有用户(从V2存储中查找) + const affectedUsers = await db.getUsersByTag(groupName); + + // 从用户的tags中移除该用户组 + for (const username of affectedUsers) { + const userInfo = await db.getUserInfoV2(username); + if (userInfo && userInfo.tags) { + const newTags = userInfo.tags.filter(tag => tag !== groupName); + await db.updateUserInfoV2(username, { tags: newTags }); } - }); + } // 删除用户组 adminConfig.UserConfig.Tags.splice(groupIndex, 1); @@ -413,10 +442,11 @@ export async function POST(request: NextRequest) { // 更新用户的用户组 if (userGroups && userGroups.length > 0) { - targetEntry.tags = userGroups; + // 只更新V2存储 + await db.updateUserInfoV2(targetUsername!, { tags: userGroups }); } else { // 如果为空数组或未提供,则删除该字段,表示无用户组 - delete targetEntry.tags; + await db.updateUserInfoV2(targetUsername!, { tags: [] }); } break; @@ -431,7 +461,20 @@ export async function POST(request: NextRequest) { // 权限检查:站长可批量配置所有人的用户组,管理员只能批量配置普通用户 if (operatorRole !== 'owner') { 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) { return NextResponse.json({ error: `管理员无法操作其他管理员 ${targetUsername}` }, { status: 400 }); } @@ -440,14 +483,11 @@ export async function POST(request: NextRequest) { // 批量更新用户组 for (const targetUsername of usernames) { - const targetUser = adminConfig.UserConfig.Users.find(u => u.username === targetUsername); - if (targetUser) { - if (userGroups && userGroups.length > 0) { - targetUser.tags = userGroups; - } else { - // 如果为空数组或未提供,则删除该字段,表示无用户组 - delete targetUser.tags; - } + // 只更新V2存储 + if (userGroups && userGroups.length > 0) { + await db.updateUserInfoV2(targetUsername, { tags: userGroups }); + } else { + await db.updateUserInfoV2(targetUsername, { tags: [] }); } } diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..b733627 --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -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 } + ); + } +} diff --git a/src/app/api/auth/oidc/callback/route.ts b/src/app/api/auth/oidc/callback/route.ts index 59a846e..80889c5 100644 --- a/src/app/api/auth/oidc/callback/route.ts +++ b/src/app/api/auth/oidc/callback/route.ts @@ -148,15 +148,41 @@ export async function GET(request: NextRequest) { } // 检查用户是否已存在(通过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 cookieValue = await generateAuthCookie( - existingUser.username, - existingUser.role || 'user' - ); + const cookieValue = await generateAuthCookie(username, userRole); const expires = new Date(); expires.setDate(expires.getDate() + 7); diff --git a/src/app/api/auth/oidc/complete-register/route.ts b/src/app/api/auth/oidc/complete-register/route.ts index c20da65..7825353 100644 --- a/src/app/api/auth/oidc/complete-register/route.ts +++ b/src/app/api/auth/oidc/complete-register/route.ts @@ -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); if (existingUser) { return NextResponse.json( @@ -119,9 +132,16 @@ export async function POST(request: NextRequest) { ); } - // 检查OIDC sub是否已被使用 - const existingOIDCUser = config.UserConfig.Users.find((u: any) => u.oidcSub === oidcSession.sub); - if (existingOIDCUser) { + // 检查OIDC sub是否已被使用(优先使用新版本) + let existingOIDCUsername = await db.getUserByOidcSub(oidcSession.sub); + if (!existingOIDCUsername) { + // 回退到配置中查找 + const existingOIDCUser = config.UserConfig.Users.find((u: any) => u.oidcSub === oidcSession.sub); + if (existingOIDCUser) { + existingOIDCUsername = existingOIDCUser.username; + } + } + if (existingOIDCUsername) { return NextResponse.json( { error: '该OIDC账号已被注册' }, { status: 409 } @@ -132,9 +152,19 @@ export async function POST(request: NextRequest) { try { // 生成随机密码(OIDC用户不需要密码登录) 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); - // 将用户添加到配置中 + // 将用户添加到配置中(保持兼容性) const newUser: any = { username: username, role: 'user', @@ -143,8 +173,8 @@ export async function POST(request: NextRequest) { }; // 如果配置了默认用户组,分配给新用户 - if (siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0) { - newUser.tags = siteConfig.DefaultUserTags; + if (defaultTags) { + newUser.tags = defaultTags; } config.UserConfig.Users.push(newUser); diff --git a/src/app/api/change-password/route.ts b/src/app/api/change-password/route.ts index c9534f8..c566bb8 100644 --- a/src/app/api/change-password/route.ts +++ b/src/app/api/change-password/route.ts @@ -45,8 +45,8 @@ export async function POST(request: NextRequest) { ); } - // 修改密码 - await db.changePassword(username, newPassword); + // 修改密码(只更新V2存储) + await db.changePasswordV2(username, newPassword); return NextResponse.json({ ok: true }); } catch (error) { diff --git a/src/app/api/danmaku-filter/route.ts b/src/app/api/danmaku-filter/route.ts index b973451..abe1282 100644 --- a/src/app/api/danmaku-filter/route.ts +++ b/src/app/api/danmaku-filter/route.ts @@ -16,16 +16,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -55,16 +52,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const adminConfig = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = adminConfig.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } diff --git a/src/app/api/favorites/route.ts b/src/app/api/favorites/route.ts index 4bd7724..e28e637 100644 --- a/src/app/api/favorites/route.ts +++ b/src/app/api/favorites/route.ts @@ -24,16 +24,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -78,16 +75,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -149,16 +143,13 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts index 4967e95..b10c9d9 100644 --- a/src/app/api/login/route.ts +++ b/src/app/api/login/route.ts @@ -217,44 +217,69 @@ export async function POST(req: NextRequest) { const config = await getConfig(); 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 }); } - // 校验用户密码 - try { - const pass = await db.verifyUser(username, password); - if (!pass) { - 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 }); + if (!pass) { + return NextResponse.json( + { error: '用户名或密码错误' }, + { status: 401 } + ); } + + // 验证成功,设置认证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) { console.error('登录接口异常', error); return NextResponse.json({ error: '服务器错误' }, { status: 500 }); diff --git a/src/app/api/playrecords/route.ts b/src/app/api/playrecords/route.ts index 181ad0a..e1f3dbe 100644 --- a/src/app/api/playrecords/route.ts +++ b/src/app/api/playrecords/route.ts @@ -17,16 +17,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -50,16 +47,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -116,16 +110,13 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } diff --git a/src/app/api/register/route.ts b/src/app/api/register/route.ts index d6c8fba..7240d97 100644 --- a/src/app/api/register/route.ts +++ b/src/app/api/register/route.ts @@ -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); if (existingUser) { return NextResponse.json( @@ -131,24 +144,31 @@ export async function POST(req: NextRequest) { // 创建用户 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); - // 2. 将用户添加到管理员配置的用户列表中 + // 3. 将用户添加到管理员配置的用户列表中(保持兼容性) const newUser: any = { username: username, role: 'user', banned: false, }; - // 3. 如果配置了默认用户组,分配给新用户 - if (siteConfig.DefaultUserTags && siteConfig.DefaultUserTags.length > 0) { - newUser.tags = siteConfig.DefaultUserTags; + // 4. 如果配置了默认用户组,分配给新用户 + if (defaultTags) { + newUser.tags = defaultTags; } config.UserConfig.Users.push(newUser); - // 4. 保存更新后的配置 + // 5. 保存更新后的配置 await db.saveAdminConfig(config); // 注册成功 diff --git a/src/app/api/searchhistory/route.ts b/src/app/api/searchhistory/route.ts index 9a9e717..9333e53 100644 --- a/src/app/api/searchhistory/route.ts +++ b/src/app/api/searchhistory/route.ts @@ -23,16 +23,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -60,16 +57,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -112,16 +106,13 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } diff --git a/src/app/api/skipconfigs/route.ts b/src/app/api/skipconfigs/route.ts index 3762c4a..a011761 100644 --- a/src/app/api/skipconfigs/route.ts +++ b/src/app/api/skipconfigs/route.ts @@ -16,16 +16,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const config = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = config.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -59,16 +56,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const adminConfig = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = adminConfig.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } @@ -112,16 +106,13 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: '未登录' }, { status: 401 }); } - const adminConfig = await getConfig(); - if (authInfo.username !== process.env.ADMIN_USERNAME) { + if (authInfo.username !== process.env.USERNAME) { // 非站长,检查用户存在或被封禁 - const user = adminConfig.UserConfig.Users.find( - (u) => u.username === authInfo.username - ); - if (!user) { + const userInfoV2 = await db.getUserInfoV2(authInfo.username); + if (!userInfoV2) { return NextResponse.json({ error: '用户不存在' }, { status: 401 }); } - if (user.banned) { + if (userInfoV2.banned) { return NextResponse.json({ error: '用户已被封禁' }, { status: 401 }); } } diff --git a/src/lib/config.ts b/src/lib/config.ts index 91016cf..67e5432 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -470,14 +470,15 @@ export async function getAvailableApiSites(user?: string): Promise { return allApiSites; } - const userConfig = config.UserConfig.Users.find((u) => u.username === user); - if (!userConfig) { + // 从V2存储中获取用户信息 + const userInfoV2 = await db.getUserInfoV2(user); + if (!userInfoV2) { return allApiSites; } // 优先根据用户自己的 enabledApis 配置查找 - if (userConfig.enabledApis && userConfig.enabledApis.length > 0) { - const userApiSitesSet = new Set(userConfig.enabledApis); + if (userInfoV2.enabledApis && userInfoV2.enabledApis.length > 0) { + const userApiSitesSet = new Set(userInfoV2.enabledApis); return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({ key: s.key, name: s.name, @@ -487,11 +488,11 @@ export async function getAvailableApiSites(user?: string): Promise { } // 如果没有 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(); // 遍历用户的所有 tags,收集对应的 enabledApis - userConfig.tags.forEach(tagName => { + userInfoV2.tags.forEach(tagName => { const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName); if (tagConfig && tagConfig.enabledApis) { tagConfig.enabledApis.forEach(apiKey => enabledApisFromTags.add(apiKey)); diff --git a/src/lib/db.ts b/src/lib/db.ts index 074c4cc..7291d6a 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -132,7 +132,7 @@ export class DbManager { return favorite !== null; } - // ---------- 用户相关 ---------- + // ---------- 用户相关(旧版本,保持兼容) ---------- async registerUser(userName: string, password: string): Promise { await this.storage.registerUser(userName, password); } @@ -154,6 +154,197 @@ export class DbManager { await this.storage.deleteUser(userName); } + // ---------- 用户相关(新版本) ---------- + async createUserV2( + userName: string, + password: string, + role: 'owner' | 'admin' | 'user' = 'user', + tags?: string[], + oidcSub?: string, + enabledApis?: string[] + ): Promise { + 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 { + 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 { + if (typeof (this.storage as any).updateUserInfoV2 === 'function') { + await (this.storage as any).updateUserInfoV2(userName, updates); + } + } + + async changePasswordV2(userName: string, newPassword: string): Promise { + if (typeof (this.storage as any).changePasswordV2 === 'function') { + await (this.storage as any).changePasswordV2(userName, newPassword); + } + } + + async checkUserExistV2(userName: string): Promise { + if (typeof (this.storage as any).checkUserExistV2 === 'function') { + return (this.storage as any).checkUserExistV2(userName); + } + return false; + } + + async getUserByOidcSub(oidcSub: string): Promise { + 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 { + if (typeof (this.storage as any).deleteUserV2 === 'function') { + await (this.storage as any).deleteUserV2(userName); + } + } + + async getUsersByTag(tagName: string): Promise { + if (typeof (this.storage as any).getUsersByTag === 'function') { + return (this.storage as any).getUsersByTag(tagName); + } + return []; + } + + // ---------- 数据迁移 ---------- + async migrateUsersFromConfig(adminConfig: AdminConfig): Promise { + 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 { return this.storage.getSearchHistory(userName); diff --git a/src/lib/redis-base.db.ts b/src/lib/redis-base.db.ts index 98ba2a0..94aa0ba 100644 --- a/src/lib/redis-base.db.ts +++ b/src/lib/redis-base.db.ts @@ -242,7 +242,7 @@ export abstract class BaseRedisStorage implements IStorage { await this.withRetry(() => this.client.del(this.favKey(userName, key))); } - // ---------- 用户注册 / 登录 ---------- + // ---------- 用户注册 / 登录(旧版本,保持兼容) ---------- private userPwdKey(user: string) { 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 { + 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 { + const hashedPassword = await this.hashPassword(password); + const createdAt = Date.now(); + + // 存储用户信息到Hash + const userInfo: Record = { + 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 { + 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 { + const userInfo: Record = {}; + + 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 { + const hashedPassword = await this.hashPassword(newPassword); + await this.withRetry(() => + this.client.hSet(this.userInfoKey(userName), 'password', hashedPassword) + ); + } + + // 检查用户是否存在(新版本) + async checkUserExistV2(userName: string): Promise { + const exists = await this.withRetry(() => + this.client.exists(this.userInfoKey(userName)) + ); + return exists === 1; + } + + // 通过OIDC Sub查找用户名 + async getUserByOidcSub(oidcSub: string): Promise { + 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 { + // 获取用户信息 + 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) { return `u:${user}:sh`; // u:username:sh diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts index a274f43..4044f72 100644 --- a/src/lib/upstash.db.ts +++ b/src/lib/upstash.db.ts @@ -4,6 +4,7 @@ import { Redis } from '@upstash/redis'; import { AdminConfig } from './admin.types'; import { Favorite, IStorage, PlayRecord, SkipConfig } from './types'; +import { userInfoCache } from './user-cache'; // 搜索历史最大条数 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 { + 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 { + const hashedPassword = await this.hashPassword(password); + const createdAt = Date.now(); + + // 存储用户信息到Hash + const userInfo: Record = { + 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 { + 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 { + const userInfo: Record = {}; + + 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 { + const hashedPassword = await this.hashPassword(newPassword); + await withRetry(() => + this.client.hset(this.userInfoKey(userName), { password: hashedPassword }) + ); + + // 清除缓存 + userInfoCache?.delete(userName); + } + + // 检查用户是否存在(新版本) + async checkUserExistV2(userName: string): Promise { + const exists = await withRetry(() => + this.client.exists(this.userInfoKey(userName)) + ); + return exists === 1; + } + + // 通过OIDC Sub查找用户名 + async getUserByOidcSub(oidcSub: string): Promise { + const userName = await withRetry(() => + this.client.get(this.oidcSubKey(oidcSub)) + ); + return userName ? ensureString(userName) : null; + } + + // 获取使用特定用户组的用户列表 + async getUsersByTag(tagName: string): Promise { + 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 { + // 获取用户信息 + 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) { return `u:${user}:sh`; // u:username:sh diff --git a/src/lib/user-cache.ts b/src/lib/user-cache.ts new file mode 100644 index 0000000..92517b8 --- /dev/null +++ b/src/lib/user-cache.ts @@ -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 = 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): 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 };