239 lines
6.6 KiB
TypeScript
239 lines
6.6 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
import { jwtDecode } from 'jwt-decode'
|
|
|
|
export interface JwtPayload {
|
|
sub: string
|
|
role: string
|
|
rank: number
|
|
scope_level: string
|
|
region_code?: string
|
|
scope_id?: number
|
|
exp: number
|
|
iat: number
|
|
}
|
|
|
|
export interface User {
|
|
id: string
|
|
email: string
|
|
role: string
|
|
rank: number
|
|
scope_level: string
|
|
region_code?: string
|
|
scope_id?: number
|
|
permissions: string[]
|
|
}
|
|
|
|
export const useAuthStore = defineStore('auth', () => {
|
|
// State
|
|
const token = ref<string | null>(null)
|
|
const user = ref<User | null>(null)
|
|
const isAuthenticated = computed(() => !!token.value && !isTokenExpired())
|
|
const isLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
|
|
// Initialize token from localStorage only on client side
|
|
if (typeof window !== 'undefined') {
|
|
token.value = localStorage.getItem('admin_token')
|
|
}
|
|
|
|
// Getters
|
|
const getUserRole = computed(() => user.value?.role || '')
|
|
const getUserRank = computed(() => user.value?.rank || 0)
|
|
const getScopeLevel = computed(() => user.value?.scope_level || '')
|
|
const getRegionCode = computed(() => user.value?.region_code || '')
|
|
const getScopeId = computed(() => user.value?.scope_id || 0)
|
|
const getPermissions = computed(() => user.value?.permissions || [])
|
|
|
|
// Check if token is expired
|
|
function isTokenExpired(): boolean {
|
|
if (!token.value) return true
|
|
try {
|
|
const decoded = jwtDecode<JwtPayload>(token.value)
|
|
return Date.now() >= decoded.exp * 1000
|
|
} catch {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Parse token and set user
|
|
function parseToken(): void {
|
|
if (!token.value) {
|
|
user.value = null
|
|
return
|
|
}
|
|
|
|
try {
|
|
const decoded = jwtDecode<JwtPayload>(token.value)
|
|
|
|
// Map JWT claims to user object
|
|
user.value = {
|
|
id: decoded.sub,
|
|
email: decoded.sub, // Assuming sub is email
|
|
role: decoded.role,
|
|
rank: decoded.rank,
|
|
scope_level: decoded.scope_level,
|
|
region_code: decoded.region_code,
|
|
scope_id: decoded.scope_id,
|
|
permissions: generatePermissions(decoded.role, decoded.rank)
|
|
}
|
|
|
|
error.value = null
|
|
} catch (err) {
|
|
console.error('Failed to parse token:', err)
|
|
error.value = 'Invalid token format'
|
|
user.value = null
|
|
// Clear invalid token from storage
|
|
token.value = null
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('admin_token')
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate permissions based on role and rank
|
|
function generatePermissions(role: string, rank: number): string[] {
|
|
const permissions: string[] = []
|
|
|
|
// Base permissions based on role
|
|
switch (role) {
|
|
case 'superadmin':
|
|
permissions.push('*')
|
|
break
|
|
case 'admin':
|
|
permissions.push('view:dashboard', 'manage:users', 'manage:services', 'view:finance')
|
|
if (rank >= 5) permissions.push('manage:settings')
|
|
break
|
|
case 'moderator':
|
|
permissions.push('view:dashboard', 'moderate:services', 'view:users')
|
|
break
|
|
case 'salesperson':
|
|
permissions.push('view:dashboard', 'view:sales', 'manage:leads')
|
|
break
|
|
}
|
|
|
|
// Add geographical scope permissions
|
|
permissions.push(`scope:${role}`)
|
|
|
|
return permissions
|
|
}
|
|
|
|
// Check if user has permission
|
|
function hasPermission(permission: string): boolean {
|
|
if (!user.value) return false
|
|
if (user.value.permissions.includes('*')) return true
|
|
return user.value.permissions.includes(permission)
|
|
}
|
|
|
|
// Check if user has required role rank
|
|
function hasRank(minRank: number): boolean {
|
|
return user.value?.rank >= minRank
|
|
}
|
|
|
|
// Check if user can access scope
|
|
function canAccessScope(requestedScopeId: number, requestedRegionCode?: string): boolean {
|
|
if (!user.value) return false
|
|
|
|
// Superadmin can access everything
|
|
if (user.value.role === 'superadmin') return true
|
|
|
|
// Check scope_id match
|
|
if (user.value.scope_id && user.value.scope_id === requestedScopeId) return true
|
|
|
|
// Check region_code match
|
|
if (user.value.region_code && requestedRegionCode) {
|
|
return user.value.region_code === requestedRegionCode
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Login action - REAL API AUTHENTICATION ONLY
|
|
async function login(email: string, password: string): Promise<boolean> {
|
|
isLoading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
// Debug: Log what we're sending
|
|
console.log('Auth store: Attempting login for', email)
|
|
console.log('Auth store: Password length', password.length)
|
|
|
|
// Prepare URL-encoded form data for OAuth2 password grant (as per FastAPI auth endpoint)
|
|
// FastAPI's OAuth2PasswordRequestForm expects application/x-www-form-urlencoded
|
|
// Use explicit string encoding to guarantee FastAPI accepts it (Nuxt's $fetch messes up URLSearchParams)
|
|
const bodyString = `username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`;
|
|
|
|
console.log('Auth store: Body string created', bodyString)
|
|
|
|
// Call real backend login endpoint using $fetch (Nuxt's fetch)
|
|
// $fetch automatically throws on non-2xx responses, so we just need to catch
|
|
const data = await $fetch('/api/v1/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: bodyString
|
|
})
|
|
|
|
console.log('Auth login API response:', data)
|
|
|
|
// Extract token
|
|
const accessToken = data.access_token
|
|
if (!accessToken) {
|
|
throw new Error('No access token in response')
|
|
}
|
|
|
|
// Store token safely (SSR-safe)
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.setItem('admin_token', accessToken)
|
|
}
|
|
token.value = accessToken
|
|
parseToken()
|
|
|
|
return true
|
|
} catch (err) {
|
|
console.error('Auth store: Login catch block error:', err)
|
|
error.value = err instanceof Error ? err.message : 'Login failed'
|
|
return false
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
// Logout action
|
|
function logout(): void {
|
|
token.value = null
|
|
user.value = null
|
|
if (typeof window !== 'undefined') {
|
|
localStorage.removeItem('admin_token')
|
|
}
|
|
}
|
|
|
|
// Initialize store
|
|
if (token.value) {
|
|
parseToken()
|
|
}
|
|
|
|
return {
|
|
// State
|
|
token,
|
|
user,
|
|
isAuthenticated,
|
|
isLoading,
|
|
error,
|
|
|
|
// Getters
|
|
getUserRole,
|
|
getUserRank,
|
|
getScopeLevel,
|
|
getRegionCode,
|
|
getScopeId,
|
|
getPermissions,
|
|
|
|
// Actions
|
|
login,
|
|
logout,
|
|
hasPermission,
|
|
hasRank,
|
|
canAccessScope,
|
|
parseToken
|
|
}
|
|
}) |