codevdesign 0.0.96 → 0.0.98

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,118 +1,118 @@
1
- <template>
2
- <v-toolbar
3
- color="primary"
4
- height="72px"
5
- elevation="1"
6
- >
7
- <v-row
8
- class="pl-6 pr-6"
9
- align="center"
10
- no-gutters
11
- >
12
- <!-- Première colonne : Logo -->
13
- <v-col cols="auto">
14
- <a href="/">
15
- <img
16
- v-if="texteVide(logoUrl)"
17
- id="pivImage"
18
- src="/images/QUEBEC_blanc.svg"
19
- height="72"
20
- :alt="$t('csqc.pivFooter.logoAlt')"
21
- />
22
- <img
23
- v-else
24
- id="pivImage"
25
- :src="logoUrl"
26
- height="72"
27
- :alt="$t('csqc.pivFooter.logoAlt')"
28
- />
29
- </a>
30
- </v-col>
31
-
32
- <!-- Colonne pour le nom de l'application (Pour le mode desktop) -->
33
- <v-col
34
- v-if="!estMobile"
35
- class="d-flex justify-center"
36
- >
37
- <v-app-bar-title
38
- class="pl-12 ml-12"
39
- style="font-size: 16px !important"
40
- >
41
- {{ $t('nom_application') }}
42
- </v-app-bar-title>
43
- </v-col>
44
-
45
- <!-- Colonne pour le bouton de langue -->
46
- <v-col class="d-none d-flex justify-end">
47
- <v-btn
48
- variant="text"
49
- @click="enregistrerLangue()"
50
- >{{ $t('csqc.pivEntete.langue') }}
51
- </v-btn>
52
- </v-col>
53
-
54
- <!-- Colonne pour le nom de l'application (Pour le mode mobile) -->
55
- <v-col
56
- v-if="props.estMobile"
57
- cols="12"
58
- >
59
- <v-app-bar-title style="font-size: 16px !important">
60
- {{ $t('nom_application') }}
61
- </v-app-bar-title>
62
- </v-col>
63
- </v-row>
64
- </v-toolbar>
65
- </template>
66
-
67
- <script setup lang="ts">
68
- import { useLocale } from 'vuetify'
69
-
70
- const { current } = useLocale()
71
- const texteVide = (texte: string | undefined, retirerEspace: boolean = true): boolean => {
72
- let retour = texte === undefined || texte === null || texte === ''
73
-
74
- if (!retour && retirerEspace) {
75
- const temp = texte
76
- retour = temp!.replace(' ', '') === ''
77
- }
78
-
79
- return retour
80
- }
81
- const props = defineProps({
82
- estMobile: {
83
- type: Boolean,
84
- required: true,
85
- },
86
- urlBase: {
87
- type: String,
88
- required: true,
89
- },
90
- logoUrl: {
91
- type: String,
92
- default: '',
93
- },
94
- })
95
- const emit = defineEmits(['changementLangue'])
96
- // Fonction pour enregistrer la langue
97
-
98
- const enregistrerLangue = (): void => {
99
- const langueDispo: string = current.value === 'fr' ? 'en' : 'fr'
100
- let returnUrl = window.location.pathname + window.location.search
101
- if (import.meta.env.MODE === 'development') {
102
- returnUrl = '/'
103
- }
104
- window.location.href =
105
- props.urlBase + `/Traducteur/SetLanguage?culture=${langueDispo}&returnUrl=${encodeURIComponent(returnUrl)}`
106
- emit('changementLangue')
107
- }
108
- </script>
109
-
110
- <style scoped>
111
- .container {
112
- max-width: none !important;
113
- }
114
- .theme--light.v-app-bar.v-toolbar.v-sheet {
115
- background: #095797;
116
- color: #fff;
117
- }
118
- </style>
1
+ <template>
2
+ <v-toolbar
3
+ color="primary"
4
+ height="72px"
5
+ elevation="1"
6
+ >
7
+ <v-row
8
+ class="pl-6 pr-6"
9
+ align="center"
10
+ no-gutters
11
+ >
12
+ <!-- Première colonne : Logo -->
13
+ <v-col cols="auto">
14
+ <a href="/">
15
+ <img
16
+ v-if="texteVide(logoUrl)"
17
+ id="pivImage"
18
+ src="/images/QUEBEC_blanc.svg"
19
+ height="72"
20
+ :alt="$t('csqc.pivFooter.logoAlt')"
21
+ />
22
+ <img
23
+ v-else
24
+ id="pivImage"
25
+ :src="logoUrl"
26
+ height="72"
27
+ :alt="$t('csqc.pivFooter.logoAlt')"
28
+ />
29
+ </a>
30
+ </v-col>
31
+
32
+ <!-- Colonne pour le nom de l'application (Pour le mode desktop) -->
33
+ <v-col
34
+ v-if="!estMobile"
35
+ class="d-flex justify-center"
36
+ >
37
+ <v-app-bar-title
38
+ class="pl-12 ml-12"
39
+ style="font-size: 16px !important"
40
+ >
41
+ {{ $t('nom_application') }}
42
+ </v-app-bar-title>
43
+ </v-col>
44
+
45
+ <!-- Colonne pour le bouton de langue -->
46
+ <v-col class="d-none d-flex justify-end">
47
+ <v-btn
48
+ variant="text"
49
+ @click="enregistrerLangue()"
50
+ >{{ $t('csqc.pivEntete.langue') }}
51
+ </v-btn>
52
+ </v-col>
53
+
54
+ <!-- Colonne pour le nom de l'application (Pour le mode mobile) -->
55
+ <v-col
56
+ v-if="props.estMobile"
57
+ cols="12"
58
+ >
59
+ <v-app-bar-title style="font-size: 16px !important">
60
+ {{ $t('nom_application') }}
61
+ </v-app-bar-title>
62
+ </v-col>
63
+ </v-row>
64
+ </v-toolbar>
65
+ </template>
66
+
67
+ <script setup lang="ts">
68
+ import { useLocale } from 'vuetify'
69
+
70
+ const { current } = useLocale()
71
+ const texteVide = (texte: string | undefined, retirerEspace: boolean = true): boolean => {
72
+ let retour = texte === undefined || texte === null || texte === ''
73
+
74
+ if (!retour && retirerEspace) {
75
+ const temp = texte
76
+ retour = temp!.replace(' ', '') === ''
77
+ }
78
+
79
+ return retour
80
+ }
81
+ const props = defineProps({
82
+ estMobile: {
83
+ type: Boolean,
84
+ required: true,
85
+ },
86
+ urlBase: {
87
+ type: String,
88
+ required: true,
89
+ },
90
+ logoUrl: {
91
+ type: String,
92
+ default: '',
93
+ },
94
+ })
95
+ const emit = defineEmits(['changementLangue'])
96
+ // Fonction pour enregistrer la langue
97
+
98
+ const enregistrerLangue = (): void => {
99
+ const langueDispo: string = current.value === 'fr' ? 'en' : 'fr'
100
+ let returnUrl = window.location.pathname + window.location.search
101
+ if (import.meta.env.MODE === 'development') {
102
+ returnUrl = '/'
103
+ }
104
+ window.location.href =
105
+ props.urlBase + `/Traducteur/SetLanguage?culture=${langueDispo}&returnUrl=${encodeURIComponent(returnUrl)}`
106
+ emit('changementLangue')
107
+ }
108
+ </script>
109
+
110
+ <style scoped>
111
+ .container {
112
+ max-width: none !important;
113
+ }
114
+ .theme--light.v-app-bar.v-toolbar.v-sheet {
115
+ background: #095797;
116
+ color: #fff;
117
+ }
118
+ </style>
package/index.ts CHANGED
@@ -15,7 +15,7 @@ import csqcChaise from './composants/csqcChaise/chaiseConteneur.vue'
15
15
  import csqcAide from './composants/csqcAide.vue'
16
16
  import csqcEntete from './composants/csqcEntete.vue'
17
17
  import csqcTexteBilingue from './composants/csqcTexteBilingue.vue'
18
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ // @ts-expect-error TS7016
19
19
  import csqcEditeurTexteRiche from './composants/csqcEditeurTexteRiche.vue'
20
20
  import csqcImportCSV from './composants/csqcImportCSV.vue'
21
21
  import csqcRechercheUtilisateur from './composants/csqcRechercheUtilisateur.vue'
@@ -33,6 +33,10 @@ import response from './modeles/response'
33
33
  import csqcEn from './locales/en.json'
34
34
  import csqcFr from './locales/fr.json'
35
35
 
36
+ // outils
37
+ import csqcRafraichisseurToken from './outils/rafraichisseurToken'
38
+ import csqcAppAxios from './outils/appAxios'
39
+
36
40
  export {
37
41
  csqcFr,
38
42
  csqcEn,
@@ -63,4 +67,6 @@ export {
63
67
  data,
64
68
  response,
65
69
  NotificationGabaritDefaut,
70
+ csqcAppAxios,
71
+ csqcRafraichisseurToken
66
72
  }
package/locales/fr.json CHANGED
@@ -78,7 +78,8 @@
78
78
  "chaiseSelection": "Aucune sélection pour cette unité",
79
79
  "chaiseSelectionToutes": "Veuillez faire une sélection pour toutes les unités.",
80
80
  "supprimerMessage": "Voulez-vous vraiment supprimer {nom}?",
81
- "supprimerTitre": "Confirmation de suppression!"
81
+ "supprimerTitre": "Confirmation de suppression!",
82
+ "token": "Votre jeton de connexion est périmé ou votre connexion est échue"
82
83
  },
83
84
  "pivEntete": {
84
85
  "langue": "English"
@@ -0,0 +1,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
+
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
+ }
@@ -0,0 +1,167 @@
1
+ import i18n from '@/plugins/i18n'
2
+ import { useAppStore } from '@/store/appStore'
3
+
4
+ class RafraichisseurToken {
5
+ private intervalleEnSecondes = 15
6
+ private secondesAvantExpirationTokenPourRafraichir = 20
7
+ private skewSeconds = 5 // marge anti-derives d’horloge
8
+ private popupAffiche = false
9
+ private appStore: ReturnType<typeof useAppStore> | null = null
10
+ private timerId: number | null = null
11
+ private deconnexionEnCours = false // évite les popups multiples
12
+
13
+ // Lance une seule fois
14
+ public async demarrer(): Promise<void> {
15
+ this.appStore = useAppStore()
16
+ await this.verifierToken()
17
+ if (this.timerId != null) return
18
+ this.timerId = window.setInterval(() => { this.verifierToken() }, this.intervalleEnSecondes * 1000)
19
+ }
20
+
21
+ // Permet d’arrêter le timer (ex: au logout / destroy)
22
+ public arreter(): void {
23
+ if (this.timerId != null) {
24
+ clearInterval(this.timerId)
25
+ this.timerId = null
26
+ }
27
+ }
28
+
29
+ private async verifierToken(): Promise<void> {
30
+ if (!this.appStore) this.appStore = useAppStore()
31
+ const modele = this.appStore?.appModele
32
+ if (this.popupAffiche || !modele) return
33
+ const tokenEncode = this.lireCookie(modele.nomCookie)
34
+ if (!tokenEncode) { await this.rafraichir(); return }
35
+
36
+ let token: any
37
+ try {
38
+ token = this.parseJwt(tokenEncode)
39
+ } catch {
40
+ await this.rafraichir()
41
+ return
42
+ }
43
+
44
+ const exp = Number(token?.exp)
45
+ if (!Number.isFinite(exp)) { // exp manquant/invalide → tente refresh
46
+ await this.rafraichir()
47
+ return
48
+ }
49
+
50
+ const now = Math.floor(Date.now() / 1000)
51
+ const refreshAt = exp - this.secondesAvantExpirationTokenPourRafraichir - this.skewSeconds
52
+ if (now >= refreshAt) {
53
+ await this.rafraichir()
54
+ }
55
+ }
56
+
57
+ private async rafraichir(): Promise<void> {
58
+ if (!this.appStore) this.appStore = useAppStore()
59
+ const modele = this.appStore?.appModele
60
+ if (!modele) return
61
+ const url = this.getRefreshUrl()
62
+ const controller = new AbortController()
63
+ const timeout = setTimeout(() => controller.abort(), 10_000)
64
+
65
+ try {
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
+ if (resp.type === 'opaqueredirect' || resp.status === 302) {
76
+ this.deconnecterAzure()
77
+ return
78
+ }
79
+
80
+ // OK ou No Content: le cookie devrait être réécrit
81
+ if (resp.status === 200 || resp.status === 204) {
82
+ const jeton = this.lireCookie(modele.nomCookie)
83
+ if (!jeton) this.deconnecterAzure()
84
+ return
85
+ }
86
+
87
+ // non auth / expiré (401, 419) + IIS timeout (440)
88
+ if (resp.status === 401 || resp.status === 419 || resp.status === 440) {
89
+ this.deconnecterAzure()
90
+ return
91
+ }
92
+
93
+ console.warn('Rafraichisseur token: statut inattendu', resp.status)
94
+ } catch (err: any) {
95
+ if (err?.name === 'AbortError') console.warn('RafraichisseurToken timeout')
96
+ else console.error('Erreur rafraichisseur de token', err)
97
+ // on réessaiera au prochain tick
98
+ } finally {
99
+ clearTimeout(timeout)
100
+ }
101
+ }
102
+
103
+ private deconnecterAzure(): void {
104
+ if (this.deconnexionEnCours) return
105
+ this.deconnexionEnCours = true
106
+ const { t } = i18n.global
107
+ this.popupAffiche = true
108
+ if (!this.appStore) this.appStore = useAppStore()
109
+
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')
113
+ }
114
+
115
+ // si l’utilisateur annule, on permet un futur prompt
116
+ this.deconnexionEnCours = false
117
+ }
118
+
119
+ public deconnecterPortail(): void {
120
+ if (!this.appStore) this.appStore = useAppStore()
121
+ const modele = this.appStore?.appModele
122
+ if (!modele) return
123
+
124
+ window.location.replace(
125
+ `${modele.urlPortail}deconnecte?urlRetour=${encodeURIComponent(window.location.href)}`
126
+ )
127
+ }
128
+
129
+ public annulerDeconnecter(): void {
130
+ this.popupAffiche = false
131
+ this.deconnexionEnCours = false
132
+ }
133
+
134
+ private lireCookie(nom: string): string | null {
135
+ if (!document.cookie) return null
136
+ const cookies = document.cookie.split(';').map(c => c.trim())
137
+ for (const cookie of cookies) {
138
+ if (cookie.startsWith(`${nom}=`)) {
139
+ try { return decodeURIComponent(cookie.substring(nom.length + 1)) }
140
+ catch { return cookie.substring(nom.length + 1) }
141
+ }
142
+ }
143
+ return null
144
+ }
145
+
146
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
147
+ private parseJwt(token: string): any {
148
+ const parts = token.split('.')
149
+ const base64Url = parts[1]
150
+ if (!base64Url) throw new Error('Invalid JWT format')
151
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
152
+ const jsonPayload = decodeURIComponent(
153
+ atob(base64).split('').map(c => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`).join('')
154
+ )
155
+ return JSON.parse(jsonPayload)
156
+ }
157
+
158
+ // URL refresh selon env (dev = proxy Vite ; prod = portail)
159
+ private getRefreshUrl(): string {
160
+ if (import.meta.env.MODE === "development") return '/portail-refresh'
161
+ return this.appStore!.appModele!.urlPortailRafraichissement
162
+ }
163
+ }
164
+
165
+ // Instance
166
+ const rafraichisseurToken = new RafraichisseurToken()
167
+ export default rafraichisseurToken
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codevdesign",
3
- "version": "0.0.96",
3
+ "version": "0.0.98",
4
4
  "description": "Composants Vuetify 3 pour les projets Codev",
5
5
  "files": [
6
6
  "./**/*.vue",