数据迁移支持新用户
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 : '未知版本'
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,7 @@ export class DbManager {
|
||||
role: 'owner' | 'admin' | 'user';
|
||||
banned: boolean;
|
||||
tags?: string[];
|
||||
oidcSub?: string;
|
||||
enabledApis?: string[];
|
||||
created_at: number;
|
||||
}>;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user