Refactor user store to use apiClient instead of direct fetch

- Replace manual fetch calls with apiClient.get() and apiClient.patch()
- Remove manual token handling (apiClient handles this automatically)
- Add typed UserProfileResponse interface for API response
- Improve error handling with ApiError type checking
- Gains automatic token refresh on 401 with retry logic

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-31 00:36:42 -06:00
parent ca3aca2b38
commit 6bfc928169

View File

@ -7,7 +7,8 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { config } from '@/config'
import { apiClient } from '@/api/client'
import { ApiError } from '@/api/types'
import { useAuthStore } from './auth'
export interface LinkedAccount {
@ -38,14 +39,28 @@ export const useUserStore = defineStore('user', () => {
const hasStarterDeck = computed(() => profile.value?.hasStarterDeck ?? false)
const linkedAccounts = computed(() => profile.value?.linkedAccounts ?? [])
/** API response type for user profile */
interface UserProfileResponse {
id: string
display_name: string
avatar_url: string | null
has_starter_deck: boolean
created_at: string
linked_accounts?: Array<{
provider: string
provider_user_id: string
email: string | null
linked_at: string
}>
}
/**
* Fetch the current user's profile from the API.
*/
async function fetchProfile(): Promise<boolean> {
const auth = useAuthStore()
const token = await auth.getValidToken()
if (!token) {
if (!auth.isAuthenticated) {
error.value = 'Not authenticated'
return false
}
@ -54,25 +69,16 @@ export const useUserStore = defineStore('user', () => {
error.value = null
try {
const response = await fetch(`${config.apiBaseUrl}/api/users/me`, {
headers: {
'Authorization': `Bearer ${token}`,
},
})
const data = await apiClient.get<UserProfileResponse>('/api/users/me')
if (!response.ok) {
throw new Error('Failed to fetch profile')
}
const data = await response.json()
profile.value = {
id: data.id,
displayName: data.display_name,
avatarUrl: data.avatar_url,
hasStarterDeck: data.has_starter_deck,
createdAt: data.created_at,
linkedAccounts: data.linked_accounts?.map((acc: Record<string, unknown>) => ({
provider: acc.provider,
linkedAccounts: data.linked_accounts?.map((acc) => ({
provider: acc.provider as 'google' | 'discord',
providerUserId: acc.provider_user_id,
email: acc.email,
linkedAt: acc.linked_at,
@ -89,7 +95,11 @@ export const useUserStore = defineStore('user', () => {
return true
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to fetch profile'
if (e instanceof ApiError) {
error.value = e.detail || e.message
} else {
error.value = e instanceof Error ? e.message : 'Failed to fetch profile'
}
return false
} finally {
isLoading.value = false
@ -101,9 +111,8 @@ export const useUserStore = defineStore('user', () => {
*/
async function updateDisplayName(newName: string): Promise<boolean> {
const auth = useAuthStore()
const token = await auth.getValidToken()
if (!token) {
if (!auth.isAuthenticated) {
error.value = 'Not authenticated'
return false
}
@ -112,24 +121,17 @@ export const useUserStore = defineStore('user', () => {
error.value = null
try {
const response = await fetch(`${config.apiBaseUrl}/api/users/me`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ display_name: newName }),
})
if (!response.ok) {
throw new Error('Failed to update profile')
}
await apiClient.patch('/api/users/me', { display_name: newName })
// Refetch to get updated data
await fetchProfile()
return true
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to update profile'
if (e instanceof ApiError) {
error.value = e.detail || e.message
} else {
error.value = e instanceof Error ? e.message : 'Failed to update profile'
}
return false
} finally {
isLoading.value = false