codevdesign 1.0.69 → 1.0.71

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.
@@ -458,7 +458,7 @@
458
458
  val => {
459
459
  listeInterne.value = [...val]
460
460
  },
461
- { immediate: true },
461
+ { immediate: true, deep: true },
462
462
  )
463
463
 
464
464
  // Init/destroy Sortable si la prop change en cours de vie du composant
@@ -1,156 +1,195 @@
1
- <template>
2
- <v-navigation-drawer
3
- v-model="visible"
4
- location="right"
5
- temporary
6
- v-bind="$attrs"
7
- :width="grosseurTiroir"
8
- class="pa-0 elevation-2 csqc-ligneBleue"
9
- :persistent="persistant"
10
- @keydown.esc="!persistant ? fermeture : ''"
11
- @click:outside="!persistant ? fermeture : ''"
12
- >
13
- <v-card class="pa-0 ma-0 pl-8 pt-8">
14
- <!-- Bouton en haut à droite -->
15
- <v-btn
16
- icon="mdi-close"
17
- variant="text"
18
- class="position-absolute iconeHover"
19
- style="top: 5px; right: 5px"
20
- @click="fermeture"
21
- ></v-btn>
22
-
23
- <v-card-title
24
- class="pa-0 ma-0 pb-6 text-wrap"
25
- style="font-size: 24px"
26
- >
27
- <slot name="titre"></slot>
28
- <div text-h5>{{ titre }}</div>
29
- </v-card-title>
30
-
31
- <v-card-text class="pa-0 ma-0 pb-6 pr-6">
32
- <v-container>
33
- <slot></slot>
34
- <slot name="content"></slot>
35
- </v-container>
36
- </v-card-text>
37
- <v-card-actions class="my-2 d-flex justify-end pr-6 pb-5">
38
- <slot name="actions"></slot>
39
- <v-btn
40
- v-if="btnAnnuler"
41
- color="primary"
42
- variant="text"
43
- :loading="operationEnCours"
44
- @click="fermeture"
45
- >
46
- {{ props.btnAnnulerTexte ? props.btnAnnulerTexte : $t('csqc.bouton.annuler') }}
47
- </v-btn>
48
-
49
- <v-btn
50
- v-if="btnOk"
51
- class="Gouttiere"
52
- color="primary"
53
- variant="flat"
54
- :loading="operationEnCours"
55
- :disabled="btnOkDesactiver || operationEnCours"
56
- @click="okBouton"
57
- >
58
- {{ props.btnOkTexte ? props.btnOkTexte : $t('csqc.bouton.ok') }}
59
- </v-btn>
60
- </v-card-actions>
61
- </v-card>
62
- </v-navigation-drawer>
63
- </template>
64
- <script lang="ts" setup>
65
- import { ref, computed } from 'vue'
66
- import { useDisplay } from 'vuetify'
67
-
68
- const visible = ref(false)
69
- const display = useDisplay()
70
-
71
- // Déclaration des props
72
- const props = defineProps({
73
- titre: {
74
- type: String,
75
- default: '',
76
- required: false,
77
- },
78
- btnAnnuler: {
79
- type: Boolean,
80
- default: true,
81
- required: false,
82
- },
83
- btnOk: {
84
- type: Boolean,
85
- default: true,
86
- required: false,
87
- },
88
- btnAnnulerTexte: {
89
- type: String,
90
- default: '',
91
- required: false,
92
- },
93
- btnOkDesactiver: {
94
- type: Boolean,
95
- default: false,
96
- required: false,
97
- },
98
- operationEnCours: { type: Boolean, default: false },
99
- persistant: { type: Boolean, default: true },
100
- btnOkTexte: {
101
- type: String,
102
- default: '',
103
- required: false,
104
- },
105
- })
106
-
107
- // Déclaration des événements
108
- const emit = defineEmits(['fermer', 'ok'])
109
-
110
- // Méthodes
111
- const ouvrir = () => {
112
- visible.value = true
113
- }
114
-
115
- const fermer = () => {
116
- visible.value = false
117
- }
118
-
119
- const fermeture = () => {
120
- emit('fermer')
121
- fermer()
122
- }
123
-
124
- const okBouton = () => {
125
- emit('ok')
126
- fermer()
127
- }
128
-
129
- // Calcul des tailles du tiroir en fonction du breakpoint
130
- const grosseurTiroir = computed(() => {
131
- switch (display.name.value) {
132
- case 'xs':
133
- return ''
134
- case 'sm':
135
- return 0.95 * display.width.value
136
- case 'md':
137
- return 0.8 * display.width.value
138
- case 'lg':
139
- return 0.7 * display.width.value
140
- case 'xl':
141
- return 0.6 * display.width.value
142
- case 'xxl':
143
- return 0.5 * display.width.value
144
- default:
145
- return ''
146
- }
147
- })
148
-
149
- defineExpose({ ouvrir, fermer })
150
- </script>
151
-
152
- <style>
153
- .csqc-ligneBleue {
154
- border-left: 30px #095797 solid !important;
155
- }
156
- </style>
1
+ <template>
2
+ <v-navigation-drawer
3
+ v-model="visible"
4
+ location="right"
5
+ temporary
6
+ v-bind="$attrs"
7
+ :width="grosseurTiroir"
8
+ class="pa-0 elevation-2 csqc-ligneBleue"
9
+ :persistent="persistant"
10
+ @keydown.esc="!persistant ? fermeture : ''"
11
+ @click:outside="!persistant ? fermeture : ''"
12
+ >
13
+ <v-card class="pa-0 ma-0 pl-8 pt-8">
14
+ <!-- Bouton en haut à droite -->
15
+ <v-btn
16
+ icon="mdi-close"
17
+ variant="text"
18
+ class="position-absolute iconeHover"
19
+ style="top: 5px; right: 5px"
20
+ @click="fermeture"
21
+ ></v-btn>
22
+
23
+ <v-card-title
24
+ class="pa-0 ma-0 pb-6 text-wrap"
25
+ style="font-size: 24px"
26
+ >
27
+ <slot name="titre"></slot>
28
+ <div text-h5>{{ titre }}</div>
29
+ </v-card-title>
30
+
31
+ <v-card-text class="pa-0 ma-0 pb-6 pr-6">
32
+ <v-container>
33
+ <slot></slot>
34
+ <slot name="content"></slot>
35
+ </v-container>
36
+ </v-card-text>
37
+ <v-card-actions class="my-2 d-flex justify-end pr-6 pb-5">
38
+ <slot name="actions"></slot>
39
+ <v-btn
40
+ v-if="btnAnnuler"
41
+ color="primary"
42
+ variant="text"
43
+ :loading="operationEnCours"
44
+ @click="fermeture"
45
+ >
46
+ {{ props.btnAnnulerTexte ? props.btnAnnulerTexte : $t('csqc.bouton.annuler') }}
47
+ </v-btn>
48
+
49
+ <v-btn
50
+ v-if="btnOk"
51
+ class="Gouttiere"
52
+ color="primary"
53
+ variant="flat"
54
+ :loading="operationEnCours"
55
+ :disabled="btnOkDesactiver || operationEnCours"
56
+ @click="okBouton"
57
+ >
58
+ {{ props.btnOkTexte ? props.btnOkTexte : $t('csqc.bouton.ok') }}
59
+ </v-btn>
60
+ </v-card-actions>
61
+ </v-card>
62
+ </v-navigation-drawer>
63
+ <Teleport to="body">
64
+ <v-fab
65
+ v-if="retourEnHaut && afficherBoutonHaut"
66
+ style="position: fixed; bottom: 24px; right: 24px; z-index: 2000"
67
+ elevation="8"
68
+ color="primary"
69
+ icon="mdi-arrow-up"
70
+ @click="allerEnHaut"
71
+ />
72
+ </Teleport>
73
+ </template>
74
+ <script lang="ts" setup>
75
+ import { ref, computed, watch, nextTick } from 'vue'
76
+ import { useDisplay } from 'vuetify'
77
+
78
+ const visible = ref(false)
79
+ const display = useDisplay()
80
+
81
+ // Déclaration des props
82
+ const props = defineProps({
83
+ titre: {
84
+ type: String,
85
+ default: '',
86
+ required: false,
87
+ },
88
+ btnAnnuler: {
89
+ type: Boolean,
90
+ default: true,
91
+ required: false,
92
+ },
93
+ btnOk: {
94
+ type: Boolean,
95
+ default: true,
96
+ required: false,
97
+ },
98
+ btnAnnulerTexte: {
99
+ type: String,
100
+ default: '',
101
+ required: false,
102
+ },
103
+ btnOkDesactiver: {
104
+ type: Boolean,
105
+ default: false,
106
+ required: false,
107
+ },
108
+ operationEnCours: { type: Boolean, default: false },
109
+ persistant: { type: Boolean, default: true },
110
+ btnOkTexte: {
111
+ type: String,
112
+ default: '',
113
+ required: false,
114
+ },
115
+ retourEnHaut: {
116
+ type: Boolean,
117
+ default: true,
118
+ },
119
+ })
120
+
121
+ // Déclaration des événements
122
+ const emit = defineEmits(['fermer', 'ok'])
123
+
124
+ // Back to top
125
+ const afficherBoutonHaut = ref(false)
126
+ let conteneurScroll: Element | null = null
127
+
128
+ function onScroll() {
129
+ afficherBoutonHaut.value = (conteneurScroll?.scrollTop ?? 0) > 350
130
+ }
131
+
132
+ function allerEnHaut() {
133
+ conteneurScroll?.scrollTo({ top: 0, behavior: 'smooth' })
134
+ }
135
+
136
+ watch(visible, async val => {
137
+ if (!props.retourEnHaut) return
138
+ if (val) {
139
+ await nextTick()
140
+ conteneurScroll = document.querySelector('.v-navigation-drawer--active .v-navigation-drawer__content')
141
+ conteneurScroll?.addEventListener('scroll', onScroll)
142
+ } else {
143
+ conteneurScroll?.removeEventListener('scroll', onScroll)
144
+ conteneurScroll = null
145
+ afficherBoutonHaut.value = false
146
+ }
147
+ })
148
+
149
+ // Méthodes
150
+ const ouvrir = () => {
151
+ visible.value = true
152
+ }
153
+
154
+ const fermer = () => {
155
+ visible.value = false
156
+ }
157
+
158
+ const fermeture = () => {
159
+ emit('fermer')
160
+ fermer()
161
+ }
162
+
163
+ const okBouton = () => {
164
+ emit('ok')
165
+ fermer()
166
+ }
167
+
168
+ // Calcul des tailles du tiroir en fonction du breakpoint
169
+ const grosseurTiroir = computed(() => {
170
+ switch (display.name.value) {
171
+ case 'xs':
172
+ return ''
173
+ case 'sm':
174
+ return 0.95 * display.width.value
175
+ case 'md':
176
+ return 0.8 * display.width.value
177
+ case 'lg':
178
+ return 0.7 * display.width.value
179
+ case 'xl':
180
+ return 0.6 * display.width.value
181
+ case 'xxl':
182
+ return 0.5 * display.width.value
183
+ default:
184
+ return ''
185
+ }
186
+ })
187
+
188
+ defineExpose({ ouvrir, fermer })
189
+ </script>
190
+
191
+ <style>
192
+ .csqc-ligneBleue {
193
+ border-left: 30px #095797 solid !important;
194
+ }
195
+ </style>
@@ -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.71",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",