diff --git a/backend/api/supabase_user.py b/backend/api/supabase_user.py new file mode 100644 index 0000000..67700e0 --- /dev/null +++ b/backend/api/supabase_user.py @@ -0,0 +1,84 @@ +from fastapi import APIRouter, Depends +from ..services import SupabaseUserService +from ..models.user_model import User, UserTypeEnum + +from typing import List + + +api = APIRouter(prefix="/api/supabaseuser") + +openapi_tags = { + "name": "Supabase Users", + "description": "User profile search and related operations.", +} + + +@api.get("/", response_model=User, tags=["Supabase Users"]) +def get_user(id: str, user_svc: SupabaseUserService = Depends()): + return user_svc.get_user(id) + + +@api.post("/", response_model=User, tags=["Supabase Users"]) +def create_user( + id: str, + email: str, + password: str, + role: UserTypeEnum, + user_svc: SupabaseUserService = Depends(), +): + subject = user_svc.get_user(id) + if subject.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {subject.uuid}") + return user_svc.create_user(email=email, password=password, role=role) + + +@api.put("/", response_model=User, tags=["Supabase Users"]) +def update_user(uuid: str, user: User, user_svc: SupabaseUserService = Depends()): + subject = user_svc.get_user(uuid) + if subject.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {subject.uuid}") + + return user_svc.update_user(user) + + +@api.delete("/", response_model=None, tags=["Supabase Users"]) +def delete_user(uuid: str, user: User, user_svc: SupabaseUserService = Depends()): + subject = user_svc.get_user(uuid) + if subject.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {subject.uuid}") + + return user_svc.delete_user(user.id) + + +@api.get("/login", response_model=List[User], tags=["Supabase Users"]) +def login_user( + email: str, + password: str, + user_svc: SupabaseUserService = Depends(), +): + return user_svc.login_user(email=email, password=password) + + +@api.get("/logout", response_model=User, tags=["Supabase Users"]) +def logout_user( + id: str, + user_svc: SupabaseUserService = Depends(), +): + return user_svc.logout_user(id=id) + + +@api.get("/forgot-password", response_model=User, tags=["Supabase Users"]) +def forgot_password( + email: str, + user_svc: SupabaseUserService = Depends(), +): + return user_svc.forgot_password(email=email) + + +@api.get("/update-password", response_model=User, tags=["Supabase Users"]) +def update_password( + id: str, + new_password: str, + user_svc: SupabaseUserService = Depends(), +): + return user_svc.update_password(new_password=new_password) diff --git a/backend/main.py b/backend/main.py index 12ecaad..e8cd5f5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse from fastapi.middleware.gzip import GZipMiddleware -from .api import user, health, service, resource, tag +from .api import user, health, service, resource, tag, supabase_user description = """ Welcome to the **COMPASS** RESTful Application Programming Interface. @@ -18,13 +18,14 @@ app = FastAPI( health.openapi_tags, service.openapi_tags, resource.openapi_tags, - tag.openapi_tags + tag.openapi_tags, + supabase_user.openapi_tags, ], ) app.add_middleware(GZipMiddleware) -feature_apis = [user, health, service, resource, tag] +feature_apis = [user, health, service, resource, tag, supabase_user] for feature_api in feature_apis: app.include_router(feature_api.api) diff --git a/backend/services/__init__.py b/backend/services/__init__.py index 4067973..3aa2471 100644 --- a/backend/services/__init__.py +++ b/backend/services/__init__.py @@ -1,4 +1,5 @@ from .user import UserService from .resource import ResourceService from .tag import TagService -from .service import ServiceService \ No newline at end of file +from .service import ServiceService +from .supabase_user import SupabaseUserService diff --git a/backend/services/supabase_user.py b/backend/services/supabase_user.py new file mode 100644 index 0000000..7b02e4c --- /dev/null +++ b/backend/services/supabase_user.py @@ -0,0 +1,201 @@ +from fastapi import Depends, HTTPException +from sqlalchemy.orm import Session +from sqlalchemy import select +from supabase import create_client, Client +import os + +from ..database import db_session +from ..entities.user_entity import UserEntity +from ..models.user_model import User +from backend.services.user import UserService +from ..models.enum_for_models import ProgramTypeEnum, UserTypeEnum + + +class SupabaseUserService: + def __init__(self, session: Session = Depends(db_session)): + self._session = session + self.client = create_client( + os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY") + ) + + def get_user(self, id: str) -> User: + """ + Gets a user by id from the database + Returns: A User Pydantic model + """ + query = select(UserEntity).where(UserEntity.uuid == id) + result = self._session.execute(query) + user_entity = result.scalar_one_or_none() + + if user_entity is None: + raise HTTPException( + status_code=404, detail=f"No user found with matching id: {id}" + ) + + return user_entity.to_model() + + def create_user(self, email: str, password: str, role: UserTypeEnum) -> User: + """ + Creates a new User Entity and adds to database + Args: email and password + Returns: User model + """ + try: + if not email and not password: + raise HTTPException( + status_code=400, detail="Email and password required" + ) + + # Create user in Supabase Auth + auth_response = self.client.auth.sign_up( + {"email": email, "password": password} + ) + + if auth_response.user is None: + raise HTTPException( + status_code=400, + detail=f"Supabase auth error: {auth_response}", + ) + + supabase_user = auth_response.user + + # Create user in database using existing UserService + randomUserName = email.split("@")[0] # Simple username generation + user_model = User( + uuid=supabase_user.id, + email=email, + username=randomUserName, + role=role, + program=[ProgramTypeEnum.DOMESTIC], + experience=0, + group="volunteer", + ) + user_entity = UserEntity.from_model(user_model) + # add new user to table + self._session.add(user_entity) + self._session.commit() + + except: + raise Exception(f"Failed to create user") + + return user_entity.to_model() + + def delete_user(self, user_id: int) -> None: + """ + Delete a user + Args: the user ID to delete + Returns: none + """ + + obj = self._session.get(UserEntity, user_id) # Get user entity from database + + if obj is None: + raise HTTPException(status_code=404, detail=f"No matching user found") + + try: + # Delete user from Supabase Auth + # This is commented out because it may not be necessary to delete the user from Supabase Auth + # self.client.auth.admin.delete_user(obj.uuid, should_soft_delete=False) + + # Delete user from database + self._session.delete(obj) + self._session.commit() + except Exception as e: + self._session.rollback() + raise HTTPException( + status_code=500, detail=f"Failed to delete user: {str(e)}" + ) + + def update_user(self, user: User) -> User: + """ + Updates a user + Args: User to be updated + Returns: The updated User + """ + obj = self._session.get(UserEntity, user.id) + + if obj is None: + raise Exception(f"No matching user found") + + obj.username = user.username if user.username else obj.username + obj.role = user.role if user.role else obj.role + obj.email = user.email if user.email else obj.email + obj.program = user.program if user.program else obj.program + obj.experience = user.experience if user.experience else obj.experience + obj.group = user.group if user.group else obj.group + + self._session.commit() + + return obj.to_model() + + def get_user_by_email(self, email: str) -> User: + """ + Gets a user by email from the database + Returns: A User Pydantic model + """ + query = select(UserEntity).where(UserEntity.email == email) + result = self._session.execute(query) + user_entity = result.scalar_one_or_none() + + if user_entity is None: + raise HTTPException( + status_code=404, detail=f"No user found with email: {email}" + ) + + return user_entity.to_model() + + def login_user(self, email: str, password: str): + """ + Login user with Supabase Auth + Returns: Auth session data + """ + try: + auth_response = self.client.auth.sign_in_with_password( + {"email": email, "password": password} + ) + + if auth_response.error: + raise HTTPException(status_code=401, detail="Invalid login credentials") + + # You can then fetch the user from your database if needed + user = self.get_user(auth_response.user.id) + + return {"user": user, "session": auth_response.session} + except Exception as e: + if isinstance(e, HTTPException): + raise e + raise HTTPException(status_code=500, detail=f"Login failed: {str(e)}") + + def logout_user(self): + """ + Logout user from Supabase Auth + Returns: None + """ + try: + self.client.auth.sign_out() + except Exception as e: + raise HTTPException(status_code=500, detail=f"Logout failed: {str(e)}") + + def forgot_password(self, email: str): + """ + Send a password reset email to the user + Returns: None + """ + try: + response = self.client.auth.reset_password_for_email(email) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error sending reset email: {str(e)}" + ) + + def update_password(self, password: str): + """ + Update the user's password + Returns: None + """ + try: + response = self.client.auth.update_user({"password": password}) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Error updating password: {str(e)}" + )