🐕

Nuxt.jsでWebアプリケーション開発メモ4

に公開

概要

ログイン後、TOPページに遷移するとヘッダの右上に人型アイコンと歯車アイコンを表示する。
それぞれクリックするとポップアップメニューを表示。
歯車アイコンは管理者のみ表示。

  • 人型アイコンのポップアップメニュー
    • パスワード変更
    • ログアウト
  • 歯車アイコンのポップアップメニュー
    • ユーザー
    • グループ

プロンプト4

# 命令4
- ログインしたらヘッダの右端にユーザーメニューを表示するための人型アイコンと、管理メニューを表示するための歯車アイコンを表示してください。
    - 並びは右から人型アイコン、歯車アイコンです。
    - ログインしたユーザーが管理者権限を持っていなければ歯車アイコンは非表示にしてください。
    - ログインしていない場合はいずれのアイコンも非表示になるようにしてください。
    - ログイン時と非ログイン時のヘッダのvueファイルは同じでも同じでなくてもどちらでもよく、Nuxtでよく使われる方式を採用してください。
    (どちらの場合でもapp/layoutsフォルダ配下に作るようにしてください。)
- 人型アイコンをクリックすると、ポップアップでメニューを表示します。
    - メニュー内容は上から```パスワード変更``` 、```ログアウト``` とします。
    - パスワード変更 メニュークリック時:まだ実装不要。
    - ログアウト メニュークリック時:ログアウトAPIを実行し、ログイン画面に戻ります。
        - ログアウトAPIの相対URLは、```api/auth/logout``` です。
- 歯車アイコンをクリックするとポップアップでメニューを表示します。
    - メニュー内容は上から```ユーザー``` 、```グループ```  とします。
    - いずれのメニューもクリック時の処理はまだ実装不要です。

画面

備考

一発で実装は完了せず。
ログイン後画面更新しないとアイコンがでなかったり、バックエンドで管理者情報を渡していなかったので管理者でログインしても歯車アイコンでなかったり。
何度かの改善指示でいったん完成。

ソースコード差分

https://github.com/tkure/prototype-frontend/commit/68160e2ce6d947bbb0e3cd19df3841fec771616c

実装内容をClaude Codeに説明させてみた

以下、ClaudeCodeに今回の実装内容について説明させてみた。

それではClaude Codeの説明をお楽しみください。


概要

ログイン後にヘッダー右端にユーザーメニュー(人型アイコン)と管理メニュー(歯車アイコン)を表示する機能を実装しました。
認証状態の管理からアイコンの表示制御まで、一連の処理を解説します。

アーキテクチャ

┌─────────────────┐ ┌─────────────────┐
┌─────────────────┐
│ login.vue │───▶│ useAuth() │───▶│ default.vue

│(ログイン処理) │ │(認証状態管理) │
│(アイコン表示) │
└─────────────────┘ └─────────────────┘
└─────────────────┘

  1. 認証状態管理(composables/useAuth.ts)

基本構造

app/composables/useAuth.ts
export const useAuth = () => {
  const isLoggedIn = ref(false)
  const user = ref<any>(null)
  const isAdmin = ref(false)

  const checkAuthStatus = async () => {
    if (import.meta.client) {
      const token = localStorage.getItem('access_token')
      isLoggedIn.value = !!token
      
      if (token) {
        try {
          // APIからユーザー情報を取得
          const response = await $fetch('http://localhost:8000/api/auth/user/', {
            headers: { 'Authorization': `Bearer ${token}` }
          }) as any
          user.value = response.user
          isAdmin.value = response.user?.is_staff || response.user?.is_superuser || false
        } catch (error) {
          console.error('User info fetch error:', error)
          // APIエラーの場合はログアウト
          logout()
        }
      } else {
        user.value = null
        isAdmin.value = false
      }
    }
  }
  return {
    isLoggedIn: readonly(isLoggedIn),
    user: readonly(user),
    isAdmin: readonly(isAdmin),
    checkAuthStatus,
    updateAuthState,
    logout
  }

ポイント

  • リアクティブな状態管理: ref() でログイン状態と管理者権限を管理
  • API連携: JWTトークンを使ってユーザー情報を取得
  • 型安全性: any 型でAPI仕様変更に柔軟対応
  1. ログイン処理(pages/login.vue)
app/pages/login.vue
const { updateAuthState } = useAuth()

const handleLogin = async () => {
  try {
    const response = await $fetch('http://localhost:8000/api/auth/login/', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: {
        username: username.value,
        password: password.value,
      }
    })
    if (response.access) {
      if (response.refresh) {
        localStorage.setItem('refresh_token', response.refresh)      
      }

      // 認証状態を即座に更新
      await updateAuthState(response.access)
      await navigateTo('/top')
    }
    } catch (error: any) {
      // エラーハンドリング
    }
  }

