fix(export): return 501 with hint when Playwright browsers missing; nicer error toast in UI fix(site-header): export primary/secondary buttons as named for SC safety; keep static props for compat fix(portal): add DialogDescription for a11y; tidy preview dialog fix(csats): avoid reinit state loops with timestamp guard chore(prisma): default dev DB to prisma/db.dev.sqlite and log path chore(auth): add dev bypass flags wiring (server/client) for local testing dev: seed script for Convex demo data
159 lines
4.4 KiB
TypeScript
159 lines
4.4 KiB
TypeScript
import { cookies, headers } from "next/headers"
|
|
import { redirect } from "next/navigation"
|
|
|
|
import { env } from "@/lib/env"
|
|
import { isAdmin, isStaff } from "@/lib/authz"
|
|
import { auth } from "@/lib/auth"
|
|
|
|
type ServerSession = {
|
|
session: {
|
|
id: string
|
|
expiresAt: number
|
|
}
|
|
user: {
|
|
id: string
|
|
name: string
|
|
email: string
|
|
role: string
|
|
tenantId: string | null
|
|
avatarUrl: string | null
|
|
machinePersona: string | null
|
|
}
|
|
}
|
|
|
|
async function serializeCookies() {
|
|
const store = await cookies()
|
|
return store.getAll().map((cookie) => `${cookie.name}=${cookie.value}`).join("; ")
|
|
}
|
|
|
|
async function buildRequest() {
|
|
const cookieHeader = await serializeCookies()
|
|
const headerList = await headers()
|
|
const userAgent = headerList.get("user-agent") ?? ""
|
|
const ip =
|
|
headerList.get("x-forwarded-for") ||
|
|
headerList.get("x-real-ip") ||
|
|
headerList.get("cf-connecting-ip") ||
|
|
headerList.get("x-client-ip") ||
|
|
undefined
|
|
|
|
// Evitar fallback para localhost em produção: tenta BETTER_AUTH_URL,
|
|
// depois NEXT_PUBLIC_APP_URL e, por fim, o host do próprio request.
|
|
const forwardedProto = headerList.get("x-forwarded-proto")
|
|
const forwardedHost = headerList.get("x-forwarded-host") ?? headerList.get("host")
|
|
const requestOrigin = forwardedProto && forwardedHost ? `${forwardedProto}://${forwardedHost}` : undefined
|
|
const baseUrl = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_APP_URL || requestOrigin || "http://localhost:3000"
|
|
|
|
return new Request(baseUrl, {
|
|
headers: {
|
|
cookie: cookieHeader,
|
|
"user-agent": userAgent,
|
|
...(ip ? { "x-forwarded-for": ip } : {}),
|
|
},
|
|
})
|
|
}
|
|
|
|
export async function getServerSession(): Promise<ServerSession | null> {
|
|
try {
|
|
// Dev-only bypass to simplify local dashboard access when auth is misconfigured.
|
|
if (process.env.NODE_ENV !== "production" && process.env.DEV_BYPASS_AUTH === "1") {
|
|
return {
|
|
session: {
|
|
id: "dev-session",
|
|
expiresAt: Date.now() + 1000 * 60 * 60,
|
|
},
|
|
user: {
|
|
id: "dev-user",
|
|
name: "Dev Admin",
|
|
email: "admin@sistema.dev",
|
|
role: "admin",
|
|
tenantId: "tenant-atlas",
|
|
avatarUrl: null,
|
|
machinePersona: null,
|
|
},
|
|
}
|
|
}
|
|
const request = await buildRequest()
|
|
const session = await auth.api.getSession({
|
|
headers: request.headers,
|
|
request,
|
|
})
|
|
if (!session) return null
|
|
|
|
const expiresValue = session.session.expiresAt
|
|
const expiresAt =
|
|
expiresValue instanceof Date
|
|
? expiresValue.getTime()
|
|
: typeof expiresValue === "number"
|
|
? expiresValue
|
|
: expiresValue
|
|
? new Date(expiresValue).getTime()
|
|
: Date.now()
|
|
|
|
return {
|
|
session: {
|
|
id: session.session.id,
|
|
expiresAt,
|
|
},
|
|
user: {
|
|
id: session.user.id,
|
|
name: session.user.name,
|
|
email: session.user.email,
|
|
role: (session.user as { role?: string }).role ?? "agent",
|
|
tenantId: (session.user as { tenantId?: string | null }).tenantId ?? null,
|
|
avatarUrl: (session.user as { avatarUrl?: string | null }).avatarUrl ?? null,
|
|
machinePersona: (session.user as { machinePersona?: string | null }).machinePersona ?? null,
|
|
},
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to read Better Auth session", error)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function assertAuthenticatedSession() {
|
|
const session = await getServerSession()
|
|
return session?.user ? session : null
|
|
}
|
|
|
|
export async function requireAuthenticatedSession() {
|
|
const session = await assertAuthenticatedSession()
|
|
if (!session) {
|
|
redirect("/login")
|
|
}
|
|
return session
|
|
}
|
|
|
|
export async function assertStaffSession() {
|
|
const session = await assertAuthenticatedSession()
|
|
if (!session) return null
|
|
if (!isStaff(session.user.role)) {
|
|
return null
|
|
}
|
|
return session
|
|
}
|
|
|
|
export async function requireStaffSession() {
|
|
const session = await assertStaffSession()
|
|
if (!session) {
|
|
redirect("/portal")
|
|
}
|
|
return session
|
|
}
|
|
|
|
export async function assertAdminSession() {
|
|
const session = await assertAuthenticatedSession()
|
|
if (!session) return null
|
|
if (!isAdmin(session.user.role)) {
|
|
return null
|
|
}
|
|
return session
|
|
}
|
|
|
|
export async function requireAdminSession() {
|
|
const session = await assertAdminSession()
|
|
if (!session) {
|
|
redirect("/dashboard")
|
|
}
|
|
return session
|
|
}
|