数据迁移支持新用户

This commit is contained in:
mtvpls
2025-12-24 00:42:28 +08:00
parent d63192ef7a
commit bc8b9515a8
6 changed files with 127 additions and 6 deletions

View File

@@ -427,6 +427,7 @@ interface UserConfigProps {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
oidcSub?: string;
enabledApis?: string[];
created_at: number;
}> | null;
@@ -1453,7 +1454,7 @@ const UserConfig = ({ config, role, refreshConfig, usersV2, userPage, userTotalP
<td className='px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100'>
<div className='flex items-center gap-2'>
<span>{user.username}</span>
{(user as any).oidcSub && (
{user.oidcSub && (
<span className='px-2 py-0.5 text-xs rounded-full bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300'>
OIDC
</span>

View File

@@ -54,14 +54,26 @@ export async function POST(req: NextRequest) {
// 管理员配置
adminConfig: config,
// 所有用户数据
userData: {} as { [username: string]: any }
userData: {} as { [username: string]: any },
// V2用户信息
usersV2: [] as any[]
}
};
// 获取所有用户
// 获取所有V2用户
const usersV2Result = await db.getUserListV2(0, 10000, process.env.USERNAME);
exportData.data.usersV2 = usersV2Result.users;
// 获取所有用户(包括旧版用户)
let allUsers = await db.getAllUsers();
// 添加站长用户
allUsers.push(process.env.USERNAME);
// 添加V2用户
usersV2Result.users.forEach(user => {
if (!allUsers.includes(user.username)) {
allUsers.push(user.username);
}
});
allUsers = Array.from(new Set(allUsers));
// 为每个用户收集数据
@@ -76,7 +88,9 @@ export async function POST(req: NextRequest) {
// 跳过片头片尾配置
skipConfigs: await db.getAllSkipConfigs(username),
// 用户密码(通过验证空密码来检查用户是否存在,然后获取密码)
password: await getUserPassword(username)
password: await getUserPassword(username),
// V2用户的加密密码
passwordV2: await getUserPasswordV2(username)
};
exportData.data.userData[username] = userData;
@@ -134,3 +148,19 @@ async function getUserPassword(username: string): Promise<string | null> {
return null;
}
}
// 辅助函数获取V2用户的加密密码
async function getUserPasswordV2(username: string): Promise<string | null> {
try {
const storage = (db as any).storage;
if (storage && typeof storage.client?.hget === 'function') {
const userInfoKey = `user:${username}:info`;
const password = await storage.client.hget(userInfoKey, 'password');
return password;
}
return null;
} catch (error) {
console.error(`获取用户 ${username} V2密码失败:`, error);
return null;
}
}

View File

@@ -94,13 +94,73 @@ export async function POST(req: NextRequest) {
// 不影响主流程,继续执行
}
// 导入V2用户信息
if (importData.data.usersV2 && Array.isArray(importData.data.usersV2)) {
for (const userV2 of importData.data.usersV2) {
try {
// 跳过站长(站长使用环境变量认证)
if (userV2.role === 'owner') {
continue;
}
// 获取用户的加密密码
const userData = importData.data.userData[userV2.username];
const passwordV2 = userData?.passwordV2;
if (passwordV2) {
// 直接使用加密后的密码创建用户
const storage = (db as any).storage;
if (storage && typeof storage.client?.hset === 'function') {
const userInfoKey = `user:${userV2.username}:info`;
const createdAt = userV2.created_at || Date.now();
const userInfo: any = {
role: userV2.role,
banned: userV2.banned,
password: passwordV2,
created_at: createdAt.toString(),
};
if (userV2.tags && userV2.tags.length > 0) {
userInfo.tags = JSON.stringify(userV2.tags);
}
if (userV2.oidcSub) {
userInfo.oidcSub = userV2.oidcSub;
// 创建OIDC映射
const oidcSubKey = `oidc:sub:${userV2.oidcSub}`;
await storage.client.set(oidcSubKey, userV2.username);
}
if (userV2.enabledApis && userV2.enabledApis.length > 0) {
userInfo.enabledApis = JSON.stringify(userV2.enabledApis);
}
await storage.client.hset(userInfoKey, userInfo);
// 添加到用户列表Sorted Set
const userListKey = 'user:list';
await storage.client.zadd(userListKey, {
score: createdAt,
member: userV2.username,
});
console.log(`V2用户 ${userV2.username} 导入成功`);
}
}
} catch (error) {
console.error(`导入V2用户 ${userV2.username} 失败:`, error);
}
}
}
// 导入用户数据
const userData = importData.data.userData;
for (const username in userData) {
const user = userData[username];
// 重新注册用户(包含密码)
if (user.password) {
// 重新注册用户(包含密码)- 仅用于旧版用户
if (user.password && !importData.data.usersV2?.find((u: any) => u.username === username)) {
await db.registerUser(username, user.password);
}
@@ -139,6 +199,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({
message: '数据导入成功',
importedUsers: Object.keys(userData).length,
importedUsersV2: importData.data.usersV2?.length || 0,
timestamp: importData.timestamp,
serverVersion: typeof importData.serverVersion === 'string' ? importData.serverVersion : '未知版本'
});

View File

@@ -328,6 +328,31 @@ export async function getConfig(): Promise<AdminConfig> {
}
adminConfig = configSelfCheck(adminConfig);
cachedConfig = adminConfig;
// 自动迁移用户如果配置中有用户且V2存储支持
// 过滤掉站长后检查是否有需要迁移的用户
const nonOwnerUsers = adminConfig.UserConfig.Users.filter(
(u) => u.username !== process.env.USERNAME
);
if (!dbReadFailed && nonOwnerUsers.length > 0) {
try {
// 检查是否支持V2存储
const storage = (db as any).storage;
if (storage && typeof storage.createUserV2 === 'function') {
console.log('检测到配置中有用户,开始自动迁移...');
await db.migrateUsersFromConfig(adminConfig);
// 迁移完成后,清空配置中的用户列表并保存
adminConfig.UserConfig.Users = [];
await db.saveAdminConfig(adminConfig);
cachedConfig = adminConfig;
console.log('用户自动迁移完成');
}
} catch (error) {
console.error('自动迁移用户失败:', error);
// 不影响主流程,继续执行
}
}
return cachedConfig;
}

View File

@@ -234,6 +234,7 @@ export class DbManager {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
oidcSub?: string;
enabledApis?: string[];
created_at: number;
}>;

View File

@@ -504,6 +504,7 @@ export class UpstashRedisStorage implements IStorage {
role: 'owner' | 'admin' | 'user';
banned: boolean;
tags?: string[];
oidcSub?: string;
enabledApis?: string[];
created_at: number;
}>;
@@ -528,6 +529,7 @@ export class UpstashRedisStorage implements IStorage {
role: 'owner' as const,
banned: ownerInfo.banned,
tags: ownerInfo.tags,
oidcSub: ownerInfo.oidcSub,
enabledApis: ownerInfo.enabledApis,
created_at: ownerInfo.created_at,
});
@@ -549,6 +551,7 @@ export class UpstashRedisStorage implements IStorage {
role: userInfo.role,
banned: userInfo.banned,
tags: userInfo.tags,
oidcSub: userInfo.oidcSub,
enabledApis: userInfo.enabledApis,
created_at: userInfo.created_at,
});