mirror of
https://github.com/0xShay/SupportMe.git
synced 2026-01-10 21:03:25 +00:00
Initial commit
This commit is contained in:
6
README.md
Normal file
6
README.md
Normal file
@@ -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.
|
||||
8
data.sqbpro
Normal file
8
data.sqbpro
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><sqlb_project><db path="H:/Subjects/Computer Science Y13/NEA/Build/data.db" readonly="0" foreign_keys="1" case_sensitive_like="0" temp_store="0" wal_autocheckpoint="1000" synchronous="2"/><attached/><window><main_tabs open="structure browser pragmas query" current="3"/></window><tab_structure><column_width id="0" width="300"/><column_width id="1" width="0"/><column_width id="2" width="100"/><column_width id="3" width="1272"/><column_width id="4" width="0"/><expanded_item id="0" parent="1"/><expanded_item id="1" parent="1"/><expanded_item id="2" parent="1"/><expanded_item id="3" parent="1"/></tab_structure><tab_browse><current_table name="4,4:mainUser"/><default_encoding codec=""/><browse_table_settings><table schema="main" name="User" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk="_rowid_"><sort/><column_widths><column index="1" value="51"/><column index="2" value="72"/><column index="3" value="71"/><column index="4" value="75"/><column index="5" value="91"/><column index="6" value="77"/><column index="7" value="66"/></column_widths><filter_values/><conditional_formats/><row_id_formats/><display_formats/><hidden_columns/><plot_y_axes/><global_filter/></table></browse_table_settings></tab_browse><tab_sql><sql name="SQL 1">CREATE TABLE "Message" (
|
||||
|
||||
"messageID" INTEGER PRIMARY KEY,
|
||||
|
||||
"ticketID" INTEGER REFERENCES Ticket(ticketID),
|
||||
|
||||
"authorID" INTEGER REFERENCES User(userID),
|
||||
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
PyJWT==2.6.0
|
||||
flask==2.2.3
|
||||
607
server.py
Normal file
607
server.py
Normal file
@@ -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/<int:user_id>', 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/<int:ticket_id>', 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/<int:ticket_id>', 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/<int:user_id>', 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/<int:user_id>', 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/<int:ticket_id>', 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")
|
||||
BIN
static/icon.png
Normal file
BIN
static/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
static/profile-icons/admin.png
Normal file
BIN
static/profile-icons/admin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
BIN
static/profile-icons/blue.png
Normal file
BIN
static/profile-icons/blue.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
static/profile-icons/green.png
Normal file
BIN
static/profile-icons/green.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
static/profile-icons/purple.png
Normal file
BIN
static/profile-icons/purple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
static/profile-icons/red.png
Normal file
BIN
static/profile-icons/red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
7
static/src/constants.js
Normal file
7
static/src/constants.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const ACCOUNT_TYPE_CUSTOMER = 1
|
||||
const ACCOUNT_TYPE_ASSISTANT = 2
|
||||
|
||||
const TICKET_STATUS_OPEN = 1
|
||||
const TICKET_STATUS_CLOSED = 2
|
||||
|
||||
const SYSTEM_USER_ID = 0
|
||||
125
static/src/jwt-decode.js
Normal file
125
static/src/jwt-decode.js
Normal file
@@ -0,0 +1,125 @@
|
||||
// CODE IS FROM https://github.com/auth0/jwt-decode
|
||||
|
||||
(function (factory) {
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
factory();
|
||||
}((function () { 'use strict';
|
||||
|
||||
/**
|
||||
* The code was extracted from:
|
||||
* https://github.com/davidchambers/Base64.js
|
||||
*/
|
||||
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
|
||||
|
||||
function InvalidCharacterError(message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
InvalidCharacterError.prototype = new Error();
|
||||
InvalidCharacterError.prototype.name = "InvalidCharacterError";
|
||||
|
||||
function polyfill(input) {
|
||||
var str = String(input).replace(/=+$/, "");
|
||||
if (str.length % 4 == 1) {
|
||||
throw new InvalidCharacterError(
|
||||
"'atob' failed: The string to be decoded is not correctly encoded."
|
||||
);
|
||||
}
|
||||
for (
|
||||
// initialize result and counters
|
||||
var bc = 0, bs, buffer, idx = 0, output = "";
|
||||
// get next character
|
||||
(buffer = str.charAt(idx++));
|
||||
// character found in table? initialize bit storage and add its ascii value;
|
||||
~buffer &&
|
||||
((bs = bc % 4 ? bs * 64 + buffer : buffer),
|
||||
// and if not first of each 4 characters,
|
||||
// convert the first 8 bits to one ascii character
|
||||
bc++ % 4) ?
|
||||
(output += String.fromCharCode(255 & (bs >> ((-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
|
||||
143
static/src/ticketTools.js
Normal file
143
static/src/ticketTools.js
Normal file
@@ -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"];
|
||||
};
|
||||
|
||||
}
|
||||
130
static/src/userTools.js
Normal file
130
static/src/userTools.js
Normal file
@@ -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();
|
||||
};
|
||||
|
||||
}
|
||||
89
static/style.css
Normal file
89
static/style.css
Normal file
@@ -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%);
|
||||
}
|
||||
}
|
||||
46
templates/base.html
Normal file
46
templates/base.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SupportMe</title>
|
||||
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<link rel="icon" href="/icon.png">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p id="loggedInMessage">...</p>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="/home" style="text-decoration: none; color: black;">
|
||||
<h1>💬 SupportMe 💁</h1>
|
||||
</a>
|
||||
|
||||
<script src="/src/constants.js"></script>
|
||||
<script src="/src/jwt-decode.js"></script>
|
||||
<script src="/src/userTools.js"></script>
|
||||
<script src="/src/ticketTools.js"></script>
|
||||
|
||||
<script>
|
||||
const currentUser = getLoggedInUser();
|
||||
if (currentUser != null) {
|
||||
getProfile(currentUser["user_id"]).then(profile => {
|
||||
document.getElementById("loggedInMessage").innerText = "Logged in: " + profile["username"];
|
||||
});
|
||||
} else {
|
||||
document.getElementById("loggedInMessage").innerText = "You are not logged in.";
|
||||
};
|
||||
</script>
|
||||
|
||||
{% block content %} {% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
91
templates/home.html
Normal file
91
templates/home.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Home</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="/ticket/new"><button id="open_ticket_button">Open a support ticket</button></a>
|
||||
<br>
|
||||
<a href="/profile"><button>Edit profile</button></a>
|
||||
<button onclick=logout()>Log out</button>
|
||||
|
||||
<section>
|
||||
|
||||
<h3>Open tickets</h3>
|
||||
|
||||
<ul id="open_tickets_list"></ul>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="claimable_tickets">
|
||||
|
||||
<h3>Claimable tickets</h3>
|
||||
|
||||
<ul id="unclaimed_tickets_list"></ul>
|
||||
|
||||
</section>
|
||||
|
||||
<section>
|
||||
|
||||
<h3>Closed tickets</h3>
|
||||
|
||||
<ul id="closed_tickets_list"></ul>
|
||||
|
||||
</section>
|
||||
|
||||
<script>
|
||||
if (currentUser == null) window.location.href = "/login";
|
||||
|
||||
const openTicketsList = document.getElementById("open_tickets_list")
|
||||
const closedTicketsList = document.getElementById("closed_tickets_list")
|
||||
const unclaimedTicketsList = document.getElementById("unclaimed_tickets_list")
|
||||
|
||||
getOpenTicketsByUserID(currentUser["user_id"]).then(openTickets => {
|
||||
if (openTickets.length == 0) openTicketsList.innerHTML = "<i>No tickets to show.</i>"
|
||||
for (t of openTickets) {
|
||||
let _a = document.createElement("a");
|
||||
_a.innerText = "#" + t["ticket_id"] + " - " + t["title"];
|
||||
_a.href = "/ticket/" + t["ticket_id"];
|
||||
let _li = document.createElement("li");
|
||||
_li.appendChild(_a);
|
||||
openTicketsList.appendChild(_li);
|
||||
};
|
||||
});
|
||||
|
||||
getClosedTicketsByUserID(currentUser["user_id"]).then(closedTickets => {
|
||||
if (closedTickets.length == 0) closedTicketsList.innerHTML = "<i>No tickets to show.</i>"
|
||||
for (t of closedTickets) {
|
||||
let _a = document.createElement("a");
|
||||
_a.innerText = "#" + t["ticket_id"] + " - " + t["title"];
|
||||
_a.href = "/ticket/" + t["ticket_id"];
|
||||
let _li = document.createElement("li");
|
||||
_li.appendChild(_a);
|
||||
closedTicketsList.appendChild(_li);
|
||||
};
|
||||
});
|
||||
|
||||
if (currentUser["account_type"] == ACCOUNT_TYPE_CUSTOMER) {
|
||||
|
||||
document.getElementById("claimable_tickets").style.display = "none";
|
||||
|
||||
} else if (currentUser["account_type"] == ACCOUNT_TYPE_ASSISTANT) {
|
||||
|
||||
document.getElementById("claimable_tickets").style.display = "block";
|
||||
document.getElementById("open_ticket_button").style.display = "none";
|
||||
|
||||
getUnclaimedTickets().then(unclaimedTickets => {
|
||||
if (unclaimedTickets.length == 0) unclaimedTicketsList.innerHTML = "<i>No tickets to show.</i>"
|
||||
for (t of unclaimedTickets) {
|
||||
_a = document.createElement("a");
|
||||
_a.innerText = "#" + t["ticket_id"] + " - " + t["title"];
|
||||
_a.href = "/ticket/" + t["ticket_id"];
|
||||
_li = document.createElement("li");
|
||||
_li.appendChild(_a);
|
||||
unclaimedTicketsList.appendChild(_li);
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
27
templates/login.html
Normal file
27
templates/login.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Login</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<label for="username">Username</label><br>
|
||||
<input type="text" id="username_input" name="username" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" id="password_input" name="password" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<button type="submit" onclick="login()">Login</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<a href="register">Don't have an account? Click here to sign up!</a>
|
||||
|
||||
<script>
|
||||
if (currentUser != null) window.location.href = "/home";
|
||||
</script>
|
||||
{% endblock %}
|
||||
68
templates/profile.html
Normal file
68
templates/profile.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Profile</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<img id="profile_icon">
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="user_id">Your user ID</label><br>
|
||||
<input type="text" id="user_id_input" name="user_id" disabled />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="username">Your username (cannot be changed)</label><br>
|
||||
<input type="text" id="username_input" name="username" disabled />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="new_password">New password</label><br>
|
||||
<input type="password" id="new_password_input" name="new_password" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="new_passwordc">Confirm new password</label><br>
|
||||
<input type="password" id="new_passwordc_input" name="new_passwordc" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="old_password">Old password</label><br>
|
||||
<input type="password" id="old_password_input" name="old_password" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="profile_icon">Profile picture</label><br>
|
||||
<select name="profile_icon" id="profile_icon_select" oninput="updateProfileIconPreview()">
|
||||
<option value="/profile-icons/blue.png">Blue</option>
|
||||
<option value="/profile-icons/green.png">Green</option>
|
||||
<option value="/profile-icons/purple.png">Purple</option>
|
||||
<option value="/profile-icons/red.png">Red</option>
|
||||
</select>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button type="submit" onclick="updateProfile()">Save changes</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<a href="register">Don't have an account? Click here to sign up!</a>
|
||||
|
||||
<script>
|
||||
if (currentUser == null) window.location.href = "/login";
|
||||
|
||||
getProfile(currentUser["user_id"]).then(profile => {
|
||||
document.getElementById("profile_icon").src = profile["profile_icon"];
|
||||
document.getElementById("profile_icon").width = 150;
|
||||
document.getElementById("user_id_input").value = profile["user_id"];
|
||||
document.getElementById("username_input").value = profile["username"];
|
||||
document.getElementById("profile_icon_select").value = profile["profile_icon"];
|
||||
});
|
||||
|
||||
function updateProfileIconPreview() {
|
||||
document.getElementById("profile_icon").src = document.getElementById("profile_icon_select").value;
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
37
templates/register.html
Normal file
37
templates/register.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Register</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<label for="username">Username</label><br>
|
||||
<input type="text" id="username_input" name="username" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="email">Email</label><br>
|
||||
<input type="email" id="email_input" name="email" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="password">Password</label><br>
|
||||
<input type="password" id="password_input" name="password" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="passwordc">Confirm Password</label><br>
|
||||
<input type="password" id="passwordc_input" name="passwordc" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<button type="submit" onclick="register()">Register</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<a href="login">Already have an account? Click here to log in!</a>
|
||||
|
||||
<script>
|
||||
if (currentUser != null) window.location.href = "/home";
|
||||
</script>
|
||||
{% endblock %}
|
||||
25
templates/ticket/new.html
Normal file
25
templates/ticket/new.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Open a new support ticket</h2>
|
||||
|
||||
<br>
|
||||
|
||||
<label for="ticket_title">Ticket Title</label><br>
|
||||
<input type="text" id="ticket_title_input" name="ticket_title" />
|
||||
|
||||
<br><br>
|
||||
|
||||
<label for="message">Ticket Description</label><br>
|
||||
<textarea type="text" id="message_input" name="message" rows="10"></textarea>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button type="submit" onclick="openTicket()">Open ticket</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<script>
|
||||
if (currentUser == null) window.location.href = "/login";
|
||||
</script>
|
||||
{% endblock %}
|
||||
102
templates/ticket/ticket.html
Normal file
102
templates/ticket/ticket.html
Normal file
@@ -0,0 +1,102 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2 id="ticket_title">...</h2>
|
||||
<p id="ticket_id">Ticket #{{ ticket_id }}</p>
|
||||
|
||||
<p>
|
||||
<span id="opened_at"></span><br>
|
||||
<span id="closed_at"></span>
|
||||
</p>
|
||||
<p>
|
||||
<span id="opened_by">...</span><br>
|
||||
<span id="claimed_by">This ticket has not been claimed</span>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<label for="message">Send a message:</label><br>
|
||||
<textarea type="text" id="message_input" name="message" rows="10"></textarea>
|
||||
|
||||
<br><br>
|
||||
|
||||
<button id="close_ticket_btn" type="submit" onclick="sendMessage('!close')">Close ticket</button>
|
||||
<button id="open_ticket_btn" type="submit" onclick="sendMessage('!open')">Reopen ticket</button>
|
||||
<button id="send_message_btn" type="submit" onclick="sendMessage()">Send</button>
|
||||
<button id="claim_ticket_btn" type="submit" onclick="sendMessage('!claim')">Claim ticket</button>
|
||||
|
||||
<br><br>
|
||||
|
||||
<table id="ticket_messages"></table>
|
||||
|
||||
<script>
|
||||
if (currentUser == null) window.location.href = "/login";
|
||||
|
||||
const profiles = {};
|
||||
|
||||
const ticketID = parseInt("{{ ticket_id }}");
|
||||
|
||||
getTicket(ticketID).then(ticket => {
|
||||
if (ticket != false) {
|
||||
ticket = ticket["ticket_data"];
|
||||
document.getElementById("ticket_title").innerText = "[" + (ticket["closed_at"] == -1 ? "OPEN" : "CLOSED") + "] " + ticket["title"];
|
||||
document.getElementById("opened_at").innerText = "Opened: " + new Date(ticket["opened_at"] * 1000).toLocaleString();
|
||||
if (ticket["closed_at"] != -1) {
|
||||
document.getElementById("closed_at").innerText = "Closed: " + new Date(ticket["closed_at"] * 1000).toLocaleString();
|
||||
document.getElementById("open_ticket_btn").style.display = "inline";
|
||||
} else {
|
||||
document.getElementById("close_ticket_btn").style.display = "inline";
|
||||
};
|
||||
getProfile(ticket["customer_id"]).then(customer => {
|
||||
document.getElementById("opened_by").innerHTML = "Opened by <b>" + customer["username"] + "</b> (ID: " + ticket["customer_id"] + ")";
|
||||
});
|
||||
if (currentUser["account_type"] == ACCOUNT_TYPE_ASSISTANT && ticket["assistant_id"] != currentUser["user_id"]) {
|
||||
document.getElementById("claim_ticket_btn").style.display = "inline";
|
||||
};
|
||||
if (ticket["assistant_id"] != -1) {
|
||||
getProfile(ticket["assistant_id"]).then(assistant => {
|
||||
document.getElementById("claimed_by").innerHTML = "Claimed by <b>" + assistant["username"] + "</b> (ID: " + ticket["assistant_id"] + ")";
|
||||
});
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const ticketMessagesTable = document.getElementById("ticket_messages");
|
||||
|
||||
getMessages(ticketID).then(async messages => {
|
||||
for (a_id of messages.map(m => m["author_id"])) { profiles[a_id] = await getProfile(a_id); }
|
||||
|
||||
for (msg of messages) {
|
||||
|
||||
let _tr = document.createElement("tr");
|
||||
let _td1 = document.createElement("td");
|
||||
let _img = document.createElement("img");
|
||||
_img.src = profiles[msg["author_id"]]["profile_icon"];
|
||||
_img.width = 150;
|
||||
let _td2 = document.createElement("td");
|
||||
let _h3_sender = document.createElement("h3");
|
||||
_h3_sender.classList.add("message_sender");
|
||||
_h3_sender.innerText = profiles[msg["author_id"]]["username"];
|
||||
let _p_body = document.createElement("p");
|
||||
_p_body.classList.add("message_body");
|
||||
_p_body.innerText = msg["body"];
|
||||
let _br1 = document.createElement("br");
|
||||
let _p_sent_at = document.createElement("p");
|
||||
_p_sent_at.classList.add("message_sent_at");
|
||||
_p_sent_at.innerText = new Date(msg["sent_at"] * 1000).toLocaleString();
|
||||
let _br2 = document.createElement("br");
|
||||
_td1.appendChild(_img);
|
||||
_td2.appendChild(_h3_sender);
|
||||
_td2.appendChild(_p_body);
|
||||
_td2.appendChild(_br1);
|
||||
_td2.appendChild(_p_sent_at);
|
||||
_td2.appendChild(_br2);
|
||||
_tr.appendChild(_td1);
|
||||
_tr.appendChild(_td2);
|
||||
ticketMessagesTable.appendChild(_tr);
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
17
tools/changeAccountType.py
Normal file
17
tools/changeAccountType.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user