208 lines
6.0 KiB
JavaScript
208 lines
6.0 KiB
JavaScript
/**
|
|
* Redis Client for Session Persistence
|
|
*
|
|
* Provides persistent storage for user credentials across server restarts.
|
|
* Falls back to in-memory Map if Redis is unavailable.
|
|
*/
|
|
|
|
import { createClient } from 'redis';
|
|
|
|
const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
|
|
const USER_CACHE_TTL = 30 * 60; // 30 minutes in seconds
|
|
|
|
let redisClient = null;
|
|
let redisAvailable = false;
|
|
|
|
// Fallback in-memory cache (used if Redis is unavailable)
|
|
const memoryCache = new Map();
|
|
|
|
/**
|
|
* Initialize Redis client
|
|
*/
|
|
export async function initRedis() {
|
|
try {
|
|
console.error('[Redis] Connecting to Redis at', REDIS_URL);
|
|
|
|
redisClient = createClient({
|
|
url: REDIS_URL,
|
|
socket: {
|
|
connectTimeout: 5000,
|
|
reconnectStrategy: (retries) => {
|
|
if (retries > 3) {
|
|
console.error('[Redis] Max reconnection attempts reached, falling back to memory cache');
|
|
redisAvailable = false;
|
|
return false; // Stop reconnecting
|
|
}
|
|
return Math.min(retries * 100, 3000);
|
|
}
|
|
}
|
|
});
|
|
|
|
redisClient.on('error', (err) => {
|
|
console.error('[Redis] Error:', err.message);
|
|
redisAvailable = false;
|
|
});
|
|
|
|
redisClient.on('connect', () => {
|
|
console.error('[Redis] Connected successfully');
|
|
redisAvailable = true;
|
|
});
|
|
|
|
redisClient.on('reconnecting', () => {
|
|
console.error('[Redis] Reconnecting...');
|
|
});
|
|
|
|
await redisClient.connect();
|
|
redisAvailable = true;
|
|
console.error('[Redis] Ready to use');
|
|
|
|
} catch (error) {
|
|
console.error('[Redis] Failed to initialize:', error.message);
|
|
console.error('[Redis] Falling back to in-memory cache');
|
|
redisAvailable = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set user credentials in cache (Redis or memory)
|
|
*/
|
|
export async function setUserCredentials(userIdentifier, credentials) {
|
|
if (!userIdentifier) {
|
|
console.error('[Redis] Cannot set credentials: no userIdentifier');
|
|
return false;
|
|
}
|
|
|
|
const key = `user:creds:${userIdentifier}`;
|
|
const value = JSON.stringify({
|
|
...credentials,
|
|
lastUsed: Date.now()
|
|
});
|
|
|
|
if (redisAvailable && redisClient) {
|
|
try {
|
|
await redisClient.setEx(key, USER_CACHE_TTL, value);
|
|
console.error(`[Redis] Saved credentials for user ${userIdentifier} (TTL: ${USER_CACHE_TTL}s)`);
|
|
return true;
|
|
} catch (error) {
|
|
console.error('[Redis] Error saving to Redis:', error.message);
|
|
console.error('[Redis] Falling back to memory cache for this operation');
|
|
redisAvailable = false;
|
|
}
|
|
}
|
|
|
|
// Fallback to memory cache
|
|
memoryCache.set(key, {
|
|
value,
|
|
expiresAt: Date.now() + (USER_CACHE_TTL * 1000)
|
|
});
|
|
console.error(`[Redis] Saved credentials for user ${userIdentifier} to memory cache`);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get user credentials from cache (Redis or memory)
|
|
*/
|
|
export async function getUserCredentials(userIdentifier) {
|
|
if (!userIdentifier) {
|
|
return null;
|
|
}
|
|
|
|
const key = `user:creds:${userIdentifier}`;
|
|
|
|
if (redisAvailable && redisClient) {
|
|
try {
|
|
const value = await redisClient.get(key);
|
|
if (value) {
|
|
console.error(`[Redis] Retrieved credentials for user ${userIdentifier} from Redis`);
|
|
const creds = JSON.parse(value);
|
|
|
|
// Update lastUsed timestamp
|
|
await setUserCredentials(userIdentifier, {
|
|
website: creds.website,
|
|
token: creds.token,
|
|
tokenHash: creds.tokenHash,
|
|
profileName: creds.profileName
|
|
});
|
|
|
|
return creds;
|
|
}
|
|
} catch (error) {
|
|
console.error('[Redis] Error reading from Redis:', error.message);
|
|
console.error('[Redis] Falling back to memory cache');
|
|
redisAvailable = false;
|
|
}
|
|
}
|
|
|
|
// Fallback to memory cache
|
|
const cached = memoryCache.get(key);
|
|
if (cached) {
|
|
if (Date.now() < cached.expiresAt) {
|
|
console.error(`[Redis] Retrieved credentials for user ${userIdentifier} from memory cache`);
|
|
const creds = JSON.parse(cached.value);
|
|
|
|
// Update expiration
|
|
memoryCache.set(key, {
|
|
value: cached.value,
|
|
expiresAt: Date.now() + (USER_CACHE_TTL * 1000)
|
|
});
|
|
|
|
return creds;
|
|
} else {
|
|
console.error(`[Redis] Memory cache expired for user ${userIdentifier}`);
|
|
memoryCache.delete(key);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Delete user credentials from cache
|
|
*/
|
|
export async function deleteUserCredentials(userIdentifier) {
|
|
if (!userIdentifier) {
|
|
return;
|
|
}
|
|
|
|
const key = `user:creds:${userIdentifier}`;
|
|
|
|
if (redisAvailable && redisClient) {
|
|
try {
|
|
await redisClient.del(key);
|
|
console.error(`[Redis] Deleted credentials for user ${userIdentifier} from Redis`);
|
|
} catch (error) {
|
|
console.error('[Redis] Error deleting from Redis:', error.message);
|
|
}
|
|
}
|
|
|
|
// Also delete from memory cache
|
|
memoryCache.delete(key);
|
|
console.error(`[Redis] Deleted credentials for user ${userIdentifier} from memory cache`);
|
|
}
|
|
|
|
/**
|
|
* Get Redis health status
|
|
*/
|
|
export function getRedisStatus() {
|
|
return {
|
|
available: redisAvailable,
|
|
connected: redisClient?.isOpen || false,
|
|
url: REDIS_URL,
|
|
fallbackCacheSize: memoryCache.size
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Close Redis connection (for graceful shutdown)
|
|
*/
|
|
export async function closeRedis() {
|
|
if (redisClient) {
|
|
try {
|
|
await redisClient.quit();
|
|
console.error('[Redis] Connection closed');
|
|
} catch (error) {
|
|
console.error('[Redis] Error closing connection:', error.message);
|
|
}
|
|
}
|
|
}
|