@wishbone-media/spark 0.9.1 → 0.10.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 +386 -341
- package/package.json +1 -1
- package/src/plugins/axios.js +86 -0
- package/src/plugins/index.js +2 -1
- package/src/plugins/router.js +6 -0
- package/src/stores/auth.js +25 -5
- package/src/utils/cookies.js +21 -7
package/package.json
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
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 errors (unauthorized)
|
|
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
|
+
return Promise.reject(error)
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return instance
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Setup axios for a Vue app with global availability
|
|
72
|
+
* @param {App} app - Vue app instance
|
|
73
|
+
* @param {Object} config - Axios configuration options
|
|
74
|
+
* @returns {AxiosInstance} Configured axios instance
|
|
75
|
+
*/
|
|
76
|
+
export function setupAxios(app, config = {}) {
|
|
77
|
+
const axiosInstance = createAxiosInstance(config)
|
|
78
|
+
|
|
79
|
+
// Make axios available via injection (Composition API)
|
|
80
|
+
app.provide('axios', axiosInstance)
|
|
81
|
+
|
|
82
|
+
// Also make available via global properties (Options API compatibility)
|
|
83
|
+
app.config.globalProperties.$axios = axiosInstance
|
|
84
|
+
|
|
85
|
+
return axiosInstance
|
|
86
|
+
}
|
package/src/plugins/index.js
CHANGED
package/src/plugins/router.js
CHANGED
|
@@ -68,6 +68,12 @@ function processNavigation(to, next, authStore, defaultAuthenticatedRoute) {
|
|
|
68
68
|
const requiresAuth = to.meta.auth !== false
|
|
69
69
|
const isAuthenticated = authStore.check
|
|
70
70
|
|
|
71
|
+
// Block logout route if using override token
|
|
72
|
+
if (authStore.state.overrideToken && to.path === '/logout') {
|
|
73
|
+
next({ path: defaultAuthenticatedRoute })
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
// Handle unauthenticated users
|
|
72
78
|
if (!isAuthenticated) {
|
|
73
79
|
// Allow access to public routes (login, forgot-password, etc.)
|
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,
|
package/src/utils/cookies.js
CHANGED
|
@@ -38,14 +38,28 @@ export const deleteCookie = (name, options = {}) => {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export const getDomain = () => {
|
|
41
|
-
|
|
42
|
-
// For STAGING: returns .ENV.letsbolt.io
|
|
43
|
-
// For DEV: returns .letsbolt.test
|
|
44
|
-
let reserveSlices = -3
|
|
41
|
+
const hostname = window.location.hostname
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
// Case 1: Pure localhost (localhost:5173)
|
|
44
|
+
if (hostname === 'localhost') {
|
|
45
|
+
return 'localhost'
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
// Case 2: .test domain (web.bolt.test, dash.bolt.test:8080) -> .bolt.test
|
|
49
|
+
if (hostname.endsWith('.test')) {
|
|
50
|
+
return '.' + hostname.split('.').slice(-2).join('.')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Case 3: Staging .io domain (alpha.letsbolt.io, buzz.letsbolt.io) -> .letsbolt.io
|
|
54
|
+
if (hostname.endsWith('.io')) {
|
|
55
|
+
return '.' + hostname.split('.').slice(-2).join('.')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Case 4: Production .com.au domain (letsbolt.com.au, buzz.letsbolt.com.au) -> .letsbolt.com.au
|
|
59
|
+
if (hostname.endsWith('.com.au')) {
|
|
60
|
+
return '.' + hostname.split('.').slice(-3).join('.')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Fallback: return hostname as-is
|
|
64
|
+
return hostname
|
|
51
65
|
}
|