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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|