重要な処理

app/composables/useAuth.ts
  const updateAuthState = async (token: string) => {
    localStorage.setItem('access_token', token)
    await checkAuthStatus()
  }

ログイン成功後に updateAuthState() を呼び出すことで、画面遷移前に認証状態を更新します。

  1. レイアウトでのアイコン表示(layouts/default.vue)

テンプレート部分

app/layouts/default.vue
  <template>
    <div class="bg-green-500 text-white px-6 py-4">
      <div class="flex items-center justify-between">
        <h1 class="text-lg font-medium">Prototype SPA</h1>

        <!-- Header Icons (右側) -->
        <div v-if="isLoggedIn" class="flex items-centerspace-x-4">
          <!-- 管理メニュー(歯車アイコン) -->
          <div v-if="isAdmin" class="relative">
            <button @click.stop="toggleAdminMenu">
              <UIcon name="i-heroicons-cog-6-tooth" class="w-5 h-5" />
            </button>
            <!-- ポップアップメニュー -->
          </div>

          <!-- ユーザーメニュー(人型アイコン) -->
          <div class="relative">
            <button @click.stop="toggleUserMenu">
              <UIcon name="i-heroicons-user" class="w-5 h-5" />        
            </button>
            <!-- ポップアップメニュー -->
          </div>
        </div>
      </div>
    </div>
  </template>

スクリプト部分

app/layouts/default.vue
<script setup lang="ts">
  const { isLoggedIn, isAdmin, logout, checkAuthStatus } = useAuth()
  const route = useRoute()

  // ページ遷移時と初期表示時に認証状態をチェック
  onMounted(async () => {
    await checkAuthStatus()
  })

  watch(() => route.path, async () => {
    await checkAuthStatus()
  })
  </script>
  1. 表示制御の仕組み

条件分岐による表示制御

app/layouts/default.vue
  <!-- ログイン時のみ表示 -->
  <div v-if="isLoggedIn" class="flex items-center space-x-4">
    <!-- 管理者のみ表示 -->
    <div v-if="isAdmin" class="relative">
      <!-- 歯車アイコン -->
    </div>

    <!-- ログイン済みユーザー全員に表示 -->
    <div class="relative">
      <!-- 人型アイコン -->
    </div>
  </div>

セキュリティ考慮事項

  • フロントエンド: UI表示制御のみ(UX向上目的)
  • バックエンド: 実際の権限チェック(permission_classes = [IsAdminUser])
  1. 処理フロー
    以下、コードブロックでmermaidを指定してsequenceDiagramを書いています。
  1. ハマりポイントと解決策

localStorage変更の検知問題

  • 問題: watch() でlocalStorageの変更を検知できない
// ❌ これは動かない
watch(() => localStorage.getItem('access_token'), () => {
  checkAuthStatus()
})
  • 解決: 手動での状態更新
// ✅ ログイン時に明示的に呼び出し
await updateAuthState(response.access)

ページ遷移時の状態同期

  • 問題: ログイン後の画面遷移でアイコンが表示されない
  • 解決: レイアウトでの認証状態チェック
  onMounted(async () => {
    await checkAuthStatus()
  })

  watch(() => route.path, async () => {
    await checkAuthStatus()
  })

まとめ

この実装により、ログイン後に自動でヘッダーアイコンが表示され、管理者権限に応じて適切なメニューが表示されます。composableを使った
状態管理により、複数のコンポーネント間で認証状態を効率的に共有できています。


→ Nuxt.jsでWebアプリケーション開発メモ5

Discussion