codevdesign 1.0.69 → 1.0.70

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