持久化二维码状态,以防止多节点不同步
This commit is contained in:
@@ -1,12 +1,15 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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 const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const { token } = await request.json();
|
const { token } = await request.json();
|
||||||
const session = getQrLoginSession(token);
|
const session = await getQrLoginSession(token);
|
||||||
if (session) session.status = 'cancelled';
|
if (session) {
|
||||||
|
session.status = 'cancelled';
|
||||||
|
await saveQrLoginSession(session);
|
||||||
|
}
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
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 const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const { token } = await request.json();
|
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 || session.status === 'expired') return NextResponse.json({ error: '二维码已过期' }, { status: 410 });
|
||||||
if (session.status === 'cancelled' || session.status === 'used') return NextResponse.json({ error: '二维码不可用' }, { status: 400 });
|
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.status = 'confirmed';
|
||||||
session.authToken = authCookie;
|
session.authToken = authCookie;
|
||||||
session.userAgent = request.headers.get('user-agent') || '';
|
session.userAgent = request.headers.get('user-agent') || '';
|
||||||
|
await saveQrLoginSession(session);
|
||||||
return NextResponse.json({ ok: true, status: 'confirmed' });
|
return NextResponse.json({ ok: true, status: 'confirmed' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createQrLoginSession } from '@/lib/qr-login/store';
|
|||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const session = createQrLoginSession();
|
const session = await createQrLoginSession();
|
||||||
const origin = new URL(request.url).origin;
|
const origin = new URL(request.url).origin;
|
||||||
const qrUrl = `${origin}/qr-login?token=${encodeURIComponent(session.token)}`;
|
const qrUrl = `${origin}/qr-login?token=${encodeURIComponent(session.token)}`;
|
||||||
return NextResponse.json({ token: session.token, qrUrl, expiresAt: session.expiresAt, ttl: 120 });
|
return NextResponse.json({ token: session.token, qrUrl, expiresAt: session.expiresAt, ttl: 120 });
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
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 const runtime = 'nodejs';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const token = new URL(request.url).searchParams.get('token');
|
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) return NextResponse.json({ status: 'expired' });
|
||||||
|
|
||||||
if (session.status === 'confirmed' && session.authToken) {
|
if (session.status === 'confirmed' && session.authToken) {
|
||||||
session.status = 'used';
|
session.status = 'used';
|
||||||
|
await saveQrLoginSession(session);
|
||||||
const response = NextResponse.json({ status: 'confirmed' });
|
const response = NextResponse.json({ status: 'confirmed' });
|
||||||
const expires = new Date();
|
const expires = new Date();
|
||||||
expires.setDate(expires.getDate() + 60);
|
expires.setDate(expires.getDate() + 60);
|
||||||
|
|||||||
@@ -17,8 +17,43 @@ const g = globalThis as GlobalWithQr;
|
|||||||
export const qrLoginStore = g.__moonTvQrLoginStore || new Map<string, QrLoginSession>();
|
export const qrLoginStore = g.__moonTvQrLoginStore || new Map<string, QrLoginSession>();
|
||||||
g.__moonTvQrLoginStore = qrLoginStore;
|
g.__moonTvQrLoginStore = qrLoginStore;
|
||||||
|
|
||||||
export function createQrLoginSession(ttlMs = 120_000) {
|
const QR_LOGIN_HASH_KEY = 'qr_login_sessions';
|
||||||
cleanupQrLoginSessions();
|
|
||||||
|
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 token = crypto.randomBytes(24).toString('base64url');
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const session: QrLoginSession = {
|
const session: QrLoginSession = {
|
||||||
@@ -27,24 +62,61 @@ export function createQrLoginSession(ttlMs = 120_000) {
|
|||||||
createdAt: now,
|
createdAt: now,
|
||||||
expiresAt: now + ttlMs,
|
expiresAt: now + ttlMs,
|
||||||
};
|
};
|
||||||
qrLoginStore.set(token, session);
|
await persistQrLoginSession(session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQrLoginSession(token?: string | null) {
|
export async function getQrLoginSession(token?: string | null) {
|
||||||
if (!token) return 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') {
|
if (session && session.expiresAt <= Date.now() && session.status !== 'confirmed' && session.status !== 'used') {
|
||||||
session.status = 'expired';
|
session.status = 'expired';
|
||||||
|
await persistQrLoginSession(session);
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanupQrLoginSessions() {
|
export async function saveQrLoginSession(session: QrLoginSession) {
|
||||||
|
await persistQrLoginSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupQrLoginSessions() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const [token, session] of Array.from(qrLoginStore.entries())) {
|
for (const [token, session] of Array.from(qrLoginStore.entries())) {
|
||||||
if (session.expiresAt + 300_000 < now || session.status === 'used') {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user