Skip to content
Merged
Binary file added .DS_Store
Binary file not shown.
4 changes: 4 additions & 0 deletions Server/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Copy this file to `.env` and fill the values.
# Do NOT commit your real API key. Keep `.env` out of version control.

# MongoDB connection string
MONGODB_URL=your_mongodb_connection_string_here
MONGODB_DB_NAME=your_database_name_here

# Firebase Web API Key (used for email/password sign-in via Firebase REST API)
FIREBASE_API_KEY=your_firebase_web_api_key_here

Expand Down
3 changes: 2 additions & 1 deletion Server/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
firebase-service-account.json
__pycache__
.env
.venv
.venv
venv
Binary file modified Server/app/__pycache__/main.cpython-314.pyc
Binary file not shown.
66 changes: 47 additions & 19 deletions Server/app/core/firebase_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,55 @@
import firebase_admin
from firebase_admin import credentials

print("Attempting to initialize Firebase Admin SDK...")

env_path = os.getenv("FIREBASE_CREDENTIAL_PATH")
if env_path:
cred_path = Path(env_path)
else:
# Go up two directories from app/firebase_setup.py to the root
cred_path = Path(__file__).resolve().parent.parent / "firebase-service-account.json"

if not cred_path.exists():
print(f"Firebase service account file not found at {cred_path}.")
print("Set FIREBASE_CREDENTIAL_PATH environment variable if the file is located elsewhere.")
# In a real app, you might want to raise an error here
# raise FileNotFoundError(f"Firebase service account file not found at {cred_path}.")
else:

# Singleton holder for the initialized Firebase app
_FIREBASE_APP = None


def _locate_credential_path() -> Path:
env_path = os.getenv("FIREBASE_CREDENTIAL_PATH")
if env_path:
return Path(env_path)
return Path(__file__).resolve().parent.parent / "firebase-service-account.json"


def get_firebase_app():
"""Return the initialized Firebase app instance, initializing it if necessary.

This function implements a safe singleton pattern: repeated calls return the same
app instance and initialization is only attempted once.
"""
global _FIREBASE_APP
if _FIREBASE_APP:
return _FIREBASE_APP

cred_path = _locate_credential_path()
if not cred_path.exists():
# Credential not found; do not raise here to keep tests running in environments
# without Firebase credentials. Caller can decide how to handle None.
print(f"Firebase service account file not found at {cred_path}.")
print("Set FIREBASE_CREDENTIAL_PATH environment variable if the file is located elsewhere.")
return None

try:
# Initialize Firebase Admin SDK
cred = credentials.Certificate(str(cred_path))
firebase_admin.initialize_app(cred)
_FIREBASE_APP = firebase_admin.initialize_app(cred)
print("Firebase Admin SDK initialized successfully.")
return _FIREBASE_APP
except Exception as e:
print(f"Error initializing Firebase Admin SDK: {e}")
# Depending on your app's needs, you might want to exit or raise
# exit(1)
return None


def get_firebase_admin():
"""Return the `firebase_admin` module (for access to `auth`, etc.)."""
return firebase_admin


# Preserve previous behavior: attempt initialization at import time but in a safe way
try:
get_firebase_app()
except Exception:
# Swallow exceptions to avoid failing import in test environments
pass

4 changes: 3 additions & 1 deletion Server/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# --- Import Routers ---
from app.routers import general, auth, users
import app.routers.vehicles as vehicles
import app.routers.rents as rents


# --- Import DB Connection Handlers ---
Expand Down Expand Up @@ -42,4 +43,5 @@ async def lifespan(app: FastAPI):
app.include_router(general.router)
app.include_router(auth.router)
app.include_router(users.router)
app.include_router(vehicles.router)
app.include_router(vehicles.router)
app.include_router(rents.router)
43 changes: 43 additions & 0 deletions Server/app/repositories/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from motor.motor_asyncio import AsyncIOMotorDatabase
from typing import Any, List


class BaseRepository:
"""Generic repository providing basic CRUD operations for a MongoDB collection.

Usage:
repo = BaseRepository(db, "users")
await repo.create({...})
await repo.get_by_id("some_id")
"""

