chore: move orgRoles from session to Redis

This commit is contained in:
Rotimi Best
2026-05-20 11:52:49 +01:00
parent 31f8636e7e
commit f7d6f81901
7 changed files with 50 additions and 28 deletions
-3
View File
@@ -51,7 +51,6 @@ export const app = new Hono()
if (isPublicCorsPath(c.req.path)) {
c.set('user', null);
c.set('session', null);
c.set('orgRoles', {});
return next();
}
@@ -67,7 +66,6 @@ export const app = new Hono()
if (!session) {
c.set('user', null);
c.set('session', null);
c.set('orgRoles', {});
await next();
return;
@@ -75,7 +73,6 @@ export const app = new Hono()
c.set('user', session.user);
c.set('session', session.session);
c.set('orgRoles', (session as { orgRoles?: Record<string, number> }).orgRoles ?? {});
await next();
})
@@ -3,6 +3,7 @@ import type { TOrganizationApiKeyScope } from '@cio/utils/validation/organizatio
import { ErrorCodes } from '@api/utils/errors';
import { ROLE } from '@cio/utils/constants';
import { getOrgRolesForUser } from '@api/utils/redis/org-roles-cache';
import { organizationApiKeyHasScopes } from '@api/services/organization/automation-key';
export const orgAdminOrAutomationKeyMiddleware =
@@ -55,7 +56,7 @@ export const orgAdminOrAutomationKeyMiddleware =
);
}
const orgRoles = (c.get('orgRoles') as Record<string, number> | undefined) ?? {};
const orgRoles = await getOrgRolesForUser(user.id);
if (orgRoles[orgId] !== ROLE.ADMIN) {
return c.json(
{
+3 -3
View File
@@ -2,11 +2,11 @@ import { Context, Next } from 'hono';
import { ErrorCodes } from '@api/utils/errors';
import { ROLE } from '@cio/utils/constants';
import { getOrgRolesForUser } from '@api/utils/redis/org-roles-cache';
/**
* Middleware to check if the authenticated user is an admin of the organization.
* Reads the role from the Better Auth session (populated by the customSession plugin),
* so no per-request DB query is performed.
* Reads `orgRoles` from the Redis-backed cache (DB fallback on miss).
*
* Requires authMiddleware to be applied first.
* Expects orgId in the 'cio-org-id' header.
@@ -37,7 +37,7 @@ export const orgAdminMiddleware = async (c: Context, next: Next) => {
);
}
const orgRoles = (c.get('orgRoles') as Record<string, number> | undefined) ?? {};
const orgRoles = await getOrgRolesForUser(user.id);
const roleId = orgRoles[orgId];
if (roleId !== ROLE.ADMIN) {
+5 -3
View File
@@ -1,9 +1,11 @@
import { Context, Next } from 'hono';
import { getOrgRolesForUser } from '@api/utils/redis/org-roles-cache';
/**
* Middleware to check if the authenticated user is a member of the organization.
* Reads the role from the Better Auth session (populated by the customSession plugin),
* so no per-request DB query is performed.
* Loads `orgRoles` from the Redis-backed cache (DB fallback on miss) so the
* lookup stays out of the Better Auth session cookie — keeping that cookie
* small enough for the oauth-proxy redirect to fit in a Location header.
*
* Requires authMiddleware to be applied first.
* Expects orgId in the 'cio-org-id' header.
@@ -35,7 +37,7 @@ export const orgMemberMiddleware = async (c: Context, next: Next) => {
);
}
const orgRoles = (c.get('orgRoles') as Record<string, number> | undefined) ?? {};
const orgRoles = await getOrgRolesForUser(user.id);
const roleId = orgRoles[orgId];
if (roleId === undefined) {
-1
View File
@@ -6,7 +6,6 @@ export type AuthSession = {
actorId: string | null;
automationKey: TOrganizationApiKey | null;
orgId: string | null;
orgRoles: Record<string, number>;
session: typeof auth.$Infer.Session.session | null;
user: typeof auth.$Infer.Session.user | null;
userRole: number | null;
@@ -0,0 +1,39 @@
import { getUserOrgRolesMap } from '@cio/db/queries/organization';
import { logRedisUnavailableOnce, redis } from './redis';
const TTL_SECONDS = 60 * 60; // 1 hour, matches the prior cookieCache.maxAge.
const cacheKey = (userId: string) => `org-roles:${userId}`;
export async function getOrgRolesForUser(userId: string): Promise<Record<string, number>> {
try {
const cached = await redis.get(cacheKey(userId));
if (cached) {
try {
return JSON.parse(cached) as Record<string, number>;
} catch {
// Stale/corrupt entry — fall through to DB.
}
}
} catch (error) {
logRedisUnavailableOnce('Redis GET org-roles failed, falling back to DB', error);
}
const orgRoles = await getUserOrgRolesMap(userId);
try {
await redis.set(cacheKey(userId), JSON.stringify(orgRoles), { EX: TTL_SECONDS });
} catch (error) {
logRedisUnavailableOnce('Redis SET org-roles failed, cache disabled', error);
}
return orgRoles;
}
export async function invalidateOrgRolesCache(userId: string): Promise<void> {
try {
await redis.del(cacheKey(userId));
} catch (error) {
logRedisUnavailableOnce('Redis DEL org-roles failed, falling back to TTL', error);
}
}