Initial commit
This commit is contained in:
142
mcp-server/auth/apiClient.js
Normal file
142
mcp-server/auth/apiClient.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import axios from "axios";
|
||||
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { sessionApiClients, getSessionCredentials, setCredentials, findRoleByToken } from "./credentials.js";
|
||||
import { assertSafeCmsTarget } from "../utils/cmsTargetSafety.js";
|
||||
const DEFAULT_ROLE = 'developer';
|
||||
|
||||
/**
|
||||
* Check if session is configured with valid credentials
|
||||
*/
|
||||
export const ensureConfigured = async (sessionId) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
if (!creds.token || !creds.web_url || !creds.api_web_url) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
"Acai site not configured for safe local execution. Use the project MCP config/select_project flow so ACAI_API_WEB_URL points to the local environment."
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
assertSafeCmsTarget(creds, "apiClient");
|
||||
} catch (error) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
error.message
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rebuild API client for a session
|
||||
*/
|
||||
export const rebuildApiClient = async (sessionId) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
if (!creds.token || !creds.web_url || !creds.api_web_url) {
|
||||
return null;
|
||||
}
|
||||
assertSafeCmsTarget(creds, "apiClient");
|
||||
const client = axios.create({
|
||||
baseURL: creds.api_web_url,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Acai-Token": creds.token,
|
||||
...(creds.forge_host ? { Host: creds.forge_host } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Request interceptor: always send latest token
|
||||
client.interceptors.request.use((config) => {
|
||||
if (creds.token) {
|
||||
config.headers["X-Acai-Token"] = creds.token;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
sessionApiClients.set(sessionId, client);
|
||||
return client;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get or create API client for a session
|
||||
* @param {string} sessionId - The session ID
|
||||
*/
|
||||
export const getApiClient = async (sessionId) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
|
||||
console.error(`[API Client] getApiClient called for session ${sessionId}`);
|
||||
console.error(`[API Client] Current creds: token=${!!creds.token}, web_url=${creds.web_url}`);
|
||||
|
||||
await ensureConfigured(sessionId);
|
||||
let client = sessionApiClients.get(sessionId);
|
||||
if (!client) {
|
||||
console.error(`[API Client] No cached client, rebuilding...`);
|
||||
client = await rebuildApiClient(sessionId);
|
||||
}
|
||||
if (!client) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
"Unable to create API client. Verify credentials and try again."
|
||||
);
|
||||
}
|
||||
console.error(`[API Client] Returning client for session ${sessionId}`);
|
||||
return client;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set credentials and rebuild API client
|
||||
* @param {Object} credentials - The credentials object
|
||||
* @param {string} sessionId - The session ID
|
||||
* @param {string} mcpSessionId - Optional MCP-Session-Id for persistence across SSE reconnections
|
||||
*/
|
||||
export const setCredentialsAndRebuild = async (credentials, sessionId, mcpSessionId = null) => {
|
||||
await setCredentials(credentials, sessionId, mcpSessionId);
|
||||
await rebuildApiClient(sessionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for authenticated handlers
|
||||
* Supports both session-based auth and inline credentials (stateless mode).
|
||||
*
|
||||
* If args contains acaiToken + acaiWebsite, these are used directly,
|
||||
* allowing Claude to send credentials with each request.
|
||||
*/
|
||||
export const withAuth = (handler) => {
|
||||
return async (args, extra) => {
|
||||
const sessionId = extra?.sessionId || "_default";
|
||||
|
||||
console.error(`[withAuth] Called with sessionId: ${sessionId}`);
|
||||
|
||||
// Check for inline credentials (stateless mode)
|
||||
const inlineCredentials = {
|
||||
acaiToken: args.acaiToken,
|
||||
acaiWebsite: args.acaiWebsite,
|
||||
acaiTokenHash: args.acaiTokenHash
|
||||
};
|
||||
|
||||
const hasInlineCredentials = inlineCredentials.acaiToken && inlineCredentials.acaiWebsite;
|
||||
|
||||
if (hasInlineCredentials) {
|
||||
// Lookup role by token before storing credentials
|
||||
const role = findRoleByToken(inlineCredentials.acaiToken) || DEFAULT_ROLE;
|
||||
console.error(`[withAuth] Using INLINE credentials: website=${inlineCredentials.acaiWebsite}, role=${role}`);
|
||||
|
||||
// Temporarily store inline credentials in session for this request
|
||||
await setCredentials({
|
||||
token: inlineCredentials.acaiToken,
|
||||
website: inlineCredentials.acaiWebsite,
|
||||
web_url: `https://${inlineCredentials.acaiWebsite}`,
|
||||
api_web_url: null,
|
||||
forge_host: null,
|
||||
tokenHash: inlineCredentials.acaiTokenHash || null,
|
||||
profileName: 'inline',
|
||||
role: role
|
||||
}, sessionId);
|
||||
}
|
||||
|
||||
console.error(`[withAuth] Getting API client for session ${sessionId}...`);
|
||||
await getApiClient(sessionId);
|
||||
console.error(`[withAuth] API client ready, calling handler...`);
|
||||
|
||||
return handler(args, { ...extra, sessionId, inlineCredentials: hasInlineCredentials ? inlineCredentials : null });
|
||||
};
|
||||
};
|
||||
251
mcp-server/auth/credentials.js
Normal file
251
mcp-server/auth/credentials.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Session-based credentials management
|
||||
*
|
||||
* IMPORTANT: Each session is completely isolated.
|
||||
* Credentials are stored ONLY by sessionId, never shared between sessions.
|
||||
* This prevents credential leakage when multiple Claude tabs connect to different websites.
|
||||
*
|
||||
* AUTH TOKEN PERSISTENCE:
|
||||
* Claude MCP frequently reconnects SSE, creating new sessionIds each time.
|
||||
* To maintain credentials across reconnections, we also index by authToken
|
||||
* (the SimpleAuth header that Claude sends with each request).
|
||||
*/
|
||||
|
||||
const DEFAULT_ROLE = 'developer';
|
||||
|
||||
|
||||
// Session-based credentials storage (ephemeral, per-session)
|
||||
export const sessionCredentials = new Map();
|
||||
export const sessionApiClients = new Map();
|
||||
export const sessionUserTokens = new Map();
|
||||
|
||||
// Map sessionId -> McpServer instance (for role-based tool filtering)
|
||||
export const sessionServers = new Map();
|
||||
|
||||
// MCP-Session-Id -> credentials mapping for persistence across SSE reconnections
|
||||
// This is the standard MCP mechanism for session persistence
|
||||
// Key: MCP-Session-Id (UUID), Value: { credentials, lastAccess }
|
||||
export const mcpSessionCredentials = new Map();
|
||||
|
||||
// TTL for MCP session credentials (30 minutes - longer than authToken since it's per-conversation)
|
||||
const MCP_SESSION_TTL_MS = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Clean up expired MCP session credentials
|
||||
*/
|
||||
const cleanupExpiredMcpSessions = () => {
|
||||
const now = Date.now();
|
||||
for (const [mcpSessionId, data] of mcpSessionCredentials.entries()) {
|
||||
if (now - data.lastAccess > MCP_SESSION_TTL_MS) {
|
||||
console.error(`[Credentials] Cleaning up expired MCP-Session-Id ${mcpSessionId.substring(0, 8)}... (age: ${Math.round((now - data.lastAccess) / 1000)}s)`);
|
||||
mcpSessionCredentials.delete(mcpSessionId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Run cleanup every 5 minutes
|
||||
setInterval(cleanupExpiredMcpSessions, 5 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* Get credentials by MCP-Session-Id
|
||||
*/
|
||||
export const getMcpSessionCredentials = (mcpSessionId) => {
|
||||
const data = mcpSessionCredentials.get(mcpSessionId);
|
||||
if (data) {
|
||||
data.lastAccess = Date.now();
|
||||
console.error(`[Credentials] getMcpSessionCredentials(${mcpSessionId.substring(0, 8)}...) - FOUND: website=${data.credentials.website}`);
|
||||
return data.credentials;
|
||||
}
|
||||
console.error(`[Credentials] getMcpSessionCredentials(${mcpSessionId.substring(0, 8)}...) - NOT FOUND`);
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set credentials by MCP-Session-Id
|
||||
*/
|
||||
export const setMcpSessionCredentials = (mcpSessionId, credentials) => {
|
||||
mcpSessionCredentials.set(mcpSessionId, {
|
||||
credentials,
|
||||
lastAccess: Date.now()
|
||||
});
|
||||
console.error(`[Credentials] setMcpSessionCredentials(${mcpSessionId.substring(0, 8)}...) - website=${credentials.website} (total: ${mcpSessionCredentials.size})`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find role by token lookup in session storage
|
||||
* @param {string} token - The acaiToken to lookup
|
||||
* @returns {string|null} The role associated with this token, or null if not found
|
||||
*/
|
||||
export const findRoleByToken = (token) => {
|
||||
if (!token) return null;
|
||||
|
||||
// Search in sessionCredentials Map
|
||||
for (const [sessionId, creds] of sessionCredentials.entries()) {
|
||||
if (creds.token === token && creds.role) {
|
||||
console.error(`[Credentials] findRoleByToken - FOUND role=${creds.role} for token in session ${sessionId}`);
|
||||
return creds.role;
|
||||
}
|
||||
}
|
||||
|
||||
// Search in mcpSessionCredentials Map
|
||||
for (const [mcpSessionId, data] of mcpSessionCredentials.entries()) {
|
||||
if (data.credentials.token === token && data.credentials.role) {
|
||||
console.error(`[Credentials] findRoleByToken - FOUND role=${data.credentials.role} for token in MCP session ${mcpSessionId.substring(0, 8)}...`);
|
||||
return data.credentials.role;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`[Credentials] findRoleByToken - NOT FOUND for token`);
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get credentials for a specific session
|
||||
* Supports inline credentials that take priority over session credentials.
|
||||
* This allows Claude to send credentials with each request (stateless mode).
|
||||
*
|
||||
* @param {string} sessionId - The session ID
|
||||
* @param {Object} inlineCredentials - Optional inline credentials from tool params
|
||||
* @param {string} inlineCredentials.acaiToken - Token passed directly in tool call
|
||||
* @param {string} inlineCredentials.acaiWebsite - Website passed directly in tool call
|
||||
* @param {string} inlineCredentials.acaiTokenHash - Token hash passed directly in tool call
|
||||
*/
|
||||
export const getSessionCredentials = async (sessionId, inlineCredentials = null) => {
|
||||
// Priority 1: Inline credentials (stateless mode - Claude sends token with each request)
|
||||
if (inlineCredentials?.acaiToken && inlineCredentials?.acaiWebsite) {
|
||||
// Lookup role by token in session storage
|
||||
const role = findRoleByToken(inlineCredentials.acaiToken);
|
||||
console.error(`[Credentials] getSessionCredentials(${sessionId}) - USING INLINE: website=${inlineCredentials.acaiWebsite}, role=${role || 'not found'}`);
|
||||
|
||||
return {
|
||||
token: inlineCredentials.acaiToken,
|
||||
website: inlineCredentials.acaiWebsite,
|
||||
web_url: `https://${inlineCredentials.acaiWebsite}`,
|
||||
api_web_url: null,
|
||||
forge_host: null,
|
||||
tokenHash: inlineCredentials.acaiTokenHash || null,
|
||||
profileName: 'inline',
|
||||
role: role || DEFAULT_ROLE // Merge role from session storage, fallback to default
|
||||
};
|
||||
}
|
||||
|
||||
// Priority 2: Session credentials
|
||||
const sessionCreds = sessionCredentials.get(sessionId);
|
||||
if (sessionCreds) {
|
||||
console.error(`[Credentials] getSessionCredentials(${sessionId}) - FOUND: website=${sessionCreds.website}, hasToken=${!!sessionCreds.token}`);
|
||||
return sessionCreds;
|
||||
}
|
||||
|
||||
// Priority 3: Fallback to environment variables (for backwards compatibility)
|
||||
console.error(`[Credentials] getSessionCredentials(${sessionId}) - NOT FOUND, using env fallback`);
|
||||
console.error(`[Credentials] Active sessions: [${Array.from(sessionCredentials.keys()).join(', ')}]`);
|
||||
console.error(`[Credentials] Active MCP sessions: ${mcpSessionCredentials.size}`);
|
||||
|
||||
const envWebsite = process.env.ACAI_WEBSITE || null;
|
||||
return {
|
||||
token: process.env.ACAI_TOKEN || null,
|
||||
website: envWebsite,
|
||||
web_url: process.env.ACAI_WEB_URL || (envWebsite ? `https://${envWebsite}` : null),
|
||||
api_web_url: process.env.ACAI_API_WEB_URL || null,
|
||||
forge_host: process.env.ACAI_FORGE_HOST || null,
|
||||
tokenHash: process.env.ACAI_TOKEN_HASH || null,
|
||||
profileName: 'default',
|
||||
role: 'developer', // Env fallback = local dev, full access
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get X-User-Token for a specific session (for fallback login)
|
||||
*/
|
||||
export const getSessionUserToken = (sessionId) => {
|
||||
return sessionUserTokens.get(sessionId) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set X-User-Token for a specific session
|
||||
*/
|
||||
export const setSessionUserToken = (userToken, sessionId) => {
|
||||
if (userToken) {
|
||||
sessionUserTokens.set(sessionId, userToken);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set credentials for a specific session
|
||||
* Credentials are stored by sessionId AND by mcpSessionId (for SSE reconnection persistence).
|
||||
* @param {Object} credentials - The credentials object
|
||||
* @param {string} sessionId - The session ID (SSE transport session)
|
||||
* @param {string} mcpSessionId - Optional MCP-Session-Id for persistence across SSE reconnections
|
||||
*/
|
||||
export const setCredentials = async ({ website, web_url, api_web_url, forge_host, token, tokenHash, profileName, role }, sessionId, mcpSessionId = null) => {
|
||||
console.error(`[Credentials] setCredentials(${sessionId}) - website=${website}, web_url=${web_url}, api_web_url=${api_web_url}, forge_host=${forge_host || ""}, hasToken=${!!token}, hasTokenHash=${!!tokenHash}, profile=${profileName || "manual"}, role=${role || 'default'}, hasMcpSessionId=${!!mcpSessionId}`);
|
||||
|
||||
const creds = {
|
||||
website,
|
||||
web_url: web_url || (website ? `https://${website}` : null),
|
||||
api_web_url: api_web_url || null,
|
||||
forge_host: forge_host || null,
|
||||
token,
|
||||
tokenHash,
|
||||
profileName: profileName || "manual",
|
||||
role: role || DEFAULT_ROLE,
|
||||
};
|
||||
|
||||
// Store by sessionId
|
||||
sessionCredentials.set(sessionId, creds);
|
||||
|
||||
// Also store by MCP-Session-Id for persistence across SSE reconnections
|
||||
if (mcpSessionId) {
|
||||
setMcpSessionCredentials(mcpSessionId, creds);
|
||||
}
|
||||
|
||||
// Verify it was set
|
||||
const verify = sessionCredentials.get(sessionId);
|
||||
console.error(`[Credentials] Verification: exists=${!!verify}, website=${verify?.website}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear credentials for a session
|
||||
* NOTE: We only clear the sessionId mappings, NOT the authToken mappings.
|
||||
* This allows credentials to persist across SSE reconnections.
|
||||
* AuthToken credentials will be cleaned up by the TTL cleanup routine.
|
||||
*/
|
||||
export const clearSessionCredentials = (sessionId) => {
|
||||
console.error(`[Credentials] Clearing session ${sessionId} (authToken credentials preserved for reconnection)`);
|
||||
|
||||
sessionCredentials.delete(sessionId);
|
||||
sessionApiClients.delete(sessionId);
|
||||
sessionUserTokens.delete(sessionId);
|
||||
sessionServers.delete(sessionId);
|
||||
// NOTE: We intentionally do NOT clear authTokenCredentials here
|
||||
// to allow credentials to persist across SSE reconnections
|
||||
};
|
||||
|
||||
/**
|
||||
* Get common params for API requests
|
||||
* @param {string} sessionId - The session ID
|
||||
* @param {Object} extraParams - Extra parameters to include
|
||||
*/
|
||||
export const getCommonParams = async (sessionId, extraParams = {}) => {
|
||||
const creds = await getSessionCredentials(sessionId);
|
||||
const params = {
|
||||
token: creds.token,
|
||||
...extraParams
|
||||
};
|
||||
if (creds.tokenHash) {
|
||||
params.tokenHash = creds.tokenHash;
|
||||
}
|
||||
return params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract MCP-Session-Id from request headers
|
||||
* @param {Object} extra - The extra object passed to tool handlers
|
||||
* @returns {string|null} The MCP-Session-Id or null
|
||||
*/
|
||||
export const extractMcpSessionId = (extra) => {
|
||||
const headers = extra?.requestInfo?.headers;
|
||||
if (!headers) return null;
|
||||
|
||||
return headers['mcp-session-id'] || null;
|
||||
};
|
||||
26
mcp-server/auth/index.js
Normal file
26
mcp-server/auth/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export {
|
||||
sessionCredentials,
|
||||
sessionApiClients,
|
||||
sessionServers,
|
||||
mcpSessionCredentials,
|
||||
getSessionCredentials,
|
||||
setCredentials,
|
||||
clearSessionCredentials,
|
||||
getCommonParams,
|
||||
getSessionUserToken,
|
||||
setSessionUserToken,
|
||||
getMcpSessionCredentials,
|
||||
setMcpSessionCredentials,
|
||||
extractMcpSessionId
|
||||
} from './credentials.js';
|
||||
|
||||
export {
|
||||
ensureConfigured,
|
||||
rebuildApiClient,
|
||||
getApiClient,
|
||||
setCredentialsAndRebuild,
|
||||
withAuth
|
||||
} from './apiClient.js';
|
||||
|
||||
export { fetchProjectInfo, fetchProjectsList } from './localClient.js';
|
||||
|
||||
14
mcp-server/auth/localClient.js
Normal file
14
mcp-server/auth/localClient.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import axios from "axios";
|
||||
import { LOCAL_SERVER_URL } from "../config/index.js";
|
||||
|
||||
export async function fetchProjectInfo(projectName) {
|
||||
const response = await axios.get(`${LOCAL_SERVER_URL}/api/mcp/project-info`, {
|
||||
params: { project: projectName }
|
||||
});
|
||||
return response.data; // { success, web_url, token, tokenHash, domain, project_dir }
|
||||
}
|
||||
|
||||
export async function fetchProjectsList() {
|
||||
const response = await axios.get(`${LOCAL_SERVER_URL}/api/mcp/projects`);
|
||||
return response.data; // { success, projects: [...] }
|
||||
}
|
||||
207
mcp-server/auth/redisClient.js
Normal file
207
mcp-server/auth/redisClient.js
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user