def __init__(self, db: AsyncIOMotorDatabase, collection_name: str):
self.db = db
self.collection_name = collection_name
self.collection = db[collection_name]

async def create(self, doc: dict) -> dict:
result = await self.collection.insert_one(doc)
created = await self.collection.find_one({"_id": result.inserted_id})
return created

async def get_by_id(self, id_: Any) -> dict | None:
return await self.collection.find_one({"_id": id_})

async def update_by_id(self, id_: Any, update: dict) -> dict | None:
if not update:
return await self.get_by_id(id_)
result = await self.collection.update_one({"_id": id_}, {"$set": update})
if result.matched_count == 0:
return None
return await self.get_by_id(id_)

async def delete_by_id(self, id_: Any) -> bool:
result = await self.collection.delete_one({"_id": id_})
return result.deleted_count == 1

async def list(self, filter_: dict = None, limit: int = 200) -> List[dict]:
filter_ = filter_ or {}
cursor = self.collection.find(filter_)
docs = await cursor.to_list(length=limit)
return docs
89 changes: 89 additions & 0 deletions Server/app/repositories/rent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from motor.motor_asyncio import AsyncIOMotorDatabase
from typing import List, Any
from app.repositories.base import BaseRepository

RENT_COLLECTION = "rents"


def _stringify_id(doc: dict) -> dict:
if not doc:
return doc
if "_id" in doc and not isinstance(doc["_id"], str):
doc["_id"] = str(doc["_id"])
return doc


class RentRepository(BaseRepository):
def __init__(self, db: AsyncIOMotorDatabase):
super().__init__(db, RENT_COLLECTION)

async def create_rent(self, *, renter_uid: str, rent_doc: dict) -> dict:
doc = rent_doc.copy()
doc["renter_uid"] = renter_uid

# Always auto-generate id at DB level; ignore any client-provided rentid
doc.pop("rentid", None)

created = await self.create(doc)
return _stringify_id(created)

async def get_rent_by_id(self, *, rent_id: Any) -> dict | None:
doc = await self.get_by_id(rent_id)
return _stringify_id(doc)

async def list_rents_by_renter(self, *, renter_uid: str) -> List[dict]:
docs = await self.list({"renter_uid": renter_uid}, limit=200)
return [_stringify_id(d) for d in docs]

async def list_rents_by_owner(self, *, owner_uid: str) -> List[dict]:
docs = await self.list({"owner_uid": owner_uid}, limit=200)
return [_stringify_id(d) for d in docs]

async def update_rent(self, *, renter_uid: str, rent_id: Any, update_fields: dict) -> dict | None:
# Only allow renter to update the record
update_fields.pop("_id", None)
update_fields.pop("renter_uid", None)
update_fields.pop("owner_uid", None)
update_fields.pop("vehicle_id", None)

result = await self.collection.update_one({"_id": rent_id, "renter_uid": renter_uid}, {"$set": update_fields})
if result.matched_count == 0:
return None
updated = await self.get_by_id(rent_id)
return _stringify_id(updated)

async def delete_rent(self, *, renter_uid: str, rent_id: Any) -> bool:
# Only renter can delete their rent
result = await self.collection.delete_one({"_id": rent_id, "renter_uid": renter_uid})
return result.deleted_count == 1


# Backwards-compatible function-style API
async def create_rent(db: AsyncIOMotorDatabase, *, renter_uid: str, rent_doc: dict) -> dict:
repo = RentRepository(db)
return await repo.create_rent(renter_uid=renter_uid, rent_doc=rent_doc)


async def get_rent_by_id(db: AsyncIOMotorDatabase, *, rent_id: str) -> dict | None:
repo = RentRepository(db)
return await repo.get_rent_by_id(rent_id=rent_id)


async def list_rents_by_renter(db: AsyncIOMotorDatabase, *, renter_uid: str) -> List[dict]:
repo = RentRepository(db)
return await repo.list_rents_by_renter(renter_uid=renter_uid)


