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>
399 lines
18 KiB
TypeScript
399 lines
18 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
"use server"
|
|
|
|
import { eq } from "drizzle-orm"
|
|
import { db } from "@/lib/db"
|
|
import {
|
|
properties,
|
|
units,
|
|
tenants,
|
|
leases,
|
|
rent_payments,
|
|
maintenance_requests,
|
|
expenses,
|
|
profiles,
|
|
} from "@/lib/db/schema"
|
|
import { getSessionUser, isAdminUser } from "@/lib/session"
|
|
import { redirect } from "next/navigation"
|
|
|
|
export async function seedDemoData() {
|
|
const user = await getSessionUser()
|
|
if (!user) redirect("/login")
|
|
if (!isAdminUser(user)) throw new Error("Admin only")
|
|
|
|
const uid = user.id
|
|
|
|
// ── 1. Properties ──────────────────────────────────────────────
|
|
const props = await db
|
|
.insert(properties)
|
|
.values([
|
|
{
|
|
user_id: uid, name: "Maple Court Apartments",
|
|
address_line1: "14 Maple Street", city: "Austin", state: "TX", postal_code: "78701",
|
|
property_type: "residential", total_units: 3, notes: "3-storey walkup, built 2005",
|
|
},
|
|
{
|
|
user_id: uid, name: "Riverdale Flats",
|
|
address_line1: "87 Riverside Drive", city: "Austin", state: "TX", postal_code: "78702",
|
|
property_type: "residential", total_units: 2, notes: "2 ground-floor units",
|
|
},
|
|
{
|
|
user_id: uid, name: "Crestwood Villa",
|
|
address_line1: "220 Crestwood Lane", city: "Austin", state: "TX", postal_code: "78703",
|
|
property_type: "residential", total_units: 2, notes: "Detached villa with garden units",
|
|
},
|
|
])
|
|
.returning({ id: properties.id, name: properties.name })
|
|
|
|
const [maple, river, crest] = props
|
|
|
|
// ── 2. Units ───────────────────────────────────────────────────
|
|
const unitRows = await db
|
|
.insert(units)
|
|
.values([
|
|
// Maple Court — 3 units
|
|
{ user_id: uid, property_id: maple.id, unit_number: "1A", bedrooms: 2, bathrooms: 1, sq_ft: 850, rent_amount: 1800, status: "occupied" },
|
|
{ user_id: uid, property_id: maple.id, unit_number: "1B", bedrooms: 1, bathrooms: 1, sq_ft: 620, rent_amount: 1350, status: "occupied" },
|
|
{ user_id: uid, property_id: maple.id, unit_number: "2A", bedrooms: 3, bathrooms: 2, sq_ft: 1100, rent_amount: 2400, status: "occupied" },
|
|
// Riverdale — 2 units
|
|
{ user_id: uid, property_id: river.id, unit_number: "G1", bedrooms: 2, bathrooms: 1, sq_ft: 780, rent_amount: 1650, status: "occupied" },
|
|
{ user_id: uid, property_id: river.id, unit_number: "G2", bedrooms: 1, bathrooms: 1, sq_ft: 560, rent_amount: 1200, status: "vacant" },
|
|
// Crestwood — 2 units
|
|
{ user_id: uid, property_id: crest.id, unit_number: "A", bedrooms: 3, bathrooms: 2, sq_ft: 1250, rent_amount: 2800, status: "occupied" },
|
|
{ user_id: uid, property_id: crest.id, unit_number: "B", bedrooms: 2, bathrooms: 1, sq_ft: 900, rent_amount: 2100, status: "occupied" },
|
|
])
|
|
.returning({ id: units.id, unit_number: units.unit_number, property_id: units.property_id, rent_amount: units.rent_amount })
|
|
|
|
const [u1A, u1B, u2A, uG1, , uA, uB] = unitRows
|
|
|
|
// ── 3. Tenants ─────────────────────────────────────────────────
|
|
const tenantRows = await db
|
|
.insert(tenants)
|
|
.values([
|
|
{
|
|
user_id: uid, property_id: maple.id, unit_id: u1A.id,
|
|
first_name: "Sarah", last_name: "Johnson",
|
|
email: "sarah.johnson@email.com", phone: "+1 (512) 555-0101",
|
|
move_in_date: "2023-06-01", status: "active",
|
|
emergency_contact_name: "Mike Johnson", emergency_contact_phone: "+1 (512) 555-0102",
|
|
},
|
|
{
|
|
user_id: uid, property_id: maple.id, unit_id: u1B.id,
|
|
first_name: "Marcus", last_name: "Lee",
|
|
email: "marcus.lee@email.com", phone: "+1 (512) 555-0201",
|
|
move_in_date: "2023-09-01", status: "active",
|
|
emergency_contact_name: "Lisa Lee", emergency_contact_phone: "+1 (512) 555-0202",
|
|
},
|
|
{
|
|
user_id: uid, property_id: maple.id, unit_id: u2A.id,
|
|
first_name: "Priya", last_name: "Patel",
|
|
email: "priya.patel@email.com", phone: "+1 (512) 555-0301",
|
|
move_in_date: "2022-12-01", status: "active",
|
|
emergency_contact_name: "Raj Patel", emergency_contact_phone: "+1 (512) 555-0302",
|
|
},
|
|
{
|
|
user_id: uid, property_id: river.id, unit_id: uG1.id,
|
|
first_name: "David", last_name: "Kim",
|
|
email: "david.kim@email.com", phone: "+1 (512) 555-0401",
|
|
move_in_date: "2024-01-15", status: "active",
|
|
emergency_contact_name: "Jenny Kim", emergency_contact_phone: "+1 (512) 555-0402",
|
|
},
|
|
{
|
|
user_id: uid, property_id: crest.id, unit_id: uA.id,
|
|
first_name: "Emily", last_name: "Carter",
|
|
email: "emily.carter@email.com", phone: "+1 (512) 555-0501",
|
|
move_in_date: "2023-03-01", status: "active",
|
|
emergency_contact_name: "Tom Carter", emergency_contact_phone: "+1 (512) 555-0502",
|
|
},
|
|
{
|
|
user_id: uid, property_id: crest.id, unit_id: uB.id,
|
|
first_name: "James", last_name: "Williams",
|
|
email: "james.williams@email.com", phone: "+1 (512) 555-0601",
|
|
move_in_date: "2024-03-01", status: "active",
|
|
emergency_contact_name: "Anna Williams", emergency_contact_phone: "+1 (512) 555-0602",
|
|
},
|
|
])
|
|
.returning({ id: tenants.id, first_name: tenants.first_name, last_name: tenants.last_name, property_id: tenants.property_id, unit_id: tenants.unit_id })
|
|
|
|
const [tSarah, tMarcus, tPriya, tDavid, tEmily, tJames] = tenantRows
|
|
|
|
// Update units with current_tenant_id
|
|
await Promise.all([
|
|
db.update(units).set({ current_tenant_id: tSarah.id }).where(eq(units.id, u1A.id)),
|
|
db.update(units).set({ current_tenant_id: tMarcus.id }).where(eq(units.id, u1B.id)),
|
|
db.update(units).set({ current_tenant_id: tPriya.id }).where(eq(units.id, u2A.id)),
|
|
db.update(units).set({ current_tenant_id: tDavid.id }).where(eq(units.id, uG1.id)),
|
|
db.update(units).set({ current_tenant_id: tEmily.id }).where(eq(units.id, uA.id)),
|
|
db.update(units).set({ current_tenant_id: tJames.id }).where(eq(units.id, uB.id)),
|
|
])
|
|
|
|
// ── 4. Leases ──────────────────────────────────────────────────
|
|
await db.insert(leases).values([
|
|
{
|
|
user_id: uid, tenant_id: tSarah.id, property_id: maple.id, unit_id: u1A.id,
|
|
lease_start: "2023-06-01", lease_end: "2025-05-31",
|
|
rent_amount: 1800, security_deposit: 3600, lease_type: "fixed", status: "active",
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tMarcus.id, property_id: maple.id, unit_id: u1B.id,
|
|
lease_start: "2023-09-01", lease_end: "2025-08-31",
|
|
rent_amount: 1350, security_deposit: 2700, lease_type: "fixed", status: "active",
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tPriya.id, property_id: maple.id, unit_id: u2A.id,
|
|
lease_start: "2022-12-01", lease_end: "2025-05-15", // expiring soon!
|
|
rent_amount: 2400, security_deposit: 4800, lease_type: "fixed", status: "active",
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tDavid.id, property_id: river.id, unit_id: uG1.id,
|
|
lease_start: "2024-01-15", lease_end: "2026-01-14",
|
|
rent_amount: 1650, security_deposit: 3300, lease_type: "fixed", status: "active",
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tEmily.id, property_id: crest.id, unit_id: uA.id,
|
|
lease_start: "2023-03-01", lease_end: "2025-04-28", // expiring very soon!
|
|
rent_amount: 2800, security_deposit: 5600, lease_type: "fixed", status: "active",
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tJames.id, property_id: crest.id, unit_id: uB.id,
|
|
lease_start: "2024-03-01", lease_end: "2026-02-28",
|
|
rent_amount: 2100, security_deposit: 4200, lease_type: "fixed", status: "active",
|
|
},
|
|
])
|
|
|
|
// ── 5. Rent Payments (last 6 months) ──────────────────────────
|
|
const now = new Date()
|
|
|
|
function dueDate(monthsAgo: number, day = 1) {
|
|
const d = new Date(now.getFullYear(), now.getMonth() - monthsAgo, day)
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
function paidDate(monthsAgo: number, day = 3) {
|
|
const d = new Date(now.getFullYear(), now.getMonth() - monthsAgo, day)
|
|
return d.toISOString().slice(0, 10)
|
|
}
|
|
|
|
const paymentRows: (typeof rent_payments.$inferInsert)[] = []
|
|
|
|
// Sarah — always pays on time (all paid)
|
|
for (let m = 5; m >= 1; m--) {
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tSarah.id, property_id: maple.id, unit_id: u1A.id,
|
|
amount: 1800, due_date: dueDate(m), paid_date: paidDate(m), status: "paid", payment_method: "bank_transfer",
|
|
})
|
|
}
|
|
// Sarah — this month pending
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tSarah.id, property_id: maple.id, unit_id: u1A.id,
|
|
amount: 1800, due_date: dueDate(0), status: "pending",
|
|
})
|
|
|
|
// Marcus — mostly paid, one overdue
|
|
for (let m = 5; m >= 2; m--) {
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tMarcus.id, property_id: maple.id, unit_id: u1B.id,
|
|
amount: 1350, due_date: dueDate(m), paid_date: paidDate(m), status: "paid", payment_method: "card",
|
|
})
|
|
}
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tMarcus.id, property_id: maple.id, unit_id: u1B.id,
|
|
amount: 1350, due_date: dueDate(1), status: "overdue",
|
|
})
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tMarcus.id, property_id: maple.id, unit_id: u1B.id,
|
|
amount: 1350, due_date: dueDate(0), status: "pending",
|
|
})
|
|
|
|
// Priya — all paid on time
|
|
for (let m = 5; m >= 1; m--) {
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tPriya.id, property_id: maple.id, unit_id: u2A.id,
|
|
amount: 2400, due_date: dueDate(m), paid_date: paidDate(m, 2), status: "paid", payment_method: "stripe",
|
|
})
|
|
}
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tPriya.id, property_id: maple.id, unit_id: u2A.id,
|
|
amount: 2400, due_date: dueDate(0), status: "pending",
|
|
})
|
|
|
|
// David — started Jan 2024, 3 paid months
|
|
for (let m = 3; m >= 1; m--) {
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tDavid.id, property_id: river.id, unit_id: uG1.id,
|
|
amount: 1650, due_date: dueDate(m), paid_date: paidDate(m), status: "paid", payment_method: "bank_transfer",
|
|
})
|
|
}
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tDavid.id, property_id: river.id, unit_id: uG1.id,
|
|
amount: 1650, due_date: dueDate(0), status: "pending",
|
|
})
|
|
|
|
// Emily — 4 paid
|
|
for (let m = 5; m >= 2; m--) {
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tEmily.id, property_id: crest.id, unit_id: uA.id,
|
|
amount: 2800, due_date: dueDate(m), paid_date: paidDate(m), status: "paid", payment_method: "stripe",
|
|
})
|
|
}
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tEmily.id, property_id: crest.id, unit_id: uA.id,
|
|
amount: 2800, due_date: dueDate(1), status: "overdue",
|
|
})
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tEmily.id, property_id: crest.id, unit_id: uA.id,
|
|
amount: 2800, due_date: dueDate(0), status: "pending",
|
|
})
|
|
|
|
// James — new tenant, 2 paid
|
|
for (let m = 2; m >= 1; m--) {
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tJames.id, property_id: crest.id, unit_id: uB.id,
|
|
amount: 2100, due_date: dueDate(m), paid_date: paidDate(m), status: "paid", payment_method: "card",
|
|
})
|
|
}
|
|
paymentRows.push({
|
|
user_id: uid, tenant_id: tJames.id, property_id: crest.id, unit_id: uB.id,
|
|
amount: 2100, due_date: dueDate(0), status: "pending",
|
|
})
|
|
|
|
await db.insert(rent_payments).values(paymentRows)
|
|
|
|
// ── 6. Maintenance Requests ────────────────────────────────────
|
|
const twoWeeksAgo = new Date(now); twoWeeksAgo.setDate(now.getDate() - 14)
|
|
const oneWeekAgo = new Date(now); oneWeekAgo.setDate(now.getDate() - 7)
|
|
const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1)
|
|
const threeDaysAgo = new Date(now); threeDaysAgo.setDate(now.getDate() - 3)
|
|
|
|
await db.insert(maintenance_requests).values([
|
|
{
|
|
user_id: uid, tenant_id: tSarah.id, property_id: maple.id, unit_id: u1A.id,
|
|
title: "Leaking kitchen faucet",
|
|
description: "The kitchen faucet has been dripping constantly for the past week. Water is pooling under the sink cabinet.",
|
|
category: "plumbing", priority: "medium", status: "open",
|
|
created_at: oneWeekAgo.toISOString(),
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tMarcus.id, property_id: maple.id, unit_id: u1B.id,
|
|
title: "Heating unit not working",
|
|
description: "The wall heater in the bedroom has stopped working completely. It's getting cold at night.",
|
|
category: "hvac", priority: "high", status: "in_progress",
|
|
assigned_to: "HVAC Pro Services", estimated_cost: 350,
|
|
created_at: twoWeeksAgo.toISOString(),
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tPriya.id, property_id: maple.id, unit_id: u2A.id,
|
|
title: "Broken window latch — Unit 2A",
|
|
description: "The latch on the living room window is broken. Window won't close fully and is a security concern.",
|
|
category: "general", priority: "medium", status: "open",
|
|
created_at: threeDaysAgo.toISOString(),
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tEmily.id, property_id: crest.id, unit_id: uA.id,
|
|
title: "Garage door motor failure",
|
|
description: "The automatic garage door opener stopped working. The motor makes a clicking noise but door won't lift.",
|
|
category: "electrical", priority: "high", status: "open",
|
|
created_at: yesterday.toISOString(),
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tDavid.id, property_id: river.id, unit_id: uG1.id,
|
|
title: "Paint peeling in bathroom",
|
|
description: "Bathroom ceiling paint is bubbling and peeling near the shower area. Likely moisture issue.",
|
|
category: "general", priority: "low", status: "resolved",
|
|
assigned_to: "Quick Fix Painters", actual_cost: 180,
|
|
resolved_at: twoWeeksAgo.toISOString(),
|
|
resolution_notes: "Repainted bathroom ceiling with moisture-resistant paint.",
|
|
created_at: new Date(now.getFullYear(), now.getMonth() - 2, 10).toISOString(),
|
|
},
|
|
{
|
|
user_id: uid, tenant_id: tJames.id, property_id: crest.id, unit_id: uB.id,
|
|
title: "Kitchen exhaust fan noisy",
|
|
description: "The kitchen exhaust fan makes a loud rattling noise. Sounds like something is loose inside.",
|
|
category: "general", priority: "low", status: "open",
|
|
created_at: new Date(now.getFullYear(), now.getMonth(), 5).toISOString(),
|
|
},
|
|
])
|
|
|
|
// ── 7. Expenses ────────────────────────────────────────────────
|
|
await db.insert(expenses).values([
|
|
{
|
|
user_id: uid, property_id: maple.id, unit_id: u1B.id,
|
|
category: "repairs", description: "HVAC inspection and repair — Unit 1B",
|
|
amount: 320, expense_date: twoWeeksAgo.toISOString().slice(0, 10),
|
|
vendor: "HVAC Pro Services", is_recurring: false,
|
|
},
|
|
{
|
|
user_id: uid, property_id: maple.id,
|
|
category: "management", description: "Common area deep clean — Maple Court",
|
|
amount: 250, expense_date: new Date(now.getFullYear(), now.getMonth(), 8).toISOString().slice(0, 10),
|
|
vendor: "CleanPro Austin", is_recurring: true, recurrence: "monthly",
|
|
},
|
|
{
|
|
user_id: uid, property_id: river.id,
|
|
category: "insurance", description: "Building insurance premium — Riverdale Flats",
|
|
amount: 1200, expense_date: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString().slice(0, 10),
|
|
vendor: "State Farm", is_recurring: true, recurrence: "annual",
|
|
},
|
|
{
|
|
user_id: uid, property_id: crest.id, unit_id: uA.id,
|
|
category: "repairs", description: "Garage door motor replacement",
|
|
amount: 480, expense_date: new Date(now.getFullYear(), now.getMonth(), 12).toISOString().slice(0, 10),
|
|
vendor: "Austin Door Co.", is_recurring: false,
|
|
},
|
|
{
|
|
user_id: uid, property_id: maple.id,
|
|
category: "utilities", description: "Common area electricity — March",
|
|
amount: 145, expense_date: new Date(now.getFullYear(), now.getMonth() - 1, 28).toISOString().slice(0, 10),
|
|
vendor: "Austin Energy", is_recurring: true, recurrence: "monthly",
|
|
},
|
|
{
|
|
user_id: uid, property_id: maple.id,
|
|
category: "management", description: "Property management software subscription",
|
|
amount: 29, expense_date: new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10),
|
|
vendor: "Property Management Network", is_recurring: true, recurrence: "monthly",
|
|
},
|
|
{
|
|
user_id: uid, property_id: crest.id,
|
|
category: "management", description: "Garden landscaping — quarterly",
|
|
amount: 360, expense_date: new Date(now.getFullYear(), now.getMonth() - 2, 15).toISOString().slice(0, 10),
|
|
vendor: "Green Thumb Landscaping", is_recurring: true, recurrence: "quarterly",
|
|
},
|
|
{
|
|
user_id: uid, property_id: river.id,
|
|
category: "taxes", description: "Property tax Q1 payment",
|
|
amount: 2100, expense_date: new Date(now.getFullYear(), now.getMonth() - 3, 15).toISOString().slice(0, 10),
|
|
vendor: "Travis County Tax Office", is_recurring: true, recurrence: "quarterly",
|
|
},
|
|
])
|
|
|
|
redirect("/dashboard")
|
|
}
|
|
|
|
export async function setTestPlan(plan: "pro" | "landlord" | "lifetime" | "starter") {
|
|
const user = await getSessionUser()
|
|
if (!user) redirect("/login")
|
|
if (!isAdminUser(user)) throw new Error("Admin only")
|
|
|
|
await db.update(profiles).set({ plan }).where(eq(profiles.id, user.id))
|
|
|
|
redirect("/settings/demo")
|
|
}
|
|
|
|
export async function clearDemoData() {
|
|
const user = await getSessionUser()
|
|
if (!user) redirect("/login")
|
|
if (!isAdminUser(user)) throw new Error("Admin only")
|
|
|
|
const uid = user.id
|
|
|
|
// Delete in dependency order
|
|
await db.delete(expenses).where(eq(expenses.user_id, uid))
|
|
await db.delete(maintenance_requests).where(eq(maintenance_requests.user_id, uid))
|
|
await db.delete(rent_payments).where(eq(rent_payments.user_id, uid))
|
|
await db.delete(leases).where(eq(leases.user_id, uid))
|
|
await db.delete(tenants).where(eq(tenants.user_id, uid))
|
|
await db.delete(units).where(eq(units.user_id, uid))
|
|
await db.delete(properties).where(eq(properties.user_id, uid))
|
|
|
|
redirect("/dashboard")
|
|
}
|