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>
106 lines
2.8 KiB
TypeScript
106 lines
2.8 KiB
TypeScript
"use server"
|
|
|
|
import { redirect } from "next/navigation"
|
|
import { headers } from "next/headers"
|
|
import { APIError } from "better-auth/api"
|
|
import { auth } from "@/lib/auth"
|
|
|
|
const APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
|
|
|
|
export async function signUp(formData: FormData) {
|
|
const email = formData.get("email") as string
|
|
const password = formData.get("password") as string
|
|
const fullName = formData.get("full_name") as string
|
|
|
|
try {
|
|
await auth.api.signUpEmail({
|
|
body: { email, password, name: fullName },
|
|
headers: await headers(),
|
|
})
|
|
} catch (e) {
|
|
const msg = e instanceof APIError ? e.message : "Sign up failed"
|
|
redirect(`/signup?error=${encodeURIComponent(msg)}`)
|
|
}
|
|
|
|
redirect("/dashboard")
|
|
}
|
|
|
|
export async function signIn(formData: FormData) {
|
|
const email = formData.get("email") as string
|
|
const password = formData.get("password") as string
|
|
|
|
try {
|
|
await auth.api.signInEmail({
|
|
body: { email, password },
|
|
headers: await headers(),
|
|
})
|
|
} catch (e) {
|
|
const msg = e instanceof APIError ? e.message : "Invalid email or password"
|
|
redirect(`/login?error=${encodeURIComponent(msg)}`)
|
|
}
|
|
|
|
redirect("/dashboard")
|
|
}
|
|
|
|
export async function signInWithGoogle() {
|
|
let url: string | undefined
|
|
try {
|
|
const res = await auth.api.signInSocial({
|
|
body: { provider: "google", callbackURL: "/dashboard" },
|
|
headers: await headers(),
|
|
})
|
|
url = res?.url ?? undefined
|
|
} catch (e) {
|
|
const msg = e instanceof APIError ? e.message : "Google sign-in failed"
|
|
redirect(`/login?error=${encodeURIComponent(msg)}`)
|
|
}
|
|
|
|
if (url) redirect(url)
|
|
redirect("/login?error=google_failed")
|
|
}
|
|
|
|
export async function resetPassword(formData: FormData) {
|
|
const email = formData.get("email") as string
|
|
|
|
try {
|
|
await auth.api.requestPasswordReset({
|
|
body: { email, redirectTo: `${APP_URL}/update-password` },
|
|
headers: await headers(),
|
|
})
|
|
} catch {
|
|
// Always report success so we don't reveal whether an account exists.
|
|
}
|
|
|
|
redirect("/forgot-password?success=email-sent")
|
|
}
|
|
|
|
export async function signOut() {
|
|
try {
|
|
await auth.api.signOut({ headers: await headers() })
|
|
} catch {
|
|
// ignore
|
|
}
|
|
redirect("/login")
|
|
}
|
|
|
|
export async function updatePassword(formData: FormData) {
|
|
const password = formData.get("password") as string
|
|
const token = formData.get("token") as string
|
|
|
|
if (!token) {
|
|
redirect(`/update-password?error=${encodeURIComponent("Reset link is invalid or expired.")}`)
|
|
}
|
|
|
|
try {
|
|
await auth.api.resetPassword({
|
|
body: { newPassword: password, token },
|
|
headers: await headers(),
|
|
})
|
|
} catch (e) {
|
|
const msg = e instanceof APIError ? e.message : "Could not update password"
|
|
redirect(`/update-password?error=${encodeURIComponent(msg)}&token=${encodeURIComponent(token)}`)
|
|
}
|
|
|
|
redirect("/login?success=password-updated")
|
|
}
|