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>
This commit is contained in:
Leon Serfaty
2026-06-23 20:36:07 -04:00
co-authored by Claude Opus 4.8
commit 857b9a7811
291 changed files with 38996 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
import { TableSkeleton } from "@/components/shared/skeleton"
export default function Loading() { return <TableSkeleton rows={6} cols={4} /> }
+107
View File
@@ -0,0 +1,107 @@
import { redirect } from "next/navigation"
import { and, eq, gte } from "drizzle-orm"
import { db } from "@/lib/db"
import { properties as propertiesTable, rent_payments, expenses as expensesTable } from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import { formatCurrency } from "@/lib/utils"
import { TrendingUp, TrendingDown, Building2, DollarSign, Receipt, BarChart3 } from "lucide-react"
import { ReportsClient } from "./reports-client"
export const metadata = { title: "Reports" }
export default async function ReportsPage() {
const user = await getSessionUser()
if (!user) redirect("/login")
// Last 6 months range
const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 5)
sixMonthsAgo.setDate(1)
const rangeStart = sixMonthsAgo.toISOString().slice(0, 10)
const [properties, payments, expenses] = await Promise.all([
db
.select({ id: propertiesTable.id, name: propertiesTable.name })
.from(propertiesTable)
.where(eq(propertiesTable.user_id, user.id)),
db
.select({
amount: rent_payments.amount,
status: rent_payments.status,
due_date: rent_payments.due_date,
property_id: rent_payments.property_id,
})
.from(rent_payments)
.where(and(eq(rent_payments.user_id, user.id), gte(rent_payments.due_date, rangeStart))),
db
.select({
amount: expensesTable.amount,
expense_date: expensesTable.expense_date,
property_id: expensesTable.property_id,
category: expensesTable.category,
})
.from(expensesTable)
.where(and(eq(expensesTable.user_id, user.id), gte(expensesTable.expense_date, rangeStart))),
])
// Build monthly buckets for last 6 months
const months: { key: string; label: string }[] = []
for (let i = 5; i >= 0; i--) {
const d = new Date()
d.setMonth(d.getMonth() - i)
d.setDate(1)
const key = d.toISOString().slice(0, 7)
const label = d.toLocaleDateString("en-US", { month: "short", year: "2-digit" })
months.push({ key, label })
}
// Monthly revenue & expenses
const monthlyData = months.map(({ key, label }) => {
const revenue = (payments ?? []).filter(p => p.status === "paid" && p.due_date?.startsWith(key)).reduce((s, p) => s + Number(p.amount), 0)
const expense = (expenses ?? []).filter(e => e.expense_date?.startsWith(key)).reduce((s, e) => s + Number(e.amount), 0)
return { key, label, revenue, expense, net: revenue - expense }
})
// Per-property P&L
const propertyPnL = (properties ?? []).map((p: any) => {
const revenue = (payments ?? []).filter(pm => pm.status === "paid" && pm.property_id === p.id).reduce((s, pm) => s + Number(pm.amount), 0)
const expense = (expenses ?? []).filter(e => e.property_id === p.id).reduce((s, e) => s + Number(e.amount), 0)
const net = revenue - expense
const margin = revenue > 0 ? Math.round((net / revenue) * 100) : 0
return { ...p, revenue, expense, net, margin }
}).sort((a, b) => b.net - a.net)
// Summary totals
const totalRevenue = (payments ?? []).filter(p => p.status === "paid").reduce((s, p) => s + Number(p.amount), 0)
const totalExpenses = (expenses ?? []).reduce((s, e) => s + Number(e.amount), 0)
const totalNet = totalRevenue - totalExpenses
const avgMargin = totalRevenue > 0 ? Math.round((totalNet / totalRevenue) * 100) : 0
return (
<div className="space-y-6">
{/* Summary KPI cards */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{[
{ label: "Total Revenue", value: formatCurrency(totalRevenue), icon: DollarSign, color: "text-emerald-400", bg: "bg-emerald-500/10" },
{ label: "Total Expenses", value: formatCurrency(totalExpenses), icon: Receipt, color: "text-rose-400", bg: "bg-rose-500/10" },
{ label: "Net Income", value: formatCurrency(totalNet), icon: totalNet >= 0 ? TrendingUp : TrendingDown, color: totalNet >= 0 ? "text-emerald-400" : "text-red-400", bg: totalNet >= 0 ? "bg-emerald-500/10" : "bg-red-500/10" },
{ label: "Profit Margin", value: `${avgMargin}%`, icon: BarChart3, color: "text-indigo-400", bg: "bg-indigo-500/10" },
].map((s) => (
<div key={s.label} className="rounded-xl border border-white/[0.06] bg-[#16161f] p-4">
<div className="flex items-center gap-2 mb-2">
<div className={`flex h-7 w-7 items-center justify-center rounded-lg ${s.bg}`}>
<s.icon className={`h-3.5 w-3.5 ${s.color}`} />
</div>
<p className="text-xs text-white/35">{s.label}</p>
</div>
<p className={`text-xl font-bold tabular-nums ${s.color}`}>{s.value}</p>
<p className="text-[10px] text-white/25 mt-0.5">Last 6 months</p>
</div>
))}
</div>
{/* Client component for interactive charts */}
<ReportsClient monthlyData={monthlyData} propertyPnL={propertyPnL} />
</div>
)
}
+138
View File
@@ -0,0 +1,138 @@
"use client"
import { useState } from "react"
import { formatCurrency } from "@/lib/utils"
import { TrendingUp, TrendingDown, Building2 } from "lucide-react"
interface MonthData { key: string; label: string; revenue: number; expense: number; net: number }
interface PropertyPnL { id: string; name: string; revenue: number; expense: number; net: number; margin: number }
export function ReportsClient({ monthlyData, propertyPnL }: { monthlyData: MonthData[]; propertyPnL: PropertyPnL[] }) {
const [view, setView] = useState<"revenue" | "expense" | "net">("revenue")
const maxVal = Math.max(...monthlyData.map(m =>
view === "revenue" ? m.revenue : view === "expense" ? m.expense : Math.abs(m.net)
), 1)
const barColor = view === "revenue" ? "bg-indigo-500" : view === "expense" ? "bg-rose-500" : "bg-emerald-500"
const viewLabel = view === "revenue" ? "Revenue" : view === "expense" ? "Expenses" : "Net Income"
return (
<div className="space-y-6">
{/* Monthly chart */}
<div className="rounded-2xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
<div className="flex items-center justify-between px-5 py-4 border-b border-white/[0.06]">
<p className="text-sm font-semibold text-white">Monthly {viewLabel}</p>
<div className="flex items-center gap-1 rounded-lg border border-white/[0.06] p-1">
{(["revenue", "expense", "net"] as const).map((v) => (
<button
key={v}
onClick={() => setView(v)}
className={`px-3 py-1 rounded-md text-xs font-medium transition capitalize ${
view === v ? "bg-indigo-600 text-white" : "text-white/40 hover:text-white"
}`}
>
{v === "net" ? "Net" : v === "revenue" ? "Revenue" : "Expenses"}
</button>
))}
</div>
</div>
<div className="px-5 py-5">
<div className="flex items-end gap-2 h-40">
{monthlyData.map((m) => {
const val = view === "revenue" ? m.revenue : view === "expense" ? m.expense : m.net
const height = maxVal > 0 ? Math.max((Math.abs(val) / maxVal) * 100, val !== 0 ? 4 : 0) : 0
const isNegative = val < 0
return (
<div key={m.key} className="flex-1 flex flex-col items-center gap-1 group">
<div className="relative w-full flex items-end justify-center" style={{ height: "128px" }}>
<div
className={`w-full rounded-t-md transition-all duration-300 ${isNegative ? "bg-red-500" : barColor} opacity-70 group-hover:opacity-100`}
style={{ height: `${height}%` }}
title={formatCurrency(val)}
/>
<div className="absolute -top-6 left-1/2 -translate-x-1/2 hidden group-hover:block whitespace-nowrap rounded-lg bg-[#1d1d2a] border border-white/10 px-2 py-1 text-xs text-white shadow-xl z-10">
{formatCurrency(val)}
</div>
</div>
<span className="text-[10px] text-white/30">{m.label}</span>
</div>
)
})}
</div>
</div>
{/* Chart legend */}
<div className="grid grid-cols-3 divide-x divide-white/[0.04] border-t border-white/[0.06]">
{[
{ label: "Total Revenue", value: monthlyData.reduce((s, m) => s + m.revenue, 0), color: "text-emerald-400" },
{ label: "Total Expenses", value: monthlyData.reduce((s, m) => s + m.expense, 0), color: "text-rose-400" },
{ label: "Net Income", value: monthlyData.reduce((s, m) => s + m.net, 0), color: "text-indigo-400" },
].map((s) => (
<div key={s.label} className="px-4 py-3 text-center">
<p className={`text-base font-bold tabular-nums ${s.color}`}>{formatCurrency(s.value)}</p>
<p className="text-[10px] text-white/30 mt-0.5">{s.label}</p>
</div>
))}
</div>
</div>
{/* Per-property P&L */}
<div className="rounded-2xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
<div className="px-5 py-4 border-b border-white/[0.06]">
<p className="text-sm font-semibold text-white">Property P&amp;L</p>
<p className="text-xs text-white/35 mt-0.5">Income vs expenses per property (last 6 months)</p>
</div>
{propertyPnL.length === 0 ? (
<div className="py-12 text-center text-sm text-white/30">No property data available</div>
) : (
<div className="divide-y divide-white/[0.04]">
{propertyPnL.map((p) => (
<div key={p.id} className="px-5 py-4">
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-indigo-500/10">
<Building2 className="h-4 w-4 text-indigo-400" />
</div>
<div>
<p className="text-sm font-semibold text-white">{p.name}</p>
<p className="text-xs text-white/35">{p.margin}% profit margin</p>
</div>
</div>
<div className="text-right">
<p className={`text-base font-bold tabular-nums ${p.net >= 0 ? "text-emerald-400" : "text-red-400"}`}>
{formatCurrency(p.net)}
</p>
<div className={`flex items-center gap-1 justify-end text-xs ${p.net >= 0 ? "text-emerald-400/60" : "text-red-400/60"}`}>
{p.net >= 0 ? <TrendingUp className="h-3 w-3" /> : <TrendingDown className="h-3 w-3" />}
Net income
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-xl bg-white/[0.03] border border-white/[0.04] px-3 py-2.5">
<p className="text-[10px] text-white/30 mb-1">Revenue</p>
<p className="text-sm font-bold text-emerald-400 tabular-nums">{formatCurrency(p.revenue)}</p>
<div className="mt-1.5 h-1 w-full rounded-full bg-white/[0.06]">
<div className="h-1 rounded-full bg-emerald-500" style={{ width: p.revenue > 0 ? "100%" : "0%" }} />
</div>
</div>
<div className="rounded-xl bg-white/[0.03] border border-white/[0.04] px-3 py-2.5">
<p className="text-[10px] text-white/30 mb-1">Expenses</p>
<p className="text-sm font-bold text-rose-400 tabular-nums">{formatCurrency(p.expense)}</p>
<div className="mt-1.5 h-1 w-full rounded-full bg-white/[0.06]">
<div className="h-1 rounded-full bg-rose-500" style={{ width: p.revenue > 0 ? `${Math.min((p.expense / p.revenue) * 100, 100)}%` : "0%" }} />
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}