codevdesign 1.0.72 → 1.0.75

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