mongog setup
This commit is contained in:
6
backend/.env.example
Normal file
6
backend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
MONGODB_URI=mongodb://localhost:27017
|
||||
MONGODB_DB_NAME=grateful_journal
|
||||
API_PORT=8001
|
||||
ENVIRONMENT=development
|
||||
FRONTEND_URL=http://localhost:8000
|
||||
|
||||
88
backend/README.md
Normal file
88
backend/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Grateful Journal Backend API
|
||||
|
||||
FastAPI backend for Grateful Journal - a private-first gratitude journaling app.
|
||||
|
||||
**Port:** 8001
|
||||
**API Docs:** http://localhost:8001/docs
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
- MongoDB running on `mongodb://localhost:27017`
|
||||
- Python 3.9+
|
||||
|
||||
See [MongoDB Setup Guide](../docs/MONGODB_SETUP.md) for installation.
|
||||
|
||||
### 2. Install & Run
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # macOS/Linux
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run API
|
||||
python main.py
|
||||
```
|
||||
|
||||
API starts on http://0.0.0.0:8001
|
||||
|
||||
### 3. Environment Variables
|
||||
|
||||
Copy `.env.example` to `.env`. Defaults work for local dev:
|
||||
|
||||
```env
|
||||
MONGODB_URI=mongodb://localhost:27017
|
||||
MONGODB_DB_NAME=grateful_journal
|
||||
API_PORT=8001
|
||||
ENVIRONMENT=development
|
||||
FRONTEND_URL=http://localhost:8000
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- **`main.py`** — FastAPI app, CORS, route registration, lifespan events
|
||||
- **`config.py`** — Settings management (environment variables)
|
||||
- **`db.py`** — MongoDB connection (singleton pattern)
|
||||
- **`models.py`** — Pydantic data models
|
||||
- **`routers/`** — API endpoints
|
||||
- `users.py` — User registration, profile updates, deletion
|
||||
- `entries.py` — Journal entry CRUD, date filtering
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Users
|
||||
|
||||
```
|
||||
POST /api/users/register Register user (after Firebase auth)
|
||||
GET /api/users/by-email/{email} Get user by email
|
||||
PUT /api/users/update/{user_id} Update user profile
|
||||
DELETE /api/users/{user_id} Delete user & all data
|
||||
```
|
||||
|
||||
### Entries
|
||||
|
||||
```
|
||||
POST /api/entries/{user_id} Create new entry
|
||||
GET /api/entries/{user_id} List entries (paginated)
|
||||
GET /api/entries/{user_id}/{entry_id} Get single entry
|
||||
PUT /api/entries/{user_id}/{entry_id} Update entry
|
||||
DELETE /api/entries/{user_id}/{entry_id} Delete entry
|
||||
GET /api/entries/{user_id}/date/{date} Get entries by date
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
- Frontend authenticates via **Firebase Google Auth**
|
||||
- User ID is passed in URL path (no token validation yet; implementation depends on frontend requirements)
|
||||
- Optional: Add Firebase token verification in middleware later
|
||||
|
||||
## Development Notes
|
||||
|
||||
- **CORS** enabled for `localhost:8000`
|
||||
- **Async/await** used throughout for scalability
|
||||
- **Pydantic** models for request/response validation
|
||||
- **MongoDB** auto-creates collections on first write
|
||||
BIN
backend/__pycache__/config.cpython-312.pyc
Normal file
BIN
backend/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/db.cpython-312.pyc
Normal file
BIN
backend/__pycache__/db.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
BIN
backend/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/__pycache__/models.cpython-312.pyc
Normal file
BIN
backend/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
19
backend/config.py
Normal file
19
backend/config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
mongodb_uri: str = "mongodb://localhost:27017"
|
||||
mongodb_db_name: str = "grateful_journal"
|
||||
api_port: int = 8001
|
||||
environment: str = "development"
|
||||
frontend_url: str = "http://localhost:8000"
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings():
|
||||
return Settings()
|
||||
31
backend/db.py
Normal file
31
backend/db.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from pymongo import MongoClient
|
||||
from config import get_settings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class MongoDB:
|
||||
client: Optional[MongoClient] = None
|
||||
db = None
|
||||
|
||||
@staticmethod
|
||||
def connect_db():
|
||||
settings = get_settings()
|
||||
MongoDB.client = MongoClient(settings.mongodb_uri)
|
||||
MongoDB.db = MongoDB.client[settings.mongodb_db_name]
|
||||
print(f"✓ Connected to MongoDB: {settings.mongodb_db_name}")
|
||||
|
||||
@staticmethod
|
||||
def close_db():
|
||||
if MongoDB.client:
|
||||
MongoDB.client.close()
|
||||
print("✓ Disconnected from MongoDB")
|
||||
|
||||
@staticmethod
|
||||
def get_db():
|
||||
return MongoDB.db
|
||||
|
||||
# Get database instance
|
||||
|
||||
|
||||
def get_database():
|
||||
return MongoDB.get_db()
|
||||
65
backend/main.py
Normal file
65
backend/main.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from fastapi import FastAPI, HTTPException, Depends
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from db import MongoDB, get_database
|
||||
from config import get_settings
|
||||
from routers import entries, users
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
MongoDB.connect_db()
|
||||
yield
|
||||
# Shutdown
|
||||
MongoDB.close_db()
|
||||
|
||||
app = FastAPI(
|
||||
title="Grateful Journal API",
|
||||
description="Backend API for Grateful Journal - private journaling app",
|
||||
version="0.1.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[settings.frontend_url,
|
||||
"http://localhost:8000", "http://127.0.0.1:8000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(users.router, prefix="/api/users", tags=["users"])
|
||||
app.include_router(entries.router, prefix="/api/entries", tags=["entries"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "ok",
|
||||
"environment": settings.environment,
|
||||
"api_version": "0.1.0"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"message": "Grateful Journal API",
|
||||
"version": "0.1.0",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=settings.api_port,
|
||||
reload=settings.environment == "development"
|
||||
)
|
||||
84
backend/models.py
Normal file
84
backend/models.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
|
||||
# ========== User Models ==========
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
email: str
|
||||
displayName: Optional[str] = None
|
||||
photoURL: Optional[str] = None
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
displayName: Optional[str] = None
|
||||
photoURL: Optional[str] = None
|
||||
theme: Optional[str] = None
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
displayName: Optional[str] = None
|
||||
photoURL: Optional[str] = None
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
theme: Optional[str] = "light"
|
||||
|
||||
# ========== Journal Entry Models ==========
|
||||
|
||||
|
||||
class MoodEnum(str, Enum):
|
||||
happy = "happy"
|
||||
sad = "sad"
|
||||
neutral = "neutral"
|
||||
anxious = "anxious"
|
||||
grateful = "grateful"
|
||||
|
||||
|
||||
class JournalEntryCreate(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
mood: Optional[MoodEnum] = None
|
||||
tags: Optional[List[str]] = None
|
||||
isPublic: Optional[bool] = False
|
||||
|
||||
|
||||
class JournalEntryUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
mood: Optional[MoodEnum] = None
|
||||
tags: Optional[List[str]] = None
|
||||
isPublic: Optional[bool] = None
|
||||
|
||||
|
||||
class JournalEntry(BaseModel):
|
||||
id: str
|
||||
userId: str
|
||||
title: str
|
||||
content: str
|
||||
mood: Optional[MoodEnum] = None
|
||||
tags: Optional[List[str]] = None
|
||||
isPublic: bool = False
|
||||
createdAt: datetime
|
||||
updatedAt: datetime
|
||||
|
||||
# ========== Settings Models ==========
|
||||
|
||||
|
||||
class UserSettingsUpdate(BaseModel):
|
||||
notifications: Optional[bool] = None
|
||||
emailNotifications: Optional[bool] = None
|
||||
theme: Optional[str] = None
|
||||
language: Optional[str] = None
|
||||
|
||||
|
||||
class UserSettings(BaseModel):
|
||||
userId: str
|
||||
notifications: bool = True
|
||||
emailNotifications: bool = False
|
||||
theme: str = "light"
|
||||
language: str = "en"
|
||||
updatedAt: datetime
|
||||
8
backend/requirements.txt
Normal file
8
backend/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
pymongo==4.6.0
|
||||
pydantic==2.5.0
|
||||
python-dotenv==1.0.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
cors==1.0.1
|
||||
1
backend/routers/__init__.py
Normal file
1
backend/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Routers package
|
||||
BIN
backend/routers/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/routers/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/routers/__pycache__/entries.cpython-312.pyc
Normal file
BIN
backend/routers/__pycache__/entries.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/routers/__pycache__/users.cpython-312.pyc
Normal file
BIN
backend/routers/__pycache__/users.cpython-312.pyc
Normal file
Binary file not shown.
165
backend/routers/entries.py
Normal file
165
backend/routers/entries.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""Journal entry routes"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from db import get_database
|
||||
from models import JournalEntryCreate, JournalEntryUpdate
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
from bson import ObjectId
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/{user_id}", response_model=dict)
|
||||
async def create_entry(user_id: str, entry_data: JournalEntryCreate):
|
||||
"""Create a new journal entry"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
entry_doc = {
|
||||
"userId": user_id,
|
||||
"title": entry_data.title,
|
||||
"content": entry_data.content,
|
||||
"mood": entry_data.mood,
|
||||
"tags": entry_data.tags or [],
|
||||
"isPublic": entry_data.isPublic,
|
||||
"createdAt": datetime.utcnow(),
|
||||
"updatedAt": datetime.utcnow()
|
||||
}
|
||||
|
||||
result = db.entries.insert_one(entry_doc)
|
||||
entry_doc["id"] = str(result.inserted_id)
|
||||
|
||||
return {
|
||||
"id": entry_doc["id"],
|
||||
"message": "Entry created successfully"
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{user_id}")
|
||||
async def get_user_entries(user_id: str, limit: int = 50, skip: int = 0):
|
||||
"""Get all entries for a user (paginated, most recent first)"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
entries = list(
|
||||
db.entries.find(
|
||||
{"userId": user_id}
|
||||
).sort("createdAt", -1).skip(skip).limit(limit)
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
entry["id"] = str(entry["_id"])
|
||||
del entry["_id"]
|
||||
|
||||
total = db.entries.count_documents({"userId": user_id})
|
||||
|
||||
return {
|
||||
"entries": entries,
|
||||
"total": total,
|
||||
"skip": skip,
|
||||
"limit": limit
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{user_id}/{entry_id}")
|
||||
async def get_entry(user_id: str, entry_id: str):
|
||||
"""Get a specific entry"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
entry = db.entries.find_one({
|
||||
"_id": ObjectId(entry_id),
|
||||
"userId": user_id
|
||||
})
|
||||
|
||||
if not entry:
|
||||
raise HTTPException(status_code=404, detail="Entry not found")
|
||||
|
||||
entry["id"] = str(entry["_id"])
|
||||
del entry["_id"]
|
||||
|
||||
return entry
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{user_id}/{entry_id}")
|
||||
async def update_entry(user_id: str, entry_id: str, entry_data: JournalEntryUpdate):
|
||||
"""Update a journal entry"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
update_data = entry_data.model_dump(exclude_unset=True)
|
||||
update_data["updatedAt"] = datetime.utcnow()
|
||||
|
||||
result = db.entries.update_one(
|
||||
{
|
||||
"_id": ObjectId(entry_id),
|
||||
"userId": user_id
|
||||
},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
if result.matched_count == 0:
|
||||
raise HTTPException(status_code=404, detail="Entry not found")
|
||||
|
||||
return {"message": "Entry updated successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{user_id}/{entry_id}")
|
||||
async def delete_entry(user_id: str, entry_id: str):
|
||||
"""Delete a journal entry"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
result = db.entries.delete_one({
|
||||
"_id": ObjectId(entry_id),
|
||||
"userId": user_id
|
||||
})
|
||||
|
||||
if result.deleted_count == 0:
|
||||
raise HTTPException(status_code=404, detail="Entry not found")
|
||||
|
||||
return {"message": "Entry deleted successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/{user_id}/date/{date_str}")
|
||||
async def get_entries_by_date(user_id: str, date_str: str):
|
||||
"""Get entries for a specific date (format: YYYY-MM-DD)"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
from datetime import datetime as dt
|
||||
|
||||
# Parse date
|
||||
target_date = dt.strptime(date_str, "%Y-%m-%d")
|
||||
next_date = dt.fromtimestamp(target_date.timestamp() + 86400)
|
||||
|
||||
entries = list(
|
||||
db.entries.find({
|
||||
"userId": user_id,
|
||||
"createdAt": {
|
||||
"$gte": target_date,
|
||||
"$lt": next_date
|
||||
}
|
||||
}).sort("createdAt", -1)
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
entry["id"] = str(entry["_id"])
|
||||
del entry["_id"]
|
||||
|
||||
return {"entries": entries, "date": date_str}
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Invalid date format. Use YYYY-MM-DD")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
99
backend/routers/users.py
Normal file
99
backend/routers/users.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""User management routes"""
|
||||
from fastapi import APIRouter, HTTPException, Header
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
from db import get_database
|
||||
from models import UserCreate, UserUpdate, User
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/register", response_model=dict)
|
||||
async def register_user(user_data: UserCreate):
|
||||
"""
|
||||
Register a new user (called after Firebase Google Auth)
|
||||
Stores user profile in MongoDB
|
||||
"""
|
||||
db = get_database()
|
||||
|
||||
try:
|
||||
user_doc = {
|
||||
"email": user_data.email,
|
||||
"displayName": user_data.displayName or user_data.email.split("@")[0],
|
||||
"photoURL": user_data.photoURL,
|
||||
"createdAt": datetime.utcnow(),
|
||||
"updatedAt": datetime.utcnow(),
|
||||
"theme": "light"
|
||||
}
|
||||
|
||||
result = db.users.insert_one(user_doc)
|
||||
user_doc["id"] = str(result.inserted_id)
|
||||
|
||||
return {
|
||||
"id": user_doc["id"],
|
||||
"email": user_doc["email"],
|
||||
"displayName": user_doc["displayName"],
|
||||
"message": "User registered successfully"
|
||||
}
|
||||
except DuplicateKeyError:
|
||||
raise HTTPException(status_code=400, detail="User already exists")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/by-email/{email}", response_model=dict)
|
||||
async def get_user_by_email(email: str):
|
||||
"""Get user profile by email (called after Firebase Auth)"""
|
||||
db = get_database()
|
||||
|
||||
user = db.users.find_one({"email": email})
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
user["id"] = str(user["_id"])
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/update/{user_id}", response_model=dict)
|
||||
async def update_user(user_id: str, user_data: UserUpdate):
|
||||
"""Update user profile"""
|
||||
db = get_database()
|
||||
from bson import ObjectId
|
||||
|
||||
try:
|
||||
update_data = user_data.model_dump(exclude_unset=True)
|
||||
update_data["updatedAt"] = datetime.utcnow()
|
||||
|
||||
result = db.users.update_one(
|
||||
{"_id": ObjectId(user_id)},
|
||||
{"$set": update_data}
|
||||
)
|
||||
|
||||
if result.matched_count == 0:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
return {"message": "User updated successfully"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
async def delete_user(user_id: str):
|
||||
"""Delete user account and all associated data"""
|
||||
db = get_database()
|
||||
from bson import ObjectId
|
||||
|
||||
try:
|
||||
# Delete user
|
||||
db.users.delete_one({"_id": ObjectId(user_id)})
|
||||
|
||||
# Delete all entries by user
|
||||
db.entries.delete_many({"userId": user_id})
|
||||
|
||||
# Delete user settings
|
||||
db.settings.delete_one({"userId": user_id})
|
||||
|
||||
return {"message": "User and associated data deleted"}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user