File size: 6,357 Bytes
c0d43a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105ee76
c0d43a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from fastapi import FastAPI, Depends, HTTPException, Request, status, Form
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import Counter, generate_latest, CONTENT_TYPE_LATEST
from sqlalchemy.orm import Session
from loguru import logger
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from starlette.templating import Jinja2Templates

from .config import Settings
from .database import Base, engine, get_db
from .models import *
from .schemas import *
from .security import *
from .tasks import celery_app, generate_pptx, generate_docx
from .telemetry import setup_tracing

import os

settings = Settings()

# Observability
setup_tracing()

# DB init
Base.metadata.create_all(bind=engine)

# FastAPI app
app = FastAPI(title="GrowthOps OS", version="0.1.0")

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

API_CALLS = Counter("growthops_api_calls", "Total API calls", ["path", "method", "status"])
templates = Jinja2Templates(directory="templates")

class AuditMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        response = None
        error_text = None
        try:
            response = await call_next(request)
            return response
        except Exception as e:
            error_text = str(e)
            logger.exception("Unhandled error")
            raise
        finally:
            try:
                tenant_id = None
                user_id = None
                auth = request.headers.get("authorization", "")
                if auth.startswith("Bearer "):
                    token = auth.split(" ", 1)[1]
                    try:
                        payload = parse_token(token)
                        db = next(get_db())
                        user = db.query(User).filter(User.email == payload.get("sub")).first()
                        if user:
                            tenant_id = user.tenant_id
                            user_id = user.id
                    except Exception:
                        pass

                db = next(get_db())
                al = AuditLog(
                    tenant_id=tenant_id,
                    user_id=user_id,
                    path=request.url.path,
                    method=request.method,
                    status_code=getattr(response, "status_code", 500),
                    meta={"client": request.client.host if request.client else None}
                )
                db.add(al)
                if error_text:
                    err = ErrorLog(tenant_id=tenant_id, user_id=user_id, path=request.url.path, error=error_text)
                    db.add(err)
                db.commit()

                API_CALLS.labels(path=request.url.path, method=request.method, status=str(getattr(response, "status_code", 500))).inc()
            except Exception as e:
                logger.error(f"audit failed: {e}")

app.add_middleware(AuditMiddleware)

@app.get("/healthz")
def healthz():
    return {"ok": True}

@app.get("/metrics")
def metrics():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

# ------------------ Auth ------------------
@app.post("/auth/bootstrap_admin", response_model=SimpleMessage)
def bootstrap_admin(body: dict, db: Session = Depends(get_db)):
    email = body.get("email")
    password = body.get("password")
    if not email or not password:
        raise HTTPException(400, "email/password required")
    user = db.query(User).filter(User.email == email).first()
    if user:
        return {"message": "exists"}

    tenant = db.query(Tenant).filter(Tenant.name == "default").first()
    if not tenant:
        tenant = Tenant(name="default")
        db.add(tenant)
        db.commit()
        db.refresh(tenant)

    u = User(email=email, password_hash=hash_password(password), tenant_id=tenant.id, is_tenant_admin=True)
    db.add(u)
    db.commit()
    return {"message": "admin created"}

@app.post("/auth/register", response_model=UserOut)
def register(user: UserCreate, current: User = Depends(require_tenant_admin), db: Session = Depends(get_db)):
    if db.query(User).filter(User.email == user.email).first():
        raise HTTPException(400, "Email already exists")
    u = User(email=user.email, password_hash=hash_password(user.password), tenant_id=user.tenant_id, is_tenant_admin=user.is_tenant_admin)
    db.add(u)
    db.commit()
    db.refresh(u)
    return u

@app.post("/auth/login", response_model=TokenOut)
def login(req: LoginRequest, db: Session = Depends(get_db)):
    user = db.query(User).filter(User.email == req.email).first()
    if not user or not verify_password(req.password, user.password_hash):
        raise HTTPException(401, "Invalid credentials")
    token = create_access_token(sub=user.email, expires_in=3600)
    return {"access_token": token, "token_type": "bearer"}

@app.get("/auth/me", response_model=UserOut)
def me(current: User = Depends(get_current_user)):
    return current

# ------------------ Tenants / Plans ------------------
@app.post("/tenants", response_model=TenantOut)
def create_tenant_api(t: TenantCreate, current: User = Depends(require_tenant_admin), db: Session = Depends(get_db)):
    if db.query(Tenant).filter(Tenant.name == t.name).first():
        raise HTTPException(400, "Tenant exists")
    tenant = Tenant(name=t.name)
    db.add(tenant)
    db.commit()
    db.refresh(tenant)
    return tenant

@app.get("/tenants", response_model=list[TenantOut])
def list_tenants(current: User = Depends(require_tenant_admin), db: Session = Depends(get_db)):
    return db.query(Tenant).all()

@app.post("/plans", response_model=SimpleMessage)
def create_plan(p: PlanCreate, current: User = Depends(require_tenant_admin), db: Session = Depends(get_db)):
    plan = Plan(name=p.name, monthly_quota=p.monthly_quota, features=p.features)
    db.add(plan)
    db.commit()
    return {"message": "ok"}

@app.post("/subscriptions", response_model=SimpleMessage)
def create_subscription(s: SubscriptionCreate, current: User = Depends(require_tenant_admin), db: Session = Depends(get_db)):
    sub = Subscription(tenant_id=s.tenant_id, plan_id=s.plan_id, active=True)