/**
* Shared user service for accessing cached user data across APIs
* Handles cache misses by falling back to members API
*
* For containerized services, use the HTTP client version below
*/
import { getRedisClient } from '@commtool/shared-auth';
const MEMBERS_API_URL = process.env.MEMBERS_API_URL || 'http://localhost:3000';
/**
* Get user data from cache or members API
* @param {string} userUID - User UUID
* @param {string} orgaUID - Organization UUID
* @param {string} authToken - Optional auth token for API calls
* @returns {Object} - User validation data
*/
export const getCachedUserData = async (userUID, orgaUID, authToken = null) => {
try {
// Try cache first
const cacheKey = `user-org-${userUID}-${orgaUID}`;
const cached = await getRedisClient().get(cacheKey);
if (cached) {
console.log(`[getCachedUserData] Cache hit for user ${userUID} in org ${orgaUID}`);
return JSON.parse(cached);
}
// Cache miss - try to populate from members API
console.log(`[getCachedUserData] Cache miss for user ${userUID} in org ${orgaUID}, calling members API`);
if (authToken) {
const userData = await fetchUserFromMembersAPI(userUID, orgaUID, authToken);
if (userData) {
// Cache the result for future use
await getRedisClient().setEx(cacheKey, 900, JSON.stringify(userData));
console.log(`[getCachedUserData] Cached user data for ${userUID} in org ${orgaUID}`);
return userData;
}
}
// No auth token or API call failed
console.warn(`[getCachedUserData] Unable to fetch user data for ${userUID} in org ${orgaUID}`);
return { valid: false, isAdmin: false };
} catch (error) {
console.error(`[getCachedUserData] Error: ${error.message}`);
return { valid: false, isAdmin: false };
}
};
/**
* HTTP Client for other containerized APIs
* This can be copied to other services or published as a shared library
*/
export class UserServiceClient {
constructor(membersApiUrl = 'http://members-api:3000', redisUrl = null) {
this.membersApiUrl = membersApiUrl;
this.redisUrl = redisUrl; // For direct Redis access if available
}
/**
* Get user data - works across containers via HTTP
* @param {string} userUID - User UUID
* @param {string} orgaUID - Organization UUID
* @param {string} authToken - Auth token for API calls
* @returns {Object} - User validation data
*/
async getUserData(userUID, orgaUID, authToken) {
try {
const response = await fetch(`${this.membersApiUrl}/api/validate-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
'X-User-UID': userUID,
'X-Orga-UID': orgaUID
},
body: JSON.stringify({ userUID, orgaUID })
});
if (response.ok) {
const data = await response.json();
if (data.success) {
return {
valid: data.valid,
isAdmin: data.isAdmin,
userData: data.userData,
userUID: data.userUID
};
}
}
console.warn(`[UserServiceClient] API call failed: ${response.status}`);
return { valid: false, isAdmin: false };
} catch (error) {
console.error(`[UserServiceClient] Error: ${error.message}`);
return { valid: false, isAdmin: false };
}
}
}
/**
* Fetch user data from members API to populate cache
* @param {string} userUID - User UUID
* @param {string} orgaUID - Organization UUID
* @param {string} authToken - Auth token for API call
* @returns {Object} - User data or null
*/
const fetchUserFromMembersAPI = async (userUID, orgaUID, authToken) => {
try {
const response = await fetch(`${MEMBERS_API_URL}/validate-user`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
'X-User-UID': userUID,
'X-Orga-UID': orgaUID
}
});
if (response.ok) {
const data = await response.json();
if (data.success && data.userData) {
return {
valid: true,
isAdmin: data.isAdmin || false,
userData: data.userData,
userUID: userUID
};
}
}
console.warn(`[fetchUserFromMembersAPI] API call failed: ${response.status}`);
return null;
} catch (error) {
console.error(`[fetchUserFromMembersAPI] Error: ${error.message}`);
return null;
}
};
/**
* Pre-warm cache with commonly accessed users
* @param {Array} userOrgPairs - Array of {userUID, orgaUID} objects
* @param {string} authToken - Auth token for API calls
*/
export const preWarmUserCache = async (userOrgPairs, authToken) => {
console.log(`[preWarmUserCache] Pre-warming cache for ${userOrgPairs.length} user-org pairs`);
const promises = userOrgPairs.map(({ userUID, orgaUID }) =>
getCachedUserData(userUID, orgaUID, authToken)
);
await Promise.allSettled(promises);
console.log(`[preWarmUserCache] Cache pre-warming completed`);
};
/**
* Check if user data exists in cache
* @param {string} userUID - User UUID
* @param {string} orgaUID - Organization UUID
* @returns {boolean} - True if data exists in cache
*/
export const isUserDataCached = async (userUID, orgaUID) => {
const cacheKey = `user-org-${userUID}-${orgaUID}`;
const cached = await getRedisClient().get(cacheKey);
return cached !== null;
};