118 lines
3 KiB
TypeScript
118 lines
3 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
|
|
}
|
|
session: {
|
|
id: string
|
|
expiresAt: number
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue>({
|
|
session: null,
|
|
isLoading: true,
|
|
convexUserId: null,
|
|
role: null,
|
|
isAdmin: false,
|
|
isStaff: false,
|
|
isCustomer: false,
|
|
})
|
|
|
|
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)
|
|
|
|
useEffect(() => {
|
|
if (!session?.user) {
|
|
setConvexUserId(null)
|
|
}
|
|
}, [session?.user])
|
|
|
|
useEffect(() => {
|
|
if (!session?.user || 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])
|
|
|
|
const normalizedRole = session?.user?.role ? session.user.role.toLowerCase() : null
|
|
|
|
const value = useMemo<AuthContextValue>(
|
|
() => ({
|
|
session: session ?? null,
|
|
isLoading: isPending,
|
|
convexUserId,
|
|
role: normalizedRole,
|
|
isAdmin: isAdmin(normalizedRole),
|
|
isStaff: isStaff(normalizedRole),
|
|
isCustomer: false,
|
|
}),
|
|
[session, isPending, convexUserId, normalizedRole]
|
|
)
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
|
}
|