codevdesign 1.0.22 → 1.0.24

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.
@@ -7,7 +7,10 @@
7
7
  >
8
8
  <v-combobox
9
9
  v-model="codeBudgetaire"
10
- :items="codeBudgetairesProp"
10
+ :items="itemsCombobox"
11
+ item-value="code"
12
+ item-title="code"
13
+ :return-object="false"
11
14
  v-bind="$attrs"
12
15
  persistent-hint
13
16
  variant="outlined"
@@ -20,7 +23,15 @@
20
23
  @keydown="caractereAutorises"
21
24
  @update:modelValue="gererChangement"
22
25
  @paste="gererPaste"
23
- />
26
+ >
27
+ <template #item="{ props, item }">
28
+ <v-list-item
29
+ v-bind="props"
30
+ :title="item.raw.nom || item.raw.code"
31
+ :subtitle="item.raw.nom ? item.raw.code : undefined"
32
+ />
33
+ </template>
34
+ </v-combobox>
24
35
  </v-form>
25
36
  </div>
26
37
  </template>
@@ -31,13 +42,14 @@
31
42
 
32
43
  const emit = defineEmits<{
33
44
  'update:modelValue': [string | null]
34
- 'update:codeBudgetairesProp': [string[]]
35
- 'update:valide': [boolean] // 👈 nouveau
45
+ 'update:codeBudgetairesProp': [CodeBudgetaireItem[]]
46
+ 'update:valide': [boolean]
36
47
  }>()
48
+ type CodeBudgetaireItem = string | [string, string]
37
49
 
38
50
  const props = withDefaults(
39
51
  defineProps<{
40
- codeBudgetairesProp: string[]
52
+ codeBudgetairesProp: CodeBudgetaireItem[]
41
53
  modelValue: string | null
42
54
  afficherHint?: boolean
43
55
  regleMessageErreur: string
@@ -64,6 +76,22 @@
64
76
  derniereValeurSauvegardee.value = codeBudgetaire.value
65
77
  })
66
78
 
