diff --git a/src/index.ts b/src/index.ts index 8da3316..b86f71d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -58,6 +58,7 @@ async function main() { database: db, config, logger, + cookieStore, staticDir: STATIC_DIR, }); await webServer.start(); diff --git a/src/music/netease.ts b/src/music/netease.ts index 31b3a5a..bc64c16 100644 --- a/src/music/netease.ts +++ b/src/music/netease.ts @@ -188,13 +188,16 @@ export class NeteaseProvider implements MusicProvider { } async getQrCode(): Promise { - const keyRes = await this.api.get("/login/qr/key"); + const keyRes = await this.api.get("/login/qr/key", { + params: { timestamp: Date.now() }, + }); const key = keyRes.data?.data?.unikey ?? ""; const createRes = await this.api.get("/login/qr/create", { params: { key, qrimg: true }, }); return { qrUrl: createRes.data?.data?.qrurl ?? "", + qrImg: createRes.data?.data?.qrimg ?? "", key, }; } @@ -203,7 +206,7 @@ export class NeteaseProvider implements MusicProvider { key: string ): Promise<"waiting" | "scanned" | "confirmed" | "expired"> { const res = await this.api.get("/login/qr/check", { - params: { key }, + params: { key, timestamp: Date.now() }, }); const code = res.data?.code; switch (code) { diff --git a/src/music/provider.ts b/src/music/provider.ts index 8cad750..93248ca 100644 --- a/src/music/provider.ts +++ b/src/music/provider.ts @@ -43,6 +43,7 @@ export interface SearchResult { export interface QrCodeResult { qrUrl: string; + qrImg?: string; // base64 data URL of QR image key: string; } diff --git a/src/web/api/auth.ts b/src/web/api/auth.ts index 269aa80..6fd736e 100644 --- a/src/web/api/auth.ts +++ b/src/web/api/auth.ts @@ -1,11 +1,13 @@ import { Router } from "express"; import type { MusicProvider } from "../../music/provider.js"; +import type { CookieStore } from "../../music/auth.js"; import type { Logger } from "../../logger.js"; export function createAuthRouter( neteaseProvider: MusicProvider, qqProvider: MusicProvider, - logger: Logger + logger: Logger, + cookieStore?: CookieStore ): Router { const router = Router(); @@ -18,8 +20,10 @@ export function createAuthRouter( const platform = req.query.platform as string; const provider = getProvider(platform); const status = await provider.getAuthStatus(); + logger.debug({ platform, status }, "Auth status check"); res.json({ platform: provider.platform, ...status }); } catch (err) { + logger.error({ err }, "Auth status check failed"); res.status(500).json({ error: (err as Error).message }); } }); @@ -29,6 +33,7 @@ export function createAuthRouter( const { platform } = req.body; const provider = getProvider(platform); const qr = await provider.getQrCode(); + logger.info({ platform, key: qr.key }, "QR code generated"); res.json(qr); } catch (err) { logger.error({ err }, "QR code generation failed"); @@ -45,8 +50,21 @@ export function createAuthRouter( } const provider = getProvider(platform as string); const status = await provider.checkQrCodeStatus(key as string); + logger.info({ platform, status, key }, "QR status check"); + + // When confirmed, persist cookie + if (status === "confirmed") { + const cookie = provider.getCookie(); + const plat = (platform as string) === "qq" ? "qq" as const : "netease" as const; + if (cookie && cookieStore) { + cookieStore.save(plat, cookie); + logger.info({ platform: plat }, "Cookie persisted to disk"); + } + } + res.json({ status }); } catch (err) { + logger.error({ err }, "QR status check failed"); res.status(500).json({ error: (err as Error).message }); } }); @@ -83,6 +101,9 @@ export function createAuthRouter( return; } const success = await neteaseProvider.loginWithSms(phone, code); + if (success && cookieStore) { + cookieStore.save("netease", neteaseProvider.getCookie()); + } res.json({ success }); } catch (err) { res.status(500).json({ error: (err as Error).message }); @@ -97,6 +118,10 @@ export function createAuthRouter( } const provider = getProvider(platform); provider.setCookie(cookie); + const plat = platform === "qq" ? "qq" as const : "netease" as const; + if (cookieStore) { + cookieStore.save(plat, cookie); + } res.json({ success: true }); }); diff --git a/src/web/server.ts b/src/web/server.ts index ce3373f..7e0bd65 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -7,6 +7,7 @@ import type { MusicProvider } from "../music/provider.js"; import type { BotDatabase } from "../data/database.js"; import type { BotConfig } from "../data/config.js"; import type { Logger } from "../logger.js"; +import type { CookieStore } from "../music/auth.js"; import { createBotRouter } from "./api/bot.js"; import { createMusicRouter } from "./api/music.js"; import { createPlayerRouter } from "./api/player.js"; @@ -21,6 +22,7 @@ export interface WebServerOptions { database: BotDatabase; config: BotConfig; logger: Logger; + cookieStore?: CookieStore; staticDir?: string; } @@ -47,7 +49,7 @@ export function createWebServer(options: WebServerOptions): WebServer { app.use("/api/player", createPlayerRouter(options.botManager, logger)); app.use( "/api/auth", - createAuthRouter(options.neteaseProvider, options.qqProvider, logger) + createAuthRouter(options.neteaseProvider, options.qqProvider, logger, options.cookieStore) ); app.get("/api/health", (_req, res) => { diff --git a/web/src/views/Settings.vue b/web/src/views/Settings.vue index 45de7e0..f982484 100644 --- a/web/src/views/Settings.vue +++ b/web/src/views/Settings.vue @@ -277,18 +277,22 @@ async function startQrLogin(platform: string) { try { const res = await axios.post('/api/auth/qrcode', { platform }); - const { qrUrl, key } = res.data; + const { qrUrl, qrImg, key } = res.data; qr.key = key; - // Generate QR code image - qr.dataUrl = await QRCode.toDataURL(qrUrl, { - width: 200, - margin: 2, - color: { - dark: store.theme === 'dark' ? '#ffffff' : '#000000', - light: store.theme === 'dark' ? '#2a2a2a' : '#ffffff', - }, - }); + // Use server-generated QR image if available, otherwise generate client-side + if (qrImg) { + qr.dataUrl = qrImg; + } else { + qr.dataUrl = await QRCode.toDataURL(qrUrl, { + width: 200, + margin: 2, + color: { + dark: store.theme === 'dark' ? '#ffffff' : '#000000', + light: store.theme === 'dark' ? '#2a2a2a' : '#ffffff', + }, + }); + } qr.loading = false;