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
+198
View File
@@ -0,0 +1,198 @@
import { NextResponse } from "next/server"
import { and, eq, gte, lte } from "drizzle-orm"
import { db } from "@/lib/db"
import {
follow_up_rules,
follow_up_log,
rent_payments,
maintenance_requests,
leases,
units,
} from "@/lib/db/schema"
import { getSessionUser } from "@/lib/session"
import { logActivity } from "@/lib/activity"
import { sendEmail } from "@/lib/email/send"
export async function POST() {
const user = await getSessionUser()
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const rules = await db
.select()
.from(follow_up_rules)
.where(and(eq(follow_up_rules.user_id, user.id), eq(follow_up_rules.is_active, true)))
if (!rules.length) return NextResponse.json({ sent: 0, results: [] })
const now = new Date()
const followUpsToLog: any[] = []
for (const rule of rules) {
const cutoff = new Date(now)
cutoff.setDate(cutoff.getDate() - rule.trigger_days)
if (rule.type === "overdue_rent") {
const overdue = await db.query.rent_payments.findMany({
where: and(
eq(rent_payments.user_id, user.id),
eq(rent_payments.status, "overdue"),
lte(rent_payments.due_date, cutoff.toISOString().slice(0, 10))
),
columns: { id: true, amount: true, due_date: true },
with: {
tenant: { columns: { first_name: true, last_name: true, email: true } },
},
})
for (const payment of overdue) {
const tenant = payment.tenant
if (!tenant?.email) continue
const daysOverdue = Math.ceil((now.getTime() - new Date(payment.due_date).getTime()) / (1000 * 60 * 60 * 24))
followUpsToLog.push({
user_id: user.id,
rule_id: rule.id,
type: "overdue_rent",
recipient_name: `${tenant.first_name} ${tenant.last_name}`,
recipient_email: tenant.email,
subject: `Rent Payment Reminder — ${daysOverdue} Days Overdue`,
message: rule.message_template
?? `Dear ${tenant.first_name}, your rent payment of $${Number(payment.amount).toLocaleString()} was due on ${payment.due_date} and is now ${daysOverdue} days overdue. Please make your payment as soon as possible to avoid further action.`,
status: "sent",
})
}
}
if (rule.type === "maintenance_stale") {
const stale = await db.query.maintenance_requests.findMany({
where: and(
eq(maintenance_requests.user_id, user.id),
eq(maintenance_requests.status, "open"),
lte(maintenance_requests.created_at, cutoff.toISOString())
),
columns: { id: true, title: true, priority: true, created_at: true },
with: {
tenant: { columns: { first_name: true, last_name: true, email: true } },
},
})
for (const req of stale) {
const tenant = req.tenant
const daysOpen = Math.ceil((now.getTime() - new Date(req.created_at).getTime()) / (1000 * 60 * 60 * 24))
followUpsToLog.push({
user_id: user.id,
rule_id: rule.id,
type: "maintenance_stale",
recipient_name: tenant ? `${tenant.first_name} ${tenant.last_name}` : "N/A",
recipient_email: tenant?.email ?? null,
subject: `Maintenance Update: ${req.title}`,
message: rule.message_template
?? `Your maintenance request "${req.title}" has been open for ${daysOpen} days. We are working on resolving this as soon as possible and will update you shortly.`,
status: "sent",
})
}
}
if (rule.type === "lease_renewal") {
const renewalDate = new Date(now)
renewalDate.setDate(renewalDate.getDate() + rule.trigger_days)
const expiring = await db.query.leases.findMany({
where: and(
eq(leases.user_id, user.id),
eq(leases.status, "active"),
lte(leases.lease_end, renewalDate.toISOString().slice(0, 10)),
gte(leases.lease_end, now.toISOString().slice(0, 10))
),
columns: { id: true, lease_end: true, rent_amount: true },
with: {
tenant: { columns: { first_name: true, last_name: true, email: true } },
},
})
for (const lease of expiring) {
const tenant = lease.tenant
if (!tenant?.email) continue
const daysLeft = Math.ceil((new Date(lease.lease_end).getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
followUpsToLog.push({
user_id: user.id,
rule_id: rule.id,
type: "lease_renewal",
recipient_name: `${tenant.first_name} ${tenant.last_name}`,
recipient_email: tenant.email,
subject: `Lease Renewal Notice — Expires in ${daysLeft} Days`,
message: rule.message_template
?? `Dear ${tenant.first_name}, your lease expires on ${lease.lease_end} (${daysLeft} days from now). Please contact us to discuss renewal options and ensure continuity of your tenancy.`,
status: "sent",
})
}
}
if (rule.type === "vacant_unit") {
const vacant = await db.query.units.findMany({
where: and(eq(units.user_id, user.id), eq(units.status, "vacant")),
columns: { id: true, unit_number: true, rent_amount: true },
with: {
property: { columns: { name: true } },
},
})
for (const unit of vacant) {
const property = unit.property
followUpsToLog.push({
user_id: user.id,
rule_id: rule.id,
type: "vacant_unit",
recipient_name: "You",
recipient_email: null,
subject: `Vacant Unit Alert: ${property?.name ?? ""} — Unit ${unit.unit_number}`,
message: rule.message_template
?? `Unit ${unit.unit_number} at ${property?.name ?? "your property"} has been vacant. Consider reviewing your listing or adjusting the rent of $${Number(unit.rent_amount).toLocaleString()}/month to attract tenants faster.`,
status: "sent",
})
}
}
// Update last_run_at
await db
.update(follow_up_rules)
.set({ last_run_at: now.toISOString() })
.where(and(eq(follow_up_rules.id, rule.id), eq(follow_up_rules.user_id, user.id)))
}
// Send actual emails for all follow-ups that have a recipient
for (const log of followUpsToLog) {
if (log.recipient_email) {
try {
await sendEmail({
to: log.recipient_email,
subject: log.subject,
html: `<!DOCTYPE html>
<html>
<body style="font-family:sans-serif;background:#09090b;color:#fff;padding:40px 20px;max-width:560px;margin:0 auto;">
<div style="background:#16161f;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:32px;">
<p style="font-size:15px;line-height:1.6;color:rgba(255,255,255,0.8);margin:0 0 24px;">${log.message.replace(/\n/g, "<br/>")}</p>
<p style="color:rgba(255,255,255,0.3);font-size:11px;margin:24px 0 0;border-top:1px solid rgba(255,255,255,0.06);padding-top:16px;">
Property Management Network — Automated Follow-up System
</p>
</div>
</body>
</html>`
})
} catch {
log.status = "failed"
}
}
}
if (followUpsToLog.length > 0) {
await db.insert(follow_up_log).values(followUpsToLog)
}
await logActivity({
userId: user.id,
type: "ai_action",
title: `Follow-ups processed: ${followUpsToLog.length} action${followUpsToLog.length !== 1 ? "s" : ""} triggered`,
})
return NextResponse.json({ sent: followUpsToLog.length, results: followUpsToLog })
}