|
|
import os |
|
|
import logging |
|
|
import firebase_admin.messaging |
|
|
import requests |
|
|
import datetime |
|
|
import asyncio |
|
|
from fastapi import FastAPI, HTTPException, status, Body |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
from pydantic import BaseModel, Field, EmailStr |
|
|
from motor.motor_asyncio import AsyncIOMotorClient |
|
|
from passlib.context import CryptContext |
|
|
from dotenv import load_dotenv |
|
|
from typing import Optional, List |
|
|
import firebase_admin |
|
|
from firebase_admin import credentials |
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
MONGODB_URI = os.getenv("MONGODB_URI") |
|
|
if not MONGODB_URI: |
|
|
raise ValueError("MONGODB_URI environment variable not set.") |
|
|
|
|
|
|
|
|
DATABASE_NAME = "userAuthDB" |
|
|
COLLECTION_NAME = "users" |
|
|
TOKEN_COLLECTION_NAME = "fcmtokens" |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") |
|
|
|
|
|
|
|
|
app = FastAPI( |
|
|
title="Backend API", |
|
|
description="API for WhereIParked Backend OPerations.", |
|
|
version="1.5.0", |
|
|
) |
|
|
|
|
|
|
|
|
origins = ["*"] |
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=origins, |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
client: Optional[AsyncIOMotorClient] = None |
|
|
db = None |
|
|
|
|
|
cred = credentials.Certificate("whereiparked-e9eb6-firebase-adminsdk-fbsvc-1691a99d68.json") |
|
|
|
|
|
default_app = None |
|
|
|
|
|
@app.on_event("startup") |
|
|
async def startup_client(): |
|
|
"""Connect to MongoDB & Firebase on application startup.""" |
|
|
global client, db, default_app |
|
|
logger.info("Connecting to MongoDB...") |
|
|
try: |
|
|
client = AsyncIOMotorClient(MONGODB_URI) |
|
|
db = client[DATABASE_NAME] |
|
|
|
|
|
await client.admin.command('ping') |
|
|
logger.info(f"Successfully connected to MongoDB database: {DATABASE_NAME}") |
|
|
try: |
|
|
if not firebase_admin._apps: |
|
|
default_app = firebase_admin.initialize_app(cred, { |
|
|
'projectId': 'whereiparked-e9eb6' |
|
|
}) |
|
|
logger.info("Firebase Admin SDK initialized successfully.") |
|
|
else: |
|
|
default_app = firebase_admin.get_app() |
|
|
logger.info("Firebase Admin SDK already initialized.") |
|
|
print(firebase_admin._apps) |
|
|
except Exception as e: |
|
|
logger.error(f"Error initializing/getting Firebase Admin SDK: {e}") |
|
|
except Exception as e: |
|
|
logger.error(f"Failed to connect to MongoDB: {e}") |
|
|
raise RuntimeError(f"Could not connect to MongoDB: {e}") |
|
|
|
|
|
@app.on_event("shutdown") |
|
|
async def shutdown_client(): |
|
|
"""Disconnect from MongoDB on application shutdown.""" |
|
|
global client |
|
|
if client: |
|
|
logger.info("Closing MongoDB connection...") |
|
|
client.close() |
|
|
logger.info("MongoDB connection closed.") |
|
|
|
|
|
|
|
|
|
|
|
class UserBase(BaseModel): |
|
|
"""Base model for user data.""" |
|
|
email: EmailStr = Field(..., example="user@example.com") |
|
|
|
|
|
class UserCreate(UserBase): |
|
|
"""Model for user creation (signup).""" |
|
|
username: str = Field(..., min_length=3, max_length=50, example="john_doe") |
|
|
password: str = Field(..., min_length=8, example="strongpassword123") |
|
|
class UserLogin(BaseModel): |
|
|
"""Model for user login.""" |
|
|
email: EmailStr = Field(..., example="user@example.com") |
|
|
password: str = Field(..., example="strongpassword123") |
|
|
|
|
|
class UserInDB(UserBase): |
|
|
"""Model representing user data stored in the database.""" |
|
|
username: str |
|
|
hashed_password: str |
|
|
|
|
|
class Token(BaseModel): |
|
|
"""Model for returning success/token (optional).""" |
|
|
message: str |
|
|
|
|
|
class SignupResponse(BaseModel): |
|
|
"""Response model for successful signup.""" |
|
|
message: str = Field(..., example="Signup successful!") |
|
|
|
|
|
class LoginSuccessResponse(BaseModel): |
|
|
"""Response model for successful login.""" |
|
|
message: str = Field(..., example="Login successful!") |
|
|
email: EmailStr = Field(..., example="user@example.com") |
|
|
|
|
|
class TokenModel(BaseModel): |
|
|
email: str |
|
|
token: str |
|
|
|
|
|
class InAppEventNotification(BaseModel): |
|
|
token: str |
|
|
|
|
|
|
|
|
def verify_password(plain_password, hashed_password): |
|
|
return pwd_context.verify(plain_password, hashed_password) |
|
|
|
|
|
def get_password_hash(password): |
|
|
return pwd_context.hash(password) |
|
|
|
|
|
async def get_user_by_email(email: str) -> Optional[dict]: |
|
|
"""Retrieves a user from the database by email.""" |
|
|
if db is None: |
|
|
|
|
|
logger.error("Database connection is not available in get_user_by_email") |
|
|
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.") |
|
|
try: |
|
|
user = await db[COLLECTION_NAME].find_one({"email": email}) |
|
|
return user |
|
|
except Exception as e: |
|
|
logger.error(f"Error fetching user by email ({email}): {e}") |
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error accessing user data.") |
|
|
|
|
|
|
|
|
async def get_user_by_username(username: str) -> Optional[dict]: |
|
|
"""Retrieves a user from the database by username.""" |
|
|
if db is None: |
|
|
logger.error("Database connection is not available in get_user_by_username") |
|
|
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.") |
|
|
try: |
|
|
user = await db[COLLECTION_NAME].find_one({"username": username}) |
|
|
return user |
|
|
except Exception as e: |
|
|
logger.error(f"Error fetching user by username ({username}): {e}") |
|
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error accessing user data.") |
|
|
|
|
|
FCM_SERVER_KEY = os.getenv("FCM_SERVER_KEY") |
|
|
|
|
|
async def send_push(token: str, title: str, body: str, platform: str = "web"): |
|
|
try: |
|
|
if platform=="app": |
|
|
message = firebase_admin.messaging.Message( |
|
|
notification=firebase_admin.messaging.Notification( |
|
|
title=title, |
|
|
body=body, |
|
|
image="./icon.png" |
|
|
), |
|
|
data={ |
|
|
"title": title, |
|
|
"body": body |
|
|
}, |
|
|
token=token, |
|
|
) |
|
|
else: |
|
|
message = firebase_admin.messaging.Message( |
|
|
data={ |
|
|
"title": title, |
|
|
"body": body |
|
|
}, |
|
|
token=token, |
|
|
) |
|
|
|
|
|
response = firebase_admin.messaging.send(message) |
|
|
|
|
|
print('Successfully sent message:', response) |
|
|
|
|
|
return response |
|
|
except UnregisteredError: |
|
|
logger.warning(f"Token unregistered, removing from DB: {token}") |
|
|
asyncio.create_task(db[TOKEN_COLLECTION_NAME].delete_one({"token": token})) |
|
|
return {"error": "Token unregistered, deleted from DB."} |
|
|
except Exception as e: |
|
|
logger.error(f"Error sending message: {e}") |
|
|
raise |
|
|
|
|
|
from firebase_admin.messaging import UnregisteredError |
|
|
|
|
|
|
|
|
|
|
|
@app.post("/signup", response_model=SignupResponse, status_code=status.HTTP_201_CREATED) |
|
|
async def signup(user_data: UserCreate = Body(...)): |
|
|
"""Handles user registration.""" |
|
|
if db is None: |
|
|
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.") |
|
|
|
|
|
existing_user_email = await get_user_by_email(user_data.email) |
|
|
if existing_user_email: |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_400_BAD_REQUEST, |
|
|
detail="Email already registered.", |
|
|
) |
|
|
existing_user_username = await get_user_by_username(user_data.username) |
|
|
if existing_user_username: |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_400_BAD_REQUEST, |
|
|
detail="Username already taken.", |
|
|
) |
|
|
|
|
|
hashed_password = get_password_hash(user_data.password) |
|
|
user_in_db = UserInDB( |
|
|
username=user_data.username, |
|
|
email=user_data.email, |
|
|
hashed_password=hashed_password |
|
|
) |
|
|
|
|
|
try: |
|
|
new_user = await db[COLLECTION_NAME].insert_one(user_in_db.dict(by_alias=True)) |
|
|
logger.info(f"User created successfully with ID: {new_user.inserted_id}") |
|
|
except Exception as e: |
|
|
logger.error(f"Error inserting user into database: {e}") |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
|
|
detail="An error occurred while creating the account.", |
|
|
) |
|
|
|
|
|
return SignupResponse(message="Signup successful!") |
|
|
|
|
|
|
|
|
@app.post("/login", response_model=LoginSuccessResponse) |
|
|
async def login(login_data: UserLogin = Body(...)): |
|
|
"""Handles user login. Returns user email on success.""" |
|
|
if db is None: |
|
|
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database service is unavailable.") |
|
|
|
|
|
user = await get_user_by_email(login_data.email) |
|
|
|
|
|
if not user or not verify_password(login_data.password, user["hashed_password"]): |
|
|
raise HTTPException( |
|
|
status_code=status.HTTP_401_UNAUTHORIZED, |
|
|
detail="Incorrect email or password.", |
|
|
headers={"WWW-Authenticate": "Bearer"}, |
|
|
) |
|
|
|
|
|
|
|
|
logger.info(f"User {user['email']} logged in successfully.") |
|
|
return LoginSuccessResponse( |
|
|
message="Login successful!", |
|
|
email=user["email"] |
|
|
|
|
|
) |
|
|
|
|
|
@app.post("/api/save-token") |
|
|
async def save_token(token_model: TokenModel): |
|
|
existing = await db.fcmtokens.find_one({"token": token_model.token}) |
|
|
if not existing: |
|
|
await db.fcmtokens.insert_one({ |
|
|
"email": token_model.email, |
|
|
"token": token_model.token, |
|
|
"createdAt": datetime.datetime.utcnow() |
|
|
}) |
|
|
return {"message": "Token saved"} |
|
|
|
|
|
@app.post("/api/parked") |
|
|
async def user_parked(requestBody: InAppEventNotification = Body(...)): |
|
|
await send_push(requestBody.token, "WhereIParked", "You parked your car. Timer started!") |
|
|
return {"message": "Notification sent"} |
|
|
|
|
|
|
|
|
@app.post("/api/clear-location") |
|
|
async def user_cleared(requestBody: InAppEventNotification = Body(...)): |
|
|
await send_push(requestBody.token, "WhereIParked", "Parking location cleared. Safe drive!") |
|
|
return {"message": "Notification sent"} |
|
|
|
|
|
@app.post("/api/park-duration") |
|
|
async def park_duration(requestBody: InAppEventNotification = Body(...), duration = str): |
|
|
await send_push(requestBody.token, "Reminder", "You’ve been parked for over " + duration + " minutes!") |
|
|
return {"message": "Notification sent"} |
|
|
|
|
|
@app.post("/admin/push") |
|
|
async def admin_push(title: str = Body(...), body: str = Body(...)): |
|
|
tokens = await db.fcmtokens.find().to_list(length=None) |
|
|
for token_entry in tokens: |
|
|
await send_push(token_entry["token"], title, body) |
|
|
return {"message": "Notifications sent"} |
|
|
|
|
|
@app.post("/api/app/parked") |
|
|
async def app_user_parked(requestBody: InAppEventNotification = Body(...)): |
|
|
await send_push(requestBody.token, "WhereIParked", "You parked your car. Timer started!", "app") |
|
|
return {"message": "Notification sent"} |
|
|
|
|
|
|
|
|
@app.post("/api/app/clear-location") |
|
|
async def app_user_cleared(requestBody: InAppEventNotification = Body(...)): |
|
|
await send_push(requestBody.token, "WhereIParked", "Parking location cleared. Safe drive!", "app") |
|
|
return {"message": "Notification sent"} |
|
|
|
|
|
@app.post("/api/app/park-duration") |
|
|
async def app_park_duration(requestBody: InAppEventNotification = Body(...), duration = str): |
|
|
await send_push(requestBody.token, "Reminder", "You’ve been parked for over " + duration + " minutes!", "app") |
|
|
return {"message": "Notification sent"} |
|
|
|
|
|
@app.post("/admin/app/push") |
|
|
async def app_admin_push(title: str = Body(...), body: str = Body(...)): |
|
|
tokens = await db.fcmtokens.find().to_list(length=None) |
|
|
for token_entry in tokens: |
|
|
await send_push(token_entry["token"], title, body, "app") |
|
|
return {"message": "Notifications sent"} |
|
|
|
|
|
|
|
|
|
|
|
@app.get("/", status_code=status.HTTP_200_OK) |
|
|
async def read_root(): |
|
|
"""Root endpoint to check if the API is running.""" |
|
|
return {"message": "Authentication API is running!"} |
|
|
|