codevdesign 1.0.1 → 1.0.3

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,117 +1,116 @@
1
- import axios, { type AxiosInstance, type AxiosError, type AxiosResponse } from 'axios'
2
- import { useAppStore } from '@/store/appStore'
3
- import router from '@/router'
4
-
5
- type ApiReponse<T = unknown> =
6
- // Succès
7
- | {
8
- succes: true
9
- resultat: T
10
- status?: number
11
- message?: string
12
- location?: string
13
- parametres?: unknown
14
- [k: string]: unknown
15
- }
16
- // Échec (le backend peut quand même renvoyer resultat null/absent)
17
- | {
18
- succes: false
19
- resultat?: unknown
20
- status?: number
21
- message?: string
22
- location?: string
23
- parametres?: unknown
24
- [k: string]: unknown
25
- }
26
-
27
- let client: AxiosInstance | null = null
28
- let cachedBaseUrl = '' // pour éviter de régénérer une instance axios si rien n’a changé
29
-
30
- export default {
31
- clearCache: false,
32
-
33
- getAxios(): AxiosInstance {
34
- // Singleton + clearCache
35
- if (client && !this.clearCache) return client
36
-
37
- const appStore = useAppStore()
38
-
39
- const rawUrl = appStore.modeleCharger
40
- ? appStore.appModele!.urlBase
41
- : window.location.origin + import.meta.env.BASE_URL
42
-
43
- const urlBase = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl
44
-
45
- // Si la base URL n'a pas changé et qu'on a déjà un client, on le renvoie
46
- if (client && cachedBaseUrl === urlBase && !this.clearCache) return client
47
- cachedBaseUrl = urlBase
48
-
49
- client = axios.create({
50
- baseURL: `${urlBase}/api`,
51
- withCredentials: true,
52
- timeout: 30_000,
53
- headers: {
54
- Accept: 'application/json',
55
- 'Content-Type': 'application/json',
56
- 'X-Requested-With': 'XMLHttpRequest',
57
- },
58
- // validateStatus: (s) => s >= 200 && s < 300, // défaut axios
59
- })
60
-
61
- client.interceptors.response.use(
62
- (response: AxiosResponse<any>) => {
63
- const data = response.data
64
-
65
- // Détection de la réponse { succes, resultat }
66
- if (data && typeof data === 'object' && 'succes' in data) {
67
- const env = data as ApiReponse
68
- return env.succes === true ? env.resultat : Promise.reject(env)
69
- }
70
-
71
- // Sinon, renvoyer data si présent, sinon la réponse complète
72
- return data ?? response
73
- },
74
-
75
- (error: AxiosError<any>) => {
76
- const status = error.response?.status
77
- const payload = error.response?.data?.resultat ?? { message: error.message }
78
-
79
-
80
-
81
- // 403 / 404
82
- if (status === 403 || status === 404) {
83
- try {
84
- appStore.lancerErreur(payload)
85
- if (router.currentRoute.value.name !== '403') {
86
- router.push({ name: '403' })
87
- }
88
- } catch {
89
- // no-op
90
- }
91
- return Promise.reject(payload)
92
- }
93
-
94
- // gérer les autres 4XX ici
95
-
96
- // Log minimal
97
- console.error('HTTP error', {
98
- status,
99
- url: error.config?.url,
100
- payload,
101
- })
102
-
103
- // Remonter l’erreur normalisée dans appstore
104
- try {
105
- if (payload?.resultat) appStore.lancerErreur(payload)
106
- } catch {
107
- // no-op
108
- }
109
- return Promise.reject(payload)
110
- },
111
- )
112
-
113
- // reset le flag si on l’avait utilisé pour forcer la recréation de l'instance
114
- this.clearCache = false
115
- return client
116
- },
117
- }
1
+ import axios, { type AxiosInstance, type AxiosError, type AxiosResponse } from 'axios'
2
+ import { useAppStore } from '@/store/appStore'
3
+ import router from '@/router'
4
+
5
+ type ApiReponse<T = unknown> =
6
+ // Succès
7
+ | {
8
+ succes: true
9
+ resultat: T
10
+ status?: number
11
+ message?: string
12
+ location?: string
13
+ parametres?: unknown
14
+ [k: string]: unknown
15
+ }
16
+ // Échec (le backend peut quand même renvoyer resultat null/absent)
17
+ | {
18
+ succes: false
19
+ resultat?: unknown
20
+ status?: number
21
+ message?: string
22
+ location?: string
23
+ parametres?: unknown
24
+ [k: string]: unknown
25
+ }
26
+
27
+ let client: AxiosInstance | null = null
28
+ let cachedBaseUrl = '' // pour éviter de régénérer une instance axios si rien n’a changé
29
+
30
+ export default {
31
+ clearCache: false,
32
+
33
+ getAxios(): AxiosInstance {
34
+ // Singleton + clearCache
35
+ if (client && !this.clearCache) return client
36
+
37
+ const appStore = useAppStore()
38
+
39
+ const rawUrl = appStore.modeleCharger
40
+ ? appStore.appModele!.urlBase
41
+ : window.location.origin + import.meta.env.BASE_URL
42
+
43
+ const urlBase = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl
44
+
45
+ // Si la base URL n'a pas changé et qu'on a déjà un client, on le renvoie
46
+ if (client && cachedBaseUrl === urlBase && !this.clearCache) return client
47
+ cachedBaseUrl = urlBase
48
+
49
+ client = axios.create({
50
+ baseURL: `${urlBase}/api`,
51
+ withCredentials: true,
52
+ timeout: 30_000,
53
+ headers: {
54
+ Accept: 'application/json',
55
+ 'Content-Type': 'application/json',
56
+ 'X-Requested-With': 'XMLHttpRequest',
57
+ },
58
+ // validateStatus: (s) => s >= 200 && s < 300, // défaut axios
59
+ })
60
+
61
+ client.interceptors.response.use(
62
+ (response: AxiosResponse<any>) => {
63
+ const data = response.data
64
+
65
+ // Détection de la réponse { succes, resultat }
66
+ if (data && typeof data === 'object' && 'succes' in data) {
67
+ const env = data as ApiReponse
68
+ return env.succes === true ? env.resultat : Promise.reject(env)
69
+ }
70
+
71
+ // Sinon, renvoyer data si présent, sinon la réponse complète
72
+ return data ?? response
73
+ },
74
+
75
+ (error: AxiosError<any>) => {
76
+ const status = error.response?.status
77
+ const payload = error.response?.data?.resultat ?? { message: error.message }
78
+
79
+ // 403 / 404
80
+ if (status === 403 || status === 404) {
81
+ try {
82
+ appStore.lancerErreur(payload)
83
+ if (router.currentRoute.value.name !== '403') {
84
+ router.push({ name: '403' })
85
+ }
86
+ } catch {
87
+ // no-op
88
+ }
89
+ return Promise.reject(payload)
90
+ }
91
+
92
+ // gérer les autres 4XX ici
93
+
94
+ // Log minimal
95
+ console.error('HTTP error', {
96
+ status,
97
+ url: error.config?.url,
98
+ payload,
99
+ })
100
+
101
+ // Remonter l’erreur normalisée dans appstore
102
+ try {
103
+ if (payload?.resultat) appStore.lancerErreur(payload)
104
+ else if (payload) appStore.lancerErreur(payload)
105
+ } catch {
106
+ // no-op
107
+ }
108
+ return Promise.reject(payload)
109
+ },
110
+ )
111
+
112
+ // reset le flag si on l’avait utilisé pour forcer la recréation de l'instance
113
+ this.clearCache = false
114
+ return client
115
+ },
116
+ }
@@ -1,4 +1,3 @@
1
- import i18n from '@/plugins/i18n'
2
1
  import { useAppStore } from '@/store/appStore'
