Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
6.9 KiB
TypeScript
136 lines
6.9 KiB
TypeScript
import { seedDemoData, clearDemoData, setTestPlan } from "@/app/actions/seed-demo"
|
|
import { eq } from "drizzle-orm"
|
|
import { db } from "@/lib/db"
|
|
import { profiles } from "@/lib/db/schema"
|
|
import { getSessionUser, isAdminUser } from "@/lib/session"
|
|
import { redirect, notFound } from "next/navigation"
|
|
import {
|
|
Building2, Users, CreditCard, Wrench,
|
|
FileText, Receipt, Sparkles, Trash2, CheckCircle2, Zap, Crown, Infinity
|
|
} from "lucide-react"
|
|
|
|
const DEMO_CONTENTS = [
|
|
{ icon: Building2, color: "text-indigo-400 bg-indigo-500/10", label: "3 Properties", detail: "Maple Court, Riverdale Flats, Crestwood Villa" },
|
|
{ icon: Building2, color: "text-violet-400 bg-violet-500/10", label: "7 Units", detail: "Mix of 1, 2 & 3-bedroom units across all properties" },
|
|
{ icon: Users, color: "text-blue-400 bg-blue-500/10", label: "6 Tenants", detail: "Sarah, Marcus, Priya, David, Emily & James — with contacts" },
|
|
{ icon: FileText, color: "text-emerald-400 bg-emerald-500/10", label: "6 Leases", detail: "Active leases, 2 expiring soon to trigger alerts" },
|
|
{ icon: CreditCard, color: "text-teal-400 bg-teal-500/10", label: "30+ Payments", detail: "6 months of history — paid, pending & overdue statuses" },
|
|
{ icon: Wrench, color: "text-amber-400 bg-amber-500/10", label: "6 Maintenance Requests", detail: "Open, in-progress & resolved — with priorities" },
|
|
{ icon: Receipt, color: "text-rose-400 bg-rose-500/10", label: "8 Expenses", detail: "Repairs, insurance, utilities, taxes — with vendors" },
|
|
]
|
|
|
|
const PLANS = [
|
|
{ value: "starter", label: "Starter", icon: Zap, color: "text-white/60 border-white/10 hover:border-white/20", desc: "Free — no AI" },
|
|
{ value: "pro", label: "Pro", icon: Sparkles, color: "text-indigo-300 border-indigo-500/30 hover:border-indigo-500/60 bg-indigo-500/5", desc: "50 AI calls/mo" },
|
|
{ value: "landlord", label: "Landlord", icon: Crown, color: "text-violet-300 border-violet-500/30 hover:border-violet-500/60 bg-violet-500/5", desc: "200 AI calls/mo" },
|
|
{ value: "lifetime", label: "Lifetime", icon: Infinity, color: "text-amber-300 border-amber-500/30 hover:border-amber-500/60 bg-amber-500/5", desc: "Unlimited — all features" },
|
|
] as const
|
|
|
|
export default async function DemoDataPage() {
|
|
const user = await getSessionUser()
|
|
if (!user) redirect("/login")
|
|
|
|
// Admin-only testing tool — regular users get a 404 (and never see the nav link).
|
|
if (!isAdminUser(user)) notFound()
|
|
|
|
const profile = await db.query.profiles.findFirst({
|
|
where: eq(profiles.id, user.id),
|
|
columns: { plan: true },
|
|
})
|
|
|
|
const currentPlan = profile?.plan ?? "starter"
|
|
|
|
return (
|
|
<div className="max-w-2xl mx-auto space-y-6">
|
|
|
|
{/* Plan switcher — most prominent */}
|
|
<div className="rounded-2xl border border-indigo-500/20 bg-gradient-to-br from-indigo-600/10 via-[#16161f] to-violet-600/5 overflow-hidden">
|
|
<div className="px-5 py-4 border-b border-white/[0.06]">
|
|
<div className="flex items-center gap-2">
|
|
<Sparkles className="h-4 w-4 text-indigo-400" />
|
|
<p className="text-sm font-semibold text-white">Test Plan</p>
|
|
</div>
|
|
<p className="text-xs text-white/40 mt-0.5">
|
|
Switch plans instantly to test different features — current: <span className="text-indigo-300 font-semibold capitalize">{currentPlan}</span>
|
|
</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2 p-4">
|
|
{PLANS.map((plan) => {
|
|
const isActive = currentPlan === plan.value
|
|
return (
|
|
<form key={plan.value} action={setTestPlan.bind(null, plan.value)}>
|
|
<button
|
|
type="submit"
|
|
className={`w-full flex flex-col items-center gap-1.5 rounded-xl border px-3 py-3 transition-all ${plan.color} ${isActive ? "ring-2 ring-indigo-500/50 ring-offset-1 ring-offset-[#16161f]" : ""}`}
|
|
>
|
|
<plan.icon className="h-4 w-4" />
|
|
<span className="text-xs font-semibold">{plan.label}</span>
|
|
<span className="text-[10px] opacity-60">{plan.desc}</span>
|
|
{isActive && <span className="text-[9px] font-bold text-emerald-400 uppercase tracking-wider">Active</span>}
|
|
</button>
|
|
</form>
|
|
)
|
|
})}
|
|
</div>
|
|
<div className="px-5 pb-4">
|
|
<p className="text-[10px] text-white/25 text-center">
|
|
For testing only — does not affect real billing
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Demo data section */}
|
|
<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">Demo Data</p>
|
|
<p className="text-xs text-white/40 mt-0.5">Populate your account with realistic sample data</p>
|
|
</div>
|
|
<div className="divide-y divide-white/[0.04]">
|
|
{DEMO_CONTENTS.map((item) => (
|
|
<div key={item.label} className="flex items-center gap-4 px-5 py-3">
|
|
<div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-xl ${item.color}`}>
|
|
<item.icon className="h-3.5 w-3.5" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-medium text-white">{item.label}</p>
|
|
<p className="text-xs text-white/40 truncate">{item.detail}</p>
|
|
</div>
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-400/40 shrink-0 ml-auto" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Warning */}
|
|
<div className="rounded-xl border border-amber-500/20 bg-amber-500/5 px-4 py-3">
|
|
<p className="text-xs text-amber-400/80 leading-relaxed">
|
|
<span className="font-semibold text-amber-400">Note:</span> "Load demo data" adds records to your account. Use "Clear all data" to wipe everything when done testing.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex flex-col sm:flex-row gap-3">
|
|
<form action={seedDemoData} className="flex-1">
|
|
<button
|
|
type="submit"
|
|
className="w-full flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-6 py-3 text-sm font-semibold text-white hover:bg-indigo-500 transition-all hover:shadow-lg hover:shadow-indigo-500/25"
|
|
>
|
|
<Sparkles className="h-4 w-4" />
|
|
Load demo data
|
|
</button>
|
|
</form>
|
|
|
|
<form action={clearDemoData}>
|
|
<button
|
|
type="submit"
|
|
className="w-full sm:w-auto flex items-center justify-center gap-2 rounded-xl border border-red-500/20 bg-red-500/5 px-6 py-3 text-sm font-semibold text-red-400 hover:bg-red-500/10 hover:border-red-500/40 transition-all"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Clear all data
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|