Files

193 lines
5.9 KiB
TypeScript

"use client"
import { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from "react"
import { hasConsoleScopes, type ConsolePolicy } from "@/lib/console-policy-parser"
import { hasConsoleCapability, type ConsoleCapability } from "@/lib/permission-capabilities"
import type { PermissionResourceContext } from "@/lib/permission-resources"
import { CONSOLE_SCOPES, PAGE_PERMISSIONS } from "@/lib/console-permissions"
import { scheduleMicrotask } from "@/lib/schedule-microtask"
import { useAuth } from "@/contexts/auth-context"
import { useApiOptional } from "@/contexts/api-context"
interface PermissionsContextValue {
userPolicy: ConsolePolicy | null
userInfo: Record<string, unknown> | null
isLoading: boolean
hasResolvedAdmin: boolean
hasFetchedPolicy: boolean
fetchUserPolicy: () => Promise<void>
hasPermission: (action: string | string[], matchAll?: boolean) => boolean
canCapability: (capability: ConsoleCapability, context?: PermissionResourceContext) => boolean
canAccessPath: (path: string) => boolean
isAdmin: boolean
/** True when user has consoleAdmin (or is admin). Only these users can change password per RustFS backend. */
canChangePassword: boolean
}
const PermissionsContext = createContext<PermissionsContextValue | null>(null)
export function PermissionsProvider({ children }: { children: React.ReactNode }) {
const api = useApiOptional()
const { isAdmin, isAuthenticated, credentials, setIsAdmin } = useAuth()
const [userPolicy, setUserPolicy] = useState<ConsolePolicy | null>(null)
const [userInfo, setUserInfo] = useState<Record<string, unknown> | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [hasResolvedAdmin, setHasResolvedAdmin] = useState(false)
const [hasFetchedPolicy, setHasFetchedPolicy] = useState(false)
const requestEpochRef = useRef(0)
useEffect(() => {
requestEpochRef.current += 1
setUserInfo(null)
setUserPolicy(null)
setHasResolvedAdmin(false)
setHasFetchedPolicy(false)
if (isAuthenticated) {
setIsAdmin(false)
}
}, [api, credentials?.AccessKeyId, credentials?.SessionToken, isAuthenticated, setIsAdmin])
const fetchAdminStatus = useCallback(async () => {
if (!api) return
const requestEpoch = requestEpochRef.current
try {
const info = (await api.get("/is-admin")) as { is_admin?: boolean }
if (requestEpoch !== requestEpochRef.current) return
setIsAdmin(info?.is_admin ?? false)
} catch (e) {
if (requestEpoch !== requestEpochRef.current) return
console.error("Failed to resolve admin status", e)
setIsAdmin(false)
} finally {
if (requestEpoch === requestEpochRef.current) setHasResolvedAdmin(true)
}
}, [api, setIsAdmin])
const fetchUserPolicy = useCallback(async () => {
if (!api) return
const requestEpoch = requestEpochRef.current
setIsLoading(true)
try {
const info = (await api.get("/accountinfo")) as Record<string, unknown>
if (requestEpoch !== requestEpochRef.current) return
setUserInfo(info)
const policyStr = info?.policy as string
if (policyStr) {
setUserPolicy(JSON.parse(policyStr) as ConsolePolicy)
} else {
setUserPolicy(null)
}
} catch (e) {
if (requestEpoch !== requestEpochRef.current) return
console.error("Failed to fetch user policy", e)
setUserPolicy(null)
} finally {
if (requestEpoch === requestEpochRef.current) {
setIsLoading(false)
setHasFetchedPolicy(true)
}
}
}, [api])
const hasPermission = useCallback(
(action: string | string[], matchAll: boolean = true) => {
if (isAdmin) return true
if (!userPolicy) return false
const actions = Array.isArray(action) ? action : [action]
return hasConsoleScopes(userPolicy, actions, matchAll)
},
[isAdmin, userPolicy],
)
const canCapability = useCallback(
(capability: ConsoleCapability, context: PermissionResourceContext = {}) => {
if (isAdmin) return true
return hasConsoleCapability(userPolicy, capability, context)
},
[isAdmin, userPolicy],
)
const canAccessPath = useCallback(
(path: string) => {
if (isAdmin) return true
let requiredScopes = PAGE_PERMISSIONS[path]
if (!requiredScopes) {
const match = Object.keys(PAGE_PERMISSIONS).find((key) => path.startsWith(key))
if (match) {
requiredScopes = PAGE_PERMISSIONS[match]
}
}
if (!requiredScopes) return true
return hasPermission(requiredScopes)
},
[isAdmin, hasPermission],
)
useEffect(() => {
let cancelled = false
if (api && isAuthenticated && !hasResolvedAdmin) {
scheduleMicrotask(() => {
if (!cancelled) void fetchAdminStatus()
})
}
return () => {
cancelled = true
}
}, [api, isAuthenticated, hasResolvedAdmin, fetchAdminStatus])
useEffect(() => {
if (api && isAuthenticated && hasResolvedAdmin && !hasFetchedPolicy && !isLoading) {
fetchUserPolicy()
}
}, [api, isAuthenticated, hasResolvedAdmin, hasFetchedPolicy, isLoading, fetchUserPolicy])
const canChangePassword = isAdmin || hasPermission(CONSOLE_SCOPES.CONSOLE_ADMIN)
const value = useMemo(
() => ({
userPolicy,
userInfo,
isLoading,
hasResolvedAdmin,
hasFetchedPolicy,
fetchUserPolicy,
hasPermission,
canCapability,
canAccessPath,
isAdmin,
canChangePassword,
}),
[
userPolicy,
userInfo,
isLoading,
hasResolvedAdmin,
hasFetchedPolicy,
fetchUserPolicy,
hasPermission,
canCapability,
canAccessPath,
isAdmin,
canChangePassword,
],
)
return <PermissionsContext.Provider value={value}>{children}</PermissionsContext.Provider>
}
export function usePermissions() {
const ctx = useContext(PermissionsContext)
if (!ctx) {
throw new Error("usePermissions must be used within PermissionsProvider")
}
return ctx
}