codevdesign 1.0.72 → 1.0.74

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.
@@ -1,251 +1,247 @@
1
- class RafraichisseurToken {
2
- private intervalleEnSecondes = 15
3
- private secondesAvantExpirationTokenPourRafraichir = 20
4
- private skewSeconds = 5 // marge anti-derives d'horloge
5
- private popupAffiche = false
6
- private timerId: number | null = null
7
- private nomTemoin = 'csqc_jeton_secure_expiration'
8
- private urlPortail = ''
9
- private refreshPromise: Promise<void> | null = null
10
-
11
- // Lance une seule fois
12
- public async demarrer(nomTemoin: string | null, urlPortail: string): Promise<void> {
13
- urlPortail = urlPortail.replace(/\/+$/, '')
14
- if (nomTemoin == null || nomTemoin === '') nomTemoin = 'csqc_jeton_secure_expiration'
15
- this.nomTemoin = nomTemoin as string
16
- this.urlPortail = urlPortail
17
- await this.verifierJeton(nomTemoin as string, urlPortail)
18
- if (this.timerId != null) return
19
- this.timerId = window.setInterval(() => {
20
- this.verifierJeton(nomTemoin as string, urlPortail)
21
- }, this.intervalleEnSecondes * 1000)
22
- }
23
-
24
- public attendreRefreshSiNecessaire(force = false): Promise<void> {
25
- if (this.refreshPromise) return this.refreshPromise
26
- if (force || !this.estJetonValide(this.nomTemoin)) {
27
- this.refreshPromise = this.rafraichir(this.nomTemoin, this.urlPortail).finally(() => {
28
- this.refreshPromise = null
29
- })
30
- return this.refreshPromise
31
- }
32
- return Promise.resolve()
33
- }
34
-
35
- // Permet d'arrêter le timer (ex: au logout / destroy)
36
- public arreter(): void {
37
- if (this.timerId != null) {
38
- clearInterval(this.timerId)
39
- this.timerId = null
40
- }
41
- }
42
-
43
- public existeJeton = (nomTemoin: string) => {
44
- return this.estJetonValide(nomTemoin)
45
- }
46
-
47
- private async verifierJeton(nomTemoin: string, urlPortail: string): Promise<void> {
48
- if (this.popupAffiche) return
49
-
50
- if (!this.estJetonValide(nomTemoin)) {
51
- if (!this.refreshPromise) {
52
- this.refreshPromise = this.rafraichir(nomTemoin, urlPortail).finally(() => {
53
- this.refreshPromise = null
54
- })
55
- }
56
- }
57
- }
58
-
59
- private estJetonValide(nomTemoin: string): boolean {
60
- if (this.popupAffiche || !nomTemoin) return true //On fait semblant que c'est valide pour ne pas provoquer un autre affichage du popup.
61
-
62
- // Vérifier aussi le cookie principal (csqc_jeton_secure) en plus du sentinel d'expiration
63
- const nomPrincipal = nomTemoin.replace('_expiration', '')
64
- if (nomPrincipal !== nomTemoin && !this.lireCookie(nomPrincipal)) {
65
- return false
66
- }
67
-
68
- const tokenEncode = this.lireCookie(nomTemoin)
69
- if (!tokenEncode) {
70
- return false
71
- }
72
-
73
- let token: any
74
- try {
75
- token = this.parseJwt(tokenEncode)
76
- } catch {
77
- return false
78
- }
79
-
80
- const exp = Number(token?.exp)
81
- if (!Number.isFinite(exp)) {
82
- return false
83
- }
84
-
85
- const now = Math.floor(Date.now() / 1000)
86
- const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
87
- if (now >= refreshAt) {
88
- return false
89
- }
90
-
91
- return true
92
- }
93
-
94
- private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
95
- if (!nomCookie) return
96
- //console.log('[RafraichisseurToken] rafraichir() appelé')
97
- if (urlPortail == null || urlPortail == '') return
98
- this.popupAffiche = true // bloquer tout nouveau tick pendant le refresh
99
- const url = this.getRefreshUrl(urlPortail)
100
- const controller = new AbortController()
101
- const timeout = setTimeout(() => controller.abort(), 10_000)
102
-
103
- try {
104
- //Première tentative sans iframe, pour la majorité des cas.
105
- const resp = await fetch(url, {
106
- method: 'POST',
107
- credentials: 'include',
108
- headers: { Accept: 'application/json' },
109
- redirect: 'manual',
110
- signal: controller.signal,
111
- })
112
-
113
- // redirection vers Azure AD → session expirée, l'iframe ne peut pas compléter le flow OIDC cross-site
114
- //console.log('[RafraichisseurToken] fetch réponse type:', resp.type, 'status:', resp.status)
115
- if (resp.type === 'opaqueredirect' || resp.status === 302) {
116
- this.estDeconnecteAzure(urlPortail)
117
- return
118
- }
119
-
120
- // OK ou No Content: le cookie devrait être réécrit
121
- if (resp.status === 200 || resp.status === 204) {
122
- const jeton = this.lireCookie(nomCookie)
123
- if (!jeton) {
124
- // console.log('[RafraichisseurToken] 200 mais cookie absent → iframe')
125
- this.rafraichirParIframe(nomCookie, urlPortail)
126
- } else {
127
- this.popupAffiche = false // succès, reprendre la surveillance
128
- }
129
- return
130
- }
131
-
132
- // non auth / expiré (401, 419) + IIS timeout (440)
133
- if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
134
- // console.log('[RafraichisseurToken] statut auth → iframe')
135
- this.rafraichirParIframe(nomCookie, urlPortail)
136
- return
137
- }
138
-
139
- console.warn('Rafraichisseur token: statut inattendu', resp.status)
140
- this.popupAffiche = false // statut inattendu, permettre un retry
141
- } catch (err: any) {
142
- if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
143
- else console.error('Erreur rafraichisseur de token', err)
144
- this.reloadSiErreurReseau()
145
- } finally {
146
- clearTimeout(timeout)
147
- }
148
- }
149
-
150
- private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
151
- this.popupAffiche = true // bloquer le timer pendant que l'iframe tente l'auth
152
- let iframe = document.createElement('iframe')
153
- const url = this.getRefreshUrl(urlPortail)
154
- iframe.src = `${url}?urlRetour=${encodeURI(window.location.href)}`
155
- iframe.id = 'idRafrToken'
156
- iframe.style.display = 'none'
157
- document.body.appendChild(iframe)
158
-
159
- const iframeTimeout = setTimeout(() => {
160
- iframe.onload = null
161
- iframe.src = 'about:blank'
162
- iframe.remove()
163
- this.popupAffiche = false
164
- console.warn('[RafraichisseurToken] iframe timeout — popupAffiche réinitialisé')
165
- }, 15_000)
166
-
167
- iframe.onload = () => {
168
- clearTimeout(iframeTimeout)
169
- iframe.onload = null // empêcher les re-entrées sur les redirects OIDC internes
170
- const nomSecure = nomCookie.replace('_expiration', '')
171
- const cookieAVerifier = nomSecure !== nomCookie ? nomSecure : nomCookie
172
- const jeton = this.lireCookie(cookieAVerifier)
173
- if (jeton == null || jeton === '') {
174
- this.estDeconnecteAzure(urlPortail)
175
- } else {
176
- this.popupAffiche = false // cookie recréé avec succès, reprendre la surveillance
177
- }
178
-
179
- iframe.src = 'about:blank' // stopper les navigations internes
180
- iframe.remove()
181
- }
182
- }
183
-
184
- private estDeconnecteAzure(urlPortail: string): void {
185
- //on envoie au portail, pas le choix
186
- urlPortail = urlPortail.replace(/\/+$/, '')
187
- const retour = encodeURI(window.location.href)
188
- window.open(`${urlPortail}/home/SeConnecter?urlRetour=${retour}`, '_self')
189
- return
190
- }
191
-
192
- public deconnecterPortail(urlPortail: string): void {
193
- urlPortail = urlPortail.replace(/\/+$/, '')
194
- window.location.replace(`${urlPortail}/deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
195
- }
196
-
197
- private lireCookie(nom: string): string | null {
198
- if (!document.cookie) return null
199
- const cookies = document.cookie.split(';').map(c => c.trim())
200
- for (const cookie of cookies) {
201
- if (cookie.startsWith(`${nom}=`)) {
202
- try {
203
- return decodeURIComponent(cookie.substring(nom.length + 1))
204
- } catch {
205
- return cookie.substring(nom.length + 1)
206
- }
207
- }
208
- }
209
- return null
210
- }
211
-
212
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
213
- private parseJwt(token: string): any {
214
- const parts = token.split('.')
215
- const base64Url = parts[1]
216
- if (!base64Url) throw new Error('Invalid JWT format')
217
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
218
- const jsonPayload = decodeURIComponent(
219
- atob(base64)
220
- .split('')
221
- .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
222
- .join(''),
223
- )
224
- return JSON.parse(jsonPayload)
225
- }
226
-
227
- private reloadSiErreurReseau(): void {
228
- const CLE = 'rafraichisseur_dernierReload'
229
- // console.log('[RafraichisseurToken] reload si erreur')
230
- const maintenant = Date.now()
231
- const dernierReload = Number(sessionStorage.getItem(CLE) ?? 0)
232
- if (maintenant - dernierReload > 30_000) {
233
- // console.log('[RafraichisseurToken] Écriture sessionStorage:', CLE, maintenant)
234
- sessionStorage.setItem(CLE, String(maintenant))
235
-
236
- globalThis.location.reload()
237
- } else {
238
- this.popupAffiche = false
239
- }
240
- }
241
-
242
- // URL refresh selon env (dev = proxy Vite ; prod = portail)
243
- private getRefreshUrl(urlPortail: string): string {
244
- if (import.meta.env.MODE === 'development') return '/portail-refresh'
245
- return urlPortail + '/home/Refresh'
246
- }
247
- }
248
-
249
- // Instance
250
- const rafraichisseurToken = new RafraichisseurToken()
251
- export default rafraichisseurToken
1
+ class RafraichisseurToken {
2
+ private intervalleEnSecondes = 15
3
+ private secondesAvantExpirationTokenPourRafraichir = 20
4
+ private skewSeconds = 5 // marge anti-derives d'horloge
5
+ private popupAffiche = false
6
+ private timerId: number | null = null
7
+ private nomTemoin = 'csqc_jeton_secure_expiration'
8
+ private urlPortail = ''
9
+ private refreshPromise: Promise<void> | null = null
10
+
11
+ // Lance une seule fois
12
+ public async demarrer(nomTemoin: string | null, urlPortail: string): Promise<void> {
13
+ urlPortail = urlPortail.replace(/\/+$/, '')
14
+ if (nomTemoin == null || nomTemoin === '') nomTemoin = 'csqc_jeton_secure_expiration'
15
+ this.nomTemoin = nomTemoin as string
16
+ this.urlPortail = urlPortail
17
+ await this.verifierJeton(nomTemoin as string, urlPortail)
18
+ if (this.timerId != null) return
19
+ this.timerId = window.setInterval(() => {
20
+ this.verifierJeton(nomTemoin as string, urlPortail)
21
+ }, this.intervalleEnSecondes * 1000)
22
+ }
23
+
24
+ public attendreRefreshSiNecessaire(force = false): Promise<void> {
25
+ if (this.refreshPromise) return this.refreshPromise
26
+ if (force || !this.estJetonValide(this.nomTemoin)) {
27
+ this.refreshPromise = this.rafraichir(this.nomTemoin, this.urlPortail).finally(() => {
28
+ this.refreshPromise = null
29
+ })
30
+ return this.refreshPromise
31
+ }
32
+ return Promise.resolve()
33
+ }
34
+
35
+ // Permet d'arrêter le timer (ex: au logout / destroy)
36
+ public arreter(): void {
37
+ if (this.timerId != null) {
38
+ clearInterval(this.timerId)
39
+ this.timerId = null
40
+ }
41
+ }
42
+
43
+ public existeJeton = (nomTemoin: string) => {
44
+ return this.estJetonValide(nomTemoin)
45
+ }
46
+
47
+ private async verifierJeton(nomTemoin: string, urlPortail: string): Promise<void> {
48
+ if (this.popupAffiche) return
49
+
50
+ if (!this.estJetonValide(nomTemoin)) {
51
+ if (!this.refreshPromise) {
52
+ this.refreshPromise = this.rafraichir(nomTemoin, urlPortail).finally(() => {
53
+ this.refreshPromise = null
54
+ })
55
+ }
56
+ }
57
+ }
58
+
59
+ private estJetonValide(nomTemoin: string): boolean {
60
+ if (this.popupAffiche || !nomTemoin) return true //On fait semblant que c'est valide pour ne pas provoquer un autre affichage du popup.
61
+
62
+
63
+
64
+ const tokenEncode = this.lireCookie(nomTemoin)
65
+ if (!tokenEncode) {
66
+ return false
67
+ }
68
+
69
+ let token: any
70
+ try {
71
+ token = this.parseJwt(tokenEncode)
72
+ } catch {
73
+ return false
74
+ }
75
+
76
+ const exp = Number(token?.exp)
77
+ if (!Number.isFinite(exp)) {
78
+ return false
79
+ }
80
+
81
+ const now = Math.floor(Date.now() / 1000)
82
+ const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
83
+ if (now >= refreshAt) {
84
+ return false
85
+ }
86
+
87
+ return true
88
+ }
89
+
90
+ private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
91
+ if (!nomCookie) return
92
+ //console.log('[RafraichisseurToken] rafraichir() appelé')
93
+ if (urlPortail == null || urlPortail == '') return
94
+ this.popupAffiche = true // bloquer tout nouveau tick pendant le refresh
95
+ const url = this.getRefreshUrl(urlPortail)
96
+ const controller = new AbortController()
97
+ const timeout = setTimeout(() => controller.abort(), 10_000)
98
+
99
+ try {
100
+ //Première tentative sans iframe, pour la majorité des cas.
101
+ const resp = await fetch(url, {
102
+ method: 'POST',
103
+ credentials: 'include',
104
+ headers: { Accept: 'application/json' },
105
+ redirect: 'manual',
106
+ signal: controller.signal,
107
+ })
108
+
109
+ // redirection vers Azure AD → session expirée, l'iframe ne peut pas compléter le flow OIDC cross-site
110
+ //console.log('[RafraichisseurToken] fetch réponse type:', resp.type, 'status:', resp.status)
111
+ if (resp.type === 'opaqueredirect' || resp.status === 302) {
112
+ this.estDeconnecteAzure(urlPortail)
113
+ return
114
+ }
115
+
116
+ // OK ou No Content: le cookie devrait être réécrit
117
+ if (resp.status === 200 || resp.status === 204) {
118
+ const jeton = this.lireCookie(nomCookie)
119
+ if (!jeton) {
120
+ // console.log('[RafraichisseurToken] 200 mais cookie absent iframe')
121
+ this.rafraichirParIframe(nomCookie, urlPortail)
122
+ } else {
123
+ this.popupAffiche = false // succès, reprendre la surveillance
124
+ }
125
+ return
126
+ }
127
+
128
+ // non auth / expiré (401, 419) + IIS timeout (440)
129
+ if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
130
+ // console.log('[RafraichisseurToken] statut auth → iframe')
131
+ this.rafraichirParIframe(nomCookie, urlPortail)
132
+ return
133
+ }
134
+
135
+ console.warn('Rafraichisseur token: statut inattendu', resp.status)
136
+ this.popupAffiche = false // statut inattendu, permettre un retry
137
+ } catch (err: any) {
138
+ if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
139
+ else console.error('Erreur rafraichisseur de token', err)
140
+ this.reloadSiErreurReseau()
141
+ } finally {
142
+ clearTimeout(timeout)
143
+ }
144
+ }
145
+
146
+ private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
147
+ this.popupAffiche = true // bloquer le timer pendant que l'iframe tente l'auth
148
+ let iframe = document.createElement('iframe')
149
+ const url = this.getRefreshUrl(urlPortail)
150
+ iframe.src = `${url}?urlRetour=${encodeURI(window.location.href)}`
151
+ iframe.id = 'idRafrToken'
152
+ iframe.style.display = 'none'
153
+ document.body.appendChild(iframe)
154
+
155
+ const iframeTimeout = setTimeout(() => {
156
+ iframe.onload = null
157
+ iframe.src = 'about:blank'
158
+ iframe.remove()
159
+ this.popupAffiche = false
160
+ console.warn('[RafraichisseurToken] iframe timeout — popupAffiche réinitialisé')
161
+ }, 15_000)
162
+
163
+ iframe.onload = () => {
164
+ clearTimeout(iframeTimeout)
165
+ iframe.onload = null // empêcher les re-entrées sur les redirects OIDC internes
166
+ const nomSecure = nomCookie.replace('_expiration', '')
167
+ const cookieAVerifier = nomSecure !== nomCookie ? nomSecure : nomCookie
168
+ const jeton = this.lireCookie(cookieAVerifier)
169
+ if (jeton == null || jeton === '') {
170
+ this.estDeconnecteAzure(urlPortail)
171
+ } else {
172
+ this.popupAffiche = false // cookie recréé avec succès, reprendre la surveillance
173
+ }
174
+
175
+ iframe.src = 'about:blank' // stopper les navigations internes
176
+ iframe.remove()
177
+ }
178
+ }
179
+
180
+ private estDeconnecteAzure(urlPortail: string): void {
181
+ //on envoie au portail, pas le choix
182
+ urlPortail = urlPortail.replace(/\/+$/, '')
183
+ const retour = encodeURI(window.location.href)
184
+ window.open(`${urlPortail}/home/SeConnecter?urlRetour=${retour}`, '_self')
185
+ return
186
+ }
187
+
188
+ public deconnecterPortail(urlPortail: string): void {
189
+ urlPortail = urlPortail.replace(/\/+$/, '')
190
+ window.location.replace(`${urlPortail}/deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
191
+ }
192
+
193
+ private lireCookie(nom: string): string | null {
194
+ if (!document.cookie) return null
195
+ const cookies = document.cookie.split(';').map(c => c.trim())
196
+ for (const cookie of cookies) {
197
+ if (cookie.startsWith(`${nom}=`)) {
198
+ try {
199
+ return decodeURIComponent(cookie.substring(nom.length + 1))
200
+ } catch {
201
+ return cookie.substring(nom.length + 1)
202
+ }
203
+ }
204
+ }
205
+ return null
206
+ }
207
+
208
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
+ private parseJwt(token: string): any {
210
+ const parts = token.split('.')
211
+ const base64Url = parts[1]
212
+ if (!base64Url) throw new Error('Invalid JWT format')
213
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
214
+ const jsonPayload = decodeURIComponent(
215
+ atob(base64)
216
+ .split('')
217
+ .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
218
+ .join(''),
219
+ )
220
+ return JSON.parse(jsonPayload)
221
+ }
222
+
223
+ private reloadSiErreurReseau(): void {
224
+ const CLE = 'rafraichisseur_dernierReload'
225
+ // console.log('[RafraichisseurToken] reload si erreur')
226
+ const maintenant = Date.now()
227
+ const dernierReload = Number(sessionStorage.getItem(CLE) ?? 0)
228
+ if (maintenant - dernierReload > 30_000) {
229
+ // console.log('[RafraichisseurToken] Écriture sessionStorage:', CLE, maintenant)
230
+ sessionStorage.setItem(CLE, String(maintenant))
231
+
232
+ globalThis.location.reload()
233
+ } else {
234
+ this.popupAffiche = false
235
+ }
236
+ }
237
+
238
+ // URL refresh selon env (dev = proxy Vite ; prod = portail)
239
+ private getRefreshUrl(urlPortail: string): string {
240
+ if (import.meta.env.MODE === 'development') return '/portail-refresh'
241
+ return urlPortail + '/home/Refresh'
242
+ }
243
+ }
244
+
245
+ // Instance
246
+ const rafraichisseurToken = new RafraichisseurToken()
247
+ export default rafraichisseurToken
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.72",
3
+ "version": "1.0.74",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",