nuxt-authorization

アプリとサーバー内部での権限を管理します。

Nuxt Authorization

npm versionnpm downloadsLicenseNuxtpkg.pr.new

NuxtとNitroの両方で認可を簡単に処理します。

このモジュールはACLやRBACを実装していません。独自の認可ロジックを実装するために使用できる低レベルのプリミティブを提供します。

!NOTE 将来的には、このモジュールはNitroモジュールおよびNuxtモジュールとして利用可能になる可能性がありますが、Nitroモジュールはまだ準備ができていません。

このモジュールとそれが解決する問題について詳しく知るには、Nuxtでの認可の処理に関する私のブログ記事をご覧ください。

機能

  • ⛰  クライアント(Nuxt)とサーバー(Nitro)の両方で動作します
  • 🌟  一度アビリティを記述すれば、どこでも使用できます
  • 👨‍👩‍👧‍👦  認証レイヤーに依存しません
  • 🫸  コンポーネントを使用してUIの一部を条件付きで表示します
  • 💧  プリミティブは完全にカスタマイズできます

クイックセットアップ

1つのコマンドでモジュールをNuxtアプリケーションにインストールします

npx nuxi module add nuxt-authorization

以上です!これでNuxtアプリでモジュールを使用できるようになります✨

ドキュメント

!NOTE 動作中のモジュールを見るには、プレイグラウンドをご覧ください。

セットアップ

モジュールを使用し、最初のアビリティを定義する前に、2つのリゾルバーを提供する必要があります。これらの関数は内部的にユーザーを取得するために使用されますが、自分で実装する必要があります。これにより、モジュールは認証レイヤーに依存しなくなります。

Nuxtアプリの場合、plugins/authorization-resolver.tsに新しいプラグインを作成します。

export default defineNuxtPlugin({
  name: 'authorization-resolver',
  parallel: true,
  setup() {
    return {
      provide: {
        authorization: {
          resolveClientUser: () => {
            // Your logic to retrieve the user from the client
          },
        },
      },
    }
  },
})

この関数は、クライアントで認可をチェックするたびに呼び出されます。ユーザーが認証されていない場合、ユーザーオブジェクトまたはnullを返す必要があります。非同期にすることも可能です。

Nitroサーバーの場合、server/plugins/authorization-resolver.tsに新しいプラグインを作成します。

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    event.context.$authorization = {
      resolveServerUser: () => {
        // Your logic to retrieve the user from the server
      },
    }
  })
})

!NOTE event.contextについて詳しく読む

このリゾルバーはrequestフック内で設定され、イベントを受け取ります。セッションまたはリクエストからユーザーを取得するために使用できます。ユーザーが認証されていない場合、ユーザーオブジェクトまたはnullを返す必要があります。非同期にすることも可能です。

通常、アプリの起動時にプラグインを使用してユーザーをフェッチし、それを保存します。リゾルバー関数は、保存されたユーザーを返すだけで、再度フェッチしないようにする必要があります(そうしないと、重大なパフォーマンスの問題が発生する可能性があります)。

nuxt-auth-utilsの例

nuxt-auth-utilsモジュールはNuxt用の認証レイヤーを提供します。このモジュールを使用する場合、以下のリゾルバーを使用できます。

Nuxtプラグイン

export default defineNuxtPlugin({
  name: 'authorization-resolver',
  parallel: true,
  setup() {
    return {
      provide: {
        authorization: {
          resolveClientUser: () => useUserSession().user.value,
        },
      },
    }
  },
})

Nitroプラグイン

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('request', async (event) => {
    event.context.$authorization = {
      resolveServerUser: async () => {
        const session = await getUserSession(event)
        return session.user ?? null
      },
    }
  })
})

簡単!

アビリティの定義

!NOTE Nuxt 4では、クライアントとサーバー間でコードを簡単に共有するための新しいsharedディレクトリが導入されます。Alexander Lichterのビデオをご覧ください。

リゾルバーが設定されたので、最初のアビリティを定義できます。アビリティは、少なくともユーザーを受け取り、ユーザーがアクションを実行できるかどうかを示すブール値を返す関数です。追加の引数を受け取ることもできます。

アビリティを作成するために、新しいファイルshared/utils/abilities.tsを作成することをお勧めします。

export const listPosts = defineAbility(() => true) // Only authenticated users can list posts

export const editPost = defineAbility((user: User, post: Post) => {
  return user.id === post.authorId
})

多くのアビリティがある場合、shared/utils/abilities/ディレクトリを作成し、各アビリティのファイルを作成することをお勧めします。shared/utilsディレクトリにアビリティを配置すると、クライアントでオートインポートが機能し、サーバーでは~~/shared/utils/abilitiesとしてシンプルにインポートできます。共有フォルダーはディレクトリの最初のレベルのみをエクスポートすることに注意してください。したがって、shared/utils/abilities/index.tsファイルでアビリティをエクスポートする必要があります。

デフォルトでは、ゲストはどのアクションも実行できず、アビリティは呼び出されません。この動作はアビリティごとに変更できます。

export const listPosts = defineAbility({ allowGuest: true }, (user: User | null) => true)

