@drax/identity-vue 3.67.0 → 3.69.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/README.md +150 -1
- package/package.json +6 -6
- package/src/composables/setupAuthSession.ts +20 -15
- package/src/composables/useAuth.ts +14 -6
- package/src/stores/AuthStore.ts +15 -2
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Integration contracts:
|
|
|
58
58
|
null-to-null logout. Legacy stores without the field use the initial null epoch.
|
|
59
59
|
Late returned refresh credentials are revoked best-effort.
|
|
60
60
|
- Transient refresh/profile errors retain recoverable credentials; rejected
|
|
61
|
-
credentials (HTTP 401/403, including common-front wrapped errors) clear auth.
|
|
61
|
+
credentials (HTTP 401/403/498, including common-front wrapped errors) clear auth.
|
|
62
62
|
Logout clears local state immediately, with best-effort server revocation even
|
|
63
63
|
when the feature has subsequently been disabled. Offline revocation cannot be
|
|
64
64
|
guaranteed; the backend expiration/revocation policy remains authoritative.
|
|
@@ -72,6 +72,155 @@ Focused tests:
|
|
|
72
72
|
npm exec --workspace @drax/identity-vue -- vitest run test/auth-session.spec.ts test/auth-session.integration.spec.ts test/components/__tests__/IdentityLogin.spec.ts
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
## Expired sessions and external login migration
|
|
76
|
+
|
|
77
|
+
Session maintenance runs even with remember-me disabled or without a refresh token.
|
|
78
|
+
Expired/malformed access tokens without renewal are cleared together with the profile,
|
|
79
|
+
refresh credentials, persisted state and shared authorization headers. With remember-me
|
|
80
|
+
disabled, a live access token remains usable, but legacy refresh credentials are discarded.
|
|
81
|
+
`AuthStore.isAuth` checks local JWT validity, not mere token presence. This check is not
|
|
82
|
+
signature verification: the backend remains authoritative.
|
|
83
|
+
|
|
84
|
+
Store token/clear actions synchronize both shared clients. `syncAuthHeaders()` never
|
|
85
|
+
installs an expired/malformed JWT; transient refresh errors preserve recoverable tokens
|
|
86
|
+
in storage, remove expired authorization from the clients and reject the authenticated
|
|
87
|
+
REST preflight before fetch. There is no navigation or automatic request replay.
|
|
88
|
+
Removing a header directly is only a transport operation and intentionally does **not**
|
|
89
|
+
clear the store: use session/store actions for credential changes. Direct `$patch` is
|
|
90
|
+
not a session API; normal request maintenance re-reads persisted state.
|
|
91
|
+
|
|
92
|
+
External login should use the public workflow:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
useAuth().loginWithSession(
|
|
96
|
+
operation: (rememberMe: boolean) => Promise<ILoginResponse>,
|
|
97
|
+
rememberMe = false,
|
|
98
|
+
): Promise<void>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`ILoginResponse` is the existing exported type from `@drax/identity-front`:
|
|
102
|
+
`{accessToken: string, refreshToken?: string, refreshExpiresAt?: string}`.
|
|
103
|
+
The workflow clears the previous credentials/profile, captures the operation epoch,
|
|
104
|
+
then calls `operation` with the effective frontend flag + explicit opt-in. It confirms
|
|
105
|
+
the session, synchronizes headers and loads the profile using the same path as standard
|
|
106
|
+
login. Failures reject the promise; only navigate after successful completion.
|
|
107
|
+
Discarded or unwanted refresh credentials are revoked best-effort. A profile failure
|
|
108
|
+
clears the current attempted login; offline revocation cannot be guaranteed.
|
|
109
|
+
|
|
110
|
+
**Start the entire async exchange inside the callback.** Passing a promise started
|
|
111
|
+
before the workflow, or awaiting the response then calling `loginWithToken`, cannot
|
|
112
|
+
protect against logout/new login while that request is pending. Legacy
|
|
113
|
+
`loginWithToken(string)` remains available for access-token-only callers; it does not
|
|
114
|
+
accept or persist refresh credentials.
|
|
115
|
+
|
|
116
|
+
Example consumer component (the consumer's Google integration supplies
|
|
117
|
+
`obtainGoogleCredential`; no Google SDK is added to Drax):
|
|
118
|
+
|
|
119
|
+
```vue
|
|
120
|
+
<script setup lang="ts">
|
|
121
|
+
import {ref} from 'vue'
|
|
122
|
+
import {useRouter} from 'vue-router'
|
|
123
|
+
import {HttpRestClientFactory} from '@drax/common-front'
|
|
124
|
+
import type {ILoginResponse} from '@drax/identity-front'
|
|
125
|
+
import {IdentityRememberMe, useAuth} from '@drax/identity-vue'
|
|
126
|
+
import {obtainGoogleCredential} from './googleCredential'
|
|
127
|
+
|
|
128
|
+
const rememberMe = ref(false)
|
|
129
|
+
const loading = ref(false)
|
|
130
|
+
const {loginWithSession} = useAuth()
|
|
131
|
+
const router = useRouter()
|
|
132
|
+
|
|
133
|
+
async function loginGoogle() {
|
|
134
|
+
loading.value = true
|
|
135
|
+
try {
|
|
136
|
+
await loginWithSession(async effectiveRememberMe => {
|
|
137
|
+
const credential = await obtainGoogleCredential()
|
|
138
|
+
return await HttpRestClientFactory.getInstance().post('/api/google/login', {
|
|
139
|
+
credential, rememberMe: effectiveRememberMe,
|
|
140
|
+
}, {removeHeaders: ['Authorization']}) as ILoginResponse
|
|
141
|
+
}, rememberMe.value)
|
|
142
|
+
await router.push('/') // Adapt to the consumer's authorized destination.
|
|
143
|
+
} catch {
|
|
144
|
+
// Show the consumer's localized error; never log credentials or headers.
|
|
145
|
+
} finally {
|
|
146
|
+
loading.value = false
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
|
|
151
|
+
<template>
|
|
152
|
+
<IdentityRememberMe v-model="rememberMe" />
|
|
153
|
+
<v-btn :loading="loading" :disabled="loading" @click="loginGoogle">Google</v-btn>
|
|
154
|
+
</template>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
This checkbox starts unchecked and hides itself when the feature is disabled. It is
|
|
158
|
+
independent from the private checkbox inside `IdentityLogin`; an external button must
|
|
159
|
+
supply its own explicit opt-in. The callback receives a snapshot, not a later checkbox
|
|
160
|
+
value. Do not write tokens to Pinia/localStorage or manipulate Authorization manually.
|
|
161
|
+
|
|
162
|
+
Use `removeHeaders: ['Authorization']` on the external exchange: it excludes stale
|
|
163
|
+
bearer credentials and skips Drax session preflight for that request, without mutating
|
|
164
|
+
global headers or disabling browser CORS preflight. There is no second refresh/retry
|
|
165
|
+
mechanism. The backend must verify the external identity before issuing any Drax
|
|
166
|
+
session; see `identity-back/docs/remember-me.md`.
|
|
167
|
+
|
|
168
|
+
Consumer changes:
|
|
169
|
+
|
|
170
|
+
- **GoogleProvider:** add `removeHeaders: ['Authorization']` to the login POST and
|
|
171
|
+
remove redundant `setHttpClientToken` calls and return the full `ILoginResponse`.
|
|
172
|
+
Invoke that provider inside `loginWithSession(effectiveRememberMe =>
|
|
173
|
+
provider.login(credential, effectiveRememberMe), rememberMe)`; obtain the credential
|
|
174
|
+
inside the callback too if that step is asynchronous. Do not start the request first.
|
|
175
|
+
- **LoginPage:** delete the workaround that checks invalid access tokens and calls
|
|
176
|
+
`logout()`. Session cleanup belongs to maintenance, not a page (logout also navigates).
|
|
177
|
+
- **Bootstrap:** retain `app.use(pinia); await setupAuthSession(); app.use(router)`;
|
|
178
|
+
install Pinia's persisted-state plugin before these calls. No extra cleanup needed.
|
|
179
|
+
- **Router:** retain the existing `await refreshAuthSession()` plus catch and cancel
|
|
180
|
+
protected navigation while `!isAuthenticated() && isRememberMeEnabled() &&
|
|
181
|
+
authStore.refreshToken`. Do not convert transient failures into logout or redirects.
|
|
182
|
+
- **Backend external route:** replace the Google-specific middleware wrapper with
|
|
183
|
+
the supported Fastify route option `{config: {identityJwt: 'ignore'}}` on the POST
|
|
184
|
+
route. Keep its external credential verifier and normal authorization checks.
|
|
185
|
+
A public route with optional JWT authentication still rejects a supplied invalid
|
|
186
|
+
Bearer; only this explicit option ignores it. See identity-back's
|
|
187
|
+
`docs/jwt-route-policy.md`.
|
|
188
|
+
|
|
189
|
+
The two local workarounds can therefore be removed after upgrading compatible packages
|
|
190
|
+
and declaring the external route policy. Google SDK/signature verification itself is
|
|
191
|
+
consumer-owned; Drax tests use a deterministic external verifier, not Google's service.
|
|
192
|
+
REST request-time maintenance requires the shared client's optional `setBeforeRequest`;
|
|
193
|
+
GraphQL and custom clients without that hook do not get automatic preflight renewal.
|
|
194
|
+
|
|
195
|
+
### External remember-me rollout and security
|
|
196
|
+
|
|
197
|
+
This change requires new releases of **`@drax/identity-back` and
|
|
198
|
+
`@drax/identity-vue`**. No new type or runtime changes are needed in identity-front,
|
|
199
|
+
identity-share or common-front for this extension, but consumers must retain compatible
|
|
200
|
+
remember-me APIs and the shared REST preflight (avoid duplicate singleton packages).
|
|
201
|
+
No publication/version is implied by this source change. Deploy the compatible backend
|
|
202
|
+
and its verified external endpoint first, then update/rebuild the frontend coherently.
|
|
203
|
+
The user must log in again and explicitly check the option; existing ordinary sessions
|
|
204
|
+
are not upgraded.
|
|
205
|
+
|
|
206
|
+
Keep `DRAX_REMEMBER_ME_ENABLED=true`, optional
|
|
207
|
+
`DRAX_REFRESH_TOKEN_TTL_SECONDS=2592000` on the backend and
|
|
208
|
+
`VITE_DRAX_REMEMBER_ME_ENABLED=true` at frontend build time. Refresh has an absolute
|
|
209
|
+
TTL, uses the user's **currently assigned database tenant**, and does not extend JWT
|
|
210
|
+
lifetime. A tenant selected with switchTenant must be selected again after renewal.
|
|
211
|
+
|
|
212
|
+
Storage contains sensitive credentials in **localStorage**, not HttpOnly cookies.
|
|
213
|
+
Use HTTPS and XSS protections. Web Locks coordinates refresh across tabs when available;
|
|
214
|
+
without it, single-flight is per tab only. Disabling the backend flag blocks new
|
|
215
|
+
remembered sessions/refresh; disabling the frontend flag prevents opt-in and maintenance
|
|
216
|
+
strips legacy local refresh credentials. Neither flag revokes already-issued JWTs or
|
|
217
|
+
all server refresh records. Re-enabling can make unexpired, non-revoked refresh tokens
|
|
218
|
+
usable again. Logout revokes the latest known Drax refresh token best-effort.
|
|
219
|
+
|
|
220
|
+
Drax refresh tokens renew **Drax sessions only**. They are not Google's OAuth refresh
|
|
221
|
+
or access tokens and do not grant Gmail/Calendar access or offline Google API consent.
|
|
222
|
+
Keep Google API authorization and its token storage separate.
|
|
223
|
+
|
|
75
224
|
## Registro con creación de tenant (opcional, REST)
|
|
76
225
|
|
|
77
226
|
En el **backend**:
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.69.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
"format": "prettier --write src/"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@drax/common-front": "^3.
|
|
22
|
-
"@drax/common-vue": "^3.
|
|
21
|
+
"@drax/common-front": "^3.68.0",
|
|
22
|
+
"@drax/common-vue": "^3.68.0",
|
|
23
23
|
"@drax/crud-front": "^3.58.0",
|
|
24
24
|
"@drax/crud-share": "^3.65.0",
|
|
25
|
-
"@drax/crud-vue": "^3.
|
|
26
|
-
"@drax/identity-front": "^3.
|
|
25
|
+
"@drax/crud-vue": "^3.68.0",
|
|
26
|
+
"@drax/identity-front": "^3.68.0",
|
|
27
27
|
"@drax/identity-share": "^3.67.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"vue-tsc": "^3.2.4",
|
|
50
50
|
"vuetify": "^3.11.8"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "4358ebd6e083a11924349ef5bb07a20cafccbabe"
|
|
53
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {AuthSystemFactory} from '@drax/identity-front'
|
|
1
|
+
import {HttpRestClientFactory} from '@drax/common-front'
|
|
2
|
+
import {AuthHelper, AuthSystemFactory} from '@drax/identity-front'
|
|
3
3
|
import {useAuthStore} from '../stores/AuthStore'
|
|
4
4
|
|
|
5
5
|
export const isRememberMeEnabled = () => import.meta.env.VITE_DRAX_REMEMBER_ME_ENABLED === 'true'
|
|
@@ -12,11 +12,7 @@ const revisions = new WeakMap<AuthStore, number>()
|
|
|
12
12
|
export const authSessionRevision = (store = useAuthStore()) => revisions.get(store) ?? 0
|
|
13
13
|
|
|
14
14
|
export function syncAuthHeaders(store = useAuthStore()) {
|
|
15
|
-
|
|
16
|
-
for (const client of clients) {
|
|
17
|
-
if (store.accessToken) client.addHeader('Authorization', `Bearer ${store.accessToken}`)
|
|
18
|
-
else client.removeHeader('Authorization')
|
|
19
|
-
}
|
|
15
|
+
store.syncAuthHeaders()
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export function clearAuthSession(store = useAuthStore()) {
|
|
@@ -28,7 +24,7 @@ export function clearAuthSession(store = useAuthStore()) {
|
|
|
28
24
|
export function isRejectedCredential(error: unknown): boolean {
|
|
29
25
|
const e = error as {statusCode?: number, status?: number, response?: {status?: number}, cause?: unknown, error?: unknown}
|
|
30
26
|
const status = e?.statusCode ?? e?.status ?? e?.response?.status
|
|
31
|
-
return status === 401 || status === 403 ||
|
|
27
|
+
return status === 401 || status === 403 || status === 498 ||
|
|
32
28
|
(!!e?.error && e.error !== error && isRejectedCredential(e.error)) ||
|
|
33
29
|
(!!e?.cause && e.cause !== error && isRejectedCredential(e.cause))
|
|
34
30
|
}
|
|
@@ -76,11 +72,7 @@ export function isCurrentAuthSession(store: AuthStore, token: string | null, ver
|
|
|
76
72
|
}
|
|
77
73
|
|
|
78
74
|
function needsRefresh(token: string | null) {
|
|
79
|
-
|
|
80
|
-
const payload = (token?.split('.')[1] ?? '').replace(/-/g, '+').replace(/_/g, '/')
|
|
81
|
-
const {exp} = JSON.parse(atob(payload))
|
|
82
|
-
return typeof exp !== 'number' || exp * 1000 <= Date.now() + 30_000
|
|
83
|
-
} catch { return true }
|
|
75
|
+
return !AuthHelper.isJWTValid(token ?? '', 30)
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
async function withSessionLock(action: () => Promise<void>) {
|
|
@@ -115,12 +107,25 @@ async function fetchSessionUser(store: AuthStore) {
|
|
|
115
107
|
}
|
|
116
108
|
|
|
117
109
|
export function refreshAuthSession(store = useAuthStore()): Promise<void> {
|
|
118
|
-
|
|
110
|
+
|
|
119
111
|
const running = flights.get(store)
|
|
120
112
|
if (running) return running
|
|
121
113
|
const flight = withSessionLock(async () => {
|
|
122
114
|
rereadSession(store)
|
|
123
|
-
|
|
115
|
+
const valid = AuthHelper.isJWTValid(store.accessToken ?? '')
|
|
116
|
+
if (!isRememberMeEnabled() || !store.refreshToken) {
|
|
117
|
+
if (!valid) {
|
|
118
|
+
if (store.accessToken || store.refreshToken || store.refreshExpiresAt || store.authUser) clearAuthSession(store)
|
|
119
|
+
else syncAuthHeaders(store)
|
|
120
|
+
} else {
|
|
121
|
+
// Disabling remember-me must not invalidate an otherwise live ordinary JWT.
|
|
122
|
+
if (!isRememberMeEnabled() && (store.refreshToken || store.refreshExpiresAt)) {
|
|
123
|
+
store.setAccessToken(store.accessToken!)
|
|
124
|
+
}
|
|
125
|
+
syncAuthHeaders(store)
|
|
126
|
+
}
|
|
127
|
+
return
|
|
128
|
+
}
|
|
124
129
|
if (!needsRefresh(store.accessToken)) {
|
|
125
130
|
if (!store.authUser) await fetchSessionUser(store)
|
|
126
131
|
return
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {useAuthStore} from "../stores/AuthStore";
|
|
2
2
|
import {AuthHelper, AuthSystemFactory} from "@drax/identity-front";
|
|
3
|
-
import type {AuthSystem} from "@drax/identity-front";
|
|
3
|
+
import type {AuthSystem, ILoginResponse} from "@drax/identity-front";
|
|
4
4
|
import {useRouter} from 'vue-router'
|
|
5
5
|
import {authSessionRevision, clearAuthSession, isCurrentAuthOperation, isCurrentAuthSession, isRememberMeEnabled, rereadSession, revokeSessionToken, syncAuthHeaders} from './setupAuthSession'
|
|
6
6
|
|
|
@@ -11,14 +11,13 @@ export function useAuth() {
|
|
|
11
11
|
|
|
12
12
|
const authSystem: AuthSystem = AuthSystemFactory.getInstance()
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** Start the external credential exchange inside operation so logout/new login can cancel its result. */
|
|
15
|
+
const loginWithSession = async (operation: (rememberMe: boolean) => Promise<ILoginResponse>, rememberMe = false) => {
|
|
15
16
|
clearAuthSession(authStore)
|
|
16
17
|
const revision = authSessionRevision(authStore)
|
|
17
18
|
const version = authStore.sessionVersion
|
|
18
19
|
const remembered = isRememberMeEnabled() && rememberMe
|
|
19
|
-
const result = remembered
|
|
20
|
-
? await authSystem.login(username, password, true)
|
|
21
|
-
: await authSystem.login(username, password)
|
|
20
|
+
const result = await operation(remembered)
|
|
22
21
|
if (authSessionRevision(authStore) !== revision || !isCurrentAuthOperation(authStore, version)) {
|
|
23
22
|
syncAuthHeaders(authStore)
|
|
24
23
|
if (result.refreshToken) await revokeSessionToken(result.refreshToken).catch(() => {})
|
|
@@ -26,6 +25,8 @@ export function useAuth() {
|
|
|
26
25
|
}
|
|
27
26
|
if (remembered) authStore.setSession(result)
|
|
28
27
|
else authStore.setAccessToken(result.accessToken)
|
|
28
|
+
// A server returning refresh credentials without opt-in must not leave an orphan session.
|
|
29
|
+
if (!remembered && result.refreshToken) void revokeSessionToken(result.refreshToken).catch(() => {})
|
|
29
30
|
syncAuthHeaders(authStore)
|
|
30
31
|
try {
|
|
31
32
|
await fetchAuthUser()
|
|
@@ -35,11 +36,18 @@ export function useAuth() {
|
|
|
35
36
|
const token = authStore.refreshToken
|
|
36
37
|
clearAuthSession(authStore)
|
|
37
38
|
if (token) await revokeSessionToken(token).catch(() => {})
|
|
39
|
+
} else if (result.refreshToken) {
|
|
40
|
+
await revokeSessionToken(result.refreshToken).catch(() => {})
|
|
38
41
|
}
|
|
39
42
|
throw error
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
const login = async (username: string, password: string, rememberMe = false) => {
|
|
47
|
+
await loginWithSession(remembered => remembered
|
|
48
|
+
? authSystem.login(username, password, true)
|
|
49
|
+
: authSystem.login(username, password), rememberMe)
|
|
50
|
+
}
|
|
43
51
|
|
|
44
52
|
const switchTenant = async (tenantId: string) => {
|
|
45
53
|
const {accessToken} = await authSystem.switchTenant(tenantId)
|
|
@@ -150,7 +158,7 @@ export function useAuth() {
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
return {
|
|
153
|
-
login, logout, loginWithToken,
|
|
161
|
+
login, logout, loginWithToken, loginWithSession,
|
|
154
162
|
tokenIsValid, hasPermission,
|
|
155
163
|
isAuthenticated, fetchAuthUser,
|
|
156
164
|
changeOwnPassword, changeAvatar, passwordPolicy,
|
package/src/stores/AuthStore.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {IAuthFullUser} from "@drax/identity-front";
|
|
|
4
4
|
//@ts-ignore
|
|
5
5
|
import type from 'pinia-plugin-persistedstate'
|
|
6
6
|
import {AuthHelper} from "@drax/identity-front";
|
|
7
|
+
import {HttpClientFactory, HttpRestClientFactory} from '@drax/common-front'
|
|
7
8
|
|
|
8
9
|
export const useAuthStore = defineStore('AuthStore', {
|
|
9
10
|
state: () => ({
|
|
@@ -15,8 +16,7 @@ export const useAuthStore = defineStore('AuthStore', {
|
|
|
15
16
|
}),
|
|
16
17
|
getters:{
|
|
17
18
|
isAuth: (state): boolean => {
|
|
18
|
-
|
|
19
|
-
return !!state.accessToken
|
|
19
|
+
return !!state.accessToken && AuthHelper.isJWTValid(state.accessToken)
|
|
20
20
|
},
|
|
21
21
|
hasPermission: (state) => (permission:string) => {
|
|
22
22
|
return state.authUser && state.authUser.role && state.authUser.role.permissions ? state.authUser.role.permissions.includes(permission) : false
|
|
@@ -26,6 +26,16 @@ export const useAuthStore = defineStore('AuthStore', {
|
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
28
|
actions:{
|
|
29
|
+
syncAuthHeaders(){
|
|
30
|
+
const clients = new Set([HttpClientFactory.getInstance(), HttpRestClientFactory.getInstance()])
|
|
31
|
+
for (const client of clients) {
|
|
32
|
+
for (const header of Object.keys(client.baseHeaders ?? {})) {
|
|
33
|
+
if (header.toLowerCase() === 'authorization' && header !== 'Authorization') client.removeHeader(header)
|
|
34
|
+
}
|
|
35
|
+
if (this.accessToken && AuthHelper.isJWTValid(this.accessToken)) client.addHeader('Authorization', `Bearer ${this.accessToken}`)
|
|
36
|
+
else client.removeHeader('Authorization')
|
|
37
|
+
}
|
|
38
|
+
},
|
|
29
39
|
persistSession(){
|
|
30
40
|
// Persist synchronously so another tab holding the refresh lock sees rotation immediately.
|
|
31
41
|
try {
|
|
@@ -35,6 +45,7 @@ export const useAuthStore = defineStore('AuthStore', {
|
|
|
35
45
|
setAccessToken(accessToken:string){
|
|
36
46
|
this.$patch({accessToken, refreshToken: null, refreshExpiresAt: null})
|
|
37
47
|
this.persistSession()
|
|
48
|
+
this.syncAuthHeaders()
|
|
38
49
|
},
|
|
39
50
|
setSession(session: {accessToken: string, refreshToken?: string, refreshExpiresAt?: string}){
|
|
40
51
|
this.$patch({
|
|
@@ -43,6 +54,7 @@ export const useAuthStore = defineStore('AuthStore', {
|
|
|
43
54
|
refreshExpiresAt: session.refreshExpiresAt ?? null,
|
|
44
55
|
})
|
|
45
56
|
this.persistSession()
|
|
57
|
+
this.syncAuthHeaders()
|
|
46
58
|
},
|
|
47
59
|
setAuthUser(authUser:IAuthFullUser){
|
|
48
60
|
this.authUser = authUser
|
|
@@ -55,6 +67,7 @@ export const useAuthStore = defineStore('AuthStore', {
|
|
|
55
67
|
sessionVersion: globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`,
|
|
56
68
|
})
|
|
57
69
|
this.persistSession()
|
|
70
|
+
this.syncAuthHeaders()
|
|
58
71
|
}
|
|
59
72
|
},
|
|
60
73
|
|