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,
config,
logger,
cookieStore,
staticDir: STATIC_DIR,
});
await webServer.start();
+5 -2
View File
@@ -188,13 +188,16 @@ export class NeteaseProvider implements MusicProvider {
}
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 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) {
+1
View File
@@ -43,6 +43,7 @@ export interface SearchResult {
export interface QrCodeResult {
qrUrl: string;
qrImg?: string; // base64 data URL of QR image
key: string;
}
+26 -1
View File
@@ -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 });
});
+3 -1
View File
@@ -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) => {
+6 -2
View File
@@ -277,10 +277,13 @@ 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
// 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,
@@ -289,6 +292,7 @@ async function startQrLogin(platform: string) {
light: store.theme === 'dark' ? '#2a2a2a' : '#ffffff',
},
});
}
qr.loading = false;