diff --git a/apps/api/src/db/organizations.py b/apps/api/src/db/organizations.py index 3cd147e6..a1170dab 100644 --- a/apps/api/src/db/organizations.py +++ b/apps/api/src/db/organizations.py @@ -12,10 +12,12 @@ class OrganizationBase(SQLModel): class Organization(OrganizationBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) - org_uuid: str - creation_date: str - update_date: str + org_uuid: str = "" + creation_date: str = "" + update_date: str = "" +class OrganizationUpdate(OrganizationBase): + org_id: int class OrganizationCreate(OrganizationBase): pass diff --git a/apps/api/src/db/users.py b/apps/api/src/db/users.py index 726cedfc..e8cc99fc 100644 --- a/apps/api/src/db/users.py +++ b/apps/api/src/db/users.py @@ -33,6 +33,8 @@ class UserUpdatePassword(SQLModel): class UserRead(UserBase): id: int +class PublicUser(UserRead): + pass class User(UserBase, table=True): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/apps/api/src/routers/orgs.py b/apps/api/src/routers/orgs.py index f3e2928f..9f8c0aa1 100644 --- a/apps/api/src/routers/orgs.py +++ b/apps/api/src/routers/orgs.py @@ -1,63 +1,120 @@ - from fastapi import APIRouter, Depends, Request, UploadFile +from sqlmodel import Session +from src.db.users import PublicUser +from src.db.organizations import OrganizationCreate, OrganizationUpdate +from src.core.events.database import get_db_session from src.security.auth import get_current_user -from src.services.orgs.orgs import Organization, create_org, delete_org, get_organization, get_organization_by_slug, get_orgs_by_user, update_org, update_org_logo -from src.services.users.users import PublicUser, User +from src.services.orgs.orgs import ( + create_org, + delete_org, + get_organization, + get_organization_by_slug, + get_orgs_by_user, + update_org, + update_org_logo, +) router = APIRouter() @router.post("/") -async def api_create_org(request: Request, org_object: Organization, current_user: PublicUser = Depends(get_current_user)): +async def api_create_org( + request: Request, + org_object: OrganizationCreate, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Create new organization """ - return await create_org(request, org_object, current_user) + return await create_org(request, org_object, current_user, db_session) @router.get("/{org_id}") -async def api_get_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_get_org( + request: Request, + org_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get single Org by ID """ - return await get_organization(request, org_id) + return await get_organization(request, org_id, db_session) @router.get("/slug/{org_slug}") -async def api_get_org_by_slug(request: Request, org_slug: str, current_user: User = Depends(get_current_user)): +async def api_get_org_by_slug( + request: Request, + org_slug: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get single Org by Slug """ - return await get_organization_by_slug(request, org_slug) + return await get_organization_by_slug(request, org_slug, db_session) + @router.put("/{org_id}/logo") -async def api_update_org_logo(request: Request, org_id: str, logo_file:UploadFile, current_user: PublicUser = Depends(get_current_user)): +async def api_update_org_logo( + request: Request, + org_id: str, + logo_file: UploadFile, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get single Org by Slug """ - return await update_org_logo(request=request,logo_file=logo_file, org_id=org_id, current_user=current_user) + return await update_org_logo( + request=request, + logo_file=logo_file, + org_id=org_id, + current_user=current_user, + db_session=db_session, + ) + @router.get("/user/page/{page}/limit/{limit}") -async def api_user_orgs(request: Request, page: int, limit: int, current_user: PublicUser = Depends(get_current_user)): +async def api_user_orgs( + request: Request, + page: int, + limit: int, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Get orgs by page and limit by user """ - return await get_orgs_by_user(request, current_user.user_id, page, limit) + return await get_orgs_by_user( + request, db_session, str(current_user.id), page, limit + ) -@router.put("/{org_id}") -async def api_update_org(request: Request, org_object: Organization, org_id: str, current_user: PublicUser = Depends(get_current_user)): +@router.put("/") +async def api_update_org( + request: Request, + org_object: OrganizationUpdate, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Update Org by ID """ - return await update_org(request, org_object, org_id, current_user) + return await update_org(request, org_object, current_user, db_session) @router.delete("/{org_id}") -async def api_delete_org(request: Request, org_id: str, current_user: PublicUser = Depends(get_current_user)): +async def api_delete_org( + request: Request, + org_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): """ Delete Org by ID """ - return await delete_org(request, org_id, current_user) + return await delete_org(request, org_id, current_user, db_session) diff --git a/apps/api/src/services/orgs/orgs.py b/apps/api/src/services/orgs/orgs.py index a65a4f63..a24737db 100644 --- a/apps/api/src/services/orgs/orgs.py +++ b/apps/api/src/services/orgs/orgs.py @@ -1,230 +1,230 @@ +from datetime import datetime import json +from operator import or_ from typing import Literal from uuid import uuid4 +from sqlmodel import Session, select +from src.db.users import UserRead, PublicUser +from src.db.user_organizations import UserOrganization +from src.db.organizations import ( + Organization, + OrganizationCreate, + OrganizationRead, + OrganizationUpdate, +) from src.security.rbac.rbac import ( authorization_verify_based_on_roles, authorization_verify_if_user_is_anon, ) from src.services.orgs.logos import upload_org_logo -from src.services.orgs.schemas.orgs import ( - Organization, - OrganizationInDB, - PublicOrganization, -) -from src.services.users.schemas.users import UserOrganization -from src.services.users.users import PublicUser from fastapi import HTTPException, UploadFile, status, Request -async def get_organization(request: Request, org_id: str): - orgs = request.app.db["organizations"] +async def get_organization(request: Request, org_id: str, db_session: Session): + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) - org = await orgs.find_one({"org_id": org_id}) + org = result.first() if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + status_code=404, + detail="Organization not found", ) - org = PublicOrganization(**org) return org -async def get_organization_by_slug(request: Request, org_slug: str): - orgs = request.app.db["organizations"] +async def get_organization_by_slug( + request: Request, org_slug: str, db_session: Session +): + statement = select(Organization).where(Organization.slug == org_slug) + result = db_session.exec(statement) - org = await orgs.find_one({"slug": org_slug}) + org = result.first() if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + status_code=404, + detail="Organization not found", ) - org = PublicOrganization(**org) return org async def create_org( - request: Request, org_object: Organization, current_user: PublicUser + request: Request, + org_object: OrganizationCreate, + current_user: PublicUser, + db_session: Session, ): - orgs = request.app.db["organizations"] - user = request.app.db["users"] + statement = select(Organization).where(Organization.slug == org_object.slug) + result = db_session.exec(statement) - # find if org already exists using name - isOrgAvailable = await orgs.find_one({"slug": org_object.slug}) + org = result.first() - if isOrgAvailable: + if org: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Organization already exists", + ) + + org = Organization.from_orm(org_object) + + # Complete the org object + org.org_uuid = f"org_{uuid4()}" + org.creation_date = str(datetime.now()) + org.update_date = str(datetime.now()) + + db_session.add(org) + db_session.commit() + db_session.refresh(org) + + # Link user to org + user_org = UserOrganization( + user_id=int(current_user.id), + org_id=int(org.id is not None), + role_id=1, + creation_date=str(datetime.now()), + update_date=str(datetime.now()), + ) + + db_session.add(user_org) + db_session.commit() + db_session.refresh(user_org) + + return OrganizationRead.from_orm(org) + + +async def update_org( + request: Request, + org_object: OrganizationUpdate, + current_user: PublicUser, + db_session: Session, +): + statement = select(Organization).where(Organization.id == org_object.org_id) + result = db_session.exec(statement) + + org = result.first() + + if not org: + raise HTTPException( + status_code=404, + detail="Organization slug not found", + ) + + org = Organization.from_orm(org_object) + + # Verify if the new slug is already in use + statement = select(Organization).where(Organization.slug == org_object.slug) + result = db_session.exec(statement) + + slug_available = result.first() + + if slug_available: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Organization slug already exists", ) - # generate org_id with uuid4 - org_id = str(f"org_{uuid4()}") + # Remove the org_id from the org_object + del org_object.org_id - # force lowercase slug - org_object.slug = org_object.slug.lower() + # Update only the fields that were passed in + for var, value in vars(org_object).items(): + if value is not None: + setattr(org, var, value) - org = OrganizationInDB( - org_id=org_id, **org_object.dict() - ) + # Complete the org object + org.update_date = str(datetime.now()) - org_in_db = await orgs.insert_one(org.dict()) + db_session.add(org) + db_session.commit() + db_session.refresh(org) - user_organization: UserOrganization = UserOrganization( - org_id=org_id, org_role="owner" - ) - - # add org to user - await user.update_one( - {"user_id": current_user.user_id}, - {"$addToSet": {"orgs": user_organization.dict()}}, - ) - - # add role admin to org - await user.update_one( - {"user_id": current_user.user_id}, - {"$addToSet": {"roles": {"org_id": org_id, "role_id": "role_admin"}}}, - ) - - if not org_in_db: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) - - return org.dict() - - -async def update_org( - request: Request, org_object: Organization, org_id: str, current_user: PublicUser -): - # verify org rights - await verify_org_rights(request, org_id, current_user, "update") - - orgs = request.app.db["organizations"] - - await orgs.find_one({"org_id": org_id}) - - updated_org = OrganizationInDB(org_id=org_id, **org_object.dict()) - - # update org - await orgs.update_one({"org_id": org_id}, {"$set": updated_org.dict()}) - - return updated_org.dict() + return org async def update_org_logo( - request: Request, logo_file: UploadFile, org_id: str, current_user: PublicUser + request: Request, + logo_file: UploadFile, + org_id: str, + current_user: PublicUser, + db_session: Session, ): - # verify org rights - await verify_org_rights(request, org_id, current_user, "update") + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) - orgs = request.app.db["organizations"] + org = result.first() - await orgs.find_one({"org_id": org_id}) + if not org: + raise HTTPException( + status_code=404, + detail="Organization not found", + ) + # Upload logo name_in_disk = await upload_org_logo(logo_file, org_id) - # update org - await orgs.update_one({"org_id": org_id}, {"$set": {"logo": name_in_disk}}) + # Update org + org.logo_image = name_in_disk + + # Complete the org object + org.update_date = str(datetime.now()) + + db_session.add(org) + db_session.commit() + db_session.refresh(org) return {"detail": "Logo updated"} -async def delete_org(request: Request, org_id: str, current_user: PublicUser): - await verify_org_rights(request, org_id, current_user, "delete") +async def delete_org( + request: Request, org_id: str, current_user: PublicUser, db_session: Session +): + statement = select(Organization).where(Organization.id == org_id) + result = db_session.exec(statement) - orgs = request.app.db["organizations"] - - org = await orgs.find_one({"org_id": org_id}) + org = result.first() if not org: raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" + status_code=404, + detail="Organization not found", ) - isDeleted = await orgs.delete_one({"org_id": org_id}) + db_session.delete(org) + db_session.commit() - # remove org from all users - users = request.app.db["users"] - await users.update_many({}, {"$pull": {"orgs": {"org_id": org_id}}}) + # Delete links to org + statement = select(UserOrganization).where(UserOrganization.org_id == org_id) + result = db_session.exec(statement) - if isDeleted: - return {"detail": "Org deleted"} - else: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="Unavailable database", - ) + user_orgs = result.all() + + for user_org in user_orgs: + db_session.delete(user_org) + db_session.commit() + + db_session.refresh(org) + + return {"detail": "Organization deleted"} async def get_orgs_by_user( - request: Request, user_id: str, page: int = 1, limit: int = 10 -): - orgs = request.app.db["organizations"] - user = request.app.db["users"] - - if user_id == "anonymous": - # raise error - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="User not logged in" - ) - - # get user orgs - user_orgs = await user.find_one({"user_id": user_id}) - - org_ids: list[UserOrganization] = [] - - for org in user_orgs["orgs"]: - if ( - org["org_role"] == "owner" - or org["org_role"] == "editor" - or org["org_role"] == "member" - ): - org_ids.append(org["org_id"]) - - # find all orgs where org_id is in org_ids array - - all_orgs = ( - orgs.find({"org_id": {"$in": org_ids}}) - .sort("name", 1) - .skip(10 * (page - 1)) - .limit(100) - ) - - return [ - json.loads(json.dumps(org, default=str)) - for org in await all_orgs.to_list(length=100) - ] - - -#### Security #################################################### - - -async def verify_org_rights( request: Request, - org_id: str, - current_user: PublicUser, - action: Literal["create", "read", "update", "delete"], + db_session: Session, + user_id: str, + page: int = 1, + limit: int = 10, ): - orgs = request.app.db["organizations"] - users = request.app.db["users"] - - user = await users.find_one({"user_id": current_user.user_id}) - - org = await orgs.find_one({"org_id": org_id}) - - if not org: - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist" - ) - - await authorization_verify_if_user_is_anon(current_user.user_id) - - await authorization_verify_based_on_roles( - request, current_user.user_id, action, user["roles"], org_id + statement = ( + select(Organization) + .join(UserOrganization) + .where(Organization.id == UserOrganization.org_id) ) + result = db_session.exec(statement) + orgs = result.all() -#### Security #################################################### + return orgs diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 08e38700..1a9e9d9d 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -3,8 +3,8 @@ from typing import List, Literal, Optional from uuid import uuid4 from fastapi import HTTPException, Request, status from pydantic import BaseModel +from src.services.orgs.schemas.orgs import PublicOrganization from src.services.courses.chapters import get_coursechapters_meta -from src.services.orgs.orgs import PublicOrganization from src.services.users.users import PublicUser