feat: setup basic router interactions
This commit is contained in:
@@ -9,6 +9,7 @@ import { integrationRouter } from "./routers/integration";
|
|||||||
import { labelRouter } from "./routers/label";
|
import { labelRouter } from "./routers/label";
|
||||||
import { listRouter } from "./routers/list";
|
import { listRouter } from "./routers/list";
|
||||||
import { memberRouter } from "./routers/member";
|
import { memberRouter } from "./routers/member";
|
||||||
|
import { permissionRouter } from "./routers/permission";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
import { workspaceRouter } from "./routers/workspace";
|
import { workspaceRouter } from "./routers/workspace";
|
||||||
import { createTRPCRouter } from "./trpc";
|
import { createTRPCRouter } from "./trpc";
|
||||||
@@ -24,6 +25,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
list: listRouter,
|
list: listRouter,
|
||||||
member: memberRouter,
|
member: memberRouter,
|
||||||
import: importRouter,
|
import: importRouter,
|
||||||
|
permission: permissionRouter,
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
workspace: workspaceRouter,
|
workspace: workspaceRouter,
|
||||||
integration: integrationRouter,
|
integration: integrationRouter,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import * as inviteLinkRepo from "@kan/db/repository/inviteLink.repo";
|
import * as inviteLinkRepo from "@kan/db/repository/inviteLink.repo";
|
||||||
import * as memberRepo from "@kan/db/repository/member.repo";
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||||
|
import * as permissionRepo from "@kan/db/repository/permission.repo";
|
||||||
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
|
||||||
import * as userRepo from "@kan/db/repository/user.repo";
|
import * as userRepo from "@kan/db/repository/user.repo";
|
||||||
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
@@ -11,11 +12,16 @@ import {
|
|||||||
generateUID,
|
generateUID,
|
||||||
getSubscriptionByPlan,
|
getSubscriptionByPlan,
|
||||||
hasUnlimitedSeats,
|
hasUnlimitedSeats,
|
||||||
} from "@kan/shared/utils";
|
} from "@kan/shared";
|
||||||
import { updateSubscriptionSeats } from "@kan/stripe";
|
import { updateSubscriptionSeats } from "@kan/stripe";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||||
import { assertUserInWorkspace } from "../utils/auth";
|
import { assertUserInWorkspace } from "../utils/auth";
|
||||||
|
import {
|
||||||
|
assertCanManageMember,
|
||||||
|
assertCanManageRole,
|
||||||
|
assertPermission,
|
||||||
|
} from "../utils/permissions";
|
||||||
|
|
||||||
export const memberRouter = createTRPCRouter({
|
export const memberRouter = createTRPCRouter({
|
||||||
invite: protectedProcedure
|
invite: protectedProcedure
|
||||||
@@ -646,4 +652,85 @@ export const memberRouter = createTRPCRouter({
|
|||||||
workspaceSlug: workspace.slug,
|
workspaceSlug: workspace.slug,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
updateRole: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Update member role",
|
||||||
|
method: "PUT",
|
||||||
|
path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/role",
|
||||||
|
description: "Updates a member's role in a workspace",
|
||||||
|
tags: ["Workspaces"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspacePublicId: z.string().min(12),
|
||||||
|
memberPublicId: z.string().min(12),
|
||||||
|
role: z.enum(["admin", "member", "guest"]),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
role: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "User not authenticated",
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.workspacePublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Workspace not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertPermission(ctx.db, userId, workspace.id, "member:edit");
|
||||||
|
|
||||||
|
const member = await memberRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.memberPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Member not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertCanManageMember(ctx.db, userId, workspace.id, member.id);
|
||||||
|
await assertCanManageRole(ctx.db, userId, workspace.id, input.role);
|
||||||
|
|
||||||
|
// Get the workspace role to set roleId
|
||||||
|
const workspaceRole = await permissionRepo.getRoleByWorkspaceIdAndName(
|
||||||
|
ctx.db,
|
||||||
|
workspace.id,
|
||||||
|
input.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
await memberRepo.updateRole(ctx.db, {
|
||||||
|
memberId: member.id,
|
||||||
|
role: input.role,
|
||||||
|
roleId: workspaceRole?.id ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
role: input.role,
|
||||||
|
};
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
296
packages/api/src/routers/permission.ts
Normal file
296
packages/api/src/routers/permission.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||||
|
import * as permissionRepo from "@kan/db/repository/permission.repo";
|
||||||
|
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
|
||||||
|
import type { Permission } from "@kan/shared";
|
||||||
|
import {
|
||||||
|
allPermissions,
|
||||||
|
} from "@kan/shared";
|
||||||
|
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
import {
|
||||||
|
assertCanManageMember,
|
||||||
|
assertPermission,
|
||||||
|
getMemberEffectivePermissions,
|
||||||
|
getUserPermissions,
|
||||||
|
} from "../utils/permissions";
|
||||||
|
|
||||||
|
const permissionsList = [...allPermissions] as [string, ...string[]];
|
||||||
|
|
||||||
|
export const permissionRouter = createTRPCRouter({
|
||||||
|
getMyPermissions: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Get my permissions",
|
||||||
|
method: "GET",
|
||||||
|
path: "/workspaces/{workspacePublicId}/permissions/me",
|
||||||
|
description: "Get the current user's permissions in a workspace",
|
||||||
|
tags: ["Permissions"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspacePublicId: z.string().min(12),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
permissions: z.array(z.string()),
|
||||||
|
role: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "User not authenticated",
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.workspacePublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Workspace not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getUserPermissions(ctx.db, userId, workspace.id);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "You are not a member of this workspace",
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}),
|
||||||
|
getMemberPermissions: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Get member permissions",
|
||||||
|
method: "GET",
|
||||||
|
path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions",
|
||||||
|
description: "Get a specific member's permissions in a workspace",
|
||||||
|
tags: ["Permissions"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspacePublicId: z.string().min(12),
|
||||||
|
memberPublicId: z.string().min(12),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
memberPublicId: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
permissions: z.array(z.string()),
|
||||||
|
overrides: z.array(
|
||||||
|
z.object({
|
||||||
|
permission: z.string(),
|
||||||
|
granted: z.boolean(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "User not authenticated",
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.workspacePublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Workspace not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user has permission to view member permissions
|
||||||
|
await assertPermission(ctx.db, userId, workspace.id, "member:view");
|
||||||
|
|
||||||
|
const member = await memberRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.memberPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Member not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectivePermissions = await getMemberEffectivePermissions(
|
||||||
|
ctx.db,
|
||||||
|
member.id,
|
||||||
|
member.roleId ?? null,
|
||||||
|
member.role,
|
||||||
|
);
|
||||||
|
const overrides = await permissionRepo.getMemberPermissionOverrides(
|
||||||
|
ctx.db,
|
||||||
|
member.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
memberPublicId: member.publicId,
|
||||||
|
role: member.role,
|
||||||
|
permissions: effectivePermissions,
|
||||||
|
overrides: overrides.map((o) => ({
|
||||||
|
permission: o.permission,
|
||||||
|
granted: o.granted,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
grantPermission: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Grant permission to member",
|
||||||
|
method: "POST",
|
||||||
|
path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions/grant",
|
||||||
|
description: "Grant a specific permission to a member",
|
||||||
|
tags: ["Permissions"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspacePublicId: z.string().min(12),
|
||||||
|
memberPublicId: z.string().min(12),
|
||||||
|
permission: z.enum(permissionsList),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "User not authenticated",
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.workspacePublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Workspace not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertPermission(ctx.db, userId, workspace.id, "member:edit");
|
||||||
|
|
||||||
|
const member = await memberRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.memberPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Member not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertCanManageMember(ctx.db, userId, workspace.id, member.id);
|
||||||
|
|
||||||
|
await permissionRepo.grantPermission(
|
||||||
|
ctx.db,
|
||||||
|
member.id,
|
||||||
|
input.permission as Permission,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
revokePermission: protectedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
summary: "Revoke permission from member",
|
||||||
|
method: "POST",
|
||||||
|
path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions/revoke",
|
||||||
|
description: "Revoke a specific permission from a member",
|
||||||
|
tags: ["Permissions"],
|
||||||
|
protect: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspacePublicId: z.string().min(12),
|
||||||
|
memberPublicId: z.string().min(12),
|
||||||
|
permission: z.enum(permissionsList),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(z.object({ success: z.boolean() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "User not authenticated",
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspace = await workspaceRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.workspacePublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspace) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Workspace not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertPermission(ctx.db, userId, workspace.id, "member:edit");
|
||||||
|
|
||||||
|
const member = await memberRepo.getByPublicId(
|
||||||
|
ctx.db,
|
||||||
|
input.memberPublicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Member not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await assertCanManageMember(ctx.db, userId, workspace.id, member.id);
|
||||||
|
|
||||||
|
await permissionRepo.revokePermission(
|
||||||
|
ctx.db,
|
||||||
|
member.id,
|
||||||
|
input.permission as Permission,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
});
|
||||||
235
packages/api/src/utils/permissions.ts
Normal file
235
packages/api/src/utils/permissions.ts
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import type { dbClient } from "@kan/db/client";
|
||||||
|
import * as memberRepo from "@kan/db/repository/member.repo";
|
||||||
|
import * as permissionRepo from "@kan/db/repository/permission.repo";
|
||||||
|
import type { Permission, Role } from "@kan/shared";
|
||||||
|
import { canManageRole, getDefaultPermissions } from "@kan/shared";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get effective permissions for a member by combining role permissions with overrides
|
||||||
|
*/
|
||||||
|
export async function getMemberEffectivePermissions(
|
||||||
|
db: dbClient,
|
||||||
|
workspaceMemberId: number,
|
||||||
|
roleId: number | null,
|
||||||
|
roleName: string,
|
||||||
|
): Promise<Permission[]> {
|
||||||
|
let roleDefaults: Set<Permission>;
|
||||||
|
|
||||||
|
// Get role permissions from database or fallback to code defaults
|
||||||
|
if (roleId) {
|
||||||
|
const dbPermissions = await permissionRepo.getPermissionsByRoleId(
|
||||||
|
db,
|
||||||
|
roleId,
|
||||||
|
);
|
||||||
|
roleDefaults = new Set<Permission>(dbPermissions);
|
||||||
|
} else {
|
||||||
|
const codeDefaults = getDefaultPermissions(roleName as Role);
|
||||||
|
roleDefaults = new Set<Permission>([...codeDefaults]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and apply custom overrides
|
||||||
|
const overrides = await permissionRepo.getMemberPermissionOverrides(
|
||||||
|
db,
|
||||||
|
workspaceMemberId,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const override of overrides) {
|
||||||
|
if (override.granted) {
|
||||||
|
roleDefaults.add(override.permission as Permission);
|
||||||
|
} else {
|
||||||
|
roleDefaults.delete(override.permission as Permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(roleDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a member has a specific permission
|
||||||
|
*/
|
||||||
|
export async function memberHasPermission(
|
||||||
|
db: dbClient,
|
||||||
|
workspaceMemberId: number,
|
||||||
|
roleId: number | null,
|
||||||
|
roleName: string,
|
||||||
|
permission: Permission,
|
||||||
|
): Promise<boolean> {
|
||||||
|
let hasRoleDefault: boolean;
|
||||||
|
|
||||||
|
// Check role permission from database or fallback to code defaults
|
||||||
|
if (roleId) {
|
||||||
|
const dbPermissions = await permissionRepo.getPermissionsByRoleId(
|
||||||
|
db,
|
||||||
|
roleId,
|
||||||
|
);
|
||||||
|
hasRoleDefault = dbPermissions.includes(permission);
|
||||||
|
} else {
|
||||||
|
const codeDefaults = getDefaultPermissions(roleName as Role);
|
||||||
|
hasRoleDefault = codeDefaults.includes(permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for override
|
||||||
|
const override = await permissionRepo.getMemberPermissionOverride(
|
||||||
|
db,
|
||||||
|
workspaceMemberId,
|
||||||
|
permission,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Override takes precedence
|
||||||
|
if (override) {
|
||||||
|
return override.granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasRoleDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if user has a specific permission in a workspace
|
||||||
|
*/
|
||||||
|
export async function hasPermission(
|
||||||
|
db: dbClient,
|
||||||
|
userId: string,
|
||||||
|
workspaceId: number,
|
||||||
|
permission: Permission,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const member = await permissionRepo.getMemberWithRole(db, userId, workspaceId);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberHasPermission(
|
||||||
|
db,
|
||||||
|
member.id,
|
||||||
|
member.roleId,
|
||||||
|
member.role,
|
||||||
|
permission,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all permissions for a user in a workspace
|
||||||
|
*/
|
||||||
|
export async function getUserPermissions(
|
||||||
|
db: dbClient,
|
||||||
|
userId: string,
|
||||||
|
workspaceId: number,
|
||||||
|
): Promise<{
|
||||||
|
permissions: Permission[];
|
||||||
|
role: string;
|
||||||
|
roleId: number | null;
|
||||||
|
} | null> {
|
||||||
|
const member = await permissionRepo.getMemberWithRole(db, userId, workspaceId);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await getMemberEffectivePermissions(
|
||||||
|
db,
|
||||||
|
member.id,
|
||||||
|
member.roleId,
|
||||||
|
member.role,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
permissions,
|
||||||
|
role: member.role,
|
||||||
|
roleId: member.roleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert user has permission - throws FORBIDDEN if not
|
||||||
|
*/
|
||||||
|
export async function assertPermission(
|
||||||
|
db: dbClient,
|
||||||
|
userId: string,
|
||||||
|
workspaceId: number,
|
||||||
|
permission: Permission,
|
||||||
|
): Promise<void> {
|
||||||
|
const hasIt = await hasPermission(db, userId, workspaceId, permission);
|
||||||
|
|
||||||
|
if (!hasIt) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `You do not have permission to perform this action (${permission})`,
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert user can assign a specific role (based on hierarchy)
|
||||||
|
*/
|
||||||
|
export async function assertCanManageRole(
|
||||||
|
db: dbClient,
|
||||||
|
managerUserId: string,
|
||||||
|
workspaceId: number,
|
||||||
|
targetRoleName: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const managerMember = await permissionRepo.getMemberWithRole(
|
||||||
|
db,
|
||||||
|
managerUserId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!managerMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "You are not a member of this workspace",
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const managerRole = managerMember.role as Role;
|
||||||
|
|
||||||
|
if (!canManageRole(managerRole, targetRoleName as Role)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: `You cannot assign the "${targetRoleName}" role`,
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert user can manage another member based on role hierarchy
|
||||||
|
*/
|
||||||
|
export async function assertCanManageMember(
|
||||||
|
db: dbClient,
|
||||||
|
managerUserId: string,
|
||||||
|
workspaceId: number,
|
||||||
|
targetMemberId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const managerMember = await permissionRepo.getMemberWithRole(
|
||||||
|
db,
|
||||||
|
managerUserId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!managerMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "You are not a member of this workspace",
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMember = await memberRepo.getById(db, targetMemberId);
|
||||||
|
|
||||||
|
if (!targetMember) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "Target member not found",
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const managerRole = managerMember.role as Role;
|
||||||
|
const targetRole = targetMember.role as Role;
|
||||||
|
|
||||||
|
if (!canManageRole(managerRole, targetRole)) {
|
||||||
|
throw new TRPCError({
|
||||||
|
message: "You cannot manage this member due to role hierarchy",
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ export const workspaceRoles = pgTable(
|
|||||||
"workspace_roles",
|
"workspace_roles",
|
||||||
{
|
{
|
||||||
id: bigserial("id", { mode: "number" }).primaryKey(),
|
id: bigserial("id", { mode: "number" }).primaryKey(),
|
||||||
|
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
|
||||||
workspaceId: bigint("workspaceId", { mode: "number" })
|
workspaceId: bigint("workspaceId", { mode: "number" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => workspaces.id, { onDelete: "cascade" }),
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export const name = "shared";
|
|||||||
|
|
||||||
export * from "./constants";
|
export * from "./constants";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
|
export * from "./permissions";
|
||||||
|
|||||||
Reference in New Issue
Block a user