CodeIssuesPull RequestsActionsSecurityInsights
✨ AI
More
Settings

Admin login and bookings #3742

ClosedLccantynz wants to mergecursor/admin-login-and-bookings-0a01mainopened Feb 15, 2026
7 changed files+274−21
Modifiedbackend/bookingform_routes.py+2−2View fileUnifiedSplit
9191 if not _is_authed(req):
9292 return RedirectResponse(url="/admin/login", status_code=302)
9393
94 return HTMLResponse(\"\"\"<!doctype html>
94 return HTMLResponse("""<!doctype html>
9595<html>
9696<head>
9797 <meta charset="utf-8"/>
147147loadCfg();
148148</script>
149149</body>
150</html>\"\"\")
150</html>""")
Modifiedbackend/server.py+245−6View fileUnifiedSplit
1from fastapi import FastAPI
2from fastapi.responses import JSONResponse
1# backend/server.py
2# Full Hibiscus to Airport backend - admin, bookings, cockpit, API
3
4import os
5import sys
6from datetime import datetime
7
8HERE = os.path.dirname(os.path.abspath(__file__))
9ROOT = os.path.dirname(HERE)
10for p in (ROOT, HERE):
11 if p and p not in sys.path:
12 sys.path.insert(0, p)
13
14from fastapi import FastAPI, Request, Form
15from fastapi.middleware.cors import CORSMiddleware
16from fastapi.responses import JSONResponse, HTMLResponse, RedirectResponse
17
18app = FastAPI(title="Hibiscus to Airport API")
19
20# CORS - allow frontend (hibiscustoairport.co.nz, www, localhost)
21app.add_middleware(
22 CORSMiddleware,
23 allow_origins=[
24 "https://hibiscustoairport.co.nz",
25 "https://www.hibiscustoairport.co.nz",
26 "http://localhost:3000",
27 "http://127.0.0.1:3000",
28 ],
29 allow_credentials=True,
30 allow_methods=["*"],
31 allow_headers=["*"],
32)
33
34BUILD_STAMP = "HIBISCUS_FULL_20260215"
35ADMIN_COOKIE = "hibiscus_admin"
36ADMIN_API_KEY = (os.environ.get("ADMIN_API_KEY") or "").strip()
37
38
39def _utc() -> str:
40 return datetime.utcnow().isoformat() + "Z"
41
42
43def _is_authed(req: Request) -> bool:
44 if ADMIN_API_KEY == "":
45 return False
46 h = (req.headers.get("X-Admin-Key") or "").strip()
47 if h and h == ADMIN_API_KEY:
48 return True
49 c = (req.cookies.get(ADMIN_COOKIE) or "").strip()
50 return c == ADMIN_API_KEY
51
52
53# ========== Diagnostics (always-on) ==========
54@app.get("/debug/which")
55def debug_which():
56 return {"module": "server", "stamp": BUILD_STAMP, "utc": _utc()}
357
4app = FastAPI()
558
659@app.get("/debug/stamp")
760def debug_stamp():
8 return {"stamp": "ADMIN_BOOT_OK"}
61 return {"stamp": BUILD_STAMP, "utc": _utc()}
62
63
64@app.get("/healthz")
65def healthz():
66 return {"ok": True, "stamp": BUILD_STAMP, "utc": _utc()}
67
68
69# ========== Admin routes (cookie-based, for direct backend access) ==========
70@app.get("/admin/login", response_class=HTMLResponse)
71def admin_login_get():
72 return HTMLResponse("""<!doctype html>
73<html><head>
74<meta charset="utf-8"/>
75<meta name="viewport" content="width=device-width,initial-scale=1"/>
76<title>Hibiscus Admin Login</title>
77<style>
78body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;padding:24px;max-width:860px;margin:0 auto;}
79h1{margin:0 0 6px 0;}
80.meta{color:#6b7280;font-size:13px;margin-bottom:14px;}
81.card{border:1px solid #e5e7eb;border-radius:14px;padding:18px;}
82label{display:block;font-size:13px;color:#374151;margin-bottom:6px;}
83input{width:100%;padding:12px;border-radius:10px;border:1px solid #d1d5db;}
84button{margin-top:12px;padding:12px 14px;border-radius:10px;border:0;background:#111827;color:#fff;cursor:pointer;}
85small{color:#6b7280;}
86code{background:#f3f4f6;padding:2px 6px;border-radius:8px;}
87</style>
88</head>
89<body>
90<h1>Hibiscus to Airport Admin</h1>
91<div class="meta">Login uses <code>ADMIN_API_KEY</code> from Render env vars.</div>
92<div class="card">
93<form method="post" action="/admin/login">
94 <label>Admin Key</label>
95 <input name="key" type="password" placeholder="paste ADMIN_API_KEY" autocomplete="current-password" />
96 <button type="submit">Login</button>
97 <div style="margin-top:10px;"><small>If you see 401, the key is missing or mismatched.</small></div>
98</form>
99</div>
100</body></html>""")
101
9102
10103@app.post("/admin/login")
11def admin_login():
12 return {"token": "OWNER_BYPASS_TOKEN", "role": "owner"}
\ No newline at end of file
104def admin_login_post(key: str = Form(...)):
105 k = (key or "").strip()
106 if ADMIN_API_KEY == "":
107 return HTMLResponse(
108 "<h3>401 Unauthorized</h3><p>ADMIN_API_KEY is missing in Render env vars.</p><p><a href='/admin/login'>Back</a></p>",
109 status_code=401,
110 )
111 if k != ADMIN_API_KEY:
112 return HTMLResponse(
113 "<h3>401 Unauthorized</h3><p>Key mismatch.</p><p><a href='/admin/login'>Back</a></p>",
114 status_code=401,
115 )
116 resp = RedirectResponse(url="/admin", status_code=302)
117 resp.set_cookie(
118 key=ADMIN_COOKIE,
119 value=ADMIN_API_KEY,
120 httponly=True,
121 samesite="lax",
122 secure=True,
123 )
124 return resp
125
126
127@app.get("/admin/logout")
128def admin_logout():
129 resp = RedirectResponse(url="/admin/login", status_code=302)
130 resp.delete_cookie(ADMIN_COOKIE)
131 return resp
132
133
134@app.get("/admin", response_class=HTMLResponse)
135def admin_panel(req: Request):
136 if not _is_authed(req):
137 return RedirectResponse(url="/admin/login", status_code=302)
138 return HTMLResponse("""<!doctype html>
139<html><head>
140<meta charset="utf-8"/>
141<meta name="viewport" content="width=device-width,initial-scale=1"/>
142<title>Hibiscus Admin Panel</title>
143<style>
144body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;margin:0;}
145header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid #e5e7eb;}
146.meta{color:#6b7280;font-size:13px;margin-top:2px;}
147.tabs{display:flex;gap:10px;padding:10px 18px;border-bottom:1px solid #e5e7eb;}
148.tab{padding:10px 12px;border-radius:10px;border:1px solid #e5e7eb;background:#fff;cursor:pointer;}
149.tab.active{background:#111827;color:#fff;border-color:#111827;}
150main{padding:0;height:calc(100vh - 110px);}
151iframe{width:100%;height:100%;border:0;}
152a{color:#111827;text-decoration:none;font-size:14px;}
153</style>
154</head>
155<body>
156<header>
157 <div>
158 <div style="font-weight:700;">Hibiscus to Airport Admin</div>
159 <div class="meta">Cockpit + Bookings + Status</div>
160 </div>
161 <div><a href="/admin/logout">Logout</a></div>
162</header>
163<div class="tabs">
164 <button class="tab active" data-url="/admin/cockpit">Cockpit</button>
165 <button class="tab" data-url="/admin/status">Status</button>
166</div>
167<main><iframe id="frame" src="/admin/cockpit"></iframe></main>
168<script>
169const tabs=[...document.querySelectorAll('.tab')];
170const frame=document.getElementById('frame');
171tabs.forEach(t=>{
172 t.addEventListener('click',()=>{
173 tabs.forEach(x=>x.classList.remove('active'));
174 t.classList.add('active');
175 frame.src=t.dataset.url;
176 });
177});
178</script>
179</body></html>""")
180
181
182@app.get("/admin/status")
183def admin_status(req: Request):
184 if not _is_authed(req):
185 return JSONResponse({"ok": False, "error": "unauthorized"}, status_code=401)
186 return {"ok": True, "stamp": BUILD_STAMP, "utc": _utc()}
187
188
189@app.get("/admin/cockpit", response_class=HTMLResponse)
190def admin_cockpit(req: Request):
191 if not _is_authed(req):
192 return RedirectResponse(url="/admin/login", status_code=302)
193 return HTMLResponse(f"""<!doctype html>
194<html><head>
195<meta charset="utf-8"/>
196<meta name="viewport" content="width=device-width,initial-scale=1"/>
197<title>Cockpit</title>
198<style>
199body{{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;padding:18px;max-width:980px;margin:0 auto;}}
200h2{{margin:0 0 8px 0;}}
201.meta{{color:#6b7280;font-size:13px;margin-bottom:14px;}}
202.row{{display:flex;gap:10px;flex-wrap:wrap;}}
203button{{padding:10px 12px;border-radius:10px;border:1px solid #e5e7eb;background:#111827;color:#fff;cursor:pointer;}}
204pre{{background:#0b1020;color:#e5e7eb;padding:12px;border-radius:12px;overflow:auto;}}
205.card{{border:1px solid #e5e7eb;border-radius:14px;padding:14px;margin-top:12px;}}
206</style>
207</head>
208<body>
209<h2>Hibiscus Cockpit</h2>
210<div class="meta">Server stamp: <b>{BUILD_STAMP}</b></div>
211<div class="row">
212 <button onclick="hit('/debug/stamp')">/debug/stamp</button>
213 <button onclick="hit('/debug/which')">/debug/which</button>
214 <button onclick="hit('/healthz')">/healthz</button>
215 <button onclick="hit('/admin/status')">/admin/status</button>
216</div>
217<div class="card">
218 <div style="font-weight:700;">Output</div>
219 <pre id="out">(click a button)</pre>
220</div>
221<script>
222async function hit(p){{
223 const out=document.getElementById('out');
224 out.textContent='Loading '+p+'...';
225 try{{
226 const r=await fetch(p+'?ts='+Date.now(),{{headers:{{'cache-control':'no-cache'}}}});
227 out.textContent='HTTP '+r.status+'\\n\\n'+await r.text();
228 }}catch(e){{
229 out.textContent='ERROR: '+String(e);
230 }}
231}}
232</script>
233</body></html>""")
234
235
236# ========== Include booking routes (API: /api/bookings, /api/admin/login, etc.) ==========
237try:
238 from booking_routes import router as booking_router
239 app.include_router(booking_router, prefix="/api")
240except Exception as e:
241 import logging
242 logging.warning(f"Booking routes not loaded: {e}")
243
244
245# ========== Include booking form routes ==========
246try:
247 from bookingform_routes import router as bookingform_router
248 app.include_router(bookingform_router)
249except Exception as e:
250 import logging
251 logging.warning(f"Booking form routes not loaded: {e}")
Modifiedfrontend/src/App.js+17−4View fileUnifiedSplit
55import DairyFlatAirportShuttle from "./pages/DairyFlatAirportShuttle";
66import LateNightAirportShuttle from "./pages/LateNightAirportShuttle";
77import WarkworthAirportShuttle from "./pages/WarkworthAirportShuttle";
8import OrewaShuttle from "./pages/OrewaShuttle";
9import SilverdaleShuttle from "./pages/SilverdaleShuttle";
10import WhangaparaoaShuttle from "./pages/WhangaparaoaShuttle";
11import RedBeachShuttle from "./pages/RedBeachShuttle";
12import GulfHarbourShuttle from "./pages/GulfHarbourShuttle";
13import MillwaterAirportShuttle from "./pages/MillwaterAirportShuttle";
814
915import AdminShell from "./admin/AdminShell";
1016import Cockpit from "./admin/Cockpit";
11import SafeBookings from "./admin/SafeBookings";
12
13
1417import RealAdminBookings from "./pages/AdminDashboard";
18import AdminLogin from "./pages/AdminLogin";
19import BookingPage from "./pages/BookingPage";
1520
1621function AdminRoutes() {
1722 return (
1823 <Routes>
1924 <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>} />
25 <Route path="/admin/login" element={<AdminLogin />} />
2126
2227 <Route
2328 path="/admin/bookings"
4752 <Routes>
4853 <Route path="/" element={<Navigate to="/service-areas" replace />} />
4954 <Route path="/service-areas" element={<ServiceAreas />} />
55 <Route path="/book-now" element={<BookingPage />} />
56 <Route path="/booking" element={<BookingPage />} />
5057 <Route path="/dairy-flat-airport-shuttle" element={<DairyFlatAirportShuttle />} />
5158 <Route path="/late-night-airport-shuttle" element={<LateNightAirportShuttle />} />
5259 <Route path="/warkworth-airport-shuttle" element={<WarkworthAirportShuttle />} />
60 <Route path="/orewa-airport-shuttle" element={<OrewaShuttle />} />
61 <Route path="/silverdale-airport-shuttle" element={<SilverdaleShuttle />} />
62 <Route path="/whangaparaoa-airport-shuttle" element={<WhangaparaoaShuttle />} />
63 <Route path="/red-beach-airport-shuttle" element={<RedBeachShuttle />} />
64 <Route path="/gulf-harbour-airport-shuttle" element={<GulfHarbourShuttle />} />
65 <Route path="/millwater-airport-shuttle" element={<MillwaterAirportShuttle />} />
5366 <Route path="*" element={<Navigate to="/service-areas" replace />} />
5467 </Routes>
5568 );
Modifiedfrontend/src/admin/AdminShell.jsx+1−0View fileUnifiedSplit
22
33export default function AdminShell({ children }) {
44 const logout = () => {
5 localStorage.removeItem("admin_token");
56 localStorage.removeItem("HIBI_ADMIN_TOKEN");
67 window.location.href = "/admin/login";
78 };
Modifiedfrontend/src/pages/AdminDashboard.jsx+3−3View fileUnifiedSplit
1212import { useToast } from '../hooks/use-toast';
1313import axios from 'axios';
1414
15const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
15const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'https://api.hibiscustoairport.co.nz';
1616
1717const AdminDashboard = () => {
1818 const navigate = useNavigate();
8888 if (urlParams.get('calendar_authorized') === 'true') {
8989 setCalendarAuthorized(true);
9090 toast({ title: 'Success!', description: 'Google Calendar has been authorized successfully.' });
91 window.history.replaceState({}, '', '/admin/dashboard');
91 window.history.replaceState({}, '', '/admin/bookings');
9292 }
9393 if (urlParams.get('calendar_error')) {
9494 toast({ title: 'Authorization Failed', description: urlParams.get('calendar_error'), variant: 'destructive' });
95 window.history.replaceState({}, '', '/admin/dashboard');
95 window.history.replaceState({}, '', '/admin/bookings');
9696 }
9797 }, []);
9898
Modifiedfrontend/src/pages/AdminLogin.jsx+4−4View fileUnifiedSplit
66import axios from 'axios';
77import { Loader2, Mail, Lock, ArrowLeft } from 'lucide-react';
88
9const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
9const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'https://api.hibiscustoairport.co.nz';
1010
1111// Separate component to handle OAuth callback
1212const GoogleAuthCallback = ({ onSuccess, onError }) => {
9595 useEffect(() => {
9696 const token = localStorage.getItem('admin_token');
9797 if (token && !hasSessionId) {
98 navigate('/admin/dashboard');
98 navigate('/admin/bookings');
9999 }
100100 }, [navigate, hasSessionId]);
101101
102102 const handleAuthSuccess = (data) => {
103 navigate('/admin/dashboard');
103 navigate('/admin/bookings');
104104 };
105105
106106 const handleAuthError = (message) => {
127127 const response = await axios.post(`${BACKEND_URL}/api/admin/login`, credentials);
128128 localStorage.setItem('admin_token', response.data.access_token);
129129 toast({ title: 'Login Successful!', description: 'Welcome back' });
130 navigate('/admin/dashboard');
130 navigate('/admin/bookings');
131131 } catch (error) {
132132 const message = error.response?.data?.detail || 'Invalid credentials';
133133 setAuthError(message);
Modifiedfrontend/src/pages/BookingPage.jsx+2−2View fileUnifiedSplit
1import React, { useState } from 'react';
1import React, { useState } from 'react';
22import { Button } from '../components/ui/button';
33import { Input } from '../components/ui/input';
44import { Textarea } from '../components/ui/textarea';
1414
1515import { useLoadScript, Autocomplete } from '@react-google-maps/api';
1616
17const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'https://hibiscus-to-airport-1.onrender.com';
17const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'https://api.hibiscustoairport.co.nz';
1818const libraries = ['places'];
1919
2020// Compact Date Picker Modal - Clean iOS-style
2121
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts