Beginning implementation of users with supabase crossing auth and db

This commit is contained in:
blake hardee 2025-04-11 16:12:38 -04:00
parent bdc6600a3f
commit acba02dd55
4 changed files with 291 additions and 4 deletions

View File

@ -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)

View File

@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse
from fastapi.middleware.gzip import GZipMiddleware 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 = """ description = """
Welcome to the **COMPASS** RESTful Application Programming Interface. Welcome to the **COMPASS** RESTful Application Programming Interface.
@ -18,13 +18,14 @@ app = FastAPI(
health.openapi_tags, health.openapi_tags,
service.openapi_tags, service.openapi_tags,
resource.openapi_tags, resource.openapi_tags,
tag.openapi_tags tag.openapi_tags,
supabase_user.openapi_tags,
], ],
) )
app.add_middleware(GZipMiddleware) 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: for feature_api in feature_apis:
app.include_router(feature_api.api) app.include_router(feature_api.api)

View File

@ -1,4 +1,5 @@
from .user import UserService from .user import UserService
from .resource import ResourceService from .resource import ResourceService
from .tag import TagService from .tag import TagService
from .service import ServiceService from .service import ServiceService
from .supabase_user import SupabaseUserService

View File

@ -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)}"
)