feat(CG5-onboard): GET /onboard + POST /onboard/complete — kobler til /notify/webhook og /notify/email
This commit is contained in:
parent
50490b071d
commit
c3357d38aa
153
main.py
153
main.py
|
|
@ -8,6 +8,7 @@ CG3: static-mappe serveres fra /static/*.
|
|||
AUTH: /auth/login + /auth/callback for klient OAuth2 onboarding.
|
||||
CG5a: POST /notify/webhook — push til Slack/Teams/Chat/Discord/etc.
|
||||
CG5b: POST /notify/email — SendGrid digest til kunde-e-post.
|
||||
CG5-onboard: GET /onboard + POST /onboard/complete — klient-onboarding wizard.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -56,7 +57,7 @@ app = FastAPI(
|
|||
|
||||
# Paths exempt from IAP enforcement (health/readiness probes reach Cloud Run
|
||||
# directly without the IAP-injected x-goog-authenticated-user-email header).
|
||||
IAP_EXEMPT_PATHS = {"/health", "/healthz", "/readiness", "/liveness"}
|
||||
IAP_EXEMPT_PATHS = {"/health", "/healthz", "/readiness", "/liveness", "/onboard"}
|
||||
|
||||
@app.middleware("http")
|
||||
async def require_iap(request: Request, call_next):
|
||||
|
|
@ -183,6 +184,18 @@ class EmailNotifyRequest(BaseModel):
|
|||
payload: Optional[Dict[str, Any]] = Field(None, description="Valgfri rådata å inkludere i e-post")
|
||||
|
||||
|
||||
# ── ONBOARDING MODEL (CG5-onboard) ────────────────────────────────────────────
|
||||
class OnboardCompleteRequest(BaseModel):
|
||||
company: str
|
||||
contact: str
|
||||
email: str
|
||||
gcp_project: str
|
||||
tier: str
|
||||
channels: List[str]
|
||||
webhook_url: Optional[str] = None
|
||||
email_to: Optional[str] = None
|
||||
|
||||
|
||||
# ── NOTIFICATION SERVICE (CG5a + CG5b) ───────────────────────────────────────
|
||||
class NotificationService:
|
||||
"""
|
||||
|
|
@ -350,6 +363,144 @@ async def _notify_email(request: Request, req: EmailNotifyRequest):
|
|||
app.add_api_route("/notify/email", endpoint=require_auth(_notify_email), methods=["POST"])
|
||||
|
||||
|
||||
# ── ONBOARDING ENDPOINTS (CG5-onboard) ───────────────────────────────────────
|
||||
|
||||
@app.get("/onboard")
|
||||
def onboard_wizard():
|
||||
"""
|
||||
CG5-onboard: Server onboarding-wizard HTML.
|
||||
IAP-exempt — kunder når denne uten eksisterende sesjon.
|
||||
"""
|
||||
return FileResponse("static/onboard.html")
|
||||
|
||||
|
||||
@app.post("/onboard/complete")
|
||||
async def onboard_complete(req: OnboardCompleteRequest):
|
||||
"""
|
||||
CG5-onboard: Lagrer kundeprofil i Firestore og sender velkomstvarsler
|
||||
via eksisterende POST /notify/webhook og POST /notify/email.
|
||||
Ingen nye notification-endepunkter — gjenbruker _notifier singleton.
|
||||
"""
|
||||
# 1. Lagre i Firestore (best-effort — feiler ikke onboarding ved DB-feil)
|
||||
if db:
|
||||
try:
|
||||
db.collection("onboarded_customers").document(req.gcp_project).set({
|
||||
"company": req.company,
|
||||
"contact": req.contact,
|
||||
"email": req.email,
|
||||
"gcp_project": req.gcp_project,
|
||||
"tier": req.tier,
|
||||
"channels": req.channels,
|
||||
"webhook_url": req.webhook_url,
|
||||
"email_to": req.email_to,
|
||||
"onboarded_at": firestore.SERVER_TIMESTAMP,
|
||||
})
|
||||
print(f"[Onboard] {req.gcp_project} lagret i Firestore")
|
||||
except Exception as e:
|
||||
print(f"[Onboard] Firestore-feil (ikke kritisk): {e}", file=sys.stderr)
|
||||
|
||||
notify_results: Dict[str, Any] = {}
|
||||
|
||||
# 2. Webhook-velkomstmelding (via eksisterende /notify/webhook-logikk)
|
||||
if req.webhook_url and "webhook" in req.channels:
|
||||
wh_result = await _notifier.send_webhook(WebhookNotifyRequest(
|
||||
url=req.webhook_url,
|
||||
event="custom",
|
||||
title=f"🎉 {req.company} er koblet til CostGuard!",
|
||||
body=(
|
||||
f"Hei {req.contact}! CostGuard overvåker nå *{req.gcp_project}*. "
|
||||
f"Du mottar første kostnadsrapport innen 24 timer."
|
||||
),
|
||||
payload={
|
||||
"bedrift": req.company,
|
||||
"tier": req.tier,
|
||||
"prosjekt": req.gcp_project,
|
||||
"dato": datetime.date.today().isoformat(),
|
||||
},
|
||||
))
|
||||
notify_results["webhook"] = wh_result
|
||||
|
||||
# 3. Velkomst-e-post (via eksisterende /notify/email-logikk)
|
||||
if req.email_to and "email" in req.channels:
|
||||
TIER_LABELS = {
|
||||
"starter": "Starter ($499/mnd)",
|
||||
"guard": "Guard ($999/mnd)",
|
||||
"shield": "Shield ($1.999/mnd)",
|
||||
"enterprise": "Enterprise ($3.500+/mnd)",
|
||||
}
|
||||
tier_label = TIER_LABELS.get(req.tier, req.tier)
|
||||
em_result = await _notifier.send_email(EmailNotifyRequest(
|
||||
to=req.email_to,
|
||||
event="custom",
|
||||
subject=f"✅ Velkommen til CostGuard — {req.company}",
|
||||
body_html=f"""
|
||||
<div style='font-family:sans-serif;max-width:600px;margin:0 auto;padding:32px'>
|
||||
<div style='background:linear-gradient(135deg,#7c5cff,#00d4a8);padding:28px;
|
||||
border-radius:12px 12px 0 0;text-align:center'>
|
||||
<h1 style='color:#fff;margin:0;font-size:26px'>🎉 Velkommen til CostGuard!</h1>
|
||||
</div>
|
||||
<div style='background:#f9f9f9;padding:28px;border-radius:0 0 12px 12px'>
|
||||
<p style='font-size:16px'>Hei <strong>{req.contact}</strong>,</p>
|
||||
<p>CostGuard overvåker nå ditt GCP-prosjekt og vil varsle deg ved kostnadsspikes,
|
||||
anomalier og budsjettoverskridelser.</p>
|
||||
<table style='width:100%;border-collapse:collapse;margin:20px 0'>
|
||||
<tr style='background:#efefef'>
|
||||
<td style='padding:8px 12px;font-weight:700'>Bedrift</td>
|
||||
<td style='padding:8px 12px'>{req.company}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding:8px 12px;font-weight:700'>GCP-prosjekt</td>
|
||||
<td style='padding:8px 12px'><code>{req.gcp_project}</code></td>
|
||||
</tr>
|
||||
<tr style='background:#efefef'>
|
||||
<td style='padding:8px 12px;font-weight:700'>Tier</td>
|
||||
<td style='padding:8px 12px'>{tier_label}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding:8px 12px;font-weight:700'>Kanaler</td>
|
||||
<td style='padding:8px 12px'>{', '.join(req.channels)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Du mottar din første kostnadsrapport innen 24 timer.</p>
|
||||
<div style='text-align:center;margin:24px 0'>
|
||||
<a href='https://opax.vauco.no/static/costguard-dashboard.html'
|
||||
style='background:linear-gradient(135deg,#7c5cff,#00d4a8);
|
||||
color:#050509;padding:12px 28px;border-radius:99px;
|
||||
text-decoration:none;font-weight:700;font-size:14px'>
|
||||
Åpne dashboard →
|
||||
</a>
|
||||
</div>
|
||||
<hr style='border:none;border-top:1px solid #ddd'>
|
||||
<p style='font-size:12px;color:#999'>CostGuard by Vauco AS · opax.vauco.no</p>
|
||||
</div>
|
||||
</div>
|
||||
""",
|
||||
))
|
||||
notify_results["email"] = em_result
|
||||
|
||||
# 4. Intern Slack-notis til Vauco (opsjonelt — krever VAUCO_INTERNAL_WEBHOOK)
|
||||
internal_wh = os.environ.get("VAUCO_INTERNAL_WEBHOOK")
|
||||
if internal_wh:
|
||||
try:
|
||||
await _notifier.send_webhook(WebhookNotifyRequest(
|
||||
url=internal_wh,
|
||||
event="custom",
|
||||
title="🆕 Ny CostGuard-kunde onboardet",
|
||||
body=f"{req.company} ({req.gcp_project}) — tier: {req.tier}",
|
||||
payload={"kontakt": req.contact, "e-post": req.email, "kanaler": ", ".join(req.channels)},
|
||||
))
|
||||
except Exception as e:
|
||||
print(f"[Onboard] Intern Slack-feil: {e}", file=sys.stderr)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"customer": req.gcp_project,
|
||||
"company": req.company,
|
||||
"channels": req.channels,
|
||||
"notify": notify_results,
|
||||
}
|
||||
|
||||
|
||||
# ── ROOT LANDING PAGE ───────────────────────────────────────────────────
|
||||
@app.get("/")
|
||||
def root():
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user