From b09f0ad7bda94440eb9d533a5d3322d93de950a7 Mon Sep 17 00:00:00 2001 From: Shay Patel Date: Fri, 29 Dec 2023 18:31:43 +0000 Subject: [PATCH] Initial commit --- README.md | 6 + data.db | Bin 0 -> 20480 bytes data.sqbpro | 8 + requirements.txt | 2 + server.py | 607 ++++++++++++++++++++++++++++++++ static/icon.png | Bin 0 -> 37668 bytes static/profile-icons/admin.png | Bin 0 -> 7851 bytes static/profile-icons/blue.png | Bin 0 -> 7377 bytes static/profile-icons/green.png | Bin 0 -> 14712 bytes static/profile-icons/purple.png | Bin 0 -> 6191 bytes static/profile-icons/red.png | Bin 0 -> 7329 bytes static/src/constants.js | 7 + static/src/jwt-decode.js | 125 +++++++ static/src/ticketTools.js | 143 ++++++++ static/src/userTools.js | 130 +++++++ static/style.css | 89 +++++ templates/base.html | 46 +++ templates/home.html | 91 +++++ templates/login.html | 27 ++ templates/profile.html | 68 ++++ templates/register.html | 37 ++ templates/ticket/new.html | 25 ++ templates/ticket/ticket.html | 102 ++++++ tools/changeAccountType.py | 17 + 24 files changed, 1530 insertions(+) create mode 100644 README.md create mode 100644 data.db create mode 100644 data.sqbpro create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 static/icon.png create mode 100644 static/profile-icons/admin.png create mode 100644 static/profile-icons/blue.png create mode 100644 static/profile-icons/green.png create mode 100644 static/profile-icons/purple.png create mode 100644 static/profile-icons/red.png create mode 100644 static/src/constants.js create mode 100644 static/src/jwt-decode.js create mode 100644 static/src/ticketTools.js create mode 100644 static/src/userTools.js create mode 100644 static/style.css create mode 100644 templates/base.html create mode 100644 templates/home.html create mode 100644 templates/login.html create mode 100644 templates/profile.html create mode 100644 templates/register.html create mode 100644 templates/ticket/new.html create mode 100644 templates/ticket/ticket.html create mode 100644 tools/changeAccountType.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8f3c5a --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# SupportMe +## A Flask-based support ticket system powered by SQLite + +My sister works at a dance studio and often has to deal with many enquiries and issues. Usually, people raise their issues via a telephone call or by email. + +The development of a web-based support ticket system would provide a central service which users can use to submit issues, and will also allow for assistants to quickly see unclaimed or unanswered tickets and respond to them promptly. \ No newline at end of file diff --git a/data.db b/data.db new file mode 100644 index 0000000000000000000000000000000000000000..3eaa7e2d4d9e1f8a8bb81839ab766b5ab411ac50 GIT binary patch literal 20480 zcmeI)O>f#T7zgkK=wQ`ocR>jC66HkHjvjWHHfd5>=T@i%+K@`)RGHwm(egsES;e7; zb^S*D4*N>G&IxaMsnp{%^tT8}9>-3epUoi(O7HyC5lp)Z#vT>gd*!X7s>*?;DT<=X zWkW8Dl9wBW#g1Hu^)D4wX>{+)@~Fa_Ps-NsEm79w1_}fq009U<00Izz00bcLp9%c5 z3&m=sqW&@j9k}d*vvIg=9GawQ5zT6TIVGANhIFl7-h8c36%5pSy5<2Ill|W!&t~3Gb-4yDUyw;Z3h!ySksYgAMbTE4z3)Rwd}x;^};;rxU2m$ zv(;{zeeHzw>&t6vURd2$N2bsw9m^B_M`LmRWYg$3mK?;yP{eY~j74~FE^qo#AOHafKmY;|fB*y_ z009U<00IzLV}bDgU)}zr$PWqxAOHafKmY;|fB*y_009U<00QeJP*n4kQux0CJpW&> zfkieDfB*y_009U<00Izz00bZaf#(Ef&;Os4?WgBz5rY5(AOHafKmY;|fB*y_009U< z;2#N`C?%z4l+@23ds8l$$C*7Dv&CyOa{KeQU%YeV>o52|9eR%682Q&^c-ixVyCL%r NT>66!9MP}??=QP}IdcF2 literal 0 HcmV?d00001 diff --git a/data.sqbpro b/data.sqbpro new file mode 100644 index 0000000..e63bab3 --- /dev/null +++ b/data.sqbpro @@ -0,0 +1,8 @@ +
CREATE TABLE "Message" ( + "messageID" INTEGER PRIMARY KEY, + "ticketID" INTEGER REFERENCES Ticket(ticketID), + "authorID" INTEGER REFERENCES User(userID), + "body" TEXT, + "sentAt" INTEGER, + "ticketStatus" INTEGER +);
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bceef2b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyJWT==2.6.0 +flask==2.2.3 \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..684935f --- /dev/null +++ b/server.py @@ -0,0 +1,607 @@ +import json, sqlite3, time, jwt, random, re +from flask import Flask, redirect, url_for, request, send_from_directory, render_template + +JWT_SECRET_KEY = "".join([random.choice(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]) for i in range(64)]) + + +### start of global constants ### + +ACCOUNT_TYPE_CUSTOMER = 1 +ACCOUNT_TYPE_ASSISTANT = 2 + +TICKET_STATUS_OPEN = 1 +TICKET_STATUS_CLOSED = 2 + +SYSTEM_USER_ID = 0 + +### end of global constants ### + + +App = Flask( + __name__, + static_url_path='', + static_folder='static', + template_folder='templates' +) + + +### start of server-side methods ### + +def create_user(username, email, password): + + with sqlite3.connect("data.db") as connection: + cursor = connection.cursor() + + cursor.execute("SELECT * FROM User WHERE username = ?", [ username ]) + if len(cursor.fetchall()) != 0: + return (False, "That username is already taken.") + + cursor.execute("SELECT * FROM User WHERE email = ?", [ email ]) + if len(cursor.fetchall()) != 0: + return (False, "A user is already registered with that email address.") + + cursor.execute( + "INSERT INTO User (username, password, createdAt, accountType, profileIcon, email) VALUES (?, ?, ?, ?, ?, ?)", + [ username, password, int(time.time()), ACCOUNT_TYPE_CUSTOMER, f"/profile-icons/{random.choice(['blue', 'green', 'purple', 'red'])}.png", email ] + ) + + user_id = cursor.lastrowid + return (True, user_id) + +def login_user(username, password): + + with sqlite3.connect("data.db") as connection: + cursor = connection.cursor() + + cursor.execute("SELECT userID, password, accountType FROM User WHERE username = ?", [ username ]) + user_list = cursor.fetchall() + if len(user_list) == 0: + return (False, "A user with the supplied username and password was not found.") + + _user_id, _password, _account_type = user_list[0] + + if password == _password: + access_token = generate_access_token(_user_id, _account_type) + return (True, access_token) + else: + return (False, "A user with the supplied username and password was not found.") + +def get_profile(user_id): + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT username, createdAt, accountType, profileIcon FROM User WHERE userID = ?", [ user_id ]) + user_list = cursor.fetchall() + if len(user_list) == 0: + return None + + _username, _created_at, _account_type, _profile_icon = user_list[0] + + return { + "user_id": user_id, + "username": _username, + "created_at": _created_at, + "account_type": _account_type, + "profile_icon": _profile_icon + } + +def generate_access_token(user_id, account_type): + access_token = jwt.encode({ + "user_id": user_id, + "account_type": account_type, + "exp": int(time.time()) + 86400 + }, JWT_SECRET_KEY, algorithm="HS256") + return access_token + +def verify_access_token(access_token): + try: + d_at = jwt.decode(access_token.split(" ")[1], JWT_SECRET_KEY, algorithms=["HS256"]) + if d_at["exp"] < time.time(): + return None + else: + return d_at + except: + return None + +def is_valid_username(username): + if not username.isalnum(): + return False, "Username can only contain alphanumeric characters." + elif len(username) > 16: + return False, "Username cannot be more than 16 characters." + return True, True + +def is_valid_password(password): + if len(password) < 8: + return False, "Password must be at least 8 characters." + elif len(password) > 32: + return False, "Password cannot be more than 32 characters." + return True, True + +def is_valid_email(email): + if re.search('^[a-z0-9]+[\._]?[a-z0-9]+[@]\w+[.]\w{2,63}$', email): + return True, True + else: + return False, "The provided email was invalid." + +def is_valid_profile_icon(profile_icon): + if profile_icon not in [ f"/profile-icons/{c}.png" for c in ["blue", "green", "purple", "red"] ]: + return False, "Profile icon is invalid." + return True, True + +def is_valid_ticket_title(ticket_title): + if not all(x.isalnum() or x.isspace() for x in ticket_title): + return False, "Ticket title can only contain spaces and alphanumeric characters." + elif len(ticket_title) < 8: + return False, "Ticket title must be at least 8 characters - be descriptive." + elif len(ticket_title) > 64: + return False, "Ticket title cannot be more than 64 characters - keep it concise." + return True, True + +def is_valid_message(message): + if len(message) < 8 and not message.startswith("!"): + return False, "Message length must be at least 8 characters - be descriptive." + elif len(message) > 512: + return False, "Message length cannot be more than 512 characters." + return True, True + +### end of server-side methods ### + + +### start of flask endpoints ### + +@App.route('/', methods=['GET']) +@App.route('/home', methods=['GET']) +def home_req(): + if request.method == 'GET': + return (render_template('home.html'), 200) + +@App.route('/register', methods=['GET', 'POST']) +def register_req(): + + if request.method == 'GET': + + return (render_template('register.html'), 200) + + elif request.method == 'POST': + + try: + + registration_data = json.loads(request.get_data()) + + if list(registration_data.keys()) != ["username", "email", "password"]: + return ({ "error": "The request failed." }, 400) + + if "" in list(registration_data.values()): + return ({ "error": "Please don't leave any blank fields." }, 400) + + _valid_username = is_valid_username(registration_data["username"]) + if not _valid_username[0]: + return ({ "error": _valid_username[1] }, 400) + + _valid_password = is_valid_password(registration_data["password"]) + if not _valid_password[0]: + return ({ "error": _valid_password[1] }, 400) + + _valid_email = is_valid_email(registration_data["email"]) + if not _valid_email[0]: + return ({ "error": _valid_email[1] }, 400) + + cu_success, cu_res = create_user(registration_data["username"], registration_data["email"], registration_data["password"]) + if cu_success: + return ({ "access_token": generate_access_token(cu_res, ACCOUNT_TYPE_CUSTOMER) }, 200) + else: + return ({ "error": cu_res }, 400) + + except: + return ({ "error": "Server failed to parse the request." }, 400) + +@App.route('/login', methods=['GET', 'POST']) +def login_req(): + + if request.method == 'GET': + return (render_template('login.html'), 200) + + elif request.method == 'POST': + + try: + + login_data = json.loads(request.get_data()) + + if list(login_data.keys()) != ["username", "password"]: + return ({ "error": "The request failed." }, 400) + + if "" in list(login_data.values()): + return ({ "error": "Please don't leave any blank fields." }, 400) + + lu_success, lu_res = login_user(login_data["username"], login_data["password"]) + if lu_success: + return ({ "access_token": lu_res }, 200) + else: + return ({ "error": lu_res }, 400) + + except: + return ({ "error": "Server failed to parse the request." }, 400) + +@App.route('/get-profile/', methods=['GET']) +def get_profile_by_user_id_req(user_id): + if request.method == 'GET': + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None: + return ({ "error": "User is not authenticated to make this request." }, 403) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT username, createdAt, accountType, profileIcon FROM User WHERE userID = ?", [ user_id ]) + user_list = cursor.fetchall() + if len(user_list) == 0: + return ({ "error": "User not found." }, 400) + + _username, _created_at, _account_type, _profile_icon = user_list[0] + + return ({ "user": { + "user_id": user_id, + "username": _username, + "created_at": _created_at, + "account_type": _account_type, + "profile_icon": _profile_icon + } }, 200) + +@App.route('/ticket/new', methods=['GET', 'POST']) +def create_ticket_req(): + if request.method == 'GET': + return (render_template('ticket/new.html'), 200) + + elif request.method == 'POST': + + try: + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None or auth_user["account_type"] != ACCOUNT_TYPE_CUSTOMER: + return ({ "error": "User is not authenticated to make this request." }, 403) + + ticket_data = json.loads(request.get_data()) + + if list(ticket_data.keys()) != ["ticket_title", "message"]: + return ({ "error": "The request failed." }, 400) + + if "" in list(ticket_data.values()): + return ({ "error": "Please don't leave any blank fields." }, 400) + + _valid_ticket_title = is_valid_ticket_title(ticket_data["ticket_title"]) + if not _valid_ticket_title[0]: + return ({ "error": _valid_ticket_title[1] }, 400) + + _valid_message = is_valid_message(ticket_data["message"]) + if not _valid_message[0]: + return ({ "error": _valid_message[1] }, 400) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("INSERT INTO Ticket (customerID, assistantID, openedAt, closedAt, title) VALUES (?, ?, ?, ?, ?);", [ auth_user["user_id"], -1, int(time.time()), -1, ticket_data["ticket_title"] ]) + ticket_id = cursor.lastrowid + + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, auth_user["user_id"], ticket_data["message"], int(time.time()) ]) + message_id = cursor.lastrowid + + return ({ + "ticket_id": ticket_id, + "message_id": message_id + }, 200) + + except: + return ({ "error": "Server failed to parse the request." }, 400) + +@App.route('/get-ticket/', methods=['GET']) +def ticket_json_by_id_req(ticket_id): + if request.method == 'GET': + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None: + return ({ "error": "User is not authenticated to make this request." }, 403) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT customerID, assistantID, openedAt, closedAt, title FROM Ticket WHERE ticketID = ?;", [ ticket_id ]) + + ticket_list = cursor.fetchall() + if len(ticket_list) == 0: + return ({ "error": "Ticket with given ID was not found in the database." }, 404) + + _customer_id, _assistant_id, _opened_at, _closed_at, _title = ticket_list[0] + + if auth_user["user_id"] not in [_customer_id] and auth_user["account_type"] != ACCOUNT_TYPE_ASSISTANT: + return ({ "error": "User is not authenticated to make this request." }, 403) + + return ({ + "ticket_data": { + "ticket_id": ticket_id, + "customer_id": _customer_id, + "assistant_id": _assistant_id, + "opened_at": _opened_at, + "closed_at": _closed_at, + "title": _title + } + }, 200) + +@App.route('/ticket/', methods=['GET', 'POST']) +def ticket_by_id_req(ticket_id): + if request.method == 'GET': + return (render_template('ticket/ticket.html', ticket_id=ticket_id), 200) + + elif request.method == 'POST': + + try: + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None: + return ({ "error": "User is not authenticated to make this request." }, 403) + + message_data = json.loads(request.get_data()) + + if list(message_data.keys()) != ["message"]: + return ({ "error": "The request failed." }, 400) + + if "" in list(message_data.values()): + return ({ "error": "Please don't leave any blank fields." }, 400) + + _valid_message = is_valid_message(message_data["message"]) + if not _valid_message[0]: + return ({ "error": _valid_message[1] }, 400) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT customerID, assistantID, closedAt FROM Ticket WHERE ticketID = ?;", [ ticket_id ]) + + ticket_list = cursor.fetchall() + if len(ticket_list) == 0: + return ({ "error": "Ticket with given ID was not found in the database." }, 404) + + _customer_id, _assistant_id, _closed_at = ticket_list[0] + + if auth_user["user_id"] not in [_customer_id] and auth_user["account_type"] != ACCOUNT_TYPE_ASSISTANT: + return ({ "error": "User is not authenticated to make this request." }, 403) + + user_profile = get_profile(auth_user["user_id"]) + if message_data["message"] == "!close": + cursor.execute("UPDATE Ticket SET closedAt = ? WHERE (ticketID = ?);", [ int(time.time()), ticket_id ]) + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, SYSTEM_USER_ID, f"This ticket has been closed by {user_profile['username']} (ID: {auth_user['user_id']}).", int(time.time()) ]) + elif message_data["message"] == "!open": + cursor.execute("UPDATE Ticket SET closedAt = -1 WHERE (ticketID = ?);", [ ticket_id ]) + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, SYSTEM_USER_ID, f"This ticket has been reopened by {user_profile['username']} (ID: {auth_user['user_id']}).", int(time.time()) ]) + elif message_data["message"] == "!claim" and auth_user["account_type"] == ACCOUNT_TYPE_ASSISTANT: + cursor.execute("UPDATE Ticket SET assistantID = ?, closedAt = ? WHERE (ticketID = ?);", [ auth_user["user_id"], -1, ticket_id ]) + if _closed_at != -1: + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, SYSTEM_USER_ID, f"This ticket has been claimed and reopened by {user_profile['username']} (ID: {auth_user['user_id']}).", int(time.time()) ]) + else: + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, SYSTEM_USER_ID, f"This ticket has been claimed by {user_profile['username']} (ID: {auth_user['user_id']}).", int(time.time()) ]) + else: + if _closed_at != -1: + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, SYSTEM_USER_ID, f"This ticket has been reopened by {user_profile['username']} (ID: {auth_user['user_id']}).", int(time.time())-1 ]) + cursor.execute("UPDATE Ticket SET closedAt = ? WHERE (ticketID = ?);", [ -1, ticket_id ]) + cursor.execute("INSERT INTO Message (ticketID, authorID, body, sentAt) VALUES (?, ?, ?, ?);", [ ticket_id, auth_user["user_id"], message_data["message"], int(time.time())+1 ]) + + message_id = cursor.lastrowid + + return ({ + "ticket_id": ticket_id, + "message_id": message_id + }, 200) + + except: + return ({ "error": "Server failed to parse the request." }, 400) + +@App.route('/get-open-tickets/', methods=['GET']) +def get_open_tickets_by_user_id_req(user_id): + if request.method == 'GET': + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None or auth_user["user_id"] != user_id: + return ({ "error": "User is not authenticated to make this request." }, 403) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT * FROM User WHERE userID = ?;", [ user_id ]) + + if len(cursor.fetchall()) == 0: + return ({ "error": "User with given ID was not found in the database." }, 404) + + cursor.execute("SELECT ticketID, customerID, assistantID, openedAt, closedAt, title FROM Ticket WHERE (customerID = ? OR assistantID = ?) AND closedAt = -1 ORDER BY openedAt ASC;", [ user_id, user_id ]) + ticket_list = cursor.fetchall() + + response = [] + + for t in ticket_list: + _ticket_id, _customer_id, _assistant_id, _opened_at, _closed_at, _title = t + response.append({ + "ticket_id": _ticket_id, + "customer_id": _customer_id, + "assistant_id": _assistant_id, + "opened_at": _opened_at, + "closed_at": _closed_at, + "title": _title + }) + + return ({ + "tickets": response + }, 200) + +@App.route('/get-closed-tickets/', methods=['GET']) +def get_closed_tickets_by_user_id_req(user_id): + if request.method == 'GET': + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None or auth_user["user_id"] != user_id: + return ({ "error": "User is not authenticated to make this request." }, 403) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT * FROM User WHERE userID = ?;", [ user_id ]) + + if len(cursor.fetchall()) == 0: + return ({ "error": "User with given ID was not found in the database." }, 404) + + cursor.execute("SELECT ticketID, customerID, assistantID, openedAt, closedAt, title FROM Ticket WHERE (customerID = ? OR assistantID = ?) AND closedAt != -1 ORDER BY closedAt DESC;", [ user_id, user_id ]) + ticket_list = cursor.fetchall() + + response = [] + + for t in ticket_list: + _ticket_id, _customer_id, _assistant_id, _opened_at, _closed_at, _title = t + response.append({ + "ticket_id": _ticket_id, + "customer_id": _customer_id, + "assistant_id": _assistant_id, + "opened_at": _opened_at, + "closed_at": _closed_at, + "title": _title + }) + + return ({ + "tickets": response + }, 200) + +@App.route('/get-unclaimed-tickets', methods=['GET']) +def get_unclaimed_tickets_req(): + if request.method == 'GET': + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None or auth_user["account_type"] != ACCOUNT_TYPE_ASSISTANT: + return ({ "error": "User is not authenticated to make this request." }, 403) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT ticketID, customerID, assistantID, openedAt, closedAt, title FROM Ticket WHERE assistantID = -1 ORDER BY openedAt ASC;") + ticket_list = cursor.fetchall() + + response = [] + + for t in ticket_list: + _ticket_id, _customer_id, _assistant_id, _opened_at, _closed_at, _title = t + response.append({ + "ticket_id": _ticket_id, + "customer_id": _customer_id, + "assistant_id": _assistant_id, + "opened_at": _opened_at, + "closed_at": _closed_at, + "title": _title + }) + + return ({ + "tickets": response + }, 200) + +@App.route('/get-messages/', methods=['GET']) +def get_messages_by_ticket_id_req(ticket_id): + if request.method == 'GET': + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None: + return ({ "error": "User is not authenticated to make this request." }, 403) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT customerID, assistantID FROM Ticket WHERE ticketID = ?;", [ ticket_id ]) + + ticket_list = cursor.fetchall() + if len(ticket_list) == 0: + return ({ "error": "Ticket with given ID was not found in the database." }, 404) + + _customer_id, _assistant_id = ticket_list[0] + + if auth_user["user_id"] not in [_customer_id] and auth_user["account_type"] != ACCOUNT_TYPE_ASSISTANT: + return ({ "error": "User is not authenticated to make this request." }, 403) + + cursor.execute("SELECT messageID, authorID, body, sentAt FROM Message WHERE ticketID = ? ORDER BY sentAt DESC;", [ ticket_id ]) + message_list = cursor.fetchall() + + response = [] + + for m in message_list: + _message_id, _author_id, _body, _sent_at = m + response.append({ + "message_id": _message_id, + "author_id": _author_id, + "body": _body, + "sent_at": _sent_at + }) + + return ({ + "ticket_id": ticket_id, + "message_list": response + }, 200) + +@App.route('/profile', methods=['GET', 'POST']) +def profile_req(): + if request.method == 'GET': + return (render_template('profile.html'), 200) + + elif request.method == 'POST': + + try: + + auth_user = verify_access_token(request.headers.get("Authorization")) + if auth_user == None: + return ({ "error": "User is not authenticated to make this request." }, 403) + + profile_data = json.loads(request.get_data()) + + if list(profile_data.keys()) != ["new_password", "old_password", "profile_icon"]: + return ({ "error": "The request failed." }, 400) + + if profile_data["new_password"] == "": + profile_data["new_password"] = profile_data["old_password"] + + if "" in list(profile_data.values()): + return ({ "error": "Please don't leave any blank fields." }, 400) + + with sqlite3.connect("data.db") as connection: + + cursor = connection.cursor() + + cursor.execute("SELECT password FROM User WHERE userID = ?;", [ auth_user["user_id"] ]) + + user_list = cursor.fetchall() + if len(user_list) == 0: + return ({ "error": "User with given ID was not found in the database." }, 404) + + if user_list[0][0] != profile_data["old_password"]: + return ({ "error": "Incorrect password was provided." }, 403) + + _valid_password = is_valid_password(profile_data["new_password"]) + if not _valid_password[0]: + return ({ "error": _valid_password[1] }, 400) + + _valid_profile_icon = is_valid_profile_icon(profile_data["profile_icon"]) + if not _valid_profile_icon[0]: + return ({ "error": _valid_profile_icon[1] }, 400) + + cursor.execute("UPDATE User SET password = ?, profileIcon = ? WHERE (userID = ?)", [ profile_data["new_password"], profile_data["profile_icon"], auth_user["user_id"] ]) + + return ({ "msg": "Profile has been updated." }, 200) + + except: + return ({ "error": "Server failed to parse the request." }, 400) + +### end of flask endpoints ### + +if __name__ == "__main__": + App.run(host="0.0.0.0") \ No newline at end of file diff --git a/static/icon.png b/static/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..365bcb538949712aace9c25ef6b8967c0b0076d7 GIT binary patch literal 37668 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 IM6N<$f|N_Xp#T5? literal 0 HcmV?d00001 diff --git a/static/profile-icons/admin.png b/static/profile-icons/admin.png new file mode 100644 index 0000000000000000000000000000000000000000..4619e9de4f69fe46837b1f547fa5617c5297509c GIT binary patch literal 7851 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 G75X2emRD&2 literal 0 HcmV?d00001 diff --git a/static/profile-icons/green.png b/static/profile-icons/green.png new file mode 100644 index 0000000000000000000000000000000000000000..9fc16f6329ce8493570822b850cd34e00ed34180 GIT binary patch literal 14712 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 UWuJ-$&&CjgtB5P5mv2S?6Q3zO#sB~S literal 0 HcmV?d00001 diff --git a/static/profile-icons/red.png b/static/profile-icons/red.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3a9b0d6d362e3cf3581595855636742c3c3d19 GIT binary patch literal 7329 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 ((-2 * bc) & 6)))) : + 0 + ) { + // try to find character in table (0-63, not found => -1) + buffer = chars.indexOf(buffer); + } + return output; + } + + var atob = (typeof window !== "undefined" && + window.atob && + window.atob.bind(window)) || + polyfill; + + function b64DecodeUnicode(str) { + return decodeURIComponent( + atob(str).replace(/(.)/g, function(m, p) { + var code = p.charCodeAt(0).toString(16).toUpperCase(); + if (code.length < 2) { + code = "0" + code; + } + return "%" + code; + }) + ); + } + + function base64_url_decode(str) { + var output = str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw "Illegal base64url string!"; + } + + try { + return b64DecodeUnicode(output); + } catch (err) { + return atob(output); + } + } + + function InvalidTokenError(message) { + this.message = message; + } + + InvalidTokenError.prototype = new Error(); + InvalidTokenError.prototype.name = "InvalidTokenError"; + + function jwtDecode(token, options) { + if (typeof token !== "string") { + throw new InvalidTokenError("Invalid token specified"); + } + + options = options || {}; + var pos = options.header === true ? 0 : 1; + try { + return JSON.parse(base64_url_decode(token.split(".")[pos])); + } catch (e) { + throw new InvalidTokenError("Invalid token specified: " + e.message); + } + } + + /* + * Expose the function on the window object + */ + + //use amd or just through the window object. + if (window) { + if (typeof window.define == "function" && window.define.amd) { + window.define("jwt_decode", function() { + return jwtDecode; + }); + } else if (window) { + window.jwt_decode = jwtDecode; + } + } + +}))); +//# sourceMappingURL=jwt-decode.js.map diff --git a/static/src/ticketTools.js b/static/src/ticketTools.js new file mode 100644 index 0000000..ca22d3d --- /dev/null +++ b/static/src/ticketTools.js @@ -0,0 +1,143 @@ +async function openTicket() { + + let ticket_title_input = document.getElementById("ticket_title_input"); + let message_input = document.getElementById("message_input"); + + const res = await (await fetch('/ticket/new', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + }, + redirect: 'manual', + body: JSON.stringify({ + ticket_title: ticket_title_input.value, + message: message_input.value + }) + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + } else { + window.location.href = "/ticket/" + res["ticket_id"]; + }; + +} + +async function getTicket(ticket_id) { + + const res = await (await fetch('/get-ticket/' + ticket_id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + } + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return false; + } else { + return res; + }; + +} + +async function getOpenTicketsByUserID(user_id) { + + const res = await (await fetch('/get-open-tickets/' + user_id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + } + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return []; + } else { + return res["tickets"]; + }; + +} + +async function getClosedTicketsByUserID(user_id) { + + const res = await (await fetch('/get-closed-tickets/' + user_id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + } + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return []; + } else { + return res["tickets"]; + }; + +} + +async function getUnclaimedTickets() { + + const res = await (await fetch('/get-unclaimed-tickets', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + } + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return []; + } else { + return res["tickets"]; + }; + +} + +async function sendMessage(message_input=document.getElementById("message_input").value) { + + const res = await (await fetch('/ticket/' + ticketID, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + }, + body: JSON.stringify({ + message: message_input + }) + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return []; + } else { + window.location.reload(); + return res; + } + +} + +async function getMessages(ticket_id) { + + const res = await (await fetch('/get-messages/' + ticket_id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + } + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return []; + } else { + return res["message_list"]; + }; + +} \ No newline at end of file diff --git a/static/src/userTools.js b/static/src/userTools.js new file mode 100644 index 0000000..3cd7b63 --- /dev/null +++ b/static/src/userTools.js @@ -0,0 +1,130 @@ +const getDecodedAccessToken = () => { + return localStorage.getItem("access_token") != null ? jwt_decode(localStorage.getItem("access_token")) : null; +} + +function getLoggedInUser() { + let d_at = getDecodedAccessToken(); + if (d_at == null) return null; + if (d_at["exp"] < (Date.now() / 1000)) { + localStorage.removeItem("access_token"); + return null + }; + return d_at; +} + +async function register() { + + let username_input = document.getElementById("username_input"); + let email_input = document.getElementById("email_input"); + let password_input = document.getElementById("password_input"); + let passwordc_input = document.getElementById("passwordc_input"); + + if (password_input.value != passwordc_input.value) return alert("Passwords do not match.") + + const res = await (await fetch('/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'manual', + body: JSON.stringify({ + username: username_input.value, + email: email_input.value, + password: password_input.value + }) + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + } else { + localStorage.setItem("access_token", res["access_token"]); + window.location.href = "/home"; + }; + +} + +async function login() { + + let username_input = document.getElementById("username_input"); + let password_input = document.getElementById("password_input"); + + const res = await (await fetch('/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'manual', + body: JSON.stringify({ + username: username_input.value, + password: password_input.value + }) + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + } else { + localStorage.setItem("access_token", res["access_token"]); + window.location.href = "/home"; + }; + +} + +async function logout() { + + localStorage.removeItem("access_token"); + alert("Successfully logged out."); + window.location.href = "/login"; + +} + +async function getProfile(user_id) { + + const res = await (await fetch('/get-profile/' + user_id, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + } + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + return false; + } else { + return res["user"]; + }; + +} + +async function updateProfile() { + + let new_password_input = document.getElementById("new_password_input"); + let new_passwordc_input = document.getElementById("new_passwordc_input"); + let old_password_input = document.getElementById("old_password_input"); + let profile_icon_select = document.getElementById("profile_icon_select"); + + if (new_password_input.value != new_passwordc_input.value) return alert("Passwords do not match."); + if (old_password_input.value == "") return alert("Old password is needed to confirm changes."); + + const res = await (await fetch('/profile', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + localStorage.getItem("access_token") + }, + redirect: 'manual', + body: JSON.stringify({ + new_password: new_password_input.value, + old_password: old_password_input.value, + profile_icon: profile_icon_select.value + }) + })).json(); + + if (res["error"] != undefined) { + alert(res["error"]); + } else { + alert("Profile updated."); + window.location.reload(); + }; + +} \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..18b8783 --- /dev/null +++ b/static/style.css @@ -0,0 +1,89 @@ +body { + background-color: rgb(203, 255, 238); + font-family: 'Barlow', 'Gill Sans Nova', 'Calibri'; + padding: 20px; + text-align: center; +} + +p { + margin: 20px 0 0 0; +} + +h1 { + text-align: center; + font-size: min(8vw, 45px); + margin: 20px 0 0 0; +} + +h2 { + font-size: 30px; + margin: 20px 0 0 0; +} + +h3 { + font-size: 25px; + margin: 20px 0 0 0; +} + +p, label, input, button, a, li { + font-size: 20px; +} + +p#loggedInMessage { + position: absolute; + right: 20px; + top: 10px; +} + +p#ticket_id { + margin: 0 0 0 0; +} + +ul { + text-align: left; +} + +input { + width: min(225px, 80%); +} + +textarea { + width: min(225px, 80%); +} + +button { + padding: 10px 30px; + margin: 5px 0 0 0; +} + +button#close_ticket_btn, button#open_ticket_btn, button#claim_ticket_btn { + display: none; +} + +section#claimable_tickets { + display: none; +} + +table#ticket_messages tr img { + padding-bottom: 20px; +} + +table#ticket_messages tr td { + text-align: left; + padding: 0 0 0 20px; + vertical-align: top; +} + +table#ticket_messages tr td * { + margin: 0; +} + +table#ticket_messages p.message_sent_at { + font-size: 15px +} + +@media only screen and (max-width: 600px) { + input { + width: min(200px, 80%); + } +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..d3df899 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,46 @@ + + + + + + + + + SupportMe + + + + + + + + +