async def list_rents_by_owner(db: AsyncIOMotorDatabase, *, owner_uid: str) -> List[dict]:
repo = RentRepository(db)
return await repo.list_rents_by_owner(owner_uid=owner_uid)


async def update_rent(db: AsyncIOMotorDatabase, *, renter_uid: str, rent_id: str, update_fields: dict) -> dict | None:
repo = RentRepository(db)
return await repo.update_rent(renter_uid=renter_uid, rent_id=rent_id, update_fields=update_fields)


async def delete_rent(db: AsyncIOMotorDatabase, *, renter_uid: str, rent_id: str) -> bool:
repo = RentRepository(db)
return await repo.delete_rent(renter_uid=renter_uid, rent_id=rent_id)
69 changes: 32 additions & 37 deletions Server/app/repositories/user.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,47 @@
from motor.motor_asyncio import AsyncIOMotorDatabase
from app.schemas import UserProfileBase
from app.repositories.base import BaseRepository

# The name of our MongoDB collection
USER_COLLECTION = "users"


class UserRepository(BaseRepository):
def __init__(self, db: AsyncIOMotorDatabase):
super().__init__(db, USER_COLLECTION)

async def create_user_profile(self, *, uid: str, email: str, profile_data: UserProfileBase) -> dict:
user_doc = profile_data.model_dump()
user_doc["email"] = email
user_doc["_id"] = uid
return await self.create(user_doc)

async def get_user_profile_by_uid(self, *, uid: str) -> dict | None:
return await self.get_by_id(uid)

async def update_user_profile_by_uid(self, *, uid: str, update_data: dict) -> dict | None:
return await self.update_by_id(uid, update_data)

async def delete_user_profile_by_uid(self, *, uid: str) -> bool:
return await self.delete_by_id(uid)


# Backwards-compatible function API used in tests and other modules
async def create_user_profile(db: AsyncIOMotorDatabase, *, uid: str, email: str, profile_data: UserProfileBase) -> dict:
"""
Creates a new user profile document in MongoDB.
Uses the Firebase UID as the document _id.
"""
# Create a dictionary for the new user document
user_doc = profile_data.model_dump()
user_doc["email"] = email
user_doc["_id"] = uid # Use Firebase UID as the MongoDB _id

# Insert the document into the 'users' collection
result = await db[USER_COLLECTION].insert_one(user_doc)

# Retrieve the inserted document to be safe and return it
created_doc = await db[USER_COLLECTION].find_one({"_id": result.inserted_id})
return created_doc
repo = UserRepository(db)
return await repo.create_user_profile(uid=uid, email=email, profile_data=profile_data)


async def get_user_profile_by_uid(db: AsyncIOMotorDatabase, *, uid: str) -> dict | None:
"""
Fetches a user profile from MongoDB using their Firebase UID (_id).
"""
user_doc = await db[USER_COLLECTION].find_one({"_id": uid})
return user_doc
repo = UserRepository(db)
return await repo.get_user_profile_by_uid(uid=uid)


async def update_user_profile_by_uid(db: AsyncIOMotorDatabase, *, uid: str, update_data: dict) -> dict | None:
"""
Updates an existing user profile document with the provided `update_data`.
Returns the updated document, or `None` if no document matched the UID.
"""
if not update_data:
return await get_user_profile_by_uid(db, uid=uid)

result = await db[USER_COLLECTION].update_one({"_id": uid}, {"$set": update_data})
if result.matched_count == 0:
return None
updated_doc = await db[USER_COLLECTION].find_one({"_id": uid})
return updated_doc
repo = UserRepository(db)
return await repo.update_user_profile_by_uid(uid=uid, update_data=update_data)


async def delete_user_profile_by_uid(db: AsyncIOMotorDatabase, *, uid: str) -> bool:
"""
Deletes a user profile document by UID. Returns True if a document was deleted.
"""
result = await db[USER_COLLECTION].delete_one({"_id": uid})
return result.deleted_count == 1
repo = UserRepository(db)
return await repo.delete_user_profile_by_uid(uid=uid)
Loading