79
+ const itemsCombobox = computed(() => {
80
+ return props.codeBudgetairesProp.map(item => {
81
+ if (typeof item === 'string') {
82
+ return {
83
+ code: item,
84
+ nom: '', // pas de nom
85
+ }
86
+ }
87
+
88
+ const [code, nomBrut] = item
89
+ const nom = (nomBrut ?? '').toString().trim()
90
+
91
+ return { code, nom }
92
+ })
93
+ })
94
+
67
95
  const placeholder = computed(() => {
68
96
  const base = format.replace(/9/g, '0')
69
97
  const extension = activerExtension ? '-XXX/XXX' : ''
@@ -246,13 +274,15 @@
246
274
  const sauvegarder = () => {
247
275
  codeBudgetaire.value = formaterCodeBudgetaire(codeBudgetaire.value)
248
276
 
249
- // on utilise la validité globale
250
277
  if (!estValideComplet.value) return
251
278
  if (codeBudgetaire.value === derniereValeurSauvegardee.value) return
252
279
 
253
- const existe = props.codeBudgetairesProp.some(
254
- item => item.trim().toUpperCase() === codeBudgetaire.value.trim().toUpperCase(),
255
- )
280
+ const codeNormalise = codeBudgetaire.value.trim().toUpperCase()
281
+
282
+ const existe = props.codeBudgetairesProp.some(item => {
283
+ const code = typeof item === 'string' ? item : item[0]
284
+ return code.trim().toUpperCase() === codeNormalise
285
+ })
256
286
 
257
287
  if (!existe) {
258
288
  const nouvelleListe = [...props.codeBudgetairesProp, codeBudgetaire.value]
@@ -263,13 +293,35 @@
263
293
  emit('update:modelValue', codeBudgetaire.value)
264
294
  }
265
295
 
266
- const gererChangement = (val: string) => {
267
- codeBudgetaire.value = formaterCodeBudgetaire(val)
296
+ const extraireCode = (val: unknown): string => {
297
+ if (val == null) return ''
298
+ if (typeof val === 'string') return val
268
299
 
269
- const valeurFormatee = codeBudgetaire.value
270
- const estDansListe = props.codeBudgetairesProp.includes(valeurFormatee)
300
+ // Cas Vuetify enverrait { code, label }
301
+ if (typeof val === 'object' && 'code' in (val as Record<string, unknown>)) {
302
+ const obj = val as { code?: unknown }
303
+ return typeof obj.code === 'string' ? obj.code : String(obj.code ?? '')
304
+ }
305
+
306
+ return String(val)
307
+ }
308
+
309
+ const gererChangement = (val: unknown) => {
310
+ const code = extraireCode(val)
311
+ codeBudgetaire.value = formaterCodeBudgetaire(code)
312
+
313
+ const valeurFormatee = codeBudgetaire.value.trim().toUpperCase()
314
+
315
+ const estDansListe = props.codeBudgetairesProp.some(item => {
316
+ const codeItem = typeof item === 'string' ? item : item[0]
317
+ return codeItem.trim().toUpperCase() === valeurFormatee
318
+ })
271
319
 
272
- if (estDansListe && valeurFormatee !== derniereValeurSauvegardee.value && estValideComplet.value) {
320
+ if (
321
+ estDansListe &&
322
+ valeurFormatee !== (derniereValeurSauvegardee.value ?? '').toUpperCase() &&
323
+ estValideComplet.value
324
+ ) {
273
325
  sauvegarder()
274
326
  }
275
327
  }
@@ -1,175 +1,186 @@
1
-
2
- class RafraichisseurToken {
3
- private intervalleEnSecondes = 15
4
- private secondesAvantExpirationTokenPourRafraichir = 20
5
- private skewSeconds = 5 // marge anti-derives d’horloge
6
- private popupAffiche = false
7
- private timerId: number | null = null
8
-
9
- // Lance une seule fois
10
- public async demarrer(nomCookie: string | null, urlPortail: string): Promise<void> {
11
- if (nomCookie == null || nomCookie === '') nomCookie = 'csqc_jeton_secure_expiration'
12
- await this.verifierToken(nomCookie, urlPortail)
13
- if (this.timerId != null) return
14
- this.timerId = window.setInterval(() => {
15
- this.verifierToken(nomCookie, urlPortail)
16
- }, this.intervalleEnSecondes * 1000)
17
- }
18
-
19
- // Permet d’arrêter le timer (ex: au logout / destroy)
20
- public arreter(): void {
21
- if (this.timerId != null) {
22
- clearInterval(this.timerId)
23
- this.timerId = null
24
- }
25
- }
26
-
27
- private async verifierToken(nomCookie: string, urlPortail: string): Promise<void> {
28
-
29
- if (this.popupAffiche || !nomCookie) return
30
- const tokenEncode = this.lireCookie(nomCookie)
31
- if (!tokenEncode) {
32
- await this.rafraichir(nomCookie, urlPortail)
33
- return
34
- }
35
-
36
- let token: any
37
- try {
38
- token = this.parseJwt(tokenEncode)
39
- } catch {
40
- await this.rafraichir(nomCookie, urlPortail)
41
- return
42
- }
43
-
44
- const exp = Number(token?.exp)
45
- if (!Number.isFinite(exp)) {
46
- // exp manquant/invalide → tente refresh
47
- await this.rafraichir(nomCookie, urlPortail)
48
- return
49
- }
50
-
51
- const now = Math.floor(Date.now() / 1000)
52
- const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
53
- if (now >= refreshAt) {
54
- await this.rafraichir(nomCookie, urlPortail)
55
- }
56
- }
57
-
58
- private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
59
- if (!nomCookie) return
60
- const url = this.getRefreshUrl(urlPortail)
61
- const controller = new AbortController()
62
- const timeout = setTimeout(() => controller.abort(), 10_000)
63
-
64
- try {
65
- //Première tentative sans iframe, pour la majorité des cas.
66
- const resp = await fetch(url, {
67
- method: 'POST',
68
- credentials: 'include',
69
- headers: { Accept: 'application/json' },
70
- redirect: 'manual',
71
- signal: controller.signal,
72
- })
73
-
74
- // redirection (souvent => login) → traiter comme non auth
75
-
76
- if (resp.type === 'opaqueredirect' || resp.status === 302) {
77
- this.rafraichirParIframe(nomCookie, urlPortail)
78
- return
79
- }
80
-
81
- // OK ou No Content: le cookie devrait être réécrit
82
- if (resp.status === 200 || resp.status === 204) {
83
- const jeton = this.lireCookie(nomCookie)
84
- if (!jeton) this.rafraichirParIframe(nomCookie, urlPortail)
85
- return
86
- }
87
-
88
- // non auth / expiré (401, 419) + IIS timeout (440)
89
- if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
90
- this.rafraichirParIframe(nomCookie, urlPortail)
91
- return
92
- }
93
-
94
- console.warn('Rafraichisseur token: statut inattendu', resp.status)
95
- } catch (err: any) {
96
- if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
97
- else console.error('Erreur rafraichisseur de token', err)
98
- // on réessaiera au prochain tick
99
- } finally {
100
- clearTimeout(timeout)
101
- }
102
- }
103
-
104
- private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
105
- // 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é
106
- // ajax vers le refresh
107
- let iframe = document.createElement('iframe')
108
- const url = this.getRefreshUrl(urlPortail)
109
- iframe.src = `${url}?urlRetour=${encodeURI(window.localStorage.href)}`
110
- iframe.id = 'idRafrToken'
111
- iframe.style.display = 'none'
112
- document.body.appendChild(iframe)
113
- iframe.onload = () => {
114
- const jetonCSQC = this.lireCookie(nomCookie)
115
- if (jetonCSQC == null || jetonCSQC === '') {
116
- this.estDeconnecteAzure(urlPortail)
117
- }
118
-
119
- iframe.remove()
120
- }
121
- }
122
-
123
- private estDeconnecteAzure(urlPortail: string): void {
124
- //on envoie au portail, pas le choix
125
-
126
- const retour = encodeURI(window.location.href)
127
- window.open(`${urlPortail}/home/SeConnecter?urlRetour=${retour}`, '_self')
128
- return
129
- }
130
-
131
- public deconnecterPortail(urlPortail: string): void {
132
- window.location.replace(`${urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
133
- }
134
-
135
-
136
- private lireCookie(nom: string): string | null {
137
- if (!document.cookie) return null
138
- const cookies = document.cookie.split(';').map(c => c.trim())
139
- for (const cookie of cookies) {
140
- if (cookie.startsWith(`${nom}=`)) {
141
- try {
142
- return decodeURIComponent(cookie.substring(nom.length + 1))
143
- } catch {
144
- return cookie.substring(nom.length + 1)
145
- }
146
- }
147
- }
148
- return null
149
- }
150
-
151
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
- private parseJwt(token: string): any {
153
- const parts = token.split('.')
154
- const base64Url = parts[1]
155
- if (!base64Url) throw new Error('Invalid JWT format')
156
- const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
157
- const jsonPayload = decodeURIComponent(
158
- atob(base64)
159
- .split('')
160
- .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
161
- .join(''),
162
- )
163
- return JSON.parse(jsonPayload)
164
- }
165
-
166
- // URL refresh selon env (dev = proxy Vite ; prod = portail)
167
- private getRefreshUrl(urlPortail: string): string {
168
- if (import.meta.env.MODE === 'development') return '/portail-refresh'
169
- return urlPortail + '/Home/Refresh'
170
- }
171
- }
172
-
173
- // Instance
174
- const rafraichisseurToken = new RafraichisseurToken()
175
- 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
+
8
+ // Lance une seule fois
9
+ public async demarrer(nomTemoin: string | null, urlPortail: string): Promise<void> {
10
+ if (nomTemoin == null || nomTemoin === '') nomTemoin = 'csqc_jeton_secure_expiration'
11
+ await this.verifierJeton(nomTemoin, urlPortail)
12
+ if (this.timerId != null) return
13
+ this.timerId = window.setInterval(() => {
14
+ this.verifierJeton(nomTemoin, urlPortail)
15
+ }, this.intervalleEnSecondes * 1000)
16
+ }
17
+
18
+ // Permet d’arrêter le timer (ex: au logout / destroy)
19
+ public arreter(): void {
20
+ if (this.timerId != null) {
21
+ clearInterval(this.timerId)
22
+ this.timerId = null
23
+ }
24
+ }
25
+
26
+ public existeJeton = (nomTemoin: string) => {
27
+ return this.estJetonValide(nomTemoin)
28
+ }
29
+
30
+ private async verifierJeton(nomTemoin: string, urlPortail: string): Promise<void> {
31
+ if (this.popupAffiche) return
32
+
33
+ if (!this.estJetonValide(nomTemoin)) {
34
+ this.rafraichir(nomTemoin, urlPortail)
35
+ return
36
+ }
37
+ }
38
+
39
+ private estJetonValide(nomTemoin: string): boolean {
40
+ if (this.popupAffiche || !nomTemoin) return true //On fait semblant que c'est valide pour ne pas provoquer un autre affichage du popup.
41
+
42
+ const tokenEncode = this.lireCookie(nomTemoin)
43
+ if (!tokenEncode) {
44
+ return false
45
+ }
46
+
47
+ let token: any
48
+ try {
49
+ token = this.parseJwt(tokenEncode)
50
+ } catch {
51
+ return false
52
+ }
53
+
54
+ const exp = Number(token?.exp)
55
+ if (!Number.isFinite(exp)) {
56
+ // exp manquant/invalide → tente refresh
57
+
58
+ return false
59
+ }
60
+
61
+ const now = Math.floor(Date.now() / 1000)
62
+ const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
63
+ if (now >= refreshAt) {
64
+ return false
65
+ }
66
+
67
+ return true
68
+ }
69
+
70
+ private async rafraichir(nomCookie: string, urlPortail: string): Promise<void> {
71
+ if (!nomCookie) return
72
+ const url = this.getRefreshUrl(urlPortail)
73
+ const controller = new AbortController()
74
+ const timeout = setTimeout(() => controller.abort(), 10_000)
75
+
76
+ try {
77
+ //Première tentative sans iframe, pour la majorité des cas.
78
+ const resp = await fetch(url, {
79
+ method: 'POST',
80
+ credentials: 'include',
81
+ headers: { Accept: 'application/json' },
82
+ redirect: 'manual',
83
+ signal: controller.signal,
84
+ })
85
+
86
+ // redirection (souvent => login) → traiter comme non auth
87
+
88
+ if (resp.type === 'opaqueredirect' || resp.status === 302) {
89
+ this.rafraichirParIframe(nomCookie, urlPortail)
90
+ return
91
+ }
92
+
93
+ // OK ou No Content: le cookie devrait être réécrit
94
+ if (resp.status === 200 || resp.status === 204) {
95
+ const jeton = this.lireCookie(nomCookie)
96
+ if (!jeton) this.rafraichirParIframe(nomCookie, urlPortail)
97
+ return
98
+ }
99
+
100
+ // non auth / expiré (401, 419) + IIS timeout (440)
101
+ if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
102
+ this.rafraichirParIframe(nomCookie, urlPortail)
103
+ return
104
+ }
105
+
106
+ console.warn('Rafraichisseur token: statut inattendu', resp.status)
107
+ } catch (err: any) {
108
+ if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
109
+ else console.error('Erreur rafraichisseur de token', err)
110
+ // on réessaiera au prochain tick
111
+ } finally {
112
+ clearTimeout(timeout)
113
+ }
114
+ }
115
+
116
+ private rafraichirParIframe(nomCookie: string, urlPortail: string): void {
117
+ // 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é
118
+ // ajax vers le refresh
119
+ let iframe = document.createElement('iframe')
120
+ const url = this.getRefreshUrl(urlPortail)
121
+ iframe.src = `${url}?urlRetour=${encodeURI(window.localStorage.href)}`
122
+ iframe.id = 'idRafrToken'
123
+ iframe.style.display = 'none'
124
+ document.body.appendChild(iframe)
125
+ iframe.onload = () => {
126
+ const jetonCSQC = this.lireCookie(nomCookie)
127
+ if (jetonCSQC == null || jetonCSQC === '') {
128
+ this.estDeconnecteAzure(urlPortail)
129
+ }
130
+
131
+ iframe.remove()
132
+ }
133
+ }
134
+
135
+ private estDeconnecteAzure(urlPortail: string): void {
136
+ //on envoie au portail, pas le choix
137
+
138
+ const retour = encodeURI(window.location.href)
139
+ window.open(`${urlPortail}/home/SeConnecter?urlRetour=${retour}`, '_self')
140
+ return
141
+ }
142
+
143
+ public deconnecterPortail(urlPortail: string): void {
144
+ window.location.replace(`${urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
145
+ }
146
+
147
+ private lireCookie(nom: string): string | null {
148
+ if (!document.cookie) return null
149
+ const cookies = document.cookie.split(';').map(c => c.trim())
150
+ for (const cookie of cookies) {
151
+ if (cookie.startsWith(`${nom}=`)) {
152
+ try {
153
+ return decodeURIComponent(cookie.substring(nom.length + 1))
154
+ } catch {
155
+ return cookie.substring(nom.length + 1)
156
+ }
157
+ }
158
+ }
159
+ return null
160
+ }
161
+
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ private parseJwt(token: string): any {
164
+ const parts = token.split('.')
165
+ const base64Url = parts[1]
166
+ if (!base64Url) throw new Error('Invalid JWT format')
167
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
168
+ const jsonPayload = decodeURIComponent(
169
+ atob(base64)
170
+ .split('')
171
+ .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
172
+ .join(''),
173
+ )
174
+ return JSON.parse(jsonPayload)
175
+ }
176
+
177
+ // URL refresh selon env (dev = proxy Vite ; prod = portail)
178
+ private getRefreshUrl(urlPortail: string): string {
179
+ if (import.meta.env.MODE === 'development') return '/portail-refresh'
180
+ return urlPortail + '/Home/Refresh'
181
+ }
182
+ }
183
+
184
+ // Instance
185
+ const rafraichisseurToken = new RafraichisseurToken()
186
+ export default rafraichisseurToken
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",