From 30554fcd77d1b66900fd8e08d9fd6086042b32db Mon Sep 17 00:00:00 2001 From: mtvpls Date: Sat, 30 May 2026 21:39:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E7=8A=B6=E6=80=81=EF=BC=8C=E4=BB=A5=E9=98=B2=E6=AD=A2?= =?UTF-8?q?=E5=A4=9A=E8=8A=82=E7=82=B9=E4=B8=8D=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/auth/qr/cancel/route.ts | 9 ++- src/app/api/auth/qr/confirm/route.ts | 5 +- src/app/api/auth/qr/create/route.ts | 2 +- src/app/api/auth/qr/status/route.ts | 5 +- src/lib/qr-login/store.ts | 86 +++++++++++++++++++++++++--- 5 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/app/api/auth/qr/cancel/route.ts b/src/app/api/auth/qr/cancel/route.ts index 8403b6a..271708a 100644 --- a/src/app/api/auth/qr/cancel/route.ts +++ b/src/app/api/auth/qr/cancel/route.ts @@ -1,12 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getQrLoginSession } from '@/lib/qr-login/store'; +import { getQrLoginSession, saveQrLoginSession } from '@/lib/qr-login/store'; export const runtime = 'nodejs'; export async function POST(request: NextRequest) { const { token } = await request.json(); - const session = getQrLoginSession(token); - if (session) session.status = 'cancelled'; + const session = await getQrLoginSession(token); + if (session) { + session.status = 'cancelled'; + await saveQrLoginSession(session); + } return NextResponse.json({ ok: true }); } diff --git a/src/app/api/auth/qr/confirm/route.ts b/src/app/api/auth/qr/confirm/route.ts index 45a5ab7..3055fa9 100644 --- a/src/app/api/auth/qr/confirm/route.ts +++ b/src/app/api/auth/qr/confirm/route.ts @@ -1,13 +1,13 @@ import { NextRequest, NextResponse } from 'next/server'; import { getAuthInfoFromCookie } from '@/lib/auth'; -import { getQrLoginSession } from '@/lib/qr-login/store'; +import { getQrLoginSession, saveQrLoginSession } from '@/lib/qr-login/store'; export const runtime = 'nodejs'; export async function POST(request: NextRequest) { const { token } = await request.json(); - const session = getQrLoginSession(token); + const session = await getQrLoginSession(token); if (!session || session.status === 'expired') return NextResponse.json({ error: '二维码已过期' }, { status: 410 }); if (session.status === 'cancelled' || session.status === 'used') return NextResponse.json({ error: '二维码不可用' }, { status: 400 }); @@ -18,5 +18,6 @@ export async function POST(request: NextRequest) { session.status = 'confirmed'; session.authToken = authCookie; session.userAgent = request.headers.get('user-agent') || ''; + await saveQrLoginSession(session); return NextResponse.json({ ok: true, status: 'confirmed' }); } diff --git a/src/app/api/auth/qr/create/route.ts b/src/app/api/auth/qr/create/route.ts index 8a68be3..97222e3 100644 --- a/src/app/api/auth/qr/create/route.ts +++ b/src/app/api/auth/qr/create/route.ts @@ -5,7 +5,7 @@ import { createQrLoginSession } from '@/lib/qr-login/store'; export const runtime = 'nodejs'; export async function POST(request: NextRequest) { - const session = createQrLoginSession(); + const session = await createQrLoginSession(); const origin = new URL(request.url).origin; const qrUrl = `${origin}/qr-login?token=${encodeURIComponent(session.token)}`; return NextResponse.json({ token: session.token, qrUrl, expiresAt: session.expiresAt, ttl: 120 }); diff --git a/src/app/api/auth/qr/status/route.ts b/src/app/api/auth/qr/status/route.ts index 9e416b8..b1d3c9e 100644 --- a/src/app/api/auth/qr/status/route.ts +++ b/src/app/api/auth/qr/status/route.ts @@ -1,16 +1,17 @@ import { NextRequest, NextResponse } from 'next/server'; -import { getQrLoginSession } from '@/lib/qr-login/store'; +import { getQrLoginSession, saveQrLoginSession } from '@/lib/qr-login/store'; export const runtime = 'nodejs'; export async function GET(request: NextRequest) { const token = new URL(request.url).searchParams.get('token'); - const session = getQrLoginSession(token); + const session = await getQrLoginSession(token); if (!session) return NextResponse.json({ status: 'expired' }); if (session.status === 'confirmed' && session.authToken) { session.status = 'used'; + await saveQrLoginSession(session); const response = NextResponse.json({ status: 'confirmed' }); const expires = new Date(); expires.setDate(expires.getDate() + 60); diff --git a/src/lib/qr-login/store.ts b/src/lib/qr-login/store.ts index 99ce2e5..5ab5a32 100644 --- a/src/lib/qr-login/store.ts +++ b/src/lib/qr-login/store.ts @@ -17,8 +17,43 @@ const g = globalThis as GlobalWithQr; export const qrLoginStore = g.__moonTvQrLoginStore || new Map(); g.__moonTvQrLoginStore = qrLoginStore; -export function createQrLoginSession(ttlMs = 120_000) { - cleanupQrLoginSessions(); +const QR_LOGIN_HASH_KEY = 'qr_login_sessions'; + +let getStorage: (() => any) | null = null; + +async function loadStorage() { + try { + if (!getStorage) { + const db = await import('@/lib/db'); + getStorage = db.getStorage; + } + const storage = getStorage(); + return storage && typeof (storage as any).adapter?.hGet === 'function' ? storage : null; + } catch { + return null; + } +} + +async function persistQrLoginSession(session: QrLoginSession) { + qrLoginStore.set(session.token, session); + + const storage = await loadStorage(); + if (!storage || typeof (storage as any).adapter?.hSet !== 'function') return; + + await (storage as any).adapter.hSet(QR_LOGIN_HASH_KEY, session.token, JSON.stringify(session)); +} + +async function deletePersistedQrLoginSession(token: string) { + qrLoginStore.delete(token); + + const storage = await loadStorage(); + if (!storage || typeof (storage as any).adapter?.hDel !== 'function') return; + + await (storage as any).adapter.hDel(QR_LOGIN_HASH_KEY, token); +} + +export async function createQrLoginSession(ttlMs = 120_000) { + await cleanupQrLoginSessions(); const token = crypto.randomBytes(24).toString('base64url'); const now = Date.now(); const session: QrLoginSession = { @@ -27,24 +62,61 @@ export function createQrLoginSession(ttlMs = 120_000) { createdAt: now, expiresAt: now + ttlMs, }; - qrLoginStore.set(token, session); + await persistQrLoginSession(session); return session; } -export function getQrLoginSession(token?: string | null) { +export async function getQrLoginSession(token?: string | null) { if (!token) return null; - const session = qrLoginStore.get(token) || null; + let session = qrLoginStore.get(token) || null; + + if (!session) { + const storage = await loadStorage(); + if (storage && typeof (storage as any).adapter?.hGet === 'function') { + const raw = await (storage as any).adapter.hGet(QR_LOGIN_HASH_KEY, token); + if (raw) { + try { + session = JSON.parse(raw) as QrLoginSession; + qrLoginStore.set(token, session); + } catch { + await deletePersistedQrLoginSession(token); + return null; + } + } + } + } + if (session && session.expiresAt <= Date.now() && session.status !== 'confirmed' && session.status !== 'used') { session.status = 'expired'; + await persistQrLoginSession(session); } return session; } -export function cleanupQrLoginSessions() { +export async function saveQrLoginSession(session: QrLoginSession) { + await persistQrLoginSession(session); +} + +export async function cleanupQrLoginSessions() { const now = Date.now(); for (const [token, session] of Array.from(qrLoginStore.entries())) { if (session.expiresAt + 300_000 < now || session.status === 'used') { - qrLoginStore.delete(token); + await deletePersistedQrLoginSession(token); + } + } + + const storage = await loadStorage(); + if (!storage || typeof (storage as any).adapter?.hGetAll !== 'function') return; + + const sessions = await (storage as any).adapter.hGetAll(QR_LOGIN_HASH_KEY); + for (const [token, raw] of Object.entries(sessions)) { + try { + const session = JSON.parse(raw as string) as QrLoginSession; + if (session.expiresAt + 300_000 < now || session.status === 'used') { + await deletePersistedQrLoginSession(token); + } + } catch { + await deletePersistedQrLoginSession(token); } } }