codevdesign 1.0.71 → 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.
@@ -39,6 +39,7 @@
39
39
  :liste="filteredItems"
40
40
  :chargement-liste="chargementListe"
41
41
  :nom-fichier="excelNomFichier"
42
+ :colonnes="colonnesAffichees"
42
43
  class="mt-1 ml-1 float-right"
43
44
  />
44
45
  <v-icon
@@ -10,28 +10,16 @@
10
10
 
11
11
  <script setup lang="ts">
12
12
  import { utils, writeFileXLSX } from '@e965/xlsx'
13
- import { computed } from 'vue'
13
+ import type Colonne from '../../modeles/composants/datatableColonne'
14
14
 
15
15
  const props = defineProps<{
16
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
17
  liste: any[]
18
18
  nomFichier: string
19
19
  chargementListe: boolean
20
+ colonnes: Colonne[]
20
21
  }>()
21
22
 
22
- // Extraction des clés uniques de tous les objets
23
- const cleDynamique = computed(() => {
24
- const keys: Set<string> = new Set()
25
-
26
- // Parcours tous les objets de la liste et ajoute leurs clés
27
- props.liste.forEach(item => {
28
- if (typeof item === 'object' && item !== null) {
29
- Object.keys(item).forEach(key => keys.add(key))
30
- }
31
- })
32
- return Array.from(keys)
33
- })
34
-
35
23
  // Fonction pour exporter les données en Excel
36
24
  const exportToXLSX = () => {
37
25
  if (!props.liste.length) {
@@ -39,17 +27,17 @@
39
27
  return
40
28
  }
41
29
 
42
- // Récupération des en-têtes depuis la fonction cleDynamique
43
- const headers = cleDynamique.value
30
+ // Colonnes affichées (on exclut action et ancre)
31
+ const colonnesExport = props.colonnes.filter(c => c.key && c.key !== 'action' && c.key !== 'ancre')
32
+
33
+ const headers = colonnesExport.map(c => c.title ?? String(c.key))
34
+ const keys = colonnesExport.map(c => String(c.key))
44
35
 
45
- // Construction des lignes en respectant l'ordre des en-têtes
46
36
  const rows = props.liste.map(item => {
47
- return headers.map(key => (key in item ? item[key] : '')) // Valeur ou vide si absente
37
+ return keys.map(key => (key in item ? item[key] : ''))
48
38
  })
49
39
 
50
- // Création des données Excel (en-têtes + lignes)
51
40
  const data = [headers, ...rows]
52
- // Génération du fichier Excel
53
41
  const ws = utils.aoa_to_sheet(data)
54
42
  const wb = utils.book_new()
55
43
  utils.book_append_sheet(wb, ws, '1')
@@ -1,245 +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
- 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
+
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.71",
3
+ "version": "1.0.74",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",