3
2
 
4
3
  class RafraichisseurToken {
@@ -8,14 +7,15 @@ class RafraichisseurToken {
8
7
  private popupAffiche = false
9
8
  private appStore: ReturnType<typeof useAppStore> | null = null
10
9
  private timerId: number | null = null
11
- private deconnexionEnCours = false // évite les popups multiples
12
10
 
13
11
  // Lance une seule fois
14
12
  public async demarrer(): Promise<void> {
15
13
  this.appStore = useAppStore()
16
14
  await this.verifierToken()
17
15
  if (this.timerId != null) return
18
- this.timerId = window.setInterval(() => { this.verifierToken() }, this.intervalleEnSecondes * 1000)
16
+ this.timerId = window.setInterval(() => {
17
+ this.verifierToken()
18
+ }, this.intervalleEnSecondes * 1000)
19
19
  }
20
20
 
21
21
  // Permet d’arrêter le timer (ex: au logout / destroy)
@@ -31,7 +31,10 @@ class RafraichisseurToken {
31
31
  const modele = this.appStore?.appModele
32
32
  if (this.popupAffiche || !modele) return
33
33
  const tokenEncode = this.lireCookie(modele.nomCookie)
34
- if (!tokenEncode) { await this.rafraichir(); return }
34
+ if (!tokenEncode) {
35
+ await this.rafraichir()
36
+ return
37
+ }
35
38
 
36
39
  let token: any
37
40
  try {
@@ -42,7 +45,8 @@ class RafraichisseurToken {
42
45
  }
43
46
 
44
47
  const exp = Number(token?.exp)
45
- if (!Number.isFinite(exp)) { // exp manquant/invalide → tente refresh
48
+ if (!Number.isFinite(exp)) {
49
+ // exp manquant/invalide → tente refresh
46
50
  await this.rafraichir()
47
51
  return
48
52
  }
@@ -63,6 +67,7 @@ class RafraichisseurToken {
63
67
  const timeout = setTimeout(() => controller.abort(), 10_000)
64
68
 
65
69
  try {
70
+ //Première tentative sans iframe, pour la majorité des cas.
66
71
  const resp = await fetch(url, {
67
72
  method: 'POST',
68
73
  credentials: 'include',
@@ -72,21 +77,22 @@ class RafraichisseurToken {
72
77
  })
73
78
 
74
79
  // redirection (souvent => login) → traiter comme non auth
80
+
75
81
  if (resp.type === 'opaqueredirect' || resp.status === 302) {
76
- this.deconnecterAzure()
82
+ this.rafraichirParIframe()
77
83
  return
78
84
  }
79
85
 
80
86
  // OK ou No Content: le cookie devrait être réécrit
81
87
  if (resp.status === 200 || resp.status === 204) {
82
88
  const jeton = this.lireCookie(modele.nomCookie)
83
- if (!jeton) this.deconnecterAzure()
89
+ if (!jeton) this.rafraichirParIframe()
84
90
  return
85
91
  }
86
92
 
87
93
  // non auth / expiré (401, 419) + IIS timeout (440)
88
94
  if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
89
- this.deconnecterAzure()
95
+ this.rafraichirParIframe()
90
96
  return
91
97
  }
92
98
 
@@ -100,20 +106,33 @@ class RafraichisseurToken {
100
106
  }
101
107
  }
102
108
 
103
- private deconnecterAzure(): void {
104
- if (this.deconnexionEnCours) return
105
- this.deconnexionEnCours = true
106
- const { t } = i18n.global
107
- this.popupAffiche = true
109
+ private rafraichirParIframe() {
108
110
  if (!this.appStore) this.appStore = useAppStore()
111
+ // 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é
112
+ // ajax vers le refresh
113
+ let iframe = document.createElement('iframe')
114
+ const url = this.getRefreshUrl()
115
+ iframe.src = `${url}?urlRetour=${encodeURI(window.localStorage.href)}`
116
+ iframe.id = 'idRafrToken'
117
+ iframe.style.display = 'none'
118
+ document.body.appendChild(iframe)
119
+ iframe.onload = () => {
120
+ const jetonCSQC = this.lireCookie(this.appStore!.appModele!.nomCookie)
121
+ if (jetonCSQC == null || jetonCSQC === '') {
122
+ this.estDeconnecteAzure()
123
+ }
109
124
 
110
- if (this.appStore?.appModele && confirm(t('csqc.message.token'))) {
111
- const retour = encodeURI(window.location.href)
112
- window.open(`${this.appStore.appModele.urlPortailSeConnecter}?urlRetour=${retour}`, '_self')
125
+ iframe.remove()
113
126
  }
127
+ }
128
+
129
+ private estDeconnecteAzure(): void {
130
+ //on envoie au portail, pas le choix
131
+ if (!this.appStore) this.appStore = useAppStore()
114
132
 
115
- // si l’utilisateur annule, on permet un futur prompt
116
- this.deconnexionEnCours = false
133
+ const retour = encodeURI(window.location.href)
134
+ window.open(`${this.appStore.appModele!.urlPortailSeConnecter}?urlRetour=${retour}`, '_self')
135
+ return
117
136
  }
118
137
 
119
138
  public deconnecterPortail(): void {
@@ -121,23 +140,20 @@ class RafraichisseurToken {
121
140
  const modele = this.appStore?.appModele
122
141
  if (!modele) return
123
142
 
124
- window.location.replace(
125
- `${modele.urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`
126
- )
143
+ window.location.replace(`${modele.urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
127
144
  }
128
145
 
129
- public annulerDeconnecter(): void {
130
- this.popupAffiche = false
131
- this.deconnexionEnCours = false
132
- }
133
146
 
134
147
  private lireCookie(nom: string): string | null {
135
148
  if (!document.cookie) return null
136
149
  const cookies = document.cookie.split(';').map(c => c.trim())
137
150
  for (const cookie of cookies) {
138
151
  if (cookie.startsWith(`${nom}=`)) {
139
- try { return decodeURIComponent(cookie.substring(nom.length + 1)) }
140
- catch { return cookie.substring(nom.length + 1) }
152
+ try {
153
+ return decodeURIComponent(cookie.substring(nom.length + 1))
154
+ } catch {
155
+ return cookie.substring(nom.length + 1)
156
+ }
141
157
  }
142
158
  }
143
159
  return null
@@ -150,14 +166,17 @@ class RafraichisseurToken {
150
166
  if (!base64Url) throw new Error('Invalid JWT format')
151
167
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
152
168
  const jsonPayload = decodeURIComponent(
153
- atob(base64).split('').map(c => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`).join('')
169
+ atob(base64)
170
+ .split('')
171
+ .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
172
+ .join(''),
154
173
  )
155
174
  return JSON.parse(jsonPayload)
156
175
  }
157
176
 
158
177
  // URL refresh selon env (dev = proxy Vite ; prod = portail)
159
178
  private getRefreshUrl(): string {
160
- if (import.meta.env.MODE === "development") return '/portail-refresh'
179
+ if (import.meta.env.MODE === 'development') return '/portail-refresh'
161
180
  return this.appStore!.appModele!.urlPortailRafraichissement
162
181
  }
163
182
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",