globalbrain/sefirot

Add `Policy` composable

Closed this issue · 0 comments

Policy is a composable to determine whether the user can perform certain actions. It provides streamlined and consistent authorization mechanism that can be integrated to other components.

Basic usage

// Define policy.
function useCanCreatePost(post: Post) {
  const user = getLoggedInUser()

  return usePolicy(({ allow, deny }) => {
    if (post.author === user) {
      return allow()
    }

    return deny()
  })
}

// Use it.
const canCreatePost = useCanCreatePost(post)

canCreatePost.value.ok // boolean

API

type Policy<Code = any> = Ref<PolicyRaw<Code>>

interface PolicyRaw<Code = any> {
  ok: boolean | null
  code: Code
  is(code: Code): boolean
}

interface PolicyResponse<Code = any> {
  ok: boolean | null
  code: Code
}

interface PolicyHelpers<Code = any> {
  allow(code?: Code): PolicyResponse<Code>
  deny(code?: Code): PolicyResponse<Code>
  pending(code?: Code): PolicyResponse<Code>
}

function usePolicy<Code = any>(
  fn: (helpers: PolicyHelpers) => PolicyResponse<Code>
): Policy<Code>

Using "code"

Policy can return response code that indicates "why" the user is not allowed to perform action. Using this, we may tell user the reason on UI.

<script setup lang="ts">
function useCanCreatePost(post: Post) {
  const user = getLoggedInUser()

  // Define code as generics.
  return usePolicy<'ok' | 'not-author'>(({ allow, deny }) => {
    // Set code on allow/deny.
    return post.author === user
      ? allow('ok')
      : deny('not-author')
  })
}

const canCreatePost = useCanCreatePost(post)
</script>

<template>
  <div>
    <div v-if="canCreatePost.ok">
      <!-- Show post -->
    </div>
    <div v-else>
      <p v-if="canCreatePost.is('not-author')">
        You are not the author of this post.
      </p>
    </div>
  </div>
</template>

Pending state

Sometimes, we need to wait for api call to determine the final authorization. In this case, use pending. The ok becomes null in this case.

function useCanCreatePost(post: MaybeRefOrGetter<Post>) {
  const user = getLoggedInUser()

  return usePolicy(({ allow, deny, pending }) => {
    const p = toValue(post)

    return p
      ? p.author === user ? allow() : deny()
      : pending()
  })
}

Real world app usage

In the real world, app, we should create a local usePolicy composable that inherits this composable and inject currently logged in user instance for easier access.

interface PolicyHelpers<Code = any> extends SefirotPolicyHelpers<Code> {
  user: User
}

function usePolicy<Code = any>(
  fn: (helpers: PolicyHelpers<Code>) => PolicyResponse<Code>
): Policy<Code> {
  const user = getLoggedInUser()

  return useSefirotPolicy((helpers) => {
    return fn({ ...helpers, user })
  })
}

function canCreatePost(): Policy {
  // Now the user object is available.
  return usePolicy({ user, allow, deny } => {
    return post.author === user
      ? allow()
      : deny()
  })
})