From ee3509957146d9ee7dc62db1bad37885c5430268 Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Sat, 14 Dec 2024 16:41:50 +0800 Subject: [PATCH 1/8] fix: update .gitignore to ignore log files and directories --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8fabcb3..cc0f38f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,8 @@ # Ignore python cache file .vscode/ __pycache__/ -*.py[cod] \ No newline at end of file +*.py[cod] + +# Ignore logs +/logs/ +*.log \ No newline at end of file From aa91859017624b2d98d4d412ea2153bc5d226f9c Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:14:07 +0800 Subject: [PATCH 2/8] chore: add the flask logging for recording the events --- src/__init__.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/__init__.py b/src/__init__.py index 1182a26..02ba529 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,16 +1,14 @@ # src/__init__.py -from flask import Flask, request, Response +from flask import Flask +from flask_caching import Cache +from flask_cors import CORS from flask_mail import Mail -from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate -from datetime import timedelta -from flask_caching import Cache -from redis import Redis +from flask_sqlalchemy import SQLAlchemy +from flasgger import Swagger from .config import Config import os -from flask_cors import CORS -from flasgger import Swagger BASEDIR = os.path.abspath(os.path.dirname(__file__)) API_PREFIX = '/api/v1' @@ -31,6 +29,8 @@ def create_app(config_class=Config): register_blueprints(app, API_PREFIX) + register_log(app) + with app.app_context(): db.create_all() @@ -54,3 +54,29 @@ def register_blueprints(app, prefix): app.register_blueprint(auth, url_prefix=f'{prefix}/auth/') app.register_blueprint(news, url_prefix=f'{prefix}/news/') app.register_blueprint(users, url_prefix=f'{prefix}/users/') + +def register_log(app: Flask): + import logging + from logging.handlers import RotatingFileHandler + from logging import Formatter + + log_formatter = Formatter( + '%(asctime)s %(levelname)s: %(message)s ' + '[in %(pathname)s:%(lineno)d]' + ) + + if not os.path.exists('logs'): + os.mkdir('logs') + print('Logs directory created') + + log_handler = RotatingFileHandler( + 'logs/app.log', 'w', maxBytes=1024*1024, backupCount=10 + ) + + log_handler.setLevel(logging.INFO) + log_handler.setFormatter(log_formatter) + + app.logger.addHandler(log_handler) + + app.logger.setLevel(logging.INFO) + app.logger.info('App startup') \ No newline at end of file From 9358ca192bfd194426d640e0dbbb8e79fa90447b Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:15:25 +0800 Subject: [PATCH 3/8] refactor: remove outdated API documentation and add new OTP and password reset specifications --- src/apidocs/auth/forgotPassword/getOtp.yml | 66 ++++++ .../auth/forgotPassword/resetPassword.yml | 69 ++++++ .../sendOtp.yml} | 28 +-- src/apidocs/auth/passwordReset.yml | 44 ---- src/apidocs/auth/postEmailReturnUserData.yml | 81 ------- src/apidocs/auth/postUserIdReturnUserData.yml | 81 ------- src/apidocs/auth/verifyOTP.yml | 12 -- src/auth/routes.py | 199 +++++++++++------- 8 files changed, 274 insertions(+), 306 deletions(-) create mode 100644 src/apidocs/auth/forgotPassword/getOtp.yml create mode 100644 src/apidocs/auth/forgotPassword/resetPassword.yml rename src/apidocs/auth/{forgetPassword.yml => forgotPassword/sendOtp.yml} (50%) delete mode 100644 src/apidocs/auth/passwordReset.yml delete mode 100644 src/apidocs/auth/postEmailReturnUserData.yml delete mode 100644 src/apidocs/auth/postUserIdReturnUserData.yml delete mode 100644 src/apidocs/auth/verifyOTP.yml diff --git a/src/apidocs/auth/forgotPassword/getOtp.yml b/src/apidocs/auth/forgotPassword/getOtp.yml new file mode 100644 index 0000000..5a15adb --- /dev/null +++ b/src/apidocs/auth/forgotPassword/getOtp.yml @@ -0,0 +1,66 @@ +This is yml file for verifyOTP API document. +In this example the specification is taken from external YAML file +--- +tags: + - Auth +produces: application/json, +parameters: + - name: body + in: body + description: "Get OTP requirements" + schema: + type: object + properties: + userId: + type: string + required: true + description: The user id + example: 1 +responses: + 200: + description: return OTP code from server + schema: + type: string + properties: + otp: + type: string + example: "1234" + message: + type: string + example: "OTP get successfully" + status: + type: string + example: "success" + 400: + description: User id is required + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: UserId is required + 404: + description: User does not exist + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: OTP not found in cache or has expired + 500: + description: Database error occurred + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: Failed to get OTP \ No newline at end of file diff --git a/src/apidocs/auth/forgotPassword/resetPassword.yml b/src/apidocs/auth/forgotPassword/resetPassword.yml new file mode 100644 index 0000000..0eaaa39 --- /dev/null +++ b/src/apidocs/auth/forgotPassword/resetPassword.yml @@ -0,0 +1,69 @@ +Reset Password +Requirements: +email, newPassword +--- +tags: + - Auth +produces: application/json, +parameters: + - name: body + in: body + description: "Password reset requirements" + schema: + type: object + properties: + email: + type: string + required: true + description: The user email + example: "411031111@mail.nknu.edu.tw" + newPassword: + type: string + required: true + description: The user new password + example: "411031111" +responses: + 200: + description: The user new password reset successfully. + schema: + type: object + properties: + status: + type: string + example: "success" + message: + type: string + example: "Password reset successfully" + 400: + description: Email and newPassword is required + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: Email and newPassword is required + 404: + description: User does not exist + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: User does not exist + 500: + description: Database error occurred + schema: + type: object + properties: + status: + type: string + example: "error" + message: + type: string + example: Database error occurred \ No newline at end of file diff --git a/src/apidocs/auth/forgetPassword.yml b/src/apidocs/auth/forgotPassword/sendOtp.yml similarity index 50% rename from src/apidocs/auth/forgetPassword.yml rename to src/apidocs/auth/forgotPassword/sendOtp.yml index 343ffe0..beb1101 100644 --- a/src/apidocs/auth/forgetPassword.yml +++ b/src/apidocs/auth/forgotPassword/sendOtp.yml @@ -1,13 +1,13 @@ -This is yml file for forgetPassword API document. -In this example the specification is taken from external YAML file +Send OTP mail to user email +Post user email and send OTP mail to user email --- tags: - - User API + - Auth produces: application/json, parameters: - name: body in: body - description: Forget password requirements + description: Send OTP to user email requirements schema: type: object properties: @@ -18,26 +18,26 @@ parameters: example: "411031111@mail.nknu.edu.tw" responses: 200: - description: OTP-mail send success + description: OTP email send success schema: type: object properties: - Message: + message: type: string - example: OTP-mail send success + example: OTP email send success 400: - description: OTP-mail send failed + description: OTP email send failed schema: type: object properties: - Message: + message: type: string - example: OTP-mail send failed - 404: - description: The user email is incorrect, no user found in database + example: Email is required + 503: + description: Failed to send OTP email schema: type: object properties: - Message: + message: type: string - example: No user found \ No newline at end of file + example: Failed to send OTP email \ No newline at end of file diff --git a/src/apidocs/auth/passwordReset.yml b/src/apidocs/auth/passwordReset.yml deleted file mode 100644 index decfa7a..0000000 --- a/src/apidocs/auth/passwordReset.yml +++ /dev/null @@ -1,44 +0,0 @@ -This is yml file for passwordReset API document. -In this example the specification is taken from external YAML file ---- -tags: - - User API -produces: application/json, -parameters: - - name: body - in: body - description: "Password reset requirements. \n - Caution: You have to test 'forgetPassword' api before you want to test this api!" - schema: - type: object - properties: - newPassword: - type: string - required: true - description: The user new password - example: "411031111" -responses: - 200: - description: The user new password reset successfully. - schema: - type: object - properties: - Message: - type: string - example: Success - 400: - description: The user not found. - schema: - type: object - properties: - Message: - type: string - example: Failed - 408: - description: The user email not found in cache. The main reason is time out. - schema: - type: object - properties: - Message: - type: string - example: timeout, please retry. \ No newline at end of file diff --git a/src/apidocs/auth/postEmailReturnUserData.yml b/src/apidocs/auth/postEmailReturnUserData.yml deleted file mode 100644 index cc212b6..0000000 --- a/src/apidocs/auth/postEmailReturnUserData.yml +++ /dev/null @@ -1,81 +0,0 @@ -This is yml file for postUserIdReturnUserData API document. -In this example the specification is taken from external YAML file ---- -tags: - - User API -produces: application/json, -parameters: - - name: body - in: body - description: Input user **email** and output the user's information without password - schema: - type: object - properties: - email: - type: string - required: true - description: The user email - example: "411031111@mail.nknu.edu.tw" -responses: - 200: - description: "The user found in database by userId.\n - It will return json includes\n - * userId\n - * username\n - * gender\n - * email\n - * registered_on\n - * is_confirmed\n - * confirmed_on\n - * online\n - * last_login\n - * last_logout\n" - schema: - type: object - properties: - userId: - type: integer - example: 1 - username: - type: string - example: George - gender: - type: string - example: male - email: - type: string - example: "411031111@mail.nknu.edu.tw" - registered_on: - type: datetime - example: 2024-10-26 16:30:55 - is_confirmed: - type: boolean - example: true - confirmed_on: - type: datetime - example: 2024-10-27 16:30:55 - online: - type: boolean - example: false - last_login: - type: datetime - example: 2024-10-27 16:35:55 - last_logout: - type: datetime - example: 2024-10-27 20:35:55 - 400: - description: No user email get. - schema: - type: object - properties: - Message: - type: string - example: No email get - 404: - description: The user not exit in database. - schema: - type: object - properties: - Message: - type: string - example: User not exit \ No newline at end of file diff --git a/src/apidocs/auth/postUserIdReturnUserData.yml b/src/apidocs/auth/postUserIdReturnUserData.yml deleted file mode 100644 index e2a12d5..0000000 --- a/src/apidocs/auth/postUserIdReturnUserData.yml +++ /dev/null @@ -1,81 +0,0 @@ -This is yml file for postUserIdReturnUserData API document. -In this example the specification is taken from external YAML file ---- -tags: - - User API -produces: application/json, -parameters: - - name: body - in: body - description: Input **userId** and output the user's information without password - schema: - type: object - properties: - userId: - type: integer - required: true - description: The userId - example: 1 -responses: - 200: - description: "The user found in database by userId.\n - It will return json includes\n - * userId\n - * username\n - * gender\n - * email\n - * registered_on\n - * is_confirmed\n - * confirmed_on\n - * online\n - * last_login\n - * last_logout\n" - schema: - type: object - properties: - userId: - type: integer - example: 1 - username: - type: string - example: George - gender: - type: string - example: male - email: - type: string - example: "411031111@mail.nknu.edu.tw" - registered_on: - type: datetime - example: 2024-10-26 16:30:55 - is_confirmed: - type: boolean - example: true - confirmed_on: - type: datetime - example: 2024-10-27 16:30:55 - online: - type: boolean - example: false - last_login: - type: datetime - example: 2024-10-27 16:35:55 - last_logout: - type: datetime - example: 2024-10-27 20:35:55 - 400: - description: No userId get. - schema: - type: object - properties: - Message: - type: string - example: No userId get - 404: - description: The user not exit in database. - schema: - type: object - properties: - Message: - type: string - example: User not exit \ No newline at end of file diff --git a/src/apidocs/auth/verifyOTP.yml b/src/apidocs/auth/verifyOTP.yml deleted file mode 100644 index 23736bf..0000000 --- a/src/apidocs/auth/verifyOTP.yml +++ /dev/null @@ -1,12 +0,0 @@ -This is yml file for verifyOTP API document. -In this example the specification is taken from external YAML file ---- -tags: - - User API -produces: application/json, -responses: - 200: - description: return OTP code from server - schema: - type: string - example: 1234 \ No newline at end of file diff --git a/src/auth/routes.py b/src/auth/routes.py index 4896c0f..1aaa04f 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -6,7 +6,7 @@ forgetPassword, verifyOTP, passwordReset. ''' -from flask import json, render_template, request, redirect, url_for, flash, session, make_response, jsonify +from flask import json, render_template, request, redirect, url_for, flash, session, make_response, jsonify, current_app from sqlalchemy.exc import SQLAlchemyError from src import db, cache @@ -26,7 +26,7 @@ @swag_from('../apidocs/auth/login.yml', methods=['POST']) def login(): """ - URI: /auth/login + URI: api/v1/auth/login Method: POST Description: User login Type: application/json @@ -42,19 +42,19 @@ def login(): "email": string, "gender": string, "userId": integer, - "Message": "Login success" + "message": "Login success" } 401: { - "Message": "Login Failed, Please Check." + "message": "Login Failed, Please Check." } 403: { - "Message": "Account not confirmed. Please check your email." + "message": "Account not confirmed. Please check your email." } 404: { - "Message": "User not exists. Please sign up first." + "message": "User not exists. Please sign up first." } """ if request.method == 'POST': @@ -88,29 +88,29 @@ def login(): 'email': user.email, 'gender': user.gender, 'userId': userId, - 'Message': 'Login success' + 'message': 'Login success' }), 200 # 用戶輸入密碼錯誤 else: flash('Login Failed, Please Check.', 'danger') print('Login Failed, Please Check.') - return jsonify({'Message': 'Login Failed, Please Check.'}), 401 + return jsonify({'message': 'Login Failed, Please Check.'}), 401 # 用戶未認證 else: flash('Account not confirmed. Please check your email.') print('Account not confirmed. Please check your email.') - return jsonify({'Message': 'Account not confirmed. Please check your email.'}), 403 + return jsonify({'message': 'Account not confirmed. Please check your email.'}), 403 # 用戶不存在 else: flash('User not exists. Please sign up first.') print('User not exists. Please sign up first.') - return jsonify({'Message': 'User not exists. Please sign up first.'}), 404 + return jsonify({'message': 'User not exists. Please sign up first.'}), 404 @auth.route('/logout', methods=['GET', 'POST']) @swag_from('../apidocs/auth/logout.yml', methods=['POST']) def logout(): """ - URI: /auth/logout + URI: api/v1/auth/logout Method: POST Description: User logout Type: application/json @@ -121,11 +121,11 @@ def logout(): Response: 200: { - "Message": "User {userId} log out" + "message": "User {userId} log out" } 404: { - "Message": "User {userId} not found" + "message": "User {userId} not found" } """ data = request.json @@ -135,14 +135,14 @@ def logout(): if user_logout is None: print(f'User {user_id} not found') - return jsonify({'Message': f'User {user_id} not found'}), 404 + return jsonify({'message': f'User {user_id} not found'}), 404 else: user_logout.online = False user_logout.last_logout = datetime.now() db.session.flush() db.session.commit() print(f'User {user_id} log out') - return jsonify({'Message': f'User {user_id} log out'}), 200 + return jsonify({'message': f'User {user_id} log out'}), 200 # 註冊函式,註冊完成將會發送驗證信至註冊信箱中 @@ -150,7 +150,7 @@ def logout(): @swag_from('../apidocs/auth/register.yml', methods=['POST']) def register(): """ - URI: /auth/register + URI: api/v1/auth/register Method: POST Description: User register Type: application/json @@ -164,11 +164,11 @@ def register(): Response: 200: { - "Message": "Register Success" + "message": "Register Success" } 404: { - "Message": "username already exists" + "message": "username already exists" } """ if request.method == 'POST': @@ -213,10 +213,10 @@ def register(): send_email(email, subject, html) print('Confirm email has been sent!') - return jsonify({'Message': 'Register Success'}), 200 + return jsonify({'message': 'Register Success'}), 200 else: - return jsonify({'Message': 'username already exists'}), 404 + return jsonify({'message': 'username already exists'}), 404 # 後臺管理系統table展示頁面 @auth.route('/manage', methods=['GET']) @@ -238,7 +238,7 @@ def confirm_email(token): @swag_from('../apidocs/auth/resend_confirmMail.yml', methods=['POST']) def resend_confirmMail(): """ - URI: /auth/resend_confirmMail + URI: api/v1/auth/resend_confirmMail Method: POST Description: Resend confirm email Type: application/json @@ -249,11 +249,11 @@ def resend_confirmMail(): Response: 200: { - "Message": "Confirm email has been resent!" + "message": "Confirm email has been resent!" } 400: { - "Message": "Account already confirmed." + "message": "Account already confirmed." } """ if request.method == 'POST': @@ -263,7 +263,7 @@ def resend_confirmMail(): if user.is_confirmed: print('Account already confirmed.') - return jsonify({'Message': 'Account already confirmed.'}), 400 + return jsonify({'message': 'Account already confirmed.'}), 400 file = 'https://sayanythingapi.sdpmlab.org/static/images/logo_all.png' subject = "Verify your account" @@ -274,15 +274,16 @@ def resend_confirmMail(): send_email(email, subject, html) print('Confirm email has been resent!') - return jsonify({'Message': 'Confirm email has been resent!'}), 200 + return jsonify({'message': 'Confirm email has been resent!'}), 200 # =================================================================== # /forgotPassword/ -@auth.route('/forgotPassword/sendOtp/', methods=['POST']) +@auth.route('/forgotPassword/sendOtp', methods=['POST']) +@swag_from('../apidocs/auth/forgotPassword/sendOtp.yml', methods=['POST']) def sendOtp(): """ - URI: /auth/forgotPassword/sendOtp + URI: api/v1/auth/forgotPassword/sendOtp Method: POST Description: Send OTP email Type: application/json @@ -293,23 +294,29 @@ def sendOtp(): Response: 200: { - "Message": "OTP email send successfully" + "status": "success", + "message": "OTP email send successfully" } 400: { - "Message": "Email is required" + "status": "error", + "message": "Email is required" } 503: { - "Message": "Failed to send OTP email" + "status": "error", + "message": "Failed to send OTP email" } """ if request.method == 'POST': data = request.json email = data['email'] if not email: - print({'Message': 'Email is required'}) - return jsonify({'Message': 'Email is required'}), 400 + current_app.logger.error('Email is required') + return jsonify({ + 'status': 'error', + 'message': 'Email is required' + }), 400 OTP.user_email = email user = Users.query.filter_by(email=email).first() @@ -324,16 +331,21 @@ def sendOtp(): subject = 'Your OTP codes here' send_email(email, subject, html) return jsonify({ - 'Message': 'OTP email send successfully' + 'status': 'success', + 'message': 'OTP email send successfully' }), 200 except Exception as e: print(str(e)) - return jsonify({'Message': 'Failed to send OTP email'}), 503 + return jsonify({ + 'status': 'error', + 'message': 'Failed to send OTP email' + }), 503 -@auth.route('/forgotPassword/getOtp/', methods=['POST']) +@auth.route('/forgotPassword/getOtp', methods=['POST']) +@swag_from('../apidocs/auth/forgotPassword/getOtp.yml', methods=['POST']) def getOtp(): """ - URI: /auth/forgotPassword/getOtp + URI: api/v1/auth/forgotPassword/getOtp Method: POST Description: Get OTP from cache Type: application/json @@ -344,40 +356,59 @@ def getOtp(): Response: 200: { + "status": "success", "otp": integer, - "Message": "OTP found" + "message": "OTP get successfully", } 400: { - "Message": "User ID is required" + "status": "error", + "message": "User ID is required" } 404: { - "Message": "OTP not found in cache or has expired" + "status": "error", + "message": "OTP not found in cache or has expired" + } + 500: + { + "status": "error", + "message": "Failed to verify OTP" } """ data = request.json userId = data['userId'] if not userId: - return jsonify({'Message': 'User ID is required'}), 400 + return jsonify({ + 'status': 'error', + 'message': 'UserId is required' + }), 400 try: otp = cache.get(f'otp_{userId}') if otp: return jsonify({ + 'status': 'success', 'otp': otp, - 'Message': 'Verify otp success' + 'message': 'OTP get successfully' }), 200 else: - return jsonify({'Message': 'OTP not found in cache or has expired'}), 404 + return jsonify({ + 'status': 'error', + 'message': 'OTP not found in cache or has expired' + }), 404 except Exception as e: print(str(e)) - return jsonify({'Message': 'Failed to verify OTP'}), 500 + return jsonify({ + 'status': 'error', + 'message': 'Failed to get OTP' + }), 500 @auth.route('/forgotPassword/resetPassword', methods=['PUT']) +@swag_from('../apidocs/auth/forgotPassword/resetPassword.yml', methods=['PUT']) def resetPassword(): """ - URI: /auth/forgotPassword/resetPassword + URI: api/v1/auth/forgotPassword/resetPassword Method: PUT Description: Reset password Type: application/json @@ -389,48 +420,68 @@ def resetPassword(): Response: 200: { - "Message": "Password reset successfully" + "status": "success", + "message": "Password reset successfully" } 400: { - "Message": "Email and newPassword is required" + "status": "error", + "message": "Email and newPassword is required" } 404: { - "Message": "User does not exit" + "status": "error", + "message": "User does not exit" } 500: { - "Message": "Database error occurred" + "status": "error", + "message": "Database error occurred" } """ data = request.json email = data['email'] new_password = data['newPassword'] if not email or not new_password: - print({'Message': 'Email and newPassword is required'}) - return jsonify({'Message': 'Email and newPassword is required'}), 400 + print({'message': 'Email and newPassword is required'}) + return jsonify({ + 'status': 'error', + 'message': 'Email and newPassword is required' + }), 400 user = Users.query.filter_by(email=email).first() if not user: - return jsonify({'Message': 'User does not exit'}), 404 + return jsonify({ + 'status': 'error', + 'message': 'User does not exit' + }), 404 try: user.password = Users.set_password(user, new_password) db.session.flush() db.session.commit() - return jsonify({'Message': 'Password reset successfully'}), 200 + current_app.logger.info(f"Password reset successfully for user {user.email}") + return jsonify({ + 'status': 'success', + 'message': 'Password reset successfully' + }), 200 except SQLAlchemyError as e: db.session.rollback() print(str(e)) - return jsonify({'Message': 'Database error occurred'}), 500 + current_app.logger.error(f"Database error: {str(e)}") + return jsonify({ + 'status': 'error', + 'message': 'Database error occurred' + }), 500 + +# =================================================================== # 獲取線上活躍人數之統計 @auth.route('/onlineUserCount', methods=['GET']) @swag_from('../apidocs/auth/onlineUserCount.yml', methods=['GET']) def onlineUserCount(): """ - URI: /auth/onlineUserCount + URI: api/v1/auth/onlineUserCount Method: GET Description: Get online user count Type: application/json @@ -457,7 +508,7 @@ def onlineUserCount(): @swag_from('../apidocs/auth/userOnlineStatus.yml', methods=['POST']) def userOnlineStatus(): """ - URI: /auth/userOnlineStatus + URI: api/v1/auth/userOnlineStatus Method: POST Description: Get user online status Type: application/json @@ -472,7 +523,7 @@ def userOnlineStatus(): } 404: { - "Message": "User not found" + "message": "User not found" } """ if request.method == 'POST': @@ -484,14 +535,14 @@ def userOnlineStatus(): status = user.online return jsonify({'userStatus': status}), 200 else: - return jsonify({'Message': 'User not found'}), 404 + return jsonify({'message': 'User not found'}), 404 # 獲取使用者驗證之狀態 @auth.route('/userConfirmStatus', methods=['POST']) @swag_from('../apidocs/auth/userConfirmStatus.yml', methods=['POST']) def userConfirmStatus(): """ - URI: /auth/userConfirmStatus + URI: api/v1/auth/userConfirmStatus Method: POST Description: Get user confirm status Type: application/json @@ -507,7 +558,7 @@ def userConfirmStatus(): } 404: { - "Message": "User not found" + "message": "User not found" } """ data = request.json @@ -524,14 +575,14 @@ def userConfirmStatus(): 'confirmStatus': confirm_status }), 200 else: - return jsonify({'Message': 'User not found'}), 404 + return jsonify({'message': 'User not found'}), 404 # 更新使用者個人檔案 @auth.route('/updateProfile', methods=['PUT']) @swag_from('../apidocs/auth/updateProfile.yml', methods=['PUT']) def updateProfile(): """ - URI: /auth/updateProfile + URI: api/v1/auth/updateProfile Method: PUT Description: Update user profile Type: application/json @@ -543,20 +594,20 @@ def updateProfile(): Response: 200: { - "Message": "Username updated", + "message": "Username updated", "newUsername": string } 400: { - "Message": "No userId get" + "message": "No userId get" } 404: { - "Message": "User not found" + "message": "User not found" } 500: { - "Message": "Database error" + "message": "Database error" } """ data = request.json @@ -566,12 +617,12 @@ def updateProfile(): if not userId: print('No userId get') - return jsonify({'Message': 'No userId get'}), 400 + return jsonify({'message': 'No userId get'}), 400 user = Users.query.filter_by(id=userId).first() if user is None: print('No user found') - return jsonify({'Message': 'User not found'}), 404 + return jsonify({'message': 'User not found'}), 404 try: user.username = new_username @@ -579,14 +630,14 @@ def updateProfile(): db.session.commit() print('Username updated') return jsonify({ - 'Message': 'Username updated', + 'message': 'Username updated', 'newUsername': new_username }), 200 except SQLAlchemyError as e: db.session.rollback() print(str(e)) - return jsonify({'Message': 'Database error'}), 500 + return jsonify({'message': 'Database error'}), 500 @auth.route('/deleteChatlist', methods=['DELETE']) @swag_from('../apidocs/auth/deleteChatlist.yml', methods=['DELETE']) @@ -599,18 +650,18 @@ def deleteChatlist(): delete_chatlist = ChatList.query.filter_by(user_id=user_id, match_id=delete_match_id).first() if delete_chatlist is None: - print({'Message': 'No user found', 'status_code': 404}) - return jsonify({'Message': 'Delete matchId not found'}), 404 + print({'message': 'No user found', 'status_code': 404}) + return jsonify({'message': 'Delete matchId not found'}), 404 try: db.session.delete(delete_chatlist) db.session.flush() db.session.commit() - print({'Message': 'Delete successfully', 'status_code': 200}) - return jsonify({'Message': 'Delete successfully'}), 200 + print({'message': 'Delete successfully', 'status_code': 200}) + return jsonify({'message': 'Delete successfully'}), 200 except SQLAlchemyError as e: db.session.rollback() print(str(e)) - return jsonify({'Message': 'Database error'}), 500 + return jsonify({'message': 'Database error'}), 500 \ No newline at end of file From 9d7859945984f2a3963f50410049ed994f0f833f Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Sat, 14 Dec 2024 19:18:55 +0800 Subject: [PATCH 4/8] fix: enhance logging for OTP and password reset processes --- src/auth/routes.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/auth/routes.py b/src/auth/routes.py index 1aaa04f..044c76d 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -330,12 +330,13 @@ def sendOtp(): html = render_template('email_for_OTP.html', OTP_codes=otp) subject = 'Your OTP codes here' send_email(email, subject, html) + current_app.logger.info(f'OTP email send successfully to {email}') return jsonify({ 'status': 'success', 'message': 'OTP email send successfully' }), 200 except Exception as e: - print(str(e)) + current_app.logger.error(f'Failed to send OTP email: {str(e)}') return jsonify({ 'status': 'error', 'message': 'Failed to send OTP email' @@ -379,6 +380,7 @@ def getOtp(): data = request.json userId = data['userId'] if not userId: + current_app.logger.error('UserId is required') return jsonify({ 'status': 'error', 'message': 'UserId is required' @@ -386,19 +388,21 @@ def getOtp(): try: otp = cache.get(f'otp_{userId}') if otp: + current_app.logger.info(f'OTP get successfully for user {userId}') return jsonify({ 'status': 'success', 'otp': otp, 'message': 'OTP get successfully' }), 200 else: + current_app.logger.error('OTP not found in cache or has expired') return jsonify({ 'status': 'error', 'message': 'OTP not found in cache or has expired' }), 404 except Exception as e: - print(str(e)) + current_app.logger.error(f'Failed to get OTP: {str(e)}') return jsonify({ 'status': 'error', 'message': 'Failed to get OTP' @@ -443,7 +447,7 @@ def resetPassword(): email = data['email'] new_password = data['newPassword'] if not email or not new_password: - print({'message': 'Email and newPassword is required'}) + current_app.logger.error('Email and newPassword is required') return jsonify({ 'status': 'error', 'message': 'Email and newPassword is required' @@ -451,6 +455,7 @@ def resetPassword(): user = Users.query.filter_by(email=email).first() if not user: + current_app.logger.error('User does not exit') return jsonify({ 'status': 'error', 'message': 'User does not exit' @@ -467,7 +472,6 @@ def resetPassword(): }), 200 except SQLAlchemyError as e: db.session.rollback() - print(str(e)) current_app.logger.error(f"Database error: {str(e)}") return jsonify({ 'status': 'error', From 827c26d15b2d25dcabe7f59a5643a9ecfe0eebba Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Sat, 14 Dec 2024 20:29:58 +0800 Subject: [PATCH 5/8] fix: improve error handling and responses for OTP and password reset processes --- src/auth/routes.py | 19 ++++- tests/functional/test_auth.py | 126 ++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 3 deletions(-) diff --git a/src/auth/routes.py b/src/auth/routes.py index 044c76d..350374a 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -302,6 +302,11 @@ def sendOtp(): "status": "error", "message": "Email is required" } + 404: + { + "status": "error", + "message": "User not exist + } 503: { "status": "error", @@ -320,6 +325,7 @@ def sendOtp(): OTP.user_email = email user = Users.query.filter_by(email=email).first() + if user: otp = OTP.generate_OTP() @@ -341,6 +347,12 @@ def sendOtp(): 'status': 'error', 'message': 'Failed to send OTP email' }), 503 + else: + current_app.logger.error('User not exist') + return jsonify({ + 'status': 'error', + 'message': 'User not exist' + }), 404 @auth.route('/forgotPassword/getOtp', methods=['POST']) @swag_from('../apidocs/auth/forgotPassword/getOtp.yml', methods=['POST']) @@ -371,7 +383,7 @@ def getOtp(): "status": "error", "message": "OTP not found in cache or has expired" } - 500: + 503: { "status": "error", "message": "Failed to verify OTP" @@ -406,7 +418,7 @@ def getOtp(): return jsonify({ 'status': 'error', 'message': 'Failed to get OTP' - }), 500 + }), 503 @auth.route('/forgotPassword/resetPassword', methods=['PUT']) @swag_from('../apidocs/auth/forgotPassword/resetPassword.yml', methods=['PUT']) @@ -458,7 +470,7 @@ def resetPassword(): current_app.logger.error('User does not exit') return jsonify({ 'status': 'error', - 'message': 'User does not exit' + 'message': 'User does not exist' }), 404 try: @@ -470,6 +482,7 @@ def resetPassword(): 'status': 'success', 'message': 'Password reset successfully' }), 200 + except SQLAlchemyError as e: db.session.rollback() current_app.logger.error(f"Database error: {str(e)}") diff --git a/tests/functional/test_auth.py b/tests/functional/test_auth.py index c6e1e15..aefc23a 100644 --- a/tests/functional/test_auth.py +++ b/tests/functional/test_auth.py @@ -1,3 +1,4 @@ +from sqlalchemy.exc import SQLAlchemyError import pytest from src import db, API_PREFIX from flask import url_for @@ -319,4 +320,129 @@ def test_user_confirm_status_failure(client): assert response.status_code == 404 assert response.json['Message'] == 'User not found' +class Test_ForgotPassword_SendOtp: + def test_forgotPassword_sendOtp_success(self, client, user1, mocker): + # 測試: 200 -- Send OTP successfully + mocker_generate_otp = mocker.patch('src.auth.routes.OTP.generate_OTP', return_value=('1234')) + mocker_send_email = mocker.patch('src.auth.routes.send_email', return_value=('OTP email send successfully', 200)) + response = client.post(f'{API_PREFIX}/auth/forgotPassword/sendOtp', json={'email': user1.email}) + + assert response.status_code == 200 + assert response.json['status'] == 'success' + assert response.json['message'] == 'OTP email send successfully' + + mocker_generate_otp.assert_called_once() + mocker_send_email.assert_called_once_with(user1.email, 'Your OTP codes here', mocker.ANY) + + def test_forgotPassword_sendOtp_no_email(self, client): + # 測試: 400 -- No email get + response = client.post(f'{API_PREFIX}/auth/forgotPassword/sendOtp', json={'email': ''}) + + assert response.status_code == 400 + assert response.json['status'] == 'error' + assert response.json['message'] == 'Email is required' + + def test_forgotPassword_sendOtp_user_not_exist(self, client): + # 測試: 404 -- User not exist + response = client.post(f'{API_PREFIX}/auth/forgotPassword/sendOtp', json={'email': 'nouser@example.com'}) + + assert response.status_code == 404 + assert response.json['status'] == 'error' + assert response.json['message'] == 'User not exist' + + def test_forgotPassword_sendOtp_failure(self, client, user1, mocker): + # 測試: 500 -- Send OTP failure + mocker_generate_otp = mocker.patch('src.auth.routes.OTP.generate_OTP', return_value=('1234')) + mocker_send_email = mocker.patch('src.auth.routes.send_email', side_effect=Exception('Failed to send OTP email', 503)) + response = client.post(f'{API_PREFIX}/auth/forgotPassword/sendOtp', json={'email': user1.email}) + + assert response.status_code == 503 + assert response.json['status'] == 'error' + assert response.json['message'] == 'Failed to send OTP email' + + mocker_generate_otp.assert_called_once() + mocker_send_email.assert_called_once_with(user1.email, 'Your OTP codes here', mocker.ANY) + +class Test_ForgotPassword_GetOtp: + def test_forgotPassword_getOtp_success(self, client, user1, mocker): + # 測試: 200 -- Get OTP successfully + mocker_get_otp = mocker.patch('src.auth.routes.cache.get', return_value=('1234')) + response = client.post(f'{API_PREFIX}/auth/forgotPassword/getOtp', json={'userId': user1.id}) + + assert response.status_code == 200 + assert response.json['status'] == 'success' + assert response.json['otp'] == '1234' + assert response.json['message'] == 'OTP get successfully' + + mocker_get_otp.assert_called_once() + + def test_forgotPassword_getOtp_no_userId(self, client): + # 測試: 400 -- No userId get + response = client.post(f'{API_PREFIX}/auth/forgotPassword/getOtp', json={'userId': ''}) + + assert response.status_code == 400 + assert response.json['status'] == 'error' + assert response.json['message'] == 'UserId is required' + + def test_forgotPassword_getOtp_otp_not_found(self, client, user1): + # 測試: 404 -- OTP not found + response = client.post(f'{API_PREFIX}/auth/forgotPassword/getOtp', json={'userId': user1.id}) + + assert response.status_code == 404 + assert response.json['status'] == 'error' + assert response.json['message'] == 'OTP not found in cache or has expired' + + def test_forgotPassword_getOtp_failure(self, client, user1, mocker): + # 測試: 500 -- Get OTP failure + mocker_get_otp = mocker.patch('src.auth.routes.cache.get', side_effect=Exception('Failed to get OTP', 503)) + response = client.post(f'{API_PREFIX}/auth/forgotPassword/getOtp', json={'userId': user1.id}) + + assert response.status_code == 503 + assert response.json['status'] == 'error' + assert response.json['message'] == 'Failed to get OTP' + + mocker_get_otp.assert_called_once() + +class Test_ForgotPassword_ResetPassword: + def test_forgotPassword_resetPassword_success(self, client, user1): + # 測試: 200 -- Reset password successfully + response = client.put(f'{API_PREFIX}/auth/forgotPassword/resetPassword', json={'email': user1.email, 'newPassword': 'newpassword'}) + + assert response.status_code == 200 + assert response.json['status'] == 'success' + assert response.json['message'] == 'Password reset successfully' + + def test_forgotPassword_resetPassword_no_email_or_no_newPassword(self, client, user1): + # 測試: 400 -- No email get + response = client.put(f'{API_PREFIX}/auth/forgotPassword/resetPassword', json={'email': '', 'newPassword': 'newpassword'}) + + assert response.status_code == 400 + assert response.json['status'] == 'error' + assert response.json['message'] == 'Email and newPassword is required' + + response = client.put(f'{API_PREFIX}/auth/forgotPassword/resetPassword', json={'email': user1.email, 'newPassword': ''}) + + assert response.status_code == 400 + assert response.json['status'] == 'error' + assert response.json['message'] == 'Email and newPassword is required' + + def test_forgotPassword_resetPassword_user_not_exist(self, client): + # 測試: 404 -- User not exist + response = client.put(f'{API_PREFIX}/auth/forgotPassword/resetPassword', json={'email': 'nouser@example.com', 'newPassword': 'newpassword'}) + + assert response.status_code == 404 + assert response.json['status'] == 'error' + assert response.json['message'] == 'User does not exist' + + def test_forgotPassword_resetPassword_failure(self, client, user1, mocker): + # 測試: 500 -- Reset password failure + mocker_reset_password = mocker.patch('src.auth.routes.db.session.commit', side_effect=SQLAlchemyError('Database error occurred', 500)) + response = client.put(f'{API_PREFIX}/auth/forgotPassword/resetPassword', json={'email': user1.email, 'newPassword': 'newpassword'}) + + assert response.status_code == 500 + assert response.json['status'] == 'error' + assert response.json['message'] == 'Database error occurred' + + mocker_reset_password.assert_called_once() + # @pytest.mark.only \ No newline at end of file From ef9b3e1e81c9386c45ec046fdddfbe3419186ff4 Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Mon, 16 Dec 2024 01:54:27 +0800 Subject: [PATCH 6/8] fix: update logo URL in registration email --- src/auth/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auth/routes.py b/src/auth/routes.py index 350374a..c921066 100644 --- a/src/auth/routes.py +++ b/src/auth/routes.py @@ -203,7 +203,7 @@ def register(): db.session.rollback() print(str(e)) - file = 'https://sayanythingapi.sdpmlab.org/static/images/logo_all.png' + file = 'https://github.com/user-attachments/assets/38d386b2-56f7-4df0-9e25-0e7c63081a32' subject = "Verify your account" token = generate_token(email) From 505f75cb5ea7a928f4d067bed8b1aa9d759029c0 Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Mon, 16 Dec 2024 02:00:19 +0800 Subject: [PATCH 7/8] fix: standardize response message keys to lowercase in authentication tests --- tests/functional/test_auth.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_auth.py b/tests/functional/test_auth.py index aefc23a..aa1c8e8 100644 --- a/tests/functional/test_auth.py +++ b/tests/functional/test_auth.py @@ -53,7 +53,7 @@ def test_register_success(client, new_user_data): response = client.post(f'{API_PREFIX}/auth/register', json=new_user_data) assert response.status_code == 200 - assert response.json['Message'] == 'Register Success' + assert response.json['message'] == 'Register Success' user = Users.query.filter_by(email=new_user_data['email']).first() assert user is not None @@ -72,7 +72,7 @@ def test_register_user_already_exists(client, new_user_data): response = client.post(f'{API_PREFIX}/auth/register', json=new_user_data) assert response.status_code == 404 - assert response.json['Message'] == 'username already exists' + assert response.json['message'] == 'username already exists' db.session.delete(user) db.session.commit() @@ -105,7 +105,7 @@ def test_login_user_not_found(client, user_data_confirmed): response = client.post(f'{API_PREFIX}/auth/login', json={"email": user_data_confirmed['email'], "password": user_data_confirmed['password']}) assert response.status_code == 404 - assert response.json['Message'] == 'User not exists. Please sign up first.' + assert response.json['message'] == 'User not exists. Please sign up first.' def test_login_user_not_confirmed(client, user_data_confirmed): user = Users( @@ -121,26 +121,26 @@ def test_login_user_not_confirmed(client, user_data_confirmed): response = client.post(f'{API_PREFIX}/auth/login', json={"email": user_data_confirmed['email'], "password": user_data_confirmed['password']}) assert response.status_code == 403 - assert response.json['Message'] == 'Account not confirmed. Please check your email.' + assert response.json['message'] == 'Account not confirmed. Please check your email.' def test_login_wrong_password(client, user1): response = client.post(f'{API_PREFIX}/auth/login', json={"email": user1.email, "password": "wrongpassword"}) assert response.status_code == 401 - assert response.json['Message'] == 'Login Failed, Please Check.' + assert response.json['message'] == 'Login Failed, Please Check.' def test_logout_success(client, user1): response = client.post(f'{API_PREFIX}/auth/logout', json={"userId": user1.id}) assert response.status_code == 200 - assert response.json['Message'] == f'User {user1.id} log out' + assert response.json['message'] == f'User {user1.id} log out' def test_logout_user_not_found(client): user_id = 1 response = client.post(f'{API_PREFIX}/auth/logout', json={"userId": user_id}) assert response.status_code == 404 - assert response.json['Message'] == f'User {user_id} not found' + assert response.json['message'] == f'User {user_id} not found' ##################### Manage Test Cases ##################### def test_manage(client): @@ -207,7 +207,7 @@ def test_resend_email_success(client, user_data_confirmed): response = client.post(f'{API_PREFIX}/auth/resend_confirmMail', json={"email": user_data_confirmed['email']}) assert response.status_code == 200 - assert response.json['Message'] == 'Confirm email has been resent!' + assert response.json['message'] == 'Confirm email has been resent!' def test_resend_email_failure(client, user_data_confirmed): user = Users( @@ -225,7 +225,7 @@ def test_resend_email_failure(client, user_data_confirmed): response = client.post(f'{API_PREFIX}/auth/resend_confirmMail', json={"email": user_data_confirmed['email']}) assert response.status_code == 400 - assert response.json['Message'] == 'Account already confirmed.' + assert response.json['message'] == 'Account already confirmed.' ##################### Online User Count Test Cases ##################### def test_online_user_count_success(client): @@ -293,7 +293,7 @@ def test_user_online_status_failure(client): response = client.post(f'{API_PREFIX}/auth/userOnlineStatus', json={"userId": 1}) assert response.status_code == 404 - assert response.json['Message'] == 'User not found' + assert response.json['message'] == 'User not found' ##################### User Confirm Status Test Cases ##################### def test_user_confirm_status_success(client, user1): @@ -318,7 +318,7 @@ def test_user_confirm_status_failure(client): response = client.post(f'{API_PREFIX}/auth/userConfirmStatus', json={"email": "noUser@failure.com"}) assert response.status_code == 404 - assert response.json['Message'] == 'User not found' + assert response.json['message'] == 'User not found' class Test_ForgotPassword_SendOtp: def test_forgotPassword_sendOtp_success(self, client, user1, mocker): @@ -384,8 +384,9 @@ def test_forgotPassword_getOtp_no_userId(self, client): assert response.json['status'] == 'error' assert response.json['message'] == 'UserId is required' - def test_forgotPassword_getOtp_otp_not_found(self, client, user1): + def test_forgotPassword_getOtp_otp_not_found(self, client, user1, mocker): # 測試: 404 -- OTP not found + mocker_get_otp = mocker.patch('src.auth.routes.cache.get', return_value=None) response = client.post(f'{API_PREFIX}/auth/forgotPassword/getOtp', json={'userId': user1.id}) assert response.status_code == 404 From 449e969a212cd6e29d45d086683f7e6b4f42f798 Mon Sep 17 00:00:00 2001 From: George <152157371+George15526@users.noreply.github.com> Date: Mon, 23 Dec 2024 00:11:54 +0800 Subject: [PATCH 8/8] refactor: remove admin module and associated templates and views --- src/admin/__init__.py | 17 ------------ src/admin/templates/admin/create_user.html | 11 -------- src/admin/views.py | 31 ---------------------- 3 files changed, 59 deletions(-) delete mode 100644 src/admin/__init__.py delete mode 100644 src/admin/templates/admin/create_user.html delete mode 100644 src/admin/views.py diff --git a/src/admin/__init__.py b/src/admin/__init__.py deleted file mode 100644 index 29535cf..0000000 --- a/src/admin/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# __init__.py - -from flask_admin import Admin -from .views import UserAdminView, NewsAdminView, LogoutView -from src.models import db, Users, News -from flask import Flask, request, Response -from flask_basicauth import BasicAuth - - -def init_admin(app): - admin = Admin(app, name='SayAnything Flask Admin', template_mode='bootstrap4') - basic_auth = BasicAuth(app) - admin.add_view(UserAdminView(Users, db.session, endpoint='users', url='/admin/users')) - admin.add_view(NewsAdminView(News, db.session, endpoint='news', url='/admin/news')) - admin.add_view(LogoutView(name='Logout', endpoint='logout')) - - return admin diff --git a/src/admin/templates/admin/create_user.html b/src/admin/templates/admin/create_user.html deleted file mode 100644 index 968e21c..0000000 --- a/src/admin/templates/admin/create_user.html +++ /dev/null @@ -1,11 +0,0 @@ - - -
- - -