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
+203
View File
@@ -0,0 +1,203 @@
import { notFound } from "next/navigation"
import { and, desc, eq } from "drizzle-orm"
import { db } from "@/lib/db"
import { tenants, rent_payments, maintenance_requests, leases } from "@/lib/db/schema"
import { formatCurrency, formatDate } from "@/lib/utils"
import { Home, DollarSign, Wrench, FileText } from "lucide-react"
import { TenantMaintenanceForm } from "./tenant-maintenance-form"
export const dynamic = "force-dynamic"
export default async function TenantPortalPage({ params }: { params: Promise<{ token: string }> }) {
const { token } = await params
// Find tenant by portal token (PUBLIC — no session filter)
const tenant = (await db.query.tenants.findFirst({
where: eq(tenants.portal_token, token),
with: {
unit: {
columns: { unit_number: true, rent_amount: true, property_id: true },
with: {
property: { columns: { name: true, address_line1: true, city: true, state: true } },
},
},
},
})) as any
if (!tenant) notFound()
const property = tenant.unit?.property
const unit = tenant.unit
// Get rent payments for this tenant
const payments = await db
.select({
amount: rent_payments.amount,
due_date: rent_payments.due_date,
paid_date: rent_payments.paid_date,
status: rent_payments.status,
payment_method: rent_payments.payment_method,
})
.from(rent_payments)
.where(eq(rent_payments.tenant_id, tenant.id))
.orderBy(desc(rent_payments.due_date))
.limit(12)
// Get maintenance requests for this tenant
const maintenance = await db
.select({
title: maintenance_requests.title,
status: maintenance_requests.status,
priority: maintenance_requests.priority,
created_at: maintenance_requests.created_at,
resolved_at: maintenance_requests.resolved_at,
})
.from(maintenance_requests)
.where(eq(maintenance_requests.tenant_id, tenant.id))
.orderBy(desc(maintenance_requests.created_at))
// Get active lease for this tenant
const lease = await db.query.leases.findFirst({
where: and(eq(leases.tenant_id, tenant.id), eq(leases.status, "active")),
columns: {
lease_start: true,
lease_end: true,
rent_amount: true,
security_deposit: true,
lease_type: true,
status: true,
},
})
const paymentStatusColors: Record<string, string> = {
paid: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
pending: "text-amber-400 bg-amber-500/10 border-amber-500/20",
overdue: "text-red-400 bg-red-500/10 border-red-500/20",
partial: "text-blue-400 bg-blue-500/10 border-blue-500/20",
}
const maintenanceStatusColors: Record<string, string> = {
open: "text-amber-400 bg-amber-500/10 border-amber-500/20",
in_progress: "text-blue-400 bg-blue-500/10 border-blue-500/20",
resolved: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
closed: "text-white/40 bg-white/5 border-white/10",
}
return (
<div className="min-h-screen bg-[#09090b] text-white">
<div className="mx-auto max-w-3xl px-4 py-10 space-y-8">
{/* Header */}
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] p-6">
<div className="flex items-center gap-3 mb-4">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-600">
<Home className="h-5 w-5 text-white" />
</div>
<div>
<p className="text-xs text-white/40">Tenant Portal</p>
<h1 className="text-lg font-bold text-white">
{tenant.first_name} {tenant.last_name}
</h1>
</div>
</div>
<div className="space-y-1 text-sm text-white/60">
{property && (
<>
<p>{property.name}</p>
<p className="text-white/40 text-xs">
{property.address_line1}{property.city ? `, ${property.city}` : ""}{property.state ? `, ${property.state}` : ""}
</p>
</>
)}
{unit && <p>Unit {unit.unit_number} · {formatCurrency(unit.rent_amount)}/mo</p>}
{tenant.email && <p className="text-white/40 text-xs">{tenant.email}</p>}
</div>
</div>
{/* Lease Info */}
{lease && (
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
<div className="flex items-center gap-3 border-b border-white/[0.06] px-5 py-4">
<FileText className="h-4 w-4 text-indigo-400" />
<h2 className="text-sm font-semibold text-white">Current Lease</h2>
</div>
<div className="grid grid-cols-2 gap-4 p-5 sm:grid-cols-4">
{[
{ label: "Start Date", value: formatDate(lease.lease_start) },
{ label: "End Date", value: formatDate(lease.lease_end) },
{ label: "Monthly Rent", value: formatCurrency(lease.rent_amount) },
{ label: "Deposit", value: lease.security_deposit ? formatCurrency(lease.security_deposit) : "—" },
].map((item) => (
<div key={item.label}>
<p className="text-xs text-white/40">{item.label}</p>
<p className="mt-1 text-sm font-medium text-white">{item.value}</p>
</div>
))}
</div>
</div>
)}
{/* Rent Payments */}
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
<div className="flex items-center gap-3 border-b border-white/[0.06] px-5 py-4">
<DollarSign className="h-4 w-4 text-indigo-400" />
<h2 className="text-sm font-semibold text-white">Rent History</h2>
</div>
{!payments?.length ? (
<p className="px-5 py-8 text-sm text-center text-white/30">No payment records yet</p>
) : (
<div className="divide-y divide-white/[0.04]">
{payments.map((p, i) => (
<div key={i} className="flex items-center justify-between px-5 py-3">
<div>
<p className="text-sm text-white">Due {formatDate(p.due_date)}</p>
{p.paid_date && <p className="text-xs text-white/40">Paid {formatDate(p.paid_date)}</p>}
</div>
<div className="flex items-center gap-3">
<p className="text-sm font-medium text-white">{formatCurrency(p.amount)}</p>
<span className={`rounded-md border px-2 py-0.5 text-xs font-medium capitalize ${paymentStatusColors[p.status] ?? paymentStatusColors.pending}`}>
{p.status}
</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Maintenance */}
<div className="rounded-xl border border-white/[0.06] bg-[#16161f] overflow-hidden">
<div className="flex items-center gap-3 border-b border-white/[0.06] px-5 py-4">
<Wrench className="h-4 w-4 text-indigo-400" />
<h2 className="text-sm font-semibold text-white">Maintenance Requests</h2>
</div>
{!maintenance?.length ? (
<p className="px-5 py-6 text-sm text-center text-white/30">No maintenance requests</p>
) : (
<div className="divide-y divide-white/[0.04]">
{maintenance.map((m, i) => (
<div key={i} className="flex items-center justify-between px-5 py-3">
<div>
<p className="text-sm text-white">{m.title}</p>
<p className="text-xs text-white/40">{formatDate(m.created_at)}</p>
</div>
<span className={`rounded-md border px-2 py-0.5 text-xs font-medium capitalize ${maintenanceStatusColors[m.status] ?? maintenanceStatusColors.open}`}>
{m.status.replace("_", " ")}
</span>
</div>
))}
</div>
)}
<div className="border-t border-white/[0.06] p-5">
<TenantMaintenanceForm tenantId={tenant.id} propertyId={tenant.unit?.property_id} unitId={tenant.unit_id} portalToken={token} />
</div>
</div>
<p className="text-center text-xs text-white/20">
Powered by Property Management Network · This is a private link for your use only
</p>
</div>
</div>
)
}
@@ -0,0 +1,106 @@
"use client"
import { useState } from "react"
import { Plus, CheckCircle } from "lucide-react"
import { Select } from "@/components/ui/select"
const CATEGORIES = [
{ value: "plumbing", label: "Plumbing" },
{ value: "electrical", label: "Electrical" },
{ value: "hvac", label: "HVAC" },
{ value: "appliance", label: "Appliance" },
{ value: "structural", label: "Structural" },
{ value: "pest", label: "Pest" },
{ value: "general", label: "General" },
]
interface Props {
tenantId: string
propertyId: string
unitId: string | null
portalToken: string
}
export function TenantMaintenanceForm({ tenantId, propertyId, unitId, portalToken }: Props) {
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [done, setDone] = useState(false)
const [error, setError] = useState("")
const cls = "w-full rounded-lg border border-white/10 bg-white/5 px-4 py-2.5 text-sm text-white placeholder-white/30 outline-none ring-indigo-500 transition focus:border-indigo-500/50 focus:ring-1"
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setError("")
const fd = new FormData(e.currentTarget)
const res = await fetch("/api/maintenance", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: fd.get("title"),
description: fd.get("description"),
category: fd.get("category"),
priority: "medium",
property_id: propertyId,
unit_id: unitId ?? undefined,
tenant_id: tenantId,
portal_token: portalToken, // verified server-side — no user_id from client
}),
})
const data = await res.json()
setLoading(false)
if (!res.ok) {
setError(typeof data.error === "string" ? data.error : "Something went wrong")
return
}
setDone(true)
setOpen(false)
}
if (done) {
return (
<div className="flex items-center gap-2 text-sm text-emerald-400">
<CheckCircle className="h-4 w-4" />
Request submitted! Your landlord will be in touch.
</div>
)
}
if (!open) {
return (
<button
onClick={() => setOpen(true)}
className="flex items-center gap-2 rounded-lg border border-white/10 px-4 py-2 text-sm text-white/60 hover:border-white/20 hover:text-white transition"
>
<Plus className="h-4 w-4" /> Submit New Request
</button>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<p className="text-sm font-medium text-white">New Maintenance Request</p>
{error && <p className="text-xs text-red-400">{error}</p>}
<div>
<input name="title" required placeholder="Brief title (e.g. Leaking faucet)" className={cls} />
</div>
<div>
<textarea name="description" rows={3} placeholder="Describe the issue..." className={cls + " resize-none"} />
</div>
<div>
<Select name="category" defaultValue="plumbing" options={CATEGORIES} />
</div>
<div className="flex gap-3">
<button type="button" onClick={() => setOpen(false)} className="rounded-lg border border-white/10 px-4 py-2 text-sm text-white/50 hover:text-white transition">
Cancel
</button>
<button type="submit" disabled={loading} className="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50 transition">
{loading ? "Submitting..." : "Submit"}
</button>
</div>
</form>
)
}