diff --git a/app/__init__.py b/app/__init__.py index 75e454890367ec299fec9a078e3b9cee21455e9d..be141adb15748a878d8485b38e338c04efebcaeb 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,31 +1,32 @@ -from flask import Flask, url_for, redirect, session, g, abort +from flask import Flask, g, abort, current_app, request from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_login import LoginManager, current_user -from flask_security import Security -from flask_principal import Principal, Permission, RoleNeed, Identity, identity_loaded, identity_changed +from flask_principal import Principal, Permission, RoleNeed, identity_loaded from flask_wtf.csrf import CSRFProtect from dotenv import load_dotenv from functools import wraps import os +# Initialize extensions +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() +csrf = CSRFProtect() +principal = Principal() + def permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if not permission.can(): + current_app.logger.debug(f'Permission denied for {current_user} attempting to access {request.endpoint}.') abort(403) return f(*args, **kwargs) return decorated_function return decorator -db = SQLAlchemy() -migrate = Migrate() -login_manager = LoginManager() -csrf = CSRFProtect() - -from app.models import User, Role, RoleUsers super_admin_permission = Permission(RoleNeed('super-admin')) admin_permission = Permission(RoleNeed('admin')) @@ -51,31 +52,28 @@ def create_app(config_class=Config): db.init_app(app) migrate.init_app(app, db) - - # Use RoleUsers.get_datastore() instead of RoleUsers.user_datastore - security = Security(app, RoleUsers.get_datastore()) - principal = Principal(app) + login_manager.init_app(app) + csrf.init_app(app) + principal.init_app(app) # Register blueprints and URL prefixes register_blueprints(app) - # Protect internal endpoints from external use - csrf.init_app(app) - - # Identity loader - @identity_loaded.connect_via(app) - # Identity loader @identity_loaded.connect_via(app) def on_identity_loaded(sender, identity): identity.user = current_user if current_user.is_authenticated: + from app.models import User identity.provides.add(RoleNeed('user')) - for role in current_user.roles: - identity.provides.add(RoleNeed(role.name)) - # Should only be allocated to the root account, used for changing users to user -> admin - if role.name == 'super-admin': + user = User.query.get(current_user.id) + if user and user.role: + identity.provides.add(RoleNeed(user.role.name)) + if user.role.name == 'super-admin': identity.provides.add(RoleNeed('admin')) - identity.provides.add(RoleNeed('user')) + identity.provides.add(RoleNeed('super-admin')) + else: + current_app.logger.debug(f'No role found for user {identity.user.username}.') + # Add global template variables @@ -102,29 +100,32 @@ def create_app(config_class=Config): 'super_admin_permission': g.super_admin_permission } - - @app.errorhandler(Exception) - def handle_exception(e): - app.logger.error(f"Unhandled exception: {e}") - session['error_message'] = str(e) - return redirect(url_for('errors.quandary')) - @app.before_request def before_request(): g.admin_permission = admin_permission g.user_permission = user_permission g.super_admin_permission = super_admin_permission - if current_user.is_authenticated: - identity_changed.send(current_user._get_current_object(), identity=Identity(current_user.id)) + if current_user.is_authenticated: + role = current_user.role + if role: + if role.name == 'super-admin': + g.super_admin_permission = super_admin_permission + g.admin_permission = admin_permission + g.user_permission = user_permission + elif role.name == 'admin': + g.admin_permission = admin_permission + g.user_permission = user_permission + elif role.name == 'user': + g.user_permission = user_permission login_manager.login_view = 'profile.login' - login_manager.init_app(app) - + return app @login_manager.user_loader def load_user(user_id): + from app.models import User return User.query.get(int(user_id)) def register_blueprints(app): @@ -139,3 +140,10 @@ def register_blueprints(app): for module_name, url_prefix in blueprints: module = __import__(f'app.{module_name}', fromlist=['bp']) app.register_blueprint(module.bp, url_prefix=url_prefix) + +# @app.errorhandler(Exception) + # def handle_exception(e): + # app.logger.error(f"Unhandled exception: {e}") + # session['error_message'] = str(e) + # return redirect(url_for('errors.quandary')) + # pass \ No newline at end of file diff --git a/app/bookings/routes.py b/app/bookings/routes.py index 92779b487c4793013914a6293a9c20450040a932..a340d57c28ec9dbaa3123c56e492c06a351540c4 100644 --- a/app/bookings/routes.py +++ b/app/bookings/routes.py @@ -1,10 +1,10 @@ from flask import render_template, redirect, url_for, g from app.bookings import bp from app.models import Listings, ListingImages -from app import admin_permission, permission_required +from app import admin_permission, permission_required, user_permission, super_admin_permission @bp.route('/home') -@permission_required(admin_permission) +@permission_required(user_permission) def index(): listing_ids = [] top_listings = Listings.get_top_listings(5) diff --git a/app/models/__init__.py b/app/models/__init__.py index 0e4cd79972ae43b67102256add861bf191b4760a..de002ee550559f9e3ab304de74c795b7e9718841 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -2,5 +2,4 @@ from .user import User from .listings import Listings from .listing_images import ListingImages -from .role import Role -from .role_users import RoleUsers \ No newline at end of file +from .role import Role \ No newline at end of file diff --git a/app/models/role_users.py b/app/models/role_users.py deleted file mode 100644 index ed3275773405771c621149c587555d6cb8cf8862..0000000000000000000000000000000000000000 --- a/app/models/role_users.py +++ /dev/null @@ -1,22 +0,0 @@ -from flask_security import SQLAlchemyUserDatastore -from app import db - -class RoleUsers: - roles_users = db.Table('roles_users', - db.Column('user_id', db.Integer(), db.ForeignKey('users.id'), primary_key=True, index=True), - db.Column('role_id', db.Integer(), db.ForeignKey('roles.id')) - ) - - @staticmethod - def get_datastore(): - from app.models.role import Role - from app.models.user import User - return SQLAlchemyUserDatastore(db, User, Role) - - @staticmethod - def add_role_to_user(user, role_name): - from app.models.role import Role - role = Role.query.filter_by(name=role_name).first() - if role and role not in user.roles: - user.roles.append(role) - db.session.commit() diff --git a/app/models/user.py b/app/models/user.py index 5fd1487a59c7beb4b6a4d056dcd768eb3fe3c3c3..c0b838ad0893f17f0ce3f45d8345165c74da3d23 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -1,9 +1,7 @@ -from flask import request, jsonify from flask_login import UserMixin -from werkzeug.security import generate_password_hash, check_password_hash +from werkzeug.security import generate_password_hash from app import db import os -# Avoid importing Role and RoleUsers here to prevent circular import class User(UserMixin, db.Model): __tablename__ = 'users' @@ -12,12 +10,10 @@ class User(UserMixin, db.Model): username = db.Column(db.String(255), nullable=False, unique=True) email = db.Column(db.String(255), nullable=False, unique=True) password = db.Column(db.String(255), nullable=False) - fs_uniquifier = db.Column(db.String(64), unique=True, nullable=False) # Add fs_uniquifier field + fs_uniquifier = db.Column(db.String(64), unique=True, nullable=False) - # Import Role and RoleUsers only when defining the roles relationship - from app.models.role_users import RoleUsers - from app.models.role import Role - roles = db.relationship('Role', secondary=RoleUsers.roles_users, backref=db.backref('users', lazy='dynamic')) + role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) + role = db.relationship('Role', backref=db.backref('users', lazy=True)) @classmethod def create_user(cls, username, email, password, role_name='user'): @@ -27,32 +23,36 @@ class User(UserMixin, db.Model): role = Role.query.filter_by(name=role_name).first() if role: - new_user.roles.append(role) + new_user.role = role db.session.add(new_user) db.session.commit() return new_user - @classmethod def search_user_id(cls, user_id): return cls.query.get(user_id) - + @classmethod def search_user_by_email(cls, user_email): - email_exist = cls.query.filter_by(email=user_email).first() - - return email_exist + return cls.query.filter_by(email=user_email).first() @classmethod def search_user_by_username(cls, user_name): - user_exist = cls.query.filter_by(username=user_name).first() - - return user_exist + return cls.query.filter_by(username=user_name).first() @classmethod def change_user_password(cls, email, password): user = cls.search_user_by_email(email) - if user is None: raise ValueError("Error") + hashed_password = generate_password_hash(password, method='pbkdf2:sha256') + user.password = hashed_password + db.session.commit() + + @classmethod + def get_user_role(cls, user_id): + user = cls.query.get(user_id) + if user and user.role: + return user.role.name + return None diff --git a/app/profile/routes.py b/app/profile/routes.py index bd6df0448ca7d4e4b62d97fbe356170351b622b9..4b04a1a61371fd4021608044ad02da8801ab7bec 100644 --- a/app/profile/routes.py +++ b/app/profile/routes.py @@ -1,11 +1,13 @@ #https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login#step-1-installing-packages -from flask import render_template, redirect, url_for, request, flash, jsonify, session -from app.profile import bp +from flask import render_template, redirect, url_for, request, flash, jsonify, session, current_app +from flask_principal import Identity, identity_changed +from flask_login import login_user, logout_user, login_required, current_user from werkzeug.security import check_password_hash +from app.profile import bp from app.models import User -from app import db -from flask_login import login_user, logout_user, login_required, current_user from app.logger import auth_logger +from app import db + @bp.route('/signup', methods=['GET', 'POST']) def signup(): @@ -61,6 +63,8 @@ def is_valid_username(username): return all(c.isalnum() or c in allowed_special_chars for c in username) +from flask_principal import Identity, identity_changed + @bp.route('/login', methods=['POST']) def login_post(): username_field = request.form.get('username') @@ -77,9 +81,13 @@ def login_post(): return redirect(url_for('profile.login', error=True)) login_user(user, remember=remember) + + identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) + return redirect(url_for('profile.index')) + @bp.route('/check-username', methods=['POST']) def check_username(): data = request.get_json() diff --git a/migrations/versions/6c7070736062_add_role_id_to_users_table.py b/migrations/versions/6c7070736062_add_role_id_to_users_table.py new file mode 100644 index 0000000000000000000000000000000000000000..06ca58762200695b2506fcaba3b348e875d99077 --- /dev/null +++ b/migrations/versions/6c7070736062_add_role_id_to_users_table.py @@ -0,0 +1,26 @@ +"""Add role_id to users table + +Revision ID: 6c7070736062 +Revises: ad8ca3c3dfaa +Create Date: 2025-01-06 20:16:19.191868 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '6c7070736062' +down_revision = 'ad8ca3c3dfaa' +branch_labels = None +depends_on = None + +def upgrade(): + # Add column role_id to users table + op.add_column('users', sa.Column('role_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id']) + +def downgrade(): + # Remove column role_id from users table + op.drop_constraint(None, 'users', type_='foreignkey') + op.drop_column('users', 'role_id') diff --git a/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py b/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py index e93fdc531d938284d03cd0cd797f28dfed8b5ce9..59f4c9a590293c9ba96b7d83bef1228cdce3a182 100644 --- a/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py +++ b/migrations/versions/ac9d4555724d_add_api_token_and_expiry.py @@ -23,7 +23,5 @@ def upgrade(): sa.Column('username', sa.String(255), nullable=False, unique=True), sa.Column('email', sa.String(255), nullable=False, unique=True), sa.Column('password', sa.String(255), nullable=False), - sa.Column('role_id', sa.SmallInteger(), nullable=False, server_default='1'), #Standard user permission level - sa.Column('api_token', sa.String(255), nullable=True, unique=True), - sa.Column('token_expiry', sa.DateTime(), nullable=True) + sa.Column('role_id', sa.SmallInteger(), nullable=False, server_default='3') ) \ No newline at end of file diff --git a/migrations/versions/ad8ca3c3dfaa_add_composite_primary_key_and_index_to_.py b/migrations/versions/ad8ca3c3dfaa_add_composite_primary_key_and_index_to_.py deleted file mode 100644 index 6ffcbf94f8f82a0daec75b7331366ac24c3f77c6..0000000000000000000000000000000000000000 --- a/migrations/versions/ad8ca3c3dfaa_add_composite_primary_key_and_index_to_.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Add composite primary key and index to roles_users - -Revision ID: ad8ca3c3dfaa -Revises: 22de5b143d05 -Create Date: 2025-01-06 13:56:13.747100 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'ad8ca3c3dfaa' -down_revision = '22de5b143d05' -branch_labels = None -depends_on = None - - -def upgrade(): - op.drop_table('roles_users') - - op.create_table( - 'roles_users', - sa.Column('user_id', sa.Integer, sa.ForeignKey('users.id'), primary_key=True, index=True), - sa.Column('role_id', sa.Integer, sa.ForeignKey('roles.id'), primary_key=True) - ) - -def downgrade(): - op.drop_table('roles_users') - - op.create_table( - 'roles_users', - sa.Column('user_id', sa.Integer, sa.ForeignKey('users.id')), - sa.Column('role_id', sa.Integer, sa.ForeignKey('roles.id')) - )