diff --git a/app/__init__.py b/app/__init__.py index 0285909..5ab3565 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -9,7 +9,7 @@ app = Flask(__name__) from app.config import FlaskTesting, FlaskProduction app.config.from_object(FlaskTesting) -mail = Mail(app) +flask_mail = Mail(app) jwt_manager = JWTManager(app) swag = Swagger(app, template=main_swagger) diff --git a/app/api/routes/error_routes.py b/app/api/routes/error_routes.py index 2088351..e2b442d 100644 --- a/app/api/routes/error_routes.py +++ b/app/api/routes/error_routes.py @@ -10,7 +10,7 @@ def unauthorized(e): @bp_errors.app_errorhandler(403) def forbidden(e): - return {"Forbidden": "Forbidden from accessing this resource. Try logging in"}, 403 + return {"Forbidden": "You shall not pass"}, 403 @bp_errors.app_errorhandler(404) def not_found(e): diff --git a/app/api/routes/user_routes.py b/app/api/routes/user_routes.py index 1212583..af49961 100644 --- a/app/api/routes/user_routes.py +++ b/app/api/routes/user_routes.py @@ -43,7 +43,10 @@ def logout(): jti = jwt['jti'] exp = jwt['exp'] - result, status_code = UserService.logout(jti, exp) + + user_id = get_jwt_identity() + + result, status_code = UserService.logout(jti, exp, user_id) return result, status_code @@ -62,7 +65,7 @@ def update_username(): jti = jwt['jti'] exp = jwt['exp'] - UserService.logout(jti, exp) + UserService.logout(jti, exp, user_id) return result, status_code @@ -81,7 +84,7 @@ def update_displayname(): jti = jwt['jti'] exp = jwt['exp'] - UserService.logout(jti, exp) + UserService.logout(jti, exp, user_id) return result, status_code @@ -100,7 +103,7 @@ def update_email(): jti = jwt['jti'] exp = jwt['exp'] - UserService.logout(jti, exp) + UserService.logout(jti, exp, username) return result, status_code @@ -120,7 +123,7 @@ def update_password(): jti = jwt['jti'] exp = jwt['exp'] - UserService.logout(jti, exp) + UserService.logout(jti, exp, username) return result, status_code @@ -136,6 +139,6 @@ def delete_user(): jti = jwt['jti'] exp = jwt['exp'] - UserService.logout(jti, exp) + UserService.logout(jti, exp, user_id) return result, status_code \ No newline at end of file diff --git a/app/mail/mail.py b/app/mail/mail.py new file mode 100644 index 0000000..487f948 --- /dev/null +++ b/app/mail/mail.py @@ -0,0 +1,20 @@ +from flask_mail import Message + +from app import flask_mail + +from app.mail.messages import messages + + +def send_mail(message: str, recipient: str): + + body = messages[message]["body"] + subject = messages[message]["subject"] + + msg = Message(subject, recipients=[recipient], body=body) + + try: + flask_mail.send(msg) + return True + except Exception as e: + print(f"Failed to send email. Error: {e}") + return False \ No newline at end of file diff --git a/app/mail/messages.py b/app/mail/messages.py new file mode 100644 index 0000000..895c810 --- /dev/null +++ b/app/mail/messages.py @@ -0,0 +1,24 @@ +import datetime + +messages = { + "register": { + "subject": "Successfully registered!", + "body": "Congratulations! Your account has been successfully created.\nThis mail also serves as a test that the email address is correct" + }, + "login": { + "subject": "New Login detected!", + "body": "A new login token has been created" + }, + "logout": { + "subject": "Successfully logged out", + "body": "A login has been revoked. No further action is needed." + }, + "update": { + "subject": "Account updated", + "body": "Your account has been successfully updated. This also means you have been logged out of everywhere" + }, + "delete": { + "subject": "Account Deleted!", + "body": "Your account has been deleted. No further action needed" + } +} \ No newline at end of file diff --git a/app/mail_utils.py b/app/mail_utils.py deleted file mode 100644 index 768ea98..0000000 --- a/app/mail_utils.py +++ /dev/null @@ -1,15 +0,0 @@ -from flask_mail import Message - -from app import mail - - -def send_mail(subject: str, recipient: str, body: str): - msg = Message(subject, recipients=[recipient]) - msg.body = body - - try: - mail.send(msg) - return True - except Exception as e: - print(f"Failed to send email. Error: {e}") - return False \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py index 2e7aec1..0e1bafd 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -9,7 +9,7 @@ from flask_jwt_extended import create_access_token from app.extensions import db_connection from app.extensions import jwt_redis_blocklist -from app.mail_utils import send_mail +from app.mail.mail import send_mail class UserService: @@ -62,7 +62,7 @@ class UserService: print(f"Error: {e}") return {"Failed": "Failed to insert into database. Username or email are likely in use already"}, 500 - send_mail("Successfully registered!", email, "Congratulations! Your account has been successfully created.\nThis mail also serves as a test that the email address is correct") + send_mail("register", email) return {"Success": "User created successfully"}, 200 @@ -81,10 +81,11 @@ class UserService: try: with db_connection.cursor(dictionary=True) as cursor: - cursor.execute("select id, password from user where username = %s", (username,)) + cursor.execute("select id, email, password from user where username = %s", (username,)) result = cursor.fetchone() user_id = result['id'] + email = result['email'] password_hash = result['password'] if user_id is None: @@ -97,13 +98,15 @@ class UserService: token = create_access_token(identity=user_id, expires_delta=expire) + send_mail("login", email) + return {"token": token}, 200 except Error as e: return {"Failed": f"Failed to login. Error: {e}"}, 500 @staticmethod - def logout(jti, exp) -> Tuple[Union[dict, str], int]: + def logout(jti, exp, user_id) -> Tuple[Union[dict, str], int]: """ Logs out a user by invalidating the provided JWT. @@ -114,7 +117,9 @@ class UserService: :return: Tuple containing a dictionary and an HTTP status code. :rtype: Tuple[Union[dict, str], int] """ + UserService.__invalidate_token(jti, exp) + UserService.__send_email("logout", id=user_id) return {"Success": "Successfully logged out"}, 200 @@ -128,6 +133,8 @@ class UserService: :return: Tuple containing a dictionary and an HTTP status code. :rtype: Tuple[Union[dict, str], int] """ + + UserService.__send_email("delete", id=user_id) try: with db_connection.cursor() as cursor: @@ -215,6 +222,38 @@ class UserService: return {"Success": "Password successfully updated"}, 200 + @staticmethod + def __send_email(message: str, username: str = None, id: str = None, email: str = None): + if email is not None: + send_mail(message, email) + return + + if username is not None: + try: + with db_connection.cursor(dictionary=True) as cursor: + cursor.execute("select email from user where username = %s", (username,)) + result = cursor.fetchone() + email = result['email'] + send_mail("logout", email) + + except Error as e: + return {"Failed": f"Failed to fetch some data. Error: {e}"}, 500 + return + + if id is not None: + try: + with db_connection.cursor(dictionary=True) as cursor: + cursor.execute("select email from user where id = %s", (id,)) + result = cursor.fetchone() + email = result['email'] + send_mail("logout", email) + + except Error as e: + return {"Failed": f"Failed to fetch some data. Error: {e}"}, 500 + return + + raise ValueError("Invalid input data to send mail") + @staticmethod def __invalidate_token(jti: str, exp: int): """