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
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
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 @@
-
-
-
-
-
- Document
-
-
- Hi, I am create user page
-
-
\ No newline at end of file
diff --git a/src/admin/views.py b/src/admin/views.py
deleted file mode 100644
index 341cea2..0000000
--- a/src/admin/views.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from flask_admin.contrib.sqla import ModelView
-from flask_admin import expose, BaseView
-from flask import session, make_response, url_for, redirect
-
-class UserAdminView(ModelView):
- column_list = [
- 'id', 'username', 'gender', 'email',
- 'registered_on','is_confirmed',
- 'confirmed_on', 'online',
- 'last_login', 'last_logout'
- ]
- form_excluded_columns = ['password_hashed', 'registered_on', 'confirmed_on', 'last_login', 'last_logout']
- form_columns = ['username', 'email', 'is_confirmed', 'online']
- can_create = False
- can_delete = False
- can_edit = True
-
-
-class NewsAdminView(ModelView):
- column_list = ['id', 'title', 'content', 'created_time']
- form_columns = ['title', 'content']
- can_create = True
- can_edit = True
- can_delete = True
- can_export = True
- export_types = ['csv', 'xls', 'json']
-
-class LogoutView(BaseView):
- @expose('/')
- def index(self):
- return redirect(url_for('logout'))
\ No newline at end of file
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..c921066 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':
@@ -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)
@@ -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,26 +294,38 @@ 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"
+ }
+ 404:
+ {
+ "status": "error",
+ "message": "User not exist
}
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()
+
if user:
otp = OTP.generate_OTP()
@@ -323,17 +336,29 @@ 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({
- '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
+ current_app.logger.error(f'Failed to send OTP email: {str(e)}')
+ return jsonify({
+ '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'])
+@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 +369,62 @@ 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"
+ }
+ 503:
+ {
+ "status": "error",
+ "message": "Failed to verify OTP"
}
"""
data = request.json
userId = data['userId']
if not userId:
- return jsonify({'Message': 'User ID is required'}), 400
+ current_app.logger.error('UserId is required')
+ return jsonify({
+ 'status': 'error',
+ 'message': 'UserId is required'
+ }), 400
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': 'Verify otp success'
+ 'message': 'OTP get successfully'
}), 200
else:
- return jsonify({'Message': 'OTP not found in cache or has expired'}), 404
+ 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))
- return jsonify({'Message': 'Failed to verify OTP'}), 500
+ current_app.logger.error(f'Failed to get OTP: {str(e)}')
+ return jsonify({
+ 'status': 'error',
+ 'message': 'Failed to get OTP'
+ }), 503
@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 +436,69 @@ 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
+ current_app.logger.error('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
+ current_app.logger.error('User does not exit')
+ return jsonify({
+ 'status': 'error',
+ 'message': 'User does not exist'
+ }), 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 +525,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 +540,7 @@ def userOnlineStatus():
}
404:
{
- "Message": "User not found"
+ "message": "User not found"
}
"""
if request.method == 'POST':
@@ -484,14 +552,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 +575,7 @@ def userConfirmStatus():
}
404:
{
- "Message": "User not found"
+ "message": "User not found"
}
"""
data = request.json
@@ -524,14 +592,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 +611,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 +634,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 +647,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 +667,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
diff --git a/tests/functional/test_auth.py b/tests/functional/test_auth.py
index c6e1e15..aa1c8e8 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
@@ -52,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
@@ -71,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()
@@ -104,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(
@@ -120,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):
@@ -206,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(
@@ -224,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):
@@ -292,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):
@@ -317,6 +318,132 @@ 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):
+ # 測試: 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, 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
+ 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