/**
* User utilities for handling user selection, validation, and assignment
*
* With Bearer-only auth (no sessions), user data is cached in Redis to avoid
* database queries on every API call.
*/
import { query, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { getRedisClient } from '@commtool/shared-auth';
import { isAdmin } from '../utils/authChecks.js';
/**
* Invalidate user cache for a specific user (call when user data changes)
* @param {string} userUID - User UUID to invalidate, or '*' for all users
* @param {string} [orgaUID] - Optional: specific org to invalidate, otherwise all orgs
*/
export const invalidateUserCache = async (userUID, orgaUID = null) => {
try {
const redis = getRedisClient();
if (userUID === '*' && orgaUID) {
// Invalidate ALL users for a specific organization
const keys = await redis.keys(`user-data-*-${orgaUID}`);
const keys2 = await redis.keys(`user-org-*-${orgaUID}`);
const allKeys = [...keys, ...keys2];
if (allKeys.length > 0) {
await redis.del(allKeys);
}
console.log(`[invalidateUserCache] Invalidated ${allKeys.length} cache entries for org ${orgaUID}`);
} else if (orgaUID) {
// Invalidate specific user-org combo
await redis.del(`user-data-${userUID}-${orgaUID}`);
await redis.del(`user-org-${userUID}-${orgaUID}`);
console.log(`[invalidateUserCache] Invalidated cache for user ${userUID} in org ${orgaUID}`);
} else {
// Invalidate all cached data for this user across all orgs
const keys = await redis.keys(`user-data-${userUID}-*`);
const keys2 = await redis.keys(`user-org-${userUID}-*`);
const allKeys = [...keys, ...keys2];
if (allKeys.length > 0) {
await redis.del(allKeys);
}
console.log(`[invalidateUserCache] Invalidated ${allKeys.length} cache entries for user ${userUID}`);
}
} catch (error) {
console.error(`[invalidateUserCache] Error: ${error.message}`);
}
};
/**
* Validate user access to organization with caching
* @param {string} userUID - User UUID
* @param {string} orgaUID - Organization UUID
* @param {string} tokenId - Optional token ID for caching (deprecated - auth library handles this)
* @returns {Promise<Object>} - {valid, isAdmin, userData, userUID}
*/
export const validateUserForOrganization = async (userUID, orgaUID, tokenId = null) => {
try {
// Simplified cache key - auth library handles token caching, we just need org-specific validation
const cacheKey = `user-org-${userUID}-${orgaUID}`;
const cached = await getRedisClient().get(cacheKey);
if (cached) {
console.log(`[validateUserForOrganization] Cache hit for user ${userUID} in org ${orgaUID}`);
return JSON.parse(cached);
}
// Validate user exists and has access to organization
const userQuery = await query(`
SELECT
MUser.UID,
MUser.Data,
MUser.Display,
MLink.Type,
CASE WHEN MLink.Type IN ('memberA', 'memberSys') THEN 1 ELSE 0 END as isAdmin
FROM Member AS MUser
INNER JOIN Links AS MLink ON (MUser.UID = MLink.UID)
INNER JOIN ObjectBase ON (MUser.UID = ObjectBase.UID)
WHERE MUser.UID = ?
AND MLink.UIDTarget = ?
AND MLink.Type IN ('member', 'memberA', 'memberSys')
AND ObjectBase.Type = 'extern'
`, [UUID2hex(userUID), UUID2hex(orgaUID)], { cast: ['json', 'UUID'] });
if (userQuery.length > 0) {
const userData = userQuery[0];
const result = {
valid: true,
isAdmin: userData.isAdmin === 1,
userData: userData,
userUID: userUID
};
// Cache for 15 minutes (reduced since auth library caches user identity)
await getRedisClient().setEx(cacheKey, 900, JSON.stringify(result));
console.log(`[validateUserForOrganization] User ${userUID} validated for org ${orgaUID}`);
return result;
} else {
console.warn(`[validateUserForOrganization] User ${userUID} not found or no access to org ${orgaUID}`);
return { valid: false, isAdmin: false };
}
} catch (error) {
console.error(`[validateUserForOrganization] Error validating user: ${error.message}`);
return { valid: false, isAdmin: false };
}
};
/**
* Get user object for the logged-in user for their organizations
* Uses Redis caching to avoid database queries on every API call with Bearer auth
* @param {Object} req - Express request object
* @returns {Promise<Object>} - User object or false if not found
*/
export const getUser = async (req) => {
try {
const userUID = req.session.authUser.userUID;
const orgaUID = req.session.root;
// Cache key based on user + organization
const cacheKey = `user-data-${userUID}-${orgaUID}`;
// Try to get from cache first
const cached = await getRedisClient().get(cacheKey);
if (cached) {
const user = JSON.parse(cached);
// Re-evaluate isAdmin from current session (in case roles changed)
user.isAdmin = await isAdmin(req.session);
return user;
}
// Cache miss - query database
let users = await query(
`SELECT Links.UIDTarget AS UID, MUser.Data, MUser.Display, MLink.UIDTarget AS UIDorga, ObjectBase.Type FROM Links
INNER JOIN Links AS MLink ON (Links.UIDTarget=MLink.UID AND MLink.Type IN ('member','memberA','memberSys'))
INNER JOIN Member AS MUser ON (MLink.UID=MUser.UID)
INNER JOIN ObjectBase ON (MUser.UID=ObjectBase.UID)
WHERE Links.UID=? AND Links.Type='identifyer'`,
[UUID2hex(userUID)],
{ cast: ['json', 'UUID'], log: false }
);
const user = users.find(u => u.UIDorga === orgaUID);
if (user) {
// Add the orgas the user has access to
user.userOrgas = users.map(o => o.UIDorga);
user.isAdmin = await isAdmin(req.session);
// Cache for 15 minutes (user data doesn't change often)
await getRedisClient().setEx(cacheKey, 900, JSON.stringify(user));
return user;
} else {
if (await isAdmin(req.session)) {
// Return system user for this group
const sysUsers = await query(
`SELECT ObjectBase.UID, SysUser.Data, SysUser.Display, ObjectBase.Type, 'extra' AS module
FROM Member AS SysUser
INNER JOIN ObjectBase ON (SysUser.UID=ObjectBase.UIDBelongsTo AND ObjectBase.Type='extern')
INNER JOIN Links ON (ObjectBase.UID= Links.UID AND Links.UIDTarget=? AND Links.Type = 'memberSys')`,
[UUID2hex(orgaUID)],
{ cast: ['json', 'UUID'] }
);
if (sysUsers.length > 0) {
const sysUser = sysUsers[0];
sysUser.userOrgas = [orgaUID];
sysUser.isAdmin = true;
// Cache sysUser too
await getRedisClient().setEx(cacheKey, 900, JSON.stringify(sysUser));
return sysUser;
} else {
console.error('[getUser] No sysuser defined for org:', orgaUID);
return false;
}
} else {
console.error('[getUser] No user found and user not admin');
return false;
}
}
} catch (error) {
console.error('[getUser] Error retrieving user:', error);
throw error;
}
};
/**
* Get fake user data for impersonation (admin only)
* Uses Redis caching to avoid database queries on every API call
* @param {string} fakeUserUID - The UUID of the user to impersonate
* @param {string} orgaUID - The organization UUID
* @returns {Promise<Object|null>} - Fake user object or null if not found
*/
export const getFakeUser = async (fakeUserUID, orgaUID) => {
try {
// Validate UUID format
if (!fakeUserUID?.match(/^UUID-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)) {
return null;
}
// Use cached fake user data (same cache as getUser)
const cacheKey = `user-data-${fakeUserUID}-${orgaUID}`;
const cached = await getRedisClient().get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Cache miss - query database
const fake = await query(
`SELECT Links.UIDTarget AS UID, MUser.Data, MUser.Display, MLink.UIDTarget AS UIDorga FROM Links
INNER JOIN Links AS MLink ON (Links.UIDTarget=MLink.UID AND MLink.Type IN ('member','memberA','memberSys'))
INNER JOIN Member AS MUser ON (MLink.UID=MUser.UID)
WHERE Links.UID=? AND Links.Type='identifyer'
AND MLink.UIDTarget=?`,
[UUID2hex(fakeUserUID), UUID2hex(orgaUID)],
{ cast: ['json', 'UUID'] }
);
if (fake.length > 0) {
const fakeUser = fake[0];
fakeUser.userOrgas = fake.map(o => o.UIDorga);
// Cache for 15 minutes
await getRedisClient().setEx(cacheKey, 900, JSON.stringify(fakeUser));
return fakeUser;
}
return null;
} catch (error) {
console.error('[getFakeUser] Error:', error.message);
return null;
}
};