Skip to content

Commit

Permalink
firebase integration
Browse files Browse the repository at this point in the history
  • Loading branch information
mmiqball committed Oct 16, 2024
1 parent 1a44016 commit fa2fc6f
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 26 deletions.
2 changes: 1 addition & 1 deletion backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def create_app():
),
}
),
{"storageBucket": os.getenv("FIREBASE_STORAGE_DEFAULT_BUCKET")},
# {"storageBucket": os.getenv("FIREBASE_STORAGE_DEFAULT_BUCKET")},
)

# from . import models, rest
Expand Down
12 changes: 12 additions & 0 deletions backend/app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from sqlalchemy.orm import sessionmaker, Session
from app.models import Base

# Create a SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=Base.metadata.bind)

def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
40 changes: 40 additions & 0 deletions backend/app/migrations/versions/59bb2488a76b_insert_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""insert roles
Revision ID: 59bb2488a76b
Revises: 4ba3479cb8df
Create Date: 2024-10-16 16:55:42.324525
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '59bb2488a76b'
down_revision: Union[str, None] = '4ba3479cb8df'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.bulk_insert(
sa.table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=False),
),
[
{'id': 1, 'name': 'participant'},
{'id': 2, 'name': 'volunteer'},
{'id': 3, 'name': 'admin'},
]
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Add auth_id to User model
Revision ID: 79de0b981dd8
Revises: 59bb2488a76b
Create Date: 2024-10-16 17:06:45.820859
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '79de0b981dd8'
down_revision: Union[str, None] = '59bb2488a76b'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('auth_id', sa.String(), nullable=True))
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'auth_id')
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Update users id to UUID with default
Revision ID: c9bc2b4d1036
Revises: 79de0b981dd8
Create Date: 2024-10-16 17:13:53.820521
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'c9bc2b4d1036'
down_revision: Union[str, None] = '79de0b981dd8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('users', 'id',
existing_type=sa.VARCHAR(),
type_=sa.UUID(),
postgresql_using="id::uuid",
server_default=sa.text("gen_random_uuid()"),
existing_nullable=False)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('users', 'id',
existing_type=sa.UUID(),
type_=sa.VARCHAR(),
postgresql_using="id::text",
server_default=None,
existing_nullable=False)
# ### end Alembic commands ###
7 changes: 5 additions & 2 deletions backend/app/models/User.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
import uuid

from .Base import Base


class User(Base):
__tablename__ = "users"
id = Column(String, primary_key=True) # UUID

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
auth_id = Column(String, nullable=True) # Firebase Auth ID
first_name = Column(String(80), nullable=True)
last_name = Column(String(80), nullable=True)
email = Column(String(120), unique=True, nullable=False)
Expand Down
1 change: 1 addition & 0 deletions backend/app/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
def init_app():
engine = create_engine(os.environ["POSTGRES_DATABASE_URL"])
Base.metadata.create_all(bind=engine)
Base.metadata.bind = engine
11 changes: 9 additions & 2 deletions backend/app/routes/user.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
from fastapi import APIRouter, Depends, HTTPException
from app.schemas.user import UserCreate, User
from app.services.implementations.user_service import UserService
from app.utilities.db_utils import get_db
from sqlalchemy.orm import Session

router = APIRouter(
prefix="/users",
tags=["users"],
)

# to do: send email verification via auth_service
# to do:
# send email verification via auth_service
# allow signup methods other than email (like sign up w Google)??

@router.post("/", response_model=User)
async def create_user(user: UserCreate, user_service: UserService = Depends()):
async def create_user(user: UserCreate, db: Session = Depends(get_db)):
user_service = UserService(db)
try:
created_user = await user_service.create_user(user)
return created_user
except HTTPException as http_ex:
raise http_ex
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
24 changes: 18 additions & 6 deletions backend/app/schemas/user.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
from pydantic import BaseModel, EmailStr, Field, validator
from enum import Enum
from uuid import UUID

class Role(str, Enum):
class UserRole(str, Enum):
PARTICIPANT = "participant"
VOLUNTEER = "volunteer"
ADMIN = "admin"

class RoleBase(BaseModel):
id: int
name: UserRole

class Config:
from_attributes = True

class UserBase(BaseModel):
first_name: str = Field(..., min_length=1, max_length=50)
last_name: str = Field(..., min_length=1, max_length=50)
email: EmailStr
role: Role
role: RoleBase

class UserCreate(UserBase):
password: str = Field(..., min_length=8)
Expand All @@ -25,20 +34,23 @@ def password_complexity(cls, v):
return v

