Files
property-management-network/lib/session.ts
T
Leon SerfatyandClaude Opus 4.8 857b9a7811 Initial import: property management SaaS + security hardening + admin dashboard
Property Management Network — Next.js 16 (App Router), Better Auth,
Drizzle ORM over PostgreSQL, Stripe, OpenAI, Resend.

Includes:
- Security hardening: access-control/IDOR fixes, TLS-by-default DB layer,
  constant-time cron auth, strict security headers, atomic AI quota gating,
  HTML/email output encoding, demo-backdoor disabled in production.
- Superadmin dashboard at /admin (overview/MRR, server-paginated users with
  ban/impersonate/plan/delete, billing, platform activity + admin audit log,
  AI usage, system health) via the Better Auth admin plugin.
- Seed/migration utility scripts under scripts/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:36:07 -04:00

72 lines
2.8 KiB
TypeScript

import { headers } from "next/headers"
import { redirect } from "next/navigation"
import { eq } from "drizzle-orm"
import { auth } from "@/lib/auth"
import { db } from "@/lib/db"
import { profiles } from "@/lib/db/schema"
/**
* Returns the authenticated Better Auth user for the current request, or null.
*
* Usage in a route / server component:
* const user = await getSessionUser()
* if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
*/
export async function getSessionUser() {
const session = await auth.api.getSession({ headers: await headers() })
return session?.user ?? null
}
export async function getSession() {
return auth.api.getSession({ headers: await headers() })
}
// ── Admin gating ─────────────────────────────────────────────────────────────
// Admins come from the Better Auth `user.role === "admin"` field (set via the
// admin plugin / bootstrap env). ADMIN_USER_IDS / ADMIN_EMAILS act as an
// env-level fallback so the first admin can be bootstrapped without DB access.
const ADMIN_USER_IDS = (process.env.ADMIN_USER_IDS ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean)
const ADMIN_EMAILS = (process.env.ADMIN_EMAILS ?? "")
.split(",")
.map((s) => s.trim().toLowerCase())
.filter(Boolean)
export function isAdminUser(
u: { id?: string; email?: string; role?: string | null } | null | undefined
): boolean {
if (!u) return false
if (u.role === "admin") return true
if (u.id && ADMIN_USER_IDS.includes(u.id)) return true
if (u.email && ADMIN_EMAILS.includes(u.email.toLowerCase())) return true
return false
}
/**
* For API routes / server actions: returns `{ user, profile }` if the caller is
* an admin, otherwise null (caller returns 401/403). NEVER skip this — admin
* queries bypass user_id scoping, so this gate is the only data protection.
*/
export async function getAdminSession() {
const session = await auth.api.getSession({ headers: await headers() })
const user = session?.user ?? null
if (!isAdminUser(user as { role?: string | null })) return null
const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, user!.id) })
return { user: user!, profile: profile ?? null }
}
/**
* For server components / the (admin) layout: redirects non-admins
* (anonymous → /login, logged-in non-admin → /dashboard).
*/
export async function requireAdmin() {
const session = await auth.api.getSession({ headers: await headers() })
const user = session?.user ?? null
if (!user) redirect("/login")
if (!isAdminUser(user as { role?: string | null })) redirect("/dashboard")
const profile = await db.query.profiles.findFirst({ where: eq(profiles.id, user.id) })
return { user, profile: profile ?? null }
}