feat: evolve MetaBloom into full product foundation
MetaBloom CI / app (push) Canceled after 0s
MetaBloom CI / api (push) Canceled after 0s

This commit is contained in:
AI Company
2026-07-21 19:24:59 +00:00
parent 082cd868d4
commit ea8b5be058
38 changed files with 610 additions and 58 deletions
+109
View File
@@ -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)