Files
service-finder/frontend_admin/stores/auth.ts
2026-06-24 11:29:45 +00:00

180 lines
4.8 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface UserProfile {
id: number
email: string
first_name: string | null
last_name: string | null
is_active: boolean
role: string
region_code: string
subscription_plan: string
subscription_expires_at?: string | null
max_vehicles?: number
max_garages?: number
scope_level: string
scope_id: string | null
ui_mode: string
active_organization_id: number | null
preferred_language: string
person_id: number | null
person: Record<string, any> | null
system_capabilities?: Record<string, boolean>
org_capabilities?: Record<string, Record<string, boolean>>
}
export const useAuthStore = defineStore('auth', () => {
// ── State ──────────────────────────────────────────────────────────
const user = ref<UserProfile | null>(null)
const token = ref<string | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
const isInitialized = ref(false)
// ── Getters ────────────────────────────────────────────────────────
const isAuthenticated = computed(() => !!token.value && !!user.value)
const isAdmin = computed(() => {
const role = user.value?.role
if (!role) return false
const staffRoles = ['SUPERADMIN', 'ADMIN', 'MODERATOR', 'SALES_REP', 'SERVICE_MGR']
return staffRoles.includes(role.toUpperCase())
})
const userName = computed(() => {
if (!user.value) return ''
const { first_name, last_name } = user.value
if (first_name && last_name) return `${first_name} ${last_name}`
return first_name || last_name || user.value.email
})
const userEmail = computed(() => user.value?.email ?? '')
const userRole = computed(() => user.value?.role ?? 'guest')
// ── Actions ────────────────────────────────────────────────────────
/**
* Login with email + password.
* Calls the FastAPI /auth/login endpoint using OAuth2PasswordRequestForm.
* Stores the JWT token in a cookie (via useCookie) for SSR compatibility.
*/
async function login(email: string, password: string) {
isLoading.value = true
error.value = null
try {
const body = new URLSearchParams()
body.append('username', email)
body.append('password', password)
const res = await $fetch<{
access_token: string
refresh_token?: string
token_type: string
is_active: boolean
}>('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
})
// Persist token in a cookie (SSR-safe) and in memory
const tokenCookie = useCookie('access_token', {
maxAge: 60 * 60 * 24 * 7, // 7 days
path: '/',
sameSite: 'lax',
secure: true,
})
tokenCookie.value = res.access_token
token.value = res.access_token
// Fetch user profile
await fetchUser()
} catch (err: any) {
error.value = err?.data?.detail || err?.message || 'Login failed'
throw err
} finally {
isLoading.value = false
}
}
/**
* Fetch the current user profile from /auth/me.
*/
async function fetchUser() {
const tokenCookie = useCookie('access_token')
const currentToken = tokenCookie.value || token.value
if (!currentToken) return
try {
const res = await $fetch<UserProfile>('/api/v1/auth/me', {
headers: { Authorization: `Bearer ${currentToken}` },
})
user.value = res
} catch (err: any) {
if (err?.response?.status === 401) {
logout()
}
throw err
}
}
/**
* Logout — clear token, user state, and cookie.
*/
function logout() {
token.value = null
user.value = null
error.value = null
const tokenCookie = useCookie('access_token')
tokenCookie.value = null
}
/**
* Initialize the store on app load — if a token cookie exists, fetch the user.
*/
async function init() {
const tokenCookie = useCookie('access_token')
if (tokenCookie.value) {
token.value = tokenCookie.value
try {
await fetchUser()
} catch {
logout()
}
}
isInitialized.value = true
}
/**
* Clear any stored error message.
*/
function clearError() {
error.value = null
}
return {
// State
user,
token,
isLoading,
error,
isInitialized,
// Getters
isAuthenticated,
isAdmin,
userName,
userEmail,
userRole,
// Actions
login,
fetchUser,
logout,
init,
clearError,
}
})