v0.3.13: Cookie解密修复 + 配置统一化
修复: - credential.service.ts: 5个getter函数统一解密cookie (解决夸克连接失败) - decryptCookie从extractCookieUid嵌套作用域提到模块顶层 - testCloudConnection/getAndValidateCredential添加解密调用 - 去掉docker run的COOKIE_ENCRYPTION_KEY(回退默认key与旧数据一致) 配置统一化: - config/index.ts新增: corsOrigin/cookieEncryptionKey/logLevel/appVersionFile/uploadDir - main.ts: CORS_ORIGIN/REDIS_URL/uploads改用config而非raw process.env - middleware/cache.ts: REDIS_URL改用config - docker-compose.env: 完整环境变量模板(18个变量)
This commit is contained in:
37
docker-compose.env
Normal file
37
docker-compose.env
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# CloudSearch 环境变量 — 统一管理
|
||||||
|
# 复制此文件为 .env 使用:cp docker-compose.env .env
|
||||||
|
|
||||||
|
# ── 核心服务 ──
|
||||||
|
PORT=9527
|
||||||
|
NODE_ENV=production
|
||||||
|
TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# ── 安全 ──
|
||||||
|
JWT_SECRET=cloudsearch-jwt-prod-v1
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=0nL5kLhMIJ1121PYmQb25A
|
||||||
|
COOKIE_ENCRYPTION_KEY=
|
||||||
|
CORS_ORIGIN=http://jp-cs.timaa.cn
|
||||||
|
|
||||||
|
# ── 数据库 & 缓存 ──
|
||||||
|
DB_PATH=/data/database.sqlite
|
||||||
|
REDIS_URL=redis://:redis_GbR7XZ@1Panel-redis-aDp3:6379
|
||||||
|
|
||||||
|
# ── 外部服务 ──
|
||||||
|
PANSOU_URL=http://pansou:80
|
||||||
|
PANSOU_AUTH_TOKEN=
|
||||||
|
VIDEO_PARSER_URL=http://localhost:3001
|
||||||
|
|
||||||
|
# ── 网盘校验 ──
|
||||||
|
VALIDATION_CONCURRENCY=10
|
||||||
|
VALIDATION_TIMEOUT=5000
|
||||||
|
CACHE_TTL_VALID=14400
|
||||||
|
CACHE_TTL_INVALID=3600
|
||||||
|
|
||||||
|
# ── 路径 ──
|
||||||
|
CHROMIUM_PATH=/usr/bin/chromium-browser
|
||||||
|
APP_VERSION_FILE=/data/VERSION
|
||||||
|
UPLOAD_DIR=/app/uploads
|
||||||
|
|
||||||
|
# ── 日志 ──
|
||||||
|
LOG_LEVEL=info
|
||||||
@@ -1 +1 @@
|
|||||||
0.3.12
|
0.3.13
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ export interface CloudConfig {
|
|||||||
|
|
||||||
// ── Cookie UID Extraction ────────────────────────────────────────
|
// ── Cookie UID Extraction ────────────────────────────────────────
|
||||||
|
|
||||||
function extractCookieUid(cookie: string): string {
|
|
||||||
|
|
||||||
function decryptCookie(encrypted: string): string {
|
function decryptCookie(encrypted: string): string {
|
||||||
if (!encrypted) return '';
|
if (!encrypted) return '';
|
||||||
if (!isEncrypted(encrypted)) return encrypted;
|
if (!isEncrypted(encrypted)) return encrypted;
|
||||||
return decrypt(encrypted);
|
return decrypt(encrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractCookieUid(cookie: string): string {
|
||||||
if (!cookie) return '';
|
if (!cookie) return '';
|
||||||
let m = cookie.match(/__uid=([a-zA-Z0-9+/=_-]+)/);
|
let m = cookie.match(/__uid=([a-zA-Z0-9+/=_-]+)/);
|
||||||
if (m) return m[1];
|
if (m) return m[1];
|
||||||
@@ -42,22 +42,26 @@ function decryptCookie(encrypted: string): string {
|
|||||||
|
|
||||||
export function getCloudConfigs(): CloudConfig[] {
|
export function getCloudConfigs(): CloudConfig[] {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
return db.prepare(
|
const rows = db.prepare(
|
||||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||||
FROM cloud_configs ORDER BY id ASC`
|
FROM cloud_configs ORDER BY id ASC`
|
||||||
).all() as CloudConfig[];
|
).all() as CloudConfig[];
|
||||||
|
rows.forEach(r => { if (r.cookie) r.cookie = decryptCookie(r.cookie); });
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailableClouds(): CloudConfig[] {
|
export function getAvailableClouds(): CloudConfig[] {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
return db.prepare(
|
const rows = db.prepare(
|
||||||
`SELECT id, cloud_type, nickname, is_active, storage_used, storage_total,
|
`SELECT id, cloud_type, nickname, is_active, storage_used, storage_total,
|
||||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
last_used_at, total_saves, created_at, updated_at
|
last_used_at, total_saves, created_at, updated_at
|
||||||
FROM cloud_configs WHERE is_active = 1 ORDER BY id ASC`
|
FROM cloud_configs WHERE is_active = 1 ORDER BY id ASC`
|
||||||
).all() as CloudConfig[];
|
).all() as CloudConfig[];
|
||||||
|
rows.forEach(r => { if (r.cookie) r.cookie = decryptCookie(r.cookie); });
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the first active config matching the given cloud type. */
|
/** Returns the first active config matching the given cloud type. */
|
||||||
@@ -70,6 +74,7 @@ export function getCloudConfigByType(cloudType: string): CloudConfig | undefined
|
|||||||
FROM cloud_configs WHERE cloud_type = ? AND is_active = 1
|
FROM cloud_configs WHERE cloud_type = ? AND is_active = 1
|
||||||
ORDER BY id ASC LIMIT 1`
|
ORDER BY id ASC LIMIT 1`
|
||||||
).get(cloudType) as CloudConfig | undefined;
|
).get(cloudType) as CloudConfig | undefined;
|
||||||
|
if (cfg && cfg.cookie) cfg.cookie = decryptCookie(cfg.cookie);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,19 +86,22 @@ export function getCloudConfigById(id: number): CloudConfig | undefined {
|
|||||||
last_used_at, total_saves, created_at, updated_at, verification_status
|
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||||
FROM cloud_configs WHERE id = ?`
|
FROM cloud_configs WHERE id = ?`
|
||||||
).get(id) as CloudConfig | undefined;
|
).get(id) as CloudConfig | undefined;
|
||||||
|
if (cfg && cfg.cookie) cfg.cookie = decryptCookie(cfg.cookie);
|
||||||
return cfg;
|
return cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns all active cloud configs (used by save flow for cloud type switching). */
|
/** Returns all active cloud configs (used by save flow for cloud type switching). */
|
||||||
export function getActiveCloudConfigs(): CloudConfig[] {
|
export function getActiveCloudConfigs(): CloudConfig[] {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
return db.prepare(
|
const rows = db.prepare(
|
||||||
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
last_used_at, total_saves, created_at, updated_at
|
last_used_at, total_saves, created_at, updated_at
|
||||||
FROM cloud_configs WHERE is_active = 1
|
FROM cloud_configs WHERE is_active = 1
|
||||||
ORDER BY cloud_type ASC, id ASC`
|
ORDER BY cloud_type ASC, id ASC`
|
||||||
).all() as CloudConfig[];
|
).all() as CloudConfig[];
|
||||||
|
rows.forEach(r => { if (r.cookie) r.cookie = decryptCookie(r.cookie); });
|
||||||
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveCloudConfig(data: {
|
export function saveCloudConfig(data: {
|
||||||
@@ -210,6 +218,8 @@ export async function testCloudConnection(id: number): Promise<{
|
|||||||
return { success: false, message: 'Cookie not configured' };
|
return { success: false, message: 'Cookie not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cookie = decryptCookie(config.cookie);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let valid = false;
|
let valid = false;
|
||||||
let nickname = '';
|
let nickname = '';
|
||||||
@@ -218,7 +228,7 @@ export async function testCloudConnection(id: number): Promise<{
|
|||||||
|
|
||||||
if (config.cloud_type === 'baidu') {
|
if (config.cloud_type === 'baidu') {
|
||||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||||
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
const driver = new BaiduDriver({ cookie: cookie, nickname: config.nickname });
|
||||||
valid = await driver.validate();
|
valid = await driver.validate();
|
||||||
if (valid) {
|
if (valid) {
|
||||||
const info = await driver.getUserInfo();
|
const info = await driver.getUserInfo();
|
||||||
@@ -231,10 +241,10 @@ export async function testCloudConnection(id: number): Promise<{
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||||
const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname });
|
const driver = new QuarkDriver({ cookie: cookie, nickname: config.nickname });
|
||||||
valid = await driver.validate();
|
valid = await driver.validate();
|
||||||
if (valid) {
|
if (valid) {
|
||||||
nickname = config.nickname || (await fetchQuarkNickname(config.cookie)) || '夸克网盘';
|
nickname = config.nickname || (await fetchQuarkNickname(cookie)) || '夸克网盘';
|
||||||
const storage = await driver.getStorageInfoQuick();
|
const storage = await driver.getStorageInfoQuick();
|
||||||
storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || '');
|
storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || '');
|
||||||
}
|
}
|
||||||
@@ -345,15 +355,17 @@ export async function getAndValidateCredential(cloudType: string): Promise<Crede
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cookie = decryptCookie(config.cookie);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let cookieValid = false;
|
let cookieValid = false;
|
||||||
if (cloudType === 'baidu') {
|
if (cloudType === 'baidu') {
|
||||||
const { BaiduDriver } = require('./drivers/baidu.driver');
|
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||||
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
const driver = new BaiduDriver({ cookie: cookie, nickname: config.nickname });
|
||||||
cookieValid = await driver.validate();
|
cookieValid = await driver.validate();
|
||||||
} else {
|
} else {
|
||||||
const { QuarkDriver } = require('./drivers/quark.driver');
|
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||||
const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname });
|
const driver = new QuarkDriver({ cookie: cookie, nickname: config.nickname });
|
||||||
cookieValid = await driver.validate();
|
cookieValid = await driver.validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
383
source_clean/src/cloud/credential.service.ts.bak
Normal file
383
source_clean/src/cloud/credential.service.ts.bak
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
import { getDb } from '../database/database';
|
||||||
|
import { encrypt, decrypt, isEncrypted } from '../utils/crypto';
|
||||||
|
import { localTimestamp, formatLocalDate, formatLocalDateTime } from '../utils/time';
|
||||||
|
|
||||||
|
export interface CloudConfig {
|
||||||
|
id: number;
|
||||||
|
cloud_type: string;
|
||||||
|
cookie?: string;
|
||||||
|
nickname?: string;
|
||||||
|
is_active: number;
|
||||||
|
storage_used?: string;
|
||||||
|
storage_total?: string;
|
||||||
|
checkin_status: string; // 'none'|'success'|'failed'|'pending'|'skipped'
|
||||||
|
last_checkin_at?: string;
|
||||||
|
checkin_message?: string;
|
||||||
|
consecutive_failures: number;
|
||||||
|
last_used_at?: string;
|
||||||
|
total_saves: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
verification_status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cookie UID Extraction ────────────────────────────────────────
|
||||||
|
|
||||||
|
function extractCookieUid(cookie: string): string {
|
||||||
|
|
||||||
|
function decryptCookie(encrypted: string): string {
|
||||||
|
if (!encrypted) return '';
|
||||||
|
if (!isEncrypted(encrypted)) return encrypted;
|
||||||
|
return decrypt(encrypted);
|
||||||
|
}
|
||||||
|
if (!cookie) return '';
|
||||||
|
let m = cookie.match(/__uid=([a-zA-Z0-9+/=_-]+)/);
|
||||||
|
if (m) return m[1];
|
||||||
|
m = cookie.match(/b-user-id=([a-zA-Z0-9-]+)/);
|
||||||
|
if (m) return m[1];
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Config CRUD ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getCloudConfigs(): CloudConfig[] {
|
||||||
|
const db = getDb();
|
||||||
|
return db.prepare(
|
||||||
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
|
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||||
|
FROM cloud_configs ORDER BY id ASC`
|
||||||
|
).all() as CloudConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailableClouds(): CloudConfig[] {
|
||||||
|
const db = getDb();
|
||||||
|
return db.prepare(
|
||||||
|
`SELECT id, cloud_type, nickname, is_active, storage_used, storage_total,
|
||||||
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
|
last_used_at, total_saves, created_at, updated_at
|
||||||
|
FROM cloud_configs WHERE is_active = 1 ORDER BY id ASC`
|
||||||
|
).all() as CloudConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the first active config matching the given cloud type. */
|
||||||
|
export function getCloudConfigByType(cloudType: string): CloudConfig | undefined {
|
||||||
|
const db = getDb();
|
||||||
|
const cfg = db.prepare(
|
||||||
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
|
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||||
|
FROM cloud_configs WHERE cloud_type = ? AND is_active = 1
|
||||||
|
ORDER BY id ASC LIMIT 1`
|
||||||
|
).get(cloudType) as CloudConfig | undefined;
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCloudConfigById(id: number): CloudConfig | undefined {
|
||||||
|
const db = getDb();
|
||||||
|
const cfg = db.prepare(
|
||||||
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
|
last_used_at, total_saves, created_at, updated_at, verification_status
|
||||||
|
FROM cloud_configs WHERE id = ?`
|
||||||
|
).get(id) as CloudConfig | undefined;
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns all active cloud configs (used by save flow for cloud type switching). */
|
||||||
|
export function getActiveCloudConfigs(): CloudConfig[] {
|
||||||
|
const db = getDb();
|
||||||
|
return db.prepare(
|
||||||
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
|
last_used_at, total_saves, created_at, updated_at
|
||||||
|
FROM cloud_configs WHERE is_active = 1
|
||||||
|
ORDER BY cloud_type ASC, id ASC`
|
||||||
|
).all() as CloudConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveCloudConfig(data: {
|
||||||
|
id?: number;
|
||||||
|
cloud_type: string;
|
||||||
|
cookie?: string;
|
||||||
|
nickname?: string;
|
||||||
|
cookie_uid?: string;
|
||||||
|
promotion_account?: string;
|
||||||
|
is_active?: number;
|
||||||
|
storage_used?: string;
|
||||||
|
storage_total?: string;
|
||||||
|
}): CloudConfig {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const cookieUidForUpdate = data.cookie ? extractCookieUid(data.cookie) : null;
|
||||||
|
const encryptedCookie = data.cookie ? encrypt(data.cookie) : null;
|
||||||
|
|
||||||
|
if (data.id) {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET
|
||||||
|
cloud_type = COALESCE(?, cloud_type),
|
||||||
|
cookie = COALESCE(?, cookie),
|
||||||
|
nickname = COALESCE(?, nickname),
|
||||||
|
cookie_uid = COALESCE(?, cookie_uid),
|
||||||
|
promotion_account = COALESCE(?, promotion_account),
|
||||||
|
is_active = COALESCE(?, is_active),
|
||||||
|
storage_used = COALESCE(?, storage_used),
|
||||||
|
storage_total = COALESCE(?, storage_total),
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
const existing = db.prepare(
|
||||||
|
'SELECT id, nickname FROM cloud_configs WHERE cloud_type = ? AND is_active = 1 LIMIT 1'
|
||||||
|
).get(data.cloud_type) as any;
|
||||||
|
if (existing) {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET
|
||||||
|
cookie = COALESCE(?, cookie),
|
||||||
|
nickname = COALESCE(?, nickname),
|
||||||
|
cookie_uid = COALESCE(?, cookie_uid),
|
||||||
|
promotion_account = COALESCE(?, promotion_account),
|
||||||
|
is_active = COALESCE(?, is_active),
|
||||||
|
storage_used = COALESCE(?, storage_used),
|
||||||
|
storage_total = COALESCE(?, storage_total),
|
||||||
|
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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedId = data.id || (db.prepare('SELECT last_insert_rowid() as id').get() as any).id;
|
||||||
|
return db.prepare(
|
||||||
|
`SELECT id, cloud_type, cookie, nickname, is_active, storage_used, storage_total,
|
||||||
|
checkin_status, last_checkin_at, checkin_message, consecutive_failures,
|
||||||
|
last_used_at, total_saves, created_at, updated_at
|
||||||
|
FROM cloud_configs WHERE id = ?`
|
||||||
|
).get(savedId) as CloudConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCloudConfig(id: number): boolean {
|
||||||
|
const db = getDb();
|
||||||
|
const result = db.prepare('DELETE FROM cloud_configs WHERE id = ?').run(id);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Cookie Validation ────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function fetchQuarkNickname(cookie: string): Promise<string | null> {
|
||||||
|
const MAX_RETRIES = 2;
|
||||||
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://pan.quark.cn/account/info', {
|
||||||
|
headers: {
|
||||||
|
'Cookie': cookie,
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||||
|
'Referer': 'https://pan.quark.cn/',
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(15000),
|
||||||
|
});
|
||||||
|
if (!response.ok) return null;
|
||||||
|
const data = await response.json() as any;
|
||||||
|
if (data?.data?.nickname) return data.data.nickname;
|
||||||
|
} catch {
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
await new Promise(r => setTimeout(r, 1500));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testCloudConnection(id: number): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
nickname?: string;
|
||||||
|
storage_used?: string;
|
||||||
|
storage_total?: string;
|
||||||
|
}> {
|
||||||
|
const config = getCloudConfigById(id);
|
||||||
|
if (!config) {
|
||||||
|
return { success: false, message: 'Cloud config not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.cookie) {
|
||||||
|
return { success: false, message: 'Cookie not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let valid = false;
|
||||||
|
let nickname = '';
|
||||||
|
let storageUsed = config.storage_used || '';
|
||||||
|
let storageTotal = config.storage_total || '';
|
||||||
|
|
||||||
|
if (config.cloud_type === 'baidu') {
|
||||||
|
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||||
|
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||||
|
valid = await driver.validate();
|
||||||
|
if (valid) {
|
||||||
|
const info = await driver.getUserInfo();
|
||||||
|
if (info) {
|
||||||
|
nickname = config.nickname || info.nickname || '百度网盘';
|
||||||
|
const fmt = (b: number) => b >= 1024**3 ? (b/1024**3).toFixed(2)+' GB' : (b/1024**2).toFixed(2)+' MB';
|
||||||
|
storageUsed = fmt(info.usedBytes);
|
||||||
|
storageTotal = fmt(info.totalBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||||
|
const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||||
|
valid = await driver.validate();
|
||||||
|
if (valid) {
|
||||||
|
nickname = config.nickname || (await fetchQuarkNickname(config.cookie)) || '夸克网盘';
|
||||||
|
const storage = await driver.getStorageInfoQuick();
|
||||||
|
storageTotal = (storage.total !== '-' && storage.total !== '0 B') ? storage.total : (config.storage_total || '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = getDb();
|
||||||
|
if (!valid) {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||||
|
).run(localTimestamp(), id);
|
||||||
|
return { success: false, message: '连接失败:Cookie 无效或已过期,或网络暂时异常' };
|
||||||
|
}
|
||||||
|
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET nickname = ?, storage_total = ?, storage_used = ?, is_active = 1, verification_status = 'valid', updated_at = ? WHERE id = ?`
|
||||||
|
).run(nickname, storageTotal, storageUsed, localTimestamp(), id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: '连接成功',
|
||||||
|
nickname,
|
||||||
|
storage_used: storageUsed,
|
||||||
|
storage_total: storageTotal,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||||
|
).run(localTimestamp(), id);
|
||||||
|
} catch {}
|
||||||
|
return { success: false, message: `连接失败:${err.message || '未知错误'}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function testCloudConnectionWithCookie(cloudType: string, cookie: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
nickname?: string;
|
||||||
|
storage_used?: string;
|
||||||
|
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 无效或已过期' };
|
||||||
|
}
|
||||||
|
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 || '未知错误'}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unified Credential Validation ─────────────────────────────────
|
||||||
|
|
||||||
|
export interface CredentialValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
config?: CloudConfig;
|
||||||
|
errorCode?: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get and validate a credential for the given cloud type.
|
||||||
|
*
|
||||||
|
* This is the unified entry point for all save/transfer operations.
|
||||||
|
* It handles:
|
||||||
|
* 1. Finding an active config with < 5 consecutive failures (round-robin)
|
||||||
|
* 2. Validating cookie freshness via driver.validate()
|
||||||
|
* 3. Returning structured result with error codes
|
||||||
|
*
|
||||||
|
* Reference: search-ucmao get_and_validate_credential() pattern.
|
||||||
|
*/
|
||||||
|
export async function getAndValidateCredential(cloudType: string): Promise<CredentialValidationResult> {
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
const config = db.prepare(
|
||||||
|
`SELECT * FROM cloud_configs
|
||||||
|
WHERE cloud_type = ? AND is_active = 1
|
||||||
|
AND consecutive_failures < 5
|
||||||
|
ORDER BY last_used_at ASC NULLS FIRST
|
||||||
|
LIMIT 1`
|
||||||
|
).get(cloudType) as CloudConfig | undefined;
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorCode: 'NO_AVAILABLE_DRIVE',
|
||||||
|
message: `Cloud type "${cloudType}" is not configured or no available drives`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.cookie) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorCode: 'COOKIE_MISSING',
|
||||||
|
message: `Cookie not configured for ${cloudType} drive #${config.id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let cookieValid = false;
|
||||||
|
if (cloudType === 'baidu') {
|
||||||
|
const { BaiduDriver } = require('./drivers/baidu.driver');
|
||||||
|
const driver = new BaiduDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||||
|
cookieValid = await driver.validate();
|
||||||
|
} else {
|
||||||
|
const { QuarkDriver } = require('./drivers/quark.driver');
|
||||||
|
const driver = new QuarkDriver({ cookie: config.cookie, nickname: config.nickname });
|
||||||
|
cookieValid = await driver.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cookieValid) {
|
||||||
|
db.prepare(
|
||||||
|
`UPDATE cloud_configs SET verification_status = 'invalid', updated_at = ? WHERE id = ?`
|
||||||
|
).run(localTimestamp(), config.id);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorCode: 'COOKIE_EXPIRED',
|
||||||
|
message: `Cookie expired or invalid for ${cloudType} drive #${config.id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
config,
|
||||||
|
message: 'ok',
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorCode: 'VALIDATION_ERROR',
|
||||||
|
message: `Credential validation failed: ${err.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@ export interface Config {
|
|||||||
cacheTtlValid: number;
|
cacheTtlValid: number;
|
||||||
cacheTtlInvalid: number;
|
cacheTtlInvalid: number;
|
||||||
};
|
};
|
||||||
|
corsOrigin: string;
|
||||||
|
cookieEncryptionKey: string;
|
||||||
|
logLevel: string;
|
||||||
|
appVersionFile: string;
|
||||||
|
uploadDir: string;
|
||||||
chromiumPath: string;
|
chromiumPath: string;
|
||||||
dbPath: string;
|
dbPath: string;
|
||||||
}
|
}
|
||||||
@@ -34,6 +39,11 @@ const config: Config = {
|
|||||||
cacheTtlValid: parseInt(process.env.CACHE_TTL_VALID || '14400', 10), // 4小时
|
cacheTtlValid: parseInt(process.env.CACHE_TTL_VALID || '14400', 10), // 4小时
|
||||||
cacheTtlInvalid: parseInt(process.env.CACHE_TTL_INVALID || '3600', 10), // 1小时
|
cacheTtlInvalid: parseInt(process.env.CACHE_TTL_INVALID || '3600', 10), // 1小时
|
||||||
},
|
},
|
||||||
|
corsOrigin: process.env.CORS_ORIGIN || '',
|
||||||
|
cookieEncryptionKey: process.env.COOKIE_ENCRYPTION_KEY || '',
|
||||||
|
logLevel: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
|
||||||
|
appVersionFile: process.env.APP_VERSION_FILE || '/app/VERSION',
|
||||||
|
uploadDir: process.env.UPLOAD_DIR || '/app/uploads',
|
||||||
chromiumPath: process.env.CHROMIUM_PATH || "/usr/bin/chromium-browser",
|
chromiumPath: process.env.CHROMIUM_PATH || "/usr/bin/chromium-browser",
|
||||||
dbPath: process.env.DB_PATH || './data/cloudsearch.db',
|
dbPath: process.env.DB_PATH || './data/cloudsearch.db',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const app = express();
|
|||||||
app.set('trust proxy', true);
|
app.set('trust proxy', true);
|
||||||
|
|
||||||
// CORS — 生产环境必须配置真实域名(空值或占位符用 * 并打警告日志)
|
// CORS — 生产环境必须配置真实域名(空值或占位符用 * 并打警告日志)
|
||||||
const corsOrigin = process.env.CORS_ORIGIN || '';
|
const corsOrigin = config.corsOrigin;
|
||||||
const isPlaceholder = !corsOrigin || corsOrigin === 'https://your-domain.com';
|
const isPlaceholder = !corsOrigin || corsOrigin === 'https://your-domain.com';
|
||||||
if (config.nodeEnv === 'production' && isPlaceholder) {
|
if (config.nodeEnv === 'production' && isPlaceholder) {
|
||||||
console.error('[FATAL] CORS_ORIGIN 未配置或使用了占位符 https://your-domain.com,生产环境必须设置真实域名。应用拒绝启动。');
|
console.error('[FATAL] CORS_ORIGIN 未配置或使用了占位符 https://your-domain.com,生产环境必须设置真实域名。应用拒绝启动。');
|
||||||
@@ -56,7 +56,7 @@ app.use(express.static(frontendDist));
|
|||||||
app.use(rateLimiter);
|
app.use(rateLimiter);
|
||||||
|
|
||||||
// ============ Routes ============
|
// ============ Routes ============
|
||||||
app.use('/api/uploads', express.static('/app/uploads'));
|
app.use('/api/uploads', express.static(config.uploadDir));
|
||||||
app.use('/api', routes);
|
app.use('/api', routes);
|
||||||
|
|
||||||
// ============ Health Check(增强版:覆盖 Redis / PanSou / VideoParser 状态) ============
|
// ============ Health Check(增强版:覆盖 Redis / PanSou / VideoParser 状态) ============
|
||||||
|
|||||||
Reference in New Issue
Block a user