From 924d34f0311b595ac241bdb4253df9493ec1a435 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 00:01:52 +0000 Subject: [PATCH 01/13] fix: fix build issue --- apps/client/pages/admin/roles/[id].tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/pages/admin/roles/[id].tsx b/apps/client/pages/admin/roles/[id].tsx index faec359bb..de8685570 100644 --- a/apps/client/pages/admin/roles/[id].tsx +++ b/apps/client/pages/admin/roles/[id].tsx @@ -105,6 +105,7 @@ export default function UpdateRole() { } else { setSelectedPermissions( selectedPermissions.filter( + //@ts-ignore (p: Permission) => !categoryPermissions.includes(p) ) ); From fbf836969c46d8ce0f530caf61ff3ce322c580b5 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 00:37:16 +0000 Subject: [PATCH 02/13] chore: little bit of tidying up --- apps/api/src/controllers/config.ts | 23 ++++ apps/api/src/controllers/roles.ts | 79 ++++++----- apps/client/pages/admin/roles/[id].tsx | 1 - apps/client/pages/admin/roles/index.tsx | 171 +++++++++++++++++------- 4 files changed, 188 insertions(+), 86 deletions(-) diff --git a/apps/api/src/controllers/config.ts b/apps/api/src/controllers/config.ts index 6e7b21f40..be7cd03fe 100644 --- a/apps/api/src/controllers/config.ts +++ b/apps/api/src/controllers/config.ts @@ -384,4 +384,27 @@ export function configRoutes(fastify: FastifyInstance) { }); } ); + + // Toggle all roles + fastify.patch( + "/api/v1/config/toggle-roles", + + async (request: FastifyRequest, reply: FastifyReply) => { + const { isActive }: any = request.body; + + const config = await prisma.config.findFirst(); + + await prisma.config.update({ + where: { id: config!.id }, + data: { + roles_active: isActive, + }, + }); + + reply.send({ + success: true, + message: "Roles updated!", + }); + } + ); } diff --git a/apps/api/src/controllers/roles.ts b/apps/api/src/controllers/roles.ts index a277293f2..16e1a14ee 100644 --- a/apps/api/src/controllers/roles.ts +++ b/apps/api/src/controllers/roles.ts @@ -9,7 +9,7 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.post( "/api/v1/role/create", { - preHandler: requirePermission(['role::create']), + preHandler: requirePermission(["role::create"]), }, async (request: FastifyRequest, reply: FastifyReply) => { const user = await checkSession(request); @@ -20,9 +20,9 @@ export function roleRoutes(fastify: FastifyInstance) { }); if (existingRole) { - return reply.status(400).send({ - message: "Role already exists", - success: false + return reply.status(400).send({ + message: "Role already exists", + success: false, }); } @@ -50,16 +50,22 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.get( "/api/v1/roles/all", { - preHandler: requirePermission(['role::read']), + preHandler: requirePermission(["role::read"]), }, async (request: FastifyRequest, reply: FastifyReply) => { const roles = await prisma.role.findMany({ include: { - users: true, + users: false, + }, + }); + + const active = await prisma.config.findFirst({ + select: { + roles_active: true, }, }); - reply.status(200).send({ roles, success: true }); + reply.status(200).send({ roles, success: true, roles_active: active }); } ); @@ -67,11 +73,11 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.get( "/api/v1/role/:id", { - preHandler: requirePermission(['role::read']), + preHandler: requirePermission(["role::read"]), }, async (request: FastifyRequest, reply: FastifyReply) => { const { id }: any = request.params; - + const role = await prisma.role.findUnique({ where: { id }, include: { @@ -80,9 +86,9 @@ export function roleRoutes(fastify: FastifyInstance) { }); if (!role) { - return reply.status(404).send({ - message: "Role not found", - success: false + return reply.status(404).send({ + message: "Role not found", + success: false, }); } @@ -94,11 +100,12 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.put( "/api/v1/role/:id/update", { - preHandler: requirePermission(['role::update']), + preHandler: requirePermission(["role::update"]), }, async (request: FastifyRequest, reply: FastifyReply) => { const { id }: any = request.params; - const { name, description, permissions, isDefault, users }: any = request.body; + const { name, description, permissions, isDefault, users }: any = + request.body; try { const updatedRole = await prisma.role.update({ @@ -110,17 +117,19 @@ export function roleRoutes(fastify: FastifyInstance) { 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 + set: Array.isArray(users) + ? users.map((userId) => ({ id: userId })) + : [{ id: users }], // Ensure users is an array of objects with unique IDs when updating }, }, }); reply.status(200).send({ role: updatedRole, success: true }); } catch (error: any) { - if (error.code === 'P2025') { - return reply.status(404).send({ - message: "Role not found", - success: false + if (error.code === "P2025") { + return reply.status(404).send({ + message: "Role not found", + success: false, }); } throw error; @@ -132,11 +141,11 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.delete( "/api/v1/role/:id/delete", { - preHandler: requirePermission(['role::delete']), + preHandler: requirePermission(["role::delete"]), }, async (request: FastifyRequest, reply: FastifyReply) => { const { id }: any = request.params; - + try { await prisma.role.delete({ where: { id }, @@ -144,10 +153,10 @@ export function roleRoutes(fastify: FastifyInstance) { reply.status(200).send({ success: true }); } catch (error: any) { - if (error.code === 'P2025') { - return reply.status(404).send({ - message: "Role not found", - success: false + if (error.code === "P2025") { + return reply.status(404).send({ + message: "Role not found", + success: false, }); } throw error; @@ -159,7 +168,7 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.post( "/api/v1/role/assign", { - preHandler: requirePermission(['role::update']), + preHandler: requirePermission(["role::update"]), }, async (request: FastifyRequest, reply: FastifyReply) => { const { userId, roleId }: any = request.body; @@ -179,10 +188,10 @@ export function roleRoutes(fastify: FastifyInstance) { reply.status(200).send({ user: updatedUser, success: true }); } catch (error: any) { - if (error.code === 'P2025') { - return reply.status(404).send({ - message: "User or Role not found", - success: false + if (error.code === "P2025") { + return reply.status(404).send({ + message: "User or Role not found", + success: false, }); } throw error; @@ -194,7 +203,7 @@ export function roleRoutes(fastify: FastifyInstance) { fastify.post( "/api/v1/role/remove", { - // preHandler: requirePermission(['role::remove']), + // preHandler: requirePermission(['role::remove']), }, async (request: FastifyRequest, reply: FastifyReply) => { const { userId, roleId }: any = request.body; @@ -214,10 +223,10 @@ export function roleRoutes(fastify: FastifyInstance) { reply.status(200).send({ user: updatedUser, success: true }); } catch (error: any) { - if (error.code === 'P2025') { - return reply.status(404).send({ - message: "User or Role not found", - success: false + if (error.code === "P2025") { + return reply.status(404).send({ + message: "User or Role not found", + success: false, }); } throw error; diff --git a/apps/client/pages/admin/roles/[id].tsx b/apps/client/pages/admin/roles/[id].tsx index de8685570..738d0a2a3 100644 --- a/apps/client/pages/admin/roles/[id].tsx +++ b/apps/client/pages/admin/roles/[id].tsx @@ -125,7 +125,6 @@ export default function UpdateRole() { return (
- {/* ... same stepper UI ... */} {step === 1 ? ( diff --git a/apps/client/pages/admin/roles/index.tsx b/apps/client/pages/admin/roles/index.tsx index b2977a663..a988cef07 100644 --- a/apps/client/pages/admin/roles/index.tsx +++ b/apps/client/pages/admin/roles/index.tsx @@ -1,3 +1,4 @@ +import { toast } from "@/shadcn/hooks/use-toast"; import { hasAccess } from "@/shadcn/lib/hasAccess"; import { Card, CardContent, CardHeader, CardTitle } from "@/shadcn/ui/card"; import { getCookie } from "cookies-next"; @@ -6,6 +7,7 @@ import { useEffect, useState } from "react"; export default function Roles() { const [roles, setRoles] = useState([]); + const [isAllRolesActive, setIsAllRolesActive] = useState(); const [loading, setLoading] = useState(true); const router = useRouter(); @@ -20,6 +22,7 @@ export default function Roles() { if (hasAccess(response)) { const data = await response.json(); setRoles(data.roles); + setIsAllRolesActive(data.roles_active.roles_active); setLoading(false); } }; @@ -41,6 +44,37 @@ export default function Roles() { fetchRoles(); }; + const handleToggleRole = async (roleId: string, isActive: boolean) => { + await fetch(`/api/v1/role/${roleId}/toggle`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${getCookie("session")}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ isActive: !isActive }), + }); + + fetchRoles(); + }; + + const handleToggleAllRoles = async (isActive: boolean) => { + await fetch(`/api/v1/config/toggle-roles`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${getCookie("session")}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ isActive }), + }); + + toast({ + title: "Role Status Updated!", + description: "Roles have been updated successfully.", + }); + + fetchRoles(); + }; + if (loading) { return
Loading...
; } @@ -48,58 +82,95 @@ export default function Roles() { return (
- +
+ + + +
- - - Roles - - - {roles.length === 0 ? ( -
No roles available
- ) : ( -
    - {roles.map((role) => ( -
  • -
    -
    - {role.name} - - ID: {role.id} - -
    -
    - - + {!loading && ( + + + Roles + + {isAllRolesActive ? "Active" : "Inactive"} + + + + {roles.length === 0 ? ( +
    No roles available
    + ) : ( +
      + {roles.map((role) => ( +
    • +
      +
      +
      + {role.name} +
      + + ID: {role.id} + +
      +
      + {/* */} + + +
      -
    -
  • - ))} -
- )} -
-
+ + ))} + + )} + + + )}
); } From a4c5375f5f8e2343bc68c8d8479eac815e97759f Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 10:25:44 +0000 Subject: [PATCH 03/13] fix: email replyto's --- apps/api/src/lib/services/auth.service.ts | 60 ++++++---- apps/api/src/lib/services/imap.service.ts | 131 ++++++++++++---------- 2 files changed, 109 insertions(+), 82 deletions(-) diff --git a/apps/api/src/lib/services/auth.service.ts b/apps/api/src/lib/services/auth.service.ts index 073ad9268..8327824f3 100644 --- a/apps/api/src/lib/services/auth.service.ts +++ b/apps/api/src/lib/services/auth.service.ts @@ -1,50 +1,62 @@ -import { GoogleAuth } from 'google-auth-library'; -import { prisma } from '../../prisma'; -import { EmailQueue } from '../types/email'; +import { GoogleAuth } from "google-auth-library"; +import { prisma } from "../../prisma"; +import { EmailQueue } from "../types/email"; export class AuthService { - public static generateXOAuth2Token(username: string, accessToken: string): string { + public static generateXOAuth2Token( + username: string, + accessToken: string + ): string { const authString = [ `user=${username}`, `auth=Bearer ${accessToken}`, - '', - '' - ].join('\x01'); - return Buffer.from(authString).toString('base64'); + "", + "", + ].join("\x01"); + return Buffer.from(authString).toString("base64"); } static async getValidAccessToken(queue: EmailQueue): Promise { - const { clientId, clientSecret, refreshToken, accessToken, expiresIn } = queue; + const { clientId, clientSecret, refreshToken, accessToken, expiresIn } = + queue; + // Check if token is still valid const now = Math.floor(Date.now() / 1000); if (accessToken && expiresIn && now < expiresIn) { return accessToken; } + // Initialize GoogleAuth client const auth = new GoogleAuth({ - clientOptions: { clientId, clientSecret } + clientOptions: { + clientId: clientId, + clientSecret: clientSecret, + }, }); const oauth2Client = auth.fromJSON({ client_id: clientId, client_secret: clientSecret, - refresh_token: refreshToken + refresh_token: refreshToken, }); + // Refresh the token if expired const tokenInfo = await oauth2Client.getAccessToken(); - if (!tokenInfo.token) { - throw new Error('Unable to refresh access token.'); - } - const expiryDate = (expiresIn || 0) + 3600; - await prisma.emailQueue.update({ - where: { id: queue.id }, - data: { - accessToken: tokenInfo.token, - expiresIn: expiryDate - } - }); + const expiryDate = expiresIn! + 3600; - return tokenInfo.token; + if (tokenInfo.token) { + await prisma.emailQueue.update({ + where: { id: queue.id }, + data: { + accessToken: tokenInfo.token, + expiresIn: expiryDate, + }, + }); + + return tokenInfo.token; + } else { + throw new Error("Unable to refresh access token."); + } } -} \ No newline at end of file +} diff --git a/apps/api/src/lib/services/imap.service.ts b/apps/api/src/lib/services/imap.service.ts index 783b0db69..878d71616 100644 --- a/apps/api/src/lib/services/imap.service.ts +++ b/apps/api/src/lib/services/imap.service.ts @@ -1,52 +1,64 @@ -import EmailReplyParser from 'email-reply-parser'; -import Imap from 'imap'; -import { simpleParser } from 'mailparser'; -import { prisma } from '../../prisma'; -import { EmailConfig, EmailQueue } from '../types/email'; -import { AuthService } from './auth.service'; +import EmailReplyParser from "email-reply-parser"; +import Imap from "imap"; +import { simpleParser } from "mailparser"; +import { prisma } from "../../prisma"; +import { EmailConfig, EmailQueue } from "../types/email"; +import { AuthService } from "./auth.service"; function getReplyText(email: any): string { - const parsed = new EmailReplyParser().read(email.text); - let replyText = '' + const parsed = new EmailReplyParser().read(email.text); + const fragments = parsed.getFragments(); - parsed.fragments.forEach(fragment => { - if (fragment.isHidden() && !fragment.isSignature() && !fragment.isQuoted()) return; - replyText += fragment.content; - }); + let replyText = ""; - return replyText; - + fragments.forEach((fragment: any) => { + console.log("FRAGMENT", fragment._content, fragment.content); + if (!fragment._isHidden && !fragment._isSignature && !fragment._isQuoted) { + replyText += fragment._content; + } + }); + + return replyText; } export class ImapService { private static async getImapConfig(queue: EmailQueue): Promise { switch (queue.serviceType) { - case 'gmail': { - const validatedAccessToken = await AuthService.getValidAccessToken(queue); + case "gmail": { + const validatedAccessToken = await AuthService.getValidAccessToken( + queue + ); + return { user: queue.username, host: queue.hostname, port: 993, tls: true, - xoauth2: AuthService.generateXOAuth2Token(queue.username, validatedAccessToken), - tlsOptions: { rejectUnauthorized: false, servername: queue.hostname } + xoauth2: AuthService.generateXOAuth2Token( + queue.username, + validatedAccessToken + ), + tlsOptions: { rejectUnauthorized: false, servername: queue.hostname }, }; } - case 'other': + case "other": return { user: queue.username, password: queue.password, host: queue.hostname, port: queue.tls ? 993 : 143, tls: queue.tls || false, - tlsOptions: { rejectUnauthorized: false, servername: queue.hostname } + tlsOptions: { rejectUnauthorized: false, servername: queue.hostname }, }; default: - throw new Error('Unsupported service type'); + throw new Error("Unsupported service type"); } } - private static async processEmail(parsed: any, isReply: boolean): Promise { + private static async processEmail( + parsed: any, + isReply: boolean + ): Promise { const { from, subject, text, html, textAsHtml } = parsed; if (isReply) { @@ -57,7 +69,7 @@ export class ImapService { const ticketId = ticketIdMatch[1]; const ticket = await prisma.ticket.findFirst({ - where: { Number: Number(ticketId) } + where: { Number: Number(ticketId) }, }); if (!ticket) { @@ -65,93 +77,96 @@ export class ImapService { } const replyText = getReplyText(parsed); + await prisma.comment.create({ data: { - text: text ? replyText : 'No Body', + text: text ? replyText : "No Body", userId: null, ticketId: ticket.id, reply: true, replyEmail: from.value[0].address, - public: true - } + public: true, + }, }); } else { const imapEmail = await prisma.imap_Email.create({ data: { from: from.value[0].address, - subject: subject || 'No Subject', - body: text || 'No Body', - html: html || '', - text: textAsHtml - } + subject: subject || "No Subject", + body: text || "No Body", + html: html || "", + text: textAsHtml, + }, }); await prisma.ticket.create({ data: { email: from.value[0].address, name: from.value[0].name, - title: imapEmail.subject || '-', + title: imapEmail.subject || "-", isComplete: false, - priority: 'Low', + priority: "Low", fromImap: true, - detail: html || textAsHtml - } + detail: html || textAsHtml, + }, }); } } static async fetchEmails(): Promise { - const queues = (await prisma.emailQueue.findMany()) as unknown as EmailQueue[]; + const queues = + (await prisma.emailQueue.findMany()) as unknown as EmailQueue[]; const today = new Date(); for (const queue of queues) { try { const imapConfig = await this.getImapConfig(queue); - if (!imapConfig.password) { - console.error('IMAP configuration is missing a password'); - throw new Error('IMAP configuration is missing a password'); + + if (queue.serviceType === "other" && !imapConfig.password) { + console.error("IMAP configuration is missing a password"); + throw new Error("IMAP configuration is missing a password"); } // @ts-ignore const imap = new Imap(imapConfig); await new Promise((resolve, reject) => { - imap.once('ready', () => { - imap.openBox('INBOX', false, (err) => { + imap.once("ready", () => { + imap.openBox("INBOX", false, (err) => { if (err) { reject(err); return; } - imap.search(['UNSEEN', ['ON', today]], (err, results) => { + imap.search(["UNSEEN", ["ON", today]], (err, results) => { if (err) reject(err); if (!results?.length) { - console.log('No new messages'); + console.log("No new messages"); imap.end(); resolve(null); return; } - const fetch = imap.fetch(results, { bodies: '' }); - - fetch.on('message', (msg) => { - msg.on('body', (stream) => { + const fetch = imap.fetch(results, { bodies: "" }); + + fetch.on("message", (msg) => { + msg.on("body", (stream) => { simpleParser(stream, async (err, parsed) => { if (err) throw err; - const isReply = parsed.subject?.includes('Re:'); + const isReply = parsed.subject?.includes("Re:"); await this.processEmail(parsed, isReply || false); }); }); - msg.once('attributes', (attrs) => { - imap.addFlags(attrs.uid, ['\\Seen'], () => { - console.log('Marked as read!'); + msg.once("attributes", (attrs) => { + imap.addFlags(attrs.uid, ["\\Seen"], () => { + console.log("Marked as read!"); }); }); }); - fetch.once('error', reject); - fetch.once('end', () => { - console.log('Done fetching messages'); + fetch.once("error", reject); + fetch.once("end", () => { + console.log("Done fetching messages"); imap.end(); resolve(null); }); @@ -159,9 +174,9 @@ export class ImapService { }); }); - imap.once('error', reject); - imap.once('end', () => { - console.log('Connection ended'); + imap.once("error", reject); + imap.once("end", () => { + console.log("Connection ended"); resolve(null); }); @@ -172,4 +187,4 @@ export class ImapService { } } } -} \ No newline at end of file +} From f36ab11132de548671d5e865519d491e626f6162 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 13:35:05 +0000 Subject: [PATCH 04/13] fix: duplication bug --- apps/api/src/lib/roles.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/api/src/lib/roles.ts b/apps/api/src/lib/roles.ts index f68e68c10..03705b256 100644 --- a/apps/api/src/lib/roles.ts +++ b/apps/api/src/lib/roles.ts @@ -26,9 +26,9 @@ export function hasPermission( requireAll: boolean = true ): boolean { // Admins have all permissions - if (user?.isAdmin) { - return true; - } + if (user?.isAdmin) { + return true; + } // Convert single permission to array for consistent handling const permissions = Array.isArray(requiredPermissions) @@ -93,15 +93,16 @@ export function requirePermission( if (!hasPermission(userWithRoles, requiredPermissions, requireAll)) { return res.status(401).send({ - message: "You do not have the required permission to access this resource.", + message: + "You do not have the required permission to access this resource.", success: false, status: 403, }); } - next(); + return; } else { - next(); + return; } } catch (error) { next(error); From 5e8816fd7b172cd4a015f1bfad4f4d7190eef88f Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 16:47:38 +0000 Subject: [PATCH 05/13] patch: improved session sec --- apps/api/src/controllers/auth.ts | 155 ++++++++++++------ apps/api/src/lib/session.ts | 67 ++++++-- .../20241114164206_sessions/migration.sql | 5 + apps/api/src/prisma/schema.prisma | 7 +- 4 files changed, 172 insertions(+), 62 deletions(-) create mode 100644 apps/api/src/prisma/migrations/20241114164206_sessions/migration.sql diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index f98b646c7..d502c7d78 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -1,5 +1,6 @@ import axios from "axios"; import bcrypt from "bcrypt"; +import crypto from "crypto"; import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import jwt from "jsonwebtoken"; import { LRUCache } from "lru-cache"; @@ -320,22 +321,31 @@ export function authRoutes(fastify: FastifyInstance) { throw new Error("Password is not valid"); } - var b64string = process.env.SECRET; - var buf = new Buffer(b64string!, "base64"); // Ta-da - - let token = jwt.sign( + // Generate a secure session token + var secret = Buffer.from(process.env.SECRET!, "base64"); + const token = jwt.sign( { - data: { id: user!.id }, + data: { + id: user!.id, + // Add a unique identifier for this session + sessionId: crypto.randomBytes(32).toString('hex') + } }, - buf, - { expiresIn: "7d" } + secret, + { + expiresIn: "8h", + algorithm: 'HS256' + } ); + // Store session with additional security info await prisma.session.create({ data: { userId: user!.id, sessionToken: token, - expires: new Date(Date.now() + 60 * 60 * 1000), + expires: new Date(Date.now() + 8 * 60 * 60 * 1000), // 8 hours + userAgent: request.headers['user-agent'] || '', + ipAddress: request.ip, }, }); @@ -763,18 +773,12 @@ export function authRoutes(fastify: FastifyInstance) { password: string; }; - const bearer = request.headers.authorization!.split(" ")[1]; - - let session = await prisma.session.findUnique({ - where: { - sessionToken: bearer, - }, - }); + const session = await checkSession(request); const hashedPass = await bcrypt.hash(password, 10); await prisma.user.update({ - where: { id: session?.userId }, + where: { id: session?.id }, data: { password: hashedPass, }, @@ -831,13 +835,7 @@ export function authRoutes(fastify: FastifyInstance) { fastify.put( "/api/v1/auth/profile", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - - let session = await prisma.session.findUnique({ - where: { - sessionToken: bearer, - }, - }); + const session = await checkSession(request); const { name, email, language } = request.body as { name: string; @@ -846,7 +844,7 @@ export function authRoutes(fastify: FastifyInstance) { }; let user = await prisma.user.update({ - where: { id: session?.userId }, + where: { id: session?.id }, data: { name: name, email: email, @@ -864,12 +862,7 @@ export function authRoutes(fastify: FastifyInstance) { fastify.put( "/api/v1/auth/profile/notifcations/emails", async (request: FastifyRequest, reply: FastifyReply) => { - const bearer = request.headers.authorization!.split(" ")[1]; - let session = await prisma.session.findUnique({ - where: { - sessionToken: bearer, - }, - }); + const session = await checkSession(request); const { notify_ticket_created, @@ -879,7 +872,7 @@ export function authRoutes(fastify: FastifyInstance) { } = request.body as any; let user = await prisma.user.update({ - where: { id: session?.userId }, + where: { id: session?.id }, data: { notify_ticket_created: notify_ticket_created, notify_ticket_assigned: notify_ticket_assigned, @@ -912,28 +905,37 @@ export function authRoutes(fastify: FastifyInstance) { fastify.put( "/api/v1/auth/user/role", async (request: FastifyRequest, reply: FastifyReply) => { - const { id, role } = request.body as { id: string; role: boolean }; - // check for atleast one admin on role downgrade - if (role === false) { - const admins = await prisma.user.findMany({ - where: { isAdmin: true }, - }); - if (admins.length === 1) { - reply.code(400).send({ - message: "Atleast one admin is required", - success: false, + const session = await checkSession(request); + + if (session?.isAdmin) { + const { id, role } = request.body as { id: string; role: boolean }; + // check for atleast one admin on role downgrade + if (role === false) { + const admins = await prisma.user.findMany({ + where: { isAdmin: true }, }); - return; + if (admins.length === 1) { + reply.code(400).send({ + message: "Atleast one admin is required", + success: false, + }); + return; + } } - } - await prisma.user.update({ - where: { id }, - data: { - isAdmin: role, - }, - }); + await prisma.user.update({ + where: { id }, + data: { + isAdmin: role, + }, + }); - reply.send({ success: true }); + reply.send({ success: true }); + } else { + reply.code(401).send({ + message: "Unauthorized", + success: false, + }); + } } ); @@ -955,4 +957,57 @@ export function authRoutes(fastify: FastifyInstance) { reply.send({ success: true }); } ); + + // Add a new endpoint to list and manage active sessions + fastify.get("/api/v1/auth/sessions", + async (request: FastifyRequest, reply: FastifyReply) => { + const currentUser = await checkSession(request); + if (!currentUser) { + return reply.code(401).send({ message: "Unauthorized" }); + } + + const sessions = await prisma.session.findMany({ + where: { userId: currentUser.id }, + select: { + id: true, + userAgent: true, + ipAddress: true, + createdAt: true, + expires: true + } + }); + + reply.send({ sessions }); + } + ); + + // Add ability to revoke specific sessions + fastify.delete("/api/v1/auth/sessions/:sessionId", + async (request: FastifyRequest, reply: FastifyReply) => { + const currentUser = await checkSession(request); + if (!currentUser) { + return reply.code(401).send({ message: "Unauthorized" }); + } + + const { sessionId } = request.params as { sessionId: string }; + + // Only allow users to delete their own sessions + const session = await prisma.session.findFirst({ + where: { + id: sessionId, + userId: currentUser.id + } + }); + + if (!session) { + return reply.code(404).send({ message: "Session not found" }); + } + + await prisma.session.delete({ + where: { id: sessionId } + }); + + reply.send({ success: true }); + } + ); } diff --git a/apps/api/src/lib/session.ts b/apps/api/src/lib/session.ts index c302e7795..ed2dfa3a3 100644 --- a/apps/api/src/lib/session.ts +++ b/apps/api/src/lib/session.ts @@ -1,18 +1,63 @@ +import { FastifyRequest } from "fastify"; +import jwt from "jsonwebtoken"; import { prisma } from "../prisma"; // Checks session token and returns user object -export async function checkSession(request: any) { - const token = request.headers.authorization!.split(" ")[1]; +export async function checkSession(request: FastifyRequest) { + try { + const bearer = request.headers.authorization?.split(" ")[1]; + if (!bearer) { + return null; + } - let session = await prisma.session.findUnique({ - where: { - sessionToken: token, - }, - }); + // Verify JWT token is valid + var b64string = process.env.SECRET; + var secret = Buffer.from(b64string!, "base64"); - let user = await prisma.user.findUnique({ - where: { id: session!.userId }, - }); + try { + jwt.verify(bearer, secret); + } catch (e) { + // Token is invalid or expired + await prisma.session.delete({ + where: { sessionToken: bearer }, + }); + return null; + } - return user; + // Check if session exists and is not expired + const session = await prisma.session.findUnique({ + where: { sessionToken: bearer }, + include: { user: true }, + }); + + if (!session || session.expires < new Date()) { + // Session expired or doesn't exist + if (session) { + await prisma.session.delete({ + where: { id: session.id }, + }); + } + return null; + } + + // Verify the request is coming from the same client + const currentUserAgent = request.headers["user-agent"]; + const currentIp = request.ip; + + if ( + session.userAgent !== currentUserAgent && + session.ipAddress !== currentIp + ) { + // Potential session hijacking attempt - invalidate the session + await prisma.session.delete({ + where: { id: session.id }, + }); + + return null; + } + + return session.user; + } catch (error) { + return null; + } } diff --git a/apps/api/src/prisma/migrations/20241114164206_sessions/migration.sql b/apps/api/src/prisma/migrations/20241114164206_sessions/migration.sql new file mode 100644 index 000000000..d0d9480e0 --- /dev/null +++ b/apps/api/src/prisma/migrations/20241114164206_sessions/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Session" ADD COLUMN "apiKey" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "ipAddress" TEXT, +ADD COLUMN "userAgent" TEXT; diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index 50450db8d..50a49029a 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -48,7 +48,12 @@ model Session { sessionToken String @unique userId String expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + userAgent String? + ipAddress String? + apiKey Boolean @default(false) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model PasswordResetToken { From 5291aecf162c77fb0f48d8f6bcc48c7c481f0eef Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 17:16:36 +0000 Subject: [PATCH 06/13] fix: admin only check --- apps/api/src/controllers/users.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/api/src/controllers/users.ts b/apps/api/src/controllers/users.ts index 8689dba12..fa6d17892 100644 --- a/apps/api/src/controllers/users.ts +++ b/apps/api/src/controllers/users.ts @@ -2,6 +2,7 @@ import bcrypt from "bcrypt"; import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import { track } from "../lib/hog"; +import { checkSession } from "../lib/session"; import { prisma } from "../prisma"; export function userRoutes(fastify: FastifyInstance) { @@ -70,20 +71,25 @@ export function userRoutes(fastify: FastifyInstance) { // (ADMIN) Reset password fastify.put( "/api/v1/user/reset-password", - async (request: FastifyRequest, reply: FastifyReply) => { const { password, id }: any = request.body; - const hashedPass = await bcrypt.hash(password, 10); - await prisma.user.update({ - where: { id: id }, - data: { - password: hashedPass, - }, - }); - reply - .status(201) - .send({ message: "password updated success", failed: false }); + const session = await checkSession(request); + + if (session!.isAdmin) { + const hashedPass = await bcrypt.hash(password, 10); + await prisma.user.update({ + where: { id: id }, + data: { + password: hashedPass, + }, + }); + reply + .status(201) + .send({ message: "password updated success", failed: false }); + } else { + reply.status(403).send({ message: "Unauthorized", failed: true }); + } } ); From 7d5199de9c36ca6cb8c98ba8740c55d2c8e0c9c2 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 17:18:04 +0000 Subject: [PATCH 07/13] fix: admin only --- apps/api/src/controllers/users.ts | 46 +++++++++++++++++-------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/apps/api/src/controllers/users.ts b/apps/api/src/controllers/users.ts index fa6d17892..5cbf5393a 100644 --- a/apps/api/src/controllers/users.ts +++ b/apps/api/src/controllers/users.ts @@ -38,33 +38,39 @@ export function userRoutes(fastify: FastifyInstance) { "/api/v1/user/new", async (request: FastifyRequest, reply: FastifyReply) => { - const { email, password, name, admin }: any = request.body; + const session = await checkSession(request); - const e = email.toLowerCase(); + if (session!.isAdmin) { + const { email, password, name, admin }: any = request.body; - const hash = await bcrypt.hash(password, 10); + const e = email.toLowerCase(); - await prisma.user.create({ - data: { - name, - email: e, - password: hash, - isAdmin: admin, - }, - }); + const hash = await bcrypt.hash(password, 10); - const client = track(); + await prisma.user.create({ + data: { + name, + email: e, + password: hash, + isAdmin: admin, + }, + }); - client.capture({ - event: "user_created", - distinctId: "uuid", - }); + const client = track(); - client.shutdownAsync(); + client.capture({ + event: "user_created", + distinctId: "uuid", + }); - reply.send({ - success: true, - }); + client.shutdownAsync(); + + reply.send({ + success: true, + }); + } else { + reply.status(403).send({ message: "Unauthorized", failed: true }); + } } ); From 89cb5938eadbaba6a209067aafae687ac05b6102 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 17:23:53 +0000 Subject: [PATCH 08/13] patch: correct handlers --- apps/api/src/controllers/auth.ts | 47 ++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts index d502c7d78..f686bef30 100644 --- a/apps/api/src/controllers/auth.ts +++ b/apps/api/src/controllers/auth.ts @@ -9,6 +9,7 @@ import { AuthorizationCode } from "simple-oauth2"; import { getOAuthProvider, getOidcConfig } from "../lib/auth"; import { track } from "../lib/hog"; import { forgotPassword } from "../lib/nodemailer/auth/forgot-password"; +import { requirePermission } from "../lib/roles"; import { checkSession } from "../lib/session"; import { getOAuthClient } from "../lib/utils/oauth_client"; import { getOidcClient } from "../lib/utils/oidc_client"; @@ -325,16 +326,16 @@ export function authRoutes(fastify: FastifyInstance) { var secret = Buffer.from(process.env.SECRET!, "base64"); const token = jwt.sign( { - data: { + data: { id: user!.id, // Add a unique identifier for this session - sessionId: crypto.randomBytes(32).toString('hex') - } + sessionId: crypto.randomBytes(32).toString("hex"), + }, }, secret, - { + { expiresIn: "8h", - algorithm: 'HS256' + algorithm: "HS256", } ); @@ -344,7 +345,7 @@ export function authRoutes(fastify: FastifyInstance) { userId: user!.id, sessionToken: token, expires: new Date(Date.now() + 8 * 60 * 60 * 1000), // 8 hours - userAgent: request.headers['user-agent'] || '', + userAgent: request.headers["user-agent"] || "", ipAddress: request.ip, }, }); @@ -697,6 +698,9 @@ export function authRoutes(fastify: FastifyInstance) { // Delete a user fastify.delete( "/api/v1/auth/user/:id", + { + preHandler: requirePermission(["user::delete"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const { id } = request.params as { id: string }; @@ -793,6 +797,9 @@ export function authRoutes(fastify: FastifyInstance) { // Reset password by admin fastify.post( "/api/v1/auth/admin/reset-password", + { + preHandler: requirePermission(["user::manage"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { let { password, user } = request.body as { password: string; @@ -834,6 +841,9 @@ export function authRoutes(fastify: FastifyInstance) { // Update a users profile/config fastify.put( "/api/v1/auth/profile", + { + preHandler: requirePermission(["user::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const session = await checkSession(request); @@ -861,6 +871,9 @@ export function authRoutes(fastify: FastifyInstance) { // Update a users Email notification settings fastify.put( "/api/v1/auth/profile/notifcations/emails", + { + preHandler: requirePermission(["user::update"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const session = await checkSession(request); @@ -904,12 +917,14 @@ export function authRoutes(fastify: FastifyInstance) { // Update a users role fastify.put( "/api/v1/auth/user/role", + { + preHandler: requirePermission(["user::manage"]), + }, async (request: FastifyRequest, reply: FastifyReply) => { const session = await checkSession(request); if (session?.isAdmin) { const { id, role } = request.body as { id: string; role: boolean }; - // check for atleast one admin on role downgrade if (role === false) { const admins = await prisma.user.findMany({ where: { isAdmin: true }, @@ -959,7 +974,8 @@ export function authRoutes(fastify: FastifyInstance) { ); // Add a new endpoint to list and manage active sessions - fastify.get("/api/v1/auth/sessions", + fastify.get( + "/api/v1/auth/sessions", async (request: FastifyRequest, reply: FastifyReply) => { const currentUser = await checkSession(request); if (!currentUser) { @@ -973,8 +989,8 @@ export function authRoutes(fastify: FastifyInstance) { userAgent: true, ipAddress: true, createdAt: true, - expires: true - } + expires: true, + }, }); reply.send({ sessions }); @@ -982,7 +998,8 @@ export function authRoutes(fastify: FastifyInstance) { ); // Add ability to revoke specific sessions - fastify.delete("/api/v1/auth/sessions/:sessionId", + fastify.delete( + "/api/v1/auth/sessions/:sessionId", async (request: FastifyRequest, reply: FastifyReply) => { const currentUser = await checkSession(request); if (!currentUser) { @@ -993,10 +1010,10 @@ export function authRoutes(fastify: FastifyInstance) { // Only allow users to delete their own sessions const session = await prisma.session.findFirst({ - where: { + where: { id: sessionId, - userId: currentUser.id - } + userId: currentUser.id, + }, }); if (!session) { @@ -1004,7 +1021,7 @@ export function authRoutes(fastify: FastifyInstance) { } await prisma.session.delete({ - where: { id: sessionId } + where: { id: sessionId }, }); reply.send({ success: true }); From 44204cdacc5a771925be0332a0ca19d2e537f685 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 22:46:14 +0000 Subject: [PATCH 09/13] feat: delete comments --- apps/api/src/controllers/ticket.ts | 20 +++++++++++ apps/api/src/lib/services/imap.service.ts | 2 +- .../client/components/TicketDetails/index.tsx | 34 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index 018b8fe34..240548f80 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -527,6 +527,26 @@ export function ticketRoutes(fastify: FastifyInstance) { } ); + fastify.post( + "/api/v1/ticket/comment/delete", + { + preHandler: requirePermission(["issue::comment"]), + }, + async (request: FastifyRequest, reply: FastifyReply) => { + const { id }: any = request.body; + + await prisma.comment.delete({ + where: { + id: id, + }, + }); + + reply.send({ + success: true, + }); + } + ); + // Update status of a ticket fastify.put( "/api/v1/ticket/status/update", diff --git a/apps/api/src/lib/services/imap.service.ts b/apps/api/src/lib/services/imap.service.ts index 878d71616..631bb205e 100644 --- a/apps/api/src/lib/services/imap.service.ts +++ b/apps/api/src/lib/services/imap.service.ts @@ -105,7 +105,7 @@ export class ImapService { name: from.value[0].name, title: imapEmail.subject || "-", isComplete: false, - priority: "Low", + priority: "low", fromImap: true, detail: html || textAsHtml, }, diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index 01d4337b3..d9d8d6e55 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -272,6 +272,29 @@ export default function Ticket() { .then(() => refetch()); } + async function deleteComment(id: string) { + await fetch(`/api/v1/ticket/comment/delete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ id }), + }) + .then((res) => res.json()) + .then((res) => { + if (res.success) { + refetch(); + } else { + toast({ + variant: "destructive", + title: "Error", + description: "Failed to delete comment", + }); + } + }); + } + async function addTime() { if (data && data.ticket && data.ticket.locked) return; @@ -846,7 +869,7 @@ export default function Ticket() { data.ticket.comments.map((comment: any) => (
  • @@ -869,6 +892,15 @@ export default function Ticket() { {moment(comment.createdAt).format("LLL")} + {comment.user && + comment.userId === user.id && ( + { + deleteComment(comment.id); + }} + /> + )}
    {comment.text}
  • From acb4a67b37abef710a18aae45b08d6a89310afc0 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 22:56:25 +0000 Subject: [PATCH 10/13] fix: update perms --- apps/client/@/shadcn/lib/types/permissions.ts | 20 +-- .../client/components/TicketDetails/index.tsx | 149 ++++++++++++------ 2 files changed, 109 insertions(+), 60 deletions(-) diff --git a/apps/client/@/shadcn/lib/types/permissions.ts b/apps/client/@/shadcn/lib/types/permissions.ts index ba0f146bd..059e2f32d 100644 --- a/apps/client/@/shadcn/lib/types/permissions.ts +++ b/apps/client/@/shadcn/lib/types/permissions.ts @@ -168,16 +168,16 @@ export const PERMISSIONS_CONFIG = [ // "kb::manage" // ] // }, - { - category: "System Settings", - permissions: [ - "settings::view", - "settings::manage", - "webhook::manage", - "integration::manage", - "email_template::manage", - ], - }, + // { + // category: "System Settings", + // permissions: [ + // "settings::view", + // "settings::manage", + // "webhook::manage", + // "integration::manage", + // "email_template::manage", + // ], + // }, // { // category: "Time Tracking", // permissions: [ diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index d9d8d6e55..100149247 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -156,7 +156,7 @@ export default function Ticket() { async function update() { if (data && data.ticket && data.ticket.locked) return; - await fetch(`/api/v1/ticket/update`, { + const res = await fetch(`/api/v1/ticket/update`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -167,38 +167,52 @@ export default function Ticket() { detail: JSON.stringify(debouncedValue), note, title: debounceTitle, - priority: priority ? priority.value : undefined, - status: ticketStatus ? ticketStatus.value : undefined, + priority: priority?.value, + status: ticketStatus?.value, }), - }) - .then((res) => res.json()) - .then(() => { - setEdit(false); + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to update ticket", }); + return; + } + setEdit(false); } async function updateStatus() { if (data && data.ticket && data.ticket.locked) return; - await fetch(`/api/v1/ticket/status/update`, { + const res = await fetch(`/api/v1/ticket/status/update`, { method: "PUT", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ status: !data.ticket.isComplete, id, }), - }) - .then((res) => res.json()) - .then(() => refetch()); + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to update status", + }); + return; + } + refetch(); } async function hide(hidden) { if (data && data.ticket && data.ticket.locked) return; - await fetch(`/api/v1/ticket/status/hide`, { + const res = await fetch(`/api/v1/ticket/status/hide`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -208,13 +222,21 @@ export default function Ticket() { hidden, id, }), - }) - .then((res) => res.json()) - .then(() => refetch()); + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to update visibility", + }); + return; + } + refetch(); } async function lock(locked) { - await fetch(`/api/v1/ticket/status/lock`, { + const res = await fetch(`/api/v1/ticket/status/lock`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -224,9 +246,17 @@ export default function Ticket() { locked, id, }), - }) - .then((res) => res.json()) - .then(() => refetch()); + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to update lock status", + }); + return; + } + refetch(); } async function deleteIssue(locked) { @@ -256,7 +286,7 @@ export default function Ticket() { async function addComment() { if (data && data.ticket && data.ticket.locked) return; - await fetch(`/api/v1/ticket/comment`, { + const res = await fetch(`/api/v1/ticket/comment`, { method: "POST", headers: { "Content-Type": "application/json", @@ -267,9 +297,17 @@ export default function Ticket() { id, public: publicComment, }), - }) - .then((res) => res.json()) - .then(() => refetch()); + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to add comment", + }); + return; + } + refetch(); } async function deleteComment(id: string) { @@ -326,44 +364,55 @@ export default function Ticket() { } async function fetchUsers() { - await fetch(`/api/v1/users/all`, { + const res = await fetch(`/api/v1/users/all`, { method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, - }) - .then((res) => res.json()) - .then((res) => { - if (res) { - setUsers(res.users); - } + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to fetch users", }); + return; + } + + if (res.users) { + setUsers(res.users); + } } async function transferTicket() { if (data && data.ticket && data.ticket.locked) return; + if (n === undefined) return; - if (n !== undefined) { - await fetch(`/api/v1/ticket/transfer`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - user: n.id, - id, - }), - }) - .then((res) => res.json()) - .then((res) => { - if (res.success) { - setAssignedEdit(false); - refetch(); - } - }); + const res = await fetch(`/api/v1/ticket/transfer`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + user: n.id, + id, + }), + }).then((res) => res.json()); + + if (!res.success) { + toast({ + variant: "destructive", + title: "Error", + description: res.message || "Failed to transfer ticket", + }); + return; } + + setAssignedEdit(false); + refetch(); } const handleFileChange = (e: React.ChangeEvent) => { From 78388de8e32460ba3740ec786acbc89b7d53176e Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 22:57:01 +0000 Subject: [PATCH 11/13] patch: temp hide --- apps/client/@/shadcn/lib/types/permissions.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/client/@/shadcn/lib/types/permissions.ts b/apps/client/@/shadcn/lib/types/permissions.ts index 059e2f32d..261056061 100644 --- a/apps/client/@/shadcn/lib/types/permissions.ts +++ b/apps/client/@/shadcn/lib/types/permissions.ts @@ -118,16 +118,16 @@ export const PERMISSIONS_CONFIG = [ "issue::comment", ], }, - { - category: "User Management", - permissions: [ - "user::create", - "user::read", - "user::update", - "user::delete", - "user::manage", - ], - }, + // { + // category: "User Management", + // permissions: [ + // "user::create", + // "user::read", + // "user::update", + // "user::delete", + // "user::manage", + // ], + // }, { category: "Role Management", permissions: [ From 2eab42c35446bdf00eba2088e966bd0b560919a2 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 22:57:25 +0000 Subject: [PATCH 12/13] patch: update perms --- apps/client/@/shadcn/lib/types/permissions.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/client/@/shadcn/lib/types/permissions.ts b/apps/client/@/shadcn/lib/types/permissions.ts index 261056061..71bd79c44 100644 --- a/apps/client/@/shadcn/lib/types/permissions.ts +++ b/apps/client/@/shadcn/lib/types/permissions.ts @@ -148,16 +148,16 @@ export const PERMISSIONS_CONFIG = [ // "team::manage" // ] // }, - { - category: "Client Management", - permissions: [ - "client::create", - "client::read", - "client::update", - "client::delete", - "client::manage", - ], - }, + // { + // category: "Client Management", + // permissions: [ + // "client::create", + // "client::read", + // "client::update", + // "client::delete", + // "client::manage", + // ], + // }, // { // category: "Knowledge Base", // permissions: [ From a3ef4ec541ce82111b3b1082cde2e211990759f9 Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Thu, 14 Nov 2024 23:01:19 +0000 Subject: [PATCH 13/13] fix: ui update --- apps/client/components/TicketDetails/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index 100149247..615931e27 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -189,7 +189,7 @@ export default function Ticket() { const res = await fetch(`/api/v1/ticket/status/update`, { method: "PUT", headers: { - "Content-Type": "application/json", + "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ @@ -226,7 +226,7 @@ export default function Ticket() { if (!res.success) { toast({ - variant: "destructive", + variant: "destructive", title: "Error", description: res.message || "Failed to update visibility", }); @@ -251,7 +251,7 @@ export default function Ticket() { if (!res.success) { toast({ variant: "destructive", - title: "Error", + title: "Error", description: res.message || "Failed to update lock status", }); return; @@ -380,7 +380,7 @@ export default function Ticket() { }); return; } - + if (res.users) { setUsers(res.users); } @@ -410,7 +410,7 @@ export default function Ticket() { }); return; } - + setAssignedEdit(false); refetch(); } @@ -504,7 +504,7 @@ export default function Ticket() { // Loads the previously stored editor contents. useEffect(() => { - if (status === "success") { + if (status === "success" && data && data.ticket) { loadFromStorage().then((content) => { if (typeof content === "object") { setInitialContent(content);