diff --git a/src/app/api/admin/data_migration/export/route.ts b/src/app/api/admin/data_migration/export/route.ts index 61c499f..f01362f 100644 --- a/src/app/api/admin/data_migration/export/route.ts +++ b/src/app/api/admin/data_migration/export/route.ts @@ -152,7 +152,18 @@ async function getUserPasswordV2(username: string): Promise { const storage = (db as any).storage; if (!storage) return null; - // 直接调用hGetAll获取完整用户信息(包括密码) + // 检查存储类型 + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + + // D1 存储:使用 getUserPasswordHash 方法 + if (storageType === 'd1') { + if (typeof storage.getUserPasswordHash === 'function') { + return await storage.getUserPasswordHash(username); + } + return null; + } + + // Redis 存储:直接调用hGetAll获取完整用户信息(包括密码) const userInfoKey = `user:${username}:info`; if (typeof storage.withRetry === 'function' && storage.client?.hgetall) { diff --git a/src/app/api/admin/data_migration/import/route.ts b/src/app/api/admin/data_migration/import/route.ts index 8ddb383..939aedb 100644 --- a/src/app/api/admin/data_migration/import/route.ts +++ b/src/app/api/admin/data_migration/import/route.ts @@ -104,6 +104,7 @@ export async function POST(req: NextRequest) { // 导入用户数据和user:info const userData = importData.data.userData; const storage = (db as any).storage; + // 使用前面已声明的 storageType 变量 const usersV2Map = new Map((importData.data.usersV2 || []).map((u: any) => [u.username, u])); const userCount = Object.keys(userData).length; @@ -127,44 +128,69 @@ export async function POST(req: NextRequest) { const createdAt = userV2?.created_at || Date.now(); - // 直接设置用户信息(不经过createUserV2,避免密码被再次hash) - const userInfoKey = `user:${username}:info`; - const userInfo: Record = { - role, - banned: String(userV2?.banned || false), - password: user.passwordV2, // 已经是hash过的密码,直接使用 - created_at: createdAt.toString(), - }; + // 根据存储类型使用不同的导入方法 + if (storageType === 'd1') { + // D1 存储:使用 createUserWithHashedPassword 方法 + try { + if (typeof storage.createUserWithHashedPassword === 'function') { + await storage.createUserWithHashedPassword( + username, + user.passwordV2, // 已经是hash过的密码 + role, + createdAt, + userV2?.tags, + userV2?.oidcSub, + userV2?.enabledApis, + userV2?.banned + ); + importedCount++; + console.log(`用户 ${username} 导入成功 (D1)`); + } else { + console.error(`D1 storage 缺少 createUserWithHashedPassword 方法`); + } + } catch (err) { + console.error(`导入用户 ${username} 失败:`, err); + } + } else { + // Redis 存储:直接设置用户信息 + const userInfoKey = `user:${username}:info`; + const userInfo: Record = { + role, + banned: String(userV2?.banned || false), + password: user.passwordV2, // 已经是hash过的密码,直接使用 + created_at: createdAt.toString(), + }; - if (userV2?.tags && userV2.tags.length > 0) { - userInfo.tags = JSON.stringify(userV2.tags); + if (userV2?.tags && userV2.tags.length > 0) { + userInfo.tags = JSON.stringify(userV2.tags); + } + + if (userV2?.oidcSub) { + userInfo.oidcSub = userV2.oidcSub; + } + + if (userV2?.enabledApis && userV2.enabledApis.length > 0) { + userInfo.enabledApis = JSON.stringify(userV2.enabledApis); + } + + // 使用storage.withRetry直接设置用户信息 + await storage.withRetry(() => storage.client.hSet(userInfoKey, userInfo)); + + // 添加到用户列表 + await storage.withRetry(() => storage.client.zAdd('user:list', { + score: createdAt, + value: username, + })); + + // 如果有oidcSub,创建映射 + if (userV2?.oidcSub) { + const oidcSubKey = `oidc:sub:${userV2.oidcSub}`; + await storage.withRetry(() => storage.client.set(oidcSubKey, username)); + } + + importedCount++; + console.log(`用户 ${username} 导入成功 (Redis)`); } - - if (userV2?.oidcSub) { - userInfo.oidcSub = userV2.oidcSub; - } - - if (userV2?.enabledApis && userV2.enabledApis.length > 0) { - userInfo.enabledApis = JSON.stringify(userV2.enabledApis); - } - - // 使用storage.withRetry直接设置用户信息 - await storage.withRetry(() => storage.client.hSet(userInfoKey, userInfo)); - - // 添加到用户列表 - await storage.withRetry(() => storage.client.zAdd('user:list', { - score: createdAt, - value: username, - })); - - // 如果有oidcSub,创建映射 - if (userV2?.oidcSub) { - const oidcSubKey = `oidc:sub:${userV2.oidcSub}`; - await storage.withRetry(() => storage.client.set(oidcSubKey, username)); - } - - importedCount++; - console.log(`用户 ${username} 导入成功`); } else { console.log(`跳过用户 ${username}:没有passwordV2`); } diff --git a/src/lib/d1-adapter.ts b/src/lib/d1-adapter.ts index 8bda51f..179c6ac 100644 --- a/src/lib/d1-adapter.ts +++ b/src/lib/d1-adapter.ts @@ -7,6 +7,13 @@ * 注意:此模块仅在服务端使用,通过 webpack 配置排除客户端打包 */ +// Cloudflare D1 Database 接口 +export interface D1Database { + prepare(query: string): D1PreparedStatement; + batch(statements: any[]): Promise; + exec(query: string): Promise; +} + // D1 PreparedStatement 接口 export interface D1PreparedStatement { bind(...values: any[]): D1PreparedStatement; diff --git a/src/lib/d1.db.ts b/src/lib/d1.db.ts index 0c806a0..681aa39 100644 --- a/src/lib/d1.db.ts +++ b/src/lib/d1.db.ts @@ -14,8 +14,8 @@ import { DanmakuFilterConfig, Notification, MovieRequest, - AdminConfig, } from './types'; +import { AdminConfig } from './admin.types'; import { DatabaseAdapter } from './d1-adapter'; /** @@ -735,6 +735,72 @@ export class D1Storage implements IStorage { } } + // 获取用户密码哈希(用于数据导出) + async getUserPasswordHash(userName: string): Promise { + try { + const user = await this.db + .prepare('SELECT password_hash FROM users WHERE username = ?') + .bind(userName) + .first(); + + return user ? (user.password_hash as string) : null; + } catch (err) { + console.error('D1Storage.getUserPasswordHash error:', err); + return null; + } + } + + // 直接设置用户密码哈希(用于数据导入,不进行二次哈希) + async setUserPasswordHash(userName: string, passwordHash: string): Promise { + try { + await this.db + .prepare('UPDATE users SET password_hash = ? WHERE username = ?') + .bind(passwordHash, userName) + .run(); + } catch (err) { + console.error('D1Storage.setUserPasswordHash error:', err); + throw err; + } + } + + // 直接创建用户(用于数据导入,密码已经是哈希值) + async createUserWithHashedPassword( + userName: string, + passwordHash: string, + role: 'owner' | 'admin' | 'user', + createdAt: number, + tags?: string[], + oidcSub?: string, + enabledApis?: string[], + banned?: boolean + ): Promise { + try { + await this.db + .prepare(` + INSERT INTO users ( + username, password_hash, role, banned, tags, oidc_sub, + enabled_apis, created_at, playrecord_migrated, + favorite_migrated, skip_migrated + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, 1, 1) + `) + .bind( + userName, + passwordHash, + role, + banned ? 1 : 0, + tags ? JSON.stringify(tags) : null, + oidcSub || null, + enabledApis ? JSON.stringify(enabledApis) : null, + createdAt + ) + .run(); + } catch (err) { + console.error('D1Storage.createUserWithHashedPassword error:', err); + throw err; + } + } + async getUserEmail?(userName: string): Promise { try { const result = await this.db diff --git a/src/lib/db.ts b/src/lib/db.ts index 7735ef7..a5c4fec 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -49,41 +49,40 @@ function getD1Adapter(): any { // 动态导入适配器以避免客户端打包 const { CloudflareD1Adapter, SQLiteAdapter } = require('./d1-adapter'); + // 检查是否为 Cloudflare 构建 + const isCloudflare = process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare'; + // 生产环境:Cloudflare Workers/Pages - if (typeof process !== 'undefined' && (process as any).env?.DB) { + if (isCloudflare && typeof process !== 'undefined' && (process as any).env?.DB) { console.log('Using Cloudflare D1 database'); return new CloudflareD1Adapter((process as any).env.DB); } // 开发环境:better-sqlite3 - if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { - try { - const Database = require('better-sqlite3'); - const path = require('path'); - const fs = require('fs'); + try { + const Database = require('better-sqlite3'); + const path = require('path'); + const fs = require('fs'); - const dbPath = path.join(process.cwd(), '.data', 'moontv.db'); + const dbPath = path.join(process.cwd(), '.data', 'moontv.db'); - // 检查数据库文件是否存在 - if (!fs.existsSync(dbPath)) { - console.error('❌ SQLite database not found. Please run: npm run init:sqlite'); - throw new Error('SQLite database not initialized'); - } - - const db = new Database(dbPath); - db.pragma('journal_mode = WAL'); // 启用 WAL 模式提升性能 - - console.log('Using SQLite database (development mode)'); - console.log('Database location:', dbPath); - - return new SQLiteAdapter(db); - } catch (err) { - console.error('Failed to initialize SQLite:', err); - throw err; + // 检查数据库文件是否存在 + if (!fs.existsSync(dbPath)) { + console.error('❌ SQLite database not found. Please run: npm run init:sqlite'); + throw new Error('SQLite database not initialized'); } - } - throw new Error('D1 database not available. Set NEXT_PUBLIC_STORAGE_TYPE to another option or configure D1.'); + const db = new Database(dbPath); + db.pragma('journal_mode = WAL'); // 启用 WAL 模式提升性能 + + console.log('Using SQLite database (development mode)'); + console.log('Database location:', dbPath); + + return new SQLiteAdapter(db); + } catch (err) { + console.error('Failed to initialize SQLite:', err); + throw err; + } } // 单例存储实例