diff --git a/alembic/versions/e63f024c23eb_merge_heads.py b/alembic/versions/e63f024c23eb_merge_heads.py new file mode 100644 index 000000000..62c776be8 --- /dev/null +++ b/alembic/versions/e63f024c23eb_merge_heads.py @@ -0,0 +1,26 @@ +"""merge heads + +Revision ID: e63f024c23eb +Revises: 0c0978bc2925, 3f455aaf9065 +Create Date: 2024-08-10 11:54:28.435165 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e63f024c23eb' +down_revision: Union[str, None] = ('0c0978bc2925', '3f455aaf9065') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/api/v1/models/organisation.py b/api/v1/models/organisation.py index 1fd7f1225..ee44cd9b7 100644 --- a/api/v1/models/organisation.py +++ b/api/v1/models/organisation.py @@ -1,16 +1,16 @@ """ The Organisation model """ -from sqlalchemy import Column, String, Text, Enum +from sqlalchemy import Column, String from sqlalchemy.orm import relationship -from api.v1.models.associations import user_organisation_association +from api.v1.models.permissions.user_org_role import user_organisation_roles from api.v1.models.base_model import BaseTableModel class Organisation(BaseTableModel): __tablename__ = "organisations" - name = Column(String, nullable=False, unique=True) + name = Column(String, nullable=False, unique=False) email = Column(String, nullable=True, unique=True) industry = Column(String, nullable=True) type = Column(String, nullable=True) @@ -20,7 +20,7 @@ class Organisation(BaseTableModel): address = Column(String, nullable=True) users = relationship( - "User", secondary=user_organisation_association, back_populates="organisations" + "User", secondary=user_organisation_roles, back_populates="organisations" ) billing_plans = relationship("BillingPlan", back_populates="organisation", cascade="all, delete-orphan") invitations = relationship("Invitation", back_populates="organisation", cascade="all, delete-orphan") @@ -36,8 +36,7 @@ class Organisation(BaseTableModel): products = relationship( "Product", back_populates="organisation", cascade="all, delete-orphan" ) - sales = relationship('Sales', back_populates='organisation', - cascade='all, delete-orphan') + sales = relationship('Sales', back_populates='organisation', cascade='all, delete-orphan') def __str__(self): return self.name diff --git a/api/v1/models/permissions/permissions.py b/api/v1/models/permissions/permissions.py index e0671d645..3ed85997b 100644 --- a/api/v1/models/permissions/permissions.py +++ b/api/v1/models/permissions/permissions.py @@ -1,9 +1,11 @@ from sqlalchemy import Column, String from api.v1.models.base_model import BaseTableModel -from uuid_extensions import uuid7 +from api.v1.models.permissions.role_permissions import role_permissions +from sqlalchemy.orm import relationship class Permission(BaseTableModel): __tablename__ = 'permissions' - title = Column(String, unique=True, nullable=False) - + title = Column(String, unique=True, nullable=False) + + role = relationship('Role', secondary=role_permissions, back_populates='permissions') diff --git a/api/v1/models/permissions/role.py b/api/v1/models/permissions/role.py index f2a382a85..1857f508c 100644 --- a/api/v1/models/permissions/role.py +++ b/api/v1/models/permissions/role.py @@ -1,6 +1,7 @@ from sqlalchemy import Column, String, Boolean, Text +from sqlalchemy.orm import relationship from api.v1.models.base_model import BaseTableModel -from uuid_extensions import uuid7 +from api.v1.models.permissions.role_permissions import role_permissions class Role(BaseTableModel): __tablename__ = 'roles' @@ -8,3 +9,5 @@ class Role(BaseTableModel): name = Column(String, unique=True, nullable=False) description = Column(Text, nullable=True) is_builtin = Column(Boolean, default=False) # True for built-in roles, False for custom roles + + permissions = relationship('Permission', secondary=role_permissions, back_populates='role') diff --git a/api/v1/models/user.py b/api/v1/models/user.py index 08bd528bc..d1d5f74c0 100644 --- a/api/v1/models/user.py +++ b/api/v1/models/user.py @@ -4,6 +4,7 @@ from sqlalchemy import Column, String, text, Boolean from sqlalchemy.orm import relationship from api.v1.models.associations import user_organisation_association +from api.v1.models.permissions.user_org_role import user_organisation_roles from api.v1.models.base_model import BaseTableModel @@ -24,7 +25,7 @@ class User(BaseTableModel): "Profile", uselist=False, back_populates="user", cascade="all, delete-orphan" ) organisations = relationship( - "Organisation", secondary=user_organisation_association, back_populates="users" + "Organisation", secondary=user_organisation_roles, back_populates="users" ) notifications = relationship( "Notification", back_populates="user", cascade="all, delete-orphan" diff --git a/api/v1/routes/auth.py b/api/v1/routes/auth.py index 3958abaf8..310b44637 100644 --- a/api/v1/routes/auth.py +++ b/api/v1/routes/auth.py @@ -29,11 +29,11 @@ def register(background_tasks: BackgroundTasks, response: Response, user_schema: user = user_service.create(db=db, schema=user_schema) # create an organization for the user - org = CreateUpdateOrganization( - name=f"{user.first_name}'s Organization", + org = CreateUpdateOrganisation( + name=f"{user.first_name}'s Organisation", email=user.email ) - user_org = organization_service.create(db=db, schema=org, user=user) + user_org = organisation_service.create(db=db, schema=org, user=user) # Create access and refresh tokens access_token = user_service.create_access_token(user_id=user.id) diff --git a/api/v1/routes/google_login.py b/api/v1/routes/google_login.py index da0aac448..7d009da01 100644 --- a/api/v1/routes/google_login.py +++ b/api/v1/routes/google_login.py @@ -10,7 +10,6 @@ from api.db.database import get_db from api.core.dependencies.google_oauth_config import google_oauth -from api.v1.schemas.organization import CreateUpdateOrganization from api.v1.services.google_oauth import GoogleOauthServices from api.utils.success_response import success_response from api.v1.schemas.google_oauth import OAuthToken diff --git a/api/v1/services/organisation.py b/api/v1/services/organisation.py index e465544db..dd3f54f82 100644 --- a/api/v1/services/organisation.py +++ b/api/v1/services/organisation.py @@ -9,8 +9,10 @@ from api.core.base.services import Service from api.utils.db_validators import check_model_existence, check_user_in_org from api.utils.pagination import paginated_response +from api.v1.models.permissions.role import Role from api.v1.models.product import Product from api.v1.models.associations import user_organisation_association +from api.v1.models.permissions.user_org_role import user_organisation_roles from api.v1.models.organisation import Organisation from api.v1.models.user import User from api.v1.schemas.organisation import ( @@ -23,23 +25,36 @@ class OrganisationService(Service): """Organisation service functionality""" + def get_role_id(self, db: Session, role: str): + '''Returns the role id associated with a role''' + + role_ = db.query(Role).filter(Role.name == role).first() + + if not role_: + raise HTTPException(status_code=404, detail="Admin role not found") + + return role_.id + + def create(self, db: Session, schema: CreateUpdateOrganisation, user: User): """Create a new product""" # Create a new organisation new_organisation = Organisation(**schema.model_dump()) + email = schema.model_dump()["email"] - name = schema.model_dump()["name"] self.check_by_email(db, email) - self.check_by_name(db, name) db.add(new_organisation) db.commit() db.refresh(new_organisation) # Add user as owner to the new organisation - stmt = user_organisation_association.insert().values( - user_id=user.id, organisation_id=new_organisation.id, role="owner" + stmt = user_organisation_roles.insert().values( + user_id=user.id, + organisation_id=new_organisation.id, + role_id=self.get_role_id(db=db, role='admin'), + is_owner=True ) db.execute(stmt) db.commit() diff --git a/api/v1/services/user.py b/api/v1/services/user.py index 87f4ff1ad..5138a5edc 100644 --- a/api/v1/services/user.py +++ b/api/v1/services/user.py @@ -136,7 +136,7 @@ def create(self, db: Session, schema: user.UserCreate): if db.query(User).filter(User.email == schema.email).first(): raise HTTPException( status_code=400, - detail="User with this email or username already exists", + detail="User with this email already exists", ) # Hash password @@ -152,7 +152,6 @@ def create(self, db: Session, schema: user.UserCreate): notification_setting_service.create(db=db, user=user) # create data privacy setting - data_privacy = DataPrivacySetting(user_id=user.id) db.add(data_privacy) diff --git a/main.py b/main.py index c13eb6496..d80d994b2 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ - import uvicorn from fastapi.staticfiles import StaticFiles import uvicorn, os @@ -18,10 +17,13 @@ from api.utils.logger import logger from api.v1.routes import api_version_one from api.utils.settings import settings +from scripts.populate_db import populate_roles_and_permissions @asynccontextmanager async def lifespan(app: FastAPI): + '''Lifespan function''' + yield diff --git a/scripts/populate_db.py b/scripts/populate_db.py new file mode 100644 index 000000000..3ca165081 --- /dev/null +++ b/scripts/populate_db.py @@ -0,0 +1,60 @@ +from api.db.database import get_db +from api.v1.models.permissions.role import Role +from api.v1.models.permissions.permissions import Permission + +db = next(get_db()) + +def populate_roles_and_permissions(): + '''Function to populate database with roles and permissions''' + + # Define roles + roles = [ + {"name": "admin", "description": "Administrator with full access", "is_builtin": True}, + {"name": "user", "description": "Regular user with limited access", "is_builtin": True}, + # {"name": "Manager", "description": "Manager with management access", "is_builtin": False}, + ] + + # Define permissions + permissions = [ + {"title": "create_user"}, + {"title": "delete_user"}, + {"title": "update_user"}, + {"title": "view_user"}, + {"title": "manage_organisation"}, + {"title": "delete_organisation"}, + ] + + # Insert roles into the database + for role_data in roles: + if not db.query(Role).filter(Role.name == role_data['name']).first(): + role = Role( + name=role_data["name"], + description=role_data["description"], + is_builtin=role_data["is_builtin"] + ) + db.add(role) + db.commit() + db.refresh(role) + + # Insert permissions into the database + for perm_data in permissions: + if not db.query(Permission).filter(Permission.title == perm_data['title']).first(): + permission = Permission(title=perm_data["title"]) + db.add(permission) + db.commit() + db.refresh(permission) + + # Assign permissions to roles (example) + admin_role = db.query(Role).filter_by(name="admin").first() + user_role = db.query(Role).filter_by(name="user").first() + + if not admin_role and not user_role: + admin_permissions = db.query(Permission).all() + user_permissions = db.query(Permission).filter(Permission.title == "view_user").all() + + admin_role.permissions.extend(admin_permissions) + user_role.permissions.extend(user_permissions) + + db.commit() + + db.close()