admin firs step
This commit is contained in:
238
frontend/admin/stores/auth.ts
Normal file
238
frontend/admin/stores/auth.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
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
|
||||
}
|
||||
})
|
||||
204
frontend/admin/stores/tiles.ts
Normal file
204
frontend/admin/stores/tiles.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAuthStore } from './auth'
|
||||
import { useRBAC, type TilePermission } from '~/composables/useRBAC'
|
||||
|
||||
export interface UserTilePreference {
|
||||
tileId: string
|
||||
visible: boolean
|
||||
position: number
|
||||
size: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
export const useTileStore = defineStore('tiles', () => {
|
||||
const authStore = useAuthStore()
|
||||
const rbac = useRBAC()
|
||||
|
||||
// State
|
||||
const userPreferences = ref<Record<string, UserTilePreference>>({})
|
||||
const isLoading = ref(false)
|
||||
|
||||
// Initialize from localStorage
|
||||
function loadPreferences() {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const userId = authStore.user?.id
|
||||
if (!userId) return
|
||||
|
||||
const stored = localStorage.getItem(`tile_preferences_${userId}`)
|
||||
if (stored) {
|
||||
try {
|
||||
userPreferences.value = JSON.parse(stored)
|
||||
} catch (err) {
|
||||
console.error('Failed to parse tile preferences:', err)
|
||||
userPreferences.value = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
function savePreferences() {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
const userId = authStore.user?.id
|
||||
if (!userId) return
|
||||
|
||||
localStorage.setItem(`tile_preferences_${userId}`, JSON.stringify(userPreferences.value))
|
||||
}
|
||||
|
||||
// Get default layout (sorted by tile ID for consistency)
|
||||
const defaultLayout = computed(() => {
|
||||
const filtered = rbac.getFilteredTiles()
|
||||
return filtered.map((tile, index) => ({
|
||||
tileId: tile.id,
|
||||
visible: true,
|
||||
position: index,
|
||||
size: 'medium' as const
|
||||
}))
|
||||
})
|
||||
|
||||
// Check if layout has been modified from default
|
||||
const isLayoutModified = computed(() => {
|
||||
const currentPrefs = Object.values(userPreferences.value)
|
||||
const defaultPrefs = defaultLayout.value
|
||||
|
||||
if (currentPrefs.length !== defaultPrefs.length) return true
|
||||
|
||||
// Check if any preference differs from default
|
||||
for (const defaultPref of defaultPrefs) {
|
||||
const currentPref = userPreferences.value[defaultPref.tileId]
|
||||
if (!currentPref) return true
|
||||
|
||||
if (currentPref.visible !== defaultPref.visible ||
|
||||
currentPref.position !== defaultPref.position ||
|
||||
currentPref.size !== defaultPref.size) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// Get user's accessible tiles with preferences
|
||||
const accessibleTiles = computed(() => {
|
||||
const filtered = rbac.getFilteredTiles()
|
||||
|
||||
return filtered.map(tile => {
|
||||
const pref = userPreferences.value[tile.id] || {
|
||||
tileId: tile.id,
|
||||
visible: true,
|
||||
position: 0,
|
||||
size: 'medium' as const
|
||||
}
|
||||
|
||||
return {
|
||||
...tile,
|
||||
preference: pref
|
||||
}
|
||||
}).sort((a, b) => a.preference.position - b.preference.position)
|
||||
})
|
||||
|
||||
// Get visible tiles only
|
||||
const visibleTiles = computed(() => {
|
||||
return accessibleTiles.value.filter(tile => tile.preference.visible)
|
||||
})
|
||||
|
||||
// Update tile preference
|
||||
function updateTilePreference(tileId: string, updates: Partial<UserTilePreference>) {
|
||||
const current = userPreferences.value[tileId] || {
|
||||
tileId,
|
||||
visible: true,
|
||||
position: Object.keys(userPreferences.value).length,
|
||||
size: 'medium'
|
||||
}
|
||||
|
||||
userPreferences.value[tileId] = {
|
||||
...current,
|
||||
...updates
|
||||
}
|
||||
|
||||
savePreferences()
|
||||
}
|
||||
|
||||
// Toggle tile visibility
|
||||
function toggleTileVisibility(tileId: string) {
|
||||
const current = userPreferences.value[tileId]
|
||||
updateTilePreference(tileId, {
|
||||
visible: !(current?.visible ?? true)
|
||||
})
|
||||
}
|
||||
|
||||
// Update tile positions (for drag and drop)
|
||||
function updateTilePositions(tileIds: string[]) {
|
||||
tileIds.forEach((tileId, index) => {
|
||||
updateTilePreference(tileId, { position: index })
|
||||
})
|
||||
}
|
||||
|
||||
// Reset to default preferences
|
||||
function resetPreferences() {
|
||||
const userId = authStore.user?.id
|
||||
if (userId) {
|
||||
localStorage.removeItem(`tile_preferences_${userId}`)
|
||||
}
|
||||
userPreferences.value = {}
|
||||
|
||||
// Reinitialize with default positions
|
||||
const tiles = rbac.getFilteredTiles()
|
||||
tiles.forEach((tile, index) => {
|
||||
userPreferences.value[tile.id] = {
|
||||
tileId: tile.id,
|
||||
visible: true,
|
||||
position: index,
|
||||
size: 'medium'
|
||||
}
|
||||
})
|
||||
|
||||
savePreferences()
|
||||
}
|
||||
|
||||
// Get tile size class for grid
|
||||
function getTileSizeClass(size: 'small' | 'medium' | 'large'): string {
|
||||
switch (size) {
|
||||
case 'small': return 'cols-12 sm-6 md-4 lg-3'
|
||||
case 'medium': return 'cols-12 sm-6 md-6 lg-4'
|
||||
case 'large': return 'cols-12 md-12 lg-8'
|
||||
default: return 'cols-12 sm-6 md-4 lg-3'
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when auth changes
|
||||
authStore.$subscribe(() => {
|
||||
if (authStore.isAuthenticated) {
|
||||
loadPreferences()
|
||||
} else {
|
||||
userPreferences.value = {}
|
||||
}
|
||||
})
|
||||
|
||||
// Initial load
|
||||
if (authStore.isAuthenticated) {
|
||||
loadPreferences()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
userPreferences,
|
||||
isLoading,
|
||||
|
||||
// Getters
|
||||
accessibleTiles,
|
||||
visibleTiles,
|
||||
defaultLayout,
|
||||
isLayoutModified,
|
||||
|
||||
// Actions
|
||||
updateTilePreference,
|
||||
toggleTileVisibility,
|
||||
updateTilePositions,
|
||||
resetPreferences,
|
||||
getTileSizeClass,
|
||||
loadPreferences,
|
||||
savePreferences
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user