@wishbone-media/spark 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1370 -1063
- package/package.json +1 -1
- package/src/plugins/axios.js +100 -0
- package/src/plugins/index.js +2 -1
- package/src/plugins/router.js +58 -0
- package/src/stores/auth.js +25 -5
- package/src/views/SparkError403View.vue +70 -0
- package/src/views/SparkError404View.vue +69 -0
- package/src/views/SparkErrorGeneralView.vue +83 -0
- package/src/views/index.js +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { useSparkAuthStore } from '../stores/auth.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a configured axios instance with auth interceptors
|
|
6
|
+
* @param {Object} config - Axios configuration options
|
|
7
|
+
* @param {string} config.baseURL - Base URL for API requests
|
|
8
|
+
* @param {number} config.timeout - Request timeout in milliseconds
|
|
9
|
+
* @param {Object} config.headers - Default headers
|
|
10
|
+
* @returns {AxiosInstance} Configured axios instance
|
|
11
|
+
*/
|
|
12
|
+
export function createAxiosInstance(config = {}) {
|
|
13
|
+
const instance = axios.create({
|
|
14
|
+
baseURL: config.baseURL || '',
|
|
15
|
+
timeout: config.timeout || 30000,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
...config.headers,
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Request interceptor - automatically add auth token from Pinia store
|
|
23
|
+
instance.interceptors.request.use(
|
|
24
|
+
(requestConfig) => {
|
|
25
|
+
try {
|
|
26
|
+
const authStore = useSparkAuthStore()
|
|
27
|
+
// Override token always wins if present
|
|
28
|
+
if (authStore.state.overrideToken) {
|
|
29
|
+
requestConfig.headers.Authorization = `Bearer ${authStore.state.overrideToken}`
|
|
30
|
+
} else if (authStore.state.token) {
|
|
31
|
+
// Otherwise use normal token from cookie
|
|
32
|
+
requestConfig.headers.Authorization = `Bearer ${authStore.state.token}`
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Auth store not initialized yet, skip adding token
|
|
36
|
+
// This can happen during app initialization
|
|
37
|
+
}
|
|
38
|
+
return requestConfig
|
|
39
|
+
},
|
|
40
|
+
(error) => {
|
|
41
|
+
return Promise.reject(error)
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Response interceptor - handle 401 and 403 errors
|
|
46
|
+
instance.interceptors.response.use(
|
|
47
|
+
(response) => response,
|
|
48
|
+
async (error) => {
|
|
49
|
+
// Auto-logout on 401 Unauthorized responses
|
|
50
|
+
if (error.response?.status === 401) {
|
|
51
|
+
try {
|
|
52
|
+
const authStore = useSparkAuthStore()
|
|
53
|
+
// Clear auth state
|
|
54
|
+
await authStore.logout()
|
|
55
|
+
// Redirect to login page
|
|
56
|
+
window.location.href = authStore.state.routes.auth
|
|
57
|
+
} catch (logoutError) {
|
|
58
|
+
// If logout fails, still redirect to login
|
|
59
|
+
console.error('Error during auto-logout:', logoutError)
|
|
60
|
+
window.location.href = '/login'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Redirect on 403 Forbidden responses
|
|
65
|
+
if (error.response?.status === 403) {
|
|
66
|
+
try {
|
|
67
|
+
const authStore = useSparkAuthStore()
|
|
68
|
+
// Redirect to forbidden page
|
|
69
|
+
window.location.href = authStore.state.routes.forbidden
|
|
70
|
+
} catch (redirectError) {
|
|
71
|
+
// If redirect fails, fallback to default forbidden route
|
|
72
|
+
console.error('Error during 403 redirect:', redirectError)
|
|
73
|
+
window.location.href = '/error/403'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return Promise.reject(error)
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return instance
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Setup axios for a Vue app with global availability
|
|
86
|
+
* @param {App} app - Vue app instance
|
|
87
|
+
* @param {Object} config - Axios configuration options
|
|
88
|
+
* @returns {AxiosInstance} Configured axios instance
|
|
89
|
+
*/
|
|
90
|
+
export function setupAxios(app, config = {}) {
|
|
91
|
+
const axiosInstance = createAxiosInstance(config)
|
|
92
|
+
|
|
93
|
+
// Make axios available via injection (Composition API)
|
|
94
|
+
app.provide('axios', axiosInstance)
|
|
95
|
+
|
|
96
|
+
// Also make available via global properties (Options API compatibility)
|
|
97
|
+
app.config.globalProperties.$axios = axiosInstance
|
|
98
|
+
|
|
99
|
+
return axiosInstance
|
|
100
|
+
}
|
package/src/plugins/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { Icons, addIcons, setupFontAwesome } from './fontawesome.js'
|
|
2
|
-
export { createAuthRoutes, setupAuthGuards } from './router.js'
|
|
2
|
+
export { createAuthRoutes, setupAuthGuards, create403Route, create404Route } from './router.js'
|
|
3
|
+
export { createAxiosInstance, setupAxios } from './axios.js'
|
package/src/plugins/router.js
CHANGED
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
SparkLogoutView,
|
|
5
5
|
SparkForgotPasswordView,
|
|
6
6
|
SparkResetPasswordView,
|
|
7
|
+
SparkError403View,
|
|
8
|
+
SparkError404View,
|
|
9
|
+
SparkErrorGeneralView,
|
|
7
10
|
} from '../views/index.js'
|
|
8
11
|
|
|
9
12
|
export function createAuthRoutes(options = {}) {
|
|
@@ -68,6 +71,12 @@ function processNavigation(to, next, authStore, defaultAuthenticatedRoute) {
|
|
|
68
71
|
const requiresAuth = to.meta.auth !== false
|
|
69
72
|
const isAuthenticated = authStore.check
|
|
70
73
|
|
|
74
|
+
// Block logout route if using override token
|
|
75
|
+
if (authStore.state.overrideToken && to.path === '/logout') {
|
|
76
|
+
next({ path: defaultAuthenticatedRoute })
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
71
80
|
// Handle unauthenticated users
|
|
72
81
|
if (!isAuthenticated) {
|
|
73
82
|
// Allow access to public routes (login, forgot-password, etc.)
|
|
@@ -120,3 +129,52 @@ function processNavigation(to, next, authStore, defaultAuthenticatedRoute) {
|
|
|
120
129
|
next()
|
|
121
130
|
}
|
|
122
131
|
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Creates error page route for 403 Forbidden
|
|
135
|
+
* NOTE: 404 catch-all route is separate - use create404Route() and add it LAST
|
|
136
|
+
* NOTE: General error page is available as SparkErrorGeneralView component for manual use
|
|
137
|
+
* @param {Object} options - Configuration options
|
|
138
|
+
* @param {string} options.forbiddenPath - Path for 403 forbidden page (default: '/error/403')
|
|
139
|
+
* @param {string} options.logo - Logo URL to display on error page
|
|
140
|
+
* @param {string} options.homeRoute - Default route for "Go Home" button (default: '/dashboard')
|
|
141
|
+
* @returns {Object} Route configuration for 403 error page
|
|
142
|
+
*/
|
|
143
|
+
export function create403Route(options = {}) {
|
|
144
|
+
const {
|
|
145
|
+
forbiddenPath = '/error/403',
|
|
146
|
+
logo = '',
|
|
147
|
+
homeRoute = '/dashboard',
|
|
148
|
+
} = options
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
path: forbiddenPath,
|
|
152
|
+
name: 'error-403',
|
|
153
|
+
component: SparkError403View,
|
|
154
|
+
props: { logo, homeRoute },
|
|
155
|
+
meta: { auth: false },
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Creates 404 catch-all route
|
|
161
|
+
* IMPORTANT: This MUST be added as the last route in your router configuration
|
|
162
|
+
* @param {Object} options - Configuration options
|
|
163
|
+
* @param {string} options.logo - Logo URL to display on error page
|
|
164
|
+
* @param {string} options.homeRoute - Default route for "Go Home" button (default: '/dashboard')
|
|
165
|
+
* @returns {Object} Route configuration for 404 catch-all
|
|
166
|
+
*/
|
|
167
|
+
export function create404Route(options = {}) {
|
|
168
|
+
const {
|
|
169
|
+
logo = '',
|
|
170
|
+
homeRoute = '/dashboard',
|
|
171
|
+
} = options
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
path: '/:pathMatch(.*)*',
|
|
175
|
+
name: 'error-404',
|
|
176
|
+
component: SparkError404View,
|
|
177
|
+
props: { logo, homeRoute },
|
|
178
|
+
meta: { auth: false },
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/stores/auth.js
CHANGED
|
@@ -3,13 +3,15 @@ import { reactive, computed } from 'vue'
|
|
|
3
3
|
import axios from 'axios'
|
|
4
4
|
import { getCookie, setCookie, deleteCookie } from '../utils/cookies.js'
|
|
5
5
|
|
|
6
|
-
const TOKEN_NAME = 'bolt-token'
|
|
6
|
+
const TOKEN_NAME = 'bolt-next-token'
|
|
7
7
|
|
|
8
8
|
export const useSparkAuthStore = defineStore('auth', () => {
|
|
9
9
|
const state = reactive({
|
|
10
10
|
user: null,
|
|
11
11
|
token: null,
|
|
12
12
|
ready: false,
|
|
13
|
+
// Dev JWT override token (bypasses normal auth flow)
|
|
14
|
+
overrideToken: null,
|
|
13
15
|
// Configurable endpoints
|
|
14
16
|
endpoints: {
|
|
15
17
|
login: '/login',
|
|
@@ -52,6 +54,9 @@ export const useSparkAuthStore = defineStore('auth', () => {
|
|
|
52
54
|
if (config.callbacks) {
|
|
53
55
|
Object.assign(state.callbacks, config.callbacks)
|
|
54
56
|
}
|
|
57
|
+
if (config.overrideToken) {
|
|
58
|
+
state.overrideToken = config.overrideToken
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
// JWT Cookie Management
|
|
@@ -116,7 +121,9 @@ export const useSparkAuthStore = defineStore('auth', () => {
|
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
const fetchUser = async () => {
|
|
119
|
-
|
|
124
|
+
// Use override token if present, otherwise use cookie token
|
|
125
|
+
const token = state.overrideToken || getTokenCookie()
|
|
126
|
+
|
|
120
127
|
if (!token) {
|
|
121
128
|
state.ready = true
|
|
122
129
|
return null
|
|
@@ -130,15 +137,28 @@ export const useSparkAuthStore = defineStore('auth', () => {
|
|
|
130
137
|
})
|
|
131
138
|
|
|
132
139
|
state.user = data
|
|
133
|
-
|
|
140
|
+
// Only set token in state if not using override (don't write override to state.token)
|
|
141
|
+
if (!state.overrideToken) {
|
|
142
|
+
state.token = token
|
|
143
|
+
}
|
|
134
144
|
} catch (error) {
|
|
135
|
-
|
|
145
|
+
// Only clear cookie if not using override token
|
|
146
|
+
if (!state.overrideToken) {
|
|
147
|
+
clearTokenCookie()
|
|
148
|
+
}
|
|
136
149
|
} finally {
|
|
137
150
|
state.ready = true
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
|
|
141
|
-
const check = computed(() =>
|
|
154
|
+
const check = computed(() => {
|
|
155
|
+
// If override token exists, check is always true (user will be fetched)
|
|
156
|
+
if (state.overrideToken) {
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
// Otherwise check normal token and user
|
|
160
|
+
return !!state.token && !!state.user
|
|
161
|
+
})
|
|
142
162
|
|
|
143
163
|
return {
|
|
144
164
|
state,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full grid place-content-center relative">
|
|
3
|
+
<div class="absolute top-8 left-8">
|
|
4
|
+
<img v-if="props.logo" :src="props.logo" alt="Logo" class="h-[23px] w-auto" />
|
|
5
|
+
<svg
|
|
6
|
+
v-else
|
|
7
|
+
width="59"
|
|
8
|
+
height="23"
|
|
9
|
+
viewBox="0 0 59 23"
|
|
10
|
+
fill="none"
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M49.2029 17.1264V8.03835H44.0829V5.22235H58.0989V8.03835H52.9629V17.1264H49.2029Z"
|
|
15
|
+
fill="#1C64F2"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M34.5 5.22235H38.228V14.1664H46.5V17.1264H34.5V5.22235Z" fill="#1C64F2" />
|
|
18
|
+
<path
|
|
19
|
+
d="M28.3161 0C29.1499 0 29.7522 0.798785 29.5209 1.59757L27.1856 9.77748H30.9046C31.747 9.77748 32.4279 10.4584 32.4279 11.3008C32.4279 11.7504 32.2315 12.1738 31.891 12.4619L20.5989 22.0517C20.3719 22.2438 20.0839 22.3485 19.787 22.3485C18.9533 22.3485 18.351 21.5497 18.5823 20.751L20.9176 12.571H17.1463C16.33 12.571 15.6709 11.9119 15.6709 11.1001C15.6709 10.6679 15.8586 10.262 16.186 9.98263L27.5043 0.301181C27.7312 0.104759 28.0193 0 28.3161 0ZM26.7404 3.71021L18.8311 10.4759H22.3056C22.633 10.4759 22.9429 10.6286 23.1437 10.8905C23.3445 11.1524 23.4056 11.4929 23.3139 11.8072L21.3584 18.6601L29.355 11.8727H25.7976C25.4702 11.8727 25.1603 11.7199 24.9595 11.458C24.7587 11.1961 24.6976 10.8556 24.7893 10.5413L26.7404 3.71021Z"
|
|
20
|
+
fill="#1C64F2"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
d="M0 17.1264V5.22235H10.192C13.6 5.22235 14.544 6.53435 14.544 7.94235V8.16635C14.544 9.70235 13.232 10.3264 12.656 10.4864C13.472 10.6944 15.216 11.3984 15.216 13.4784V13.7024C15.216 15.5904 14.144 17.1264 10.288 17.1264H0ZM9.552 7.73435H3.728V9.67035H9.552C10.592 9.67035 10.848 9.19035 10.848 8.71035V8.67835C10.848 8.18235 10.592 7.73435 9.552 7.73435ZM9.872 12.1984H3.728V14.5344H9.872C11.12 14.5344 11.344 13.8464 11.344 13.3664V13.3024C11.344 12.7904 11.104 12.1984 9.872 12.1984Z"
|
|
24
|
+
fill="#1C64F2"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="max-w-lg grid gap-y-6 text-center -mt-8">
|
|
30
|
+
<div>
|
|
31
|
+
<div class="text-primary-600 text-7xl font-bold mb-4">403</div>
|
|
32
|
+
<h1 class="text-3xl text-gray-900 font-semibold tracking-tight mb-3">
|
|
33
|
+
Access Forbidden
|
|
34
|
+
</h1>
|
|
35
|
+
<p class="text-gray-600">
|
|
36
|
+
You don't have permission to access this resource.<br>
|
|
37
|
+
If you believe this is an error, please contact your administrator.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="flex gap-4 justify-center">
|
|
42
|
+
<spark-button @click="goHome" size="lg">
|
|
43
|
+
Go to Home
|
|
44
|
+
</spark-button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup>
|
|
51
|
+
import { useRouter } from 'vue-router'
|
|
52
|
+
import { SparkButton } from '@/index.js'
|
|
53
|
+
|
|
54
|
+
const router = useRouter()
|
|
55
|
+
|
|
56
|
+
const props = defineProps({
|
|
57
|
+
logo: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: '',
|
|
60
|
+
},
|
|
61
|
+
homeRoute: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: '/dashboard',
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const goHome = () => {
|
|
68
|
+
router.push(props.homeRoute)
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full grid place-content-center relative">
|
|
3
|
+
<div class="absolute top-8 left-8">
|
|
4
|
+
<img v-if="props.logo" :src="props.logo" alt="Logo" class="h-[23px] w-auto" />
|
|
5
|
+
<svg
|
|
6
|
+
v-else
|
|
7
|
+
width="59"
|
|
8
|
+
height="23"
|
|
9
|
+
viewBox="0 0 59 23"
|
|
10
|
+
fill="none"
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M49.2029 17.1264V8.03835H44.0829V5.22235H58.0989V8.03835H52.9629V17.1264H49.2029Z"
|
|
15
|
+
fill="#1C64F2"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M34.5 5.22235H38.228V14.1664H46.5V17.1264H34.5V5.22235Z" fill="#1C64F2" />
|
|
18
|
+
<path
|
|
19
|
+
d="M28.3161 0C29.1499 0 29.7522 0.798785 29.5209 1.59757L27.1856 9.77748H30.9046C31.747 9.77748 32.4279 10.4584 32.4279 11.3008C32.4279 11.7504 32.2315 12.1738 31.891 12.4619L20.5989 22.0517C20.3719 22.2438 20.0839 22.3485 19.787 22.3485C18.9533 22.3485 18.351 21.5497 18.5823 20.751L20.9176 12.571H17.1463C16.33 12.571 15.6709 11.9119 15.6709 11.1001C15.6709 10.6679 15.8586 10.262 16.186 9.98263L27.5043 0.301181C27.7312 0.104759 28.0193 0 28.3161 0ZM26.7404 3.71021L18.8311 10.4759H22.3056C22.633 10.4759 22.9429 10.6286 23.1437 10.8905C23.3445 11.1524 23.4056 11.4929 23.3139 11.8072L21.3584 18.6601L29.355 11.8727H25.7976C25.4702 11.8727 25.1603 11.7199 24.9595 11.458C24.7587 11.1961 24.6976 10.8556 24.7893 10.5413L26.7404 3.71021Z"
|
|
20
|
+
fill="#1C64F2"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
d="M0 17.1264V5.22235H10.192C13.6 5.22235 14.544 6.53435 14.544 7.94235V8.16635C14.544 9.70235 13.232 10.3264 12.656 10.4864C13.472 10.6944 15.216 11.3984 15.216 13.4784V13.7024C15.216 15.5904 14.144 17.1264 10.288 17.1264H0ZM9.552 7.73435H3.728V9.67035H9.552C10.592 9.67035 10.848 9.19035 10.848 8.71035V8.67835C10.848 8.18235 10.592 7.73435 9.552 7.73435ZM9.872 12.1984H3.728V14.5344H9.872C11.12 14.5344 11.344 13.8464 11.344 13.3664V13.3024C11.344 12.7904 11.104 12.1984 9.872 12.1984Z"
|
|
24
|
+
fill="#1C64F2"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="max-w-lg grid gap-y-6 text-center -mt-8">
|
|
30
|
+
<div>
|
|
31
|
+
<div class="text-primary-600 text-7xl font-bold mb-4">404</div>
|
|
32
|
+
<h1 class="text-3xl text-gray-900 font-semibold tracking-tight mb-3">
|
|
33
|
+
Page Not Found
|
|
34
|
+
</h1>
|
|
35
|
+
<p class="text-gray-600">
|
|
36
|
+
The page you're looking for doesn't exist or has been moved.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="flex gap-4 justify-center">
|
|
41
|
+
<spark-button @click="goHome" size="lg">
|
|
42
|
+
Go to Home
|
|
43
|
+
</spark-button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup>
|
|
50
|
+
import { useRouter } from 'vue-router'
|
|
51
|
+
import { SparkButton } from '@/index.js'
|
|
52
|
+
|
|
53
|
+
const router = useRouter()
|
|
54
|
+
|
|
55
|
+
const props = defineProps({
|
|
56
|
+
logo: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: '',
|
|
59
|
+
},
|
|
60
|
+
homeRoute: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: '/dashboard',
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const goHome = () => {
|
|
67
|
+
router.push(props.homeRoute)
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-full grid place-content-center relative">
|
|
3
|
+
<div class="absolute top-8 left-8">
|
|
4
|
+
<img v-if="props.logo" :src="props.logo" alt="Logo" class="h-[23px] w-auto" />
|
|
5
|
+
<svg
|
|
6
|
+
v-else
|
|
7
|
+
width="59"
|
|
8
|
+
height="23"
|
|
9
|
+
viewBox="0 0 59 23"
|
|
10
|
+
fill="none"
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M49.2029 17.1264V8.03835H44.0829V5.22235H58.0989V8.03835H52.9629V17.1264H49.2029Z"
|
|
15
|
+
fill="#1C64F2"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M34.5 5.22235H38.228V14.1664H46.5V17.1264H34.5V5.22235Z" fill="#1C64F2" />
|
|
18
|
+
<path
|
|
19
|
+
d="M28.3161 0C29.1499 0 29.7522 0.798785 29.5209 1.59757L27.1856 9.77748H30.9046C31.747 9.77748 32.4279 10.4584 32.4279 11.3008C32.4279 11.7504 32.2315 12.1738 31.891 12.4619L20.5989 22.0517C20.3719 22.2438 20.0839 22.3485 19.787 22.3485C18.9533 22.3485 18.351 21.5497 18.5823 20.751L20.9176 12.571H17.1463C16.33 12.571 15.6709 11.9119 15.6709 11.1001C15.6709 10.6679 15.8586 10.262 16.186 9.98263L27.5043 0.301181C27.7312 0.104759 28.0193 0 28.3161 0ZM26.7404 3.71021L18.8311 10.4759H22.3056C22.633 10.4759 22.9429 10.6286 23.1437 10.8905C23.3445 11.1524 23.4056 11.4929 23.3139 11.8072L21.3584 18.6601L29.355 11.8727H25.7976C25.4702 11.8727 25.1603 11.7199 24.9595 11.458C24.7587 11.1961 24.6976 10.8556 24.7893 10.5413L26.7404 3.71021Z"
|
|
20
|
+
fill="#1C64F2"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
d="M0 17.1264V5.22235H10.192C13.6 5.22235 14.544 6.53435 14.544 7.94235V8.16635C14.544 9.70235 13.232 10.3264 12.656 10.4864C13.472 10.6944 15.216 11.3984 15.216 13.4784V13.7024C15.216 15.5904 14.144 17.1264 10.288 17.1264H0ZM9.552 7.73435H3.728V9.67035H9.552C10.592 9.67035 10.848 9.19035 10.848 8.71035V8.67835C10.848 8.18235 10.592 7.73435 9.552 7.73435ZM9.872 12.1984H3.728V14.5344H9.872C11.12 14.5344 11.344 13.8464 11.344 13.3664V13.3024C11.344 12.7904 11.104 12.1984 9.872 12.1984Z"
|
|
24
|
+
fill="#1C64F2"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="max-w-lg grid gap-y-6 text-center -mt-8">
|
|
30
|
+
<div>
|
|
31
|
+
<div class="text-primary-600 text-7xl font-bold mb-4">
|
|
32
|
+
{{ props.errorCode || 'Error' }}
|
|
33
|
+
</div>
|
|
34
|
+
<h1 class="text-3xl text-gray-900 font-semibold tracking-tight mb-3">
|
|
35
|
+
{{ props.title || 'Something went wrong' }}
|
|
36
|
+
</h1>
|
|
37
|
+
<p class="text-gray-600">
|
|
38
|
+
{{ props.message || 'An unexpected error occurred. Please try again later.' }}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="flex gap-4 justify-center">
|
|
43
|
+
<spark-button @click="goHome" size="lg">
|
|
44
|
+
Go to Home
|
|
45
|
+
</spark-button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script setup>
|
|
52
|
+
import { useRouter } from 'vue-router'
|
|
53
|
+
import { SparkButton } from '@/index.js'
|
|
54
|
+
|
|
55
|
+
const router = useRouter()
|
|
56
|
+
|
|
57
|
+
const props = defineProps({
|
|
58
|
+
logo: {
|
|
59
|
+
type: String,
|
|
60
|
+
default: '',
|
|
61
|
+
},
|
|
62
|
+
homeRoute: {
|
|
63
|
+
type: String,
|
|
64
|
+
default: '/dashboard',
|
|
65
|
+
},
|
|
66
|
+
errorCode: {
|
|
67
|
+
type: [String, Number],
|
|
68
|
+
default: null,
|
|
69
|
+
},
|
|
70
|
+
title: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: '',
|
|
73
|
+
},
|
|
74
|
+
message: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: '',
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const goHome = () => {
|
|
81
|
+
router.push(props.homeRoute)
|
|
82
|
+
}
|
|
83
|
+
</script>
|
package/src/views/index.js
CHANGED
|
@@ -2,3 +2,6 @@ export { default as SparkLoginView } from './SparkLoginView.vue'
|
|
|
2
2
|
export { default as SparkLogoutView } from './SparkLogoutView.vue'
|
|
3
3
|
export { default as SparkForgotPasswordView } from './SparkForgotPasswordView.vue'
|
|
4
4
|
export { default as SparkResetPasswordView } from './SparkResetPasswordView.vue'
|
|
5
|
+
export { default as SparkError403View } from './SparkError403View.vue'
|
|
6
|
+
export { default as SparkError404View } from './SparkError404View.vue'
|
|
7
|
+
export { default as SparkErrorGeneralView } from './SparkErrorGeneralView.vue'
|