diff --git a/src/modules/api/auth.ts b/src/modules/api/auth.ts index 30dabf1e..7c686b8a 100644 --- a/src/modules/api/auth.ts +++ b/src/modules/api/auth.ts @@ -9,7 +9,19 @@ import { createHash } from 'crypto'; import ms from 'ms'; export const auth = { - login: async (req: express.Request, res: express.Response, data, rememberMe: boolean) => { // Logging in via amethyst.host + /** + * Logins in the user via amethyst.host + * @param {express.Request} req Express Request + * @param {express.Response} res Express Response + * @param {Object} data Data based from DB response + * @param {Boolean} rememberMe If JWT session should be longer or not + * @returns {Promise} Email, Access Token, and Expiration. + * + * @example + * const loginToken = await .login(req, res, account, rememberMe); + * if (loginToken == 403) return res.sendStatus(403); + */ + login: async (req: express.Request, res: express.Response, data: any, rememberMe: boolean) => { // Logging in via amethyst.host const createdIn = parseInt(Date.now().toString().slice(0, -3)) // because for some reason node js decides to use an expanded timestamp const ipAddr = req.socket.remoteAddress; const salt = Buffer.alloc((process.env.SALT.length * 2) - 1, process.env.SALT) @@ -19,7 +31,7 @@ export const auth = { secret: Buffer.from(data.salt, 'hex') }) if (!verifiedHash) return 403; - const userData = { email: data.email, name: data.name, id: data.user_id } + const userData = { email: data.email, name: data.name, id: data.user_id, permission_id: data.permission_id } const accessTokenOpts = sql.jwtOptions let expiresIn = ms('1h') if (rememberMe) { @@ -27,18 +39,19 @@ export const auth = { } expiresIn = parseInt(expiresIn.toString().slice(0, -3)) accessTokenOpts.expiresIn = expiresIn - const accessToken = jwt.sign(userData, process.env.ACCESS_TOKEN, accessTokenOpts); + const accessToken = jwt.sign(userData, process.env.ACCESS_TOKEN, accessTokenOpts); // Will implement Refresh Token soon const ip = createHash('sha256').update(ipAddr).digest('hex'); // Convert IP address to SHA256 hash expiresIn = expiresIn + createdIn; sql.db.prepare("INSERT INTO sessions (user_id, jwt, createdIn, expiresIn, ip) VALUES (?, ?, ?, ?, ?)").run(userData.id, accessToken, createdIn, expiresIn, ip) // Adds the token to DB in case the user decides to logout. return { email: data.email, accessToken: accessToken, expiresIn: expiresIn }; }, - setCookie: async (req: express.Request, res: express.Response, value: string, expiresIn: number) => { + setCookie: async (req: express.Request, res: express.Response, value: string, expiresIn: number): Promise => { res.cookie('jwt', value, { secure: true, httpOnly: true, maxAge: expiresIn, sameSite: 'strict' }); // Client Side wont access this because httpOnly. + return true; }, - getUserData: async (req: express.Request, res: express.Response) => { + getUserData: async (req: express.Request, res: express.Response): Promise => { if (req.cookies.jwt) { - const verifyToken = await auth.verifyToken(0, req, res, false, false) + const verifyToken = await auth.verifyToken(req, res, false, false) if (verifyToken && verifyToken["accessToken"]) { return verifyToken; } else { @@ -48,12 +61,39 @@ export const auth = { return false; } }, - verifyToken: (user_id: number, req: express.Request, res: express.Response, sendResponse: boolean, useAuthorization: boolean) => { // Probably not a good idea to do this, as most people use next() + updateJWT: async (req: express.Request, res: express.Response): Promise => { // Wont be used until I implement Refresh Tokens + if (req.cookies.jwt) { + const verifyToken = await auth.verifyToken(req, res, false, false) + if (verifyToken && verifyToken["accessToken"]) { + const account = await sql.db.prepare("SELECT user_id, name, email, password, salt, verified, permission_id FROM users WHERE user_id = ?") + .get(verifyToken["user_id"]); + if (!account) return false; + + } else { + return false; + } + } else { + return false; + } + }, + /** + * Verifies if the JWT token is valid or not and gives response with User Data. + * @param {express.Request} req Express Request + * @param {express.Response} res Express Response + * @param {boolean} sendResponse If it should use res.sendStatus or not. + * @param {boolean} useAuthorization If it should use the Authorization header. + * @returns {any} User Data or Response. + * + * @example + * const userData = await .verifyToken(req, res, false, false); // Request, Response, sendResponse (False), useAuthorization (False). + * if (typeof userData != "object") return res.sendStatus(userData); // If its not an object. (Could be either undefined or number.) + */ + verifyToken: (req: express.Request, res: express.Response, sendResponse: boolean, useAuthorization: boolean): any => { // Probably not a good idea to do this, as most people use next() const currentDate = parseInt(Date.now().toString().slice(0, -3)) let authorization; let token; if (useAuthorization) { - authorization = req.headers['authorization']; + authorization = req.headers['Authorization'] || req.headers['authorization']; if (authorization) { token = authorization.split(' ')[1]; } @@ -64,13 +104,15 @@ export const auth = { if (!authorization) return (sendResponse) ? res.sendStatus(403) : 403; const tokenValid = jwt.verify(token, process.env.ACCESS_TOKEN, sql.jwtOptions) if (!tokenValid) return (sendResponse) ? res.sendStatus(403) : 403; // Forbidden. - if (tokenValid.id != user_id && useAuthorization) return (sendResponse) ? res.sendStatus(403) : 403; // Forbidden + //if (tokenValid.id != user_id && useAuthorization) return (sendResponse) ? res.sendStatus(403) : 403; // Forbidden const ip = createHash('sha256').update(req.ip).digest('hex'); const tokenInDB = sql.db.prepare('SELECT user_id FROM sessions WHERE user_id = ? AND jwt = ? AND expiresIn > ? AND ip = ?').pluck().all(tokenValid.id, token, currentDate, ip); if (!tokenInDB.length) return (sendResponse) ? res.sendStatus(403) : 403; if (tokenValid.exp < currentDate) return (sendResponse) ? res.sendStatus(403) : 403; + const userExists = sql.db.prepare('SELECT count(*) FROM users WHERE user_id = ?').pluck().get(tokenValid.id); + if (!userExists) return (sendResponse) ? res.sendStatus(404) : 404; //res.setHeader('Authorization', 'Bearer ' + tokenValid) - const response = { accessToken: token, user_id: tokenInDB[0], name: tokenValid.name, email: tokenValid.email } + const response = { accessToken: token, user_id: tokenInDB[0], name: tokenValid.name, email: tokenValid.email, permission_id: tokenValid.permission_id } return (sendResponse) ? res.status(200).json(response) : response; }, discord: async (data) => { // Authenticating with Discord @@ -87,14 +129,11 @@ export const prop = { const { email, password, rememberMe } = req.body; if ([email, password].includes(undefined)) return res.status(406) .send("Please enter in an Email, and Password."); - const account = await sql.db.prepare("SELECT user_id, name, email, password, salt, verified FROM users WHERE email = ?") - .get(email); // Checks if the user exists. - //if (!account) return res.sendStatus(404); // User doesn't exist. + const account = await sql.db.prepare("SELECT user_id, name, email, password, salt, verified, permission_id FROM users WHERE email = ?") + .get(email); // Checks if the user exists. if (!account) return res.sendStatus(404); // User doesn't exist. - const loginToken = await auth.login(req, res, account, rememberMe); if (loginToken == 403) return res.sendStatus(403); - auth.setCookie(req, res, loginToken.accessToken, loginToken.expiresIn); res.json(loginToken) } diff --git a/src/modules/api/tickets.ts b/src/modules/api/tickets.ts new file mode 100644 index 00000000..c85cd686 --- /dev/null +++ b/src/modules/api/tickets.ts @@ -0,0 +1,302 @@ +/** + * API for the Support Ticket System + */ +import express from 'express'; +import { sql } from '../sql'; +import { auth } from './auth'; +import { permissions } from '../permissions' +import ticket_categories from '../../ticket_categories.json'; + +function encode_base64(str) { + if (!str.length) return false; + return btoa(encodeURIComponent(str)); +} +function decode_base64(str) { + if (!str.length) return false; + return decodeURIComponent(atob(str)); +} + +const settings = { + maxTitle: 100, // Maximum Length for the title of the ticket. + maxBody: 1000, // Maximum Length for messages. + maxUploadLimit: 12 // 12 MB limit for files/images. +} + +/** + * Allowed Method + * @param req Request + * @param res Response + * @param type Type of whats allowed (GET, POST, etc) + * @param paramName The name of the parameter + * @param userData userData + * @returns boolean + */ +function allowedMethod(req: express.Request, res: express.Response, type: Array, paramName: string, userData: any): boolean { + if (!permissions.hasPermission(userData['permission_id'], `/tickets/${paramName}`)) { + res.sendStatus(403); + return false; + } + res.set("Allow", type.join(", ")); + if (!type.includes(req.method)) { + res.sendStatus(405); + return false; + } + return true; +} +function editContent(content, timestamp, ticket_id) { + sql.db.prepare('UPDATE tickets SET content = ?, editedIn = ? WHERE ticket_id = ?').run(content, timestamp, ticket_id); +} + +export const prop = { + name: "tickets", + desc: "Support Ticket System", + run: async (req: express.Request, res: express.Response): Promise => { + const allowedMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"]; + const params = req.params[0].split("/").slice(1); // Probably a better way to do this in website.ts via doing /api/:method/:optionalparam*? but it doesnt work for me. + res.set("Allow", allowedMethods.join(", ")); // To give the method of whats allowed + if (!allowedMethods.includes(req.method)) return res.sendStatus(405) // If the request isn't included from allowed methods, then respond with Method not Allowed. + const userData = await auth.verifyToken(req, res, false, false) //true + if (typeof userData != "object") return res.sendStatus(userData); + const timestamp = Date.now(); + let paramName = params[0] + if (!isNaN(parseInt(paramName))) { + paramName = ":ticketid"; + } + let level = userData['permission_id'].split(":") + if ([3,4].includes(level[0])) { // Developer & Administrator + level = 5; + } + if (level[1]) { // Support Level + level = parseInt(level[1]) + } + + function newMsg(msg) { + msg["content"] = decode_base64(msg["content"]); + msg["createdIn"] = new Date(msg["createdIn"]); + msg["editedIn"] = (msg["editedIn"] == 0) ? null : new Date(msg["editedIn"]); + return msg; + } + function newTicket(ticket) { + const name = sql.db.prepare('SELECT name FROM users WHERE user_id = ?').pluck().get(ticket.user_id); + if (name && name.length) { + ticket['name'] = name; + if (ticket['content'].length > 100) { + ticket['content'] = ticket['content'].slice(0, 100); + } + ticket["subject"] = decode_base64(ticket["subject"]); + ticket["content"] = decode_base64(ticket["content"]); + ticket["opened"] = new Date(ticket["opened"]); + ticket["editedIn"] = (ticket["editedIn"] == 0) ? null : new Date(ticket["editedIn"]); + ticket["closed"] = (ticket["closed"] == 0) ? null : new Date(ticket["closed"]); + return ticket; + } + return null; + } + + switch (paramName) { + case "create": {// Creates the ticket. + if (allowedMethod(req, res, ["POST"], paramName, userData)) { + const { subject, content, categories } = req.body; + if (!subject || !content) return res.sendStatus(406); + // subject=Hello World&content=Lorem ipsum dolor sit amet, consectetur...&categories=0,1,2 + if (subject.length > settings.maxTitle) return res.status(403).send(`Subject is too long. Max Length is ${settings.maxTitle}`); + if (content.length > settings.maxBody) return res.status(403).send(`Content is too long. Max Length is ${settings.maxBody}`); + const category_ids = (categories) ? categories.split(",").map(category => { + const findCategory = ticket_categories.find(cate => cate.id == parseInt(category)); + if (findCategory) { + category = parseInt(category); // Converting it to Int in case of any strings at the end. + return category; + } + }) : [] + await sql.db.prepare('INSERT INTO tickets (user_id, subject, content, category_ids, opened, createdIn) VALUES (?, ?, ?, ?, ?, ?)') + .run(userData["user_id"], encode_base64(subject), encode_base64(content), category_ids.join(","), timestamp, timestamp); + const getTicket = await sql.db.prepare('SELECT ticket_id, user_id, subject, content, opened FROM tickets WHERE user_id = ? AND subject = ? AND opened = ?').get(userData["user_id"], encode_base64(subject), timestamp); + if (!getTicket) return res.sendStatus(201) + return res.status(201).json(getTicket); + } + break; + } + case "categories": { // Categories + res.set("Allow", "GET"); + if (req.method != "GET") return res.sendStatus(405); + return res.status(200).json(ticket_categories); + } + case "list": { // Lists the tickets + if (allowedMethod(req, res, ["GET"], paramName, userData)) { + let page = 1; + let status = 0; + let pageLimit = 10; + if (req.query.page) page = parseInt(req.query.page.toString()); + if (req.query.status == "closed") status = 1; + if (req.query.limit) pageLimit = parseInt(req.query.limit.toString()); + let tickets = []; + if (typeof level == 'object') { + if (pageLimit > 10) pageLimit = 10; // Users will have access to less pages, just in case. + tickets = await sql.db.prepare('SELECT ticket_id, user_id, subject, content, category_ids, opened, closed, level FROM tickets WHERE user_id = ? AND status = ? ORDER BY opened ASC LIMIT ? OFFSET ?') + .all(userData["user_id"], status, pageLimit, ((page - 1) * pageLimit)); + } else { + if (level > 5 || level < 3) return res.sendStatus(403); + if (pageLimit > 50) pageLimit = 50; // Making sure server isn't vulnerable to this kind of attack. + tickets = await sql.db.prepare('SELECT ticket_id, user_id, subject, content, category_ids, opened, closed, level FROM tickets WHERE level < ? AND status = ? ORDER BY opened ASC LIMIT ? OFFSET ?') + .all(level + 1, status, pageLimit, ((page - 1) * pageLimit)); + } + /*const result = tickets.map(ticket => { + const name = sql.db.prepare('SELECT name FROM users WHERE user_id = ?').pluck().get(ticket.user_id); + if (name && name.length) { + ticket['name'] = name; + if (ticket['content'].length > 100) { + ticket['content'] = ticket['content'].slice(0, 100); + } + return ticket; + } + });*/ + return res.status(200).json(tickets.map(newTicket)); + } + break; + } + case ":ticketid": { // Ticket ID + const ticketID = parseInt(params[0]) + if (params[1]) { // Without api/tickets/:ticketid/:msgid + if (allowedMethod(req, res, ["GET", "PATCH", "DELETE"], paramName, userData)) { // copy paste + if (ticketID < 0) return res.sendStatus(406); + const getTicket = await sql.db.prepare('SELECT ticket_id, user_id, subject, content, level, category_ids, opened, closed, level FROM tickets WHERE ticket_id = ?') + .get(ticketID); + if (!getTicket) return res.status(404).send("Ticket not found."); // If no tickets are found. + if (getTicket.user_id != userData["user_id"] && userData["permission_id"] != `2:${getTicket.level}`) return res.sendStatus(403); + const msgID = parseInt(params[1]) + if (msgID < 0) return res.sendStatus(406); + const getMessage = await sql.db.prepare('SELECT msg_id, ticket_id, user_id, content, files, createdIn, editedIn FROM ticket_msgs WHERE ticket_id = ? AND msg_id = ?') + .get(ticketID, msgID); + if (!getMessage) return res.status(404).send("Message not found."); // If no message is found. + const { content } = req.body; + switch (req.method) { + case "GET": { // Viewing the contents of a message (Not really sure why you would want to do this but it's there.) + return res.status(200).json(newMsg(getMessage)); + } + case "PATCH": { // Editing the message. + if (getMessage.user_id != userData["user_id"]) return res.sendStatus(403); + const newContent = encode_base64(content); + await sql.db.prepare('UPDATE ticket_msgs SET content = ?, editedIn = ? WHERE ticket_id = ? AND msg_id = ?').run(newContent, timestamp, getTicket["ticket_id"], getMessage["msg_id"]) + getMessage["content"] = newContent; + getMessage["editedIn"] = timestamp; + return res.status(200).json(newMsg(getMessage)); + } + case "DELETE": { // Deleting a message + await sql.db.prepare('DELETE FROM ticket_msgs WHERE msg_id = ? AND ticket_id = ?').run(getMessage["msg_id"], getTicket["ticket_id"]); + return res.sendStatus(204); + } + default: + return res.sendStatus(404); // This should never happen. + } + } + } else { + if (allowedMethod(req, res, ["GET", "POST", "PUT", "DELETE"], paramName, userData)) { + if (ticketID < 0) return res.sendStatus(406); + const getTicket = await sql.db.prepare('SELECT ticket_id, user_id, subject, content, level, category_ids, opened, closed, level FROM tickets WHERE ticket_id = ?') + .get(ticketID); // ESLint wants me to use const + if (!getTicket) return res.status(404).send("Ticket not found."); // If no tickets are found. + if (getTicket.user_id != userData["user_id"] && userData["permission_id"] != `2:${getTicket.level}`) return res.sendStatus(403); + const { content } = req.body; + // Using {} at switch cases because ESLint is complaining + + switch (req.method) { + case "GET": { // Viewing the Ticket Conversation. + let page = 1; + let pageLimit = 10; + if (req.query.page) page = parseInt(req.query.page.toString()); + if (req.query.limit) pageLimit = parseInt(req.query.limit.toString()); + let messages = await sql.db.prepare('SELECT msg_id, user_id, content, files, createdIn, editedIn FROM ticket_msgs WHERE ticket_id = ? ORDER BY createdIn ASC LIMIT ? OFFSET ?') + .all(ticketID, pageLimit, ((page - 1) * pageLimit)); + if (messages.length) { // If there are messages + messages = messages.map(newMsg); + getTicket['msgs'] = messages; + } + + return res.status(200).json(newTicket(getTicket)); + } + case "POST": { // Sends a new message to that ticket. (Responds with the message content) + if (!content) return res.sendStatus(406); + if (content.length > settings.maxBody) return res.status(403).send(`Content is too long. Max Length is ${settings.maxBody}`); + + await sql.db.prepare('INSERT INTO ticket_msgs (ticket_id, user_id, content, createdIn) VALUES (?, ?, ?, ?)') + .run(getTicket["ticket_id"], userData["user_id"], encode_base64(content), timestamp); + const getMsg = await sql.db.prepare('SELECT msg_id, ticket_id, user_id, content, createdIn FROM ticket_msgs WHERE user_id = ? AND content = ? AND createdIn = ?').get(userData["user_id"], encode_base64(content), timestamp); + if (!getMsg) return res.sendStatus(201); + return res.status(201).json(getMsg); + } + case "PUT": { // Updates the status on the ticket (Either opening it again after being closed, setting tags, etc) + const { closed, subject, categories, reopen } = req.body; + + /** + * closed (closed=1) - Close the ticket + * subject (subject=Hello World) - Subject of the Ticket (title) + * content (content=Lorem ipsum dolor...) - Content of the Ticket. + * categories (categories=0,1) - Categories for the ticket. + * reopen (reopen=1) - Reopens the ticket + */ + + if (closed && closed == "1" && !reopen) { // If closed is provided, close the ticket. + if (getTicket["closed"] != 0) return res.sendStatus(204); + sql.db.prepare('UPDATE tickets SET status = 1, closed = ? WHERE ticket_id = ?').run(timestamp, getTicket["ticket_id"]); + return res.sendStatus(204); + } + if (reopen && reopen == "1") { // If reopen is provided, Open the ticket again. + if (getTicket["closed"] == 0) return res.sendStatus(406); + sql.db.prepare('UPDATE tickets SET status = 0, opened = ?, closed = 0 WHERE ticket_id = ?').run(timestamp, getTicket["ticket_id"]); + return res.sendStatus(204); + } + + + if (getTicket["closed"] != 0) return res.sendStatus(406); // If ticket is closed + if (getTicket["user_id"] != userData["user_id"]) return res.sendStatus(403); // No Staff is allowed to change the users title and content. + if (subject && subject.length) { + sql.db.prepare('UPDATE tickets SET subject = ? editedIn = ? WHERE ticket_id = ?').run(encode_base64(subject), timestamp, getTicket["ticket_id"]); + if (content && content.length) { + editContent(encode_base64(content), timestamp, getTicket["ticket_id"]) + } + return res.sendStatus(204); + } + if (content && content.length) { + editContent(encode_base64(content), timestamp, getTicket["ticket_id"]); + return res.sendStatus(204); + } + if (categories && categories.length) { + const category_ids = (categories) ? categories.split(",").map(category => { + const findCategory = ticket_categories.find(cate => cate.id == parseInt(category)); + if (findCategory) { + category = parseInt(category); // Converting it to Int in case of any strings at the end. + return category; + } + }) : [] + if (!category_ids.length) return res.sendStatus(406); + sql.db.prepare('UPDATE tickets SET category_ids = ? WHERE ticket_id = ?').run(category_ids.join(","), getTicket["ticket_id"]); + return res.sendStatus(204); + } + return res.sendStatus(406); + } + case "DELETE": { // Closes the Ticket. + if (permissions.hasPermission(userData['permission_id'], `/tickets/:ticketid/delete`) && req.body.force) { // Force delete a message. (Used for spam tickets) + await sql.db.prepare('DELETE FROM tickets WHERE ticket_id = ?').run(getTicket["ticket_id"]); + if (req.body.msgs) { + await sql.db.prepare('DELETE FROM ticket_msgs WHERE ticket_id = ?').run(getTicket["ticket_id"]); + } + return res.sendStatus(204); + } else { // Closing a ticket. (Not deleting) + await sql.db.prepare('UPDATE tickets SET status = 1, closed = ? WHERE ticket_id = ?').run(timestamp, getTicket["ticket_id"]); + return res.sendStatus(204); + } + break; + } + default: + return res.sendStatus(404); // This should never happen. + } + } + } + break; + } + default: // If none of the above are provided. + return res.sendStatus(404); + } + } +} + \ No newline at end of file diff --git a/src/modules/permissions.ts b/src/modules/permissions.ts new file mode 100644 index 00000000..df086821 --- /dev/null +++ b/src/modules/permissions.ts @@ -0,0 +1,21 @@ +import permission from '../permissions.json' + +/** + * -- Permission Levels -- + * Edit permissions in src/permissions.json + * + * 0 - Everyone - Users who are registered have access to these pages/endpoints + * 1 - Client - Users who have bought a service can access pages normal users cannot access, like services. + * 2:level - Support - Staff members who have access to list available tickets at their level. They can only access tickets from their level below. + * 3 - Developer - Users who have access to special pages. + * 4 - Administrator - Users who has access to almost all pages. + * + */ + +export const permissions = { + hasPermission: (permissionID: string, path: string): boolean => { + if (!permissionID) return false; + if (parseInt(permissionID) == 0) return false; + return permission[permissionID].accessAPI.includes(path) + } +} diff --git a/src/modules/website.ts b/src/modules/website.ts index 2b5c366a..ea9f8421 100644 --- a/src/modules/website.ts +++ b/src/modules/website.ts @@ -74,13 +74,9 @@ const apiMethod = function(r: express.Request, s: express.Response) { /* amethyst.host/api/bill amethyst.host/api/auth and so on.. */ -app.get("/api/:method", (r: express.Request, s: express.Response) => { +app.all("/api/:method*", (r: express.Request, s: express.Response) => { apiMethod(r, s); }); -app.post("/api/:method", (r: express.Request, s: express.Response) => { - apiMethod(r, s); -}); - // billing app.get("/billing", async (r: express.Request, s: express.Response) => { const userData = await auth.getUserData(r, s) diff --git a/src/permissions.json b/src/permissions.json new file mode 100644 index 00000000..95fdb16f --- /dev/null +++ b/src/permissions.json @@ -0,0 +1,21 @@ +{ + "1": { + "accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid"] + }, + "2": { + "accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid"] + }, + "2:3": { + "accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid"] + }, + "2:4": { + "accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid"] + }, + "2:5": { + "accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid"] + }, + "4": { + "accessAPI": ["/tickets", "/tickets/create", "/tickets/list", "/tickets/:ticketid", "/tickets/:ticketid/delete"] + } + +} \ No newline at end of file diff --git a/src/sql/init.sql b/src/sql/init.sql index 1be73da4..b9199fb8 100644 --- a/src/sql/init.sql +++ b/src/sql/init.sql @@ -1,12 +1,13 @@ -- initialize all tables CREATE TABLE IF NOT EXISTS users ( - user_id INTEGER NOT NULL PRIMARY KEY, -- The Users ID - registered TIMESTAMP NOT NULL, -- When the user registered - name TEXT NOT NULL, -- The users real name - email TEXT NOT NULL, -- For contacting the user - password TEXT NOT NULL, -- Required - salt TEXT NOT NULL, -- Extra Security, this will be used as an extra salt - verified INTEGER NOT NULL DEFAULT 0 -- If the user verified their email (1) or if they verified their phone # (2) + user_id INTEGER NOT NULL PRIMARY KEY, -- The Users ID + registered TIMESTAMP NOT NULL, -- When the user registered + name TEXT NOT NULL, -- The users real name + email TEXT NOT NULL, -- For contacting the user + password TEXT NOT NULL, -- Required + salt TEXT NOT NULL, -- Extra Security, this will be used as an extra salt + verified INTEGER NOT NULL DEFAULT 0, -- If the user verified their email (1) or if they verified their phone # (2) + permission_id INTEGER NOT NULL DEFAULT 0 -- Users permission ID. ); CREATE TABLE IF NOT EXISTS servers ( @@ -33,3 +34,28 @@ CREATE TABLE IF NOT EXISTS sessions ( ip TEXT NOT NULL, -- Remote Address rememberMe INTEGER NOT NULL DEFAULT 0 -- Will change what expiresIn should be ); + +CREATE TABLE IF NOT EXISTS tickets ( + ticket_id INTEGER NOT NULL PRIMARY KEY, -- Ticket ID + user_id INTEGER NOT NULL, -- User ID of who created the ticket. + subject TEXT NOT NULL DEFAULT 'Ticket', -- Ticket Subject (Or title) + content TEXT NOT NULL DEFAULT 'Message', -- Contents of the ticket. + category_ids TEXT NOT NULL DEFAULT '0,1', -- Category(s) for the ticket. (0 being billing, and 1 being bug) + status INTEGER NOT NULL DEFAULT 0, -- Status of the Ticket, if its open (0), or if its closed (1). + opened TIMESTAMP NOT NULL, -- When the ticket was opened. + closed TIMESTAMP NOT NULL DEFAULT 0, -- When the ticket was closed. + files TEXT NOT NULL DEFAULT 0, -- Any files that are uploaded. (Will be shown in URL form) + level INTEGER NOT NULL DEFAULT 3, -- Level of support + createdIn TIMESTAMP NOT NULL, -- When the ticket was created. + editedIn TIMESTAMP NOT NULL DEFAULT 0 -- When the ticket was edited. +); + +CREATE TABLE IF NOT EXISTS ticket_msgs ( + msg_id INTEGER NOT NULL PRIMARY KEY, -- Message ID + ticket_id INTEGER NOT NULL, -- Ticket ID + user_id INTEGER NOT NULL, -- User ID of who sent the message. + content TEXT NOT NULL DEFAULT 'Message', -- Message Content (Encoded in Base64, will probably encrypt in AES256) + files TEXT NOT NULL DEFAULT 0, -- Any files that are uploaded. (Will be shown in URL form) + createdIn TIMESTAMP NOT NULL, -- When the message was created. + editedIn TIMESTAMP NOT NULL DEFAULT 0 -- When the message was edited. +) \ No newline at end of file diff --git a/src/ticket_categories.json b/src/ticket_categories.json new file mode 100644 index 00000000..19c56964 --- /dev/null +++ b/src/ticket_categories.json @@ -0,0 +1,11 @@ +[{ + "name": "Billing", + "id": 0, + "color": "#99ff66", + "description": "Ask any billing questions here!" +}, { + "name": "Partner", + "id": 1, + "color": "#9966ff", + "description": "If you want to partner with us!" +}] \ No newline at end of file