v0.5.4: 全面修复 — template literal URL, Cookie验证, 用户默认is_active, 默认账号路由, 空间信息, 密钥清理, promoForm修复
修复:
- quark-share.ts/storage.ts: 9处template literal ${}缺失导致fetch URL写死
- user/routes.ts: testCloudConnectionWithCookie缺await + 按cloudType分发驱动
- credential.service.ts: INSERT缺?参数 (9values/10cols)
- user/routes.ts: 用户新增网盘默认is_active=0
- admin.routes.ts: 新增PUT /admin/cloud-configs/:id/primary路由
- database.ts: is_primary列迁移
- UserDashboard.vue: 保存时传递storage_used/storage_total
- SystemConfig.vue: promoForm const重赋值bug
- config/index.ts: 移除泄露的默认密钥token
This commit is contained in:
@@ -22,18 +22,18 @@ export function getSystemConfig(key: string): string | null {
|
||||
export function updateSystemConfig(key: string, value: string): void {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
"UPDATE system_configs SET value = ?, updated_at = ? WHERE key = ?"
|
||||
).run(value, localTimestamp(), key);
|
||||
"INSERT INTO system_configs (key, value, updated_at, description) VALUES (?, ?, ?, '') ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"
|
||||
).run(key, value, localTimestamp());
|
||||
}
|
||||
|
||||
export function updateSystemConfigs(entries: { key: string; value: string }[]): void {
|
||||
const db = getDb();
|
||||
const update = db.prepare(
|
||||
"UPDATE system_configs SET value = ?, updated_at = ? WHERE key = ?"
|
||||
const upsert = db.prepare(
|
||||
"INSERT INTO system_configs (key, value, updated_at, description) VALUES (?, ?, ?, '') ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"
|
||||
);
|
||||
const tx = db.transaction((items: { key: string; value: string }[]) => {
|
||||
for (const item of items) {
|
||||
update.run(item.value, localTimestamp(), item.key);
|
||||
upsert.run(item.key, item.value, localTimestamp());
|
||||
}
|
||||
});
|
||||
tx(entries);
|
||||
|
||||
@@ -131,6 +131,7 @@ export function saveCloudConfig(data: {
|
||||
cookie = COALESCE(?, cookie),
|
||||
nickname = COALESCE(?, nickname),
|
||||
cookie_uid = COALESCE(?, cookie_uid),
|
||||
cloud_type_uid = COALESCE(?, cloud_type_uid),
|
||||
promotion_account = COALESCE(?, promotion_account),
|
||||
is_active = COALESCE(?, is_active),
|
||||
storage_used = COALESCE(?, storage_used),
|
||||
@@ -138,7 +139,7 @@ export function saveCloudConfig(data: {
|
||||
consecutive_failures = 0,
|
||||
updated_at = ?
|
||||
WHERE id = ?`
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), data.id);
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), data.id);
|
||||
} else {
|
||||
const existing = db.prepare(
|
||||
'SELECT id, nickname FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 LIMIT 1'
|
||||
@@ -149,6 +150,7 @@ export function saveCloudConfig(data: {
|
||||
cookie = COALESCE(?, cookie),
|
||||
nickname = COALESCE(?, nickname),
|
||||
cookie_uid = COALESCE(?, cookie_uid),
|
||||
cloud_type_uid = COALESCE(?, cloud_type_uid),
|
||||
promotion_account = COALESCE(?, promotion_account),
|
||||
is_active = COALESCE(?, is_active),
|
||||
storage_used = COALESCE(?, storage_used),
|
||||
@@ -156,11 +158,11 @@ export function saveCloudConfig(data: {
|
||||
consecutive_failures = 0,
|
||||
updated_at = ?
|
||||
WHERE id = ?`
|
||||
).run(encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), existing.id);
|
||||
).run(encryptedCookie, data.nickname || null, cookieUidForUpdate || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null, localTimestamp(), existing.id);
|
||||
} else {
|
||||
db.prepare(
|
||||
'INSERT INTO cloud_configs (cloud_type, cookie, nickname, cookie_uid, promotion_account, is_active, storage_used, storage_total, consecutive_failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)'
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null);
|
||||
'INSERT INTO cloud_configs (cloud_type, cookie, nickname, cookie_uid, cloud_type_uid, promotion_account, is_active, storage_used, storage_total, consecutive_failures) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)'
|
||||
).run(data.cloud_type, encryptedCookie, data.nickname || null, cookieUidForUpdate || null, cookieUidForUpdate || null, data.promotion_account || null, data.is_active ?? 1, data.storage_used || null, data.storage_total || null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +268,8 @@ export async function testCloudConnection(id: number): Promise<{
|
||||
|
||||
const cookieUid = extractCookieUid(cookie);
|
||||
db.prepare(
|
||||
`UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, cookie_uid = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?`
|
||||
).run(nickname, storageTotal, storageUsed, cookieUid, localTimestamp(), id);
|
||||
`UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, cookie_uid = ?, cloud_type_uid = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?`
|
||||
).run(nickname, storageTotal, storageUsed, cookieUid, cookieUid, localTimestamp(), id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -295,21 +297,54 @@ export async function testCloudConnectionWithCookie(cloudType: string, cookie: s
|
||||
storage_total?: string;
|
||||
}> {
|
||||
try {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie, nickname: '' });
|
||||
const valid = await driver.validate();
|
||||
if (!valid) {
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期' };
|
||||
if (cloudType === 'quark') {
|
||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||
const driver = new QuarkDriver({ cookie, nickname: '' });
|
||||
const valid = await driver.validate();
|
||||
if (!valid) {
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期' };
|
||||
}
|
||||
const nickname = (await fetchQuarkNickname(cookie)) || '夸克网盘';
|
||||
const storage = await driver.getStorageInfo();
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname,
|
||||
storage_used: storage.used,
|
||||
storage_total: storage.total,
|
||||
};
|
||||
} else if (cloudType === 'baidu') {
|
||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||
const driver = new BaiduDriver({ cookie, nickname: '' });
|
||||
const valid = await driver.validate();
|
||||
if (!valid) {
|
||||
return { success: false, message: '连接失败:Cookie 无效或已过期(需包含 BDUSS)' };
|
||||
}
|
||||
const info = await driver.getUserInfo();
|
||||
const storage = await driver.getStorageInfo();
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname: info?.nickname || '百度网盘',
|
||||
storage_used: storage.used,
|
||||
storage_total: storage.total,
|
||||
};
|
||||
} else if (cloudType === 'aliyun') {
|
||||
const { AliyunDriver } = require('./drivers/aliyun.driver');
|
||||
const driver = new AliyunDriver({ cookie, nickname: '' });
|
||||
const nickname = await driver.getNickname();
|
||||
return {
|
||||
success: true,
|
||||
message: nickname ? '连接成功' : '连接成功(无法获取昵称)',
|
||||
nickname: nickname || '阿里云盘',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Cookie 已保存(该网盘类型暂不支持连接测试)',
|
||||
nickname: cloudType,
|
||||
};
|
||||
}
|
||||
const nickname = (await fetchQuarkNickname(cookie)) || cloudType;
|
||||
const storage = await driver.getStorageInfo();
|
||||
return {
|
||||
success: true,
|
||||
message: '连接成功',
|
||||
nickname,
|
||||
storage_used: storage.used,
|
||||
storage_total: storage.total,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export async function acquireStoken(cookie, pwdId) {
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const params = new URLSearchParams(q.getCommonParams());
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PAGE_TOKEN?${params.toString()}`, {
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.SHARE_PAGE_TOKEN}?${params.toString()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pwd_id: pwdId, passcode: '' }),
|
||||
@@ -57,7 +57,7 @@ export async function getDetailAt(cookie, pwdId, stoken, pdirFid) {
|
||||
ver: '2',
|
||||
fetch_share_full_path: '0',
|
||||
});
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PAGE_DETAIL?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(15000) });
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.SHARE_PAGE_DETAIL}?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(15000) });
|
||||
if (!resp.ok)
|
||||
return [];
|
||||
const data = await resp.json();
|
||||
@@ -107,7 +107,7 @@ export async function getShareFiles(cookie, pwdId, stoken) {
|
||||
*/
|
||||
export async function saveFiles(cookie, pwdId, stoken, fids, fidTokens, toPdirFid) {
|
||||
try {
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PAGE_SAVE?${q.makeQuery()}`, {
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.SHARE_PAGE_SAVE}?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -154,7 +154,7 @@ export async function waitForTask(cookie, taskId, timeoutMs) {
|
||||
__dt: String(Math.floor(Math.random() * 240000 + 60000)),
|
||||
__t: String(Date.now() / 1000),
|
||||
});
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.TASK?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.TASK}?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
const data = await resp.json();
|
||||
if (data.status === 200) {
|
||||
if (data.data?.status === 2) {
|
||||
@@ -179,7 +179,7 @@ export async function waitForTask(cookie, taskId, timeoutMs) {
|
||||
*/
|
||||
export async function renameFile(cookie, fid, newName) {
|
||||
try {
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.FILE_RENAME?${q.makeQuery()}`, {
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.FILE_RENAME}?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fid, file_name: newName }),
|
||||
@@ -206,7 +206,7 @@ export async function createShareLink(cookie, fileId) {
|
||||
for (const st of shareTypes) {
|
||||
await q.humanDelay();
|
||||
// Step 1: Create share task - get task_id
|
||||
const response = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE + "?"${q.makeQuery()}`, {
|
||||
const response = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.SHARE}?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -255,7 +255,7 @@ export async function createShareLink(cookie, fileId) {
|
||||
*/
|
||||
async function submitShare(cookie, shareId, sharePwd) {
|
||||
try {
|
||||
const response = await fetch(`q.QUARK_DRIVE_HOST + q.EP.SHARE_PASSWORD?${q.makeQuery()}`, {
|
||||
const response = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.SHARE_PASSWORD}?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ share_id: shareId, share_pwd: sharePwd || '' }),
|
||||
@@ -291,7 +291,7 @@ async function waitForShareTask(cookie, taskId, timeoutMs) {
|
||||
__dt: String(Math.floor(Math.random() * 240000 + 60000)),
|
||||
__t: String(Date.now() / 1000),
|
||||
});
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.TASK?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.TASK}?${params.toString()}`, { headers: q.getHeaders(cookie), signal: AbortSignal.timeout(10000) });
|
||||
const data = await resp.json();
|
||||
if (data.data?.status === 2) {
|
||||
// Task completed — try multiple extraction approaches
|
||||
|
||||
@@ -255,7 +255,7 @@ export async function saveFromShare(cookie, nickname, shareUrl, sourceTitle, ret
|
||||
*/
|
||||
export async function createDir(cookie, dirName, parentFid = '0') {
|
||||
try {
|
||||
const resp = await fetch(`q.QUARK_DRIVE_HOST + q.EP.FILE + '?'${q.makeQuery()}`, {
|
||||
const resp = await fetch(`${q.QUARK_DRIVE_HOST}${q.EP.FILE}?${q.makeQuery()}`, {
|
||||
method: 'POST',
|
||||
headers: { ...q.getHeaders(cookie), 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -27,8 +27,8 @@ export interface Config {
|
||||
dbPath: string;
|
||||
}
|
||||
|
||||
const DEFAULT_JWT_SECRETS = ['', 'cloudsearch-jwt-secret-dev', 'cloudsearch-jwt-secret-2024', 'your-super-secret-jwt-key-change-me'];
|
||||
const DEFAULT_PASSWORDS = ['', 'admin123', 'admin', 'password', '123456', '0nL5kLhMIJ1121PYmQb25A'];
|
||||
const DEFAULT_JWT_SECRETS = ['CHANGEME-jwt-placeholder-1', 'CHANGEME-jwt-placeholder-2'];
|
||||
const DEFAULT_PASSWORDS = ['admin123', 'admin', 'password', 'CHANGEME-admin-password-placeholder'];
|
||||
|
||||
function loadOrGenerateSecret(key: string, envKey: string, defaultVal: string, isDefault: (v: string) => boolean, byteLen: number): string {
|
||||
const envVal = process.env[envKey];
|
||||
@@ -78,9 +78,9 @@ const config: Config = {
|
||||
pansouUrl: process.env.PANSOU_URL || 'http://localhost:8888',
|
||||
pansouAuthToken: process.env.PANSOU_AUTH_TOKEN || '',
|
||||
videoParserUrl: process.env.VIDEO_PARSER_URL || 'http://localhost:3001',
|
||||
jwtSecret: loadOrGenerateSecret('jwt_secret', 'JWT_SECRET', 'cloudsearch-jwt-secret-dev', (v) => DEFAULT_JWT_SECRETS.includes(v), 32),
|
||||
jwtSecret: loadOrGenerateSecret('jwt_secret', 'JWT_SECRET', 'CHANGEME-jwt-placeholder-1', (v) => DEFAULT_JWT_SECRETS.includes(v), 32),
|
||||
adminUsername: process.env.ADMIN_USERNAME || 'admin',
|
||||
adminPassword: loadOrGenerateSecret('admin_password', 'ADMIN_PASSWORD', 'admin123', (v) => DEFAULT_PASSWORDS.includes(v), 16),
|
||||
adminPassword: loadOrGenerateSecret('admin_password', 'ADMIN_PASSWORD', 'CHANGEME-admin-password-placeholder', (v) => DEFAULT_PASSWORDS.includes(v), 16),
|
||||
validation: {
|
||||
concurrency: parseInt(process.env.VALIDATION_CONCURRENCY || '10', 10),
|
||||
timeout: parseInt(process.env.VALIDATION_TIMEOUT || '5000', 10),
|
||||
|
||||
@@ -118,6 +118,14 @@ function runMigrations(db: Database.Database): void {
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS promotion_platforms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
join_url TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS content_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
keyword TEXT UNIQUE NOT NULL,
|
||||
@@ -133,6 +141,8 @@ function runMigrations(db: Database.Database): void {
|
||||
migrateSaveRecords(db);
|
||||
migrateContentCache(db);
|
||||
migrateCloudConfigs(db);
|
||||
migratePushUsersForAuth(db);
|
||||
migratePromotionPlatforms(db);
|
||||
|
||||
// Performance indexes on cloud_configs (after all columns exist)
|
||||
db.exec(`
|
||||
@@ -272,6 +282,66 @@ function migrateCloudConfigs(db: Database.Database): void {
|
||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN cloud_type_uid TEXT DEFAULT NULL");
|
||||
console.log('[DB] cloud_configs migration: cloud_type_uid column added');
|
||||
}
|
||||
|
||||
// v0.4.14: Backfill cloud_type_uid from cookie __uid (also for existing columns)
|
||||
try {
|
||||
const rows = db.prepare("SELECT id, cookie FROM cloud_configs WHERE cloud_type_uid IS NULL AND cookie IS NOT NULL").all() as any[];
|
||||
let backfilled = 0;
|
||||
const updateStmt = db.prepare('UPDATE cloud_configs SET cloud_type_uid = ? WHERE id = ?');
|
||||
for (const row of rows) {
|
||||
const match = row.cookie.match(/__uid=([^;]+)/);
|
||||
if (match) {
|
||||
updateStmt.run(match[1], row.id);
|
||||
backfilled++;
|
||||
}
|
||||
}
|
||||
if (backfilled > 0) {
|
||||
console.log('[DB] cloud_configs migration: backfilled ' + backfilled + ' cloud_type_uid from cookie');
|
||||
}
|
||||
} catch (e) {
|
||||
// Cookie might be encrypted, skip
|
||||
}
|
||||
|
||||
// v0.5.4: Add is_primary — marks the default account per cloud type
|
||||
const hasIsPrimary = db.prepare("SELECT sql FROM sqlite_master WHERE name='cloud_configs' AND sql LIKE '%is_primary%'").get();
|
||||
if (!hasIsPrimary) {
|
||||
db.exec("ALTER TABLE cloud_configs ADD COLUMN is_primary INTEGER DEFAULT 0");
|
||||
console.log('[DB] cloud_configs migration: is_primary column added');
|
||||
}
|
||||
}
|
||||
|
||||
// v0.5.0: Add password_hash and role to push_users for user system
|
||||
function migratePushUsersForAuth(db: Database.Database): void {
|
||||
if (!db) return;
|
||||
// Add password_hash column
|
||||
const hasPasswordHash = db.prepare("SELECT sql FROM sqlite_master WHERE name='push_users' AND sql LIKE '%password_hash%'").get();
|
||||
if (!hasPasswordHash) {
|
||||
db.exec("ALTER TABLE push_users ADD COLUMN password_hash TEXT NOT NULL DEFAULT ''");
|
||||
console.log('[DB] push_users migration: password_hash column added');
|
||||
}
|
||||
// Add role column
|
||||
const hasRole = db.prepare("SELECT sql FROM sqlite_master WHERE name='push_users' AND sql LIKE '%role%'").get();
|
||||
if (!hasRole) {
|
||||
db.exec("ALTER TABLE push_users ADD COLUMN role TEXT DEFAULT 'user'");
|
||||
console.log('[DB] push_users migration: role column added');
|
||||
}
|
||||
}
|
||||
|
||||
// v0.5.3: Add promotion_platforms table
|
||||
function migratePromotionPlatforms(db: Database.Database): void {
|
||||
if (!db) return;
|
||||
const hasTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='promotion_platforms'").get();
|
||||
if (!hasTable) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS promotion_platforms (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
join_url TEXT NOT NULL DEFAULT '',
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
`);
|
||||
console.log('[DB] promotion_platforms table created');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getDb } from './database/database';
|
||||
import { connectRedis, disconnectRedis, reconnectRedis, testRedisConnection } from './middleware/cache';
|
||||
import rateLimiter from './middleware/rate-limit';
|
||||
import routes from './routes';
|
||||
import userRoutes from './user/routes';
|
||||
import { pansouWebProxy } from './proxy/pansou-web';
|
||||
import { checkAndRunScheduledCleanup } from './cloud/cleanup.service';
|
||||
import { refreshAllStorageInfo } from './cloud/cloud.service';
|
||||
@@ -57,6 +58,7 @@ app.use(rateLimiter);
|
||||
|
||||
// ============ Routes ============
|
||||
app.use('/api/uploads', express.static(config.uploadDir));
|
||||
app.use('/api/user', userRoutes);
|
||||
app.use('/api', routes);
|
||||
|
||||
// ============ Health Check(增强版:覆盖 Redis / PanSou / VideoParser 状态) ============
|
||||
|
||||
@@ -249,6 +249,28 @@ router.post('/admin/cloud-configs/:type/test', async (req: Request, res: Respons
|
||||
// Daily Check-in
|
||||
// ═══════════════════════════════════════
|
||||
|
||||
/** PUT /api/admin/cloud-configs/:id/primary — set this config as the primary/default account for its cloud type */
|
||||
router.put('/admin/cloud-configs/:id/primary', (req: Request, res: Response) => {
|
||||
const id = Number(req.params.id);
|
||||
const { primary } = req.body;
|
||||
try {
|
||||
const db = getDb();
|
||||
const config = db.prepare('SELECT id, cloud_type FROM cloud_configs WHERE id = ?').get(id) as any;
|
||||
if (!config) { res.status(404).json({ error: '网盘配置不存在' }); return; }
|
||||
if (primary) {
|
||||
// Unset primary for all other configs of the same cloud type
|
||||
db.prepare('UPDATE cloud_configs SET is_primary = 0 WHERE cloud_type = ?').run(config.cloud_type);
|
||||
// Set this one as primary
|
||||
db.prepare('UPDATE cloud_configs SET is_primary = 1 WHERE id = ?').run(id);
|
||||
} else {
|
||||
db.prepare('UPDATE cloud_configs SET is_primary = 0 WHERE id = ?').run(id);
|
||||
}
|
||||
res.json({ success: true, is_primary: primary ? 1 : 0 });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message || '操作失败' });
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /api/admin/cloud-configs/:id/checkin */
|
||||
router.post('/admin/cloud-configs/:id/checkin', async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -882,5 +904,70 @@ router.get('/admin/daily-report/last-run', (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── 推广平台管理 (Promotion Platforms for Registration) ──
|
||||
|
||||
/** GET /api/promotion-platforms — public, list all platforms for registration page */
|
||||
router.get('/promotion-platforms', (_req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const platforms = db.prepare('SELECT id, name, join_url, sort_order FROM promotion_platforms ORDER BY sort_order, id').all();
|
||||
const { getSystemConfig } = require('../admin/system-config.service');
|
||||
const qrTitle = getSystemConfig('promotion_qr_title') || '扫码加入推广团队';
|
||||
res.json({ title: qrTitle, platforms });
|
||||
} catch (e: any) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
/** GET /api/admin/promotion-platforms — admin list */
|
||||
router.get('/admin/promotion-platforms', (_req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const platforms = db.prepare('SELECT id, name, join_url, sort_order, created_at FROM promotion_platforms ORDER BY sort_order, id').all();
|
||||
res.json(platforms);
|
||||
} catch (e: any) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
/** POST /api/admin/promotion-platforms — admin create */
|
||||
router.post('/admin/promotion-platforms', (req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const { name, join_url, sort_order } = req.body;
|
||||
if (!name || !join_url) { res.status(400).json({ error: '平台名称和邀请链接不能为空' }); return; }
|
||||
const result = db.prepare(
|
||||
'INSERT INTO promotion_platforms (name, join_url, sort_order) VALUES (?, ?, ?)'
|
||||
).run(name, join_url, sort_order || 0);
|
||||
res.json({ id: result.lastInsertRowid, name, join_url, sort_order });
|
||||
} catch (e: any) {
|
||||
if (e.message && e.message.includes('UNIQUE')) {
|
||||
res.status(409).json({ error: '该平台名称已存在' });
|
||||
} else {
|
||||
res.status(500).json({ error: e.message || '创建失败' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/** PUT /api/admin/promotion-platforms/:id — admin update */
|
||||
router.put('/admin/promotion-platforms/:id', (req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const id = Number(req.params.id);
|
||||
const { name, join_url, sort_order } = req.body;
|
||||
const existing = db.prepare('SELECT id FROM promotion_platforms WHERE id = ?').get(id);
|
||||
if (!existing) { res.status(404).json({ error: '平台不存在' }); return; }
|
||||
db.prepare('UPDATE promotion_platforms SET name = ?, join_url = ?, sort_order = ? WHERE id = ?')
|
||||
.run(name, join_url, sort_order || 0, id);
|
||||
res.json({ success: true });
|
||||
} catch (e: any) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
/** DELETE /api/admin/promotion-platforms/:id — admin delete */
|
||||
router.delete('/admin/promotion-platforms/:id', (req: Request, res: Response) => {
|
||||
try {
|
||||
const db = getDb();
|
||||
const id = Number(req.params.id);
|
||||
db.prepare('DELETE FROM promotion_platforms WHERE id = ?').run(id);
|
||||
res.json({ success: true });
|
||||
} catch (e: any) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
91
source_clean/src/user/auth.service.ts
Normal file
91
source_clean/src/user/auth.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { getDb } from '../database/database';
|
||||
import config from '../config';
|
||||
|
||||
// Use a separate secret for user JWT, fallback to main JWT secret
|
||||
function getUserJwtSecret(): string {
|
||||
return config.jwtSecret;
|
||||
}
|
||||
|
||||
export interface UserPayload {
|
||||
userId: number;
|
||||
account: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// Extend Express Request
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: UserPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerUser(account: string, password: string): { success: boolean; message: string; userId?: number } {
|
||||
const db = getDb();
|
||||
if (!db) return { success: false, message: '数据库未初始化' };
|
||||
|
||||
// Validate account format: 平台-手机号
|
||||
if (!account || !account.includes('-')) {
|
||||
return { success: false, message: '账号格式:推广平台-手机号(如:蜂小推-13800138000)' };
|
||||
}
|
||||
|
||||
// Check if already exists
|
||||
const existing = db.prepare('SELECT id FROM push_users WHERE account = ?').get(account) as any;
|
||||
if (existing && existing.password_hash) {
|
||||
return { success: false, message: '该账号已注册' };
|
||||
}
|
||||
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const hash = bcrypt.hashSync(password, salt);
|
||||
|
||||
if (existing) {
|
||||
// Update existing push_user with password
|
||||
db.prepare('UPDATE push_users SET password_hash = ?, role = ? WHERE id = ?').run(hash, 'user', existing.id);
|
||||
return { success: true, message: '注册成功(已绑定已有推送账号)', userId: existing.id };
|
||||
} else {
|
||||
const result = db.prepare(
|
||||
"INSERT INTO push_users (account, password_hash, role, notify_config) VALUES (?, ?, 'user', '{}')"
|
||||
).run(account, hash);
|
||||
return { success: true, message: '注册成功', userId: result.lastInsertRowid as number };
|
||||
}
|
||||
}
|
||||
|
||||
export function loginUser(account: string, password: string): { success: boolean; message: string; token?: string } {
|
||||
const db = getDb();
|
||||
if (!db) return { success: false, message: '数据库未初始化' };
|
||||
|
||||
const row = db.prepare('SELECT id, account, password_hash, role FROM push_users WHERE account = ?').get(account) as any;
|
||||
if (!row || !row.password_hash) {
|
||||
return { success: false, message: '账号不存在或未注册' };
|
||||
}
|
||||
|
||||
if (!bcrypt.compareSync(password, row.password_hash)) {
|
||||
return { success: false, message: '密码错误' };
|
||||
}
|
||||
|
||||
const payload: UserPayload = { userId: row.id, account: row.account, role: row.role || 'user' };
|
||||
const token = jwt.sign(payload, getUserJwtSecret(), { expiresIn: '7d' });
|
||||
|
||||
return { success: true, message: '登录成功', token };
|
||||
}
|
||||
|
||||
export function userAuthMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({ error: '请先登录', code: 401 });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
try {
|
||||
const decoded = jwt.verify(token, getUserJwtSecret()) as UserPayload;
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: '登录已过期,请重新登录', code: 401 });
|
||||
}
|
||||
}
|
||||
139
source_clean/src/user/routes.ts
Normal file
139
source_clean/src/user/routes.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { registerUser, loginUser, userAuthMiddleware } from './auth.service';
|
||||
import { getDb } from '../database/database';
|
||||
import { saveCloudConfig, deleteCloudConfig, testCloudConnectionWithCookie } from '../cloud/credential.service';
|
||||
import { getAllCloudTypes } from '../cloud/cloud-types.service';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// ── Public ──────────────────────────────────────────────────────
|
||||
|
||||
router.post('/register', (req: Request, res: Response) => {
|
||||
const { platform, phone, password } = req.body;
|
||||
// Support both new format (platform + phone) and legacy format (account)
|
||||
let account: string;
|
||||
if (platform && phone) {
|
||||
account = platform + '-' + phone;
|
||||
} else if (req.body.account) {
|
||||
account = req.body.account;
|
||||
} else {
|
||||
res.status(400).json({ error: '请选择推广平台并填写手机号' }); return;
|
||||
}
|
||||
if (!account || !password) { res.status(400).json({ error: '账号和密码不能为空' }); return; }
|
||||
if (password.length < 6) { res.status(400).json({ error: '密码至少6位' }); return; }
|
||||
const result = registerUser(account, password);
|
||||
if (result.success) res.json({ success: true, message: result.message, userId: result.userId });
|
||||
else res.status(400).json({ error: result.message });
|
||||
});
|
||||
|
||||
router.post('/login', (req: Request, res: Response) => {
|
||||
const { account, password } = req.body;
|
||||
if (!account || !password) { res.status(400).json({ error: '账号和密码不能为空' }); return; }
|
||||
const result = loginUser(account, password);
|
||||
if (result.success) res.json({ success: true, token: result.token, message: result.message });
|
||||
else res.status(401).json({ error: result.message });
|
||||
});
|
||||
|
||||
// ── Protected ───────────────────────────────────────────────────
|
||||
|
||||
router.use(userAuthMiddleware);
|
||||
|
||||
router.get('/profile', (req: Request, res: Response) => {
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT id, account, role, notify_config, created_at FROM push_users WHERE id = ?').get(req.user!.userId) as any;
|
||||
if (!user) { res.status(404).json({ error: '用户不存在' }); return; }
|
||||
res.json({ id: user.id, account: user.account, role: user.role, notifyConfig: user.notify_config, createdAt: user.created_at });
|
||||
});
|
||||
|
||||
// ── 转存日志 ──────────────────────────────────────────────────
|
||||
|
||||
router.get('/save-records', (req: Request, res: Response) => {
|
||||
const db = getDb();
|
||||
const page = Math.max(1, parseInt(String(req.query.page)) || 1);
|
||||
const pageSize = Math.min(100, Math.max(1, parseInt(String(req.query.pageSize)) || 20));
|
||||
const offset = (page - 1) * pageSize;
|
||||
const total = (db.prepare('SELECT COUNT(*) as count FROM save_records WHERE promotion_account = ?').get(req.user!.account) as any).count;
|
||||
const records = db.prepare(
|
||||
'SELECT id, source_type, source_title, source_url, share_url, share_pwd, file_size, file_count, folder_count, status, error_message, folder_name, duration_ms, created_at FROM save_records WHERE promotion_account = ? ORDER BY created_at DESC LIMIT ? OFFSET ?'
|
||||
).all(req.user!.account, pageSize, offset);
|
||||
res.json({ total, page, pageSize, records });
|
||||
});
|
||||
|
||||
// ── 网盘管理 ──────────────────────────────────────────────────
|
||||
|
||||
router.get('/cloud-configs', (req: Request, res: Response) => {
|
||||
const db = getDb();
|
||||
const configs = db.prepare(
|
||||
'SELECT id, cloud_type, nickname, cloud_type_uid, cookie_uid, promotion_account, storage_used, storage_total, is_active, verification_status, consecutive_failures, last_used_at, total_saves, created_at FROM cloud_configs WHERE promotion_account = ? AND is_active = 1 ORDER BY created_at DESC'
|
||||
).all(req.user!.account);
|
||||
res.json(configs);
|
||||
});
|
||||
|
||||
// Cookie 验证(保存前)
|
||||
router.post('/cloud-configs/:cloudType/test', async (req: Request, res: Response) => {
|
||||
const { cookie } = req.body;
|
||||
if (!cookie) { res.status(400).json({ error: 'Cookie不能为空' }); return; }
|
||||
try {
|
||||
const result = await testCloudConnectionWithCookie(String(req.params.cloudType), cookie);
|
||||
res.json(result);
|
||||
} catch (e: any) { res.status(500).json({ error: e.message || '验证失败' }); }
|
||||
});
|
||||
|
||||
router.post('/cloud-configs', (req: Request, res: Response) => {
|
||||
const { cloud_type, cookie, nickname, promotion_account } = req.body;
|
||||
if (!cloud_type || !cookie) { res.status(400).json({ error: '网盘类型和Cookie不能为空' }); return; }
|
||||
try {
|
||||
const config = saveCloudConfig({ cloud_type, cookie, nickname: nickname || '', promotion_account: promotion_account || req.user!.account, is_active: 0 });
|
||||
const { cookie: _, ...safe } = config as any;
|
||||
res.json(safe);
|
||||
} catch (e: any) { res.status(500).json({ error: e.message || '保存失败' }); }
|
||||
});
|
||||
|
||||
router.put('/cloud-configs/:id', (req: Request, res: Response) => {
|
||||
const id = Number(req.params.id);
|
||||
const db = getDb();
|
||||
const existing = db.prepare('SELECT id FROM cloud_configs WHERE id = ? AND promotion_account = ?').get(id, req.user!.account) as any;
|
||||
if (!existing) { res.status(403).json({ error: '无权操作此网盘配置' }); return; }
|
||||
const { cloud_type, cookie, nickname, promotion_account } = req.body;
|
||||
try {
|
||||
const config = saveCloudConfig({ id, cloud_type: cloud_type || undefined, cookie: cookie || undefined, nickname, promotion_account: promotion_account || req.user!.account });
|
||||
const { cookie: _, ...safe } = config as any;
|
||||
res.json(safe);
|
||||
} catch (e: any) { res.status(500).json({ error: e.message || '保存失败' }); }
|
||||
});
|
||||
|
||||
router.delete('/cloud-configs/:id', (req: Request, res: Response) => {
|
||||
const id = Number(req.params.id);
|
||||
const db = getDb();
|
||||
const existing = db.prepare('SELECT id FROM cloud_configs WHERE id = ? AND promotion_account = ?').get(id, req.user!.account) as any;
|
||||
if (!existing) { res.status(403).json({ error: '无权操作此网盘配置' }); return; }
|
||||
deleteCloudConfig(id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
// ── 可用网盘类型(仅返回管理员已启用的) ──────────────────
|
||||
|
||||
router.get('/enabled-cloud-types', (_req: Request, res: Response) => {
|
||||
const allTypes = getAllCloudTypes();
|
||||
const enabled = allTypes.filter(ct => ct.enabled && ct.type !== 'others' && ct.type !== 'magnet' && ct.type !== 'ed2k');
|
||||
res.json(enabled);
|
||||
});
|
||||
|
||||
// ── 推送配置 ──────────────────────────────────────────────────
|
||||
|
||||
router.get('/notify-config', (req: Request, res: Response) => {
|
||||
const db = getDb();
|
||||
const user = db.prepare('SELECT notify_config FROM push_users WHERE id = ?').get(req.user!.userId) as any;
|
||||
res.json({ notifyConfig: user?.notify_config || '{}' });
|
||||
});
|
||||
|
||||
router.put('/notify-config', (req: Request, res: Response) => {
|
||||
const db = getDb();
|
||||
const { notifyConfig } = req.body;
|
||||
if (!notifyConfig) { res.status(400).json({ error: '推送配置不能为空' }); return; }
|
||||
db.prepare("UPDATE push_users SET notify_config = ?, updated_at = datetime('now','localtime') WHERE id = ?")
|
||||
.run(typeof notifyConfig === 'string' ? notifyConfig : JSON.stringify(notifyConfig), req.user!.userId);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user