diff --git a/apps/api/docker-compose.yml b/apps/api/docker-compose.yml
index e7220ca6f..5667d5fd0 100644
--- a/apps/api/docker-compose.yml
+++ b/apps/api/docker-compose.yml
@@ -13,6 +13,11 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
+ mailhog:
+ image: jcalonso/mailhog
+ ports:
+ - 1025:1025 # smtp server
+ - 8025:8025 # web ui
volumes:
postgres_data:
diff --git a/apps/api/package.json b/apps/api/package.json
index 41eec030a..ade8215e0 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -16,6 +16,7 @@
"@types/bcrypt": "^5.0.0",
"@types/jsonwebtoken": "^8.5.8",
"@types/node": "^17.0.23",
+ "@types/nodemailer": "^6.4.14",
"@types/passport-local": "^1.0.35",
"prisma": "5.2.0",
"ts-node": "^10.7.0",
@@ -38,6 +39,7 @@
"imap": "^0.8.19",
"jsonwebtoken": "9.0.2",
"mailparser": "^3.6.5",
+ "nodemailer": "^6.9.7",
"posthog-node": "^3.1.3",
"prisma": "^5.6.0"
},
diff --git a/apps/api/src/controllers/auth.ts b/apps/api/src/controllers/auth.ts
index c4a0d5dfe..7f85004e1 100644
--- a/apps/api/src/controllers/auth.ts
+++ b/apps/api/src/controllers/auth.ts
@@ -1,7 +1,10 @@
+import axios from "axios";
import bcrypt from "bcrypt";
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import jwt from "jsonwebtoken";
+import { track } from "../lib/hog";
import { checkToken } from "../lib/jwt";
+import { forgotPassword } from "../lib/nodemailer/auth/forgot-password";
import { prisma } from "../prisma";
export function authRoutes(fastify: FastifyInstance) {
@@ -42,7 +45,7 @@ export function authRoutes(fastify: FastifyInstance) {
});
}
- await prisma.user.create({
+ const user = await prisma.user.create({
data: {
email,
password: await bcrypt.hash(password, 10),
@@ -51,13 +54,116 @@ export function authRoutes(fastify: FastifyInstance) {
},
});
+ const hog = track();
+
+ hog.capture({
+ event: "user_registered",
+ distinctId: user.id,
+ });
+
reply.send({
success: true,
});
}
);
- // User login route
+ // Forgot password & generate code
+ fastify.post(
+ "/api/v1/auth/password-reset",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const { email, link } = request.body as { email: string; link: string };
+
+ let user = await prisma.user.findUnique({
+ where: { email },
+ });
+
+ if (!user) {
+ reply.code(401).send({
+ message: "Invalid email",
+ success: false,
+ });
+ }
+
+ function generateRandomCode() {
+ const min = 100000; // Minimum 6-digit number
+ const max = 999999; // Maximum 6-digit number
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+
+ const code = generateRandomCode();
+
+ const uuid = await prisma.passwordResetToken.create({
+ data: {
+ userId: user!.id,
+ code: String(code),
+ },
+ });
+
+ forgotPassword(email, String(code), link, uuid.id);
+
+ reply.send({
+ success: true,
+ });
+ }
+ );
+
+ // Check code & uuid us valid
+ fastify.post(
+ "/api/v1/auth/password-reset/code",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const { code, uuid } = request.body as { code: string; uuid: string };
+
+ const reset = await prisma.passwordResetToken.findUnique({
+ where: { code: code, id: uuid },
+ });
+
+ if (!reset) {
+ reply.code(401).send({
+ message: "Invalid Code",
+ success: false,
+ });
+ } else {
+ reply.send({
+ success: true,
+ });
+ }
+ }
+ );
+
+ // Reset users password via code
+ fastify.post(
+ "/api/v1/auth/password-reset/password",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const { password, code } = request.body as {
+ password: string;
+ code: string;
+ };
+
+ const user = await prisma.passwordResetToken.findUnique({
+ where: { code: code },
+ });
+
+ if (!user) {
+ reply.code(401).send({
+ message: "Invalid Code",
+ success: false,
+ });
+ }
+
+ await prisma.user.update({
+ where: { id: user!.userId },
+ data: {
+ password: await bcrypt.hash(password, 10),
+ },
+ });
+
+ reply.send({
+ success: true,
+ });
+ }
+ );
+
+ // User password login route
fastify.post(
"/api/v1/auth/login",
{
@@ -137,6 +243,133 @@ export function authRoutes(fastify: FastifyInstance) {
}
);
+ // Checks if a user is SSO or password
+ fastify.post(
+ "/api/v1/auth/sso/check",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ let { email } = request.body as {
+ email: string;
+ };
+
+ let user = await prisma.user.findUnique({
+ where: { email },
+ });
+
+ if (!user) {
+ reply.code(401).send({
+ message: "Invalid email",
+ success: false,
+ });
+ }
+
+ const authtype = await prisma.config.findMany({
+ where: {
+ sso_active: true,
+ },
+ });
+
+ const provider = await prisma.provider.findMany();
+ const oauth = provider[0];
+
+ console.log(authtype);
+
+ if (authtype.length === 0) {
+ reply.code(200).send({
+ success: true,
+ message: "SSO not enabled",
+ oauth: false,
+ });
+ }
+
+ const url = "https://github.com/login/oauth/authorize";
+
+ reply.send({
+ oauth: true,
+ success: true,
+ ouath_url: `${url}?client_id=${oauth.clientId}&redirect_uri=${oauth.redirectUri}&state=${email}&login=${email}&scope=user`,
+ });
+ }
+ );
+
+ // SSO api callback route
+ fastify.get(
+ "/api/v1/auth/sso/login/callback",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const { code, state } = request.query as { code: string; state: string };
+
+ const provider = await prisma.provider.findFirst({});
+
+ const data = await axios.post(
+ `https://github.com/login/oauth/access_token`,
+ {
+ client_id: provider?.clientId,
+ client_secret: provider?.clientSecret,
+ code: code,
+ redirect_uri: provider?.redirectUri,
+ },
+ {
+ headers: {
+ Accept: "application/json",
+ },
+ }
+ );
+
+ const access_token = data.data;
+
+ if (access_token) {
+ const gh = await axios.get(`https://api.github.com/user/emails`, {
+ headers: {
+ Accept: "application/vnd.github+json",
+ Authorization: `token ${access_token.access_token}`,
+ },
+ });
+
+ const emails = gh.data;
+
+ const filter = emails.filter((e: any) => e.primary === true);
+
+ let user = await prisma.user.findUnique({
+ where: { email: filter[0].email },
+ });
+
+ if (!user) {
+ reply.send({
+ success: false,
+ message: "Invalid email",
+ });
+ }
+
+ var b64string = process.env.SECRET;
+ var buf = new Buffer(b64string!, "base64"); // Ta-da
+
+ let token = jwt.sign(
+ {
+ data: { id: user!.id },
+ },
+ buf,
+ { expiresIn: "8h" }
+ );
+
+ await prisma.session.create({
+ data: {
+ userId: user!.id,
+ sessionToken: token,
+ expires: new Date(Date.now() + 8 * 60 * 60 * 1000),
+ },
+ });
+
+ reply.send({
+ token,
+ success: true,
+ });
+ } else {
+ reply.status(403).send({
+ success: false,
+ });
+ }
+ }
+ );
+
// Delete a user
fastify.delete(
"/api/v1/auth/user/:id",
@@ -177,10 +410,12 @@ export function authRoutes(fastify: FastifyInstance) {
if (!user) {
reply.code(401).send({
- message: "Invalid email or password",
+ message: "Invalid user",
});
}
+ const config = await prisma.config.findFirst();
+
const data = {
id: user!.id,
email: user!.email,
@@ -191,6 +426,7 @@ export function authRoutes(fastify: FastifyInstance) {
ticket_status_changed: user!.notify_ticket_status_changed,
ticket_comments: user!.notify_ticket_comments,
ticket_assigned: user!.notify_ticket_assigned,
+ sso_status: config!.sso_active,
};
reply.send({
@@ -282,6 +518,50 @@ export function authRoutes(fastify: FastifyInstance) {
}
);
+ // Update a users Email notification settings
+ fastify.put(
+ "/api/v1/auth/profile/notifcations/emails",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+
+ //checks if token is valid and returns valid token
+ const token = checkToken(bearer);
+
+ if (token) {
+ let session = await prisma.session.findUnique({
+ where: {
+ sessionToken: bearer,
+ },
+ });
+
+ const {
+ notify_ticket_created,
+ notify_ticket_assigned,
+ notify_ticket_comments,
+ notify_ticket_status_changed,
+ } = request.body as any;
+
+ let user = await prisma.user.update({
+ where: { id: session?.userId },
+ data: {
+ notify_ticket_created: notify_ticket_created,
+ notify_ticket_assigned: notify_ticket_assigned,
+ notify_ticket_comments: notify_ticket_comments,
+ notify_ticket_status_changed: notify_ticket_status_changed,
+ },
+ });
+
+ reply.send({
+ user,
+ });
+ } else {
+ reply.send({
+ sucess: false,
+ });
+ }
+ }
+ );
+
// Logout a user (deletes session)
fastify.get(
"/api/v1/auth/user/:id/logout",
@@ -299,4 +579,25 @@ export function authRoutes(fastify: FastifyInstance) {
}
}
);
+
+ // Update a users role
+ fastify.put(
+ "/api/v1/auth/user/role",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+ if (token) {
+ const { id, role } = request.body as { id: string; role: boolean };
+
+ await prisma.user.update({
+ where: { id },
+ data: {
+ isAdmin: role,
+ },
+ });
+
+ reply.send({ success: true });
+ }
+ }
+ );
}
diff --git a/apps/api/src/controllers/clients.ts b/apps/api/src/controllers/clients.ts
index c3a9b63a5..d2c92adc5 100644
--- a/apps/api/src/controllers/clients.ts
+++ b/apps/api/src/controllers/clients.ts
@@ -1,4 +1,5 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
+import { track } from "../lib/hog";
import { checkToken } from "../lib/jwt";
import { prisma } from "../prisma";
@@ -14,7 +15,7 @@ export function clientRoutes(fastify: FastifyInstance) {
if (token) {
const { name, email, number, contactName }: any = request.body;
- await prisma.client.create({
+ const client = await prisma.client.create({
data: {
name,
contactName,
@@ -23,6 +24,13 @@ export function clientRoutes(fastify: FastifyInstance) {
},
});
+ const hog = track();
+
+ hog.capture({
+ event: "client_created",
+ distinctId: client.id,
+ });
+
reply.send({
success: true,
});
diff --git a/apps/api/src/controllers/config.ts b/apps/api/src/controllers/config.ts
new file mode 100644
index 000000000..9936d47d4
--- /dev/null
+++ b/apps/api/src/controllers/config.ts
@@ -0,0 +1,237 @@
+// Check Github Version
+// Add outbound email provider
+// Email Verification
+// SSO Provider
+// Portal Locale
+// Feature Flags
+
+import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
+
+import { checkToken } from "../lib/jwt";
+import { prisma } from "../prisma";
+
+export function configRoutes(fastify: FastifyInstance) {
+ // Check if SSO is enabled
+ fastify.get(
+ "/api/v1/config/sso/enabled",
+
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+
+ if (token) {
+ const config = await prisma.config.findFirst();
+
+ //@ts-expect-error
+ const { sso_active } = config;
+
+ if (sso_active) {
+ const provider = await prisma.provider.findFirst({});
+
+ reply.send({
+ success: true,
+ sso: sso_active,
+ provider: provider,
+ });
+ }
+
+ reply.send({
+ success: true,
+ sso: sso_active,
+ });
+ }
+ }
+ );
+
+ // Update SSO Provider Settings
+ fastify.post(
+ "/api/v1/config/sso/provider",
+
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+
+ if (token) {
+ const {
+ name,
+ client_id,
+ client_secret,
+ redirect_uri,
+ tenantId,
+ issuer,
+ }: any = request.body;
+
+ const conf = await prisma.config.findFirst();
+
+ //update config to true
+ await prisma.config.update({
+ where: { id: conf!.id },
+ data: {
+ sso_active: true,
+ sso_provider: name,
+ },
+ });
+
+ const check_provider = await prisma.provider.findFirst({});
+
+ if (check_provider === null) {
+ await prisma.provider.create({
+ data: {
+ name: name,
+ clientId: client_id,
+ clientSecret: client_secret,
+ active: true,
+ redirectUri: redirect_uri,
+ tenantId: tenantId,
+ issuer: issuer,
+ },
+ });
+ } else {
+ await prisma.provider.update({
+ where: { id: check_provider.id },
+ data: {
+ name: name,
+ clientId: client_id,
+ clientSecret: client_secret,
+ active: true,
+ redirectUri: redirect_uri,
+ tenantId: tenantId,
+ issuer: issuer,
+ },
+ });
+ }
+
+ reply.send({
+ success: true,
+ message: "SSO Provider updated!",
+ });
+ }
+ }
+ );
+
+ // Delete SSO Provider
+ fastify.delete(
+ "/api/v1/config/sso/provider",
+
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+
+ if (token) {
+ const conf = await prisma.config.findFirst();
+
+ //update config to true
+ await prisma.config.update({
+ where: { id: conf!.id },
+ data: {
+ sso_active: false,
+ sso_provider: "",
+ },
+ });
+
+ const provider = await prisma.provider.findFirst({});
+ await prisma.provider.delete({
+ where: { id: provider!.id },
+ });
+
+ reply.send({
+ success: true,
+ message: "SSO Provider deleted!",
+ });
+ }
+ }
+ );
+
+ // Check if Emails are enabled & GET email settings
+ fastify.get(
+ "/api/v1/config/email",
+
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+
+ if (token) {
+ // GET EMAIL SETTINGS
+ const config = await prisma.email.findFirst({});
+
+ if (config === null) {
+ reply.send({
+ success: true,
+ active: false,
+ });
+ }
+
+ if (config?.active) {
+ reply.send({
+ success: true,
+ active: true,
+ email: config,
+ });
+ } else {
+ reply.send({
+ success: true,
+ active: false,
+ email: config,
+ });
+ }
+ }
+ }
+ );
+
+ // Update Email Provider Settings
+ fastify.put(
+ "/api/v1/config/email",
+
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+
+ if (token) {
+ const {
+ host,
+ active,
+ port,
+ reply: replyto,
+ username,
+ password,
+ }: any = request.body;
+
+ const email = await prisma.email.findFirst();
+
+ if (email === null) {
+ await prisma.email.create({
+ data: {
+ host: host,
+ port: port,
+ reply: replyto,
+ user: username,
+ pass: password,
+ active: true,
+ },
+ });
+ } else {
+ await prisma.email.update({
+ where: { id: email.id },
+ data: {
+ host: host,
+ port: port,
+ reply: replyto,
+ user: username,
+ pass: password,
+ active: active,
+ },
+ });
+ }
+
+ reply.send({
+ success: true,
+ message: "SSO Provider updated!",
+ });
+ }
+ }
+ );
+
+ // Test email is working
+
+ // Disable/Enable Email
+}
diff --git a/apps/api/src/controllers/notifications.ts b/apps/api/src/controllers/notifications.ts
new file mode 100644
index 000000000..b5f9fe62d
--- /dev/null
+++ b/apps/api/src/controllers/notifications.ts
@@ -0,0 +1,4 @@
+// Create a new notification
+// Get All notifications
+// Mark as read
+// Delete notification
diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts
index 327ee6fa0..cd3dc0725 100644
--- a/apps/api/src/controllers/ticket.ts
+++ b/apps/api/src/controllers/ticket.ts
@@ -2,9 +2,24 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import axios from "axios";
import { checkToken } from "../lib/jwt";
+
+//@ts-ignore
+import { track } from "../lib/hog";
+import { sendAssignedEmail } from "../lib/nodemailer/ticket/assigned";
+import { sendComment } from "../lib/nodemailer/ticket/comment";
+import { sendTicketCreate } from "../lib/nodemailer/ticket/create";
+import { sendTicketStatus } from "../lib/nodemailer/ticket/status";
import { checkSession } from "../lib/session";
import { prisma } from "../prisma";
+const validateEmail = (email: string) => {
+ return String(email)
+ .toLowerCase()
+ .match(
+ /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|.(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ );
+};
+
export function ticketRoutes(fastify: FastifyInstance) {
// Create a new ticket
fastify.post(
@@ -47,6 +62,20 @@ export function ticketRoutes(fastify: FastifyInstance) {
},
});
+ if (!email && !validateEmail(email)) {
+ await sendTicketCreate(ticket);
+ }
+
+ if (engineer && engineer.name !== "Unassigned") {
+ const assgined = await prisma.user.findUnique({
+ where: {
+ id: ticket.userId,
+ },
+ });
+
+ await sendAssignedEmail(assgined!.email);
+ }
+
const webhook = await prisma.webhooks.findMany({
where: {
type: "ticket_created",
@@ -55,19 +84,43 @@ export function ticketRoutes(fastify: FastifyInstance) {
for (let i = 0; i < webhook.length; i++) {
if (webhook[i].active === true) {
- console.log(webhook[i].url);
- await axios.post(`${webhook[i].url}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- data: `Ticket ${ticket.id} created by ${ticket.name} -> ${ticket.email}. Priority -> ${ticket.priority}`,
- }),
- });
+ const url = webhook[i].url;
+ if (url.includes("discord.com")) {
+ const message = {
+ content: `Ticket ${ticket.id} created by ${ticket.name} -> ${ticket.email}. Priority -> ${ticket.priority}`,
+ 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.name} -> ${ticket.email}. Priority -> ${ticket.priority}`,
+ }),
+ });
+ }
}
}
+ const hog = track();
+
+ hog.capture({
+ event: "ticket_created",
+ distinctId: ticket.id,
+ });
+
reply.status(200).send({
message: "Ticket created correctly",
success: true,
@@ -177,6 +230,41 @@ export function ticketRoutes(fastify: FastifyInstance) {
}
);
+ // Get all tickets (admin)
+ fastify.get(
+ "/api/v1/tickets/all/admin",
+ async (request: FastifyRequest, reply: FastifyReply) => {
+ const bearer = request.headers.authorization!.split(" ")[1];
+ const token = checkToken(bearer);
+
+ if (token) {
+ const tickets = await prisma.ticket.findMany({
+ 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,
+ });
+ }
+ }
+ );
+
// Get all open tickets for a user
fastify.get(
"/api/v1/tickets/user/open",
@@ -308,7 +396,7 @@ export function ticketRoutes(fastify: FastifyInstance) {
const { user, id }: any = request.body;
if (token) {
- await prisma.user.update({
+ const assigned = await prisma.user.update({
where: { id: user },
data: {
tickets: {
@@ -319,6 +407,10 @@ export function ticketRoutes(fastify: FastifyInstance) {
},
});
+ const { email } = assigned;
+
+ await sendAssignedEmail(email);
+
reply.send({
success: true,
});
@@ -327,6 +419,7 @@ export function ticketRoutes(fastify: FastifyInstance) {
);
// Link a ticket to another ticket
+
// fastify.post(
// "/api/v1/ticket/link",
@@ -378,7 +471,7 @@ export function ticketRoutes(fastify: FastifyInstance) {
const bearer = request.headers.authorization!.split(" ")[1];
const token = checkToken(bearer);
- const { text, id }: any = request.body;
+ const { text, id, public: public_comment }: any = request.body;
if (token) {
const user = await checkSession(bearer);
@@ -386,12 +479,32 @@ export function ticketRoutes(fastify: FastifyInstance) {
await prisma.comment.create({
data: {
text: text,
- public: Boolean(false),
+ public: public_comment,
ticketId: id,
userId: user!.id,
},
});
+ const ticket = await prisma.ticket.findUnique({
+ where: {
+ id: id,
+ },
+ });
+
+ //@ts-expect-error
+ const { email, title } = ticket;
+
+ if (public_comment && email) {
+ sendComment(text, title, email);
+ }
+
+ const hog = track();
+
+ hog.capture({
+ event: "ticket_comment",
+ distinctId: ticket!.id,
+ });
+
reply.send({
success: true,
});
@@ -410,16 +523,12 @@ export function ticketRoutes(fastify: FastifyInstance) {
if (token) {
const { status, id }: any = request.body;
- const ticket: any = await prisma.ticket
- .update({
- where: { id: id },
- data: {
- isComplete: status,
- },
- })
- .then(async (ticket) => {
- // await sendTicketStatus(ticket);
- });
+ const ticket: any = await prisma.ticket.update({
+ where: { id: id },
+ data: {
+ isComplete: status,
+ },
+ });
const webhook = await prisma.webhooks.findMany({
where: {
@@ -428,21 +537,41 @@ export function ticketRoutes(fastify: FastifyInstance) {
});
for (let i = 0; i < webhook.length; i++) {
+ const url = webhook[i].url;
+
if (webhook[i].active === true) {
const s = status ? "Completed" : "Outstanding";
- await axios.post(`${webhook[i].url}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- data: `Ticket ${ticket.id} created by ${ticket.email}, has had it's status changed to ${s}`,
- }),
- redirect: "follow",
- });
+ 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);
+
reply.send({
success: true,
});
diff --git a/apps/api/src/controllers/time.ts b/apps/api/src/controllers/time.ts
index 463c561ee..b1a23742f 100644
--- a/apps/api/src/controllers/time.ts
+++ b/apps/api/src/controllers/time.ts
@@ -1,19 +1,32 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
+import { prisma } from "../prisma";
export function timeTrackingRoutes(fastify: FastifyInstance) {
// Create a new entry
- fastify.get(
- "/api/v1/time/entries/new",
+ fastify.post(
+ "/api/v1/time/new",
async (request: FastifyRequest, reply: FastifyReply) => {
- // check jwt is valid
- // check user is admin
+ const { time, ticket, title, user }: any = request.body;
+
+ console.log(request.body);
+
+ await prisma.timeTracking.create({
+ data: {
+ time: Number(time),
+ title,
+ userId: user,
+ ticketId: ticket,
+ },
+ });
+
+ reply.send({
+ success: true,
+ });
}
);
// Get all entries
// Delete an entry
-
- // Link an entry to a ticket
}
diff --git a/apps/api/src/lib/hog.ts b/apps/api/src/lib/hog.ts
new file mode 100644
index 000000000..ba543c993
--- /dev/null
+++ b/apps/api/src/lib/hog.ts
@@ -0,0 +1,9 @@
+import { PostHog } from "posthog-node";
+
+export function track() {
+ return new PostHog(
+ "phc_2gbpy3JPtDC6hHrQy35yMxMci1NY0fD1sttGTcPjwVf",
+
+ { host: "https://app.posthog.com" }
+ );
+}
diff --git a/apps/api/src/lib/imap.ts b/apps/api/src/lib/imap.ts
index 159083fd1..f1b882e25 100644
--- a/apps/api/src/lib/imap.ts
+++ b/apps/api/src/lib/imap.ts
@@ -85,31 +85,43 @@ export const getEmails = async () => {
simpleParser(stream, async (err: any, parsed: any) => {
const { from, subject, textAsHtml, text, html } = parsed;
+ console.log("from", from);
+
const parsedData = parseEmailContent(textAsHtml);
- const imap = await client.imap_Email.create({
- data: {
- from: from.text,
- subject: subject ? subject : "No Subject",
- body: text ? text : "No Body",
- html: html ? html : "",
- text: textAsHtml,
- },
- });
-
- const ticket = await client.ticket.create({
- data: {
- email: imap.from,
- name: imap.from,
- title: imap.subject ? imap.subject : "-",
- isComplete: Boolean(false),
- priority: "Low",
- fromImap: Boolean(true),
- detail: textAsHtml,
- },
- });
-
- console.log(imap, ticket);
+ if (subject !== undefined && subject.includes("Re:")) {
+ return await client.comment.create({
+ data: {
+ text: text ? text : "No Body",
+ userId: null,
+ ticketId: subject.split(" ")[1].split("#")[1],
+ },
+ });
+ } else {
+ const imap = await client.imap_Email.create({
+ data: {
+ from: from.value[0].address,
+ subject: subject ? subject : "No Subject",
+ body: text ? text : "No Body",
+ html: html ? html : "",
+ text: textAsHtml,
+ },
+ });
+
+ const ticket = await client.ticket.create({
+ data: {
+ email: from.value[0].address,
+ name: from.value[0].name,
+ title: imap.subject ? imap.subject : "-",
+ isComplete: Boolean(false),
+ priority: "Low",
+ fromImap: Boolean(true),
+ detail: textAsHtml,
+ },
+ });
+
+ console.log(imap, ticket);
+ }
});
});
msg.once("attributes", (attrs: any) => {
diff --git a/apps/api/src/lib/nodemailer/auth/forgot-password.ts b/apps/api/src/lib/nodemailer/auth/forgot-password.ts
new file mode 100644
index 000000000..03e242cd4
--- /dev/null
+++ b/apps/api/src/lib/nodemailer/auth/forgot-password.ts
@@ -0,0 +1,98 @@
+import nodeMailer from "nodemailer";
+import { prisma } from "../../../prisma";
+
+export async function forgotPassword(
+ email: string,
+ code: string,
+ link: string,
+ token: string
+) {
+ try {
+ let mail;
+
+ const emails = await prisma.email.findMany();
+
+ const resetlink = `${link}/auth/reset-password?token=${token}`;
+
+ if (emails.length > 0) {
+ if (process.env.ENVIRONMENT === "development") {
+ let testAccount = await nodeMailer.createTestAccount();
+ mail = nodeMailer.createTransport({
+ port: 1025,
+ secure: false, // true for 465, false for other ports
+ auth: {
+ user: testAccount.user, // generated ethereal user
+ pass: testAccount.pass, // generated ethereal password
+ },
+ });
+ } else {
+ const email = emails[0];
+ mail = nodeMailer.createTransport({
+ // @ts-ignore
+ host: email.host,
+ port: email.port,
+ secure: email.secure, // true for 465, false for other ports
+ auth: {
+ user: email.user, // generated ethereal user
+ pass: email.pass, // generated ethereal password
+ },
+ });
+ }
+
+ console.log("Sending email to: ", email);
+
+ let info = await mail.sendMail({
+ from: "noreply@peppermint.sh", // sender address
+ to: email, // list of receivers
+ subject: `Password Reset Request`, // Subject line
+ text: `Password Reset Code: ${code}, follow this link to reset your password ${resetlink}`, // plain text body
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+ Password Reset
+
+ Password code: ${code}
+ Reset Here
+
+ Kind regards,
+
+
+
+
+
+ Our blog | Documentation | Discord
+ This was an automated message sent by peppermint.sh -> An open source helpdesk solution
+ ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
+ |
+
+
+
+ |
+
+
+
+
+ `,
+ });
+
+ console.log("Message sent: %s", info.messageId);
+
+ // Preview only available when sending through an Ethereal account
+ console.log("Preview URL: %s", nodeMailer.getTestMessageUrl(info));
+ }
+ } catch (error) {
+ console.log(error);
+ }
+}
diff --git a/apps/api/src/lib/nodemailer/ticket/assigned.ts b/apps/api/src/lib/nodemailer/ticket/assigned.ts
new file mode 100644
index 000000000..213174491
--- /dev/null
+++ b/apps/api/src/lib/nodemailer/ticket/assigned.ts
@@ -0,0 +1,90 @@
+import nodeMailer from "nodemailer";
+import { prisma } from "../../../prisma";
+
+export async function sendAssignedEmail(email: any) {
+ try {
+ let mail;
+
+ const emails = await prisma.email.findMany();
+
+ if (emails.length > 0) {
+ if (process.env.ENVIRONMENT === "development") {
+ let testAccount = await nodeMailer.createTestAccount();
+ mail = nodeMailer.createTransport({
+ port: 1025,
+ secure: false, // true for 465, false for other ports
+ auth: {
+ user: testAccount.user, // generated ethereal user
+ pass: testAccount.pass, // generated ethereal password
+ },
+ });
+ } else {
+ const email = emails[0];
+ mail = nodeMailer.createTransport({
+ // @ts-ignore
+ host: email.host,
+ port: email.port,
+ secure: email.secure, // true for 465, false for other ports
+ auth: {
+ user: email.user, // generated ethereal user
+ pass: email.pass, // generated ethereal password
+ },
+ });
+ }
+
+ console.log("Sending email to: ", email);
+
+ await mail
+ .sendMail({
+ from: "noreply@peppermint.sh", // sender address
+ to: email, // list of receivers
+ subject: `A new ticket has been assigned to you`, // Subject line
+ text: `Hello there, a ticket has been assigned to you`, // plain text body
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+ Ticket Assigned
+
+ Hello, A new ticket has been assigned to you.
+
+ Kind regards,
+
+
+
+
+
+ Documentation | Discord
+ This was an automated message sent by peppermint.sh -> An open source helpdesk solution
+ ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
+ |
+
+
+
+ |
+
+
+
+
+ `,
+ })
+ .then((info) => {
+ console.log("Message sent: %s", info.messageId);
+ })
+ .catch((err) => console.log(err));
+ }
+ } catch (error) {
+ console.log(error);
+ }
+}
diff --git a/apps/api/src/lib/nodemailer/ticket/comment.ts b/apps/api/src/lib/nodemailer/ticket/comment.ts
new file mode 100644
index 000000000..c730d2258
--- /dev/null
+++ b/apps/api/src/lib/nodemailer/ticket/comment.ts
@@ -0,0 +1,96 @@
+import nodeMailer from "nodemailer";
+import { prisma } from "../../../prisma";
+
+export async function sendComment(
+ comment: string,
+ title: string,
+ email: string
+) {
+ try {
+ let mail;
+
+ console.log("Sending email to: ", email);
+
+ const emails = await prisma.email.findMany();
+
+ if (emails.length > 0) {
+ if (process.env.ENVIRONMENT === "development") {
+ let testAccount = await nodeMailer.createTestAccount();
+ mail = nodeMailer.createTransport({
+ port: 1025,
+ secure: false, // true for 465, false for other ports
+ auth: {
+ user: testAccount.user, // generated ethereal user
+ pass: testAccount.pass, // generated ethereal password
+ },
+ });
+ } else {
+ const email = emails[0];
+ mail = nodeMailer.createTransport({
+ // @ts-ignore
+ host: email.host,
+ port: email.port,
+ secure: email.secure, // true for 465, false for other ports
+ auth: {
+ user: email.user, // generated ethereal user
+ pass: email.pass, // generated ethereal password
+ },
+ });
+ }
+
+ await mail
+ .sendMail({
+ from: '"No reply 👻" noreply@peppermint.sh', // sender address
+ to: email,
+ subject: `New comment on a ticket`, // Subject line
+ text: `Hello there, Ticket: ${title}, has had an update with a comment of ${comment}`, // plain text body
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ticket Update for: ${title}
+
+ ${comment}
+
+ Kind regards,
+
+
+
+
+
+ Documentation | Discord
+ This was an automated message sent by peppermint.sh -> An open source helpdesk solution
+ ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
+ |
+
+
+
+ |
+
+
+
+
+
+ `,
+ })
+ .then((info) => {
+ console.log("Message sent: %s", info.messageId);
+ })
+ .catch((err) => console.log(err));
+ }
+ } catch (error) {
+ console.log(error);
+ }
+}
diff --git a/apps/api/src/lib/nodemailer/ticket/create.ts b/apps/api/src/lib/nodemailer/ticket/create.ts
new file mode 100644
index 000000000..2885064a5
--- /dev/null
+++ b/apps/api/src/lib/nodemailer/ticket/create.ts
@@ -0,0 +1,90 @@
+import nodeMailer from "nodemailer";
+import { prisma } from "../../../prisma";
+
+export async function sendTicketCreate(ticket: any) {
+ try {
+ let mail;
+
+ const emails = await prisma.email.findMany();
+
+ if (emails.length > 0) {
+ if (process.env.ENVIRONMENT === "development") {
+ let testAccount = await nodeMailer.createTestAccount();
+ mail = nodeMailer.createTransport({
+ port: 1025,
+ secure: false, // true for 465, false for other ports
+ auth: {
+ user: testAccount.user, // generated ethereal user
+ pass: testAccount.pass, // generated ethereal password
+ },
+ });
+ } else {
+ const email = emails[0];
+ mail = nodeMailer.createTransport({
+ // @ts-ignore
+ host: email.host,
+ port: email.port,
+ secure: email.secure, // true for 465, false for other ports
+ auth: {
+ user: email.user, // generated ethereal user
+ pass: email.pass, // generated ethereal password
+ },
+ });
+ }
+
+ await mail
+ .sendMail({
+ from: '"No reply 👻" noreply@peppermint.sh', // sender address
+ to: ticket.email,
+ subject: `Ticket ${ticket.id} has just been created & logged`, // Subject line
+ text: `Hello there, Ticket ${ticket.id}, which you reported on ${ticket.createdAt}, has now been created and logged`, // plain text body
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ticket Created: ${ticket.id}
+
+ Hello, Your ticket has now been created and logged.
+
+ Kind regards,
+
+
+
+
+
+ Documentation | Discord
+ This was an automated message sent by peppermint.sh -> An open source helpdesk solution
+ ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
+ |
+
+
+
+ |
+
+
+
+
+
+ `,
+ })
+ .then((info) => {
+ console.log("Message sent: %s", info.messageId);
+ })
+ .catch((err) => console.log(err));
+ }
+ } catch (error) {
+ console.log(error);
+ }
+}
diff --git a/apps/api/src/lib/nodemailer/ticket/status.ts b/apps/api/src/lib/nodemailer/ticket/status.ts
new file mode 100644
index 000000000..a75fa9b2f
--- /dev/null
+++ b/apps/api/src/lib/nodemailer/ticket/status.ts
@@ -0,0 +1,101 @@
+import nodeMailer from "nodemailer";
+import { prisma } from "../../../prisma";
+
+export async function sendTicketStatus(ticket: any) {
+ let mail;
+
+ const emails = await prisma.email.findMany();
+
+ if (emails.length > 0) {
+ if (process.env.ENVIRONMENT === "development") {
+ let testAccount = await nodeMailer.createTestAccount();
+ mail = nodeMailer.createTransport({
+ port: 1025,
+ secure: false, // true for 465, false for other ports
+ auth: {
+ user: testAccount.user, // generated ethereal user
+ pass: testAccount.pass, // generated ethereal password
+ },
+ });
+ } else {
+ const email = emails[0];
+ mail = nodeMailer.createTransport({
+ //@ts-ignore
+ host: email.host,
+ port: email.port,
+ secure: email.secure, // true for 465, false for other ports
+ auth: {
+ user: email.user, // generated ethereal user
+ pass: email.pass, // generated ethereal password
+ },
+ });
+ }
+
+ await mail
+ .sendMail({
+ from: "noreply@peppermint.sh", // sender address
+ to: ticket.email,
+ subject: `Ticket ${ticket.id} status is now ${
+ ticket.isComplete ? "COMPLETED" : "OUTSTANDING"
+ }`, // Subject line
+ text: `Hello there, Ticket ${ticket.id}, now has a status of ${
+ ticket.isComplete ? "COMPLETED" : "OUTSTANDING"
+ }`, // plain text body
+ html: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ Ticket: ${
+ ticket.title
+ }
+
+ Your Ticket, now has a status of ${
+ ticket.isComplete ? "completed" : "open"
+ }
+ Kind regards,
+
+ Peppermint ticket management
+
+
+
+
+
+
+ Documentation | Discord
+ This was an automated message sent by peppermint.sh -> An open source helpdesk solution
+ ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
+ |
+
+
+
+ |
+
+
+
+
+
+ `,
+ })
+ .then((info) => {
+ console.log("Message sent: %s", info.messageId);
+ })
+ .catch((err) => console.log(err));
+ }
+}
diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts
index 4405a4e45..d48f70c13 100644
--- a/apps/api/src/main.ts
+++ b/apps/api/src/main.ts
@@ -4,7 +4,7 @@ import Fastify, { FastifyInstance } from "fastify";
import { getEmails } from "./lib/imap";
import { exec } from "child_process";
-import { PostHog } from "posthog-node";
+import { track } from "./lib/hog";
import { prisma } from "./prisma";
import { registerRoutes } from "./routes";
@@ -112,7 +112,7 @@ const start = async () => {
const port = process.env.PORT || 5003;
- await server.listen(
+ server.listen(
{ port: Number(port), host: "0.0.0.0" },
async (err, address) => {
if (err) {
@@ -120,18 +120,14 @@ const start = async () => {
process.exit(1);
}
- const client = new PostHog(
- "phc_2gbpy3JPtDC6hHrQy35yMxMci1NY0fD1sttGTcPjwVf",
-
- { host: "https://app.posthog.com" }
- );
+ const client = track();
client.capture({
event: "server_started",
distinctId: "uuid",
});
- await client.shutdownAsync();
+ client.shutdownAsync();
console.info(`Server listening on ${address}`);
}
);
diff --git a/apps/api/src/prisma/migrations/20231130184144_uptime_kb_config/migration.sql b/apps/api/src/prisma/migrations/20231130184144_uptime_kb_config/migration.sql
new file mode 100644
index 000000000..800cbf950
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231130184144_uptime_kb_config/migration.sql
@@ -0,0 +1,59 @@
+-- AlterTable
+ALTER TABLE "Config" ADD COLUMN "client_version" TEXT,
+ADD COLUMN "feature_previews" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "gh_version" TEXT,
+ADD COLUMN "out_of_office" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "out_of_office_end" TIMESTAMP(3),
+ADD COLUMN "out_of_office_message" TEXT,
+ADD COLUMN "out_of_office_start" TIMESTAMP(3),
+ADD COLUMN "portal_locale" TEXT,
+ADD COLUMN "sso_active" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "sso_provider" TEXT;
+
+-- CreateTable
+CREATE TABLE "SSO" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "clientId" TEXT NOT NULL,
+ "clientSecret" TEXT NOT NULL,
+ "active" BOOLEAN NOT NULL,
+ "issuer" TEXT,
+ "tenantId" TEXT,
+
+ CONSTRAINT "SSO_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Uptime" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT NOT NULL,
+ "url" TEXT NOT NULL,
+ "active" BOOLEAN NOT NULL DEFAULT false,
+ "webhook" TEXT,
+ "latency" INTEGER,
+ "status" BOOLEAN,
+
+ CONSTRAINT "Uptime_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "knowledgeBase" (
+ "id" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "title" TEXT NOT NULL,
+ "content" TEXT NOT NULL,
+ "tags" TEXT[],
+ "author" TEXT NOT NULL,
+ "public" BOOLEAN NOT NULL DEFAULT false,
+ "ticketId" TEXT,
+
+ CONSTRAINT "knowledgeBase_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "knowledgeBase" ADD CONSTRAINT "knowledgeBase_ticketId_fkey" FOREIGN KEY ("ticketId") REFERENCES "Ticket"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/apps/api/src/prisma/migrations/20231130184716_sso/migration.sql b/apps/api/src/prisma/migrations/20231130184716_sso/migration.sql
new file mode 100644
index 000000000..216e9b680
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231130184716_sso/migration.sql
@@ -0,0 +1,8 @@
+/*
+ Warnings:
+
+ - You are about to drop the `SSO` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropTable
+DROP TABLE "SSO";
diff --git a/apps/api/src/prisma/migrations/20231130185305_allow/migration.sql b/apps/api/src/prisma/migrations/20231130185305_allow/migration.sql
new file mode 100644
index 000000000..7adf3b10d
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231130185305_allow/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Config" ALTER COLUMN "notifications" DROP NOT NULL;
diff --git a/apps/api/src/prisma/migrations/20231201011858_redirect/migration.sql b/apps/api/src/prisma/migrations/20231201011858_redirect/migration.sql
new file mode 100644
index 000000000..0429f1b5a
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231201011858_redirect/migration.sql
@@ -0,0 +1,18 @@
+/*
+ Warnings:
+
+ - You are about to drop the `Account` table. If the table is not empty, all the data it contains will be lost.
+ - You are about to drop the `VerificationToken` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey";
+
+-- AlterTable
+ALTER TABLE "Provider" ADD COLUMN "redirectUri" TEXT;
+
+-- DropTable
+DROP TABLE "Account";
+
+-- DropTable
+DROP TABLE "VerificationToken";
diff --git a/apps/api/src/prisma/migrations/20231202030625_/migration.sql b/apps/api/src/prisma/migrations/20231202030625_/migration.sql
new file mode 100644
index 000000000..c0f80f7db
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231202030625_/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Config" ADD COLUMN "encryption_key" TEXT;
diff --git a/apps/api/src/prisma/migrations/20231202030940_/migration.sql b/apps/api/src/prisma/migrations/20231202030940_/migration.sql
new file mode 100644
index 000000000..f32bebc27
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231202030940_/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "Config" ADD COLUMN "first_time_setup" BOOLEAN NOT NULL DEFAULT true;
diff --git a/apps/api/src/prisma/migrations/20231202031821_/migration.sql b/apps/api/src/prisma/migrations/20231202031821_/migration.sql
new file mode 100644
index 000000000..e4610340a
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231202031821_/migration.sql
@@ -0,0 +1,9 @@
+/*
+ Warnings:
+
+ - The `encryption_key` column on the `Config` table would be dropped and recreated. This will lead to data loss if there is data in the column.
+
+*/
+-- AlterTable
+ALTER TABLE "Config" DROP COLUMN "encryption_key",
+ADD COLUMN "encryption_key" BYTEA;
diff --git a/apps/api/src/prisma/migrations/20231203164241_/migration.sql b/apps/api/src/prisma/migrations/20231203164241_/migration.sql
new file mode 100644
index 000000000..5e32c2625
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231203164241_/migration.sql
@@ -0,0 +1,8 @@
+-- DropForeignKey
+ALTER TABLE "Comment" DROP CONSTRAINT "Comment_userId_fkey";
+
+-- AlterTable
+ALTER TABLE "Comment" ALTER COLUMN "userId" DROP NOT NULL;
+
+-- AddForeignKey
+ALTER TABLE "Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/apps/api/src/prisma/migrations/20231203224035_code/migration.sql b/apps/api/src/prisma/migrations/20231203224035_code/migration.sql
new file mode 100644
index 000000000..b29e5f0a3
--- /dev/null
+++ b/apps/api/src/prisma/migrations/20231203224035_code/migration.sql
@@ -0,0 +1,11 @@
+-- CreateTable
+CREATE TABLE "PasswordResetToken" (
+ "id" TEXT NOT NULL,
+ "code" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+
+ CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PasswordResetToken_code_key" ON "PasswordResetToken"("code");
diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma
index 9e1226fc5..e45d759b6 100644
--- a/apps/api/src/prisma/schema.prisma
+++ b/apps/api/src/prisma/schema.prisma
@@ -15,26 +15,7 @@ model Provider {
active Boolean
issuer String?
tenantId String?
-}
-
-model Account {
- id String @id @default(cuid())
- userId String
- type String
- provider String
- providerAccountId String
- refresh_token String? @db.Text
- access_token String? @db.Text
- expires_at Int?
- token_type String?
- scope String?
- id_token String? @db.Text
- session_state String?
- refresh_token_expires_in Int?
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- @@unique([provider, providerAccountId])
+ redirectUri String?
}
model Session {
@@ -45,6 +26,12 @@ model Session {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
+model PasswordResetToken {
+ id String @id @default(cuid())
+ code String @unique
+ userId String
+}
+
model User {
id String @id @default(uuid())
createdAt DateTime @default(now())
@@ -69,19 +56,10 @@ model User {
Team Team? @relation(fields: [teamId], references: [id])
teamId String?
Comment Comment[]
- Account Account[]
Session Session[]
TimeTracking TimeTracking[]
}
-model VerificationToken {
- identifier String
- token String @unique
- expires DateTime
-
- @@unique([identifier, token])
-}
-
model Team {
id String @id @default(uuid())
createdAt DateTime @default(now())
@@ -114,12 +92,13 @@ model Ticket {
Comment Comment[]
TimeTracking TimeTracking[]
- team Team? @relation(fields: [teamId], references: [id])
- teamId String?
- assignedTo User? @relation(fields: [userId], references: [id])
- client Client? @relation(fields: [clientId], references: [id])
- clientId String?
- userId String?
+ team Team? @relation(fields: [teamId], references: [id])
+ teamId String?
+ assignedTo User? @relation(fields: [userId], references: [id])
+ client Client? @relation(fields: [clientId], references: [id])
+ clientId String?
+ userId String?
+ knowledgeBase knowledgeBase[]
}
model TimeTracking {
@@ -145,10 +124,10 @@ model Comment {
text String
public Boolean @default(false)
- userId String
- user User @relation(fields: [userId], references: [id])
+ userId String?
+ user User? @relation(fields: [userId], references: [id])
ticketId String
- ticket Ticket @relation(fields: [ticketId], references: [id])
+ ticket Ticket @relation(fields: [ticketId], references: [id])
}
model Client {
@@ -258,7 +237,47 @@ model Config {
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
- notifications Json // Array of service names and an active field of TRUE OR FALSE
+ notifications Json? // Array of service names and an active field of TRUE OR FALSE
+ sso_provider String?
+ sso_active Boolean @default(false)
+ gh_version String?
+ client_version String?
+ portal_locale String?
+ feature_previews Boolean @default(false)
+ out_of_office Boolean @default(false)
+ out_of_office_message String?
+ out_of_office_start DateTime?
+ out_of_office_end DateTime?
+ encryption_key Bytes?
+ first_time_setup Boolean @default(true)
+}
+
+model Uptime {
+ id String @id @default(uuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+
+ name String
+ url String
+ active Boolean @default(false)
+ webhook String?
+ latency Int?
+ status Boolean?
+}
+
+model knowledgeBase {
+ id String @id @default(uuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @default(now())
+
+ title String
+ content String
+ tags String[]
+ author String
+ public Boolean @default(false)
+
+ ticketId String?
+ ticket Ticket? @relation(fields: [ticketId], references: [id])
}
model Imap_Email {
diff --git a/apps/api/src/prisma/seed.js b/apps/api/src/prisma/seed.js
index 647e68276..f43a4aa17 100644
--- a/apps/api/src/prisma/seed.js
+++ b/apps/api/src/prisma/seed.js
@@ -1,33 +1,59 @@
const { PrismaClient } = require("@prisma/client");
+const crypto = require("crypto");
const prisma = new PrismaClient();
async function main() {
- const admin = await prisma.user.upsert({
- where: { email: "admin@admin.com" },
- update: {},
- create: {
- email: `admin@admin.com`,
- name: "admin",
- isAdmin: true,
- password: "$2b$10$BFmibvOW7FtY0soAAwujoO9y2tIyB7WEJ2HNq9O7zh9aeejMvRsKu",
- language: "en",
- },
- });
+ const setup = await prisma.config.findFirst({});
- const internal = await prisma.client.upsert({
- where: { email: `internal@admin.com` },
- update: {},
- create: {
- email: `internal@admin.com`,
- name: "internal",
- contactName: "admin",
- number: "123456789",
- active: true,
- },
- });
+ if (setup === null) {
+ await prisma.user.upsert({
+ where: { email: "admin@admin.com" },
+ update: {},
+ create: {
+ email: `admin@admin.com`,
+ name: "admin",
+ isAdmin: true,
+ password:
+ "$2b$10$BFmibvOW7FtY0soAAwujoO9y2tIyB7WEJ2HNq9O7zh9aeejMvRsKu",
+ language: "en",
+ },
+ });
+
+ await prisma.client.upsert({
+ where: { email: `internal@admin.com` },
+ update: {},
+ create: {
+ email: `internal@admin.com`,
+ name: "internal",
+ contactName: "admin",
+ number: "123456789",
+ active: true,
+ },
+ });
+
+ const encryptionKey = crypto.randomBytes(32); // Generates a random key
+
+ const conf = await prisma.config.create({
+ data: {
+ gh_version: "0.3.6",
+ client_version: "0.3.6",
+ portal_locale: "en",
+ encryption_key: encryptionKey,
+ },
+ });
- console.log({ admin, internal });
+ await prisma.config.update({
+ where: {
+ id: conf.id,
+ },
+ data: {
+ first_time_setup: false,
+ },
+ });
+ } else {
+ console.log("No need to seed, already seeded");
+ }
}
main()
diff --git a/apps/api/src/routes.ts b/apps/api/src/routes.ts
index a342a457f..446be6441 100644
--- a/apps/api/src/routes.ts
+++ b/apps/api/src/routes.ts
@@ -1,10 +1,12 @@
import { FastifyInstance } from "fastify";
import { authRoutes } from "./controllers/auth";
import { clientRoutes } from "./controllers/clients";
+import { configRoutes } from "./controllers/config";
import { dataRoutes } from "./controllers/data";
import { notebookRoutes } from "./controllers/notebook";
import { emailQueueRoutes } from "./controllers/queue";
import { ticketRoutes } from "./controllers/ticket";
+import { timeTrackingRoutes } from "./controllers/time";
import { todoRoutes } from "./controllers/todos";
import { userRoutes } from "./controllers/users";
import { webhookRoutes } from "./controllers/webhooks";
@@ -19,4 +21,6 @@ export function registerRoutes(fastify: FastifyInstance) {
notebookRoutes(fastify);
clientRoutes(fastify);
webhookRoutes(fastify);
+ configRoutes(fastify);
+ timeTrackingRoutes(fastify);
}
diff --git a/apps/client/components/TicketViews/admin.tsx b/apps/client/components/TicketViews/admin.tsx
new file mode 100644
index 000000000..e59b3c734
--- /dev/null
+++ b/apps/client/components/TicketViews/admin.tsx
@@ -0,0 +1,377 @@
+import { getCookie } from "cookies-next";
+import moment from "moment";
+import { useRouter } from "next/router";
+import React, { useMemo } from "react";
+import { useQuery } from "react-query";
+import {
+ useFilters,
+ useGlobalFilter,
+ usePagination,
+ useTable,
+} from "react-table";
+import TicketsMobileList from "../../components/TicketsMobileList";
+
+const fetchALLTIckets = async () => {
+ const res = await fetch(
+ `${process.env.NEXT_PUBLIC_API_URL}/api/v1/tickets/all/admin`,
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${getCookie("session")}`,
+ },
+ }
+ );
+ return res.json();
+};
+
+function DefaultColumnFilter({ column: { filterValue, setFilter } }: any) {
+ return (
+ {
+ setFilter(e.target.value || undefined); // Set undefined to remove the filter entirely
+ }}
+ placeholder="Type to filter"
+ />
+ );
+}
+function Table({ columns, data }: any) {
+ const filterTypes = React.useMemo(
+ () => ({
+ // Add a new fuzzyTextFilterFn filter type.
+ // fuzzyText: fuzzyTextFilterFn,
+ // Or, override the default text filter to use
+ // "startWith"
+ text: (rows: any, id: any, filterValue: any) =>
+ rows.filter((row: any) => {
+ const rowValue = row.values[id];
+ return rowValue !== undefined
+ ? String(rowValue)
+ .toLowerCase()
+ .startsWith(String(filterValue).toLowerCase())
+ : true;
+ }),
+ }),
+ []
+ );
+
+ const defaultColumn = React.useMemo(
+ () => ({
+ // Let's set up our default Filter UI
+ Filter: DefaultColumnFilter,
+ }),
+ []
+ );
+
+ const {
+ getTableProps,
+ getTableBodyProps,
+ headerGroups,
+ //@ts-expect-error
+ page,
+ prepareRow,
+ //@ts-expect-error
+ canPreviousPage,
+ //@ts-expect-error
+ canNextPage,
+ //@ts-expect-error
+ pageCount,
+ //@ts-expect-error
+ gotoPage,
+ //@ts-expect-error
+ nextPage,
+ //@ts-expect-error
+ previousPage,
+ //@ts-expect-error
+ setPageSize,
+ //@ts-expect-error
+ state: { pageIndex, pageSize },
+ } = useTable(
+ {
+ columns,
+ data,
+ //@ts-expect-error
+ defaultColumn, // Be sure to pass the defaultColumn option
+ filterTypes,
+ initialState: {
+ //@ts-expect-error
+ pageIndex: 0,
+ },
+ },
+ useFilters, // useFilters!
+ useGlobalFilter,
+ usePagination
+ );
+
+ return (
+
+
+
+
+
+ {headerGroups.map((headerGroup: any) => (
+ header.id)}
+ >
+ {headerGroup.headers.map((column: any) =>
+ column.hideHeader === false ? null : (
+
+ {column.render("Header")}
+ {/* Render the columns filter UI */}
+
+ {column.canFilter ? column.render("Filter") : null}
+
+ |
+ )
+ )}
+
+ ))}
+
+
+ {page.map((row: any, i: any) => {
+ prepareRow(row);
+ return (
+
+ {row.cells.map((cell: any) => (
+
+ {cell.render("Cell")}
+ |
+ ))}
+
+ );
+ })}
+
+
+
+ {data.length > 10 && (
+
+ )}
+
+
+
+ );
+}
+
+export default function AdminTicketLayout() {
+ const { data, status, refetch } = useQuery(
+ "fetchallTickets",
+ fetchALLTIckets
+ );
+
+ const router = useRouter();
+
+ const high = "bg-red-100 text-red-800";
+ const low = "bg-blue-100 text-blue-800";
+ const normal = "bg-green-100 text-green-800";
+
+ const columns = useMemo(
+ () => [
+ {
+ Header: "Type",
+ accessor: "type",
+ id: "type",
+ width: 50,
+ },
+ {
+ Header: "Summary",
+ accessor: "title",
+ id: "summary",
+ Cell: ({ row, value }: any) => {
+ return (
+ <>
+ {value}
+ >
+ );
+ },
+ },
+ {
+ Header: "Assignee",
+ accessor: "assignedTo.name",
+ id: "assignee",
+ Cell: ({ row, value }: any) => {
+ return (
+ <>
+ {value ? value : "n/a"}
+ >
+ );
+ },
+ },
+ {
+ Header: "Client",
+ accessor: "client.name",
+ id: "client",
+ Cell: ({ row, value }: any) => {
+ return (
+ <>
+ {value ? value : "n/a"}
+ >
+ );
+ },
+ },
+ {
+ Header: "Priority",
+ accessor: "priority",
+ id: "priority",
+ Cell: ({ row, value }) => {
+ let p = value;
+ let badge;
+
+ if (p === "Low") {
+ badge = low;
+ }
+ if (p === "Normal") {
+ badge = normal;
+ }
+ if (p === "High") {
+ badge = high;
+ }
+
+ return (
+ <>
+
+ {value}
+
+ >
+ );
+ },
+ },
+ {
+ Header: "Status",
+ accessor: "status",
+ id: "status",
+ Cell: ({ row, value }) => {
+ let p = value;
+ let badge;
+
+ return (
+ <>
+
+ {value === "needs_support" && Needs Support}
+ {value === "in_progress" && In Progress}
+ {value === "in_review" && In Review}
+ {value === "done" && Done}
+
+ >
+ );
+ },
+ },
+ {
+ Header: "Created",
+ accessor: "createdAt",
+ id: "created",
+ Cell: ({ row, value }) => {
+ const now = moment(value).format("DD/MM/YYYY");
+ return (
+ <>
+ {now}
+ >
+ );
+ },
+ },
+ ],
+ []
+ );
+
+ return (
+ <>
+ {status === "success" && (
+ <>
+ {data.tickets && data.tickets.length > 0 && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ {data.tickets.length === 0 && (
+ <>
+
+
+
+
+ You currently don't have any assigned tickets. :)
+
+
+ >
+ )}
+ >
+ )}
+ >
+ );
+}
diff --git a/apps/client/components/UpdateUserModal/index.js b/apps/client/components/UpdateUserModal/index.tsx
similarity index 66%
rename from apps/client/components/UpdateUserModal/index.js
rename to apps/client/components/UpdateUserModal/index.tsx
index baf67ff17..d8f62daec 100644
--- a/apps/client/components/UpdateUserModal/index.js
+++ b/apps/client/components/UpdateUserModal/index.tsx
@@ -1,39 +1,29 @@
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
+import { getCookie } from "cookies-next";
import { useRouter } from "next/router";
-import React, { Fragment, useState } from "react";
+import { Fragment, useState } from "react";
export default function UpdateUserModal({ user }) {
const [open, setOpen] = useState(false);
- const [name, setName] = useState(user.name);
- const [email, setEmail] = useState(user.email);
const [admin, setAdmin] = useState(user.isAdmin);
- const [error, setError] = useState(null);
const router = useRouter();
- const notificationMethods = [
- { id: "user", title: "user" },
- { id: "admin", title: "admin" },
- ];
-
async function updateUser() {
- if (name.length > 0 && email.length > 0) {
- await fetch("/api/v1/admin/user/update", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- email,
- name,
- admin,
- id: user.id,
- }),
- }).then(() => router.reload(router.pathname));
- }
- setError("Length needs to be more than zero");
+ await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/user/role`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${getCookie("session")}`,
+ },
+ body: JSON.stringify({
+ role: admin,
+ id: user.id,
+ }),
+ });
+ // .then(() => router.reload());
}
return (
@@ -43,7 +33,7 @@ export default function UpdateUserModal({ user }) {
type="button"
className="inline-flex items-center px-4 py-1.5 border font-semibold border-gray-300 shadow-sm text-xs rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
- Edit
+ Role
@@ -97,54 +87,10 @@ export default function UpdateUserModal({ user }) {
as="h3"
className="text-lg leading-6 font-medium text-gray-900"
>
- Edit User
+ Edit User Role
- <>
-
setName(e.target.value)}
- value={name}
- focus={
- error !== null && name.length > 0 ? true : false
- }
- />
- {error !== null && name.length === 0 && (
-
{error}
- )}
- >
-
- <>
-
setEmail(e.target.value)}
- value={email}
- focus={
- error !== null && email.length > 0 ? true : false
- }
- />
- {error !== null && email.length === 0 && (
-
{error}
- )}
- >
-
-
diff --git a/apps/client/layouts/settings.tsx b/apps/client/layouts/settings.tsx
index 3f4f4190e..b5be9a85e 100644
--- a/apps/client/layouts/settings.tsx
+++ b/apps/client/layouts/settings.tsx
@@ -1,13 +1,13 @@
-import { useState } from "react";
-
import useTranslation from "next-translate/useTranslation";
import Link from "next/link";
import { useRouter } from "next/router";
+import { useUser } from "../store/session";
export default function Settings({ children }) {
const router = useRouter();
const { t } = useTranslation("peppermint");
+ const { user } = useUser();
const linkStyles = {
active:
@@ -15,7 +15,6 @@ export default function Settings({ children }) {
inactive:
"w-full border-transparent text-gray-900 hover:bg-gray-50 hover:text-gray-900 group mt-1 border-l-4 px-3 py-2 flex items-center text-sm font-medium",
};
- const [show, setShow] = useState("profile");
return (
@@ -78,31 +77,33 @@ export default function Settings({ children }) {
{t("notifications")}
-
-
-
{t("reset_password")}
-
+
+
{t("reset_password")}
+
+ )}
diff --git a/apps/client/lib/nodemailer/ticket/create.js b/apps/client/lib/nodemailer/ticket/create.js
deleted file mode 100644
index e24273182..000000000
--- a/apps/client/lib/nodemailer/ticket/create.js
+++ /dev/null
@@ -1,97 +0,0 @@
-import nodeMailer from "nodemailer";
-import { prisma } from "../../../prisma/prisma";
-
-export async function sendTicketCreate(ticket) {
- try {
- let mail;
-
- const emails = await prisma.email.findMany();
-
- if (emails.length > 0) {
- if (process.env.NODE_ENV === "development") {
- let testAccount = await nodeMailer.createTestAccount();
- mail = nodeMailer.createTransport({
- port: 1025,
- secure: false, // true for 465, false for other ports
- auth: {
- user: testAccount.user, // generated ethereal user
- pass: testAccount.pass, // generated ethereal password
- },
- });
- } else {
- const email = emails[0];
- mail = nodeMailer.createTransport({
- host: email.host,
- port: email.port,
- secure: email.secure, // true for 465, false for other ports
- auth: {
- user: email.user, // generated ethereal user
- pass: email.pass, // generated ethereal password
- },
- });
- }
-
- let info = await mail.sendMail({
- from: '"No reply 👻" noreply@peppermint.sh', // sender address
- to: ticket.email,
- subject: `Ticket ${ticket.id} has just been created & logged`, // Subject line
- text: `Hello there, Ticket ${ticket.id}, which you reported on ${ticket.createdAt}, has now been created and logged`, // plain text body
- html: `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
- Ticket Created: ${ticket.id}
-
- Hello, Your ticket has now been created and logged.
-
- Kind regards,
-
- Peppermint ticket management
-
-
-
-
-
-
- Our blog | Documentation | Discord
- This was an automated message sent by peppermint.sh -> An open source helpdesk solution
- ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
- |
-
-
-
- |
-
-
-
-
-
- `,
- });
-
- console.log("Message sent: %s", info.messageId);
-
- // Preview only available when sending through an Ethereal account
- console.log("Preview URL: %s", nodeMailer.getTestMessageUrl(info));
- }
- } catch (error) {
- console.log(error);
- }
-}
diff --git a/apps/client/lib/nodemailer/ticket/status.js b/apps/client/lib/nodemailer/ticket/status.js
deleted file mode 100644
index 064d07a8e..000000000
--- a/apps/client/lib/nodemailer/ticket/status.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import nodeMailer from "nodemailer";
-import { prisma } from "../../../prisma/prisma";
-
-export async function sendTicketStatus(ticket) {
- let mail;
-
- const emails = await prisma.email.findMany();
-
- if (emails.length > 0) {
- if (process.env.NODE_ENV === "development") {
- let testAccount = await nodeMailer.createTestAccount();
- mail = nodeMailer.createTransport({
- port: 1025,
- secure: false, // true for 465, false for other ports
- auth: {
- user: testAccount.user, // generated ethereal user
- pass: testAccount.pass, // generated ethereal password
- },
- });
- } else {
- const email = emails[0];
- mail = nodeMailer.createTransport({
- host: email.host,
- port: email.port,
- secure: email.secure, // true for 465, false for other ports
- auth: {
- user: email.user, // generated ethereal user
- pass: email.pass, // generated ethereal password
- },
- });
- }
-
- let info = await mail.sendMail({
- from: "", // sender address
- to: ticket.email,
- subject: `Ticket ${ticket.id} status is now ${
- ticket.isComplete ? "COMPLETED" : "OUTSTANDING"
- }`, // Subject line
- text: `Hello there, Ticket ${ticket.id}, now has a status of ${
- ticket.isComplete ? "COMPLETED" : "OUTSTANDING"
- }`, // plain text body
- html: `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
-
-
-
- Ticket Created: ${
- ticket.title
- }
-
- Hello there, Your Ticket, which was #${
- ticket.number
- }, now has a status of ${
- ticket.isComplete ? "completed" : "open"
- }
- Kind regards,
-
- Peppermint ticket management
-
-
-
-
-
-
- Our blog | Documentation | Discord
- This was an automated message sent by peppermint.sh -> An open source helpdesk solution
- ©2022 Peppermint Ticket Management, a Peppermint Labs product. All rights reserved.
- |
-
-
-
- |
-
-
-
-
-
- `,
- });
-
- console.log("Message sent: %s", info.messageId);
-
- // Preview only available when sending through an Ethereal account
- console.log("Preview URL: %s", nodeMailer.getTestMessageUrl(info));
- }
-}
diff --git a/apps/client/locales/da/peppermint.json b/apps/client/locales/da/peppermint.json
index d08aeaee9..c72385655 100644
--- a/apps/client/locales/da/peppermint.json
+++ b/apps/client/locales/da/peppermint.json
@@ -78,7 +78,11 @@
"profile_desc": "Disse oplysninger vil blive vist offentligt, så vær forsigtig med, hvad du deler.",
"language": "Sprog",
"notifications": "Notifikationer",
- "save_and_reload": "Gem og genindlæs"
+ "save_and_reload": "Gem og genindlæs",
+ "select_a_client": "Vælg en kunde",
+ "select_an_engineer": "Vælg en ingeniør",
+ "ticket_create": "Opret billet",
+ "internallycommented_at": "Kommenteret internt kl."
}
\ No newline at end of file
diff --git a/apps/client/locales/de/peppermint.json b/apps/client/locales/de/peppermint.json
index 908263930..e998b1d08 100644
--- a/apps/client/locales/de/peppermint.json
+++ b/apps/client/locales/de/peppermint.json
@@ -78,5 +78,9 @@
"profile_desc": "Diese Informationen werden öffentlich angezeigt, also achten Sie darauf, was Sie teilen.",
"language": "Sprache",
"notifications": "Benachrichtigungen",
- "save_and_reload": "Speichern und neu laden"
+ "save_and_reload": "Speichern und neu laden",
+ "select_a_client": "Kunden auswählen",
+ "select_an_engineer": "Einen Ingenieur auswählen",
+ "ticket_create": "Ticket erstellen",
+ "internallycommented_at": "Intern kommentiert um"
}
diff --git a/apps/client/locales/en/peppermint.json b/apps/client/locales/en/peppermint.json
index 594d220e6..53c47c66f 100644
--- a/apps/client/locales/en/peppermint.json
+++ b/apps/client/locales/en/peppermint.json
@@ -78,5 +78,9 @@
"profile_desc": "This information will be displayed publicly so be careful what you share.",
"language": "Language",
"notifications": "Notifications",
- "save_and_reload": "Save and Reload"
+ "save_and_reload": "Save and Reload",
+ "select_a_client": "Select a client",
+ "select_an_engineer": "Select an engineer",
+ "ticket_create": "Create Ticket",
+ "internallycommented_at": "Internally Commented At"
}
diff --git a/apps/client/locales/es/peppermint.json b/apps/client/locales/es/peppermint.json
index 5a1b18f4c..6978c7393 100644
--- a/apps/client/locales/es/peppermint.json
+++ b/apps/client/locales/es/peppermint.json
@@ -78,6 +78,10 @@
"profile_desc": "Esta información se mostrará públicamente, así que ten cuidado con lo que compartes.",
"language": "Idioma",
"notifications": "Notificaciones",
- "save_and_reload": "Guardar y recargar"
+ "save_and_reload": "Guardar y recargar",
+ "select_a_client": "Seleccionar un cliente",
+ "select_an_engineer": "Seleccionar un ingeniero",
+ "ticket_create": "Crear ticket",
+ "internallycommented_at": "Comentado internamente en"
}
\ No newline at end of file
diff --git a/apps/client/locales/fr/peppermint.json b/apps/client/locales/fr/peppermint.json
index 0207fe08d..89357cb5a 100644
--- a/apps/client/locales/fr/peppermint.json
+++ b/apps/client/locales/fr/peppermint.json
@@ -78,7 +78,11 @@
"profile_desc": "Ces informations seront affichées publiquement, alors faites attention à ce que vous partagez.",
"language": "Langue",
"notifications": "Notifications",
- "save_and_reload": "Enregistrer et recharger"
+ "save_and_reload": "Enregistrer et recharger",
+ "select_a_client": "Sélectionner un client",
+ "select_an_engineer": "Sélectionner un ingénieur",
+ "ticket_create": "Créer un ticket",
+ "internallycommented_at": "Commenté en interne à"
}
\ No newline at end of file
diff --git a/apps/client/locales/no/peppermint.json b/apps/client/locales/no/peppermint.json
index 611f149c6..d9c823d56 100644
--- a/apps/client/locales/no/peppermint.json
+++ b/apps/client/locales/no/peppermint.json
@@ -78,6 +78,10 @@
"profile_desc": "Denne informasjonen vil bli vist offentlig, så vær forsiktig med hva du deler.",
"language": "Språk",
"notifications": "Varsler",
- "save_and_reload": "Lagre og last inn på nytt"
+ "save_and_reload": "Lagre og last inn på nytt",
+ "select_a_client": "Velg en kunde",
+ "select_an_engineer": "Velg en ingeniør",
+ "ticket_create": "Opprett billett",
+ "internallycommented_at": "Kommentert internt kl."
}
\ No newline at end of file
diff --git a/apps/client/locales/pt/peppermint.json b/apps/client/locales/pt/peppermint.json
index 20bd655d4..f26e2f3a1 100644
--- a/apps/client/locales/pt/peppermint.json
+++ b/apps/client/locales/pt/peppermint.json
@@ -78,6 +78,10 @@
"profile_desc": "Estas informações serão exibidas publicamente, então tenha cuidado com o que compartilha.",
"language": "Idioma",
"notifications": "Notificações",
- "save_and_reload": "Salvar e Recarregar"
+ "save_and_reload": "Salvar e Recarregar",
+ "select_a_client": "Selecionar um cliente",
+ "select_an_engineer": "Selecionar um engenheiro",
+ "ticket_create": "Criar ticket",
+ "internallycommented_at": "Comentado internamente às"
}
\ No newline at end of file
diff --git a/apps/client/locales/se/peppermint.json b/apps/client/locales/se/peppermint.json
index 1306e0635..107e1343b 100644
--- a/apps/client/locales/se/peppermint.json
+++ b/apps/client/locales/se/peppermint.json
@@ -78,5 +78,9 @@
"profile_desc": "Denna information kommer att visas offentligt, så var försiktig med vad du delar.",
"language": "Språk",
"notifications": "Notifieringar",
- "save_and_reload": "Spara och ladda om"
+ "save_and_reload": "Spara och ladda om",
+ "select_a_client": "Välj en klient",
+ "select_an_engineer": "Välj en ingenjör",
+ "ticket_create": "Skapa en biljett",
+ "internallycommented_at": "Kommenterat internt kl."
}
diff --git a/apps/client/locales/tl/peppermint.json b/apps/client/locales/tl/peppermint.json
index fee9879b7..9345f950e 100644
--- a/apps/client/locales/tl/peppermint.json
+++ b/apps/client/locales/tl/peppermint.json
@@ -78,7 +78,11 @@
"profile_desc": "Ang impormasyong ito ay ipapakita sa publiko kaya mag-ingat sa iyong ibabahagi.",
"language": "Wika",
"notifications": "Mga Abiso",
- "save_and_reload": "I-save at I-reload"
+ "save_and_reload": "I-save at I-reload",
+ "select_a_client": "Pumili ng isang kliyente",
+ "select_an_engineer": "Pumili ng isang inhinyero",
+ "ticket_create": "Gumawa ng tiket",
+ "internallycommented_at": "Internallyo nag-komento sa"
}
\ No newline at end of file
diff --git a/apps/client/package.json b/apps/client/package.json
index 3817591b0..cfc2acf00 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -16,6 +16,7 @@
"@mantine/notifications": "^6.0.13",
"@mantine/spotlight": "^6.0.13",
"@mantine/tiptap": "^6.0.13",
+ "@radix-ui/themes": "^2.0.1",
"@tabler/icons-react": "^2.20.0",
"@tailwindcss/forms": "^0.4.0",
"@tiptap/extension-highlight": "^2.0.3",
diff --git a/apps/client/pages/_app.tsx b/apps/client/pages/_app.tsx
index 936d9a267..15b48ff63 100644
--- a/apps/client/pages/_app.tsx
+++ b/apps/client/pages/_app.tsx
@@ -1,3 +1,4 @@
+import "@radix-ui/themes/styles.css";
import "../styles/globals.css";
import {
@@ -9,6 +10,7 @@ import {
import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import { SpotlightProvider } from "@mantine/spotlight";
+import { Theme } from "@radix-ui/themes";
import { useRouter } from "next/router";
import { QueryClient, QueryClientProvider } from "react-query";
@@ -37,10 +39,7 @@ function Auth({ children }: any) {
}
return (
-
- {/* */}
- loading
-
+
);
}
@@ -92,32 +91,35 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: any) {
if (router.asPath.slice(0, 5) === "/auth") {
return (
-
+ <>
+
-
+ >
);
}
if (router.pathname.includes("/admin")) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
@@ -192,22 +194,24 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }: any) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/apps/client/pages/admin/email-queues/new.js b/apps/client/pages/admin/email-queues/new.js
index 3b9b92a4b..5f78b24e0 100644
--- a/apps/client/pages/admin/email-queues/new.js
+++ b/apps/client/pages/admin/email-queues/new.js
@@ -108,6 +108,7 @@ export default function EmailQueues() {
onChange={(e) => setHostname(e.target.value)}
/>
+