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:
+90
@@ -0,0 +1,90 @@
|
||||
import { betterAuth } from "better-auth"
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle"
|
||||
import { nextCookies } from "better-auth/next-js"
|
||||
import { admin } from "better-auth/plugins"
|
||||
import { db } from "@/lib/db"
|
||||
import { user, session, account, verification, profiles } from "@/lib/db/schema"
|
||||
import { sendEmail } from "@/lib/email/send"
|
||||
|
||||
// Bootstrap superadmins from env — no API path lets a user self-promote.
|
||||
const ADMIN_USER_IDS = (process.env.ADMIN_USER_IDS ?? "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
export const auth = betterAuth({
|
||||
baseURL: process.env.BETTER_AUTH_URL,
|
||||
secret: process.env.BETTER_AUTH_SECRET,
|
||||
database: drizzleAdapter(db, {
|
||||
provider: "pg",
|
||||
schema: { user, session, account, verification },
|
||||
}),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
// Login works immediately; flip to true once verification email is desired.
|
||||
// Recommended for production: set requireEmailVerification to true.
|
||||
requireEmailVerification: false,
|
||||
minPasswordLength: 8,
|
||||
sendResetPassword: async ({ user: u, url }) => {
|
||||
await sendEmail({
|
||||
to: u.email,
|
||||
subject: "Reset your Property Management Network password",
|
||||
html: resetPasswordHtml(url),
|
||||
})
|
||||
},
|
||||
},
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
|
||||
},
|
||||
},
|
||||
// Throttle auth endpoints (per IP) to slow brute-force / credential stuffing.
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
window: 60, // seconds
|
||||
max: 20, // requests per window per IP for auth endpoints
|
||||
},
|
||||
// Auto-create the app `profiles` row whenever Better Auth creates a user
|
||||
// (replaces the old `handle_new_user` Postgres trigger).
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
after: async (u) => {
|
||||
try {
|
||||
await db
|
||||
.insert(profiles)
|
||||
.values({ id: u.id, email: u.email, full_name: u.name ?? null })
|
||||
.onConflictDoNothing()
|
||||
} catch {
|
||||
// Never block sign-up on profile creation.
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// `admin` enables role/ban/impersonation; `nextCookies` MUST stay last.
|
||||
plugins: [
|
||||
admin({ adminUserIds: ADMIN_USER_IDS }),
|
||||
nextCookies(),
|
||||
],
|
||||
})
|
||||
|
||||
function resetPasswordHtml(url: string) {
|
||||
return `
|
||||
<!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;">
|
||||
<h1 style="font-size: 20px; margin: 0 0 8px; color: #fff;">Reset your password</h1>
|
||||
<p style="color: rgba(255,255,255,0.6); margin: 0 0 24px;">
|
||||
Click the button below to choose a new password. If you didn't request this, you can ignore this email.
|
||||
</p>
|
||||
<a href="${url}" style="display: inline-block; background: #6366f1; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600;">
|
||||
Reset Password
|
||||
</a>
|
||||
<p style="color: rgba(255,255,255,0.4); font-size: 12px; margin: 24px 0 0;">Property Management Network</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
}
|
||||
Reference in New Issue
Block a user