Skip to content

Commit 498eb8e

Browse files
committed
implemented crud operations for users
1 parent 3000fc2 commit 498eb8e

File tree

4 files changed

+204
-11
lines changed

4 files changed

+204
-11
lines changed

backend/app/models/User.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import uuid
22

3-
from sqlalchemy import Column, ForeignKey, Integer, String
3+
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
44
from sqlalchemy.dialects.postgresql import UUID
55
from sqlalchemy.orm import relationship
66

@@ -15,5 +15,6 @@ class User(Base):
1515
email = Column(String(120), unique=True, nullable=False)
1616
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
1717
auth_id = Column(String, nullable=False)
18+
approved = Column(Boolean, default=False)
1819

1920
role = relationship("Role")

backend/app/routes/user.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
from typing import List
2+
from uuid import UUID
3+
14
from fastapi import APIRouter, Depends, HTTPException
25

36
from app.middleware.auth import has_roles
4-
from app.schemas.user import UserCreateRequest, UserCreateResponse, UserRole
7+
from app.schemas.user import UserCreateRequest, UserCreateResponse, UserListResponse, UserResponse, UserRole, UserUpdateRequest
58
from app.services.implementations.user_service import UserService
69
from app.utilities.service_utils import get_user_service
710

@@ -28,3 +31,65 @@ async def create_user(
2831
raise http_ex
2932
except Exception as e:
3033
raise HTTPException(status_code=500, detail=str(e))
34+
35+
36+
# admin only get all users
37+
@router.get("/", response_model=UserListResponse)
38+
async def get_users(
39+
user_service: UserService = Depends(get_user_service),
40+
authorized: bool = has_roles([UserRole.ADMIN]),
41+
):
42+
try:
43+
users = user_service.get_users()
44+
return UserListResponse(users=users, total=len(users))
45+
except HTTPException as http_ex:
46+
raise http_ex
47+
except Exception as e:
48+
raise HTTPException(status_code=500, detail=str(e))
49+
50+
51+
# admin only get user by ID
52+
@router.get("/{user_id}", response_model=UserResponse)
53+
async def get_user(
54+
user_id: str,
55+
user_service: UserService = Depends(get_user_service),
56+
authorized: bool = has_roles([UserRole.ADMIN]),
57+
):
58+
try:
59+
return user_service.get_user_by_id(user_id)
60+
except HTTPException as http_ex:
61+
raise http_ex
62+
except Exception as e:
63+
raise HTTPException(status_code=500, detail=str(e))
64+
65+
66+
# admin only update user (mainly for approvals)
67+
@router.put("/{user_id}", response_model=UserResponse)
68+
async def update_user(
69+
user_id: str,
70+
user_update: UserUpdateRequest,
71+
user_service: UserService = Depends(get_user_service),
72+
authorized: bool = Depends(has_roles([UserRole.ADMIN])),
73+
):
74+
try:
75+
return user_service.update_user_by_id(user_id, user_update)
76+
except HTTPException as http_ex:
77+
raise http_ex
78+
except Exception as e:
79+
raise HTTPException(status_code=500, detail=str(e))
80+
81+
82+
# admin only delete user
83+
@router.delete("/{user_id}")
84+
async def delete_user(
85+
user_id: str,
86+
user_service: UserService = Depends(get_user_service),
87+
authorized: bool = has_roles([UserRole.ADMIN]),
88+
):
89+
try:
90+
user_service.delete_user_by_id(user_id)
91+
return {"message": "User deleted successfully"}
92+
except HTTPException as http_ex:
93+
raise http_ex
94+
except Exception as e:
95+
raise HTTPException(status_code=500, detail=str(e))

backend/app/schemas/user.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"""
55

66
from enum import Enum
7-
from typing import Optional
7+
from typing import List, Optional
88
from uuid import UUID
99

1010
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator
@@ -73,6 +73,18 @@ def validate_password(cls, password: Optional[str], info):
7373
return password
7474

7575

76+
class UserUpdateRequest(BaseModel):
77+
"""
78+
Request schema for user updates, all fields optional
79+
"""
80+
81+
first_name: Optional[str] = Field(None, min_length=1, max_length=50)
82+
last_name: Optional[str] = Field(None, min_length=1, max_length=50)
83+
email: Optional[EmailStr] = None
84+
role: Optional[UserRole] = None
85+
approved: Optional[bool] = None
86+
87+
7688
class UserCreateResponse(BaseModel):
7789
"""
7890
Response schema for user creation, maps directly from ORM User object.
@@ -84,6 +96,44 @@ class UserCreateResponse(BaseModel):
8496
email: EmailStr
8597
role_id: int
8698
auth_id: str
99+
approved: bool
87100

88101
# from_attributes enables automatic mapping from SQLAlchemy model to Pydantic model
89102
model_config = ConfigDict(from_attributes=True)
103+
104+
105+
class UserResponse(BaseModel):
106+
"""
107+
Response schema for user data including role information
108+
"""
109+
110+
id: UUID
111+
first_name: str
112+
last_name: str
113+
email: EmailStr
114+
role_id: int
115+
auth_id: str
116+
approved: bool
117+
role: "RoleResponse"
118+
119+
model_config = ConfigDict(from_attributes=True)
120+
121+
122+
class RoleResponse(BaseModel):
123+
"""
124+
Response schema for role data
125+
"""
126+
127+
id: int
128+
name: str
129+
130+
model_config = ConfigDict(from_attributes=True)
131+
132+
133+
class UserListResponse(BaseModel):
134+
"""
135+
Response schema for listing users
136+
"""
137+
138+
users: List[UserResponse]
139+
total: int

backend/app/services/implementations/user_service.py

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import logging
2+
from typing import List
3+
from uuid import UUID
24

35
import firebase_admin.auth
46
from fastapi import HTTPException
@@ -10,7 +12,9 @@
1012
SignUpMethod,
1113
UserCreateRequest,
1214
UserCreateResponse,
15+
UserResponse,
1316
UserRole,
17+
UserUpdateRequest,
1418
)
1519
from app.utilities.constants import LOGGER_NAME
1620

