chore: sync in-progress work across marketing, admin, API and tests
Snapshot of uncommitted work that had accumulated in the tree alongside the Turnstile changes: - marketing pages, SEO helpers (lib/seo.ts, lib/marketing/) and structured data - admin billing actions and a per-user portfolio view, plus an admin error boundary - rate limiting (lib/rate-limit.ts) applied across the /api/v1 surface - CSP and proxy adjustments, accounting/webhook lib updates - Playwright config and an e2e/unit test suite - next bumped to ^16.3.4 with the lockfile regenerated - generated AGENTS.md / CLAUDE.md Authored by other sessions working in this tree; committed here so the Turnstile work could be pushed without leaving the tree dirty. Typecheck passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8f90347659
commit
1d02598786
@@ -401,6 +401,107 @@ export async function getUserDetail(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── per-user portfolio (admin support view) ───────────────────────────────────
|
||||
// Read-only window into ONE user's actual records. Before this existed an admin
|
||||
// could see only aggregate counts, so answering "what does this customer
|
||||
// actually have?" meant impersonating them — which mutates their session and
|
||||
// shows up in their own audit trail. This is deliberately read-only: it answers
|
||||
// support questions without touching anything.
|
||||
//
|
||||
// Every query is scoped by user_id. Admin queries bypass the app's normal
|
||||
// ownership scoping, so the caller MUST have passed requireAdmin()/getAdminSession().
|
||||
export async function getUserPortfolio(userId: string) {
|
||||
const [propertyRows, unitRows, tenantRows, leaseRows, paymentRows, maintenanceRows] =
|
||||
await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: properties.id,
|
||||
name: properties.name,
|
||||
address_line1: properties.address_line1,
|
||||
city: properties.city,
|
||||
state: properties.state,
|
||||
total_units: properties.total_units,
|
||||
created_at: properties.created_at,
|
||||
})
|
||||
.from(properties)
|
||||
.where(eq(properties.user_id, userId))
|
||||
.orderBy(desc(properties.created_at))
|
||||
.limit(100),
|
||||
db
|
||||
.select({
|
||||
id: units.id,
|
||||
property_id: units.property_id,
|
||||
unit_number: units.unit_number,
|
||||
rent_amount: units.rent_amount,
|
||||
status: units.status,
|
||||
})
|
||||
.from(units)
|
||||
.where(eq(units.user_id, userId))
|
||||
.orderBy(desc(units.created_at))
|
||||
.limit(200),
|
||||
db
|
||||
.select({
|
||||
id: tenants.id,
|
||||
first_name: tenants.first_name,
|
||||
last_name: tenants.last_name,
|
||||
email: tenants.email,
|
||||
phone: tenants.phone,
|
||||
status: tenants.status,
|
||||
move_in_date: tenants.move_in_date,
|
||||
})
|
||||
.from(tenants)
|
||||
.where(eq(tenants.user_id, userId))
|
||||
.orderBy(desc(tenants.created_at))
|
||||
.limit(100),
|
||||
db
|
||||
.select({
|
||||
id: leases.id,
|
||||
tenant_id: leases.tenant_id,
|
||||
lease_start: leases.lease_start,
|
||||
lease_end: leases.lease_end,
|
||||
rent_amount: leases.rent_amount,
|
||||
status: leases.status,
|
||||
})
|
||||
.from(leases)
|
||||
.where(eq(leases.user_id, userId))
|
||||
.orderBy(desc(leases.created_at))
|
||||
.limit(100),
|
||||
db
|
||||
.select({
|
||||
id: rent_payments.id,
|
||||
amount: rent_payments.amount,
|
||||
due_date: rent_payments.due_date,
|
||||
paid_date: rent_payments.paid_date,
|
||||
status: rent_payments.status,
|
||||
})
|
||||
.from(rent_payments)
|
||||
.where(eq(rent_payments.user_id, userId))
|
||||
.orderBy(desc(rent_payments.due_date))
|
||||
.limit(50),
|
||||
db
|
||||
.select({
|
||||
id: maintenance_requests.id,
|
||||
title: maintenance_requests.title,
|
||||
priority: maintenance_requests.priority,
|
||||
status: maintenance_requests.status,
|
||||
created_at: maintenance_requests.created_at,
|
||||
})
|
||||
.from(maintenance_requests)
|
||||
.where(eq(maintenance_requests.user_id, userId))
|
||||
.orderBy(desc(maintenance_requests.created_at))
|
||||
.limit(50),
|
||||
])
|
||||
|
||||
return {
|
||||
properties: propertyRows,
|
||||
units: unitRows,
|
||||
tenants: tenantRows,
|
||||
leases: leaseRows,
|
||||
payments: paymentRows,
|
||||
maintenance: maintenanceRows,
|
||||
}
|
||||
}
|
||||
|
||||
// ── CSV helpers (shared by admin export routes) ─────────────────────────────────
|
||||
export function toCsv(headers: string[], rows: (string | number | null | undefined)[][]) {
|
||||
const esc = (v: string | number | null | undefined) => {
|
||||
|
||||
Reference in New Issue
Block a user