增加扫码登录状态配置

This commit is contained in:
mtvpls
2026-06-01 00:29:45 +08:00
parent 8d712afa0d
commit 3099cf9bfa
2 changed files with 122 additions and 33 deletions
+121 -33
View File
@@ -11,6 +11,16 @@ export interface QrLoginSession {
userAgent?: string;
}
export type QrLoginStoreMode = 'auto' | 'memory' | 'hybrid' | 'shared';
type ResolvedQrLoginStoreMode = Exclude<QrLoginStoreMode, 'auto'>;
type QrLoginStorageAdapter = {
hSet?: (key: string, field: string, value: string) => Promise<unknown>;
hGet?: (key: string, field: string) => Promise<string | null>;
hGetAll?: (key: string) => Promise<Record<string, string>>;
hDel?: (key: string, field: string) => Promise<unknown>;
};
type QrLoginStorage = { adapter?: QrLoginStorageAdapter };
type GlobalWithQr = typeof globalThis & { __moonTvQrLoginStore?: Map<string, QrLoginSession> };
const g = globalThis as GlobalWithQr;
@@ -18,38 +28,104 @@ export const qrLoginStore = g.__moonTvQrLoginStore || new Map<string, QrLoginSes
g.__moonTvQrLoginStore = qrLoginStore;
const QR_LOGIN_HASH_KEY = 'qr_login_sessions';
const VALID_STORE_MODES = new Set<QrLoginStoreMode>(['auto', 'memory', 'hybrid', 'shared']);
let getStorage: (() => any) | null = null;
let getStorage: (() => unknown) | null = null;
async function loadStorage() {
function resolveQrLoginStoreMode(): ResolvedQrLoginStoreMode {
const configuredMode = (process.env.QR_LOGIN_STORE_MODE || 'auto').toLowerCase() as QrLoginStoreMode;
const mode = VALID_STORE_MODES.has(configuredMode) ? configuredMode : 'auto';
if (mode !== 'auto') return mode;
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
const isCloudflare =
process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare';
if (storageType === 'upstash' || isCloudflare) return 'hybrid';
return 'memory';
}
async function loadStorage(required = false) {
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 {
const storage = getStorage() as QrLoginStorage | null;
if (storage && typeof storage.adapter?.hGet === 'function') return storage;
} catch (error) {
if (required) throw error;
}
if (required) {
throw new Error('QR login shared storage is unavailable');
}
return null;
}
async function readSharedQrLoginSession(token: string, required = false) {
const storage = await loadStorage(required);
if (!storage || typeof storage.adapter?.hGet !== 'function') return null;
try {
const raw = await storage.adapter.hGet(QR_LOGIN_HASH_KEY, token);
if (!raw) return null;
return JSON.parse(raw) as QrLoginSession;
} catch (error) {
if (required) throw error;
try {
await storage.adapter.hDel?.(QR_LOGIN_HASH_KEY, token);
} catch {
// ignore best-effort cleanup failures in hybrid mode
}
return null;
}
}
async function persistQrLoginSession(session: QrLoginSession) {
qrLoginStore.set(session.token, session);
const mode = resolveQrLoginStoreMode();
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hSet !== 'function') return;
if (mode !== 'shared') {
qrLoginStore.set(session.token, session);
}
await (storage as any).adapter.hSet(QR_LOGIN_HASH_KEY, session.token, JSON.stringify(session));
if (mode === 'memory') return;
const storage = await loadStorage(mode === 'shared');
if (!storage || typeof storage.adapter?.hSet !== 'function') {
if (mode === 'shared') throw new Error('QR login shared storage does not support hSet');
return;
}
try {
await storage.adapter.hSet(QR_LOGIN_HASH_KEY, session.token, JSON.stringify(session));
} catch (error) {
if (mode === 'shared') throw error;
}
}
async function deletePersistedQrLoginSession(token: string) {
qrLoginStore.delete(token);
const mode = resolveQrLoginStoreMode();
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hDel !== 'function') return;
if (mode !== 'shared') {
qrLoginStore.delete(token);
}
await (storage as any).adapter.hDel(QR_LOGIN_HASH_KEY, token);
if (mode === 'memory') return;
const storage = await loadStorage(mode === 'shared');
if (!storage || typeof storage.adapter?.hDel !== 'function') {
if (mode === 'shared') throw new Error('QR login shared storage does not support hDel');
return;
}
try {
await storage.adapter.hDel(QR_LOGIN_HASH_KEY, token);
} catch (error) {
if (mode === 'shared') throw error;
}
}
export async function createQrLoginSession(ttlMs = 120_000) {
@@ -68,21 +144,17 @@ export async function createQrLoginSession(ttlMs = 120_000) {
export async function getQrLoginSession(token?: string | null) {
if (!token) return null;
let session = qrLoginStore.get(token) || null;
const mode = resolveQrLoginStoreMode();
let session: QrLoginSession | null = 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 (mode === 'memory') {
session = qrLoginStore.get(token) || null;
} else {
session = await readSharedQrLoginSession(token, mode === 'shared');
if (session && mode === 'hybrid') {
qrLoginStore.set(token, session);
} else if (!session && mode === 'hybrid') {
session = qrLoginStore.get(token) || null;
}
}
@@ -98,17 +170,33 @@ export async function saveQrLoginSession(session: QrLoginSession) {
}
export async function cleanupQrLoginSessions() {
const mode = resolveQrLoginStoreMode();
const now = Date.now();
for (const [token, session] of Array.from(qrLoginStore.entries())) {
if (session.expiresAt + 300_000 < now || session.status === 'used') {
await deletePersistedQrLoginSession(token);
if (mode !== 'shared') {
for (const [token, session] of Array.from(qrLoginStore.entries())) {
if (session.expiresAt + 300_000 < now || session.status === 'used') {
await deletePersistedQrLoginSession(token);
}
}
}
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hGetAll !== 'function') return;
if (mode === 'memory') return;
const storage = await loadStorage(mode === 'shared');
if (!storage || typeof storage.adapter?.hGetAll !== 'function') {
if (mode === 'shared') throw new Error('QR login shared storage does not support hGetAll');
return;
}
let sessions: Record<string, string>;
try {
sessions = await storage.adapter.hGetAll(QR_LOGIN_HASH_KEY);
} catch (error) {
if (mode === 'shared') throw error;
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;