@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishbone-media/spark",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -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
+ }
@@ -1,2 +1,3 @@
1
1
  export { Icons, addIcons, setupFontAwesome } from './fontawesome.js'
2
- export { createAuthRoutes, setupAuthGuards } from './router.js'
2
+ export { createAuthRoutes, setupAuthGuards } from './router.js'
3
+ export { createAxiosInstance, setupAxios } from './axios.js'
@@ -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.)
@@ -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
- const token = getTokenCookie()
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
- state.token = token
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
- clearTokenCookie()
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(() => !!state.token && !!state.user)
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,
@@ -38,14 +38,28 @@ export const deleteCookie = (name, options = {}) => {
38
38
  }
39
39
 
40
40
  export const getDomain = () => {
41
- // For PRODUCTION: returns .letsbolt.com.au
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
- if (window.location.hostname.endsWith('.test')) {
47
- reserveSlices = -2
43
+ // Case 1: Pure localhost (localhost:5173)
44
+ if (hostname === 'localhost') {
45
+ return 'localhost'
48
46
  }
49
47
 
50
- return window.location.hostname.split('.').slice(reserveSlices).join('.')
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
  }