Files
MoonTVPlus/src/lib/refresh-token.ts

255 lines
6.4 KiB
TypeScript
Raw Normal View History

2026-01-24 15:31:13 +08:00
/* eslint-disable no-console */
2026-01-30 14:27:43 +08:00
import { TOKEN_CONFIG } from './token-config';
// Re-export TOKEN_CONFIG for backward compatibility
export { TOKEN_CONFIG };
2026-01-26 08:58:04 +08:00
// 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();
}
2026-01-24 15:31:13 +08:00
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}`;
2026-01-26 08:58:04 +08:00
const storage = await loadStorage();
2026-01-24 15:31:13 +08:00
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}`;
2026-01-26 08:58:04 +08:00
const storage = await loadStorage();
2026-01-24 15:31:13 +08:00
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}`;
2026-01-26 08:58:04 +08:00
const storage = await loadStorage();
2026-01-24 15:31:13 +08:00
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}`;
2026-01-26 08:58:04 +08:00
const storage = await loadStorage();
2026-01-24 15:31:13 +08:00
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}`;
2026-01-26 08:58:04 +08:00
const storage = await loadStorage();
2026-01-24 15:31:13 +08:00
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}`;
2026-01-26 08:58:04 +08:00
const storage = await loadStorage();
2026-01-24 15:31:13 +08:00
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;
}
}