Files
MoonTVPlus/src/lib/refresh-token.ts
2026-01-30 14:27:43 +08:00

255 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* eslint-disable no-console */
import { TOKEN_CONFIG } from './token-config';
// Re-export TOKEN_CONFIG for backward compatibility
export { TOKEN_CONFIG };
// Lazy import to avoid Edge Runtime issues in middleware
let getStorage: (() => any) | null = null;
async function loadStorage() {
if (!getStorage) {
const db = await import('./db');
getStorage = db.getStorage;
}
return getStorage();
}
interface TokenData {
token: string;
deviceInfo: string;
createdAt: number;
expiresAt: number;
lastUsed: number;
}
// 生成随机 Token ID
export function generateTokenId(): string {
const array = new Uint8Array(16);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// 生成随机 Refresh Token
export function generateRefreshToken(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
// 存储 Refresh Token使用 Redis Hash
export async function storeRefreshToken(
username: string,
tokenId: string,
tokenData: TokenData
): Promise<void> {
const hashKey = `user_tokens:${username}`;
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hSet !== 'function') {
console.warn('Redis Hash not supported, skipping token storage');
return;
}
try {
await (storage as any).adapter.hSet(
hashKey,
tokenId,
JSON.stringify(tokenData)
);
console.log(`Stored refresh token for ${username}:${tokenId}`);
} catch (error) {
console.error('Failed to store refresh token:', error);
throw error;
}
}
// 验证 Refresh Token
export async function verifyRefreshToken(
username: string,
tokenId: string,
refreshToken: string
): Promise<boolean> {
const hashKey = `user_tokens:${username}`;
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hGet !== 'function') {
console.warn('Redis Hash not supported');
return false;
}
try {
const dataStr = await (storage as any).adapter.hGet(hashKey, tokenId);
if (!dataStr) {
return false;
}
const tokenData: TokenData = JSON.parse(dataStr);
// 检查是否过期
if (Date.now() > tokenData.expiresAt) {
// 过期了,删除
await (storage as any).adapter.hDel(hashKey, tokenId);
return false;
}
// 验证 Token
if (tokenData.token !== refreshToken) {
return false;
}
// 更新最后使用时间
tokenData.lastUsed = Date.now();
await (storage as any).adapter.hSet(
hashKey,
tokenId,
JSON.stringify(tokenData)
);
return true;
} catch (error) {
console.error('Failed to verify refresh token:', error);
return false;
}
}
// 撤销单个 Token
export async function revokeRefreshToken(
username: string,
tokenId: string
): Promise<void> {
const hashKey = `user_tokens:${username}`;
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hDel !== 'function') {
console.warn('Redis Hash not supported');
return;
}
try {
await (storage as any).adapter.hDel(hashKey, tokenId);
console.log(`Revoked refresh token for ${username}:${tokenId}`);
} catch (error) {
console.error('Failed to revoke refresh token:', error);
}
}
// 获取用户的所有设备
export async function getUserDevices(username: string): Promise<Array<{
tokenId: string;
deviceInfo: string;
createdAt: number;
lastUsed: number;
expiresAt: number;
}>> {
const hashKey = `user_tokens:${username}`;
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hGetAll !== 'function') {
console.warn('Redis Hash not supported');
return [];
}
try {
const allTokens = await (storage as any).adapter.hGetAll(hashKey);
if (!allTokens || typeof allTokens !== 'object') {
return [];
}
const devices = [];
const now = Date.now();
for (const [tokenId, dataStr] of Object.entries(allTokens)) {
try {
const tokenData: TokenData = JSON.parse(dataStr as string);
// 检查是否过期
if (now > tokenData.expiresAt) {
// 过期了,删除
await (storage as any).adapter.hDel(hashKey, tokenId);
continue;
}
devices.push({
tokenId,
deviceInfo: tokenData.deviceInfo,
createdAt: tokenData.createdAt,
lastUsed: tokenData.lastUsed,
expiresAt: tokenData.expiresAt,
});
} catch (err) {
console.error(`Failed to parse token data for ${tokenId}:`, err);
}
}
return devices;
} catch (error) {
console.error('Failed to get user devices:', error);
return [];
}
}
// 撤销所有 Token
export async function revokeAllRefreshTokens(username: string): Promise<void> {
const hashKey = `user_tokens:${username}`;
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.del !== 'function') {
console.warn('Redis Hash not supported');
return;
}
try {
await (storage as any).adapter.del(hashKey);
console.log(`Revoked all refresh tokens for ${username}`);
} catch (error) {
console.error('Failed to revoke all refresh tokens:', error);
}
}
// 清理过期的 Token定期任务
export async function cleanupExpiredTokens(username: string): Promise<number> {
const hashKey = `user_tokens:${username}`;
const storage = await loadStorage();
if (!storage || typeof (storage as any).adapter?.hGetAll !== 'function') {
return 0;
}
try {
const allTokens = await (storage as any).adapter.hGetAll(hashKey);
if (!allTokens || typeof allTokens !== 'object') {
return 0;
}
const now = Date.now();
let cleanedCount = 0;
for (const [tokenId, dataStr] of Object.entries(allTokens)) {
try {
const tokenData: TokenData = JSON.parse(dataStr as string);
if (now > tokenData.expiresAt) {
await (storage as any).adapter.hDel(hashKey, tokenId);
cleanedCount++;
}
} catch (err) {
console.error(`Failed to parse token data for ${tokenId}:`, err);
}
}
if (cleanedCount > 0) {
console.log(`Cleaned up ${cleanedCount} expired tokens for ${username}`);
}
return cleanedCount;
} catch (error) {
console.error('Failed to cleanup expired tokens:', error);
return 0;
}
}