feat(auth): enhance project access handling with pre-fetched data and new utility functions

This commit is contained in:
Yusuf İpek
2026-04-09 17:17:39 +03:00
parent 173261149f
commit 26cf58a28c
3 changed files with 121 additions and 17 deletions
+102
View File
@@ -90,6 +90,108 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
type ProjectAccessIntent = 'view' | 'manage' | 'delete';
// ---------------------------------------------------------------------------
// Fast-path: pre-fetch access data alongside any existing DB query so that
// computeProjectAccess() can resolve the result with zero extra round-trips.
// ---------------------------------------------------------------------------
/** Prisma include fragment to attach to any project fetch. */
export function projectAccessInclude(userId: string | undefined) {
return {
workspace: {
select: {
id: true,
ownerId: true,
owner: {
select: {
subscriptionStatus: true,
trialEndsAt: true,
stripeCurrentPeriodEnd: true,
billingAccessEndedAt: true,
},
},
members: userId
? { where: { userId }, take: 1, orderBy: { createdAt: 'asc' as const }, select: { role: true } }
: { take: 0, select: { role: true } },
},
},
members: userId
? { where: { userId }, take: 1, orderBy: { createdAt: 'asc' as const }, select: { role: true } }
: { take: 0, select: { role: true } },
};
}
type ProjectAccessIncludes = ReturnType<typeof projectAccessInclude>;
type WorkspaceForAccess = ProjectAccessIncludes['workspace']['select'] extends object
? {
id: string;
ownerId: string;
owner: Parameters<typeof hasBillingAccess>[0] | null;
members: Array<{ role: WorkspaceMemberRole }>;
}
: never;
export type EnrichedProjectForAccess = {
id: string;
ownerId: string;
workspaceId: string;
visibility: string;
workspace: WorkspaceForAccess;
members: Array<{ role: ProjectMemberRole }>;
};
/**
* Pure access computation — no DB queries.
* Use after fetching a project with `projectAccessInclude(userId)`.
*/
export function computeProjectAccess(
project: EnrichedProjectForAccess,
userId: string | undefined,
) {
const isOwner = userId === project.ownerId;
const isPublic = project.visibility === 'PUBLIC';
const projectMember = project.members[0] ?? null;
const isProjectMember = !!projectMember;
const isProjectAdmin = projectMember?.role === ProjectMemberRole.ADMIN;
const workspaceOwnerBillingAccess = project.workspace.owner
? hasBillingAccess(project.workspace.owner)
: false;
let workspaceRole: WorkspaceMemberRole | 'OWNER' | null = null;
if (userId === project.workspace.ownerId) {
workspaceRole = 'OWNER';
} else {
const wsMember = project.workspace.members[0] ?? null;
if (wsMember) workspaceRole = wsMember.role;
}
const isWorkspaceMember = !!workspaceRole;
const isWorkspaceAdmin =
workspaceRole === WorkspaceMemberRole.ADMIN || workspaceRole === 'OWNER';
const hasAccess =
workspaceOwnerBillingAccess &&
(isOwner || isProjectMember || isPublic || isWorkspaceMember);
const canEdit =
workspaceOwnerBillingAccess && (isOwner || isProjectAdmin || isWorkspaceAdmin);
const canDelete =
workspaceOwnerBillingAccess && (isOwner || workspaceRole === 'OWNER');
return {
isOwner,
isProjectMember,
isProjectAdmin,
isWorkspaceMember,
isWorkspaceAdmin,
hasAccess,
canEdit,
canDelete,
ownerBillingActive: workspaceOwnerBillingAccess,
};
}
// Helper to check project access including workspace membership
export async function checkProjectAccess(
project: { id: string; ownerId: string; workspaceId: string; visibility: string },