fix: QR login now persists cookies, shows server-generated QR image, passes timestamp

- Auth router accepts CookieStore and persists cookies on login
- NeteaseProvider passes timestamp to prevent cached QR responses
- QR image from server used directly (qrimg field)
- Cookie saved to disk on confirmed QR scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.6 committed 2026-03-30 01:47:25 +08:00
1 parent f77a6f0fd7
commit 9755ba2909
6 files changed
+42 -6

No files matched your search

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