Files
dashboards/apps/business/src/context/AuthContext.tsx
T
Alireza HassaniandCursor c33cab40fe Wire SMS registration, forgot password, and one-time login.
Connect business and customer login pages to login-otp and reset-password, and document the auth flows in project context.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 15:10:51 +03:30

158 lines
4.1 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from 'react'
import { ApiError, getAccessToken, isAbortError } from '../lib/api'
import {
setActiveBusinessById,
} from '../lib/businessContext'
import { getBusinessDomain } from '../lib/config'
import {
fetchCurrentUser,
login as loginRequest,
loginWithOtp as loginWithOtpRequest,
logout as logoutRequest,
} from '../services/authService'
import { resolveTenantByDomain } from '../services/tenantService'
import type { AuthUser } from '../types/auth'
interface AuthContextValue {
user: AuthUser | null
isLoading: boolean
login: (cellNumber: string, password: string) => Promise<void>
loginWithOtp: (cellNumber: string, code: string) => Promise<void>
logout: () => void
setUser: (user: AuthUser) => void
}
const AuthContext = createContext<AuthContextValue | null>(null)
const BUSINESS_ACCESS_MESSAGE =
'This account cannot access the business dashboard. Only business owners and staff can sign in here.'
function isSuperAdminUser(user: AuthUser) {
return user.isSuperAdmin === true || user.roles.includes('super_admin')
}
function canAccessBusinessDashboard(user: AuthUser) {
if (isSuperAdminUser(user)) {
return true
}
return user.dashboard === 'business' && user.businesses.length > 0
}
async function prepareBusinessContext(user: AuthUser) {
const domain = getBusinessDomain()
if (isSuperAdminUser(user)) {
const tenant = await resolveTenantByDomain(domain)
setActiveBusinessById(String(tenant.id), tenant.domain)
return
}
const membership =
user.businesses.find((business) => business.slug === tenantSlugFromDomain(domain)) ??
user.businesses[0]
if (!membership) {
throw new ApiError('No business membership found for this account.', 403)
}
setActiveBusinessById(String(membership.id), domain)
}
function tenantSlugFromDomain(domain: string) {
return domain.split('.')[0]?.toLowerCase() ?? domain
}
async function assertBusinessAccess(user: AuthUser) {
if (!canAccessBusinessDashboard(user)) {
logoutRequest()
throw new ApiError(BUSINESS_ACCESS_MESSAGE, 403)
}
await prepareBusinessContext(user)
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null)
const [isLoading, setIsLoading] = useState(true)
const logout = useCallback(() => {
logoutRequest()
setUser(null)
}, [])
useEffect(() => {
const controller = new AbortController()
async function init() {
if (!getAccessToken()) {
if (!controller.signal.aborted) setIsLoading(false)
return
}
try {
const { user: currentUser } = await fetchCurrentUser(controller.signal)
if (controller.signal.aborted) return
if (!canAccessBusinessDashboard(currentUser)) {
logout()
return
}
await prepareBusinessContext(currentUser)
if (controller.signal.aborted) return
setUser(currentUser)
} catch (err) {
if (isAbortError(err) || controller.signal.aborted) return
logout()
} finally {
if (!controller.signal.aborted) setIsLoading(false)
}
}
void init()
return () => {
controller.abort()
}
}, [logout])
const login = useCallback(async (cellNumber: string, password: string) => {
const data = await loginRequest(cellNumber, password)
await assertBusinessAccess(data.user)
setUser(data.user)
}, [])
const loginWithOtp = useCallback(async (cellNumber: string, code: string) => {
const data = await loginWithOtpRequest(cellNumber, code)
await assertBusinessAccess(data.user)
setUser(data.user)
}, [])
const value = useMemo(
() => ({ user, isLoading, login, loginWithOtp, logout, setUser }),
[user, isLoading, login, loginWithOtp, logout],
)
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
}
export function useAuth() {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within AuthProvider')
}
return context
}
export { BUSINESS_ACCESS_MESSAGE }