Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
@@ -0,0 +1,123 @@
|
||||
import { redirect } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
import { db } from "@/lib/db"
|
||||
import { profiles, properties, tenants, leases, rent_payments } from "@/lib/db/schema"
|
||||
import { getSessionUser } from "@/lib/session"
|
||||
import { completeOnboarding } from "@/app/actions/onboarding"
|
||||
import { Building2, Users, FileText, CreditCard, CheckCircle2, ArrowRight, Sparkles } from "lucide-react"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
async function count(table: typeof properties | typeof tenants | typeof leases | typeof rent_payments, userId: string) {
|
||||
const [row] = await db.select({ c: sql<number>`count(*)::int` }).from(table).where(eq(table.user_id, userId))
|
||||
return row?.c ?? 0
|
||||
}
|
||||
|
||||
export default async function OnboardingPage() {
|
||||
const user = await getSessionUser()
|
||||
if (!user) redirect("/login")
|
||||
|
||||
const profile = await db.query.profiles.findFirst({
|
||||
where: eq(profiles.id, user.id),
|
||||
columns: { onboarding_completed: true, full_name: true },
|
||||
})
|
||||
|
||||
// Users who already finished onboarding don't need this screen.
|
||||
if (profile?.onboarding_completed) redirect("/dashboard")
|
||||
|
||||
const [propCount, tenantCount, leaseCount, rentCount] = await Promise.all([
|
||||
count(properties, user.id),
|
||||
count(tenants, user.id),
|
||||
count(leases, user.id),
|
||||
count(rent_payments, user.id),
|
||||
])
|
||||
|
||||
const steps = [
|
||||
{ label: "Add your first property", desc: "Create a building or unit to manage.", href: "/properties/new", icon: Building2, done: propCount > 0 },
|
||||
{ label: "Add a tenant", desc: "Record who's renting from you.", href: "/tenants/new", icon: Users, done: tenantCount > 0 },
|
||||
{ label: "Set up a lease", desc: "Track term, rent, and deposit.", href: "/leases/new", icon: FileText, done: leaseCount > 0 },
|
||||
{ label: "Record a rent payment", desc: "Log or generate the first payment.", href: "/rent/new", icon: CreditCard, done: rentCount > 0 },
|
||||
]
|
||||
const doneCount = steps.filter((s) => s.done).length
|
||||
const firstName = profile?.full_name?.split(" ")[0]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl py-4">
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-indigo-600 shadow-lg shadow-indigo-500/25">
|
||||
<Sparkles className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">
|
||||
Welcome{firstName ? `, ${firstName}` : ""} 👋
|
||||
</h1>
|
||||
<p className="text-sm text-white/50">Let's get your portfolio set up in four quick steps.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="mb-6">
|
||||
<div className="mb-1.5 flex items-center justify-between text-xs text-white/50">
|
||||
<span>{doneCount} of {steps.length} complete</span>
|
||||
<span>{Math.round((doneCount / steps.length) * 100)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-white/[0.06]">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all"
|
||||
style={{ width: `${(doneCount / steps.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<ol className="space-y-3">
|
||||
{steps.map((step, i) => (
|
||||
<li
|
||||
key={step.label}
|
||||
className={`flex items-center gap-4 rounded-2xl border p-4 transition-colors ${
|
||||
step.done
|
||||
? "border-emerald-500/20 bg-emerald-500/[0.04]"
|
||||
: "border-white/[0.06] bg-[#16161f]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl ${
|
||||
step.done ? "bg-emerald-500/15 text-emerald-400" : "bg-white/[0.04] text-white/50"
|
||||
}`}
|
||||
>
|
||||
{step.done ? <CheckCircle2 className="h-5 w-5" /> : <step.icon className="h-5 w-5" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={`text-sm font-semibold ${step.done ? "text-white/60 line-through" : "text-white"}`}>
|
||||
{i + 1}. {step.label}
|
||||
</p>
|
||||
<p className="text-xs text-white/40">{step.desc}</p>
|
||||
</div>
|
||||
{step.done ? (
|
||||
<span className="shrink-0 text-xs font-medium text-emerald-400">Done</span>
|
||||
) : (
|
||||
<Link
|
||||
href={step.href}
|
||||
className="flex shrink-0 items-center gap-1 rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-indigo-500"
|
||||
>
|
||||
Add <ArrowRight className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
{/* Finish */}
|
||||
<form action={completeOnboarding} className="mt-6 flex items-center justify-between gap-3">
|
||||
<p className="text-xs text-white/40">You can always finish these later from the dashboard.</p>
|
||||
<button
|
||||
type="submit"
|
||||
className="shrink-0 rounded-lg border border-white/10 bg-white/5 px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/10"
|
||||
>
|
||||
{doneCount === steps.length ? "Finish setup" : "Skip to dashboard"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user