Azure Static Web Apps用のNuxtモジュールを公開しました

Nuxt 3では、[Azure Static Web Apps(以下Azure SWA)にデプロイするための設定](https://nuxt.com/deploy/azure)が用意されています。
しかしAzure SWAで提供されている機能を使おうとすると、そのままではいろいろと不都合な状態にあります。

こうした状態を解消するためのNuxtモジュールである、`nuxt-swa`ライブラリを公開しました。(記事執筆時点でv0.5.0)

https://www.npmjs.com/package/nuxt-swa
- [GitHubのリポジトリ](https://github.com/ddradar/nuxt-swa)
  - IssueやPull Requestは随時受け付けています(日本語OK) 
- [公式ドキュメント(兼Playground)](https://proud-plant-0fe90bc00.4.azurestaticapps.net/)

## 何が困るのか？

- 認証機能(`/.auth/...`)などをサーバーサイドから呼べない
- ルート規則がPage Routing時に機能しない

### 認証機能(`/.auth/...`)などをサーバーサイドから呼べない

Azure SWAでは、[組み込みの認証機能を利用するAPIエンドポイント](https://learn.microsoft.com/ja-jp/azure/static-web-apps/authentication-authorization)が以下のように提供されています。

- `/.auth/login/{認証プロバイダ名}`: ログイン
- `/.auth/me`: ログイン中のユーザー情報(Client Principal)を取得
- `/.auth/logout`: ログアウト

しかしながら、NuxtはSSR時に、内部API(`/`始まり)に対するfetchリクエストを**全て内部実装の直接呼び出しに変換します。**(Direct call)   
そのため、以下のようなコードはサーバーサイド側で`[Vue Router warn]: No match found for location with path "..."`といったエラーを発生させます。
```vue
<template>
  <button @click="navigateTo('/.auth/logout')">ログアウト</button>
</template>

<script setup lang="ts">
const { clientPrincipal } = await $fetch('/.auth/me')
</script>
```

また、現在パブリックプレビューである[Data API Builder](https://learn.microsoft.com/ja-jp/azure/static-web-apps/database-overview)のエンドポイント(`/data-api/`配下)についても同様です。

### ルート規則がPage Routing時に機能しない

Azure SWAは、[特定のルートへのアクセスを認証済みユーザーのみに制限する機能](https://learn.microsoft.com/ja-jp/azure/static-web-apps/configuration)も持っています。
```jsonc:staticwebapp.config.json
{
  "routes": [
    {
      // /profile 以下はログインユーザーのみ閲覧可能
      "route": "/profile*",
      "allowedRoles": ["authenticated"]
    },
    {
      // /admin/index.html はadministratorロールを持つユーザーのみ閲覧可能
      "route": "/admin/index.html",
      "allowedRoles": ["administrator"]
    },
    {
      // /api/ 以下のPUT, POST, PATCH, DELETEリクエストはadministratorロールを持つユーザーのみ
      "route": "/api/*",
      "methods": ["PUT", "POST", "PATCH", "DELETE"],
      "allowedRoles": ["administrator"]
    }
  ]
}
```

しかし、これが機能するのは**Azure SWAに対して直接リクエストを発行した場合のみ**になります。  
そのためクライアント側でルーティングを実施した場合(例: Vue Router)や、前述のようなAPIのDirect callが発生した場合は、**認証がスキップされてしまいます。**

## `nuxt-swa`が提供する機能

下記は一例ですので、ほかの機能については[ドキュメント](https://proud-plant-0fe90bc00.4.azurestaticapps.net/)をご覧ください。

### SWA組み込みAPIに対するプロキシ

`/.auth/`以下および、`/data-api/`以下のルートにリクエストがあった場合、ドメインを付与したフルパスにプロキシするイベントハンドラーを組み込んでいます。

> 「Azure Functions側で付与される`x-ms-original-url`を読み取ることでドメインを判別する」仕組みなので、サーバー側API(/serverフォルダ配下)から`$fetch`を呼び出す際はリクエストヘッダーを明示的にバイパスする必要がある点に留意してください。

### Global Page Middleware

Pageコンポーネントで以下のように設定することで、モジュール内部で指定したロールを持っていない場合はレンダリング前に401/403エラーを返します。

```vue
<script setup lang="ts">
definePageMeta({ allowedRoles: 'authenticated' })
</script>

<template>
<div>Private Page!</div>
</template>
```

> 既存の`staticwebapp.config.json`との併用も可能ですが、初期リクエスト時(Azure SWAのエラーページ)とページルーティング時(Nuxtのエラーページ)で表示されるエラーページが変わってしまうため、上記設定に寄せることをオススメします。

### 組み込み認証を利用するためのComposable関数

```ts
const {
  clientPrincipal, // 認証情報
  isLoggedIn, // ログインしているかどうか
  hasRole,  // ロールを持つかどうかを返すcomputed<boolean>を生成する関数
  login, // navigateTo(`/.auth/login/{provider}`)
  logout, // navigateTo(`/.auth/logout`)
} = await useEasyAuth()
```

### サーバー側でユーザー情報を取得するユーティリティ関数の提供

```ts
export default defineEventHandler(event => {
  // リクエストしたユーザーの認証情報を取得
  const clientPrincipal = getClientPrincipal(event)
})

export default defineEventHandler(event => {
  // administratorロールを持つか
  if (!hasRole(event, 'administrator')) {
    throw createError({ statusCode: 403 })
  }
})
```

### `nitro.azure.config`に対する型定義の提供

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278493/1797ce48-53e6-4df5-fed8-2c90a74ab8ef.png)

上記設定がない場合は、モジュール側でも警告します。

## 今後の展望(ToDo)

- [Nuxt Modules](https://nuxt.com/modules)への登録
- [`async`/`await`を使うとNuxtコンテキストを失ってしまうことがある](https://nuxt.com/docs/api/composables/use-nuxt-app#runwithcontext)らしいので、構造を再検討する
- 型定義がうまく反映されない(特に/server側)ので、適切に設定する
- インテグレーションテストの整備
  - Azure SWA環境でないと厳しいかも
- ビルド後に`staticwebapp.config.json`を`/.output/public`に移動させるオプションを用意
  - [`Azure/static-web-apps-deploy`アクションのビルドをスキップする設定](https://learn.microsoft.com/ja-jp/azure/static-web-apps/build-configuration?tabs=github-actions#skip-building-front-end-app)用