数据迁移支持新用户
This commit is contained in:
@@ -427,6 +427,7 @@ interface UserConfigProps {
|
|||||||
role: 'owner' | 'admin' | 'user';
|
role: 'owner' | 'admin' | 'user';
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
enabledApis?: string[];
|
enabledApis?: string[];
|
||||||
created_at: number;
|
created_at: number;
|
||||||
}> | null;
|
}> | 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'>
|
<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'>
|
<div className='flex items-center gap-2'>
|
||||||
<span>{user.username}</span>
|
<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'>
|
<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
|
OIDC
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -54,14 +54,26 @@ export async function POST(req: NextRequest) {
|
|||||||
// 管理员配置
|
// 管理员配置
|
||||||
adminConfig: config,
|
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();
|
let allUsers = await db.getAllUsers();
|
||||||
// 添加站长用户
|
// 添加站长用户
|
||||||
allUsers.push(process.env.USERNAME);
|
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));
|
allUsers = Array.from(new Set(allUsers));
|
||||||
|
|
||||||
// 为每个用户收集数据
|
// 为每个用户收集数据
|
||||||
@@ -76,7 +88,9 @@ export async function POST(req: NextRequest) {
|
|||||||
// 跳过片头片尾配置
|
// 跳过片头片尾配置
|
||||||
skipConfigs: await db.getAllSkipConfigs(username),
|
skipConfigs: await db.getAllSkipConfigs(username),
|
||||||
// 用户密码(通过验证空密码来检查用户是否存在,然后获取密码)
|
// 用户密码(通过验证空密码来检查用户是否存在,然后获取密码)
|
||||||
password: await getUserPassword(username)
|
password: await getUserPassword(username),
|
||||||
|
// V2用户的加密密码
|
||||||
|
passwordV2: await getUserPasswordV2(username)
|
||||||
};
|
};
|
||||||
|
|
||||||
exportData.data.userData[username] = userData;
|
exportData.data.userData[username] = userData;
|
||||||
@@ -134,3 +148,19 @@ async function getUserPassword(username: string): Promise<string | null> {
|
|||||||
return 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;
|
const userData = importData.data.userData;
|
||||||
for (const username in userData) {
|
for (const username in userData) {
|
||||||
const user = userData[username];
|
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);
|
await db.registerUser(username, user.password);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +199,7 @@ export async function POST(req: NextRequest) {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
message: '数据导入成功',
|
message: '数据导入成功',
|
||||||
importedUsers: Object.keys(userData).length,
|
importedUsers: Object.keys(userData).length,
|
||||||
|
importedUsersV2: importData.data.usersV2?.length || 0,
|
||||||
timestamp: importData.timestamp,
|
timestamp: importData.timestamp,
|
||||||
serverVersion: typeof importData.serverVersion === 'string' ? importData.serverVersion : '未知版本'
|
serverVersion: typeof importData.serverVersion === 'string' ? importData.serverVersion : '未知版本'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -328,6 +328,31 @@ export async function getConfig(): Promise<AdminConfig> {
|
|||||||
}
|
}
|
||||||
adminConfig = configSelfCheck(adminConfig);
|
adminConfig = configSelfCheck(adminConfig);
|
||||||
cachedConfig = 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;
|
return cachedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,6 +234,7 @@ export class DbManager {
|
|||||||
role: 'owner' | 'admin' | 'user';
|
role: 'owner' | 'admin' | 'user';
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
enabledApis?: string[];
|
enabledApis?: string[];
|
||||||
created_at: number;
|
created_at: number;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -504,6 +504,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
role: 'owner' | 'admin' | 'user';
|
role: 'owner' | 'admin' | 'user';
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
oidcSub?: string;
|
||||||
enabledApis?: string[];
|
enabledApis?: string[];
|
||||||
created_at: number;
|
created_at: number;
|
||||||
}>;
|
}>;
|
||||||
@@ -528,6 +529,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
role: 'owner' as const,
|
role: 'owner' as const,
|
||||||
banned: ownerInfo.banned,
|
banned: ownerInfo.banned,
|
||||||
tags: ownerInfo.tags,
|
tags: ownerInfo.tags,
|
||||||
|
oidcSub: ownerInfo.oidcSub,
|
||||||
enabledApis: ownerInfo.enabledApis,
|
enabledApis: ownerInfo.enabledApis,
|
||||||
created_at: ownerInfo.created_at,
|
created_at: ownerInfo.created_at,
|
||||||
});
|
});
|
||||||
@@ -549,6 +551,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||||||
role: userInfo.role,
|
role: userInfo.role,
|
||||||
banned: userInfo.banned,
|
banned: userInfo.banned,
|
||||||
tags: userInfo.tags,
|
tags: userInfo.tags,
|
||||||
|
oidcSub: userInfo.oidcSub,
|
||||||
enabledApis: userInfo.enabledApis,
|
enabledApis: userInfo.enabledApis,
|
||||||
created_at: userInfo.created_at,
|
created_at: userInfo.created_at,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user