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:
@@ -1,50 +1,9 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import type { NextRequest } from "next/server"
|
||||
import { withAuth } from "next-auth/middleware"
|
||||
|
||||
// Simple in-memory rate limiter (use Redis in production)
|
||||
const rateLimit = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
function checkRateLimit(ip: string, limit: number = 60, windowMs: number = 60000): boolean {
|
||||
const now = Date.now()
|
||||
const entry = rateLimit.get(ip)
|
||||
|
||||
if (!entry || entry.resetAt < now) {
|
||||
rateLimit.set(ip, { count: 1, resetAt: now + windowMs })
|
||||
return true
|
||||
}
|
||||
|
||||
if (entry.count >= limit) return false
|
||||
entry.count++
|
||||
return true
|
||||
}
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const response = NextResponse.next()
|
||||
|
||||
// Rate limit API routes
|
||||
if (request.nextUrl.pathname.startsWith("/api/")) {
|
||||
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown"
|
||||
if (!checkRateLimit(ip)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests" },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Add security headers
|
||||
response.headers.set("X-Frame-Options", "SAMEORIGIN")
|
||||
response.headers.set("X-Content-Type-Options", "nosniff")
|
||||
response.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
|
||||
// Allow iframe embedding for pledge pages
|
||||
if (request.nextUrl.pathname.startsWith("/p/")) {
|
||||
response.headers.delete("X-Frame-Options")
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
export default withAuth({
|
||||
pages: { signIn: "/login" },
|
||||
})
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
||||
matcher: ["/dashboard/:path*"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user