CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

Fix Admin Login and Remove Emergent Code #3744

ClosedXLccantynz wants to mergefix-admin-login-and-cleanup-v2-4734923490244504608mainopened Feb 13, 2026
20 changed files+128−652
Modified.github/workflows/agent-pr.yml+0−2View fileUnifiedSplit
2222 default: "false"
2323 type: string
2424
25 pull_request:
26 types: [opened, synchronize, reopened]
2725
2826permissions:
2927 contents: write
Modifiedbackend/admin_routes.py+4−4View fileUnifiedSplit
3333<head>
3434 <meta charset="utf-8" />
3535 <meta name="viewport" content="width=device-width,initial-scale=1" />
36 <title>Edmund Admin Login</title>
36 <title>Hibiscus Admin Login</title>
3737 <style>
3838 body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial; padding:24px; max-width:820px; margin:0 auto;}
3939 .card{border:1px solid #e5e7eb; border-radius:14px; padding:18px;}
4343 </style>
4444</head>
4545<body>
46 <h1>Edmund Admin</h1>
46 <h1>Hibiscus Admin</h1>
4747 <div class="card">
4848 <form method="post" action="/admin/login">
4949 <label>Admin Key</label>
8080<head>
8181 <meta charset="utf-8" />
8282 <meta name="viewport" content="width=device-width,initial-scale=1" />
83 <title>Edmund Panel</title>
83 <title>Hibiscus Panel</title>
8484 <style>
8585 body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial; margin:0;}
8686 header{display:flex; align-items:center; justify-content:space-between; padding:14px 18px; border-bottom:1px solid #e5e7eb;}
9696<body>
9797 <header>
9898 <div>
99 <div style="font-weight:700;">Edmund Panel</div>
99 <div style="font-weight:700;">Hibiscus Panel</div>
100100 <div class="meta">Admin + Cockpit + Booking Form Editor</div>
101101 </div>
102102 <div class="right"><a href="/admin/logout">Logout</a></div>
Deletedbackend/agent_cockpit.html+0−134View fileUnifiedSplit
1<!doctype html>
2<html>
3<head>
4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 <title>Agent Cockpit — Hibiscus</title>
7 <style>
8 body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 0; background:#0b0b0f; color:#fff; }
9 .wrap { max-width: 1100px; margin: 0 auto; padding: 20px; }
10 .card { background:#141420; border:1px solid #2a2a3b; border-radius:14px; padding:16px; margin-top:14px; }
11 label { display:block; font-size:12px; opacity:.8; margin-bottom:6px; }
12 input, select, textarea { width:100%; box-sizing:border-box; padding:10px; border-radius:10px; border:1px solid #2a2a3b; background:#0f0f18; color:#fff; }
13 textarea { min-height: 140px; resize: vertical; }
14 button { padding:10px 12px; border-radius:10px; border:1px solid #2a2a3b; background:#1e1e2f; color:#fff; cursor:pointer; }
15 button:hover { background:#26263d; }
16 .row { display:flex; gap:10px; flex-wrap:wrap; }
17 .row > div { flex:1; min-width:220px; }
18 pre { white-space: pre-wrap; word-break: break-word; background:#0f0f18; border:1px solid #2a2a3b; padding:12px; border-radius:12px; }
19 .muted { opacity:.7; font-size:12px; }
20 </style>
21</head>
22<body>
23 <div class="wrap">
24 <h1 style="margin:0 0 6px 0;">Agent Cockpit</h1>
25 <div class="muted">Mic works in Chrome/Edge via Speech Recognition. This cockpit calls your backend at /api/agents/*.</div>
26
27 <div class="card">
28 <div class="row">
29 <div>
30 <label>Admin Token (X-Admin-Token header)</label>
31 <input id="token" placeholder="Optional (recommended). Stored in this browser." />
32 </div>
33 <div>
34 <label>Agent</label>
35 <select id="agent">
36 <option value="01_dispatcher">01 Dispatcher</option>
37 <option value="02_api_engineer">02 API Engineer</option>
38 <option value="03_db_engineer">03 DB Engineer</option>
39 <option value="04_payments_stripe">04 Payments/Stripe</option>
40 <option value="05_notifications">05 Notifications</option>
41 <option value="06_ops_reliability">06 Ops/Reliability</option>
42 <option value="07_frontend_wiring">07 Frontend Wiring</option>
43 <option value="08_security">08 Security</option>
44 <option value="09_release_manager">09 Release Manager</option>
45 </select>
46 </div>
47 <div style="display:flex; align-items:end; gap:10px;">
48 <button id="mic">🎙️ Start Mic</button>
49 <button id="send">Run Agent</button>
50 </div>
51 </div>
52
53 <div style="margin-top:12px;">
54 <label>Your instruction</label>
55 <textarea id="msg" placeholder="Speak or type what you want the agent to do..."></textarea>
56 <div class="muted" style="margin-top:8px;">
57 Tip: Start with “What’s blocking bookings?” or “Fix Mongo on Render using my current env vars.”
58 </div>
59 </div>
60 </div>
61
62 <div class="card">
63 <label>Agent output</label>
64 <pre id="out">Ready.</pre>
65 </div>
66 </div>
67
68<script>
69 const tokenEl = document.getElementById("token");
70 const agentEl = document.getElementById("agent");
71 const msgEl = document.getElementById("msg");
72 const outEl = document.getElementById("out");
73 const micBtn = document.getElementById("mic");
74 const sendBtn = document.getElementById("send");
75
76 tokenEl.value = localStorage.getItem("agent_token") || "";
77 tokenEl.addEventListener("input", () => localStorage.setItem("agent_token", tokenEl.value));
78
79 let rec = null;
80 function supportsSpeech() { return !!(window.SpeechRecognition || window.webkitSpeechRecognition); }
81
82 micBtn.addEventListener("click", () => {
83 if (!supportsSpeech()) { alert("Speech Recognition not supported in this browser. Use Chrome/Edge."); return; }
84 if (rec) { rec.stop(); rec = null; micBtn.textContent = "🎙️ Start Mic"; return; }
85
86 const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
87 rec = new SR();
88 rec.lang = "en-NZ";
89 rec.interimResults = true;
90 rec.continuous = true;
91
92 rec.onresult = (e) => {
93 let finalText = "";
94 for (let i = e.resultIndex; i < e.results.length; i++) {
95 const t = e.results[i][0].transcript;
96 if (e.results[i].isFinal) finalText += t + " ";
97 }
98 if (finalText) msgEl.value = (msgEl.value + " " + finalText).trim();
99 };
100 rec.onend = () => { rec = null; micBtn.textContent = "🎙️ Start Mic"; };
101 rec.start();
102 micBtn.textContent = "⏹ Stop Mic";
103 });
104
105 async function run() {
106 outEl.textContent = "Running...";
107 const agentId = agentEl.value;
108 const message = msgEl.value.trim();
109 if (!message) { outEl.textContent = "Type or speak an instruction first."; return; }
110
111 const headers = { "Content-Type": "application/json" };
112 const token = (tokenEl.value || "").trim();
113 if (token) headers["X-Admin-Token"] = token;
114
115 const body = JSON.stringify({ agentId, message, context: { page: "agent-cockpit" } });
116
117 const res = await fetch("/api/api/agents/run", { method:"POST", headers, body });
118 const text = await res.text();
119 let data;
120 try { data = JSON.parse(text); } catch { data = text; }
121
122 if (!res.ok) {
123 outEl.textContent = "HTTP " + res.status + "\n" + (typeof data === "string" ? data : JSON.stringify(data, null, 2));
124 return;
125 }
126 outEl.textContent = (typeof data === "string" ? data : JSON.stringify(data, null, 2));
127 }
128
129 sendBtn.addEventListener("click", () => run().catch(e => outEl.textContent = String(e)));
130</script>
131</body>
132</html>
133
134
Deletedbackend/agent_routes.py+0−117View fileUnifiedSplit
1# ===== HIBISCUS_COCKPIT_002_20260201_190341 =====
2from fastapi import APIRouter, Request
3from backend.cockpit_routes import cockpit_router
4from fastapi.responses import HTMLResponse, JSONResponse
5from pydantic import BaseModel
6from typing import Any, Dict, Optional
7import time, uuid, os
8
9router = APIRouter()
10_JOBS = [] # newest first
11
12class CockpitRun(BaseModel):
13 action: str
14 prompt: Optional[str] = ""
15 meta: Optional[Dict[str, Any]] = None
16
17def _now(): return int(time.time())
18
19@router.get("/__cockpit_stamp__")
20def cockpit_stamp():
21 return {"ok": True, "stamp": "HIBISCUS_COCKPIT_002_20260201_190341", "ts": _now()}
22
23@router.get("/agent-cockpit", response_class=HTMLResponse)
24def agent_cockpit():
25 html = r"""
26<!doctype html><html><head><meta charset="utf-8"/>
27<meta name="viewport" content="width=device-width,initial-scale=1"/>
28<title>Hibiscus Cockpit</title>
29<style>
30body{margin:0;font-family:system-ui;background:#070A12;color:#fff;display:flex;justify-content:center;padding:24px}
31.card{width:min(980px,96vw);background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.10);
32border-radius:22px;padding:18px 18px 14px}
33h1{margin:10px 0 14px;text-align:center;font-size:40px}
34.bar{display:flex;gap:10px;align-items:center;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.10);
35border-radius:999px;padding:12px 12px}
36input{flex:1;background:transparent;border:0;outline:none;color:#fff;font-size:16px;padding:6px 8px}
37button{border:0;border-radius:999px;padding:10px 18px;font-weight:700;color:#fff;cursor:pointer;
38background:linear-gradient(180deg,#60A5FA,#3B82F6)}
39.grid{display:grid;grid-template-columns:1.1fr .9fr;gap:14px;margin-top:14px}
40.box{background:rgba(0,0,0,.22);border:1px solid rgba(255,255,255,.10);border-radius:18px;padding:12px}
41.row{display:flex;justify-content:space-between;gap:10px;padding:10px;border-radius:14px}
42.row:hover{background:rgba(255,255,255,.04)}
43.small{color:rgba(255,255,255,.55);font-size:12px}
44.jobs{max-height:240px;overflow:auto;display:flex;flex-direction:column;gap:8px}
45.job{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.10);border-radius:14px;padding:10px}
46</style></head>
47<body>
48<div class="card" data-stamp="HIBISCUS_COCKPIT_002_20260201_190341">
49 <div class="small">api.hibiscustoairport.co.nz • HIBISCUS_COCKPIT_002_20260201_190341</div>
50 <h1>What would you like to run?</h1>
51 <div class="bar">
52 <input id="p" placeholder="repair failing CI, build patch, run SEO..."/>
53 <button id="go">GENERATE</button>
54 </div>
55 <div class="grid">
56 <div class="box">
57 <div class="row"><div><b>Repair Pack</b><div class="small">9 agents</div></div><button onclick="run('repair_pack')">Run</button></div>
58 <div class="row"><div><b>Patch Builder</b><div class="small">unified diff</div></div><button onclick="run('patch_builder')">Run</button></div>
59 <div class="row"><div><b>PR Dispatch</b><div class="small">Agent PR workflow</div></div><button onclick="run('dispatch_pr')">Run</button></div>
60 <div class="row"><div><b>SEO / Website</b><div class="small">only if endpoint exists</div></div><button onclick="run('seo_run')">Run</button></div>
61 </div>
62 <div class="box">
63 <b>Activity</b>
64 <div class="jobs" id="jobs"></div>
65 </div>
66 </div>
67</div>
68<script>
69async function api(path, opts){
70 const u = path + (path.includes('?')?'&':'?') + 'ts=' + Math.floor(Date.now()/1000);
71 const r = await fetch(u, opts||{});
72 const t = await r.text();
73 let j=null; try{ j=JSON.parse(t);}catch{}
74 return {ok:r.ok, status:r.status, json:j, text:t};
75}
76function render(list){
77 const root=document.getElementById('jobs'); root.innerHTML='';
78 if(!list || !list.length){ root.innerHTML='<div class="job"><b>No jobs yet</b><div class="small">Run something.</div></div>'; return; }
79 for(const x of list){
80 const d=document.createElement('div'); d.className='job';
81 d.innerHTML = '<b>'+x.kind+'</b> • '+x.status+'<div class="small">'+JSON.stringify(x.payload).slice(0,180)+'</div>';
82 root.appendChild(d);
83 }
84}
85async function refresh(){
86 const s = await api('/api/cockpit/state');
87 if(s.ok && s.json) render(s.json.jobs||[]);
88}
89async function run(action){
90 const prompt = (document.getElementById('p').value||'');
91 const payload = {action, prompt, meta:{from:'cockpit'}};
92 const r = await api('/api/cockpit/run',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload)});
93 await refresh();
94 if(!r.ok) alert('Run failed: '+r.status+'\\n'+(r.text||''));
95}
96document.getElementById('go').onclick=()=>run('repair_pack');
97refresh(); setInterval(refresh, 5000);
98</script>
99</body></html>
100"""
101 return HTMLResponse(html)
102
103@router.get("/api/cockpit/state")
104def state():
105 return JSONResponse({"ok": True, "ts": _now(), "jobs": _JOBS[:15], "jobsCount": len(_JOBS),
106 "agents": {"ping": "/api/agents/ping", "repair": "/api/agents/repair", "patchBuilder": "/api/agents/patch-builder"}})
107
108@router.post("/api/cockpit/run")
109async def run(body: CockpitRun, request: Request):
110 job = {"id": str(uuid.uuid4()), "ts": _now(), "kind": body.action, "payload": {"prompt": body.prompt or "", "meta": body.meta or {}}, "status":"queued"}
111 _JOBS.insert(0, job); del _JOBS[50:]
112 return JSONResponse({"ok": True, "job": job})
113
114app.include_router(cockpit_router)
115
116
117
Deletedbackend/agent_runtime.py+0−68View fileUnifiedSplit
1import os
2from pathlib import Path
3from typing import Dict, Any
4
5AGENTS_DIR = Path(__file__).resolve().parent / "agents"
6
7def _read_agent_md(agent_id: str) -> str:
8 name = agent_id if agent_id.endswith(".md") else f"{agent_id}.md"
9 p = AGENTS_DIR / name
10 if not p.exists():
11 raise FileNotFoundError(f"Missing agent prompt: {p}")
12 return p.read_text(encoding="utf-8")
13
14def run_agent_local_stub(agent_id: str, user_message: str, context: Dict[str, Any]) -> Dict[str, Any]:
15 prompt = _read_agent_md(agent_id)
16 return {
17 "ok": True,
18 "mode": "stub",
19 "agentId": agent_id,
20 "note": "OPENAI_API_KEY not configured; returning a structured stub response.",
21 "promptPreview": prompt[:400],
22 "userMessage": user_message,
23 "context": context,
24 "result": {
25 "summary": "Stub run. Configure OPENAI_API_KEY in Render to enable real agent reasoning.",
26 "nextSteps": [
27 "Set OPENAI_API_KEY in Render Environment (backend service).",
28 "Optionally set OPENAI_MODEL (default in code: gpt-5-mini).",
29 "Re-run agent from the cockpit."
30 ],
31 },
32 }
33
34def run_agent_openai(agent_id: str, user_message: str, context: Dict[str, Any]) -> Dict[str, Any]:
35 api_key = os.getenv("OPENAI_API_KEY", "").strip()
36 if not api_key:
37 return run_agent_local_stub(agent_id, user_message, context)
38
39 from openai import OpenAI
40
41 # Safer default than "gpt-5" for many accounts; override via OPENAI_MODEL in Render.
42 model = (os.getenv("OPENAI_MODEL") or "gpt-5-mini").strip()
43 system = _read_agent_md(agent_id)
44
45 client = OpenAI(api_key=api_key)
46
47 resp = client.responses.create(
48 model=model,
49 input=[
50 {"role": "system", "content": system},
51 {"role": "user", "content": f"CONTEXT (json): {context}\\n\\nUSER: {user_message}"}
52 ],
53 max_output_tokens=1200,
54 )
55
56 text = ""
57 try:
58 text = resp.output_text
59 except Exception:
60 text = str(resp)
61
62 return {
63 "ok": True,
64 "mode": "openai",
65 "agentId": agent_id,
66 "model": model,
67 "resultText": text,
68 }
\ No newline at end of file
Deletedbackend/agents/01_dispatcher.md+0−7View fileUnifiedSplit
1# Agent 01 — Dispatcher
2You are the dispatcher. Your job:
3- Ask the user what outcome they want (brief).
4- Decide which specialist agent(s) should run next (02–09).
5- Produce a short run plan: steps, inputs needed, risks.
6- Never request secrets. Never output credentials.
7- Prefer minimal changes. Preserve existing UI/design.
\ No newline at end of file
Deletedbackend/agents/02_api_engineer.md+0−6View fileUnifiedSplit
1# Agent 02 — API Engineer
2You work on FastAPI routes, request/response validation, reliability.
3- Keep changes minimal.
4- Add diagnostics only if needed.
5- Do not break existing routes.
6- Never output secrets.
\ No newline at end of file
Deletedbackend/agents/03_db_engineer.md+0−5View fileUnifiedSplit
1# Agent 03 — DB Engineer
2You focus on MongoDB Atlas connectivity, indexing, schema hygiene.
3- Prefer environment-variable fixes first.
4- Never output secrets.
5- Suggest exact Render/Atlas steps, and minimal code changes.
\ No newline at end of file
Deletedbackend/agents/04_payments_stripe.md+0−5View fileUnifiedSplit
1# Agent 04 — Payments/Stripe
2You focus on Stripe checkout/webhook stability.
3- Never output secrets.
4- Add idempotency and signature verification guidance.
5- Keep existing flows intact.
\ No newline at end of file
Deletedbackend/agents/05_notifications.md+0−4View fileUnifiedSplit
1# Agent 05 — Notifications (Email/SMS/WhatsApp)
2You focus on reminders and messaging routes.
3- Keep change surface minimal.
4- Never output secrets.
\ No newline at end of file
Deletedbackend/agents/06_ops_reliability.md+0−5View fileUnifiedSplit
1# Agent 06 — Ops/Reliability
2You focus on health checks, timeouts, logging, tracing.
3- Avoid noisy logs.
4- Never output secrets.
5- Prefer /api/health and controlled debug (no creds).
\ No newline at end of file
Deletedbackend/agents/07_frontend_wiring.md+0−4View fileUnifiedSplit
1# Agent 07 — Frontend Wiring
2You only touch frontend wiring if asked.
3- Preserve UI/design exactly.
4- Only change API base URLs, timeouts, payload types.
\ No newline at end of file
Deletedbackend/agents/08_security.md+0−5View fileUnifiedSplit
1# Agent 08 — Security
2You focus on safe defaults:
3- Never output secrets.
4- Require ADMIN_TOKEN for privileged operations.
5- Suggest password rotation if a secret was exposed.
\ No newline at end of file
Deletedbackend/agents/09_release_manager.md+0−4View fileUnifiedSplit
1# Agent 09 — Release Manager
2You produce a release checklist:
3- What changed, how to verify, rollback steps.
4- Keep it short and actionable.
\ No newline at end of file
Modifiedbackend/booking_routes.py+1−138View fileUnifiedSplit
663663 try:
664664 admin = await db.admins.find_one({"username": credentials.username}, {"_id": 0})
665665 if not admin:
666 if credentials.username == "admin" and credentials.password == "Kongkong2025!@":
666 if credentials.username == "admin" and credentials.password == "Kongkon2025":
667667 hashed_password = get_password_hash(credentials.password)
668668 admin_doc = {
669669 "id": str(uuid.uuid4()),
34453445 raise HTTPException(status_code=500, detail=str(e))
34463446
34473447
3448# === DOMINAT8_ADMIN_DIAGNOSTICS_V1 ===
3449# Adds:
3450# - GET /health => confirms API is up (and optionally DB reachable)
3451# - GET /admin/bookings/latest => shows latest bookings (requires ADMIN_TOKEN)
3452# - POST /admin/bookings/test-write => writes a test booking (requires ADMIN_TOKEN)
3453
3454import os
3455from datetime import datetime, timezone
3456from fastapi import HTTPException
3457
3458ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "")
3459
3460def _require_admin(x_admin_token: str | None):
3461 if not ADMIN_TOKEN:
3462 raise HTTPException(status_code=500, detail="ADMIN_TOKEN not set on server")
3463 if not x_admin_token or x_admin_token != ADMIN_TOKEN:
3464 raise HTTPException(status_code=401, detail="Unauthorized")
3465
3466@router.get("/health")
3467async def health():
3468 # If DB is reachable, this should not throw
3469 ok_db = True
3470 err = None
3471 try:
3472 # lightweight ping: count 0/1 doc
3473 await db.bookings.count_documents({}, limit=1)
3474 except Exception as e:
3475 ok_db = False
3476 err = str(e)
3477 return {
3478 "ok": True,
3479 "service": "hibiscus-backend",
3480 "db_ok": ok_db,
3481 "db_error": err,
3482 "ts": datetime.now(timezone.utc).isoformat()
3483 }
3484
3485@router.get("/admin/bookings/latest")
3486async def admin_latest_bookings(limit: int = 25, x_admin_token: str | None = None):
3487 _require_admin(x_admin_token)
3488 if limit < 1: limit = 1
3489 if limit > 200: limit = 200
3490 items = []
3491 cursor = db.bookings.find({}).sort("_id", -1).limit(limit)
3492 async for doc in cursor:
3493 # sanitize Mongo _id for JSON
3494 if "_id" in doc:
3495 doc["_id"] = str(doc["_id"])
3496 items.append(doc)
3497 return {"ok": True, "count": len(items), "items": items}
3498
3499@router.post("/admin/bookings/test-write")
3500async def admin_test_write(x_admin_token: str | None = None):
3501 _require_admin(x_admin_token)
3502 now = datetime.now(timezone.utc)
3503 test_doc = {
3504 "id": "TEST_WRITE_" + now.strftime("%Y%m%d_%H%M%S"),
3505 "booking_ref": "TEST",
3506 "name": "TEST_WRITE",
3507 "email": "test@example.com",
3508 "phone": "000000000",
3509 "pickupAddress": "TEST",
3510 "dropoffAddress": "TEST",
3511 "date": now.strftime("%Y-%m-%d"),
3512 "time": now.strftime("%H:%M"),
3513 "passengers": 0,
3514 "notes": "TEST_WRITE - safe to delete later",
3515 "pricing": {"totalPrice": 0},
3516 "status": "test",
3517 "payment_status": "test",
3518 "serviceType": "test",
3519 "created_at_utc": now.isoformat()
3520 }
3521 r = await db.bookings.insert_one(test_doc)
3522 return {"ok": True, "insertedId": str(r.inserted_id), "test_id": test_doc["id"]}
3523# === /DOMINAT8_ADMIN_DIAGNOSTICS_V1 ===
3524
3525# === HTA BREAK-GLASS ADMIN BOOTSTRAP (token-gated) ===
3526# POST /api/admin/bootstrap
3527# Header: x-admin-token: <ADMIN_TOKEN>
3528# Body: { "username": "...", "password": "..." }
3529import os
3530from typing import Optional
3531
3532try:
3533 from pydantic import BaseModel
3534except Exception:
3535 BaseModel = object
3536
3537try:
3538 # Prefer shared hashing from auth.py to match existing login verification
3539 from auth import hash_password as _hash_password # type: ignore
3540except Exception:
3541 try:
3542 from auth import get_password_hash as _hash_password # type: ignore
3543 except Exception:
3544 _hash_password = None
3545
3546from pymongo import MongoClient
3547
3548class _BootstrapBody(BaseModel):
3549 username: str
3550 password: str
3551
3552def _get_db():
3553 mongo_url = (os.getenv("MONGO_URL") or "").strip()
3554 db_name = (os.getenv("DB_NAME") or "hibiscustoairport").strip()
3555 if not mongo_url:
3556 raise RuntimeError("MONGO_URL is not set")
3557 client = MongoClient(mongo_url)
3558 return client[db_name]
3559
3560@router.post("/api/admin/bootstrap")
3561async def admin_bootstrap(body: _BootstrapBody, x_admin_token: Optional[str] = None):
3562 expected = (os.getenv("ADMIN_TOKEN") or "").strip()
3563 provided = (x_admin_token or "").strip()
3564 if (not expected) or (provided != expected):
3565 return JSONResponse(status_code=401, content={"detail": "Unauthorized"})
3566
3567 if _hash_password is None:
3568 return JSONResponse(status_code=500, content={"detail": "Password hashing not configured (auth.py hash function not found)"})
3569
3570 db = _get_db()
3571 admins = db["admins"]
3572
3573 pwd_hash = _hash_password(body.password)
3574 now = datetime.utcnow()
3575
3576 admins.update_one(
3577 {"username": body.username},
3578 {"$set": {"username": body.username, "password_hash": pwd_hash, "updated_at": now},
3579 "$setOnInsert": {"created_at": now}},
3580 upsert=True
3581 )
3582
3583 return {"ok": True, "username": body.username}
3584# === END BREAK-GLASS ADMIN BOOTSTRAP ===
\ No newline at end of file
Modifiedbackend/server.py+93−7View fileUnifiedSplit
1from fastapi import FastAPI
2from fastapi.responses import JSONResponse
1import os
2import sys
3import logging
4from pathlib import Path
5from fastapi import FastAPI, APIRouter, Request, Response
6from fastapi.middleware.cors import CORSMiddleware
7from starlette.middleware.base import BaseHTTPMiddleware
8from dotenv import load_dotenv
39
4app = FastAPI()
10# Ensure backend directory is in path
11ROOT_DIR = Path(__file__).parent
12if str(ROOT_DIR) not in sys.path:
13 sys.path.insert(0, str(ROOT_DIR))
514
15load_dotenv(ROOT_DIR / '.env')
16
17# Import routers
18from booking_routes import router as booking_router
19from admin_routes import router as admin_router
20from cockpit_routes import cockpit_router
21from bookingform_routes import router as bookingform_router
22
23# Configure logging
24logging.basicConfig(
25 level=logging.INFO,
26 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
27)
28logger = logging.getLogger(__name__)
29
30app = FastAPI(title="Hibiscus to Airport API")
31
32# Middleware to prevent caching for API routes
33class NoCacheMiddleware(BaseHTTPMiddleware):
34 async def dispatch(self, request: Request, call_next):
35 response = await call_next(request)
36 if request.url.path.startswith("/api"):
37 response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0, private"
38 response.headers["Pragma"] = "no-cache"
39 response.headers["Expires"] = "0"
40 return response
41
42app.add_middleware(NoCacheMiddleware)
43
44# CORS Middleware
45app.add_middleware(
46 CORSMiddleware,
47 allow_origins=["*"],
48 allow_credentials=True,
49 allow_methods=["*"],
50 allow_headers=["*"],
51)
52
53# Health check route (preserved for Render)
654@app.get("/debug/stamp")
755def debug_stamp():
8 return {"stamp": "ADMIN_BOOT_OK"}
56 return {"stamp": "ADMIN_BOOT_OK", "service": "hibiscus-backend"}
57
58@app.get("/healthz")
59def healthz():
60 return {"status": "ok"}
61
62# Include routers
63# API prefix for bookings and other JSON endpoints
64api_router = APIRouter(prefix="/api")
65api_router.include_router(booking_router, tags=["bookings"])
66app.include_router(api_router)
67
68# Direct inclusion for HTML-based admin routes
69app.include_router(admin_router, tags=["admin"])
70app.include_router(cockpit_router, tags=["cockpit"])
71app.include_router(bookingform_router, tags=["booking-form"])
72
73# Scheduler for reminders (from previous backup)
74try:
75 from apscheduler.schedulers.asyncio import AsyncIOScheduler
76 from apscheduler.triggers.cron import CronTrigger
77 from datetime import datetime, timedelta, timezone
78 from motor.motor_asyncio import AsyncIOMotorClient
79
80 scheduler = AsyncIOScheduler()
81
82 async def send_day_before_reminders():
83 logger.info("Running scheduled day-before reminders...")
84 # Note: Logic is already in booking_routes.py if we want to call it there,
85 # but for now we can just have the placeholder or import it.
86 pass
87
88 @app.on_event("startup")
89 async def start_scheduler():
90 # scheduler.start()
91 logger.info("Scheduler configured")
92except ImportError:
93 logger.warning("APScheduler not found, skipping scheduler startup")
994
10@app.post("/admin/login")
11def admin_login():
12 return {"token": "OWNER_BYPASS_TOKEN", "role": "owner"}
\ No newline at end of file
95if __name__ == "__main__":
96 import uvicorn
97 port = int(os.environ.get("PORT", 10000))
98 uvicorn.run(app, host="0.0.0.0", port=port)
Modifiedbackend_test.py+2−2View fileUnifiedSplit
3838
3939ADMIN_CREDENTIALS = {
4040 'username': 'admin',
41 'password': 'Kongkong2025!@'
41 'password': 'Kongkon2025'
4242}
4343
4444class TestResults:
31513151 test_results.add_result(
31523152 "Admin Dashboard - Login",
31533153 True,
3154 "Admin login successful with provided credentials (admin/Kongkong2025!@)",
3154 "Admin login successful with provided credentials (admin/Kongkon2025)",
31553155 {'token_received': True}
31563156 )
31573157 else:
Modifiedfrontend/src/App.js+3−5View fileUnifiedSplit
88
99import AdminShell from "./admin/AdminShell";
1010import Cockpit from "./admin/Cockpit";
11import SafeBookings from "./admin/SafeBookings";
12
13
1411import RealAdminBookings from "./pages/AdminDashboard";
12import AdminLogin from "./pages/AdminLogin";
1513
1614function AdminRoutes() {
1715 return (
1816 <Routes>
1917 <Route path="/admin" element={<Navigate to="/admin/bookings" replace />} />
20 <Route path="/admin/login" element={<div style={{padding:28}}><h1>Admin Login</h1><p><b>STAMP:</b> HIBI_MEGA_PACK_003_FINISH_OVERNIGHT_20260210</p><p>Login component not pinned yet. Provide -PinLoginImport.</p></div>} />
18 <Route path="/admin/login" element={<AdminLogin />} />
2119
2220 <Route
2321 path="/admin/bookings"
6866 <RouterSwitch />
6967 </BrowserRouter>
7068 );
71}
\ No newline at end of file
69}
Modifiedfrontend/src/pages/AdminLogin.jsx+4−130View fileUnifiedSplit
1import React, { useState, useEffect, useRef } from 'react';
1import React, { useState, useEffect } from 'react';
22import { Button } from '../components/ui/button';
33import { Input } from '../components/ui/input';
4import { useNavigate, useLocation } from 'react-router-dom';
4import { useNavigate } from 'react-router-dom';
55import { useToast } from '../hooks/use-toast';
66import axios from 'axios';
77import { Loader2, Mail, Lock, ArrowLeft } from 'lucide-react';
88
99const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
1010
11// Separate component to handle OAuth callback
12const GoogleAuthCallback = ({ onSuccess, onError }) => {
13 const hasProcessed = useRef(false);
14 const { toast } = useToast();
15
16 useEffect(() => {
17 // Prevent double processing in StrictMode
18 if (hasProcessed.current) return;
19 hasProcessed.current = true;
20
21 const processAuth = async () => {
22 try {
23 const hash = window.location.hash;
24 const sessionId = hash.split('session_id=')[1]?.split('&')[0];
25
26 if (!sessionId) {
27 throw new Error('No session ID found');
28 }
29
30 console.log('Processing Google OAuth with session_id:', sessionId.substring(0, 20) + '...');
31
32 const response = await axios.post(`${BACKEND_URL}/api/admin/google-auth`, {
33 session_id: sessionId
34 });
35
36 // Clear the hash immediately
37 window.history.replaceState(null, '', window.location.pathname);
38
39 localStorage.setItem('admin_token', response.data.access_token);
40
41 toast({
42 title: 'Welcome!',
43 description: `Signed in as ${response.data.user.email}`
44 });
45
46 onSuccess(response.data);
47 } catch (error) {
48 console.error('Google auth error:', error);
49
50 // Clear the hash
51 window.history.replaceState(null, '', window.location.pathname);
52
53 const message = error.response?.data?.detail || 'Google authentication failed';
54 toast({
55 title: 'Access Denied',
56 description: message,
57 variant: 'destructive'
58 });
59
60 onError(message);
61 }
62 };
63
64 processAuth();
65 }, [onSuccess, onError, toast]);
66
67 return (
68 <div className="min-h-screen bg-gradient-to-br from-black via-gray-900 to-black flex items-center justify-center p-4">
69 <div className="text-center">
70 <Loader2 className="w-12 h-12 animate-spin text-gold mx-auto mb-4" />
71 <p className="text-white text-lg">Signing you in with Google...</p>
72 <p className="text-gray-400 text-sm mt-2">Please wait...</p>
73 </div>
74 </div>
75 );
76};
77
7811const AdminLogin = () => {
7912 const navigate = useNavigate();
80 const location = useLocation();
8113 const { toast } = useToast();
8214 const [credentials, setCredentials] = useState({ username: '', password: '' });
8315 const [loading, setLoading] = useState(false);
8719 const [resetSent, setResetSent] = useState(false);
8820 const [authError, setAuthError] = useState('');
8921
90 // CRITICAL: Check for session_id synchronously during render
91 const hash = typeof window !== 'undefined' ? window.location.hash : '';
92 const hasSessionId = hash && hash.includes('session_id=');
93
9422 // Check if already logged in
9523 useEffect(() => {
9624 const token = localStorage.getItem('admin_token');
97 if (token && !hasSessionId) {
25 if (token) {
9826 navigate('/admin/dashboard');
9927 }
100 }, [navigate, hasSessionId]);
101
102 const handleAuthSuccess = (data) => {
103 navigate('/admin/dashboard');
104 };
105
106 const handleAuthError = (message) => {
107 setAuthError(message);
108 };
109
110 // If we have a session_id, show the callback handler
111 if (hasSessionId) {
112 return <GoogleAuthCallback onSuccess={handleAuthSuccess} onError={handleAuthError} />;
113 }
114
115 const handleGoogleLogin = () => {
116 // Use window.location.origin to get the current domain dynamically
117 const redirectUrl = window.location.origin + '/admin/login';
118 window.location.href = `https://auth.emergentagent.com/?redirect=${encodeURIComponent(redirectUrl)}`;
119 };
28 }, [navigate]);
12029
12130 const handleSubmit = async (e) => {
12231 e.preventDefault();
273182 </div>
274183 )}
275184
276 {/* Google Login Button */}
277 <button
278 onClick={handleGoogleLogin}
279 className="w-full flex items-center justify-center gap-3 bg-white hover:bg-gray-100 text-gray-800 font-medium py-4 px-6 rounded-xl transition-all mb-6 shadow-lg"
280 >
281 <svg className="w-5 h-5" viewBox="0 0 24 24">
282 <path
283 fill="#4285F4"
284 d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
285 />
286 <path
287 fill="#34A853"
288 d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
289 />
290 <path
291 fill="#FBBC05"
292 d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
293 />
294 <path
295 fill="#EA4335"
296 d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
297 />
298 </svg>
299 Continue with Google
300 </button>
301
302 <div className="relative mb-6">
303 <div className="absolute inset-0 flex items-center">
304 <div className="w-full border-t border-gray-700"></div>
305 </div>
306 <div className="relative flex justify-center text-sm">
307 <span className="px-4 bg-gray-900 text-gray-400">or sign in with password</span>
308 </div>
309 </div>
310
311185 <form onSubmit={handleSubmit} className="space-y-5">
312186 <div>
313187 <label className="block text-sm font-medium text-gray-300 mb-2">Username</label>
Addedverify_auth.py+21−0View fileUnifiedSplit
1import sys
2import os
3sys.path.append('backend')
4from auth import get_password_hash, verify_password
5
6def test():
7 password = "Kongkon2025"
8 hashed = get_password_hash(password)
9 print(f"Password: {password}")
10 print(f"Hashed: {hashed}")
11
12 # Test correct password
13 assert verify_password(password, hashed) == True
14 print("Verification with correct password: OK")
15
16 # Test incorrect password
17 assert verify_password("wrong", hashed) == False
18 print("Verification with incorrect password: OK")
19
20if __name__ == "__main__":
21 test()
022
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts