codevdesign 1.0.1 → 1.0.2

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,32 @@ 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
+ iframe.src = `${this.appStore.appModele!.urlPortailRafraichissement}?urlRetour=${encodeURI(window.localStorage.href)}`
115
+ iframe.id = 'idRafrToken'
116
+ iframe.style.display = 'none'
117
+ document.body.appendChild(iframe)
118
+ iframe.onload = () => {
119
+ const jetonCSQC = this.lireCookie(this.appStore!.appModele!.nomCookie)
120
+ if (jetonCSQC == null || jetonCSQC === '') {
121
+ this.estDeconnecteAzure()
122
+ }
109
123
 
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')
124
+ iframe.remove()
113
125
  }
126
+ }
127
+
128
+ private estDeconnecteAzure(): void {
129
+ //on envoie au portail, pas le choix
130
+ if (!this.appStore) this.appStore = useAppStore()
114
131
 
115
- // si l’utilisateur annule, on permet un futur prompt
116
- this.deconnexionEnCours = false
132
+ const retour = encodeURI(window.location.href)
133
+ window.open(`${this.appStore.appModele!.urlPortailSeConnecter}?urlRetour=${retour}`, '_self')
134
+ return
117
135
  }
118
136
 
119
137
  public deconnecterPortail(): void {
@@ -121,23 +139,20 @@ class RafraichisseurToken {
121
139
  const modele = this.appStore?.appModele
122
140
  if (!modele) return
123
141
 
124
- window.location.replace(
125
- `${modele.urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`
126
- )
142
+ window.location.replace(`${modele.urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`)
127
143
  }
128
144
 
129
- public annulerDeconnecter(): void {
130
- this.popupAffiche = false
131
- this.deconnexionEnCours = false
132
- }
133
145
 
134
146
  private lireCookie(nom: string): string | null {
135
147
  if (!document.cookie) return null
136
148
  const cookies = document.cookie.split(';').map(c => c.trim())
137
149
  for (const cookie of cookies) {
138
150
  if (cookie.startsWith(`${nom}=`)) {
139
- try { return decodeURIComponent(cookie.substring(nom.length + 1)) }
140
- catch { return cookie.substring(nom.length + 1) }
151
+ try {
152
+ return decodeURIComponent(cookie.substring(nom.length + 1))
153
+ } catch {
154
+ return cookie.substring(nom.length + 1)
155
+ }
141
156
  }
142
157
  }
143
158
  return null
@@ -150,14 +165,17 @@ class RafraichisseurToken {
150
165
  if (!base64Url) throw new Error('Invalid JWT format')
151
166
  const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
152
167
  const jsonPayload = decodeURIComponent(
153
- atob(base64).split('').map(c => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`).join('')
168
+ atob(base64)
169
+ .split('')
170
+ .map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
171
+ .join(''),
154
172
  )
155
173
  return JSON.parse(jsonPayload)
156
174
  }
157
175
 
158
176
  // URL refresh selon env (dev = proxy Vite ; prod = portail)
159
177
  private getRefreshUrl(): string {
160
- if (import.meta.env.MODE === "development") return '/portail-refresh'
178
+ if (import.meta.env.MODE === 'development') return '/portail-refresh'
161
179
  return this.appStore!.appModele!.urlPortailRafraichissement
162
180
  }
163
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",