feat: evolve MetaBloom into full product foundation
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import Depends, FastAPI, HTTPException, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from .config import get_settings
|
||||
from .db import Base, engine, get_db
|
||||
from .models import Entry, Profile, User
|
||||
from .schemas import EntryIn, EntryOut, ProfileIn, ProfileOut, Register, Token
|
||||
from .security import current_user, hash_password, make_token, verify_password
|
||||
|
||||
cfg = get_settings()
|
||||
if cfg.environment == "production" and (len(cfg.jwt_secret) < 32 or cfg.jwt_secret.startswith("change-me")):
|
||||
raise RuntimeError("Set a strong JWT_SECRET in production")
|
||||
|
||||
app = FastAPI(title="MetaBloom API", version="1.0.0", description="Wellness tracking API — not a medical device.")
|
||||
app.add_middleware(CORSMiddleware, allow_origins=cfg.origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup():
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "medical_notice": "MetaBloom does not diagnose or replace medical care."}
|
||||
|
||||
|
||||
@app.post("/auth/register", response_model=Token, status_code=201)
|
||||
def register(data: Register, db: Session = Depends(get_db)):
|
||||
email = data.email.lower()
|
||||
if db.scalar(select(User).where(User.email == email)):
|
||||
raise HTTPException(409, "Email already registered")
|
||||
user = User(email=email, password_hash=hash_password(data.password))
|
||||
db.add(user); db.flush()
|
||||
db.add(Profile(user_id=user.id)); db.commit()
|
||||
return Token(access_token=make_token(user.id))
|
||||
|
||||
|
||||
@app.post("/auth/token", response_model=Token)
|
||||
def login(form: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
||||
user = db.scalar(select(User).where(User.email == form.username.lower()))
|
||||
if not user or not verify_password(form.password, user.password_hash):
|
||||
raise HTTPException(401, "Invalid credentials", headers={"WWW-Authenticate": "Bearer"})
|
||||
return Token(access_token=make_token(user.id))
|
||||
|
||||
|
||||
@app.get("/profile", response_model=ProfileOut)
|
||||
def get_profile(user: User = Depends(current_user)):
|
||||
return user.profile
|
||||
|
||||
|
||||
@app.put("/profile", response_model=ProfileOut)
|
||||
def put_profile(data: ProfileIn, user: User = Depends(current_user), db: Session = Depends(get_db)):
|
||||
profile = user.profile or Profile(user_id=user.id)
|
||||
for key, value in data.model_dump().items(): setattr(profile, key, value)
|
||||
db.add(profile); db.commit(); db.refresh(profile)
|
||||
return profile
|
||||
|
||||
|
||||
@app.get("/entries", response_model=list[EntryOut])
|
||||
def list_entries(kind: str | None = None, limit: int = 100, user: User = Depends(current_user), db: Session = Depends(get_db)):
|
||||
limit = min(max(limit, 1), 500)
|
||||
query = select(Entry).where(Entry.user_id == user.id)
|
||||
if kind: query = query.where(Entry.kind == kind)
|
||||
return db.scalars(query.order_by(Entry.recorded_at.desc()).limit(limit)).all()
|
||||
|
||||
|
||||
@app.post("/entries", response_model=EntryOut, status_code=201)
|
||||
def create_entry(data: EntryIn, user: User = Depends(current_user), db: Session = Depends(get_db)):
|
||||
if not user.profile or not user.profile.consent_health_data:
|
||||
raise HTTPException(403, "Health-data consent is required")
|
||||
entry = Entry(user_id=user.id, **data.model_dump(exclude_none=True))
|
||||
db.add(entry); db.commit(); db.refresh(entry)
|
||||
return entry
|
||||
|
||||
|
||||
def owned_entry(entry_id: str, user: User, db: Session) -> Entry:
|
||||
entry = db.scalar(select(Entry).where(Entry.id == entry_id, Entry.user_id == user.id))
|
||||
if not entry: raise HTTPException(404, "Entry not found")
|
||||
return entry
|
||||
|
||||
|
||||
@app.put("/entries/{entry_id}", response_model=EntryOut)
|
||||
def update_entry(entry_id: str, data: EntryIn, user: User = Depends(current_user), db: Session = Depends(get_db)):
|
||||
entry = owned_entry(entry_id, user, db)
|
||||
for key, value in data.model_dump(exclude_none=True).items(): setattr(entry, key, value)
|
||||
db.commit(); db.refresh(entry)
|
||||
return entry
|
||||
|
||||
|
||||
@app.delete("/entries/{entry_id}", status_code=204)
|
||||
def delete_entry(entry_id: str, user: User = Depends(current_user), db: Session = Depends(get_db)):
|
||||
db.delete(owned_entry(entry_id, user, db)); db.commit()
|
||||
return Response(status_code=204)
|
||||
|
||||
|
||||
@app.get("/account/export")
|
||||
def export_account(user: User = Depends(current_user)):
|
||||
return {"exported_at": datetime.now(timezone.utc), "medical_notice": "Informational data only; consult a professional for medical interpretation.", "profile": ProfileOut.model_validate(user.profile), "entries": [EntryOut.model_validate(x) for x in user.entries]}
|
||||
|
||||
|
||||
@app.delete("/account", status_code=204)
|
||||
def delete_account(user: User = Depends(current_user), db: Session = Depends(get_db)):
|
||||
db.delete(user); db.commit()
|
||||
return Response(status_code=204)
|
||||
|
||||
Reference in New Issue
Block a user