production auth: signup, login, protected dashboard, landing page, WAHA QR fix

AUTH:
- NextAuth with credentials provider (bcrypt password hashing)
- /api/auth/signup: creates org + user in transaction
- /login, /signup pages with clean minimal UI
- Middleware protects all /dashboard/* routes → redirects to /login
- Session-based org resolution (no more hardcoded 'demo' headers)
- SessionProvider wraps entire app
- Dashboard header shows org name + sign out button

LANDING PAGE:
- Full marketing page at / with hero, problem, how-it-works, features, CTA
- 'Get Started Free' → /signup → auto-login → /dashboard/setup
- Clean responsive design, no auth required for public pages

WAHA QR FIX:
- WAHA CORE doesn't expose QR value via API or webhook
- Now uses /api/screenshot (full browser capture) with CSS crop to QR area
- Settings panel shows cropped screenshot with overflow:hidden
- Auto-polls every 5s, refresh button

MULTI-TENANT:
- getOrgId() tries session first, then header, then first-org fallback
- All dashboard APIs use session-based org
- Signup creates isolated org per charity
This commit is contained in:
2026-03-03 05:37:04 +08:00
parent 6894f091fd
commit 4f23f28873
22 changed files with 708 additions and 221 deletions

View File

@@ -31,7 +31,7 @@ export default function SettingsPage() {
const [error, setError] = useState<string | null>(null)
useEffect(() => {
fetch("/api/settings", { headers: { "x-org-id": "demo" } })
fetch("/api/settings")
.then((r) => r.json())
.then((data) => { if (data.name) setSettings(data) })
.catch(() => setError("Failed to load settings"))
@@ -44,7 +44,7 @@ export default function SettingsPage() {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json", "x-org-id": "demo" },
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
if (res.ok) { setSaved(section); setTimeout(() => setSaved(null), 2000) }
@@ -150,7 +150,7 @@ function WhatsAppPanel() {
const res = await fetch("/api/whatsapp/qr")
const data = await res.json()
setStatus(data.status)
if (data.qrImage) setQrImage(data.qrImage)
if (data.screenshot) setQrImage(data.screenshot)
if (data.phone) setPhone(data.phone)
if (data.pushName) setPushName(data.pushName)
} catch {
@@ -231,23 +231,33 @@ function WhatsAppPanel() {
<div className="flex flex-col items-center gap-4">
{qrImage ? (
<div className="relative">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={qrImage} alt="WhatsApp QR Code" className="w-64 h-64 rounded-xl border-2 border-[#25D366]/20" />
{/* Crop to QR area: the screenshot shows full WhatsApp web page.
QR code is roughly in center. We use overflow hidden + object positioning. */}
<div className="w-72 h-72 rounded-xl border-2 border-[#25D366]/20 overflow-hidden bg-white">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={qrImage}
alt="WhatsApp QR Code"
className="w-[200%] h-auto max-w-none"
style={{ marginLeft: "-30%", marginTop: "-35%" }}
/>
</div>
<div className="absolute -bottom-2 -right-2 w-8 h-8 rounded-full bg-[#25D366] flex items-center justify-center shadow-lg">
<MessageCircle className="h-4 w-4 text-white" />
</div>
</div>
) : (
<div className="w-64 h-64 rounded-xl border-2 border-dashed border-muted flex items-center justify-center">
<div className="w-72 h-72 rounded-xl border-2 border-dashed border-muted flex items-center justify-center">
<Loader2 className="h-8 w-8 text-muted-foreground animate-spin" />
</div>
)}
<div className="text-center space-y-1">
<p className="text-sm font-medium">Scan with WhatsApp</p>
<p className="text-xs text-muted-foreground">QR refreshes automatically every 5 seconds</p>
<p className="text-xs text-muted-foreground">Open WhatsApp Settings Linked Devices Link a Device</p>
<p className="text-xs text-muted-foreground">Auto-refreshes every 5 seconds</p>
</div>
<Button variant="outline" size="sm" onClick={checkStatus} className="gap-1.5">
<RefreshCw className="h-3 w-3" /> Refresh QR
<RefreshCw className="h-3 w-3" /> Refresh
</Button>
</div>
</CardContent>