From e2328771e6437f7499606888986b2708de253aa2 Mon Sep 17 00:00:00 2001 From: Kfir Dayan Date: Sun, 7 Jan 2024 16:21:35 +0200 Subject: [PATCH] improved structure + error handling --- app.py | 16 ++- middelwares/errorHandlers.py | 11 ++ middelwares/eventMiddelwares.py | 36 ++++++ middelwares/userMiddelwares.py | 73 ++++++++++++ middlewares.py | 107 ------------------ .../5569d39a87cf_initial_migration.py | 42 ------- migrations/versions/9d6b0ea04d2c_.py | 46 -------- migrations/versions/b962126d3578_.py | 36 ------ .../bb3a88007475_initial_migration.py | 49 ++++++++ models.py | 4 +- routes/eventRoutes.py | 12 +- routes/userRoutes.py | 16 ++- services/EventService.py | 22 +++- services/UserService.py | 2 +- 14 files changed, 221 insertions(+), 251 deletions(-) create mode 100644 middelwares/errorHandlers.py create mode 100644 middelwares/eventMiddelwares.py create mode 100644 middelwares/userMiddelwares.py delete mode 100644 migrations/versions/5569d39a87cf_initial_migration.py delete mode 100644 migrations/versions/9d6b0ea04d2c_.py delete mode 100644 migrations/versions/b962126d3578_.py create mode 100644 migrations/versions/bb3a88007475_initial_migration.py diff --git a/app.py b/app.py index 713fbbf..3619f99 100644 --- a/app.py +++ b/app.py @@ -5,6 +5,9 @@ from routes.userRoutes import userRoutes from routes.eventRoutes import eventRoutes import config from flask_jwt_extended import JWTManager +from middelwares.errorHandlers import handle_auth_error, handle_invalid_token +from flask_jwt_extended.exceptions import NoAuthorizationError +from jwt.exceptions import InvalidTokenError class App: def __init__(self): @@ -13,6 +16,7 @@ class App: self.set_up_db() self.set_up_jwt() self.register_blueprints() + self.setup_error_handlers() def set_config(self): self.app.config.from_object(config.Config) @@ -21,6 +25,10 @@ class App: db.init_app(self.app) self.migrate = Migrate(self.app, db) + def setup_error_handlers(self): + self.app.register_error_handler(NoAuthorizationError, handle_auth_error) + self.app.register_error_handler(InvalidTokenError, handle_invalid_token) + def set_up_jwt(self): self.jwt_manager = JWTManager(self.app) self.app.config['JWT_TOKEN_LOCATION'] = ['cookies'] @@ -43,7 +51,9 @@ class App: function_name = self.app.view_functions[rule.endpoint].__name__ print(f" Function: {function_name}") + +app_class_instance = App() +app_instance = app_class_instance.app + if __name__ == '__main__': - app_instance = App() - app_instance.print_endpoints() - app_instance.run() + app_class_instance.run() diff --git a/middelwares/errorHandlers.py b/middelwares/errorHandlers.py new file mode 100644 index 0000000..36a6cee --- /dev/null +++ b/middelwares/errorHandlers.py @@ -0,0 +1,11 @@ +from functools import wraps +from flask import request, jsonify + +def handle_auth_error(e): + return jsonify({"error": "Please login"}), 401 + +def handle_invalid_token(e): + return jsonify({"error": "Invalid session token"}), 401 + +def handle_invalid_header_error(e): + return jsonify({"error": "Please login again"}), 401 \ No newline at end of file diff --git a/middelwares/eventMiddelwares.py b/middelwares/eventMiddelwares.py new file mode 100644 index 0000000..6af5ac2 --- /dev/null +++ b/middelwares/eventMiddelwares.py @@ -0,0 +1,36 @@ +from functools import wraps +from flask import request, jsonify +from datetime import datetime +from flask_jwt_extended import jwt_required, get_jwt_identity + +def validate_event_post_request(f): + @wraps(f) + def decorated_function(*args, **kwargs): + data = request.get_json() + if not data: + return jsonify({"message": "No input data provided"}), 400 + + # Check required fields + required_fields = ['title', 'duedate', 'location', 'description'] + if not all(field in data for field in required_fields): + return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400 + + # Validate 'title' + if not isinstance(data['title'], str) or not data['title'].strip(): + return jsonify({"message": "Invalid title"}), 400 + + # Validate 'description' + if not isinstance(data['description'], str): + return jsonify({"message": "Invalid description"}), 400 + # Validate 'time' (ensure it's a valid datetime string) + try: + datetime.strptime(data['duedate'], '%Y-%m-%dT%H:%M:%S') + except ValueError: + return jsonify({"message": "Invalid time format. Use YYYY-MM-DDTHH:MM:SS"}), 400 + + # Validate 'location' + if not isinstance(data['location'], str) or not data['location'].strip(): + return jsonify({"message": "Invalid location"}), 400 + + return f(*args, **kwargs) + return decorated_function diff --git a/middelwares/userMiddelwares.py b/middelwares/userMiddelwares.py new file mode 100644 index 0000000..dd5658e --- /dev/null +++ b/middelwares/userMiddelwares.py @@ -0,0 +1,73 @@ +from functools import wraps +from flask import request, jsonify, g +from flask_jwt_extended import jwt_required, get_jwt_identity + +def validate_user_post_request(f): + @wraps(f) + def decorated_function(*args, **kwargs): + data = request.get_json() + if not data: + return jsonify({"message": "No input data provided"}), 400 + + # Check required fields + required_fields = ['username', 'password', 'email', 'location'] + if not all(field in data for field in required_fields): + return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400 + + # Validate 'username' + if not isinstance(data['username'], str) or not data['username'].strip(): + return jsonify({"message": "Invalid username"}), 400 + + # Validate 'password' + if not isinstance(data['password'], str) or not data['password'].strip(): + return jsonify({"message": "Invalid password"}), 400 + + # Validate 'email' + if not isinstance(data['email'], str) or not data['email'].strip(): + return jsonify({"message": "Invalid email"}), 400 + + # Validate 'location' + if not isinstance(data['location'], str) or not data['location'].strip(): + return jsonify({"message": "Invalid location"}), 400 + + return f(*args, **kwargs) + return decorated_function + + +def validate_user_login_request(f): + @wraps(f) + def decorated_function(*args, **kwargs): + data = request.get_json() + if not data: + return jsonify({"message": "No input data provided"}), 400 + + # Check required fields + required_fields = ['email', 'password'] + if not all(field in data for field in required_fields): + return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400 + + # Validate 'email' + if not isinstance(data['email'], str) or not data['email'].strip(): + return jsonify({"message": "Invalid email"}), 400 + + # Validate 'password' + if not isinstance(data['password'], str) or not data['password'].strip(): + return jsonify({"message": "Invalid password"}), 400 + + return f(*args, **kwargs) + return decorated_function + +def authenticate_user(f): + @wraps(f) + @jwt_required(locations=["cookies"]) # Specify to look for the token in cookies + def decorated_function(*args, **kwargs): + # Get user identity from JWT + user_id = get_jwt_identity() + if user_id: + g.user_id = user_id + else: + return jsonify({"error": "Invalid session token"}), 401 + + return f(*args, **kwargs) + return decorated_function + diff --git a/middlewares.py b/middlewares.py index 6f93fc6..e69de29 100644 --- a/middlewares.py +++ b/middlewares.py @@ -1,107 +0,0 @@ -from functools import wraps -from flask import request, jsonify -from datetime import datetime -from flask_jwt_extended import jwt_required, get_jwt_identity - - -def validate_user_post_request(f): - @wraps(f) - def decorated_function(*args, **kwargs): - data = request.get_json() - if not data: - return jsonify({"message": "No input data provided"}), 400 - - # Check required fields - required_fields = ['username', 'password', 'email', 'location'] - if not all(field in data for field in required_fields): - return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400 - - # Validate 'username' - if not isinstance(data['username'], str) or not data['username'].strip(): - return jsonify({"message": "Invalid username"}), 400 - - # Validate 'password' - if not isinstance(data['password'], str) or not data['password'].strip(): - return jsonify({"message": "Invalid password"}), 400 - - # Validate 'email' - if not isinstance(data['email'], str) or not data['email'].strip(): - return jsonify({"message": "Invalid email"}), 400 - - # Validate 'location' - if not isinstance(data['location'], str) or not data['location'].strip(): - return jsonify({"message": "Invalid location"}), 400 - - return f(*args, **kwargs) - return decorated_function - - -def validate_user_login_request(f): - @wraps(f) - def decorated_function(*args, **kwargs): - data = request.get_json() - if not data: - return jsonify({"message": "No input data provided"}), 400 - - # Check required fields - required_fields = ['email', 'password'] - if not all(field in data for field in required_fields): - return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400 - - # Validate 'email' - if not isinstance(data['email'], str) or not data['email'].strip(): - return jsonify({"message": "Invalid email"}), 400 - - # Validate 'password' - if not isinstance(data['password'], str) or not data['password'].strip(): - return jsonify({"message": "Invalid password"}), 400 - - return f(*args, **kwargs) - return decorated_function - -def validate_event_post_request(f): - @wraps(f) - def decorated_function(*args, **kwargs): - data = request.get_json() - if not data: - return jsonify({"message": "No input data provided"}), 400 - - # Check required fields - required_fields = ['title', 'duedate', 'location', 'description'] - if not all(field in data for field in required_fields): - return jsonify({"message": "Please check your data, you missing some props; visit our docs https://git.dayanhub.com/kfir"}), 400 - - # Validate 'title' - if not isinstance(data['title'], str) or not data['title'].strip(): - return jsonify({"message": "Invalid title"}), 400 - - # Validate 'description' - if not isinstance(data['description'], str): - return jsonify({"message": "Invalid description"}), 400 - - # Validate 'time' (ensure it's a valid datetime string) - try: - datetime.strptime(data['duedate'], '%Y-%m-%dT%H:%M:%S') - except ValueError: - return jsonify({"message": "Invalid time format. Use YYYY-MM-DDTHH:MM:SS"}), 400 - - # Validate 'location' - if not isinstance(data['location'], str) or not data['location'].strip(): - return jsonify({"message": "Invalid location"}), 400 - - return f(*args, **kwargs) - return decorated_function - -def authenticate_user(f): - @wraps(f) - @jwt_required(locations=["cookies"]) # Specify to look for the token in cookies - def decorated_function(*args, **kwargs): - # Get user identity from JWT - user_id = get_jwt_identity() - if user_id: - request.user_id = user_id - else: - return jsonify({"error": "Invalid session token"}), 401 - - return f(*args, **kwargs) - return decorated_function \ No newline at end of file diff --git a/migrations/versions/5569d39a87cf_initial_migration.py b/migrations/versions/5569d39a87cf_initial_migration.py deleted file mode 100644 index 7c2eef6..0000000 --- a/migrations/versions/5569d39a87cf_initial_migration.py +++ /dev/null @@ -1,42 +0,0 @@ -"""initial migration - -Revision ID: 5569d39a87cf -Revises: -Create Date: 2024-01-04 13:44:40.811421 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '5569d39a87cf' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('event', schema=None) as batch_op: - batch_op.add_column(sa.Column('title', sa.String(length=100), nullable=False)) - batch_op.add_column(sa.Column('description', sa.String(length=200), nullable=True)) - batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True)) - batch_op.add_column(sa.Column('deleted', sa.Boolean(), nullable=True)) - batch_op.add_column(sa.Column('duedate', sa.DateTime(), nullable=True)) - batch_op.add_column(sa.Column('created_at', sa.DateTime(), nullable=True)) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('event', schema=None) as batch_op: - batch_op.drop_column('created_at') - batch_op.drop_column('duedate') - batch_op.drop_column('deleted') - batch_op.drop_column('location') - batch_op.drop_column('description') - batch_op.drop_column('title') - - # ### end Alembic commands ### diff --git a/migrations/versions/9d6b0ea04d2c_.py b/migrations/versions/9d6b0ea04d2c_.py deleted file mode 100644 index dd41d2c..0000000 --- a/migrations/versions/9d6b0ea04d2c_.py +++ /dev/null @@ -1,46 +0,0 @@ -"""empty message - -Revision ID: 9d6b0ea04d2c -Revises: 5569d39a87cf -Create Date: 2024-01-07 11:34:58.903280 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '9d6b0ea04d2c' -down_revision = '5569d39a87cf' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.add_column(sa.Column('name', sa.String(length=100), nullable=True)) - batch_op.add_column(sa.Column('email', sa.String(length=120), nullable=False)) - batch_op.add_column(sa.Column('location', sa.String(length=100), nullable=True)) - batch_op.alter_column('id', - existing_type=sa.INTEGER(), - type_=sa.String(length=36), - existing_nullable=False) - batch_op.create_unique_constraint('uq_user_email', ['email']) # Added constraint name here - batch_op.drop_column('username') - # ### end Alembic commands ### - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.add_column(sa.Column('username', sa.VARCHAR(length=80), nullable=False)) - batch_op.drop_constraint('uq_user_email', type_='unique') # Updated constraint name here - batch_op.alter_column('id', - existing_type=sa.String(length=36), - type_=sa.INTEGER(), - existing_nullable=False) - batch_op.drop_column('location') - batch_op.drop_column('email') - batch_op.drop_column('name') - # ### end Alembic commands ### - diff --git a/migrations/versions/b962126d3578_.py b/migrations/versions/b962126d3578_.py deleted file mode 100644 index bcba3c2..0000000 --- a/migrations/versions/b962126d3578_.py +++ /dev/null @@ -1,36 +0,0 @@ -"""empty message - -Revision ID: b962126d3578 -Revises: 9d6b0ea04d2c -Create Date: 2024-01-07 11:41:03.752411 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'b962126d3578' -down_revision = '9d6b0ea04d2c' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.alter_column('location', - existing_type=sa.VARCHAR(length=100), - nullable=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.alter_column('location', - existing_type=sa.VARCHAR(length=100), - nullable=True) - - # ### end Alembic commands ### diff --git a/migrations/versions/bb3a88007475_initial_migration.py b/migrations/versions/bb3a88007475_initial_migration.py new file mode 100644 index 0000000..cdfb0a4 --- /dev/null +++ b/migrations/versions/bb3a88007475_initial_migration.py @@ -0,0 +1,49 @@ +"""initial migration + +Revision ID: bb3a88007475 +Revises: +Create Date: 2024-01-07 14:12:47.164418 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bb3a88007475' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=100), nullable=True), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password_hash', sa.String(length=128), nullable=True), + sa.Column('location', sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ) + op.create_table('event', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=200), nullable=True), + sa.Column('location', sa.String(length=100), nullable=True), + sa.Column('deleted', sa.Boolean(), nullable=True), + sa.Column('duedate', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('user_id', sa.String(length=36), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('event') + op.drop_table('user') + # ### end Alembic commands ### diff --git a/models.py b/models.py index 7fcb49e..fc0e07e 100644 --- a/models.py +++ b/models.py @@ -13,6 +13,7 @@ class Event(db.Model): deleted = db.Column(db.Boolean, default=False) duedate = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=db.func.now()) + user_id = db.Column(db.String(36), db.ForeignKey('user.id'), nullable=False) def to_dict(self): return { @@ -21,7 +22,8 @@ class Event(db.Model): 'description': self.description, 'location': self.location, 'duedate': self.duedate.isoformat() if self.duedate else None, - 'created_at': self.created_at.isoformat() + 'created_at': self.created_at.isoformat(), + 'user_id': self.user_id } class User(db.Model): diff --git a/routes/eventRoutes.py b/routes/eventRoutes.py index 2a6825a..b7cccc6 100644 --- a/routes/eventRoutes.py +++ b/routes/eventRoutes.py @@ -1,6 +1,7 @@ -from flask import Blueprint, jsonify, request +from flask import Blueprint, jsonify, request, g from services.EventService import EventService -from middlewares import validate_event_post_request, authenticate_user +from middelwares.userMiddelwares import authenticate_user +from middelwares.eventMiddelwares import validate_event_post_request eventRoutes = Blueprint('eventRoutes', __name__) @@ -12,6 +13,8 @@ eventRoutes = Blueprint('eventRoutes', __name__) def create_event(): try: data = request.json + data['user_id'] = g.user_id + print(data) new_event = EventService.create_event(data) if new_event: return jsonify(new_event.to_dict()), 201 @@ -26,7 +29,8 @@ def create_event(): @authenticate_user def get_events(): try: - return {"events": EventService.get_all_events()}, 200 + user_events = EventService.get_all_user_events(g.user_id) + return {"events": user_events, "count": len(user_events)}, 200 except Exception as e: return {"error": str(e)}, 500 @@ -35,7 +39,7 @@ def get_events(): @authenticate_user def get_event(event_id): try: - return {"event": EventService.get_event_by_id(event_id)}, 200 + return {"event": EventService.get_event_by_id(event_id, g.user_id)}, 200 except Exception as e: return {"error": str(e)}, 500 diff --git a/routes/userRoutes.py b/routes/userRoutes.py index ce1828f..1332ba4 100644 --- a/routes/userRoutes.py +++ b/routes/userRoutes.py @@ -1,14 +1,14 @@ from flask import Blueprint, jsonify, request from services.UserService import UserService from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity -from middlewares import validate_user_post_request, validate_user_login_request, authenticate_user +from middelwares.userMiddelwares import validate_user_post_request, validate_user_login_request +from middelwares.eventMiddelwares import validate_event_post_request userRoutes = Blueprint('userRoutes', __name__) @userRoutes.route('/', methods=['GET']) @userRoutes.route('', methods=['GET']) -@authenticate_user def allUsers(): users = UserService.get_all_users() return jsonify(users), 200 @@ -36,9 +36,10 @@ def loginUser(): data = request.json user = UserService.verify_user(data) if user: - sessionToken = create_access_token(identity=user.id) - response = jsonify(user.to_dict()) - response.set_cookie('access_token_cookie', sessionToken, httponly=True, path='/') + token = login(user) + response = user.to_dict() + response['token'] = token + response = jsonify(response) return response, 200 else: return jsonify({'error': 'Invalid credentials'}), 400 @@ -55,3 +56,8 @@ def logoutUser(): except Exception as e: return jsonify({'error': str(e)}), 500 +def login(user): + sessionToken = create_access_token(identity=user.id) + response = jsonify(user.to_dict()) + response.set_cookie('access_token_cookie', sessionToken, httponly=True, path='/') + return sessionToken \ No newline at end of file diff --git a/services/EventService.py b/services/EventService.py index 1545c4d..a197431 100644 --- a/services/EventService.py +++ b/services/EventService.py @@ -4,24 +4,34 @@ from datetime import datetime class EventService: @staticmethod def create_event(data): + print("#########") + print(data) new_event = Event( title=data['title'], description=data.get('description', ''), location=data.get('location', ''), - duedate=datetime.strptime(data['duedate'], '%Y-%m-%dT%H:%M:%S') + duedate=datetime.strptime(data['duedate'], '%Y-%m-%dT%H:%M:%S'), + user_id=data['user_id'] ) db.session.add(new_event) db.session.commit() return new_event @staticmethod - def get_all_events(): - events=Event.query.filter_by(deleted=False).all() - return [event.to_dict() for event in events] + def get_all_user_events(user_id): + events=Event.query.filter_by(user_id=user_id, deleted=False).all() + if(events): + return [event.to_dict() for event in events] + else: + return [] @staticmethod - def get_event_by_id(event_id): - return Event.query.filter_by(id=event_id, deleted=False).first().to_dict() + def get_event_by_id(event_id, user_id): + event = Event.query.filter_by(id=event_id,user_id=user_id, deleted=False).first() + if(event): + return event.to_dict() + else: + return [] @staticmethod def update_event(event_id, data): diff --git a/services/UserService.py b/services/UserService.py index d043bf9..7ce4d4a 100644 --- a/services/UserService.py +++ b/services/UserService.py @@ -14,7 +14,7 @@ class UserService: ) db.session.add(new_user) db.session.commit() - return new_user + return new_user.to_dict() @staticmethod def get_all_users():