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>
43 lines
1.0 KiB
TypeScript
43 lines
1.0 KiB
TypeScript
import { headers } from "next/headers"
|
|
import { db } from "@/lib/db"
|
|
import { admin_audit_log } from "@/lib/db/schema"
|
|
|
|
export type AdminAction =
|
|
| "plan_change"
|
|
| "ban"
|
|
| "unban"
|
|
| "set_role"
|
|
| "impersonate"
|
|
| "stop_impersonate"
|
|
| "delete_user"
|
|
| "resend_verification"
|
|
|
|
/**
|
|
* Append one immutable row to admin_audit_log. Call this for EVERY mutating
|
|
* admin action (the caller must already have passed getAdminSession()).
|
|
*/
|
|
export async function logAdminAction(opts: {
|
|
adminId: string
|
|
action: AdminAction
|
|
targetUserId?: string | null
|
|
metadata?: Record<string, unknown>
|
|
}) {
|
|
let ip: string | null = null
|
|
try {
|
|
const h = await headers()
|
|
ip =
|
|
h.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
|
h.get("x-real-ip") ??
|
|
null
|
|
} catch {
|
|
// headers() unavailable outside a request — fine.
|
|
}
|
|
await db.insert(admin_audit_log).values({
|
|
admin_id: opts.adminId,
|
|
action: opts.action,
|
|
target_user_id: opts.targetUserId ?? null,
|
|
metadata: opts.metadata ?? {},
|
|
ip_address: ip,
|
|
})
|
|
}
|