@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/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 (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) { return 'hc-dark' }
18
- if (site.theme.hc && preferHC) { return 'hc' }
19
- if (site.theme.dark && preferDark) { return 'dark' }
20
- return 'default'
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 (jwt) {
23
- if (!jwt) { return }
24
- const decoded = jwtDecode(jwt)
25
- if (!decoded) { return }
26
- const now = Math.ceil(Date.now().valueOf() / 1000)
27
- if (typeof decoded.exp !== 'undefined' && decoded.exp < now) {
28
- // token expired
29
- return
30
- }
31
- if (typeof decoded.nbf !== 'undefined' && decoded.nbf > now) {
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 (state.user?.siteOwner) {
127
- if (state.user.siteOwner.type === 'user' && state.user.siteOwner.id === state.user.id) {
128
- state.siteRole = 'admin'
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
- readState()
136
- debug('initial state', state)
137
- if (!ssr) {
138
- // sessionData is also stored in localStorage as a way to access it in simpler pages that do not require use-session
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
- // we cannot use onUnmounted here or we get warnings "onUnmounted is called when there is no active component instance to be associated with. "
147
- // TODO: should we have another cleanup mechanism ?
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
- const asAdmin = async (user) => {
212
- if (user) {
213
- await customFetch(`${options.directoryUrl}/api/auth/asadmin`, { method: 'POST', body: user })
214
- } else {
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
- goTo(null)
218
- }
219
- const cancelDeletion = async () => {
220
- if (state.user == null) { return }
221
- await customFetch(`${options.directoryUrl}/api/users/${state.user.id}`, { method: 'PATCH', body: ({ plannedDeletion: null }) })
222
- readState()
223
- }
224
- const switchLang = (value) => {
225
- const maxAge = 60 * 60 * 24 * 365 // 1 year
226
- cookies.set('i18n_lang', value, { maxAge, path: cookiesPath })
227
- goTo(null)
228
- }
229
- const switchTheme = (value) => {
230
- const maxAge = 60 * 60 * 24 * 365 // 1 year
231
- cookies.set('theme', value, { maxAge, path: cookiesPath })
232
- goTo(null)
233
- }
234
- const keepalive = async () => {
235
- if (state.user == null) { return }
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
- window.localStorage.setItem('sd-keepalive' + options.sitePath, `${new Date().getTime()}`)
238
- }
239
- try {
240
- await customFetch(`${options.directoryUrl}/api/auth/keepalive`, { method: 'POST' })
241
- } catch (err) {
242
- if (err instanceof FetchError && err.statusCode === 401) {
243
- console.warn('session was expired or deleted server side')
244
- } else {
245
- throw err
246
- }
247
- readState()
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
- const refreshSiteInfo = async () => {
251
- const siteInfo = await customFetch(`${options.directoryUrl}/api/sites/_public`) ?? null
252
- if (siteInfo.theme) {
253
- fullSite.value = siteInfo
254
- const partialSite = {
255
- main: siteInfo.main,
256
- logo: siteInfo.theme.logo,
257
- colors: siteInfo.theme.colors
258
- }
259
- if (theme.value == null) { theme.value = getDefaultTheme(siteInfo) }
260
- if (theme.value === 'hc') { partialSite.colors = siteInfo.theme.hcColors }
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
- if (options.siteInfo) { await refreshSiteInfo() }
275
- // immediately performs a keepalive, but only on top windows (not iframes or popups)
276
- // and only if it was not done very recently (maybe from a refreshed page next to this one)
277
- // also run an auto-refresh loop
278
- if (!ssr && !inIframe && !('triggerCapture' in window)) {
279
- const lastKeepalive = window.localStorage.getItem('sd-keepalive' + options.sitePath)
280
- // check cookies.get('id_token') not state.user so that we do a keepalive on expired id tokens
281
- if (cookies.get('id_token') && (!lastKeepalive || (new Date().getTime() - Number(lastKeepalive)) > 10000)) {
282
- await keepalive()
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 refreshLoopDelay = 10 * 60 * 1000 // 10 minutes
285
- setInterval(() => {
286
- const lastKeepalive = window.localStorage.getItem('sd-keepalive' + options.sitePath)
287
- if (!lastKeepalive || (new Date().getTime() - Number(lastKeepalive)) > refreshLoopDelay / 2) {
288
- keepalive().catch(err => console.error(err))
289
- }
290
- }, refreshLoopDelay)
291
- }
292
- const session = {
293
- state,
294
- organization: computed(() => state.organization),
295
- user: computed(() => state.user),
296
- account: computed(() => state.account),
297
- accountRole: computed(() => state.accountRole),
298
- siteRole: computed(() => state.siteRole),
299
- lang: computed(() => state.lang),
300
- theme: readonly(theme),
301
- site: shallowReadonly(site),
302
- fullSite: shallowReadonly(fullSite),
303
- loginUrl,
304
- login,
305
- logout,
306
- switchOrganization,
307
- setAdminMode,
308
- asAdmin,
309
- cancelDeletion,
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 (initOptions) {
322
- const session = await getSession(initOptions)
323
- return {
324
- ...session,
325
- install (app) { app.provide(sessionKey, session) },
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
- const session = inject(sessionKey)
330
- if (!session) { throw new Error('useSession requires using the plugin createSession') }
331
- return session
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 (errorBuilder) {
334
- const session = useSession()
335
- if (!session.state.user) {
336
- if (errorBuilder) { throw errorBuilder() } else { throw new Error('useSessionAuthenticated requires a logged in user') }
337
- }
338
- return session
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;