Skip to content

Commit 388d19e

Browse files
committed
feat: RBAC
1 parent c5d4964 commit 388d19e

File tree

9 files changed

+319
-31
lines changed

9 files changed

+319
-31
lines changed

apps/api/src/controllers/data.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,10 @@ export function dataRoutes(fastify: FastifyInstance) {
5858
fastify.get(
5959
"/api/v1/data/logs",
6060
async (request: FastifyRequest, reply: FastifyReply) => {
61-
const bearer = request.headers.authorization!.split(" ")[1];
62-
const token = checkToken(bearer);
63-
64-
if (token) {
65-
try {
66-
const logs = await import("fs/promises").then((fs) =>
67-
fs.readFile("logs.log", "utf-8")
68-
);
69-
reply.send({ logs: logs });
70-
} catch (error) {
71-
reply.code(500).send({ error: "Failed to read logs file" });
72-
}
73-
}
61+
const logs = await import("fs/promises").then((fs) =>
62+
fs.readFile("logs.log", "utf-8")
63+
);
64+
reply.send({ logs: logs });
7465
}
7566
);
7667
}

apps/api/src/controllers/webhooks.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
22
import { track } from "../lib/hog";
3+
import { requirePermission } from "../lib/roles";
4+
import { checkSession } from "../lib/session";
35
import { prisma } from "../prisma";
46