@@ -79,10 +83,38 @@ async def create_user(self, user: UserCreateRequest) -> UserCreateResponse:
7983
raise HTTPException(status_code=500, detail=str(e))
8084

8185
def delete_user_by_email(self, email: str):
82-
pass
86+
try:
87+
db_user = self.db.query(User).filter(User.email == email).first()
88+
if not db_user:
89+
raise HTTPException(status_code=404, detail="User not found")
90+
91+
self.db.delete(db_user)
92+
self.db.commit()
8393

94+
except HTTPException:
95+
raise
96+
except Exception as e:
97+
self.db.rollback()
98+
self.logger.error(f"Error deleting user with email {email}: {str(e)}")
99+
raise HTTPException(status_code=500, detail=str(e))
100+
84101
def delete_user_by_id(self, user_id: str):
85-
pass
102+
try:
103+
db_user = self.db.query(User).filter(User.id == UUID(user_id)).first()
104+
if not db_user:
105+
raise HTTPException(status_code=404, detail="User not found")
106+
107+
self.db.delete(db_user)
108+
self.db.commit()
109+
110+
except ValueError:
111+
raise HTTPException(status_code=400, detail="Invalid user ID format")
112+
except HTTPException:
113+
raise
114+
except Exception as e:
115+
self.db.rollback()
116+
self.logger.error(f"Error deleting user {user_id}: {str(e)}")
117+
raise HTTPException(status_code=500, detail=str(e))
86118

87119
def get_user_id_by_auth_id(self, auth_id: str) -> str:
88120
"""Get user ID for a user by their Firebase auth_id"""
@@ -97,8 +129,19 @@ def get_user_by_email(self, email: str):
97129
raise ValueError(f"User with email {email} not found")
98130
return user
99131

100-
def get_user_by_id(self, user_id: str):
101-
pass
132+
def get_user_by_id(self, user_id: str) -> UserResponse:
133+
try:
134+
user = self.db.query(User).join(Role).filter(User.id == UUID(user_id)).first()
135+
if not user:
136+
raise HTTPException(status_code=404, detail="User not found")
137+
return UserResponse.model_validate(user)
138+
except ValueError:
139+
raise HTTPException(status_code=400, detail="Invalid user ID format")
140+
except HTTPException:
141+
raise
142+
except Exception as e:
143+
self.logger.error(f"Error retrieving user {user_id}: {str(e)}")
144+
raise HTTPException(status_code=500, detail=str(e))
102145

103146
def get_auth_id_by_user_id(self, user_id: str) -> str:
104147
"""Get Firebase auth_id for a user"""
@@ -114,8 +157,42 @@ def get_user_role_by_auth_id(self, auth_id: str) -> str:
114157
raise ValueError(f"User with auth_id {auth_id} not found")
115158
return user.role.name
116159

117-
def get_users(self):
118-
pass
160+
def get_users(self) -> List[UserResponse]:
161+
try:
162+
users = self.db.query(User).join(Role).all()
163+
return [UserResponse.model_validate(user) for user in users]
164+
except Exception as e:
165+
self.logger.error(f"Error retrieving users: {str(e)}")
166+
raise HTTPException(status_code=500, detail=str(e))
167+
168+
def update_user_by_id(self, user_id: str, user_update: UserUpdateRequest) -> UserResponse:
169+
try:
170+
db_user = self.db.query(User).filter(User.id == UUID(user_id)).first()
171+
if not db_user:
172+
raise HTTPException(status_code=404, detail="User not found")
173+
174+
# update provided fields only
175+
update_data = user_update.model_dump(exclude_unset=True)
176+
177+
# handle role conversion if role is being updated
178+
if "role" in update_data:
179+
update_data["role_id"] = UserRole.to_role_id(update_data.pop("role"))
119180

120-
def update_user_by_id(self, user_id: str, user):
121-
pass
181+
for field, value in update_data.items():
182+
setattr(db_user, field, value)
183+
184+
self.db.commit()
185+
self.db.refresh(db_user)
186+
187+
# return user with role information
188+
updated_user = self.db.query(User).join(Role).filter(User.id == UUID(user_id)).first()
189+
return UserResponse.model_validate(updated_user)
190+
191+
except ValueError:
192+
raise HTTPException(status_code=400, detail="Invalid user ID format")
193+
except HTTPException:
194+
raise
195+
except Exception as e:
196+
self.db.rollback()
197+
self.logger.error(f"Error updating user {user_id}: {str(e)}")
198+
raise HTTPException(status_code=500, detail=str(e))

0 commit comments

Comments
 (0)