2024-03-05 16:01:26 +01:00
|
|
|
import bcrypt
|
|
|
|
import re
|
|
|
|
import jwt
|
|
|
|
import datetime
|
|
|
|
from typing import Tuple, Union
|
|
|
|
from mysql.connector import Error
|
|
|
|
from flask_jwt_extended import create_access_token
|
|
|
|
|
2024-03-07 07:52:27 +01:00
|
|
|
from app.extensions import db_cursor, db_connection
|
|
|
|
from app.extensions import jwt_redis_blocklist
|
|
|
|
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
class UserService:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
UserService class provides methods for user-related operations.
|
|
|
|
|
|
|
|
Methods:
|
|
|
|
- register(username: str, email: str, password: str) -> Tuple[Union[dict, str], int]
|
|
|
|
- login(username: str, password: str) -> Tuple[Union[dict, str], int]
|
|
|
|
- logout(jti, exp) -> Tuple[Union[dict, str], int]
|
|
|
|
- update_email(user_id: str, new_email: str) -> Tuple[Union[dict, str], int]
|
|
|
|
- update_username(user_id: str, new_username: str) -> Tuple[Union[dict, str], int]
|
|
|
|
- update_password(user_id: str, new_password: str) -> Tuple[Union[dict, str], int]
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
@staticmethod
|
2024-03-07 07:52:27 +01:00
|
|
|
def register(username: str, email: str, password: str) -> Tuple[Union[dict, str], int]:
|
|
|
|
"""
|
|
|
|
Registers a new user with the provided username, email, and password.
|
|
|
|
|
|
|
|
:param username: User's username.
|
|
|
|
:type username: str
|
|
|
|
:param email: User's email address.
|
|
|
|
:type email: str
|
|
|
|
:param password: User's password.
|
|
|
|
:type password: str
|
|
|
|
:return: Tuple containing a dictionary and an HTTP status code.
|
|
|
|
:rtype: Tuple[Union[dict, str], int]
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
if not UserService.__verify_username(username):
|
|
|
|
return {"Failed": "Failed to verify username. Try another username"}, 400
|
|
|
|
|
|
|
|
if not UserService.__verify_email(email):
|
|
|
|
return {"Failed": "Failed to verify email. Try another email"}, 400
|
|
|
|
|
|
|
|
if not UserService.__verify_password(password):
|
|
|
|
return {"Failed": "Failed to verify password. Try another (stronger) password"}, 400
|
|
|
|
|
|
|
|
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
|
|
|
|
|
|
|
try:
|
2024-03-05 21:35:58 +01:00
|
|
|
db_cursor.execute("select max(user_id) as max_id from user")
|
|
|
|
last_id = db_cursor.fetchone()['max_id']
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
if last_id < 23000:
|
2024-03-05 21:35:58 +01:00
|
|
|
return {"Failed": "Error occurred when fetching last user id"}
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
new_id = last_id + 1
|
|
|
|
|
|
|
|
db_cursor.execute("insert into user (username, email, password, user_id, role_id) values (%s, %s, %s, %s, 1)", (username, email, hashed_password, new_id))
|
|
|
|
db_connection.commit()
|
|
|
|
except Error as e:
|
|
|
|
print(f"Error: {e}")
|
|
|
|
return {"Failed": "Failed to insert into database. Username or email are likely in use already"}, 500
|
|
|
|
|
|
|
|
return {"Success": "User created successfully"}, 200
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def login(username: str, password: str) -> Tuple[Union[dict, str], int]:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Authenticates a user with the provided username and password.
|
|
|
|
|
|
|
|
:param username: User's username.
|
|
|
|
:type username: str
|
|
|
|
:param password: User's password.
|
|
|
|
:type password: str
|
|
|
|
:return: Tuple containing a dictionary with a token and an HTTP status code.
|
|
|
|
:rtype: Tuple[Union[dict, str], int]
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
db_cursor.execute("select user_id, password, last_change from user where username = %s", (username,))
|
|
|
|
result = db_cursor.fetchone()
|
|
|
|
|
2024-03-05 21:35:58 +01:00
|
|
|
user_id = result['user_id']
|
|
|
|
password_hash = result['password']
|
|
|
|
last_change = result['last_change']
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
if user_id is None:
|
|
|
|
return {"Failed": "Username not found"}, 400
|
|
|
|
|
|
|
|
if not bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')):
|
|
|
|
return {"Failed": "Incorrect password"}, 401
|
|
|
|
|
2024-03-07 07:52:27 +01:00
|
|
|
expire = datetime.timedelta(hours=1)
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
token = create_access_token(identity=user_id, expires_delta=expire,additional_claims={"lm": last_change})
|
|
|
|
|
|
|
|
return {"token": token}, 200
|
|
|
|
|
2024-03-07 07:52:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def logout(jti, exp) -> Tuple[Union[dict, str], int]:
|
|
|
|
"""
|
|
|
|
Logs out a user by invalidating the provided JWT.
|
|
|
|
|
|
|
|
:param jti: JWT ID.
|
|
|
|
:type jti: str
|
|
|
|
:param exp: JWT expiration timestamp.
|
|
|
|
:type exp: int
|
|
|
|
:return: Tuple containing a dictionary and an HTTP status code.
|
|
|
|
:rtype: Tuple[Union[dict, str], int]
|
|
|
|
"""
|
|
|
|
UserService.__invalidate_token(jti, exp)
|
|
|
|
|
|
|
|
return {"Success": "Successfully logged out"}, 200
|
|
|
|
|
2024-03-05 16:01:26 +01:00
|
|
|
@staticmethod
|
|
|
|
def update_email(user_id: str, new_email: str) -> Tuple[Union[dict, str], int]:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Updates the email address for a user with the provided user ID.
|
|
|
|
|
|
|
|
:param user_id: User's ID.
|
|
|
|
:type user_id: str
|
|
|
|
:param new_email: New email address.
|
|
|
|
:type new_email: str
|
|
|
|
:return: Tuple containing a dictionary and an HTTP status code.
|
|
|
|
:rtype: Tuple[Union[dict, str], int]
|
|
|
|
"""
|
|
|
|
|
2024-03-05 16:01:26 +01:00
|
|
|
if not UserService.__verify_email(new_email):
|
2024-03-05 21:35:58 +01:00
|
|
|
return {"Failed": "Failed to verify email. Try another email"}, 400
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
db_cursor.execute("update user set email = %s where user_id = %s", (new_email, user_id))
|
|
|
|
db_connection.commit()
|
|
|
|
except Error as e:
|
|
|
|
return {"Failed": f"Failed to update email. Email is likely in use already. Error: {e}"}, 500
|
|
|
|
|
|
|
|
return {"Success": "Email successfully updated"}, 200
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def update_username(user_id: str, new_username: str) -> Tuple[Union[dict, str], int]:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Updates the username for a user with the provided user ID.
|
|
|
|
|
|
|
|
:param user_id: User's ID.
|
|
|
|
:type user_id: str
|
|
|
|
:param new_username: New username.
|
|
|
|
:type new_username: str
|
|
|
|
:return: Tuple containing a dictionary and an HTTP status code.
|
|
|
|
:rtype: Tuple[Union[dict, str], int]
|
|
|
|
"""
|
|
|
|
|
2024-03-05 16:01:26 +01:00
|
|
|
if not UserService.__verify_username(new_username):
|
|
|
|
return {"Failed": "Failed to verify username. Try another one"}, 400
|
|
|
|
|
|
|
|
try:
|
|
|
|
db_cursor.execute("update user set username = %s where user_id = %s", (new_username, user_id))
|
|
|
|
db_connection.commit()
|
|
|
|
except Error as e:
|
|
|
|
return {"Failed": f"Failed to update username. Username is likely in use already. Error: {e}"}, 500
|
|
|
|
|
|
|
|
return {"Success": "Username successfully updated"}, 200
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def update_password(user_id: str, new_password: str) -> Tuple[Union[dict, str], int]:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Updates the password for a user with the provided user ID.
|
|
|
|
|
|
|
|
:param user_id: User's ID.
|
|
|
|
:type user_id: str
|
|
|
|
:param new_password: New password.
|
|
|
|
:type new_password: str
|
|
|
|
:return: Tuple containing a dictionary and an HTTP status code.
|
|
|
|
:rtype: Tuple[Union[dict, str], int]
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
if not UserService.__verify_password(new_password):
|
|
|
|
return {"Failed": "Failed to verify password. Try another (stronger) one"}, 400
|
|
|
|
|
|
|
|
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
|
|
|
|
|
|
|
|
try:
|
|
|
|
db_cursor.execute("update user set password = %s where user_id = %s", (new_username, user_id))
|
|
|
|
db_connection.commit()
|
|
|
|
except Error as e:
|
|
|
|
return {"Failed": f"Failed to update password. Error: {e}"}, 500
|
|
|
|
|
|
|
|
return {"Success": "Password successfully updated"}, 200
|
|
|
|
|
2024-03-07 07:52:27 +01:00
|
|
|
@staticmethod
|
|
|
|
def __invalidate_token(jti: str, exp: int):
|
|
|
|
"""
|
|
|
|
Invalidates a JWT by adding its JTI to the Redis blocklist.
|
|
|
|
|
|
|
|
:param jti: JWT ID.
|
|
|
|
:type jti: str
|
|
|
|
:param exp: JWT expiration timestamp.
|
|
|
|
:type exp: int
|
|
|
|
"""
|
|
|
|
expiration = datetime.datetime.fromtimestamp(exp)
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
|
|
|
|
delta = expiration - now
|
|
|
|
jwt_redis_blocklist.set(jti, "", ex=delta)
|
|
|
|
|
2024-03-05 16:01:26 +01:00
|
|
|
@staticmethod
|
|
|
|
def __verify_email(email: str) -> bool:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Verifies a given email string against a regular expression.
|
|
|
|
|
|
|
|
:param email: Email string.
|
|
|
|
:type email: str
|
|
|
|
:return: Boolean indicating whether the email successfully passed the check.
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
2024-03-07 07:52:27 +01:00
|
|
|
return re.match(email_regex, email) and len(email) <= 64
|
2024-03-05 16:01:26 +01:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def __verify_username(username: str) -> bool:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Verifies a given username string against a regular expression.
|
|
|
|
|
|
|
|
:param username: Username string.
|
|
|
|
:type username: str
|
|
|
|
:return: Boolean indicating whether the username successfully passed the check.
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
username_regex = r"^[a-zA-Z.-_]{1,64}$"
|
|
|
|
return re.match(username_regex, username)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def __verify_password(password: str) -> bool:
|
2024-03-07 07:52:27 +01:00
|
|
|
"""
|
|
|
|
Verifies a given password string against a regular expression.
|
|
|
|
|
|
|
|
:param password: Password string.
|
|
|
|
:type password: str
|
|
|
|
:return: Boolean indicating whether the password successfully passed the check.
|
|
|
|
:rtype: bool
|
|
|
|
"""
|
2024-03-05 16:01:26 +01:00
|
|
|
password_regex = r"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$"
|
|
|
|
return re.match(password_regex, password)
|