Files
2026-03-23 21:43:40 +00:00

238 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
}
}
// 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
async function login(email: string, password: string): Promise<boolean> {
isLoading.value = true
error.value = null
try {
// DEVELOPMENT MODE BYPASS: If email is admin@servicefinder.com or we're in dev mode
// Use the mock JWT token to bypass backend authentication
const isDevMode = typeof import.meta !== 'undefined' && (import.meta.env.DEV || import.meta.env.MODE === 'development')
const isAdminEmail = email === 'admin@servicefinder.com' || email === 'superadmin@servicefinder.com'
if (isDevMode && isAdminEmail) {
console.log('[DEV MODE] Using mock authentication bypass for:', email)
// Use the exact mock JWT string provided in the task
const mockJwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdXBlcmFkbWluQHNlcnZpY2VmaW5kZXIuY29tIiwicm9sZSI6InN1cGVyYWRtaW4iLCJyYW5rIjoxMDAsInNjb3BlX2xldmVsIjoiZ2xvYmFsIiwiZXhwIjozMDAwMDAwMDAwLCJpYXQiOjE3MDAwMDAwMDB9.dummy_signature'
// Store token safely (SSR-safe)
if (typeof window !== 'undefined') {
localStorage.setItem('admin_token', mockJwtToken)
}
token.value = mockJwtToken
parseToken()
return true
}
// Otherwise, call real backend login endpoint
const response = await fetch('http://localhost:8000/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (!response.ok) {
throw new Error('Login failed')
}
const data = await response.json()
token.value = data.access_token
if (typeof window !== 'undefined') {
localStorage.setItem('admin_token', token.value)
}
parseToken()
return true
} catch (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
}
})