@data-fair/lib-vue 1.21.0 → 1.22.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/async-action.js +43 -40
- package/computed-deep-diff.js +9 -0
- package/concept-filters.js +22 -20
- package/deep-diff.d.ts +3 -0
- package/deep-diff.js +14 -0
- package/fetch.js +57 -50
- package/locale-dayjs-global.js +4 -4
- package/locale-dayjs.js +33 -32
- package/package.json +2 -1
- package/reactive-search-params-global.js +3 -3
- package/reactive-search-params.js +140 -124
- package/session.js +369 -325
- package/ui-notif.js +80 -63
- package/vite.d.ts +1 -0
- package/vite.js +15 -14
- package/ws.js +62 -58
package/session.js
CHANGED
|
@@ -1,340 +1,384 @@
|
|
|
1
|
-
import { shallowReadonly } from 'vue'
|
|
2
|
-
import { FetchError } from 'ofetch'
|
|
3
|
-
import { reactive, computed, watch, inject, ref, shallowRef, readonly } from 'vue'
|
|
4
|
-
import { ofetch } from 'ofetch'
|
|
5
|
-
import { jwtDecode } from 'jwt-decode'
|
|
6
|
-
import cookiesModule from 'universal-cookie'
|
|
7
|
-
import Debug from 'debug'
|
|
8
|
-
import inIframe from '@data-fair/lib-utils/in-iframe.js'
|
|
9
|
-
export * from '@data-fair/lib-common-types/session/index.js'
|
|
10
|
-
const Cookies = cookiesModule
|
|
11
|
-
const debug = Debug('session')
|
|
12
|
-
debug.log = console.log.bind(console)
|
|
13
|
-
function getDefaultTheme
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
import { shallowReadonly } from 'vue';
|
|
2
|
+
import { FetchError } from 'ofetch';
|
|
3
|
+
import { reactive, computed, watch, inject, ref, shallowRef, readonly } from 'vue';
|
|
4
|
+
import { ofetch } from 'ofetch';
|
|
5
|
+
import { jwtDecode } from 'jwt-decode';
|
|
6
|
+
import cookiesModule from 'universal-cookie';
|
|
7
|
+
import Debug from 'debug';
|
|
8
|
+
import inIframe from '@data-fair/lib-utils/in-iframe.js';
|
|
9
|
+
export * from '@data-fair/lib-common-types/session/index.js';
|
|
10
|
+
const Cookies = cookiesModule;
|
|
11
|
+
const debug = Debug('session');
|
|
12
|
+
debug.log = console.log.bind(console);
|
|
13
|
+
function getDefaultTheme(site) {
|
|
14
|
+
// see https://www.scottohara.me/blog/2021/10/01/detect-high-contrast-and-dark-modes.html
|
|
15
|
+
const preferDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
16
|
+
const preferHC = window.matchMedia && window.matchMedia('(forced-colors: active)').matches;
|
|
17
|
+
if (site.theme.hcDark && preferDark && preferHC)
|
|
18
|
+
return 'hc-dark';
|
|
19
|
+
if (site.theme.hc && preferHC)
|
|
20
|
+
return 'hc';
|
|
21
|
+
if (site.theme.dark && preferDark)
|
|
22
|
+
return 'dark';
|
|
23
|
+
return 'default';
|
|
21
24
|
}
|
|
22
|
-
function jwtDecodeAlive
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.warn(`token not yet valid: ${decoded.nbf}>${now}, ${JSON.stringify(decoded)}`)
|
|
33
|
-
// do not return here, this is probably a false flag due to a slightly mismatched clock
|
|
34
|
-
}
|
|
35
|
-
return decoded
|
|
36
|
-
}
|
|
37
|
-
const getTopLocation = () => {
|
|
38
|
-
try {
|
|
39
|
-
return window.top ? window.top.location : window.location
|
|
40
|
-
} catch (err) {
|
|
41
|
-
return window.location
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const goTo = (url) => {
|
|
45
|
-
const topLocation = getTopLocation()
|
|
46
|
-
if (topLocation == null) {
|
|
47
|
-
throw new TypeError('session.goTo was called without access to the window object or its location')
|
|
48
|
-
}
|
|
49
|
-
if (url) { topLocation.href = url } else { topLocation.reload() }
|
|
50
|
-
}
|
|
51
|
-
const defaultOptions = { directoryUrl: '/simple-directory', sitePath: '', defaultLang: 'fr' }
|
|
52
|
-
export async function getSession (initOptions) {
|
|
53
|
-
const options = { ...defaultOptions, ...initOptions }
|
|
54
|
-
const cookiesPath = options.sitePath + '/'
|
|
55
|
-
debug(`init directoryUrl=${options.directoryUrl}, cookiesPath=${cookiesPath}`)
|
|
56
|
-
const ssr = !!options.req
|
|
57
|
-
if (ssr) { debug('run in SSR context') }
|
|
58
|
-
const customFetch = initOptions?.customFetch ?? ofetch
|
|
59
|
-
// use vue-router to detect page change and maintain a reference to the current page location
|
|
60
|
-
// top page if we are in iframe context
|
|
61
|
-
const topLocation = computed(() => {
|
|
62
|
-
if (ssr) { return undefined }
|
|
63
|
-
if (options.route?.fullPath) { /* empty */ } // adds reactivity
|
|
64
|
-
const location = getTopLocation()
|
|
65
|
-
debug('update location based on route change', location)
|
|
66
|
-
return location
|
|
67
|
-
})
|
|
68
|
-
// the core state of the session that is filled by reading cookies
|
|
69
|
-
const state = reactive({})
|
|
70
|
-
const fullSite = shallowRef(null)
|
|
71
|
-
const site = shallowRef(null)
|
|
72
|
-
const theme = ref(null)
|
|
73
|
-
// cookies are the source of truth and this information is transformed into the state reactive object
|
|
74
|
-
const cookies = initOptions?.cookies ?? new Cookies(options.req?.headers.cookie)
|
|
75
|
-
const readState = () => {
|
|
76
|
-
theme.value = cookies.get('theme') ?? null
|
|
77
|
-
const langCookie = cookies.get('i18n_lang')
|
|
78
|
-
state.lang = langCookie ?? options.defaultLang
|
|
79
|
-
const idToken = cookies.get('id_token')
|
|
80
|
-
const user = jwtDecodeAlive(idToken)
|
|
81
|
-
if (!user) {
|
|
82
|
-
delete state.user
|
|
83
|
-
delete state.organization
|
|
84
|
-
delete state.account
|
|
85
|
-
delete state.accountRole
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
// this is to prevent null values that are put by SD versions that do not strictly respect their schema
|
|
89
|
-
for (const org of user.organizations) {
|
|
90
|
-
if (!org.department) {
|
|
91
|
-
delete org.department
|
|
92
|
-
delete org.departmentName
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
state.user = user
|
|
96
|
-
const organizationId = cookies.get('id_token_org')
|
|
97
|
-
const departmentId = cookies.get('id_token_dep')
|
|
98
|
-
const switchedRole = cookies.get('id_token_role')
|
|
99
|
-
if (organizationId) {
|
|
100
|
-
state.organization = state.user.organizations.find(o => {
|
|
101
|
-
if (o.id !== organizationId) { return false }
|
|
102
|
-
if (departmentId && departmentId !== o.department) { return false }
|
|
103
|
-
if (switchedRole && switchedRole !== o.role) { return false }
|
|
104
|
-
return true
|
|
105
|
-
})
|
|
106
|
-
} else {
|
|
107
|
-
delete state.organization
|
|
108
|
-
}
|
|
109
|
-
if (state.organization) {
|
|
110
|
-
state.account = {
|
|
111
|
-
type: 'organization',
|
|
112
|
-
id: state.organization.id,
|
|
113
|
-
name: state.organization.name,
|
|
114
|
-
department: state.organization.department,
|
|
115
|
-
departmentName: state.organization.departmentName
|
|
116
|
-
}
|
|
117
|
-
state.accountRole = state.organization.role
|
|
118
|
-
} else {
|
|
119
|
-
state.account = {
|
|
120
|
-
type: 'user',
|
|
121
|
-
id: state.user.id,
|
|
122
|
-
name: state.user.name
|
|
123
|
-
}
|
|
124
|
-
state.accountRole = 'admin'
|
|
25
|
+
function jwtDecodeAlive(jwt) {
|
|
26
|
+
if (!jwt)
|
|
27
|
+
return;
|
|
28
|
+
const decoded = jwtDecode(jwt);
|
|
29
|
+
if (!decoded)
|
|
30
|
+
return;
|
|
31
|
+
const now = Math.ceil(Date.now().valueOf() / 1000);
|
|
32
|
+
if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
|
|
33
|
+
// token expired
|
|
34
|
+
return;
|
|
125
35
|
}
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
if (state.user.siteOwner.type === 'organization' && state.user.siteOwner.id === state.organization?.id) {
|
|
131
|
-
state.siteRole = state.organization.role
|
|
132
|
-
}
|
|
36
|
+
if (typeof decoded.nbf !== 'undefined' && decoded.nbf > now) {
|
|
37
|
+
console.warn(`token not yet valid: ${decoded.nbf}>${now}, ${JSON.stringify(decoded)}`);
|
|
38
|
+
// do not return here, this is probably a false flag due to a slightly mismatched clock
|
|
133
39
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// and in order to listen to storage event from other contexts and sync session info accross windows and tabs
|
|
140
|
-
if (!ssr) {
|
|
141
|
-
const storageListener = (event) => {
|
|
142
|
-
if (event.key === 'sd-session' + options.sitePath) { readState() }
|
|
143
|
-
}
|
|
144
|
-
window.addEventListener('storage', storageListener)
|
|
40
|
+
return decoded;
|
|
41
|
+
}
|
|
42
|
+
const getTopLocation = () => {
|
|
43
|
+
try {
|
|
44
|
+
return window.top ? window.top.location : window.location;
|
|
145
45
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// onUnmounted(() => { window.removeEventListener('storage', storageListener) })
|
|
149
|
-
// trigger some full page refresh when some key session elements are changed
|
|
150
|
-
// the danger of simply using reactivity is too high, data must be re-fetched, etc.
|
|
151
|
-
watch(() => state.account, (account, oldAccount) => {
|
|
152
|
-
if (account?.type !== oldAccount?.type || account?.id !== oldAccount?.id || account?.department !== oldAccount?.department) {
|
|
153
|
-
goTo(null)
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
watch(() => state.lang, () => {
|
|
157
|
-
goTo(null)
|
|
158
|
-
})
|
|
159
|
-
watch(() => state.dark, () => {
|
|
160
|
-
goTo(null)
|
|
161
|
-
})
|
|
162
|
-
watch(state, (state) => {
|
|
163
|
-
if (!ssr) {
|
|
164
|
-
window.localStorage.setItem('sd-session' + options.sitePath, JSON.stringify(state))
|
|
165
|
-
}
|
|
166
|
-
debug('state changed', state)
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
// login can be performed as a simple link (please use target=top) or as a function
|
|
170
|
-
function loginUrl (redirect, extraParams = {}, immediateRedirect = true) {
|
|
171
|
-
// login can also be used to redirect user immediately if he is already logged
|
|
172
|
-
if (redirect && state.user && immediateRedirect) { return redirect }
|
|
173
|
-
if (!redirect && topLocation.value) { redirect = topLocation.value.href }
|
|
174
|
-
let url = `${options.directoryUrl}/login?redirect=${encodeURIComponent(redirect ?? '')}`
|
|
175
|
-
Object.keys(extraParams).filter(key => ![null, undefined, ''].includes(extraParams[key])).forEach((key) => {
|
|
176
|
-
url += `&${key}=${encodeURIComponent(extraParams[key])}`
|
|
177
|
-
})
|
|
178
|
-
return url
|
|
179
|
-
}
|
|
180
|
-
const login = (redirect, extraParams = {}, immediateRedirect = true) => {
|
|
181
|
-
goTo(loginUrl(redirect, extraParams, immediateRedirect))
|
|
182
|
-
}
|
|
183
|
-
const logout = async (redirect) => {
|
|
184
|
-
await customFetch(`${options.directoryUrl}/api/auth`, { method: 'DELETE' })
|
|
185
|
-
// sometimes server side cookie deletion is not applied immediately in browser local js context
|
|
186
|
-
// so we do it here to
|
|
187
|
-
cookies.remove('id_token')
|
|
188
|
-
cookies.remove('id_token_org')
|
|
189
|
-
cookies.remove('id_token_dep')
|
|
190
|
-
cookies.remove('id_token_role')
|
|
191
|
-
goTo(redirect ?? options.logoutRedirectUrl ?? null)
|
|
192
|
-
}
|
|
193
|
-
const switchOrganization = (org, dep, role, updateState = true) => {
|
|
194
|
-
const cookieOpts = { path: cookiesPath }
|
|
195
|
-
if (org) { cookies.set('id_token_org', org, cookieOpts) } else { cookies.remove('id_token_org', cookieOpts) }
|
|
196
|
-
if (dep) { cookies.set('id_token_dep', dep, cookieOpts) } else { cookies.remove('id_token_dep', cookieOpts) }
|
|
197
|
-
if (role) { cookies.set('id_token_role', dep, cookieOpts) } else { cookies.remove('id_token_role', cookieOpts) }
|
|
198
|
-
if (updateState) { readState() }
|
|
199
|
-
}
|
|
200
|
-
const setAdminMode = async (adminMode, redirect) => {
|
|
201
|
-
if (adminMode) {
|
|
202
|
-
const params = { adminMode: 'true' }
|
|
203
|
-
if (state.user != null) { params.email = state.user.email }
|
|
204
|
-
const url = loginUrl(redirect, params, true)
|
|
205
|
-
goTo(url)
|
|
206
|
-
} else {
|
|
207
|
-
await customFetch(`${options.directoryUrl}/api/auth/adminmode`, { method: 'DELETE' })
|
|
208
|
-
goTo(redirect ?? null)
|
|
46
|
+
catch (err) {
|
|
47
|
+
return window.location;
|
|
209
48
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
await customFetch(`${options.directoryUrl}/api/auth/asadmin`, { method: 'DELETE' })
|
|
49
|
+
};
|
|
50
|
+
const goTo = (url) => {
|
|
51
|
+
const topLocation = getTopLocation();
|
|
52
|
+
if (topLocation == null) {
|
|
53
|
+
throw new TypeError('session.goTo was called without access to the window object or its location');
|
|
216
54
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
55
|
+
if (url)
|
|
56
|
+
topLocation.href = url;
|
|
57
|
+
else
|
|
58
|
+
topLocation.reload();
|
|
59
|
+
};
|
|
60
|
+
const defaultOptions = { directoryUrl: '/simple-directory', sitePath: '', defaultLang: 'fr' };
|
|
61
|
+
export async function getSession(initOptions) {
|
|
62
|
+
const options = { ...defaultOptions, ...initOptions };
|
|
63
|
+
const cookiesPath = options.sitePath + '/';
|
|
64
|
+
debug(`init directoryUrl=${options.directoryUrl}, cookiesPath=${cookiesPath}`);
|
|
65
|
+
const ssr = !!options.req;
|
|
66
|
+
if (ssr)
|
|
67
|
+
debug('run in SSR context');
|
|
68
|
+
const customFetch = initOptions?.customFetch ?? ofetch;
|
|
69
|
+
// use vue-router to detect page change and maintain a reference to the current page location
|
|
70
|
+
// top page if we are in iframe context
|
|
71
|
+
const topLocation = computed(() => {
|
|
72
|
+
if (ssr)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (options.route?.fullPath) { /* empty */ } // adds reactivity
|
|
75
|
+
const location = getTopLocation();
|
|
76
|
+
debug('update location based on route change', location);
|
|
77
|
+
return location;
|
|
78
|
+
});
|
|
79
|
+
// the core state of the session that is filled by reading cookies
|
|
80
|
+
const state = reactive({});
|
|
81
|
+
const fullSite = shallowRef(null);
|
|
82
|
+
const site = shallowRef(null);
|
|
83
|
+
const theme = ref(null);
|
|
84
|
+
// cookies are the source of truth and this information is transformed into the state reactive object
|
|
85
|
+
const cookies = initOptions?.cookies ?? new Cookies(options.req?.headers.cookie);
|
|
86
|
+
const readState = () => {
|
|
87
|
+
theme.value = cookies.get('theme') ?? null;
|
|
88
|
+
const langCookie = cookies.get('i18n_lang');
|
|
89
|
+
state.lang = langCookie ?? options.defaultLang;
|
|
90
|
+
const idToken = cookies.get('id_token');
|
|
91
|
+
const user = jwtDecodeAlive(idToken);
|
|
92
|
+
if (!user) {
|
|
93
|
+
delete state.user;
|
|
94
|
+
delete state.organization;
|
|
95
|
+
delete state.account;
|
|
96
|
+
delete state.accountRole;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// this is to prevent null values that are put by SD versions that do not strictly respect their schema
|
|
100
|
+
for (const org of user.organizations) {
|
|
101
|
+
if (!org.department) {
|
|
102
|
+
delete org.department;
|
|
103
|
+
delete org.departmentName;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
state.user = user;
|
|
107
|
+
const organizationId = cookies.get('id_token_org');
|
|
108
|
+
const departmentId = cookies.get('id_token_dep');
|
|
109
|
+
const switchedRole = cookies.get('id_token_role');
|
|
110
|
+
if (organizationId) {
|
|
111
|
+
state.organization = state.user.organizations.find(o => {
|
|
112
|
+
if (o.id !== organizationId)
|
|
113
|
+
return false;
|
|
114
|
+
if (departmentId && departmentId !== o.department)
|
|
115
|
+
return false;
|
|
116
|
+
if (switchedRole && switchedRole !== o.role)
|
|
117
|
+
return false;
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
delete state.organization;
|
|
123
|
+
}
|
|
124
|
+
if (state.organization) {
|
|
125
|
+
state.account = {
|
|
126
|
+
type: 'organization',
|
|
127
|
+
id: state.organization.id,
|
|
128
|
+
name: state.organization.name,
|
|
129
|
+
department: state.organization.department,
|
|
130
|
+
departmentName: state.organization.departmentName
|
|
131
|
+
};
|
|
132
|
+
state.accountRole = state.organization.role;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
state.account = {
|
|
136
|
+
type: 'user',
|
|
137
|
+
id: state.user.id,
|
|
138
|
+
name: state.user.name
|
|
139
|
+
};
|
|
140
|
+
state.accountRole = 'admin';
|
|
141
|
+
}
|
|
142
|
+
if (state.user?.siteOwner) {
|
|
143
|
+
if (state.user.siteOwner.type === 'user' && state.user.siteOwner.id === state.user.id) {
|
|
144
|
+
state.siteRole = 'admin';
|
|
145
|
+
}
|
|
146
|
+
if (state.user.siteOwner.type === 'organization' && state.user.siteOwner.id === state.organization?.id) {
|
|
147
|
+
state.siteRole = state.organization.role;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
readState();
|
|
152
|
+
debug('initial state', state);
|
|
236
153
|
if (!ssr) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
154
|
+
// sessionData is also stored in localStorage as a way to access it in simpler pages that do not require use-session
|
|
155
|
+
// and in order to listen to storage event from other contexts and sync session info accross windows and tabs
|
|
156
|
+
if (!ssr) {
|
|
157
|
+
const storageListener = (event) => {
|
|
158
|
+
if (event.key === 'sd-session' + options.sitePath)
|
|
159
|
+
readState();
|
|
160
|
+
};
|
|
161
|
+
window.addEventListener('storage', storageListener);
|
|
162
|
+
}
|
|
163
|
+
// we cannot use onUnmounted here or we get warnings "onUnmounted is called when there is no active component instance to be associated with. "
|
|
164
|
+
// TODO: should we have another cleanup mechanism ?
|
|
165
|
+
// onUnmounted(() => { window.removeEventListener('storage', storageListener) })
|
|
166
|
+
// trigger some full page refresh when some key session elements are changed
|
|
167
|
+
// the danger of simply using reactivity is too high, data must be re-fetched, etc.
|
|
168
|
+
watch(() => state.account, (account, oldAccount) => {
|
|
169
|
+
if (account?.type !== oldAccount?.type || account?.id !== oldAccount?.id || account?.department !== oldAccount?.department) {
|
|
170
|
+
goTo(null);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
watch(() => state.lang, () => {
|
|
174
|
+
goTo(null);
|
|
175
|
+
});
|
|
176
|
+
watch(() => state.dark, () => {
|
|
177
|
+
goTo(null);
|
|
178
|
+
});
|
|
179
|
+
watch(state, (state) => {
|
|
180
|
+
if (!ssr) {
|
|
181
|
+
window.localStorage.setItem('sd-session' + options.sitePath, JSON.stringify(state));
|
|
182
|
+
}
|
|
183
|
+
debug('state changed', state);
|
|
184
|
+
});
|
|
248
185
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (theme.value === 'dark') {
|
|
262
|
-
partialSite.colors = siteInfo.theme.darkColors
|
|
263
|
-
partialSite.dark = true
|
|
264
|
-
}
|
|
265
|
-
if (theme.value === 'hc-dark') {
|
|
266
|
-
partialSite.colors = siteInfo.theme.hcDarkColors
|
|
267
|
-
partialSite.dark = true
|
|
268
|
-
}
|
|
269
|
-
site.value = partialSite
|
|
270
|
-
} else {
|
|
271
|
-
site.value = siteInfo
|
|
186
|
+
// login can be performed as a simple link (please use target=top) or as a function
|
|
187
|
+
function loginUrl(redirect, extraParams = {}, immediateRedirect = true) {
|
|
188
|
+
// login can also be used to redirect user immediately if he is already logged
|
|
189
|
+
if (redirect && state.user && immediateRedirect)
|
|
190
|
+
return redirect;
|
|
191
|
+
if (!redirect && topLocation.value)
|
|
192
|
+
redirect = topLocation.value.href;
|
|
193
|
+
let url = `${options.directoryUrl}/login?redirect=${encodeURIComponent(redirect ?? '')}`;
|
|
194
|
+
Object.keys(extraParams).filter(key => ![null, undefined, ''].includes(extraParams[key])).forEach((key) => {
|
|
195
|
+
url += `&${key}=${encodeURIComponent(extraParams[key])}`;
|
|
196
|
+
});
|
|
197
|
+
return url;
|
|
272
198
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
199
|
+
const login = (redirect, extraParams = {}, immediateRedirect = true) => {
|
|
200
|
+
goTo(loginUrl(redirect, extraParams, immediateRedirect));
|
|
201
|
+
};
|
|
202
|
+
const logout = async (redirect) => {
|
|
203
|
+
await customFetch(`${options.directoryUrl}/api/auth`, { method: 'DELETE' });
|
|
204
|
+
// sometimes server side cookie deletion is not applied immediately in browser local js context
|
|
205
|
+
// so we do it here to
|
|
206
|
+
cookies.remove('id_token');
|
|
207
|
+
cookies.remove('id_token_org');
|
|
208
|
+
cookies.remove('id_token_dep');
|
|
209
|
+
cookies.remove('id_token_role');
|
|
210
|
+
goTo(redirect ?? options.logoutRedirectUrl ?? null);
|
|
211
|
+
};
|
|
212
|
+
const switchOrganization = (org, dep, role, updateState = true) => {
|
|
213
|
+
const cookieOpts = { path: cookiesPath };
|
|
214
|
+
if (org)
|
|
215
|
+
cookies.set('id_token_org', org, cookieOpts);
|
|
216
|
+
else
|
|
217
|
+
cookies.remove('id_token_org', cookieOpts);
|
|
218
|
+
if (dep)
|
|
219
|
+
cookies.set('id_token_dep', dep, cookieOpts);
|
|
220
|
+
else
|
|
221
|
+
cookies.remove('id_token_dep', cookieOpts);
|
|
222
|
+
if (role)
|
|
223
|
+
cookies.set('id_token_role', role, cookieOpts);
|
|
224
|
+
else
|
|
225
|
+
cookies.remove('id_token_role', cookieOpts);
|
|
226
|
+
if (updateState)
|
|
227
|
+
readState();
|
|
228
|
+
};
|
|
229
|
+
const setAdminMode = async (adminMode, redirect) => {
|
|
230
|
+
if (adminMode) {
|
|
231
|
+
const params = { adminMode: 'true' };
|
|
232
|
+
if (state.user != null)
|
|
233
|
+
params.email = state.user.email;
|
|
234
|
+
const url = loginUrl(redirect, params, true);
|
|
235
|
+
goTo(url);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
await customFetch(`${options.directoryUrl}/api/auth/adminmode`, { method: 'DELETE' });
|
|
239
|
+
goTo(redirect ?? null);
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
const asAdmin = async (user) => {
|
|
243
|
+
if (user) {
|
|
244
|
+
await customFetch(`${options.directoryUrl}/api/auth/asadmin`, { method: 'POST', body: user });
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
await customFetch(`${options.directoryUrl}/api/auth/asadmin`, { method: 'DELETE' });
|
|
248
|
+
}
|
|
249
|
+
goTo(null);
|
|
250
|
+
};
|
|
251
|
+
const cancelDeletion = async () => {
|
|
252
|
+
if (state.user == null)
|
|
253
|
+
return;
|
|
254
|
+
await customFetch(`${options.directoryUrl}/api/users/${state.user.id}`, { method: 'PATCH', body: ({ plannedDeletion: null }) });
|
|
255
|
+
readState();
|
|
256
|
+
};
|
|
257
|
+
const switchLang = (value) => {
|
|
258
|
+
const maxAge = 60 * 60 * 24 * 365; // 1 year
|
|
259
|
+
cookies.set('i18n_lang', value, { maxAge, path: cookiesPath });
|
|
260
|
+
goTo(null);
|
|
261
|
+
};
|
|
262
|
+
const switchTheme = (value) => {
|
|
263
|
+
const maxAge = 60 * 60 * 24 * 365; // 1 year
|
|
264
|
+
cookies.set('theme', value, { maxAge, path: cookiesPath });
|
|
265
|
+
goTo(null);
|
|
266
|
+
};
|
|
267
|
+
const keepalive = async () => {
|
|
268
|
+
if (state.user == null)
|
|
269
|
+
return;
|
|
270
|
+
if (!ssr) {
|
|
271
|
+
window.localStorage.setItem('sd-keepalive' + options.sitePath, `${new Date().getTime()}`);
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
await customFetch(`${options.directoryUrl}/api/auth/keepalive`, { method: 'POST' });
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
if (err instanceof FetchError && err.statusCode === 401) {
|
|
278
|
+
console.warn('session was expired or deleted server side');
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
readState();
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const refreshSiteInfo = async () => {
|
|
287
|
+
const siteInfo = await customFetch(`${options.directoryUrl}/api/sites/_public`) ?? null;
|
|
288
|
+
if (siteInfo.theme) {
|
|
289
|
+
fullSite.value = siteInfo;
|
|
290
|
+
const partialSite = {
|
|
291
|
+
main: siteInfo.main,
|
|
292
|
+
logo: siteInfo.theme.logo,
|
|
293
|
+
colors: siteInfo.theme.colors
|
|
294
|
+
};
|
|
295
|
+
if (theme.value == null)
|
|
296
|
+
theme.value = getDefaultTheme(siteInfo);
|
|
297
|
+
if (theme.value === 'hc')
|
|
298
|
+
partialSite.colors = siteInfo.theme.hcColors;
|
|
299
|
+
if (theme.value === 'dark') {
|
|
300
|
+
partialSite.colors = siteInfo.theme.darkColors;
|
|
301
|
+
partialSite.dark = true;
|
|
302
|
+
}
|
|
303
|
+
if (theme.value === 'hc-dark') {
|
|
304
|
+
partialSite.colors = siteInfo.theme.hcDarkColors;
|
|
305
|
+
partialSite.dark = true;
|
|
306
|
+
}
|
|
307
|
+
site.value = partialSite;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
site.value = siteInfo;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
if (options.siteInfo)
|
|
314
|
+
await refreshSiteInfo();
|
|
315
|
+
// immediately performs a keepalive, but only on top windows (not iframes or popups)
|
|
316
|
+
// and only if it was not done very recently (maybe from a refreshed page next to this one)
|
|
317
|
+
// also run an auto-refresh loop
|
|
318
|
+
if (!ssr && !inIframe && !('triggerCapture' in window)) {
|
|
319
|
+
const lastKeepalive = window.localStorage.getItem('sd-keepalive' + options.sitePath);
|
|
320
|
+
// check cookies.get('id_token') not state.user so that we do a keepalive on expired id tokens
|
|
321
|
+
if (cookies.get('id_token') && (!lastKeepalive || (new Date().getTime() - Number(lastKeepalive)) > 10000)) {
|
|
322
|
+
await keepalive();
|
|
323
|
+
}
|
|
324
|
+
const refreshLoopDelay = 10 * 60 * 1000; // 10 minutes
|
|
325
|
+
setInterval(() => {
|
|
326
|
+
const lastKeepalive = window.localStorage.getItem('sd-keepalive' + options.sitePath);
|
|
327
|
+
if (!lastKeepalive || (new Date().getTime() - Number(lastKeepalive)) > refreshLoopDelay / 2) {
|
|
328
|
+
keepalive().catch(err => console.error(err));
|
|
329
|
+
}
|
|
330
|
+
}, refreshLoopDelay);
|
|
283
331
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
keepalive,
|
|
311
|
-
refreshSiteInfo,
|
|
312
|
-
switchTheme,
|
|
313
|
-
switchLang,
|
|
314
|
-
topLocation,
|
|
315
|
-
options
|
|
316
|
-
}
|
|
317
|
-
return session
|
|
332
|
+
const session = {
|
|
333
|
+
state,
|
|
334
|
+
organization: computed(() => state.organization),
|
|
335
|
+
user: computed(() => state.user),
|
|
336
|
+
account: computed(() => state.account),
|
|
337
|
+
accountRole: computed(() => state.accountRole),
|
|
338
|
+
siteRole: computed(() => state.siteRole),
|
|
339
|
+
lang: computed(() => state.lang),
|
|
340
|
+
theme: readonly(theme),
|
|
341
|
+
site: shallowReadonly(site),
|
|
342
|
+
fullSite: shallowReadonly(fullSite),
|
|
343
|
+
loginUrl,
|
|
344
|
+
login,
|
|
345
|
+
logout,
|
|
346
|
+
switchOrganization,
|
|
347
|
+
setAdminMode,
|
|
348
|
+
asAdmin,
|
|
349
|
+
cancelDeletion,
|
|
350
|
+
keepalive,
|
|
351
|
+
refreshSiteInfo,
|
|
352
|
+
switchTheme,
|
|
353
|
+
switchLang,
|
|
354
|
+
topLocation,
|
|
355
|
+
options
|
|
356
|
+
};
|
|
357
|
+
return session;
|
|
318
358
|
}
|
|
319
359
|
// uses pattern for SSR friendly plugin/composable, cf https://antfu.me/posts/composable-vue-vueday-2021#shared-state-ssr-friendly
|
|
320
|
-
export const sessionKey = Symbol('session')
|
|
321
|
-
export async function createSession
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
360
|
+
export const sessionKey = Symbol('session');
|
|
361
|
+
export async function createSession(initOptions) {
|
|
362
|
+
const session = await getSession(initOptions);
|
|
363
|
+
return {
|
|
364
|
+
...session,
|
|
365
|
+
install(app) { app.provide(sessionKey, session); },
|
|
366
|
+
};
|
|
327
367
|
}
|
|
328
|
-
export function useSession
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
368
|
+
export function useSession() {
|
|
369
|
+
const session = inject(sessionKey);
|
|
370
|
+
if (!session)
|
|
371
|
+
throw new Error('useSession requires using the plugin createSession');
|
|
372
|
+
return session;
|
|
332
373
|
}
|
|
333
|
-
export function useSessionAuthenticated
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
374
|
+
export function useSessionAuthenticated(errorBuilder) {
|
|
375
|
+
const session = useSession();
|
|
376
|
+
if (!session.state.user) {
|
|
377
|
+
if (errorBuilder)
|
|
378
|
+
throw errorBuilder();
|
|
379
|
+
else
|
|
380
|
+
throw new Error('useSessionAuthenticated requires a logged in user');
|
|
381
|
+
}
|
|
382
|
+
return session;
|
|
339
383
|
}
|
|
340
|
-
export default useSession
|
|
384
|
+
export default useSession;
|