Source

tools/hooks/useScopeCheck.ts

import {useContext} from 'react'
import {ApiContext} from "../../services";
import {useOidc} from "./useOidc";

/**
 * @typedef {object} ScopeCheckReturn
 */
type ScopeCheckReturn = {
    /**
     * Whether the logged in user has ALL of the provided scopes
     */
    allScope: boolean
    /**
     * Whether the logged in user has ONE OR MORE of the provided scopes
     */
    anyScope: boolean
    /**
     * Whether there is a user currently logged in (if no user is present, all return values are false)
     */
    loggedIn: boolean
}

/**
 * Custom hook for checking whether an OIDC user has the provided scopes.
 *
 * The hook can accept zero or more strings that represent the requested
 * scopes, and returns a set of booleans representing the results.
 *
 * The three returned booleans are: `loggedIn`, `anyScope`, `allScope`
 * - `loggedIn`: Whether there is a user currently logged in (if no user is present, all return values are false)
 * - `anyScope`: Whether the logged in user has ONE OR MORE of the provided scopes
 * - `allScope`: Whether the logged in user has ALL of the provided scopes
 *
 * The hook uses the {@link ApiManager}'s templating engine to match provided scopes to the current environment.
 * This means you can provide generic scopes (e.g. an API interface scope) and it will templated to the
 * correct environment equivalent.
 *
 *
 * ### Hook Usage
 *
 * #### Checking if a user has a specific scope
 * ```
 * const {loggedIn, anyScope, allScope} = useScopeCheck('scope1')
 * ```
 * A single scope string can be checked against the scopes of the logged in user.
 * In this use case `anyScope` and `allScope` are equivalent, a single scope will be true for both.
 *
 * In practice, you will probably pull a scope from your {@link ApiInterface}
 * subclass, and capture just the boolean result you're interested in. You can
 * even rename your boolean as part of the destructure:
 * ```
 * const {allScope: isAdmin} = useScopeCheck(myApiInterface.scopes.ADMIN)
 * ```
 *
 * #### Checking if a user has multiple scopes
 * ```
 * const {loggedIn, anyScope, allScope} = useScopeCheck(['scope1','scope2'])
 * ```
 * Pass an array of scope strings to check multiple scopes at once. In this
 * case `anyScope` will be true if the user has *at least one* of the provided
 * scopes, and `allScope` will be true if the user has *every one* of the
 * provided scopes.
 *
 * An empty scopes array will result in `false`s in `allScope` and `anyScope`.
 *
 * #### Checking if a user is logged in
 * ```
 * const {loggedIn} = useScopeCheck()
 * ```
 * The hook can also be called without any arguments. Both `allScope` and
 * `anyScope` will be false, but `loggedIn` is valid, so you can use this to
 * check that a user has logged in without checking any specific scopes.
 *
 * @category Hooks
 *
 * @param [scopes] {string | string[]} The scope(s) to check against the current user
 * @return {ScopeCheckReturn} The results of checking the scope(s)
 */
export function useScopeCheck(scopes: string | string[] | undefined): ScopeCheckReturn {
    // Load oidcUser in case of scope props
    let {user} = useOidc()

    // Load apiManager for scope templating
    let apiContext = useContext(ApiContext)

    let allScope = false
    let anyScope = false
    let loggedIn = false

    // Check if their is a user logged in
    if (user) {
        loggedIn = true

        // If no scopes are provided, return the loggedIn value
        if (scopes !== undefined) {
            // Load scopes from the user
            let {scope: returnedScopeString} = user
            let returnedScopes = returnedScopeString.split(' ')

            if (Array.isArray(scopes)) {
                // Have to set allScope to true for the logic below to work
                // (the length check keeps allScope false if scopes is an empty array)
                allScope = scopes.length > 0
                for (let specificScope of scopes) {
                    // Convert generic scope into templated scope, if in template format
                    // Leaves un-templated scopes unchanged
                    let templatedScope = apiContext.getTemplatedScope(specificScope)
                    allScope = allScope && returnedScopes.includes(templatedScope)
                    anyScope = anyScope || returnedScopes.includes(templatedScope)
                }
            } else {
                // Convert generic scope into templated scope, if in template format
                // Leaves un-templated scopes unchanged
                let templatedScope = apiContext.getTemplatedScope(scopes)
                allScope = returnedScopes.includes(templatedScope)
                anyScope = returnedScopes.includes(templatedScope)
            }
        }
    }

    return {allScope, anyScope, loggedIn}
}