Midday / apps /api /src /trpc /middleware /team-permission.ts
Jules
Final deployment with all fixes and verified content
c09f67c
import type { Session } from "@api/utils/auth";
import { withRetryOnPrimary } from "@api/utils/db-retry";
import { teamCache } from "@midday/cache/team-cache";
import type { Database } from "@midday/db/client";
import { TRPCError } from "@trpc/server";
export const withTeamPermission = async <TReturn>(opts: {
ctx: {
session?: Session | null;
db: Database;
};
next: (opts: {
ctx: {
session?: Session | null;
db: Database;
teamId: string | null;
};
}) => Promise<TReturn>;
}) => {
const { ctx, next } = opts;
const userId = ctx.session?.user?.id;
if (!userId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "No permission to access this team",
});
}
// Try replica first (fast path), fallback to primary on failure
// This preserves the benefit of fast replicas while handling replication lag gracefully
// retryOnNull: true ensures we check primary if replica returns null (replication lag)
const result = await withRetryOnPrimary(
ctx.db,
async (db) => {
return await db.query.users.findFirst({
with: {
usersOnTeams: {
columns: {
id: true,
teamId: true,
},
},
},
where: (users, { eq }) => eq(users.id, userId),
});
},
{ retryOnNull: true },
);
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const teamId = result.teamId;
// If teamId is null, user has no team assigned but this is now allowed
if (teamId !== null) {
const cacheKey = `user:${userId}:team:${teamId}`;
let hasAccess = await teamCache.get(cacheKey);
if (hasAccess === undefined) {
hasAccess = result.usersOnTeams.some(
(membership) => membership.teamId === teamId,
);
await teamCache.set(cacheKey, hasAccess);
}
if (!hasAccess) {
throw new TRPCError({
code: "FORBIDDEN",
message: "No permission to access this team",
});
}
}
return next({
ctx: {
session: ctx.session,
teamId,
db: ctx.db,
},
});
};