...

+ +
+ +
+

💬 SupportMe 💁

+
+ + + + + + + + + {% block content %} {% endblock %} + + + + \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..36dede5 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,91 @@ +{% extends 'base.html' %} + +{% block content %} +

Home

+ +
+ + +
+ + + +
+ +

Open tickets

+ +
    + +
    + +
    + +

    Claimable tickets

    + +
      + +
      + +
      + +

      Closed tickets

      + +
        + +
        + + +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..d4f0447 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} + +{% block content %} +

        Login

        + +
        + +
        + + +

        + +
        + + +

        + + + +

        + + Don't have an account? Click here to sign up! + + +{% endblock %} \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..fc84076 --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} + +{% block content %} +

        Profile

        + +
        + + + +

        + +
        + + +

        + +
        + + +

        + +
        + + +

        + +
        + + +

        + +
        + + +

        + +
        + + +

        + + + +

        + + Don't have an account? Click here to sign up! + + +{% endblock %} \ No newline at end of file diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 0000000..b116d72 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} + +{% block content %} +

        Register

        + +
        + +
        + + +

        + +
        + + +

        + +
        + + +

        + +
        + + +

        + + + +

        + + Already have an account? Click here to log in! + + +{% endblock %} \ No newline at end of file diff --git a/templates/ticket/new.html b/templates/ticket/new.html new file mode 100644 index 0000000..9e641de --- /dev/null +++ b/templates/ticket/new.html @@ -0,0 +1,25 @@ +{% extends 'base.html' %} + +{% block content %} +

        Open a new support ticket

        + +
        + +
        + + +

        + +
        + + +

        + + + +

        + + +{% endblock %} \ No newline at end of file diff --git a/templates/ticket/ticket.html b/templates/ticket/ticket.html new file mode 100644 index 0000000..ac309e1 --- /dev/null +++ b/templates/ticket/ticket.html @@ -0,0 +1,102 @@ +{% extends 'base.html' %} + +{% block content %} +

        ...

        +

        Ticket #{{ ticket_id }}

        + +

        +
        + +

        +

        + ...
        + This ticket has not been claimed +

        + +
        + +
        + + +

        + + + + + + +

        + +
        + + +{% endblock %} \ No newline at end of file diff --git a/tools/changeAccountType.py b/tools/changeAccountType.py new file mode 100644 index 0000000..04c0071 --- /dev/null +++ b/tools/changeAccountType.py @@ -0,0 +1,17 @@ +import sqlite3 + +user_id = int(input("Enter the user ID of the account: ")) +account_type = int(input("Enter an account type to set (1 = customer, 2 = assistant): ")) + +with sqlite3.connect("../data.db") as connection: + + try: + cursor = connection.cursor() + cursor.execute("UPDATE User SET accountType = ? WHERE userID = ?;", [ account_type, user_id ]) + if cursor.rowcount == 0: + print("User with given ID was not found in the database. Terminating.") + else: + print(f"User with ID {user_id}'s account type has been successfully updated to {account_type}.") + + except sqlite3.Error as error: + print(error) \ No newline at end of file