57
export function webhookRoutes(fastify: FastifyInstance) {
68
// Create a new webhook
79
fastify.post(
810
"/api/v1/webhook/create",
9-
11+
{
12+
preHandler: requirePermission(['webhook:create']),
13+
},
1014
async (request: FastifyRequest, reply: FastifyReply) => {
15+
const user = await checkSession(request);
1116
const { name, url, type, active, secret }: any = request.body;
1217
await prisma.webhooks.create({
1318
data: {
@@ -16,7 +21,7 @@ export function webhookRoutes(fastify: FastifyInstance) {
1621
type,
1722
active,
1823
secret,
19-
createdBy: "375f7799-5485-40ff-ba8f-0a28e0855ecf",
24+
createdBy: user!.id,
2025
},
2126
});
2227

@@ -36,7 +41,9 @@ export function webhookRoutes(fastify: FastifyInstance) {
3641
// Get all webhooks
3742
fastify.get(
3843
"/api/v1/webhooks/all",
39-
44+
{
45+
preHandler: requirePermission(['webhook:read']),
46+
},
4047
async (request: FastifyRequest, reply: FastifyReply) => {
4148
const webhooks = await prisma.webhooks.findMany({});
4249

@@ -45,20 +52,20 @@ export function webhookRoutes(fastify: FastifyInstance) {
4552
);
4653

4754
// Delete a webhook
48-
4955
fastify.delete(
5056
"/api/v1/admin/webhook/:id/delete",
51-
57+
{
58+
preHandler: requirePermission(['webhook:delete']),
59+
},
5260
async (request: FastifyRequest, reply: FastifyReply) => {
53-
const bearer = request.headers.authorization!.split(" ")[1];
54-
const { id }: any = request.params;
55-
await prisma.webhooks.delete({
56-
where: {
57-
id: id,
58-
},
59-
});
61+
const { id }: any = request.params;
62+
await prisma.webhooks.delete({
63+
where: {
64+
id: id,
65+
},
66+
});
6067

61-
reply.status(200).send({ success: true });
68+
reply.status(200).send({ success: true });
6269
}
6370
);
6471
}

apps/api/src/lib/roles.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { prisma, Role, User } from "../prisma";
2+
import { checkSession } from "./session";
3+
import { Permission } from "./types/permissions";
4+
5+
type UserWithRoles = User & {
6+
roles: Role[];
7+
};
8+
9+
export class InsufficientPermissionsError extends Error {
10+
constructor(message: string = "Insufficient permissions") {
11+
super(message);
12+
this.name = "InsufficientPermissionsError";
13+
}
14+
}
15+
16+
/**
17+
* Checks if a user has the required permissions through their roles
18+
* @param user - The user with their roles loaded
19+
* @param requiredPermissions - Single permission or array of permissions to check
20+
* @param requireAll - If true, user must have ALL permissions. If false, only ONE permission is required
21+
* @returns boolean
22+
*/
23+
export function hasPermission(
24+
user: UserWithRoles,
25+
requiredPermissions: Permission | Permission[],
26+
requireAll: boolean = true
27+
): boolean {
28+
// Convert single permission to array for consistent handling
29+
const permissions = Array.isArray(requiredPermissions)
30+
? requiredPermissions
31+
: [requiredPermissions];
32+
33+
// Combine all permissions from user's roles and default role
34+
const userPermissions = new Set<Permission>();
35+
36+
// Add permissions from default role if it exists
37+
const defaultRole = user.roles.find((role) => role.isDefault);
38+
if (defaultRole) {
39+
defaultRole.permissions.forEach((perm) => userPermissions.add(perm as Permission));
40+
}
41+
42+
// Add permissions from additional roles
43+
user.roles.forEach((role) => {
44+
role.permissions.forEach((perm) => userPermissions.add(perm as Permission));
45+
});
46+
47+
if (requireAll) {
48+
// Check if user has ALL required permissions
49+
return permissions.every((permission) => userPermissions.has(permission));
50+
} else {
51+
// Check if user has AT LEAST ONE of the required permissions
52+
return permissions.some((permission) => userPermissions.has(permission));
53+
}
54+
}
55+
56+
/**
57+
* Authorization middleware that checks for required permissions
58+
* @param requiredPermissions - Single permission or array of permissions to check
59+
* @param requireAll - If true, user must have ALL permissions. If false, only ONE permission is required
60+
*/
61+
export function requirePermission(
62+
requiredPermissions: Permission | Permission[],
63+
requireAll: boolean = true
64+
) {
65+
return async (req: any, res: any, next: any) => {
66+
try {
67+
const user = await checkSession(req);
68+
const config = await prisma.config.findFirst();
69+
70+
if (!config?.roles_active) {
71+
next();
72+
}
73+
74+
const userWithRoles = user
75+
? await prisma.user.findUnique({
76+
where: { id: user.id },
77+
include: {
78+
roles: true,
79+
},
80+
})
81+
: null;
82+
83+
if (!userWithRoles) {
84+
throw new Error("User not authenticated");
85+
}
86+
87+
if (!hasPermission(userWithRoles, requiredPermissions, requireAll)) {
88+
throw new InsufficientPermissionsError();
89+
}
90+
91+
next();
92+
} catch (error) {
93+
next(error);
94+
}
95+
};
96+
}
97+
98+
// Usage examples:
99+
/*
100+
// Check single permission
101+
if (hasPermission(user, 'issue::create')) {
102+
// Allow create ticket
103+
}
104+
105+
// Check multiple permissions (all required)
106+
if (hasPermission(user, ['issue:update', 'issue:assign'])) {
107+
// Allow ticket update and assignment
108+
}
109+
110+
// Check multiple permissions (at least one required)
111+
if (hasPermission(user, ['user:manage', 'role:manage'], false)) {
112+
// Allow access if user has either permission
113+
}
114+
115+
// Use as middleware
116+
router.post('/tickets',
117+
requirePermission('issue::create'),
118+
ticketController.create
119+
);
120+
121+
// Use as middleware with multiple permissions
122+
router.put('/tickets/:id/assign',
123+
requirePermission(['issue:update', 'issue:assign']),
124+
ticketController.assign
125+
);
126+
*/
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
export type IssuePermission =
2+
| 'issue:create'
3+
| 'issue:read'
4+
| 'issue:write'
5+
| 'issue:update'
6+
| 'issue:delete'
7+
| 'issue:assign'
8+
| 'issue:transfer'
9+
| 'issue:comment';
10+
11+
export type UserPermission =
12+
| 'user:create'
13+
| 'user:read'
14+
| 'user:update'
15+
| 'user:delete'
16+
| 'user:manage';
17+
18+
export type RolePermission =
19+
| 'role:create'
20+
| 'role:read'
21+
| 'role:update'
22+
| 'role:delete'
23+
| 'role:manage';
24+
25+
export type TeamPermission =
26+
| 'team:create'
27+
| 'team:read'
28+
| 'team:update'
29+
| 'team:delete'
30+
| 'team:manage';
31+
32+
export type ClientPermission =
33+
| 'client:create'
34+
| 'client:read'
35+
| 'client:update'
36+
| 'client:delete'
37+
| 'client:manage';
38+
39+
export type KnowledgeBasePermission =
40+
| 'kb:create'
41+
| 'kb:read'
42+
| 'kb:update'
43+
| 'kb:delete'
44+
| 'kb:manage';
45+
46+
export type SystemPermission =
47+
| 'settings:view'
48+
| 'settings:manage'
49+
| 'webhook:manage'
50+
| 'integration:manage'
51+
| 'email_template:manage';
52+
53+
export type TimeTrackingPermission =
54+
| 'time_entry:create'
55+
| 'time_entry:read'
56+
| 'time_entry:update'
57+
| 'time_entry:delete';
58+
59+
export type ViewPermission =
60+
| 'docs:manage'
61+
| 'admin:panel';
62+
63+
export type WebhookPermission =
64+
| 'webhook:create'
65+
| 'webhook:read'
66+
| 'webhook:update'
67+
| 'webhook:delete';
68+
69+
export type Permission =
70+
| IssuePermission
71+
| UserPermission
72+
| RolePermission
73+
| TeamPermission
74+
| ClientPermission
75+
| KnowledgeBasePermission
76+
| SystemPermission
77+
| TimeTrackingPermission
78+
| ViewPermission
79+
| WebhookPermission;
80+
81+
// Useful type for grouping permissions by category
82+
export const PermissionCategories = {
83+
ISSUE: 'Issue Management',
84+
USER: 'User Management',
85+
ROLE: 'Role Management',
86+
TEAM: 'Team Management',
87+
CLIENT: 'Client Management',
88+
KNOWLEDGE_BASE: 'Knowledge Base',
89+
SYSTEM: 'System Settings',
90+
TIME_TRACKING: 'Time Tracking',
91+
VIEW: 'Views',
92+
WEBHOOK: 'Webhook Management',
93+
} as const;
94+
95+
export type PermissionCategory = typeof PermissionCategories[keyof typeof PermissionCategories];
96+
97+
// Helper type for permission groups
98+
export interface PermissionGroup {
99+
category: PermissionCategory;
100+
permissions: Permission[];
101+
}

apps/api/src/prisma.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { PrismaClient } from "@prisma/client";
1+
import { Hook, Permission, PrismaClient, Role, User } from "@prisma/client";
22
export const prisma: PrismaClient = new PrismaClient();
3-
export type Hook = "ticket_created" | "ticket_status_changed";
3+
export { Hook, Permission, Role, User };
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
-- CreateEnum
2+
CREATE TYPE "Permission" AS ENUM ('CREATE_TICKET', 'READ_TICKET', 'WRITE_TICKET', 'UPDATE_TICKET', 'DELETE_TICKET', 'ASSIGN_TICKET', 'TRANSFER_TICKET', 'COMMENT_TICKET', 'CREATE_USER', 'READ_USER', 'UPDATE_USER', 'DELETE_USER', 'MANAGE_USERS', 'CREATE_ROLE', 'READ_ROLE', 'UPDATE_ROLE', 'DELETE_ROLE', 'MANAGE_ROLES', 'CREATE_TEAM', 'READ_TEAM', 'UPDATE_TEAM', 'DELETE_TEAM', 'MANAGE_TEAMS', 'CREATE_CLIENT', 'READ_CLIENT', 'UPDATE_CLIENT', 'DELETE_CLIENT', 'MANAGE_CLIENTS', 'CREATE_KB', 'READ_KB', 'UPDATE_KB', 'DELETE_KB', 'MANAGE_KB', 'VIEW_REPORTS', 'MANAGE_SETTINGS', 'MANAGE_WEBHOOKS', 'MANAGE_INTEGRATIONS', 'MANAGE_EMAIL_TEMPLATES', 'CREATE_TIME_ENTRY', 'READ_TIME_ENTRY', 'UPDATE_TIME_ENTRY', 'DELETE_TIME_ENTRY', 'MANAGE_DOCS', 'ADMIN_PANEL');
3+
4+
-- CreateTable
5+
CREATE TABLE "Role" (
6+
"id" TEXT NOT NULL,
7+
"name" TEXT NOT NULL,
8+
"description" TEXT,
9+
"permissions" "Permission"[],
10+
"isDefault" BOOLEAN NOT NULL DEFAULT false,
11+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12+
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
13+
14+
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
15+
);
16+
17+
-- CreateTable
18+
CREATE TABLE "_RoleToUser" (
19+
"A" TEXT NOT NULL,
20+
"B" TEXT NOT NULL
21+
);
22+
23+
-- CreateIndex
24+
CREATE UNIQUE INDEX "Role_name_key" ON "Role"("name");
25+
26+
-- CreateIndex
27+
CREATE UNIQUE INDEX "_RoleToUser_AB_unique" ON "_RoleToUser"("A", "B");
28+
29+
-- CreateIndex
30+
CREATE INDEX "_RoleToUser_B_index" ON "_RoleToUser"("B");
31+
32+
-- AddForeignKey
33+
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE;
34+
35+
-- AddForeignKey
36+
ALTER TABLE "_RoleToUser" ADD CONSTRAINT "_RoleToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "Config" ADD COLUMN "roles_active" BOOLEAN NOT NULL DEFAULT false;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
Warnings:
3+
4+
- The `permissions` column on the `Role` table would be dropped and recreated. This will lead to data loss if there is data in the column.
5+
6+
*/
7+
-- AlterTable
8+
ALTER TABLE "Role" DROP COLUMN "permissions",
9+
ADD COLUMN "permissions" JSONB[];
10+
11+
-- DropEnum
12+
DROP TYPE "Permission";

0 commit comments

Comments
 (0)