Skip to content

Commit 88b2b5d

Browse files
kingMonkehludavidcagithub-code-quality[bot]ColinToft
authored
[F4KRP-99] Restructure User System (#67)
* update models and run migrations * Move address from user to driver * migration * add user service and update driver service 😭 * IT WORKS DWD AOWD OAWJD OAWDJ OAWJD OAWJDO WAJDOA * fix logout button 😭 * ITS WORKING 😭 * add admin to seed script * tests fix maybe ? * lint * format * Fix type checking issues * Edited Claude yml to debug * removed secret * fix for pull request finding 'Unused local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Order migration after existing migrations * Implement claude suggestions + fix lint * Fixed up small stuff, verified completion * Fix lint issues --------- Co-authored-by: David Lu <davidjylu7@gmail.com> Co-authored-by: David Lu <151972620+ludavidca@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> Co-authored-by: Colin Toft <colintoft@uwblueprint.org>
1 parent 406d82d commit 88b2b5d

23 files changed

+881
-255
lines changed

.env.example

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
POSTGRES_DB_DEV=
2+
POSTGRES_DB_TEST=
3+
POSTGRES_USER=
4+
POSTGRES_PASSWORD=
5+
DB_HOST=
6+
7+
APP_ENV=
8+
APP_NAME=
9+
10+
ADMIN_AUTH_ID=
11+
12+
# Firebase config
13+
FIREBASE_WEB_API_KEY=
14+
FIREBASE_REQUEST_URI=
15+
FIREBASE_PROJECT_ID=
16+
FIREBASE_SVC_ACCOUNT_PRIVATE_KEY_ID=
17+
FIREBASE_SVC_ACCOUNT_PRIVATE_KEY=
18+
FIREBASE_SVC_ACCOUNT_CLIENT_EMAIL=
19+
FIREBASE_SVC_ACCOUNT_CLIENT_ID=
20+
FIREBASE_SVC_ACCOUNT_AUTH_URI=
21+
FIREBASE_SVC_ACCOUNT_TOKEN_URI=
22+
FIREBASE_SVC_ACCOUNT_AUTH_PROVIDER_X509_CERT_URL=
23+
FIREBASE_SVC_ACCOUNT_CLIENT_X509_CERT_URL=
24+
25+
# Email config
26+
MAILER_REFRESH_TOKEN=
27+
MAILER_CLIENT_ID=
28+
MAILER_CLIENT_SECRET=
29+
MAILER_USER=
30+
31+
#Google Maps API
32+
GOOGLE_MAPS_API_KEY=

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ SELECT * FROM users; # Run SQL queries
465465

466466
```bash
467467
# Populate database with randomized test data
468-
docker-compose exec backend python app/seed_database.py
468+
docker-compose exec backend python -m app.seed_database
469469
```
470470

471471
## Version Control Guide

backend/python/app/dependencies/auth.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
from app.services.implementations.auth_service import AuthService
1313
from app.services.implementations.driver_service import DriverService
1414
from app.services.implementations.email_service import EmailService
15+
from app.services.implementations.user_service import UserService
1516

1617
# Initialize services
1718
logger = logging.getLogger(__name__)
1819
driver_service = DriverService(logger)
20+
user_service = UserService(logger)
1921
email_service = EmailService(
2022
logger,
2123
{
@@ -27,7 +29,7 @@
2729
settings.mailer_user,
2830
"Food4Kids",
2931
)
30-
auth_service = AuthService(logger, driver_service, email_service)
32+
auth_service = AuthService(logger, user_service, driver_service, email_service)
3133

3234
# Security scheme
3335
security = HTTPBearer()
@@ -170,16 +172,16 @@ def get_current_user_email(access_token: str = Depends(get_access_token)) -> str
170172
) from e
171173

172174

173-
async def get_current_database_driver_id(
175+
async def get_current_database_user_id(
174176
access_token: str = Depends(get_access_token),
175177
session: AsyncSession = Depends(get_session),
176178
) -> UUID:
177179
"""
178-
Get the current database driver ID from the access token
180+
Get the current database user ID from the access token
179181
180182
:param access_token: JWT access token
181183
:param session: Database session
182-
:return: Database driver ID (UUID)
184+
:return: Database user ID (UUID)
183185
"""
184186
try:
185187
decoded_token: dict[str, str] = firebase_admin.auth.verify_id_token(
@@ -188,16 +190,16 @@ async def get_current_database_driver_id(
188190
firebase_uid = decoded_token["uid"]
189191

190192
# Convert Firebase UID to database driver ID
191-
database_driver_id = await driver_service.get_driver_id_by_auth_id(
193+
database_user_id = await user_service.get_user_id_by_auth_id(
192194
session, firebase_uid
193195
)
194-
if database_driver_id is None:
196+
if database_user_id is None:
195197
raise HTTPException(
196-
status_code=status.HTTP_401_UNAUTHORIZED, detail="Driver not found"
198+
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
197199
)
198-
return database_driver_id
200+
return database_user_id
199201
except Exception as e:
200-
logger.error(f"Failed to get database driver ID from access token: {e}")
202+
logger.error(f"Failed to get database user ID from access token: {e}")
201203
raise HTTPException(
202204
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token"
203205
) from e

backend/python/app/dependencies/services.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from app.services.implementations.route_group_service import RouteGroupService
2323
from app.services.implementations.scheduler_service import SchedulerService
2424
from app.services.implementations.simple_entity_service import SimpleEntityService
25+
from app.services.implementations.user_service import UserService
2526
from app.services.protocols.routing_algorithm import RoutingAlgorithmProtocol
2627
from app.utilities.google_maps_client import GoogleMapsClient
2728

@@ -49,6 +50,13 @@ def get_email_service() -> EmailService:
4950
)
5051

5152

53+
@lru_cache
54+
def get_user_service() -> UserService:
55+
"""Get user service instance"""
56+
logger = get_logger()
57+
return UserService(logger)
58+
59+
5260
@lru_cache
5361
def get_driver_service() -> DriverService:
5462
"""Get driver service instance"""
@@ -57,12 +65,13 @@ def get_driver_service() -> DriverService:
5765

5866

5967
def get_auth_service(
68+
user_service: UserService = Depends(get_user_service),
6069
driver_service: DriverService = Depends(get_driver_service),
6170
email_service: EmailService = Depends(get_email_service),
6271
) -> AuthService:
6372
"""Get auth service instance"""
6473
logger = get_logger()
65-
return AuthService(logger, driver_service, email_service)
74+
return AuthService(logger, user_service, driver_service, email_service)
6675

6776

6877
@lru_cache

backend/python/app/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ def init_app(_app: Any | None = None) -> None:
9393
from .route_group_membership import RouteGroupMembership # noqa: F401
9494
from .route_stop import RouteStop # noqa: F401
9595
from .simple_entity import SimpleEntity # noqa: F401
96+
from .system_settings import SystemSettings # noqa: F401
97+
from .user import User # noqa: F401
9698

9799
init_database()
98100

backend/python/app/models/admin.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import datetime
21
from uuid import UUID, uuid4
32

43
from pydantic import EmailStr, field_validator
5-
from sqlmodel import Field, SQLModel
4+
from sqlmodel import Field, Relationship, SQLModel
65

6+
from app.models.user import User
77
from app.utilities.utils import validate_phone
88

99
from .base import BaseModel
@@ -12,12 +12,8 @@
1212
class AdminBase(SQLModel):
1313
"""Shared fields between table and API models"""
1414

15-
admin_name: str = Field(min_length=1, max_length=100, nullable=False)
16-
default_cap: int | None = Field(default=None)
15+
receive_email_notifications: bool = Field(default=True, nullable=False)
1716
admin_phone: str = Field(min_length=1, max_length=100, nullable=False)
18-
admin_email: EmailStr = Field(nullable=False)
19-
route_start_time: datetime.time | None = Field(default=None)
20-
warehouse_location: str | None = Field(default=None, min_length=1)
2117

2218
@field_validator("admin_phone")
2319
@classmethod
@@ -32,26 +28,37 @@ class Admin(AdminBase, BaseModel, table=True):
3228
__tablename__ = "admin_info"
3329

3430
admin_id: UUID = Field(default_factory=uuid4, primary_key=True)
31+
user_id: UUID = Field(foreign_key="users.user_id", unique=True, nullable=False)
32+
33+
user: User = Relationship()
3534

3635

3736
class AdminCreate(AdminBase):
3837
"""Create request model"""
3938

39+
user_id: UUID
4040
pass
4141

4242

4343
class AdminRead(AdminBase):
4444
"""Read response model"""
4545

4646
admin_id: UUID
47+
user_id: UUID
48+
49+
# pulled from User
50+
name: str
51+
email: EmailStr
52+
auth_id: str
53+
role: str
4754

4855

4956
class AdminUpdate(SQLModel):
5057
"""Update request model - all optional"""
5158

52-
admin_name: str | None = Field(default=None, min_length=1, max_length=100)
53-
default_cap: int | None = Field(default=None)
59+
# admin-specific
5460
admin_phone: str | None = Field(default=None, min_length=1, max_length=100)
55-
admin_email: EmailStr | None = Field(default=None)
56-
route_start_time: datetime.time | None = Field(default=None)
57-
warehouse_location: str | None = Field(default=None, min_length=1)
61+
62+
# user fields
63+
name: str | None = Field(default=None, min_length=1, max_length=255)
64+
email: EmailStr | None = Field(default=None)

backend/python/app/models/driver.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
from uuid import UUID, uuid4
22

33
from pydantic import EmailStr, field_validator
4-
from sqlmodel import Field, SQLModel
4+
from sqlmodel import Field, Relationship, SQLModel
55

6+
from app.models.user import User
67
from app.utilities.utils import validate_phone
78

89
from .base import BaseModel
910

1011

1112
class DriverBase(SQLModel):
12-
name: str = Field(min_length=1, max_length=255)
13-
email: EmailStr = Field(unique=True, index=True, max_length=254)
1413
phone: str = Field(min_length=1, max_length=20)
15-
address: str = Field(min_length=1, max_length=255)
1614
license_plate: str = Field(min_length=1, max_length=20)
1715
car_make_model: str = Field(min_length=1, max_length=255)
1816
active: bool = Field(default=True)
1917
notes: str = Field(default="", max_length=1024)
18+
address: str = Field(min_length=1, max_length=255)
2019

2120
@field_validator("phone")
2221
@classmethod
@@ -29,19 +28,33 @@ class Driver(DriverBase, BaseModel, table=True):
2928
__tablename__ = "drivers"
3029

3130
driver_id: UUID = Field(default_factory=uuid4, primary_key=True, index=True)
32-
auth_id: str = Field(nullable=False, unique=True, index=True)
31+
user_id: UUID = Field(foreign_key="users.user_id", unique=True, nullable=False)
32+
user: User = Relationship()
3333

3434

3535
class DriverCreate(DriverBase):
36-
password: str = Field(min_length=8, max_length=100)
36+
user_id: UUID # link to created User
3737

3838

3939
class DriverRead(DriverBase):
4040
driver_id: UUID
4141
auth_id: str
42+
user_id: UUID
43+
name: str
44+
email: EmailStr
45+
role: str # comes from User
4246

4347

4448
class DriverUpdate(SQLModel):
49+
phone: str | None = Field(default=None, min_length=1, max_length=20)
50+
address: str | None = Field(default=None, min_length=1, max_length=255)
51+
license_plate: str | None = Field(default=None, min_length=1, max_length=20)
52+
car_make_model: str | None = Field(default=None, min_length=1, max_length=255)
53+
active: bool | None = Field(default=None)
54+
notes: str | None = Field(default=None, max_length=1024)
55+
56+
57+
class DriverUpdatePayload(SQLModel):
4558
name: str | None = Field(default=None, min_length=1, max_length=255)
4659
email: EmailStr | None = Field(default=None, max_length=254)
4760
phone: str | None = Field(default=None, min_length=1, max_length=20)
@@ -55,13 +68,16 @@ class DriverUpdate(SQLModel):
5568
class DriverRegister(SQLModel):
5669
"""Driver registration request"""
5770

71+
# User fields
5872
name: str = Field(min_length=1, max_length=255)
5973
email: EmailStr = Field(max_length=254)
74+
password: str = Field(min_length=8, max_length=100)
75+
76+
# Driver fields
6077
phone: str = Field(min_length=1, max_length=20)
61-
address: str = Field(min_length=1, max_length=255)
6278
license_plate: str = Field(min_length=1, max_length=20)
6379
car_make_model: str = Field(min_length=1, max_length=255)
64-
password: str = Field(min_length=8, max_length=100)
80+
address: str = Field(min_length=1, max_length=255)
6581

6682
@field_validator("phone")
6783
@classmethod
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import datetime
2+
from uuid import UUID, uuid4
3+
4+
from sqlmodel import Field, SQLModel
5+
6+
from .base import BaseModel
7+
8+
9+
class SystemSettingsBase(SQLModel):
10+
"""Shared fields between table and API models"""
11+
12+
default_cap: int | None = Field(default=None)
13+
route_start_time: datetime.time | None = Field(default=None)
14+
warehouse_location: str | None = Field(default=None, min_length=1)
15+
warehouse_longitude: float | None = None
16+
warehouse_latitude: float | None = None
17+
18+
19+
class SystemSettings(SystemSettingsBase, BaseModel, table=True):
20+
"""Database table model"""
21+
22+
__tablename__ = "system_settings"
23+
24+
system_settings_id: UUID = Field(default_factory=uuid4, primary_key=True)
25+
26+
27+
class SystemSettingsCreate(SystemSettingsBase):
28+
"""Create request model"""
29+
30+
pass
31+
32+
33+
class SystemSettingsRead(SystemSettingsBase):
34+
"""Read response model"""
35+
36+
system_settings_id: UUID
37+
38+
39+
class SystemSettingsUpdate(SystemSettingsBase):
40+
"""Update request model - all optional"""
41+
42+
pass

backend/python/app/models/user.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from uuid import UUID, uuid4
2+
3+
from pydantic import EmailStr
4+
from sqlmodel import Field, SQLModel
5+
6+
from .base import BaseModel
7+
8+
9+
class UserBase(SQLModel):
10+
name: str = Field(min_length=1, max_length=255)
11+
email: EmailStr = Field(unique=True, index=True, max_length=254)
12+
13+
14+
class User(UserBase, BaseModel, table=True):
15+
__tablename__ = "users"
16+
17+
user_id: UUID = Field(default_factory=uuid4, primary_key=True, index=True)
18+
auth_id: str = Field(nullable=False, unique=True, index=True)
19+
role: str = Field(min_length=1, max_length=255, default="driver")
20+
21+
22+
class UserCreate(UserBase):
23+
password: str = Field(min_length=8, max_length=100)
24+
25+
26+
class UserRead(UserBase):
27+
user_id: UUID
28+
auth_id: str
29+
role: str
30+
31+
32+
class UserUpdate(SQLModel):
33+
name: str | None = Field(default=None, min_length=1, max_length=255)
34+
email: EmailStr | None = Field(default=None, max_length=254)
35+
36+
37+
class UserRegister(SQLModel):
38+
"""User registration request"""
39+
40+
name: str = Field(min_length=1, max_length=255)
41+
email: EmailStr = Field(max_length=254)
42+
password: str = Field(min_length=8, max_length=100)

0 commit comments

Comments
 (0)