267 lines
8.5 KiB
TypeScript
267 lines
8.5 KiB
TypeScript
"use client"
|
||
|
||
import { createContext, useContext, useEffect, useMemo, useState } from "react"
|
||
import { customSessionClient } from "better-auth/client/plugins"
|
||
import { createAuthClient } from "better-auth/react"
|
||
import type { AppAuth } from "@/lib/auth"
|
||
import { useMutation } from "convex/react"
|
||
|
||
import { api } from "@/convex/_generated/api"
|
||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||
import { isAdmin, isStaff } from "@/lib/authz"
|
||
|
||
export type AppSession = {
|
||
user: {
|
||
id: string
|
||
name?: string | null
|
||
email: string
|
||
role: string
|
||
tenantId: string | null
|
||
avatarUrl: string | null
|
||
machinePersona?: string | null
|
||
}
|
||
session: {
|
||
id: string
|
||
expiresAt: number
|
||
}
|
||
}
|
||
|
||
type MachineContext = {
|
||
machineId: string
|
||
tenantId: string
|
||
persona: string | null
|
||
assignedUserId: string | null
|
||
assignedUserEmail: string | null
|
||
assignedUserName: string | null
|
||
assignedUserRole: string | null
|
||
companyId: string | null
|
||
}
|
||
|
||
type MachineContextError = {
|
||
status: number
|
||
message: string
|
||
details?: Record<string, unknown> | null
|
||
}
|
||
|
||
declare global {
|
||
interface Window {
|
||
__machineContextDebug?: unknown
|
||
}
|
||
}
|
||
|
||
const authClient = createAuthClient({
|
||
plugins: [customSessionClient<AppAuth>()],
|
||
fetchOptions: {
|
||
credentials: "include",
|
||
},
|
||
})
|
||
|
||
type AuthContextValue = {
|
||
session: AppSession | null
|
||
isLoading: boolean
|
||
convexUserId: string | null
|
||
role: string | null
|
||
isAdmin: boolean
|
||
isStaff: boolean
|
||
isCustomer: boolean
|
||
machineContext: MachineContext | null
|
||
machineContextLoading: boolean
|
||
machineContextError: MachineContextError | null
|
||
}
|
||
|
||
const AuthContext = createContext<AuthContextValue>({
|
||
session: null,
|
||
isLoading: true,
|
||
convexUserId: null,
|
||
role: null,
|
||
isAdmin: false,
|
||
isStaff: false,
|
||
isCustomer: false,
|
||
machineContext: null,
|
||
machineContextLoading: false,
|
||
machineContextError: null,
|
||
})
|
||
|
||
export function useAuth() {
|
||
return useContext(AuthContext)
|
||
}
|
||
|
||
export const { signIn, signOut, useSession } = authClient
|
||
|
||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||
const { data: session, isPending } = useSession()
|
||
const ensureUser = useMutation(api.users.ensureUser)
|
||
const [convexUserId, setConvexUserId] = useState<string | null>(null)
|
||
const [machineContext, setMachineContext] = useState<MachineContext | null>(null)
|
||
const [machineContextLoading, setMachineContextLoading] = useState(false)
|
||
const [machineContextError, setMachineContextError] = useState<MachineContextError | null>(null)
|
||
|
||
useEffect(() => {
|
||
if (!session?.user || session.user.role === "machine") {
|
||
setConvexUserId(null)
|
||
}
|
||
}, [session?.user])
|
||
|
||
// Sempre tenta obter o contexto da máquina.
|
||
// 1) Se a sessão Better Auth indicar role "machine", buscamos normalmente.
|
||
// 2) Se a sessão vier nula (alguns ambientes WebView), ainda assim tentamos
|
||
// carregar o contexto — se a API responder 200, assumimos que há sessão válida
|
||
// do lado do servidor e populamos o contexto para o restante do app.
|
||
useEffect(() => {
|
||
const shouldFetch = Boolean(session?.user?.role === "machine") || !session?.user
|
||
if (!shouldFetch) {
|
||
setMachineContext(null)
|
||
setMachineContextError(null)
|
||
setMachineContextLoading(false)
|
||
return
|
||
}
|
||
|
||
let cancelled = false
|
||
setMachineContextLoading(true)
|
||
setMachineContextError(null)
|
||
;(async () => {
|
||
try {
|
||
const response = await fetch("/api/machines/session", { credentials: "include" })
|
||
if (!response.ok) {
|
||
let payload: Record<string, unknown> | null = null
|
||
try {
|
||
const parsed = await response.clone().json()
|
||
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
||
payload = parsed as Record<string, unknown>
|
||
}
|
||
} catch {
|
||
payload = null
|
||
}
|
||
const fallbackMessage = "Falha ao carregar o contexto da m<>quina."
|
||
const message =
|
||
(payload && typeof payload.error === "string" && payload.error.trim()) || fallbackMessage
|
||
if (!cancelled) {
|
||
const debugPayload = {
|
||
status: response.status,
|
||
message,
|
||
payload,
|
||
timestamp: new Date().toISOString(),
|
||
}
|
||
console.error("[auth] machine context request failed", debugPayload)
|
||
if (typeof window !== "undefined") {
|
||
window.__machineContextDebug = debugPayload
|
||
}
|
||
setMachineContext(null)
|
||
setMachineContextError({
|
||
status: response.status,
|
||
message,
|
||
details: payload,
|
||
})
|
||
}
|
||
return
|
||
}
|
||
const data = await response.json()
|
||
if (!cancelled) {
|
||
const machine = data.machine as {
|
||
id: string
|
||
tenantId: string
|
||
persona: string | null
|
||
assignedUserId: string | null
|
||
assignedUserEmail: string | null
|
||
assignedUserName: string | null
|
||
assignedUserRole: string | null
|
||
companyId: string | null
|
||
}
|
||
setMachineContext({
|
||
machineId: machine.id,
|
||
tenantId: machine.tenantId,
|
||
persona: machine.persona ?? null,
|
||
assignedUserId: machine.assignedUserId ?? null,
|
||
assignedUserEmail: machine.assignedUserEmail ?? null,
|
||
assignedUserName: machine.assignedUserName ?? null,
|
||
assignedUserRole: machine.assignedUserRole ?? null,
|
||
companyId: machine.companyId ?? null,
|
||
})
|
||
setMachineContextError(null)
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load machine context", error)
|
||
if (!cancelled) {
|
||
const debugPayload = {
|
||
error: error instanceof Error ? { name: error.name, message: error.message, stack: error.stack } : String(error),
|
||
timestamp: new Date().toISOString(),
|
||
}
|
||
console.error("[auth] machine context unexpected error", debugPayload)
|
||
if (typeof window !== "undefined") {
|
||
window.__machineContextDebug = debugPayload
|
||
}
|
||
setMachineContext(null)
|
||
setMachineContextError({
|
||
status: 0,
|
||
message: "Erro ao carregar o contexto da m<>quina.",
|
||
details: error instanceof Error ? { message: error.message } : null,
|
||
})
|
||
}
|
||
} finally {
|
||
if (!cancelled) {
|
||
setMachineContextLoading(false)
|
||
}
|
||
}
|
||
})()
|
||
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [session?.user])
|
||
|
||
useEffect(() => {
|
||
if (!session?.user || session.user.role === "machine" || convexUserId) return
|
||
|
||
const controller = new AbortController()
|
||
|
||
;(async () => {
|
||
try {
|
||
const ensured = await ensureUser({
|
||
tenantId: session.user.tenantId ?? DEFAULT_TENANT_ID,
|
||
name: session.user.name ?? session.user.email,
|
||
email: session.user.email,
|
||
avatarUrl: session.user.avatarUrl ?? undefined,
|
||
role: session.user.role.toUpperCase(),
|
||
})
|
||
if (!controller.signal.aborted) {
|
||
setConvexUserId(ensured?._id ?? null)
|
||
}
|
||
} catch (error) {
|
||
if (!controller.signal.aborted) {
|
||
console.error("Failed to sync user with Convex", error)
|
||
}
|
||
}
|
||
})()
|
||
|
||
return () => {
|
||
controller.abort()
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [ensureUser, session?.user?.email, session?.user?.tenantId, session?.user?.role, convexUserId])
|
||
|
||
// Se não houver sessão mas tivermos contexto de máquina, tratamos como "machine"
|
||
const baseRole = session?.user?.role ? session.user.role.toLowerCase() : (machineContext ? "machine" : null)
|
||
const personaRole = session?.user?.machinePersona ? session.user.machinePersona.toLowerCase() : null
|
||
const normalizedRole =
|
||
baseRole === "machine" ? (machineContext?.persona ?? personaRole ?? null) : baseRole
|
||
|
||
const effectiveConvexUserId = baseRole === "machine" ? (machineContext?.assignedUserId ?? null) : convexUserId
|
||
|
||
const value = useMemo<AuthContextValue>(
|
||
() => ({
|
||
session: session ?? null,
|
||
isLoading: isPending,
|
||
convexUserId: effectiveConvexUserId,
|
||
role: normalizedRole,
|
||
isAdmin: isAdmin(normalizedRole),
|
||
isStaff: isStaff(normalizedRole),
|
||
isCustomer: normalizedRole === "collaborator",
|
||
machineContext,
|
||
machineContextLoading,
|
||
machineContextError,
|
||
}),
|
||
[session, isPending, effectiveConvexUserId, normalizedRole, machineContext, machineContextLoading, machineContextError]
|
||
)
|
||
|
||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||
}
|