diff --git a/apps/api/src/controllers/data.ts b/apps/api/src/controllers/data.ts index 88fea0404..b75c3abe2 100644 --- a/apps/api/src/controllers/data.ts +++ b/apps/api/src/controllers/data.ts @@ -1,11 +1,14 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { requirePermission } from "../lib/roles"; import { prisma } from "../prisma"; export function dataRoutes(fastify: FastifyInstance) { // Get total count of all tickets fastify.get( "/api/v1/data/tickets/all", - + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const result = await prisma.ticket.count({ where: { hidden: false }, @@ -18,7 +21,9 @@ export function dataRoutes(fastify: FastifyInstance) { // Get total count of all completed tickets fastify.get( "/api/v1/data/tickets/completed", - + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const result = await prisma.ticket.count({ where: { isComplete: true, hidden: false }, @@ -31,7 +36,9 @@ export function dataRoutes(fastify: FastifyInstance) { // Get total count of all open tickets fastify.get( "/api/v1/data/tickets/open", - + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const result = await prisma.ticket.count({ where: { isComplete: false, hidden: false }, @@ -44,7 +51,9 @@ export function dataRoutes(fastify: FastifyInstance) { // Get total of all unsassigned tickets fastify.get( "/api/v1/data/tickets/unassigned", - + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const result = await prisma.ticket.count({ where: { userId: null, hidden: false, isComplete: false }, diff --git a/apps/api/src/controllers/notebook.ts b/apps/api/src/controllers/notebook.ts index 71aa66e50..378dfe575 100644 --- a/apps/api/src/controllers/notebook.ts +++ b/apps/api/src/controllers/notebook.ts @@ -1,5 +1,6 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { track } from "../lib/hog"; +import { requirePermission } from "../lib/roles"; import { checkSession } from "../lib/session"; import { prisma } from "../prisma"; @@ -19,7 +20,9 @@ export function notebookRoutes(fastify: FastifyInstance) { // Create a new entry fastify.post( "/api/v1/notebook/note/create", - + { + preHandler: requirePermission(["document::create"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const { content, title }: any = request.body; const user = await checkSession(request); @@ -43,7 +46,9 @@ export function notebookRoutes(fastify: FastifyInstance) { // Get all entries fastify.get( "/api/v1/notebooks/all", - + { + preHandler: requirePermission(["document::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const user = await checkSession(request); @@ -58,7 +63,9 @@ export function notebookRoutes(fastify: FastifyInstance) { // Get a single entry fastify.get( "/api/v1/notebooks/note/:id", - + { + preHandler: requirePermission(["document::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const user = await checkSession(request); @@ -75,14 +82,17 @@ export function notebookRoutes(fastify: FastifyInstance) { // Delete an entry fastify.delete( "/api/v1/notebooks/note/:id", + { + preHandler: requirePermission(["document::delete"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const user = await checkSession(request); const { id }: any = request.params; await prisma.notes.delete({ - where: { + where: { id: id, - userId: user!.id + userId: user!.id, }, }); @@ -95,15 +105,18 @@ export function notebookRoutes(fastify: FastifyInstance) { // Update an entry fastify.put( "/api/v1/notebooks/note/:id/update", + { + preHandler: requirePermission(["document::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const user = await checkSession(request); const { id }: any = request.params; const { content, title }: any = request.body; await prisma.notes.update({ - where: { + where: { id: id, - userId: user!.id + userId: user!.id, }, data: { title: title, diff --git a/apps/api/src/controllers/roles.ts b/apps/api/src/controllers/roles.ts index 02549144d..a277293f2 100644 --- a/apps/api/src/controllers/roles.ts +++ b/apps/api/src/controllers/roles.ts @@ -98,7 +98,7 @@ export function roleRoutes(fastify: FastifyInstance) { }, async (request: FastifyRequest, reply: FastifyReply) => { const { id }: any = request.params; - const { name, description, permissions, isDefault }: any = request.body; + const { name, description, permissions, isDefault, users }: any = request.body; try { const updatedRole = await prisma.role.update({ @@ -109,6 +109,9 @@ export function roleRoutes(fastify: FastifyInstance) { permissions, isDefault, updatedAt: new Date(), + users: { + set: Array.isArray(users) ? users.map(userId => ({ id: userId })) : [{ id: users }], // Ensure users is an array of objects with unique IDs when updating + }, }, }); @@ -156,7 +159,7 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.post( "/api/v1/role/assign", { - // preHandler: requirePermission(['role::assign']), + preHandler: requirePermission(['role::update']), }, async (request: FastifyRequest, reply: FastifyReply) => { const { userId, roleId }: any = request.body; diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index 31fba5626..018b8fe34 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -12,6 +12,7 @@ import { sendTicketStatus } from "../lib/nodemailer/ticket/status"; import { assignedNotification } from "../lib/notifications/issue/assigned"; import { commentNotification } from "../lib/notifications/issue/comment"; import { sendWebhookNotification } from "../lib/notifications/webhook"; +import { requirePermission } from "../lib/roles"; import { checkSession } from "../lib/session"; import { prisma } from "../prisma"; @@ -24,10 +25,12 @@ const validateEmail = (email: string) => { }; export function ticketRoutes(fastify: FastifyInstance) { - // Create a new ticket + // Create a new ticket - public endpoint, no preHandler needed fastify.post( "/api/v1/ticket/create", - + { + preHandler: requirePermission(["issue::create"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const { name, @@ -130,137 +133,130 @@ export function ticketRoutes(fastify: FastifyInstance) { } ); - // Get a ticket by id + // Get a ticket by id - requires auth fastify.get( "/api/v1/ticket/:id", - + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { id }: any = request.params; - if (token) { - const ticket = await prisma.ticket.findUnique({ - where: { - id: id, + const ticket = await prisma.ticket.findUnique({ + where: { + id: id, + }, + include: { + client: { + select: { id: true, name: true, number: true, notes: true }, }, - include: { - client: { - select: { id: true, name: true, number: true, notes: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, + assignedTo: { + select: { id: true, name: true }, }, - }); + }, + }); - const timeTracking = await prisma.timeTracking.findMany({ - where: { - ticketId: id, - }, - include: { - user: { - select: { - name: true, - }, + const timeTracking = await prisma.timeTracking.findMany({ + where: { + ticketId: id, + }, + include: { + user: { + select: { + name: true, }, }, - }); + }, + }); - const comments = await prisma.comment.findMany({ - where: { - ticketId: ticket!.id, - }, - include: { - user: { - select: { - name: true, - }, + const comments = await prisma.comment.findMany({ + where: { + ticketId: ticket!.id, + }, + include: { + user: { + select: { + name: true, }, }, - }); + }, + }); - const files = await prisma.ticketFile.findMany({ - where: { - ticketId: id, - }, - }); + const files = await prisma.ticketFile.findMany({ + where: { + ticketId: id, + }, + }); - var t = { - ...ticket, - comments: [...comments], - TimeTracking: [...timeTracking], - files: [...files], - }; + var t = { + ...ticket, + comments: [...comments], + TimeTracking: [...timeTracking], + files: [...files], + }; - reply.send({ - ticket: t, - sucess: true, - }); - } + reply.send({ + ticket: t, + sucess: true, + }); } ); - // Get all tickets + // Get all tickets - requires auth fastify.get( "/api/v1/tickets/open", + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const tickets = await prisma.ticket.findMany({ - where: { isComplete: false, hidden: false }, - orderBy: [ - { - createdAt: "desc", - }, - ], - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { isComplete: false, hidden: false }, + orderBy: [ + { + createdAt: "desc", }, - }); + ], + include: { + client: { + select: { id: true, name: true, number: true }, + }, + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); - // Basic Search for a ticket + // Basic Search - requires auth fastify.post( "/api/v1/tickets/search", + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { query }: any = request.body; - if (token) { - const tickets = await prisma.ticket.findMany({ - where: { - title: { - contains: query, - }, + const tickets = await prisma.ticket.findMany({ + where: { + title: { + contains: query, }, - }); + }, + }); - reply.send({ - tickets: tickets, - success: true, - }); - } + reply.send({ + tickets: tickets, + success: true, + }); } ); @@ -271,32 +267,30 @@ export function ticketRoutes(fastify: FastifyInstance) { const bearer = request.headers.authorization!.split(" ")[1]; const token = checkToken(bearer); - if (token) { - const tickets = await prisma.ticket.findMany({ - where: { hidden: false }, - orderBy: [ - { - createdAt: "desc", - }, - ], - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { hidden: false }, + orderBy: [ + { + createdAt: "desc", }, - }); + ], + include: { + client: { + select: { id: true, name: true, number: true }, + }, + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); @@ -305,32 +299,27 @@ export function ticketRoutes(fastify: FastifyInstance) { "/api/v1/tickets/user/open", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); + const user = await checkSession(request); - if (token) { - const user = await checkSession(bearer); - - const tickets = await prisma.ticket.findMany({ - where: { isComplete: false, userId: user!.id, hidden: false }, - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { isComplete: false, userId: user!.id, hidden: false }, + include: { + client: { + select: { id: true, name: true, number: true }, }, - }); + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); @@ -339,30 +328,25 @@ export function ticketRoutes(fastify: FastifyInstance) { "/api/v1/tickets/completed", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const tickets = await prisma.ticket.findMany({ - where: { isComplete: true, hidden: false }, - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { isComplete: true, hidden: false }, + include: { + client: { + select: { id: true, name: true, number: true }, }, - }); + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); @@ -371,94 +355,83 @@ export function ticketRoutes(fastify: FastifyInstance) { "/api/v1/tickets/unassigned", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const tickets = await prisma.ticket.findMany({ - where: { - isComplete: false, - assignedTo: null, - hidden: false, - }, - }); + const tickets = await prisma.ticket.findMany({ + where: { + isComplete: false, + assignedTo: null, + hidden: false, + }, + }); - reply.send({ - success: true, - tickets: tickets, - }); - } + reply.send({ + success: true, + tickets: tickets, + }); } ); // Update a ticket fastify.put( "/api/v1/ticket/update", - + { + preHandler: requirePermission(["issue::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { id, note, detail, title, priority, status }: any = request.body; - if (token) { - await prisma.ticket.update({ - where: { id: id }, - data: { - detail, - note, - title, - priority, - status, - }, - }); + await prisma.ticket.update({ + where: { id: id }, + data: { + detail, + note, + title, + priority, + status, + }, + }); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); // Transfer a ticket to another user fastify.post( "/api/v1/ticket/transfer", - + { + preHandler: requirePermission(["issue::transfer"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { user, id }: any = request.body; - if (token) { - if (user) { - const assigned = await prisma.user.update({ - where: { id: user }, - data: { - tickets: { - connect: { - id: id, - }, + if (user) { + const assigned = await prisma.user.update({ + where: { id: user }, + data: { + tickets: { + connect: { + id: id, }, }, - }); - - const { email } = assigned; + }, + }); - await sendAssignedEmail(email); - } else { - await prisma.ticket.update({ - where: { id: id }, - data: { - userId: null, - }, - }); - } + const { email } = assigned; - reply.send({ - success: true, + await sendAssignedEmail(email); + } else { + await prisma.ticket.update({ + where: { id: id }, + data: { + userId: null, + }, }); } + + reply.send({ + success: true, + }); } ); @@ -510,189 +483,174 @@ export function ticketRoutes(fastify: FastifyInstance) { // Comment on a ticket fastify.post( "/api/v1/ticket/comment", - + { + preHandler: requirePermission(["issue::comment"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { text, id, public: public_comment }: any = request.body; - if (token) { - const user = await checkSession(bearer); + const user = await checkSession(request); - await prisma.comment.create({ - data: { - text: text, - public: public_comment, - ticketId: id, - userId: user!.id, - }, - }); + await prisma.comment.create({ + data: { + text: text, + public: public_comment, + ticketId: id, + userId: user!.id, + }, + }); - const ticket = await prisma.ticket.findUnique({ - where: { - id: id, - }, - }); + const ticket = await prisma.ticket.findUnique({ + where: { + id: id, + }, + }); - //@ts-expect-error - const { email, title } = ticket; - if (public_comment && email) { - sendComment(text, title, ticket!.id, email!); - } + //@ts-expect-error + const { email, title } = ticket; + if (public_comment && email) { + sendComment(text, title, ticket!.id, email!); + } - await commentNotification(user!.id, ticket, user!.name); + await commentNotification(user!.id, ticket, user!.name); - const hog = track(); + const hog = track(); - hog.capture({ - event: "ticket_comment", - distinctId: ticket!.id, - }); + hog.capture({ + event: "ticket_comment", + distinctId: ticket!.id, + }); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); // Update status of a ticket fastify.put( "/api/v1/ticket/status/update", - + { + preHandler: requirePermission(["issue::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); + const { status, id }: any = request.body; - if (token) { - const { status, id }: any = request.body; + const ticket: any = await prisma.ticket.update({ + where: { id: id }, + data: { + isComplete: status, + }, + }); - const ticket: any = await prisma.ticket.update({ - where: { id: id }, - data: { - isComplete: status, - }, - }); + const webhook = await prisma.webhooks.findMany({ + where: { + type: "ticket_status_changed", + }, + }); - const webhook = await prisma.webhooks.findMany({ - where: { - type: "ticket_status_changed", - }, - }); + for (let i = 0; i < webhook.length; i++) { + const url = webhook[i].url; - for (let i = 0; i < webhook.length; i++) { - const url = webhook[i].url; - - if (webhook[i].active === true) { - const s = status ? "Completed" : "Outstanding"; - if (url.includes("discord.com")) { - const message = { - content: `Ticket ${ticket.id} created by ${ticket.email}, has had it's status changed to ${s}`, - avatar_url: - "https://avatars.githubusercontent.com/u/76014454?s=200&v=4", - username: "Peppermint.sh", - }; - axios - .post(url, message) - .then((response) => { - console.log("Message sent successfully!"); - console.log("Discord API response:", response.data); - }) - .catch((error) => { - console.error("Error sending message:", error); - }); - } else { - await axios.post(`${webhook[i].url}`, { - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - data: `Ticket ${ticket.id} created by ${ticket.email}, has had it's status changed to ${s}`, - }), + if (webhook[i].active === true) { + const s = status ? "Completed" : "Outstanding"; + if (url.includes("discord.com")) { + const message = { + content: `Ticket ${ticket.id} created by ${ticket.email}, has had it's status changed to ${s}`, + avatar_url: + "https://avatars.githubusercontent.com/u/76014454?s=200&v=4", + username: "Peppermint.sh", + }; + axios + .post(url, message) + .then((response) => { + console.log("Message sent successfully!"); + console.log("Discord API response:", response.data); + }) + .catch((error) => { + console.error("Error sending message:", error); }); - } + } else { + await axios.post(`${webhook[i].url}`, { + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + data: `Ticket ${ticket.id} created by ${ticket.email}, has had it's status changed to ${s}`, + }), + }); } } + } - sendTicketStatus(ticket); + sendTicketStatus(ticket); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); // Hide a ticket fastify.put( "/api/v1/ticket/status/hide", - + { + preHandler: requirePermission(["issue::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const { hidden, id }: any = request.body; + const { hidden, id }: any = request.body; - await prisma.ticket.update({ - where: { id: id }, - data: { - hidden: hidden, - }, - }); + await prisma.ticket.update({ + where: { id: id }, + data: { + hidden: hidden, + }, + }); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); // Lock a ticket fastify.put( "/api/v1/ticket/status/lock", - + { + preHandler: requirePermission(["issue::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const { locked, id }: any = request.body; + const { locked, id }: any = request.body; - await prisma.ticket.update({ - where: { id: id }, - data: { - locked: locked, - }, - }); + await prisma.ticket.update({ + where: { id: id }, + data: { + locked: locked, + }, + }); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); // Delete a ticket fastify.post( "/api/v1/ticket/delete", - + { + preHandler: requirePermission(["issue::delete"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const { id }: any = request.body; + const { id }: any = request.body; - await prisma.ticket.delete({ - where: { id: id }, - }); + await prisma.ticket.delete({ + where: { id: id }, + }); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); @@ -706,80 +664,71 @@ export function ticketRoutes(fastify: FastifyInstance) { // GET all ticket templates fastify.get( "/api/v1/ticket/templates", - + { + preHandler: requirePermission(["email_template::manage"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const templates = await prisma.emailTemplate.findMany({ - select: { - createdAt: true, - updatedAt: true, - type: true, - id: true, - }, - }); + const templates = await prisma.emailTemplate.findMany({ + select: { + createdAt: true, + updatedAt: true, + type: true, + id: true, + }, + }); - reply.send({ - success: true, - templates: templates, - }); - } + reply.send({ + success: true, + templates: templates, + }); } ); // GET ticket template by ID fastify.get( "/api/v1/ticket/template/:id", - + { + preHandler: requirePermission(["email_template::manage"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { id }: any = request.params; - if (token) { - const template = await prisma.emailTemplate.findMany({ - where: { - id: id, - }, - }); + const template = await prisma.emailTemplate.findMany({ + where: { + id: id, + }, + }); - reply.send({ - success: true, - template: template, - }); - } + reply.send({ + success: true, + template: template, + }); } ); // PUT ticket template by ID fastify.put( "/api/v1/ticket/template/:id", - + { + preHandler: requirePermission(["email_template::manage"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - const { id }: any = request.params; const { html }: any = request.body; - if (token) { - await prisma.emailTemplate.update({ - where: { - id: id, - }, - data: { - html: html, - }, - }); + await prisma.emailTemplate.update({ + where: { + id: id, + }, + data: { + html: html, + }, + }); - reply.send({ - success: true, - }); - } + reply.send({ + success: true, + }); } ); @@ -788,32 +737,27 @@ export function ticketRoutes(fastify: FastifyInstance) { "/api/v1/tickets/user/open/external", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const user = await checkSession(bearer); + const user = await checkSession(request); - const tickets = await prisma.ticket.findMany({ - where: { isComplete: false, email: user!.email, hidden: false }, - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { isComplete: false, email: user!.email, hidden: false }, + include: { + client: { + select: { id: true, name: true, number: true }, }, - }); + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); @@ -822,66 +766,58 @@ export function ticketRoutes(fastify: FastifyInstance) { "/api/v1/tickets/user/closed/external", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); - - if (token) { - const user = await checkSession(bearer); + const user = await checkSession(request); - const tickets = await prisma.ticket.findMany({ - where: { isComplete: true, email: user!.email, hidden: false }, - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { isComplete: true, email: user!.email, hidden: false }, + include: { + client: { + select: { id: true, name: true, number: true }, }, - }); + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); // Get all tickets for an external user fastify.get( "/api/v1/tickets/user/external", - + { + preHandler: requirePermission(["issue::read"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - const token = checkToken(bearer); + const user = await checkSession(request); - if (token) { - const user = await checkSession(bearer); - - const tickets = await prisma.ticket.findMany({ - where: { email: user!.email, hidden: false }, - include: { - client: { - select: { id: true, name: true, number: true }, - }, - assignedTo: { - select: { id: true, name: true }, - }, - team: { - select: { id: true, name: true }, - }, + const tickets = await prisma.ticket.findMany({ + where: { email: user!.email, hidden: false }, + include: { + client: { + select: { id: true, name: true, number: true }, }, - }); + assignedTo: { + select: { id: true, name: true }, + }, + team: { + select: { id: true, name: true }, + }, + }, + }); - reply.send({ - tickets: tickets, - sucess: true, - }); - } + reply.send({ + tickets: tickets, + sucess: true, + }); } ); } diff --git a/apps/api/src/lib/roles.ts b/apps/api/src/lib/roles.ts index cfeba7521..44662276b 100644 --- a/apps/api/src/lib/roles.ts +++ b/apps/api/src/lib/roles.ts @@ -25,6 +25,11 @@ export function hasPermission( requiredPermissions: Permission | Permission[], requireAll: boolean = true ): boolean { + // Admins have all permissions + // if (user?.isAdmin) { + // return true; + // } + // Convert single permission to array for consistent handling const permissions = Array.isArray(requiredPermissions) ? requiredPermissions @@ -69,33 +74,35 @@ export function requirePermission( const user = await checkSession(req); const config = await prisma.config.findFirst(); - if (!config?.roles_active) { - next(); - } + if (config?.roles_active) { + const userWithRoles = user + ? await prisma.user.findUnique({ + where: { id: user.id }, + include: { + roles: true, + }, + }) + : null; + + if (!userWithRoles) { + return res.status(401).send({ + message: "Unauthorized", + success: false, + }); + } + + if (!hasPermission(userWithRoles, requiredPermissions, requireAll)) { + return res.status(401).send({ + message: "You do not have the required permission to access this resource.", + success: false, + status: 403, + }); + } - const userWithRoles = user - ? await prisma.user.findUnique({ - where: { id: user.id }, - include: { - roles: true, - }, - }) - : null; - - // Admins have all permissions - if (user?.isAdmin) { + next(); + } else { next(); } - - if (!userWithRoles) { - throw new Error("User not authenticated"); - } - - if (!hasPermission(userWithRoles, requiredPermissions, requireAll)) { - throw new InsufficientPermissionsError(); - } - - next(); } catch (error) { next(error); } diff --git a/apps/client/@/shadcn/components/forbidden.tsx b/apps/client/@/shadcn/components/forbidden.tsx new file mode 100644 index 000000000..a9e8b24f2 --- /dev/null +++ b/apps/client/@/shadcn/components/forbidden.tsx @@ -0,0 +1,20 @@ +import { toast } from "../hooks/use-toast"; + +const NoPermissions = () => { + toast({ + title: "Unauthorized", + description: "Please check your permissions.", + }); + return ( +
+
+

Access Denied

+

+ You do not have permission to view this page. +

+
+
+ ); +}; + +export default NoPermissions; diff --git a/apps/client/@/shadcn/lib/hasAccess.ts b/apps/client/@/shadcn/lib/hasAccess.ts new file mode 100644 index 000000000..1ed61785f --- /dev/null +++ b/apps/client/@/shadcn/lib/hasAccess.ts @@ -0,0 +1,11 @@ +import { toast } from "@/shadcn/hooks/use-toast"; + +export const hasAccess = (response: Response) => { + if (response.status === 401) { + toast({ + title: "Unauthorized", + description: "Please check your permissions.", + }); + } + return response; +}; diff --git a/apps/client/components/NotebookEditor/index.tsx b/apps/client/components/NotebookEditor/index.tsx index 89db4f185..ef262c8a7 100644 --- a/apps/client/components/NotebookEditor/index.tsx +++ b/apps/client/components/NotebookEditor/index.tsx @@ -1,18 +1,19 @@ //@ts-nocheck -import { useRouter } from "next/router"; -import { useEffect, useMemo, useState } from "react"; -import { getCookie } from "cookies-next"; -import moment from "moment"; -import { useDebounce } from "use-debounce"; -import { BlockNoteEditor, PartialBlock } from "@blocknote/core"; -import { BlockNoteView } from "@blocknote/mantine"; +import { toast } from "@/shadcn/hooks/use-toast"; import { DropdownMenu, - DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, + DropdownMenuTrigger, } from "@/shadcn/ui/dropdown-menu"; +import { BlockNoteEditor, PartialBlock } from "@blocknote/core"; +import { BlockNoteView } from "@blocknote/mantine"; +import { getCookie } from "cookies-next"; import { Ellipsis } from "lucide-react"; +import moment from "moment"; +import { useRouter } from "next/router"; +import { useEffect, useMemo, useState } from "react"; +import { useDebounce } from "use-debounce"; import { useUser } from "../../store/session"; function isHTML(str) { @@ -76,7 +77,7 @@ export default function NotebookEditor() { async function updateNoteBook() { setSaving(true); - await fetch(`/api/v1/notebooks/note/${router.query.id}/update`, { + const res = await fetch(`/api/v1/notebooks/note/${router.query.id}/update`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -87,15 +88,23 @@ export default function NotebookEditor() { content: JSON.stringify(debouncedValue), }), }); + const data = await res.json(); setSaving(false); let date = new Date(); // @ts-ignore setLastSaved(new Date(date).getTime()); + if(data.status) { + toast({ + variant: "destructive", + title: "Error -> Unable to update", + description: data.message, + }); + } } async function deleteNotebook(id) { if (window.confirm("Do you really want to delete this notebook?")) { - await fetch(`/api/v1/documents/${router.query.id}`, { + await fetch(`/api/v1/notebooks/note/${router.query.id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}`, @@ -105,6 +114,12 @@ export default function NotebookEditor() { .then((res) => { if (res.success) { router.push("/documents"); + } else { + toast({ + variant: "destructive", + title: "Error -> Unable to delete", + description: res.message, + }); } }); } diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index 178ef7ecd..01d4337b3 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -30,6 +30,7 @@ import { useQuery } from "react-query"; import { useDebounce } from "use-debounce"; import { toast } from "@/shadcn/hooks/use-toast"; +import { hasAccess } from "@/shadcn/lib/hasAccess"; import { cn } from "@/shadcn/lib/utils"; import { Avatar, AvatarFallback, AvatarImage } from "@/shadcn/ui/avatar"; import { @@ -103,6 +104,9 @@ export default function Ticket() { Authorization: `Bearer ${token}`, }, }); + + hasAccess(res); + return res.json(); }; diff --git a/apps/client/pages/admin/roles/[id].tsx b/apps/client/pages/admin/roles/[id].tsx new file mode 100644 index 000000000..faec359bb --- /dev/null +++ b/apps/client/pages/admin/roles/[id].tsx @@ -0,0 +1,274 @@ +import { Permission, PERMISSIONS_CONFIG } from "@/shadcn/lib/types/permissions"; +import { Card, CardContent, CardHeader, CardTitle } from "@/shadcn/ui/card"; +import { Input } from "@/shadcn/ui/input"; +import { getCookie } from "cookies-next"; +import { Search } from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +export default function UpdateRole() { + const [step, setStep] = useState(1); + const [selectedPermissions, setSelectedPermissions] = useState([]); + const [roleName, setRoleName] = useState(""); + const [selectedUsers, setSelectedUsers] = useState([]); + const [users, setUsers] = useState>([]); + const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + const { id } = router.query; + + // New function to fetch role data + const fetchRoleData = async () => { + if (!id) return; + + try { + const response = await fetch(`/api/v1/role/${id}`, { + headers: { + Authorization: `Bearer ${getCookie("session")}`, + }, + }); + const data = await response.json(); + if (data.success) { + setRoleName(data.role.name); + setSelectedPermissions(data.role.permissions); + setSelectedUsers(data.role.users.map((u: any) => u.id)); + } + } catch (error) { + console.error("Error fetching role:", error); + } + }; + + // Add this function + const fetchUsers = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/v1/users/all', { + headers: { + Authorization: `Bearer ${getCookie("session")}`, + }, + }); + const data = await response.json(); + if (data.success) { + setUsers(data.users); + } + } catch (error) { + console.error("Error fetching users:", error); + } + setIsLoading(false); + }; + + // Modified to handle role update instead of creation + const handleUpdateRole = async () => { + if (!roleName || !id) return; + + await fetch(`/api/v1/role/${id}/update`, { + method: "PUT", + headers: { + Authorization: `Bearer ${getCookie("session")}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: roleName, + permissions: selectedPermissions, + users: selectedUsers, + }), + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + router.push("/admin/roles"); + } + }); + }; + + // Load role data when component mounts + useEffect(() => { + if (id) { + fetchRoleData(); + fetchUsers(); + } + }, [id]); + + const handleSelectCategory = (category: string, isSelected: boolean) => { + const categoryPermissions = + PERMISSIONS_CONFIG.find((group) => group.category === category) + ?.permissions || []; + + if (isSelected) { + const newPermissions = [ + ...selectedPermissions, + ...categoryPermissions.filter( + (p: Permission) => !selectedPermissions.includes(p) + ), + ]; + setSelectedPermissions(newPermissions); + } else { + setSelectedPermissions( + selectedPermissions.filter( + (p: Permission) => !categoryPermissions.includes(p) + ) + ); + } + }; + + const isCategoryFullySelected = (category: string) => { + const categoryPermissions = + PERMISSIONS_CONFIG.find((group) => group.category === category) + ?.permissions || []; + return categoryPermissions.every((p) => selectedPermissions.includes(p)); + }; + + const filteredUsers = users.filter((user) => + user.email.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( +
+ {/* ... same stepper UI ... */} + + {step === 1 ? ( + + +
+ setRoleName(e.target.value)} + /> + +
+
+ +
+

Select Permissions

+ {PERMISSIONS_CONFIG.map((group) => ( +
+
+

{group.category}

+ +
+
+ {group.permissions.map((permission) => ( + + ))} +
+
+ ))} +
+
+
+ ) : ( + + +
+ Select Users +
+ + +
+
+
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+ + {isLoading ? ( +
Loading users...
+ ) : ( +
+ {filteredUsers.map((user) => ( + + ))} +
+ )} + + {!isLoading && filteredUsers.length === 0 && ( +
+ {searchTerm ? "No users found" : "No users available"} +
+ )} +
+
+
+ )} +
+ ); +} diff --git a/apps/client/pages/admin/roles/index.tsx b/apps/client/pages/admin/roles/index.tsx index ee123a188..b2977a663 100644 --- a/apps/client/pages/admin/roles/index.tsx +++ b/apps/client/pages/admin/roles/index.tsx @@ -1,3 +1,4 @@ +import { hasAccess } from "@/shadcn/lib/hasAccess"; import { Card, CardContent, CardHeader, CardTitle } from "@/shadcn/ui/card"; import { getCookie } from "cookies-next"; import { useRouter } from "next/router"; @@ -14,9 +15,13 @@ export default function Roles() { Authorization: `Bearer ${getCookie("session")}`, }, }); - const data = await response.json(); - setRoles(data.roles); - setLoading(false); + hasAccess(response); + + if (hasAccess(response)) { + const data = await response.json(); + setRoles(data.roles); + setLoading(false); + } }; useEffect(() => { @@ -26,7 +31,7 @@ export default function Roles() { const handleDeleteRole = async (roleId) => { if (!confirm("Are you sure you want to delete this role?")) return; - await fetch(`/api/v1/data/roles/${roleId}`, { + await fetch(`/api/v1/role/${roleId}/delete`, { method: "DELETE", headers: { Authorization: `Bearer ${getCookie("session")}`, @@ -70,12 +75,24 @@ export default function Roles() { ID: {role.id} +
+ +
))} diff --git a/apps/client/pages/admin/roles/new.tsx b/apps/client/pages/admin/roles/new.tsx index 515427e7e..31d9e878f 100644 --- a/apps/client/pages/admin/roles/new.tsx +++ b/apps/client/pages/admin/roles/new.tsx @@ -1,18 +1,27 @@ import { Permission, PERMISSIONS_CONFIG } from "@/shadcn/lib/types/permissions"; import { Card, CardContent, CardHeader, CardTitle } from "@/shadcn/ui/card"; +import { Input } from "@/shadcn/ui/input"; import { getCookie } from "cookies-next"; -import { useState } from "react"; +import { Search } from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; export default function Roles() { + const [step, setStep] = useState(1); const [selectedPermissions, setSelectedPermissions] = useState( [] ); const [roleName, setRoleName] = useState(""); + const [selectedUsers, setSelectedUsers] = useState([]); + const [users, setUsers] = useState>([]); + const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); const handleAddRole = async () => { if (!roleName) return; - await fetch("/api/v1/roles", { + await fetch("/api/v1/role/create", { method: "POST", headers: { Authorization: `Bearer ${getCookie("session")}`, @@ -21,11 +30,15 @@ export default function Roles() { body: JSON.stringify({ name: roleName, permissions: selectedPermissions, + users: selectedUsers, }), - }); - - setSelectedPermissions([]); - setRoleName(""); + }) + .then((res) => res.json()) + .then((data) => { + if (data.success) { + router.push("/admin/roles"); + } + }); }; const handleSelectCategory = (category: string, isSelected: boolean) => { @@ -58,83 +71,221 @@ export default function Roles() { return categoryPermissions.every((p) => selectedPermissions.includes(p)); }; - console.log(selectedPermissions); + const fetchUsers = async () => { + setIsLoading(true); + try { + const response = await fetch("/api/v1/users/all", { + headers: { + Authorization: `Bearer ${getCookie("session")}`, + }, + }); + const data = await response.json(); + if (data.success) { + setUsers(data.users); + } + } catch (error) { + console.error("Error fetching users:", error); + } + setIsLoading(false); + }; + + useEffect(() => { + if (step === 2) { + fetchUsers(); + } + }, [step]); + + const filteredUsers = users.filter((user) => + user.email.toLowerCase().includes(searchTerm.toLowerCase()) + ); return (
-
-
- setRoleName(e.target.value)} - placeholder="Role name" - className="px-2 py-2 border rounded" - /> - +
+ 2 +
+ Select Users +
- - - Roles - - -
-

Select Permissions

- {PERMISSIONS_CONFIG.map((group) => ( -
-
-

{group.category}

- + + {step === 1 ? ( + + +
+ setRoleName(e.target.value)} + /> + +
+
+ +
+

Select Permissions

+ {PERMISSIONS_CONFIG.map((group) => ( +
+
+

{group.category}

+ +
+
+ {group.permissions.map((permission) => ( + + ))} +
-
- {group.permissions.map((permission) => ( + ))} +
+ + + ) : ( + + +
+ Select Users +
+ + +
+
+
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+ + {isLoading ? ( +
Loading users...
+ ) : ( +
+ {filteredUsers.map((user) => ( ))}
-
- ))} -
-
-
+ )} + + {!isLoading && filteredUsers.length === 0 && ( +
+ {searchTerm ? "No users found" : "No users available"} +
+ )} +
+ + + )}
); } diff --git a/apps/client/pages/admin/users/internal/index.js b/apps/client/pages/admin/users/internal/index.js index aa3f791de..6cbbfbb74 100644 --- a/apps/client/pages/admin/users/internal/index.js +++ b/apps/client/pages/admin/users/internal/index.js @@ -17,6 +17,7 @@ const fetchUsers = async (token) => { Authorization: `Bearer ${token}`, }, }); + return res.json(); }; diff --git a/apps/client/pages/admin/webhooks.js b/apps/client/pages/admin/webhooks.tsx similarity index 81% rename from apps/client/pages/admin/webhooks.js rename to apps/client/pages/admin/webhooks.tsx index 7fdd4810a..5f7d15705 100644 --- a/apps/client/pages/admin/webhooks.js +++ b/apps/client/pages/admin/webhooks.tsx @@ -1,3 +1,4 @@ +import { hasAccess } from "@/shadcn/lib/hasAccess"; import { Switch } from "@headlessui/react"; import { getCookie } from "cookies-next"; import { useState } from "react"; @@ -11,7 +12,14 @@ async function getHooks() { Authorization: `Bearer ${getCookie("session")}`, }, }); - return res.json(); + + hasAccess(res); + + if (!res.ok) { + throw new Error("Network response was not ok"); + } + + return res.json(); // Return the parsed JSON response } function classNames(...classes) { @@ -75,49 +83,51 @@ export default function Notifications() { Webhook Settings
-
-
-
-

- Webhooks allow external services to be notified when certain - events happen. When the specified events happen, we'll send - a POST request to each of the URLs you provide. -

-
-
- <> - - - -
-
-
+
{status === "success" && ( + <> +
+
+
+

+ Webhooks allow external services to be notified when certain + events happen. When the specified events happen, we'll send + a POST request to each of the URLs you provide. +

+
+
+ <> + + + +
+
+
{data !== undefined && data.webhooks.length > 0 ? (
@@ -151,7 +161,8 @@ export default function Notifications() { You currently have no web hooks added

)} -
+
P + )}
diff --git a/apps/client/pages/documents/index.tsx b/apps/client/pages/documents/index.tsx index f44293b71..c8f781978 100644 --- a/apps/client/pages/documents/index.tsx +++ b/apps/client/pages/documents/index.tsx @@ -1,11 +1,6 @@ +import { toast } from "@/shadcn/hooks/use-toast"; import { Button } from "@/shadcn/ui/button"; -import { getCookie } from "cookies-next"; -import { Ellipsis } from "lucide-react"; -import useTranslation from "next-translate/useTranslation"; -import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import { useQuery } from "react-query"; -import { useUser } from "../../store/session"; +import { Input } from "@/shadcn/ui/input"; import { Select, SelectContent, @@ -13,7 +8,11 @@ import { SelectTrigger, SelectValue, } from "@/shadcn/ui/select"; -import { Input } from "@/shadcn/ui/input"; +import { getCookie } from "cookies-next"; +import useTranslation from "next-translate/useTranslation"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { useQuery } from "react-query"; function groupDocumentsByDate(notebooks) { const today = new Date(); @@ -68,8 +67,16 @@ async function fetchNotebooks(token) { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - }); - return res.json(); + }).then((res) => res.json()); + + if (res.status) { + toast({ + title: "Error", + description: "You do not have permission to view this resource.", + }); + } + + return res; } export default function NoteBooksIndex() { @@ -153,7 +160,7 @@ export default function NoteBooksIndex() {
{status === "loading" &&

Loading...

} {status === "error" &&

Error loading documents.

} - {data && data.notebooks.length === 0 ? ( + {data && data.notebooks && data.notebooks.length === 0 ? (

No documents found.

) : (
-
+ {data && data.notebooks && data.notebooks.length > 0 && (
-
+ )} {data?.notebooks && Object.entries(