codevdesign 1.0.66 → 1.0.68

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.
@@ -3,7 +3,8 @@
3
3
  <!-- Affiche la carte récap si activée et qu'il y a des unités -->
4
4
  <div v-if="activerDivPreferences && unites && unites.length > 0">
5
5
  <v-card
6
- width="375"
6
+ max-width="375"
7
+ width="100%"
7
8
  variant="outlined"
8
9
  >
9
10
  <v-card-text class="pt-0 mt-0">
@@ -1,187 +1,229 @@
1
- <template>
2
- <v-app-bar
3
- :color="barreCouleur"
4
- class="px-0 mx-0"
5
- :style="{
6
- position: 'sticky',
7
- // variables CSS pour les couleurs dynamiques
8
- '--entete-texte': texteCouleur,
9
- '--entete-icone': iconeCouleur,
10
- }"
11
- height="82px"
12
- >
13
- <v-row
14
- class="pt-2"
15
- @resize="controlAffichage"
16
- >
17
- <v-col
18
- :cols="titreCol"
19
- class="pr-0 mr-0 pl-5"
20
- >
21
- <v-toolbar-title class="titre">
22
- <div class="entete-ligne">
23
- <!-- GAUCHE -->
24
- <div class="entete-gauche">
25
- <!-- Barre de retour -->
26
- <slot name="retour">
27
- <v-icon
28
- v-if="retour"
29
- size="large"
30
- start
31
- :color="iconeCouleur"
32
- icon="mdi-arrow-left-thin"
33
- @click="retournerMenu"
34
- />
35
- </slot>
36
-
37
- <div class="titre-bloc">
38
- <slot name="titre">
39
- <span class="pl-3 titre-texte">{{ props.titre }}</span>
40
- </slot>
41
-
42
- <slot name="etat">
43
- <span
44
- v-if="monEtat?.afficher"
45
- class="pl-10"
46
- >
47
- <v-btn
48
- size="small"
49
- :color="monEtat.couleur"
50
- variant="tonal"
51
- >
52
- {{ monEtat.texte }}
53
- </v-btn>
54
- </span>
55
- </slot>
56
-
57
- <slot name="etatSecondaire">
58
- <span
59
- v-if="monEtatSecondaire?.afficher"
60
- class="pl-3"
61
- >
62
- <v-btn
63
- size="small"
64
- :color="monEtatSecondaire.couleur"
65
- variant="tonal"
66
- >
67
- {{ monEtatSecondaire.texte }}
68
- </v-btn>
69
- </span>
70
- </slot>
71
-
72
- <slot name="soustitre">
73
- <span class="pl-3 soustitre-texte">{{ props.soustitre }}</span>
74
- </slot>
75
- </div>
76
- </div>
77
-
78
- <!-- DROITE -->
79
- <div class="entete-droite">
80
- <slot name="droite" />
81
- </div>
82
- </div>
83
- </v-toolbar-title>
84
- </v-col>
85
- </v-row>
86
-
87
- <!-- Barre en bas -->
88
- <div style="position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #808a9d" />
89
- </v-app-bar>
90
- </template>
91
-
92
- <script setup lang="ts">
93
- import { useRouter } from 'vue-router'
94
- import { ref, computed } from 'vue'
95
-
96
- interface EnteteEtat {
97
- afficher: boolean
98
- couleur: string
99
- texte: string
100
- }
101
-
102
- interface EnteteEtatSecondaire {
103
- afficher: boolean
104
- couleur: string
105
- texte: string
106
- }
107
-
108
- const router = useRouter()
109
-
110
- const props = defineProps<{
111
- titre: string
112
- soustitre?: string
113
- retour?: string
114
- etat?: EnteteEtat
115
- couleur?: string // couleur de la barre (Vuetify color/hex)
116
- couleurTexte?: string
117
- couleurIcone?: string
118
- etatSecondaire?: EnteteEtatSecondaire
119
- }>()
120
-
121
- const titreCol = ref(12)
122
-
123
- // Fallbacks (tes valeurs actuelles)
124
- const barreCouleur = computed(() => props.couleur ?? 'white')
125
- const texteCouleur = computed(() => props.couleurTexte ?? '#223654')
126
- const iconeCouleur = computed(() => props.couleurIcone ?? 'grisMoyen') // peut être un nom de couleur Vuetify ou un hex
127
-
128
- const monEtat = computed<EnteteEtat>(() => props.etat ?? { afficher: false, couleur: 'primary', texte: 'test' })
129
- const monEtatSecondaire = computed<EnteteEtatSecondaire>(
130
- () => props.etatSecondaire ?? { afficher: false, couleur: 'primary', texte: 'test' },
131
- )
132
-
133
- function retournerMenu() {
134
- if (props.retour) router.push({ name: props.retour })
135
- }
136
- function controlAffichage() {
137
- /* logique resize */
138
- }
139
- </script>
140
-
141
- <style scoped>
142
- .titre {
143
- font-size: 1.85rem;
144
- font-weight: bold;
145
- margin-bottom: 15px;
146
- }
147
- .titre-texte {
148
- color: var(--entete-texte, #223654);
149
- }
150
- .soustitre-texte {
151
- display: block;
152
- font-size: 1rem;
153
- font-weight: normal;
154
- color: var(--entete-texte, #223654);
155
- }
156
- .titre-bloc {
157
- display: inline-block;
158
- vertical-align: middle;
159
- padding-left: 12px;
160
- }
161
- /* Couleur de l’icône (retour) + hover */
162
- .v-icon {
163
- color: var(--entete-icone, #6b7280); /* grisMoyen approx si hex */
164
- }
165
- .v-icon:hover {
166
- filter: brightness(0.85);
167
- }
168
- .entete-ligne {
169
- display: flex;
170
- align-items: center;
171
- width: 100%;
172
- }
173
-
174
- .entete-gauche {
175
- display: flex;
176
- align-items: center;
177
- min-width: 0; /* important pour éviter overflow */
178
- flex: 1;
179
- }
180
-
181
- .entete-droite {
182
- display: flex;
183
- align-items: center;
184
- gap: 12px;
185
- padding-right: 16px;
186
- }
187
- </style>
1
+ <template>
2
+ <v-app-bar
3
+ :color="barreCouleur"
4
+ class="px-0 mx-0"
5
+ :style="{
6
+ position: 'sticky',
7
+ // variables CSS pour les couleurs dynamiques
8
+ '--entete-texte': texteCouleur,
9
+ '--entete-icone': iconeCouleur,
10
+ }"
11
+ height="auto"
12
+ >
13
+ <v-row
14
+ class="pt-2"
15
+ @resize="controlAffichage"
16
+ >
17
+ <v-col
18
+ :cols="titreCol"
19
+ class="pr-0 mr-0 pl-5"
20
+ >
21
+ <v-toolbar-title class="titre">
22
+ <div class="entete-ligne">
23
+ <!-- GAUCHE -->
24
+ <div class="entete-gauche">
25
+ <!-- Barre de retour -->
26
+ <slot name="retour">
27
+ <v-icon
28
+ v-if="retour"
29
+ size="large"
30
+ start
31
+ :color="iconeCouleur"
32
+ icon="mdi-arrow-left-thin"
33
+ @click="retournerMenu"
34
+ />
35
+ </slot>
36
+
37
+ <div class="titre-bloc">
38
+ <slot name="titre">
39
+ <span class="pl-3 titre-texte">{{ props.titre }}</span>
40
+ </slot>
41
+
42
+ <slot name="etat">
43
+ <span
44
+ v-if="monEtat?.afficher"
45
+ class="pl-10"
46
+ >
47
+ <v-btn
48
+ size="small"
49
+ :color="monEtat.couleur"
50
+ variant="tonal"
51
+ >
52
+ {{ monEtat.texte }}
53
+ </v-btn>
54
+ </span>
55
+ </slot>
56
+
57
+ <slot name="etatSecondaire">
58
+ <span
59
+ v-if="monEtatSecondaire?.afficher"
60
+ class="pl-3"
61
+ >
62
+ <v-btn
63
+ size="small"
64
+ :color="monEtatSecondaire.couleur"
65
+ variant="tonal"
66
+ >
67
+ {{ monEtatSecondaire.texte }}
68
+ </v-btn>
69
+ </span>
70
+ </slot>
71
+
72
+ <slot name="soustitre">
73
+ <span class="pl-3 soustitre-texte">{{ props.soustitre }}</span>
74
+ </slot>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- DROITE -->
79
+ <div class="entete-droite mr-1">
80
+ <slot name="droite" />
81
+ </div>
82
+ </div>
83
+ </v-toolbar-title>
84
+ </v-col>
85
+ </v-row>
86
+
87
+ <!-- Barre en bas -->
88
+ <div style="position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #808a9d" />
89
+ </v-app-bar>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { useRouter } from 'vue-router'
94
+ import { ref, computed } from 'vue'
95
+
96
+ interface EnteteEtat {
97
+ afficher: boolean
98
+ couleur: string
99
+ texte: string
100
+ }
101
+
102
+ interface EnteteEtatSecondaire {
103
+ afficher: boolean
104
+ couleur: string
105
+ texte: string
106
+ }
107
+
108
+ const router = useRouter()
109
+
110
+ const props = defineProps<{
111
+ titre: string
112
+ soustitre?: string
113
+ retour?: string
114
+ etat?: EnteteEtat
115
+ couleur?: string // couleur de la barre (Vuetify color/hex)
116
+ couleurTexte?: string
117
+ couleurIcone?: string
118
+ etatSecondaire?: EnteteEtatSecondaire
119
+ }>()
120
+
121
+ const titreCol = ref(12)
122
+
123
+ // Fallbacks (tes valeurs actuelles)
124
+ const barreCouleur = computed(() => props.couleur ?? 'white')
125
+ const texteCouleur = computed(() => props.couleurTexte ?? '#223654')
126
+ const iconeCouleur = computed(() => props.couleurIcone ?? 'grisMoyen') // peut être un nom de couleur Vuetify ou un hex
127
+
128
+ const monEtat = computed<EnteteEtat>(() => props.etat ?? { afficher: false, couleur: 'primary', texte: 'test' })
129
+ const monEtatSecondaire = computed<EnteteEtatSecondaire>(
130
+ () => props.etatSecondaire ?? { afficher: false, couleur: 'primary', texte: 'test' },
131
+ )
132
+
133
+ function retournerMenu() {
134
+ if (props.retour) router.push({ name: props.retour })
135
+ }
136
+ function controlAffichage() {
137
+ /* logique resize */
138
+ }
139
+ </script>
140
+
141
+ <style scoped>
142
+ .titre {
143
+ font-size: 1.85rem;
144
+ font-weight: bold;
145
+ margin-bottom: 15px;
146
+ }
147
+ .titre-texte {
148
+ color: var(--entete-texte, #223654);
149
+ overflow: hidden;
150
+ text-overflow: ellipsis;
151
+ white-space: nowrap;
152
+ max-width: 100%;
153
+ }
154
+ .soustitre-texte {
155
+ display: block;
156
+ font-size: 1rem;
157
+ font-weight: normal;
158
+ color: var(--entete-texte, #223654);
159
+ }
160
+ .titre-bloc {
161
+ display: inline-block;
162
+ vertical-align: middle;
163
+ padding-left: 12px;
164
+ }
165
+ /* Couleur de l’icône (retour) + hover */
166
+ .v-icon {
167
+ color: var(--entete-icone, #6b7280); /* grisMoyen approx si hex */
168
+ }
169
+ .v-icon:hover {
170
+ filter: brightness(0.85);
171
+ }
172
+ .entete-ligne {
173
+ display: flex;
174
+ align-items: center;
175
+ flex-wrap: wrap;
176
+ width: 100%;
177
+ gap: 8px;
178
+ }
179
+
180
+ .entete-gauche {
181
+ display: flex;
182
+ align-items: center;
183
+ min-width: 0; /* important pour éviter overflow */
184
+ flex: 1;
185
+ }
186
+
187
+ .entete-droite {
188
+ display: flex;
189
+ align-items: center;
190
+ gap: 12px;
191
+ padding-right: 16px;
192
+ flex-shrink: 0;
193
+ }
194
+
195
+ @media (max-width: 600px) {
196
+ .entete-gauche {
197
+ width: 100%;
198
+ }
199
+ .entete-droite {
200
+ width: 100%;
201
+ padding-right: 8px;
202
+ padding-left: 0;
203
+ justify-content: flex-end;
204
+ }
205
+ .titre {
206
+ font-size: 1.2rem;
207
+ }
208
+ .titre-bloc {
209
+ display: flex;
210
+ flex-direction: column;
211
+ align-items: flex-start;
212
+ min-width: 0;
213
+ max-width: 100%;
214
+ padding-left: 4px;
215
+ }
216
+ .titre-texte {
217
+ white-space: nowrap;
218
+ overflow: hidden;
219
+ text-overflow: ellipsis;
220
+ max-width: 100%;
221
+ }
222
+ :deep(.v-col) {
223
+ padding-left: 8px !important;
224
+ }
225
+ :deep(.v-toolbar-title__placeholder) {
226
+ padding-inline-start: 0 !important;
227
+ }
228
+ }
229
+ </style>
@@ -1,22 +1,38 @@
1
1
  class RafraichisseurToken {
2
2
  private intervalleEnSecondes = 15
3
3
  private secondesAvantExpirationTokenPourRafraichir = 20
4
- private skewSeconds = 5 // marge anti-derives dhorloge
4
+ private skewSeconds = 5 // marge anti-derives d'horloge
5
5
  private popupAffiche = false
6
6
  private timerId: number | null = null
7
+ private nomTemoin = 'csqc_jeton_secure_expiration'
8
+ private urlPortail = ''
9
+ private refreshPromise: Promise<void> | null = null
7
10
 
8
11
  // Lance une seule fois
9
12
  public async demarrer(nomTemoin: string | null, urlPortail: string): Promise<void> {
10
13
  urlPortail = urlPortail.replace(/\/+$/, '')
11
14
  if (nomTemoin == null || nomTemoin === '') nomTemoin = 'csqc_jeton_secure_expiration'
12
- await this.verifierJeton(nomTemoin, urlPortail)
15
+ this.nomTemoin = nomTemoin as string
16
+ this.urlPortail = urlPortail
17
+ await this.verifierJeton(nomTemoin as string, urlPortail)
13
18
  if (this.timerId != null) return
14
19
  this.timerId = window.setInterval(() => {
15
- this.verifierJeton(nomTemoin, urlPortail)
20
+ this.verifierJeton(nomTemoin as string, urlPortail)
16
21
  }, this.intervalleEnSecondes * 1000)
17
22
  }
18
23
 
19
- // Permet d’arrêter le timer (ex: au logout / destroy)
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)
20
36
  public arreter(): void {
21
37
  if (this.timerId != null) {
22
38
  clearInterval(this.timerId)
@@ -32,8 +48,11 @@ class RafraichisseurToken {
32
48
  if (this.popupAffiche) return
33
49
 
34
50
  if (!this.estJetonValide(nomTemoin)) {
35
- this.rafraichir(nomTemoin, urlPortail)
36
- return
51
+ if (!this.refreshPromise) {
52
+ this.refreshPromise = this.rafraichir(nomTemoin, urlPortail).finally(() => {
53
+ this.refreshPromise = null
54
+ })
55
+ }
37
56
  }
38
57
  }
39
58
 
@@ -54,8 +73,6 @@ class RafraichisseurToken {
54
73
 
55
74
  const exp = Number(token?.exp)
56
75
  if (!Number.isFinite(exp)) {
57
- // exp manquant/invalide → tente refresh
58
-
59
76
  return false
60
77
  }
61
78
 
@@ -70,6 +87,8 @@ class RafraichisseurToken {
70
87
 
71
88
  private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
72
89
  if (!nomCookie) return
90
+ //console.log('[RafraichisseurToken] rafraichir() appelé')
91
+ this.popupAffiche = true // bloquer tout nouveau tick pendant le refresh
73
92
  const url = this.getRefreshUrl(urlPortail)
74
93
  const controller = new AbortController()
75
94
  const timeout = setTimeout(() => controller.abort(), 10_000)
@@ -84,51 +103,73 @@ class RafraichisseurToken {
84
103
  signal: controller.signal,
85
104
  })
86
105
 
87
- // redirection (souvent => login)traiter comme non auth
88
-
106
+ // redirection vers Azure ADsession expirée, l'iframe ne peut pas compléter le flow OIDC cross-site
107
+ //console.log('[RafraichisseurToken] fetch réponse type:', resp.type, 'status:', resp.status)
89
108
  if (resp.type === 'opaqueredirect' || resp.status === 302) {
90
- this.rafraichirParIframe(nomCookie, urlPortail)
109
+ this.estDeconnecteAzure(urlPortail)
91
110
  return
92
111
  }
93
112
 
94
113
  // OK ou No Content: le cookie devrait être réécrit
95
114
  if (resp.status === 200 || resp.status === 204) {
96
115
  const jeton = this.lireCookie(nomCookie)
97
- if (!jeton) this.rafraichirParIframe(nomCookie, urlPortail)
116
+ if (!jeton) {
117
+ // console.log('[RafraichisseurToken] 200 mais cookie absent → iframe')
118
+ this.rafraichirParIframe(nomCookie, urlPortail)
119
+ } else {
120
+ this.popupAffiche = false // succès, reprendre la surveillance
121
+ }
98
122
  return
99
123
  }
100
124
 
101
125
  // non auth / expiré (401, 419) + IIS timeout (440)
102
126
  if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
127
+ // console.log('[RafraichisseurToken] statut auth → iframe')
103
128
  this.rafraichirParIframe(nomCookie, urlPortail)
104
129
  return
105
130
  }
106
131
 
107
132
  console.warn('Rafraichisseur token: statut inattendu', resp.status)
133
+ this.popupAffiche = false // statut inattendu, permettre un retry
108
134
  } catch (err: any) {
109
135
  if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
110
136
  else console.error('Erreur rafraichisseur de token', err)
111
- // on réessaiera au prochain tick
137
+ this.reloadSiErreurReseau()
112
138
  } finally {
113
139
  clearTimeout(timeout)
114
140
  }
115
141
  }
116
142
 
117
143
  private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
118
- // Pour éviter les cross référence, on créé un iframe qui appel portail et force la MAJ du jeton ou l'invalidation du jeton, si jamais l'utilisateur n'est plus connecté
119
- // ajax vers le refresh
144
+ this.popupAffiche = true // bloquer le timer pendant que l'iframe tente l'auth
120
145
  let iframe = document.createElement('iframe')
121
146
  const url = this.getRefreshUrl(urlPortail)
122
147
  iframe.src = `${url}?urlRetour=${encodeURI(window.location.href)}`
123
148
  iframe.id = 'idRafrToken'
124
149
  iframe.style.display = 'none'
125
150
  document.body.appendChild(iframe)
151
+
152
+ const iframeTimeout = setTimeout(() => {
153
+ iframe.onload = null
154
+ iframe.src = 'about:blank'
155
+ iframe.remove()
156
+ this.popupAffiche = false
157
+ console.warn('[RafraichisseurToken] iframe timeout — popupAffiche réinitialisé')
158
+ }, 15_000)
159
+
126
160
  iframe.onload = () => {
127
- const jetonCSQC = this.lireCookie(nomCookie)
128
- if (jetonCSQC == null || jetonCSQC === '') {
161
+ clearTimeout(iframeTimeout)
162
+ iframe.onload = null // empêcher les re-entrées sur les redirects OIDC internes
163
+ const nomSecure = nomCookie.replace('_expiration', '')
164
+ const cookieAVerifier = nomSecure !== nomCookie ? nomSecure : nomCookie
165
+ const jeton = this.lireCookie(cookieAVerifier)
166
+ if (jeton == null || jeton === '') {
129
167
  this.estDeconnecteAzure(urlPortail)
168
+ } else {
169
+ this.popupAffiche = false // cookie recréé avec succès, reprendre la surveillance
130
170
  }
131
171
 
172
+ iframe.src = 'about:blank' // stopper les navigations internes
132
173
  iframe.remove()
133
174
  }
134
175
  }
@@ -176,6 +217,21 @@ class RafraichisseurToken {
176
217
  return JSON.parse(jsonPayload)
177
218
  }
178
219
 
220
+ private reloadSiErreurReseau(): void {
221
+ const CLE = 'rafraichisseur_dernierReload'
222
+ // console.log('[RafraichisseurToken] reload si erreur')
223
+ const maintenant = Date.now()
224
+ const dernierReload = Number(sessionStorage.getItem(CLE) ?? 0)
225
+ if (maintenant - dernierReload > 30_000) {
226
+ // console.log('[RafraichisseurToken] Écriture sessionStorage:', CLE, maintenant)
227
+ sessionStorage.setItem(CLE, String(maintenant))
228
+
229
+ globalThis.location.reload()
230
+ } else {
231
+ this.popupAffiche = false
232
+ }
233
+ }
234
+
179
235
  // URL refresh selon env (dev = proxy Vite ; prod = portail)
180
236
  private getRefreshUrl(urlPortail: string): string {
181
237
  if (import.meta.env.MODE === 'development') return '/portail-refresh'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.66",
3
+ "version": "1.0.68",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",