Skip to content

Commit 37688cb

Browse files
Refactor backend services to reduce technical debt
This commit addresses several areas of technical debt in the backend: - Separated development and production dependencies into `requirements.txt` and `requirements-dev.txt`. - Refactored the configuration in `app/config.py` to improve security and clarity. - Simplified the CORS configuration in `main.py`. - Introduced a `BaseService` class to handle common CRUD operations and reduce code duplication. - Refactored `UserService`, `ExpenseService`, `GroupService`, and `AuthService` to use the `BaseService` and dependency injection. - Updated all API routers to use dependency injection for the services.
1 parent 2aacca8 commit 37688cb

15 files changed

Lines changed: 242 additions & 263 deletions

File tree

backend/app/auth/routes.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
TokenVerifyRequest,
1515
UserResponse,
1616
)
17-
from app.auth.security import create_access_token, oauth2_scheme # Import oauth2_scheme
18-
from app.auth.service import auth_service
17+
from app.auth.security import create_access_token, oauth2_scheme
18+
from app.auth.service import AuthService
1919
from app.config import settings
20+
from app.dependencies import get_auth_service
2021
from fastapi import APIRouter, Depends, HTTPException, status
2122
from fastapi.security import ( # Import OAuth2PasswordRequestForm
2223
OAuth2PasswordRequestForm,
@@ -28,7 +29,10 @@
2829
@router.post(
2930
"/token", response_model=TokenResponse, include_in_schema=False
3031
) # include_in_schema=False to hide from docs if desired, or True to show
31-
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
32+
async def login_for_access_token(
33+
form_data: OAuth2PasswordRequestForm = Depends(),
34+
auth_service: AuthService = Depends(get_auth_service),
35+
):
3236
"""
3337
OAuth2 compatible token login, get an access token for future requests.
3438
This endpoint is used by Swagger UI for authorization.
@@ -59,7 +63,9 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
5963

6064

6165
@router.post("/signup/email", response_model=AuthResponse)
62-
async def signup_with_email(request: EmailSignupRequest):
66+
async def signup_with_email(
67+
request: EmailSignupRequest, auth_service: AuthService = Depends(get_auth_service)
68+
):
6369
"""
6470
Registers a new user using email, password, and name, and returns authentication tokens and user information.
6571
@@ -101,7 +107,9 @@ async def signup_with_email(request: EmailSignupRequest):
101107

102108

103109
@router.post("/login/email", response_model=AuthResponse)
104-
async def login_with_email(request: EmailLoginRequest):
110+
async def login_with_email(
111+
request: EmailLoginRequest, auth_service: AuthService = Depends(get_auth_service)
112+
):
105113
"""
106114
Authenticates a user using email and password credentials.
107115
@@ -136,7 +144,9 @@ async def login_with_email(request: EmailLoginRequest):
136144

137145

138146
@router.post("/login/google", response_model=AuthResponse)
139-
async def login_with_google(request: GoogleLoginRequest):
147+
async def login_with_google(
148+
request: GoogleLoginRequest, auth_service: AuthService = Depends(get_auth_service)
149+
):
140150
"""
141151
Authenticates or registers a user using a Google OAuth ID token.
142152
@@ -169,7 +179,9 @@ async def login_with_google(request: GoogleLoginRequest):
169179

170180

171181
@router.post("/refresh", response_model=TokenResponse)
172-
async def refresh_token(request: RefreshTokenRequest):
182+
async def refresh_token(
183+
request: RefreshTokenRequest, auth_service: AuthService = Depends(get_auth_service)
184+
):
173185
"""
174186
Refreshes JWT tokens using a valid refresh token.
175187
@@ -213,7 +225,9 @@ async def refresh_token(request: RefreshTokenRequest):
213225

214226

215227
@router.post("/token/verify", response_model=UserResponse)
216-
async def verify_token(request: TokenVerifyRequest):
228+
async def verify_token(
229+
request: TokenVerifyRequest, auth_service: AuthService = Depends(get_auth_service)
230+
):
217231
"""
218232
Verifies an access token and returns the associated user information.
219233
@@ -236,7 +250,9 @@ async def verify_token(request: TokenVerifyRequest):
236250

237251

238252
@router.post("/password/reset/request", response_model=SuccessResponse)
239-
async def request_password_reset(request: PasswordResetRequest):
253+
async def request_password_reset(
254+
request: PasswordResetRequest, auth_service: AuthService = Depends(get_auth_service)
255+
):
240256
"""
241257
Initiates a password reset process by sending a reset link to the provided email address.
242258
@@ -256,7 +272,9 @@ async def request_password_reset(request: PasswordResetRequest):
256272

257273

258274
@router.post("/password/reset/confirm", response_model=SuccessResponse)
259-
async def confirm_password_reset(request: PasswordResetConfirm):
275+
async def confirm_password_reset(
276+
request: PasswordResetConfirm, auth_service: AuthService = Depends(get_auth_service)
277+
):
260278
"""
261279
Resets a user's password using a valid password reset token.
262280

backend/app/auth/service.py

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,10 @@
7171

7272
class AuthService:
7373
def __init__(self):
74-
# Initializes the AuthService instance.
75-
pass
76-
77-
def get_db(self):
78-
"""
79-
Returns a database connection instance from the application's database module.
80-
"""
81-
return get_database()
74+
self.db = get_database()
75+
self.users_collection = self.db["users"]
76+
self.refresh_tokens_collection = self.db["refresh_tokens"]
77+
self.password_resets_collection = self.db["password_resets"]
8278

8379
async def create_user_with_email(
8480
self, email: str, password: str, name: str
@@ -99,10 +95,8 @@ async def create_user_with_email(
9995
Raises:
10096
HTTPException: If a user with the given email already exists.
10197
"""
102-
db = self.get_db()
103-
10498
# Check if user already exists
105-
existing_user = await db.users.find_one({"email": email})
99+
existing_user = await self.users_collection.find_one({"email": email})
106100
if existing_user:
107101
raise HTTPException(
108102
status_code=status.HTTP_400_BAD_REQUEST,
@@ -122,7 +116,7 @@ async def create_user_with_email(
122116
}
123117

124118
try:
125-
result = await db.users.insert_one(user_doc)
119+
result = await self.users_collection.insert_one(user_doc)
126120
user_doc["_id"] = str(result.inserted_id)
127121

128122
# Create refresh token
@@ -154,9 +148,8 @@ async def authenticate_user_with_email(
154148
Returns:
155149
A dictionary containing the authenticated user and a new refresh token.
156150
"""
157-
db = self.get_db()
158151
try:
159-
user = await db.users.find_one({"email": email})
152+
user = await self.users_collection.find_one({"email": email})
160153
except PyMongoError as e:
161154
logger.error(f"Database error during user lookup: {e}")
162155
raise HTTPException(
@@ -215,11 +208,9 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
215208
detail="Email not provided by Google",
216209
)
217210

218-
db = self.get_db()
219-
220211
# Check if user exists
221212
try:
222-
user = await db.users.find_one(
213+
user = await self.users_collection.find_one(
223214
{"$or": [{"email": email}, {"firebase_uid": firebase_uid}]}
224215
)
225216
except PyMongoError as e:
@@ -238,7 +229,7 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
238229

239230
if update_data:
240231
try:
241-
await db.users.update_one(
232+
await self.users_collection.update_one(
242233
{"_id": user["_id"]}, {"$set": update_data}
243234
)
244235
user.update(update_data)
@@ -257,7 +248,7 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]:
257248
"hashed_password": None,
258249
}
259250
try:
260-
result = await db.users.insert_one(user_doc)
251+
result = await self.users_collection.insert_one(user_doc)
261252
user_doc["_id"] = result.inserted_id
262253
user = user_doc
263254
except PyMongoError as e:
@@ -303,11 +294,9 @@ async def refresh_access_token(self, refresh_token: str) -> str:
303294
Returns:
304295
A new refresh token string.
305296
"""
306-
db = self.get_db()
307-
308297
# Find and validate refresh token
309298
try:
310-
token_record = await db.refresh_tokens.find_one(
299+
token_record = await self.refresh_tokens_collection.find_one(
311300
{
312301
"token": refresh_token,
313302
"revoked": False,
@@ -329,7 +318,7 @@ async def refresh_access_token(self, refresh_token: str) -> str:
329318

330319
# Get user
331320
try:
332-
user = await db.users.find_one({"_id": token_record["user_id"]})
321+
user = await self.users_collection.find_one({"_id": token_record["user_id"]})
333322
except PyMongoError as e:
334323
logger.error("Error while fetching user: %s", str(e))
335324
raise HTTPException(
@@ -355,7 +344,7 @@ async def refresh_access_token(self, refresh_token: str) -> str:
355344

356345
# Revoke old token
357346
try:
358-
await db.refresh_tokens.update_one(
347+
await self.refresh_tokens_collection.update_one(
359348
{"_id": token_record["_id"]}, {"$set": {"revoked": True}}
360349
)
361350
except PyMongoError as e:
@@ -393,10 +382,8 @@ async def verify_access_token(self, token: str) -> Dict[str, Any]:
393382
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
394383
)
395384

396-
db = self.get_db()
397-
398385
try:
399-
user = await db.users.find_one({"_id": user_id})
386+
user = await self.users_collection.find_one({"_id": user_id})
400387
except Exception as e:
401388
logger.error("Error while verifying token: %s", str(e))
402389
raise HTTPException(
@@ -417,10 +404,8 @@ async def request_password_reset(self, email: str) -> bool:
417404
418405
If the user exists, generates a password reset token with a 1-hour expiration and stores it in the database. The reset token and link are logged for development purposes. Always returns True to avoid revealing whether the email is registered.
419406
"""
420-
db = self.get_db()
421-
422407
try:
423-
user = await db.users.find_one({"email": email})
408+
user = await self.users_collection.find_one({"email": email})
424409
except PyMongoError as e:
425410
logger.error(
426411
f"Database error while fetching user by email {email}: {str(e)}"
@@ -439,7 +424,7 @@ async def request_password_reset(self, email: str) -> bool:
439424

440425
try:
441426
# Store reset token
442-
await db.password_resets.insert_one(
427+
await self.password_resets_collection.insert_one(
443428
{
444429
"user_id": user["_id"],
445430
"token": reset_token,
@@ -481,11 +466,9 @@ async def confirm_password_reset(self, reset_token: str, new_password: str) -> b
481466
Raises:
482467
HTTPException: If the reset token is invalid or expired.
483468
"""
484-
db = self.get_db()
485-
486469
try:
487470
# Find and validate reset token
488-
reset_record = await db.password_resets.find_one(
471+
reset_record = await self.password_resets_collection.find_one(
489472
{
490473
"token": reset_token,
491474
"used": False,
@@ -502,18 +485,18 @@ async def confirm_password_reset(self, reset_token: str, new_password: str) -> b
502485

503486
# Update user password
504487
new_hash = get_password_hash(new_password)
505-
await db.users.update_one(
488+
await self.users_collection.update_one(
506489
{"_id": reset_record["user_id"]},
507490
{"$set": {"hashed_password": new_hash}},
508491
)
509492

510493
# Mark token as used
511-
await db.password_resets.update_one(
494+
await self.password_resets_collection.update_one(
512495
{"_id": reset_record["_id"]}, {"$set": {"used": True}}
513496
)
514497

515498
# Revoke all refresh tokens for this user (force re-login)
516-
await db.refresh_tokens.update_many(
499+
await self.refresh_tokens_collection.update_many(
517500
{"user_id": reset_record["user_id"]}, {"$set": {"revoked": True}}
518501
)
519502
logger.info(
@@ -542,15 +525,13 @@ async def _create_refresh_token_record(self, user_id: str) -> str:
542525
Returns:
543526
The generated refresh token string.
544527
"""
545-
db = self.get_db()
546-
547528
refresh_token = create_refresh_token()
548529
expires_at = datetime.now(timezone.utc) + timedelta(
549530
days=settings.refresh_token_expire_days
550531
)
551532

552533
try:
553-
await db.refresh_tokens.insert_one(
534+
await self.refresh_tokens_collection.insert_one(
554535
{
555536
"token": refresh_token,
556537
"user_id": (
@@ -573,5 +554,3 @@ async def _create_refresh_token_record(self, user_id: str) -> str:
573554
return refresh_token
574555

575556

576-
# Create service instance
577-
auth_service = AuthService()

backend/app/config.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import time
44
from logging.config import dictConfig
5-
from typing import Optional
5+
from typing import List, Optional
66

77
from pydantic_settings import BaseSettings
88
from starlette.middleware.base import BaseHTTPMiddleware
@@ -15,13 +15,13 @@ class Settings(BaseSettings):
1515
database_name: str = "splitwiser"
1616

1717
# JWT
18-
secret_key: str = "your-super-secret-jwt-key-change-this-in-production"
18+
secret_key: str
1919
algorithm: str = "HS256"
2020
access_token_expire_minutes: int = 15
2121
refresh_token_expire_days: int = 30
22+
2223
# Firebase
2324
firebase_project_id: Optional[str] = None
24-
firebase_service_account_path: str = "./firebase-service-account.json"
2525
# Firebase service account credentials as environment variables
2626
firebase_type: Optional[str] = None
2727
firebase_private_key_id: Optional[str] = None
@@ -37,9 +37,12 @@ class Settings(BaseSettings):
3737
debug: bool = False
3838

3939
# CORS - Add your frontend domain here for production
40-
allowed_origins: str = (
41-
"http://localhost:3000,http://localhost:5173,http://127.0.0.1:3000,http://localhost:8081"
42-
)
40+
allowed_origins: List[str] = [
41+
"http://localhost:3000",
42+
"http://localhost:5173",
43+
"http://127.0.0.1:3000",
44+
"http://localhost:8081",
45+
]
4346
allow_all_origins: bool = False
4447

4548
class Config:

backend/app/dependencies.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,23 @@ async def get_current_user(
5757
detail="Could not validate credentials",
5858
headers={"WWW-Authenticate": "Bearer"},
5959
)
60+
61+
62+
from app.auth.service import AuthService
63+
from app.expenses.service import ExpenseService
64+
from app.groups.service import GroupService
65+
from app.user.service import UserService
66+
67+
68+
def get_user_service() -> UserService:
69+
return UserService()
70+
71+
72+
def get_expense_service() -> ExpenseService:
73+
return ExpenseService()
74+
75+
def get_group_service() -> GroupService:
76+
return GroupService()
77+
78+
def get_auth_service() -> AuthService:
79+
return AuthService()

0 commit comments

Comments
 (0)