これで、認証されていないユーザーも投稿を一覧表示できるようになります。

アビリティの使用

アビリティを使用するには、3つのバウンサー関数(allowsdeniesauthorize)にアクセスできます。これらはクライアントとサーバーの両方で利用可能です。実装は異なりますが、APIは(ほぼ)同じであり、開発者には完全に透過的です。サーバーでは、最初のパラメータはハンドラーからのeventです。

allows関数は、ユーザーがアクションを実行できる場合にブール値を返します。

if (await allows(listPosts)) {
  // User can list posts
}

サーバーの場合

if (await allows(event, listPosts)) {
  // User can list posts
}

denies関数は、ユーザーがアクションを実行できない場合にブール値を返します。

if (await denies(editPost, post)) {
  // User cannot edit the post
}

サーバーの場合

if (await denies(event, editPost, post)) {
  // User cannot edit the post
}

authorize関数は、ユーザーがアクションを実行できない場合にエラーをスローします。

await authorize(editPost, post)

// User can edit the post

サーバーの場合

await authorize(event, editPost, post)

アビリティの戻り値ごとにエラーメッセージとステータスコードをカスタマイズできます。これは、ユーザーにリソースの存在を意識させないために、403の代わりに404を返す場合に役立ちます。

export const editPost = defineAbility((user: User, post: Post) => {
  if(user.id === post.authorId) {
    return true // or allow()
  }

  return deny('This post does not exist', 404)
})

allowdenyは、truefalseを返すのと似ていますが、denyはエラーに対してカスタムメッセージとステータスコードを返すことができます。

ほとんどの場合、APIエンドポイントはauthorizeを使用します。これは、パラメーターが不要な場合はエンドポイントの最初の行になるか、データベースクエリの後でユーザーがリソースにアクセスできるかどうかを確認するために使用されます。エラーはH3Errorであるため、Nitroサーバーによってキャッチされるため、エラーをキャッチする必要はありません。

allowsおよびdenies関数は、条件付きレンダリングやロジックを実行するためにクライアントで役立ちます。これらを使用して、認可ロジックを細かく制御することもできます。

コンポーネントの使用

このモジュールは、UIの一部を条件付きで表示するのに役立つ2つのコンポーネントを提供します。投稿を編集するためのボタンがあるとします。未承認のユーザーはそのボタンを見るべきではありません。

<template>
  <Can
    :ability="editPost"
    :args="[post]" // Optional if the ability does not take any arguments
  >
    <button>Edit</button>
  </Can>
</template>

Canコンポーネントは、ユーザーが投稿を編集できる場合にのみボタンをレンダリングします。ユーザーが投稿を編集できない場合、ボタンはレンダリングされません。

反対に、Cannotコンポーネントを使用すると、ユーザーが投稿を編集できない場合にのみボタンをレンダリングできます。

<template>
  <Cannot
    :ability="editPost"
    :args="[post]" // Optional if the ability does not take any arguments
  >
    <p>You're not allowed to edit the post.</p>
  </Cannot>
</template>

Bouncerコンポーネントは、単一のコンポーネント内で「できる」と「できない」の両方のシナリオを処理するための、より柔軟で集中化された方法を提供します。CanCannotコンポーネントを別々に使用する代わりに、Bouncerコンポーネントとその名前付きスロットを活用して、両方の状態を統一されたブロックで処理できます。

<Bouncer
  :ability="editPost"
  :args="[post]" // Optional if the ability does not take any arguments
>
  <template #can>
    <button>Edit</button>
  </template>

  <template #cannot>
    <p>You're not allowed to edit the post.</p>
  </template>
</Bouncer>

これらのコンポーネントはすべて、レンダリングするHTMLタグを定義するためのasという名前のpropを受け入れます。デフォルトでは、レンダリングなしコンポーネントです。

<Can
  :ability="editPost"
  :args="[post]"
  as="div"
>
  <button>Edit</button>
</Can>

これはレンダリングされます

<div>
  <button>Edit</button>
</div>

代わりに

<button>Edit</button>

複数のアビリティ

複数のアビリティを持っている場合、アビリティの配列をコンポーネントに提供できます。コンポーネントは、すべてのアビリティがコンポーネントの指定された要件に一致する場合にのみレンダリングされます。

<Can :ability="[editPost, deletePost]" :args="[[post], [post]]" />

<Cannot :ability="[editPost, deletePost]" :args="[[post], [post]]" />

<Bouncer :ability="[editPost, deletePost]" :args="[[post], [post]]">
  <template #can>
    <button>Edit</button>
    <button>Delete</button>
  </template>

  <template #cannot>
    <p>You're not allowed to edit or delete the post.</p>
  </template>
</Bouncer>

貢献

ローカル開発
# Install dependencies
npm install

# Generate type stubs
npm run dev:prepare

# Develop with the playground
npm run dev

# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release new version
npm run release

クレジット

このモジュールは、コードとデザインの両方において、Adonis Bouncerに強くインスパイアされています。それはよく書かれたパッケージであり、毎回車輪の再発明をする必要はないと思います。

ライセンス

MITライセンス