class UserInDB(UserBase):
id: str
id: UUID
auth_id: str

class Config:
from_attributes = True

class User(UserBase):
id: str

class Config:
orm_mode = True
from_attributes = True

class UserUpdate(BaseModel):
first_name: str | None = Field(None, min_length=1, max_length=50)
last_name: str | None = Field(None, min_length=1, max_length=50)
email: EmailStr | None = None
role: Role | None = None
role: RoleBase | None = None

class Config:
orm_mode = True
from_attributes = True
3 changes: 3 additions & 0 deletions backend/app/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

from . import models
from .routes import user
from .utilities.firebase_init import initialize_firebase

load_dotenv()

app = FastAPI()
models.init_app()
initialize_firebase()

app.include_router(user.router)

Expand Down
62 changes: 48 additions & 14 deletions backend/app/services/implementations/user_service.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import firebase_admin.auth
from fastapi import HTTPException
from sqlalchemy.orm import Session
from app.models.user import User
from app.models import Role, User
from app.schemas.user import UserCreate, UserInDB
from app.services.interfaces.user_service import IUserService
from firebase_admin.exceptions import FirebaseError
import logging

class UserService(IUserService):
def __init__(self, db: Session):
self.db = db
self.logger = logging.getLogger(__name__)

async def create_user(self, user: UserCreate) -> UserInDB:
firebase_user = None
try:
Expand All @@ -20,35 +21,68 @@ async def create_user(self, user: UserCreate) -> UserInDB:
password=user.password
)

# Create user in database
all_roles = self.db.query(Role).all()
print("Available roles:", [role.name for role in all_roles])

db_role = self.db.query(Role).filter(Role.name == user.role.value).first()
if not db_role:
raise HTTPException(status_code=400, detail="Invalid role")

# create user in database
db_user = User(
first_name=user.first_name,
last_name=user.last_name,
email=user.email,
role=user.role,
role=db_role,
auth_id=firebase_user.uid
)

self.db.add(db_user)
self.db.commit()
self.db.refresh(db_user)

return UserInDB.from_orm(db_user)

except firebase_admin.auth.AuthError as firebase_error:
# If Firebase failed to add, the user wasn't added to the db so nothing to rollback
self.logger.error(f"Firebase authentication error: {str(firebase_error)}")
raise HTTPException(status_code=400, detail=str(firebase_error))
return UserInDB.model_validate(db_user)

except firebase_admin.exceptions.FirebaseError as firebase_error:
self.logger.error(f"Firebase error: {str(firebase_error)}")
if isinstance(firebase_error, firebase_admin.auth.EmailAlreadyExistsError):
raise HTTPException(status_code=409, detail="Email already exists")
else:
raise HTTPException(status_code=400, detail=str(firebase_error))
except Exception as e:
# If database insertion fails, we need to delete the Firebase user
if firebase_user:
try:
firebase_admin.auth.delete_user(firebase_user.uid)
except firebase_admin.auth.AuthError as firebase_error:
# Log the error if we couldn't delete the Firebase user
self.logger.error(f"Failed to delete Firebase user after database insertion failed. Firebase UID: {firebase_user.uid}. Error: {str(firebase_error)}")

# Rollback the database session
self.db.rollback()
self.logger.error(f"Error creating user: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))

def delete_user_by_email(self, email: str):
pass

def delete_user_by_id(self, user_id: str):
pass

def get_auth_id_by_user_id(self, user_id: str) -> str:
pass

def get_user_by_email(self, email: str):
pass

def get_user_by_id(self, user_id: str):
pass

def get_user_id_by_auth_id(self, auth_id: str) -> str:
pass

def get_user_role_by_auth_id(self, auth_id: str) -> str:
pass

def get_users(self):
pass

def update_user_by_id(self, user_id: str, user):
pass
2 changes: 1 addition & 1 deletion backend/app/services/interfaces/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def get_users(self):
pass

@abstractmethod
def create_user(self, user, auth_id=None, signup_method="PASSWORD"):
def create_user(self, user, auth_id=None):
"""
Create a user, email verification configurable
Expand Down
15 changes: 15 additions & 0 deletions backend/app/utilities/db_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy.orm import sessionmaker, Session
from app.models import Base, init_app

# Ensure the database is initialized
init_app()

# Create a SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=Base.metadata.bind)

def get_db() -> Session:
db = SessionLocal()
try:
yield db
finally:
db.close()
Loading

0 comments on commit fa2fc6f

Please sign in to comment.