Prechádzať zdrojové kódy

Merge branch 'feature/V8-3843_my_account_page' into develop

Olivier Massot 2 rokov pred
rodič
commit
5b5ebdb5a9
35 zmenil súbory, kde vykonal 1226 pridanie a 796 odobranie
  1. 2 0
      .gitignore
  2. 13 8
      components/Layout/AlertBar/Cotisation.vue
  3. 18 12
      components/Layout/MainMenu.vue
  4. 1 1
      components/Layout/Subheader.vue
  5. 29 10
      components/Ui/ExpansionPanel.vue
  6. 1 1
      composables/layout/useMenu.ts
  7. 2 2
      composables/utils/useAbilityUtils.ts
  8. 20 32
      config/abilities/pages/addressBook.yaml
  9. 17 27
      config/abilities/pages/admin2ios.yaml
  10. 24 40
      config/abilities/pages/billing.yaml
  11. 21 27
      config/abilities/pages/communication.yaml
  12. 51 85
      config/abilities/pages/cotisations.yaml
  13. 3 5
      config/abilities/pages/donor.yaml
  14. 21 35
      config/abilities/pages/educational.yaml
  15. 3 5
      config/abilities/pages/equipment.yaml
  16. 3 5
      config/abilities/pages/medals.yaml
  17. 38 57
      config/abilities/pages/myAccount.yaml
  18. 65 101
      config/abilities/pages/parameters.yaml
  19. 25 35
      config/abilities/pages/schedule.yaml
  20. 12 20
      config/abilities/pages/stats.yaml
  21. 32 0
      layouts/error.vue
  22. 29 0
      models/Organization/DolibarrAccount.ts
  23. 29 0
      models/Organization/MobytUserStatus.ts
  24. 35 1
      nuxt.config.ts
  25. 3 2
      package.json
  26. 376 0
      pages/subscription.vue
  27. 2 2
      plugins/ability.ts
  28. 8 7
      plugins/init.server.ts
  29. 4 12
      services/data/entityManager.ts
  30. 0 1
      services/data/imageManager.ts
  31. 308 0
      services/rights/abilityBuilder.ts
  32. 0 249
      services/rights/abilityUtils.ts
  33. 4 3
      services/rights/roleUtils.ts
  34. 10 0
      stores/accessProfile.ts
  35. 17 11
      stores/organizationProfile.ts

+ 2 - 0
.gitignore

@@ -18,3 +18,5 @@ local.app-v3.opentalent.fr.key
 /.project
 
 yarn.lock
+
+coverage/*

+ 13 - 8
components/Layout/AlertBar/Cotisation.vue

@@ -7,36 +7,36 @@ Barre d'alerte qui s'affiche pour donner l'état de la cotisation
 <template>
   <main>
     <!-- TODO : fonctionnement à valider -->
-    <UiSystemBar v-if="showCotisationAccess" background-color="ot-info" text-color="ot-white">
+    <UiSystemBar v-if="showCotisationAccess" background-color="ot-info">
       <template #bar.text>
-        <a @click="goOn('AFFILIATION')">
+        <a @click="goOn('AFFILIATION')" class="text-ot-white">
           <v-icon small>fas fa-info-circle</v-icon>
           {{$t('cotisation_access')}}
         </a>
       </template>
     </UiSystemBar>
 
-    <UiSystemBar v-else-if="showUploadInvoice" background-color="ot-info" text-color="ot-white">
+    <UiSystemBar v-else-if="showUploadInvoice" background-color="ot-info">
       <template #bar.text>
-        <a @click="goOn('INVOICE')">
+        <a @click="goOn('INVOICE')" class="text-ot-white">
           <v-icon small>fas fa-info-circle</v-icon>
           {{$t('upload_cotisation_invoice')}}
         </a>
       </template>
     </UiSystemBar>
 
-    <UiSystemBar v-else-if="showRenewInsurance" background-color="ot-info" text-color="ot-white">
+    <UiSystemBar v-else-if="showRenewInsurance" background-color="ot-info">
       <template #bar.text>
-        <a @click="goOn('INSURANCE')">
+        <a @click="goOn('INSURANCE')" class="text-ot-white">
           <v-icon small>fas fa-info-circle</v-icon>
           {{$t('renew_insurance_cmf')}}
         </a>
       </template>
     </UiSystemBar>
 
-    <UiSystemBar v-else-if="showInsuranceSubscription" background-color="ot-info" text-color="ot-white">
+    <UiSystemBar v-else-if="showInsuranceSubscription" background-color="ot-info">
       <template #bar.text>
-        <a @click="goOn('ADVERTISINGINSURANCE')">
+        <a @click="goOn('ADVERTISINGINSURANCE')" class="text-ot-white">
           <v-icon small>fas fa-info-circle</v-icon>
           {{$t('insurance_cmf_subscription')}}
         </a>
@@ -146,4 +146,9 @@ const goOn = (type: ALERT_STATE_COTISATION) => {
   height: 20px;
   margin: 0 6px;
 }
+
+a {
+  // Je ne sais pas pourquoi il faut le préciser, mais sans ça le pointeur n'est pas bon
+  cursor: pointer;
+}
 </style>

+ 18 - 12
components/Layout/MainMenu.vue

@@ -78,32 +78,38 @@ import {useMenu} from "~/composables/layout/useMenu";
 import {computed} from "@vue/reactivity";
 import { useDisplay } from 'vuetify'
 
-const { buildMenu, hasMenu, isInternalLink, openMenu, isMenuOpened } = useMenu()
+const { buildMenu, hasMenu, isInternalLink, setMenuState, isMenuOpened } = useMenu()
 
-const { mdAndUp } = useDisplay()
+const { mdAndUp, lgAndUp } = useDisplay()
 
 const menu = buildMenu('Main')
-
-const hasMainMenu = computed(() => hasMenu('Main'))
+if (menu.value === null) {
+  throw new Error('No main menu to display')
+}
 
 const isOpened = computed(() => isMenuOpened('Main'))
 
-// En vue md+, on affiche toujours le menu
-const isRail = computed(() => mdAndUp.value && !isOpened.value)
-const displayMenu = computed(() => hasMainMenu && (mdAndUp.value || isOpened.value))
+// En vue lg+, on affiche toujours le menu
+const displayMenu = computed(() => {
+  return menu.value !== null && hasMenu('Main') && (lgAndUp.value || isOpened.value)
+})
+
+// En vue md+, fermer le menu le passe simplement en mode rail
+// Sinon, le fermer le masque complètement
+const isRail = computed(() => {
+  return menu.value !== null && mdAndUp.value && !isOpened.value
+})
 
-const unwatch = watch(mdAndUp, (newValue, oldValue) => {
+const unwatch = watch(lgAndUp, (newValue, oldValue) => {
 // Par défaut si l'écran est trop petit au chargement de la page, le menu doit rester fermé.
-  if (process.client && mdAndUp.value) {
-    openMenu('Main')
+  if (process.client && menu.value !== null) {
+    setMenuState('Main', lgAndUp.value)
   }
 })
 onUnmounted(() => {
   unwatch()
 })
 
-
-
 </script>
 
 <style scoped lang="scss">

+ 1 - 1
components/Layout/Subheader.vue

@@ -9,7 +9,7 @@ Contient entre autres le breadcrumb, les commandes de changement d'année et les
       class="d-md-flex bg-ot-light-grey text-body-2 px-2"
       flat
     >
-      <LayoutSubHeaderBreadcrumbs v-show="mdAndUp" class="mr-auto d-sm-none d-md-flex d-none d-sm-flex" />
+      <LayoutSubHeaderBreadcrumbs v-if="mdAndUp" class="mr-auto d-flex" />
 
       <v-card
         class="d-md-flex pt-2 mr-6 align-baseline"

+ 29 - 10
components/Ui/ExpansionPanel.vue

@@ -5,22 +5,25 @@ Panneaux déroulants de type "accordéon"
 -->
 
 <template>
-  <v-expansion-panel :id="id">
-    <v-expansion-panel-header color="ot-light_grey">
-      <v-icon class="ot-white--text ot-green icon">
-        {{ icon }}
-      </v-icon>
-      {{ $t(id) }}
-    </v-expansion-panel-header>
-    <v-expansion-panel-content>
+  <v-expansion-panel :value="title">
+    <v-expansion-panel-title color="ot-light-grey">
+      <template v-slot:default="{ expanded }">
+        <v-icon class="text-ot-white bg-ot-green icon">
+          {{ icon }}
+        </v-icon>
+        {{ $t(title) }}
+      </template>
+    </v-expansion-panel-title>
+
+    <v-expansion-panel-text>
       <slot />
-    </v-expansion-panel-content>
+    </v-expansion-panel-text>
   </v-expansion-panel>
 </template>
 
 <script setup lang="ts">
 const props = defineProps({
-  id: {
+  title: {
     type: String,
     required: true
   },
@@ -47,4 +50,20 @@ const props = defineProps({
   .v-expansion-panel--active > .v-expansion-panel-header{
     min-height: 47px;
   }
+
+  .v-expansion-panel-title {
+    padding-left: 0;
+    padding-top: 0;
+    padding-bottom: 0;
+    max-height: 47px;
+    min-height: 47px;
+  }
+
+  :deep(.v-expansion-panel-title__icon > .v-icon) {
+    font-size: 16px;
+  }
+
+  .icon {
+    text-align: center;
+  }
 </style>

+ 1 - 1
composables/layout/useMenu.ts

@@ -42,7 +42,7 @@ export const useMenu = () => {
    *
    * @param name
    */
-  const buildMenu = (name: string): Ref<MenuGroup> => {
+  const buildMenu = (name: string): Ref<MenuGroup | null> => {
     const builder = getBuilder(name)
 
     const menu = builder.build() as MenuGroup

+ 2 - 2
composables/utils/useAbilityUtils.ts

@@ -1,12 +1,12 @@
 import {useAccessProfileStore} from "~/stores/accessProfile";
 import {useOrganizationProfileStore} from "~/stores/organizationProfile";
 import {useAbility} from "@casl/vue";
-import AbilityUtils from "~/services/rights/abilityUtils";
+import AbilityBuilder from "~/services/rights/abilityBuilder";
 
 export const useAbilityUtils = () => {
     const ability = useAbility()
     const accessProfile = useAccessProfileStore()
     const organizationProfile = useOrganizationProfileStore()
 
-    return new AbilityUtils(ability, accessProfile, organizationProfile)
+    return new AbilityBuilder(ability, accessProfile, organizationProfile)
 }

+ 20 - 32
config/abilities/pages/addressBook.yaml

@@ -1,49 +1,37 @@
   accesses_page:
     action: 'display'
-    services:
-      access :
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'users'}]}
-      organization  :
-        - {function: hasModule, parameters: ['Users']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Users']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'users'}]}
 
   student_registration_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'student-registration'}]}
-      organization:
-        - {function: hasModule, parameters: ['UsersSchool']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['UsersSchool']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'student-registration'}]}
 
   education_student_next_year_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
 
   commissions_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'commissions'}]}
-      organization:
-        - {function: hasModule, parameters: ['Users']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Users']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'commissions'}]}
 
   network_children_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'network'}]}
-      organization:
-        - {function: hasModule, parameters: ['Network']}
-        - {function: isOrganizationWithChildren}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Network']}
+      - {function: organizationHasChildren}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'network'}]}
 
   network_parents_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'core'}]}
-      organization:
-        - {function: hasModule, parameters: ['NetworkOrganization']}
-        - {function: isOrganizationWithChildren, result: false}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['NetworkOrganization']}
+      - {function: organizationHasChildren, expectedResult: false}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'core'}]}

+ 17 - 27
config/abilities/pages/admin2ios.yaml

@@ -1,47 +1,37 @@
   all_accesses_page:
     action: 'display'
-    services:
-      access :
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'user'}]}
-      organization  :
-        - {function: hasModule, parameters: ['Admin2IOS']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'user'}]}
 
   all_organizations_page:
     action: 'display'
-    services:
-      access :
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'organization'}]}
-      organization  :
-        - {function: hasModule, parameters: ['Admin2IOS']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'organization'}]}
 
   tips_page:
     action: 'display'
-    services:
-      access :
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'tips'}]}
-      organization  :
-        - {function: hasModule, parameters: ['CorePremium']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CorePremium']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'tips'}]}
 
   dgv_page:
     action: 'display'
-    services:
-      organization  :
-        - {function: hasModule, parameters: ['Admin2IOS']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
 
   cmf_cotisation_page:
     action: 'display'
-    services:
-      organization  :
-        - {function: hasModule, parameters: ['Admin2IOS']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
 
   right_page:
     action: 'display'
-    services:
-      organization  :
-        - {function: hasModule, parameters: ['Admin2IOS']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}
 
   tree_page:
     action: 'display'
-    services:
-      organization  :
-        - {function: hasModule, parameters: ['Admin2IOS']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Admin2IOS']}

+ 24 - 40
config/abilities/pages/billing.yaml

@@ -1,63 +1,47 @@
   billing_product_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BillingAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
 
   billing_products_by_student_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BillingAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   billing_edition_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BillingAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
 
   billing_accounting_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BillingAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
 
   billing_payment_list_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BillingAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
 
   pes_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['Pes']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Pes']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
 
   berger_levrault_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BergerLevrault']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BergerLevrault']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
 
   jvs_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['Jvs']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Jvs']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'billings-administration'}]}

+ 21 - 27
config/abilities/pages/communication.yaml

@@ -1,35 +1,29 @@
   inbox_page:
     action: 'display'
-    services:
-      access:
-        - function: hasAbility
-          parameters:
-            - {action: 'read', subject: 'mails'}
-            - {action: 'read', subject: 'emails'}
-            - {action: 'read', subject: 'texto'}
-      organization:
-        - {function: hasModule, parameters: ['MessagesAdvanced']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['MessagesAdvanced']}
+      - function: accessHasAnyRoleAbility
+        parameters:
+          - {action: 'read', subject: 'mails'}
+          - {action: 'read', subject: 'emails'}
+          - {action: 'read', subject: 'texto'}
 
   message_send_page:
     action: 'display'
-    services:
-      access:
-        - function: hasAbility
-          parameters:
-            - {action: 'read', subject: 'mails'}
-            - {action: 'read', subject: 'emails'}
-            - {action: 'read', subject: 'texto'}
-      organization:
-        - {function: hasModule, parameters: ['MessagesAdvanced']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['MessagesAdvanced']}
+      - function: accessHasAnyRoleAbility
+        parameters:
+          - {action: 'read', subject: 'mails'}
+          - {action: 'read', subject: 'emails'}
+          - {action: 'read', subject: 'texto'}
 
   message_templates_page:
     action: 'display'
-    services:
-      access:
-        - function: hasAbility
-          parameters:
-            - {action: 'read', subject: 'mails'}
-            - {action: 'read', subject: 'emails'}
-            - {action: 'read', subject: 'texto'}
-      organization:
-        - {function: hasModule, parameters: ['MessagesAdvanced']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['MessagesAdvanced']}
+      - function: accessHasAnyRoleAbility
+        parameters:
+          - {action: 'read', subject: 'mails'}
+          - {action: 'read', subject: 'emails'}
+          - {action: 'read', subject: 'texto'}

+ 51 - 85
config/abilities/pages/cotisations.yaml

@@ -1,135 +1,101 @@
   rate_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationRate', 'CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationRate', 'CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   parameters_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   send_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   state_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   pay_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   check_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   ledger_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   magazine_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCMFAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCMFAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   ventilated_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   pay_erase_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   resume_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationTransmissionState']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationTransmissionState']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   history_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationCall']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationCall']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   call_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationStructure']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationStructure']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   history_structure_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationStructure']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationStructure']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   insurance_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationStructure', 'CotisationTransmissionState']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationStructure', 'CotisationTransmissionState']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   resume_all_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationTransmission']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationTransmission']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
 
   resume_pay_cotisation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'cotisation'}]}
-      organization:
-        - {function: hasModule, parameters: ['CotisationTransmission']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['CotisationTransmission']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'cotisation'}]}

+ 3 - 5
config/abilities/pages/donor.yaml

@@ -1,7 +1,5 @@
   donors_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'donors'}]}
-      organization:
-        - {function: hasModule, parameters: ['Donors']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Donors']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'donors'}]}

+ 21 - 35
config/abilities/pages/educational.yaml

@@ -1,55 +1,41 @@
   criteria_notations_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   education_notation_config_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['AdvancedEducationNotation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['AdvancedEducationNotation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   seizure_period_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   test_seizure_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsSeizure']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsSeizure']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
 
   test_validation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   examen_results_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   education_by_student_validation_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsSeizure']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsSeizure']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}

+ 3 - 5
config/abilities/pages/equipment.yaml

@@ -1,7 +1,5 @@
   equipment_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'equipments'}]}
-      organization:
-        - {function: hasModule, parameters: ['Equipments']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Equipments']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'equipments'}]}

+ 3 - 5
config/abilities/pages/medals.yaml

@@ -1,7 +1,5 @@
   medals_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'medals'}]}
-      organization:
-        - {function: hasModule, parameters: ['Medals']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Medals']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'medals'}]}

+ 38 - 57
config/abilities/pages/myAccount.yaml

@@ -1,101 +1,82 @@
   my_schedule_page:
     action: 'display'
-    services:
-      access:
-        - { function: isAdminAccount, result: false }
+    conditions:
+      - { function: accessIsAdminAccount, expectedResult: false }
 
   attendance_bookings_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [{action: 'write', subject: 'attendances'}] }
-        - { function: isAdminAccount, result: false }
-      organization:
-        - {function: hasModule, parameters: ['Attendances']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Attendances']}
+      # TODO: l'action write existe-t-elle?
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'write', subject: 'attendances'}] }
+      - { function: accessIsAdminAccount, expectedResult: false }
 
   my_attendance_page:
     action: 'display'
-    services:
-      access:
-        - { function: isAdminAccount, result: false }
+    conditions:
+      - { function: accessIsAdminAccount, expectedResult: false }
 
   my_invitation_page:
     action: 'display'
-    services:
-      access:
-        - { function: isAdminAccount, result: false }
+    conditions:
+      - { function: accessIsAdminAccount, expectedResult: false }
 
   my_students_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: [ 'teacher'] }
+    conditions:
+      - { function: accessHasAnyProfile, parameters: [ 'teacher'] }
 
   my_students_education_students_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: [ 'teacher'] }
+    conditions:
+      - { function: accessHasAnyProfile, parameters: [ 'teacher'] }
 
   criteria_notations_page_from_account_menu:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'criterianotation'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'criterianotation'}]}
 
   my_education_students_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: [ 'student'] }
+    conditions:
+      - { function: accessHasAnyProfile, parameters: [ 'student'] }
 
   send_an_email_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: [ 'admin', 'teacher' ] }
+    conditions:
+      - { function: accessHasAnyProfile, parameters: [ 'admin', 'teacher' ] }
 
   my_documents_page:
     action: 'display'
-    services:
-      access:
-        - { function: isAdminAccount, result: false }
+    conditions:
+      - { function: accessIsAdminAccount, expectedResult: false }
 
   my_profile_page:
     action: 'display'
-    services:
-      access:
-        - { function: isAdminAccount, result: false }
+    conditions:
+      - { function: accessIsAdminAccount, expectedResult: false }
 
   adherent_list_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: [ 'member'] }
-      organization:
-        - {function: isShowAdherentList}
-        - {function: hasModule, parameters: ['Users']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['Users'] }
+      - { function: organizationIsShowAdherentList }
+      - { function: accessHasAnyProfile, parameters: ['member'] }
 
   subscription_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: ['admin', 'administratifManager', 'pedagogicManager', 'financialManager']}
-      organization:
-        - {function: hasModule, parameters: ['GeneralConfig']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['GeneralConfig'] }
+      - { function: accessHasAnyProfile, parameters: ['admin', 'administratifManager', 'pedagogicManager', 'financialManager'] }
 
   my_bills_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasProfile, parameters: ['guardian', 'payor']}
+    conditions:
+      - { function: accessHasAnyProfile, parameters: ['guardian', 'payor']}
 
   cmf_licence_person_page:
     action: 'display'
-    services:
-      access:
-        - { function: isAdminAccount, result: false }
-      organization:
-        - {function: isCmf}
+    conditions:
+      - { function: organizationIsCmf }
+      - { function: accessIsAdminAccount, expectedResult: false }

+ 65 - 101
config/abilities/pages/parameters.yaml

@@ -1,154 +1,118 @@
   organization_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'organization'}]}
-      organization:
-        - {function: hasModule, parameters: ['GeneralConfig']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['GeneralConfig']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'organization'}]}
 
   cmf_licence_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'organization'}]}
-      organization:
-        - {function: hasModule, parameters: ['GeneralConfig']}
-        - {function: isCmf}
+    conditions:
+        - { function: organizationIsCmf}
+        - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
+        - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'organization'}] }
 
   parameters_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'general-config'}]}
-      organization:
-        - {function: hasModule, parameters: ['GeneralConfig']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'general-config'}] }
 
   parameters_communication_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+    conditions:
+      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
 
   parameters_student_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
-        - {function: isSchool}
+    conditions:
+      - { function: organizationIsSchool }
+      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
 
   parameters_education_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
-        - { function: isSchool }
+    conditions:
+      - { function: organizationIsSchool }
+      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
 
   parameters_bills_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
-        - { function: isSchool }
+    conditions:
+      - { function: organizationIsSchool }
+      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
 
   parameters_secure_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'GeneralConfig' ] }
+    conditions:
+      - { function: organizationHasAnyModule, parameters: [ 'GeneralConfig' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'general-config' } ] }
 
   place_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'place'}]}
-      organization:
-        - {function: hasModule, parameters: ['GeneralConfig']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'place'}]}
 
   education_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
 
   tag_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'tagg'}]}
-      organization:
-        - {function: hasModule, parameters: ['TaggAdvanced']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['TaggAdvanced']}
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'tagg'}]}
 
   activities_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'activity'}]}
-      organization:
-        - {function: hasModule, parameters: ['GeneralConfig']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['GeneralConfig']}
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'activity'}]}
 
   template_systems_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'general-config'}]}
-      organization:
-        - {function: hasModule, parameters: ['TemplateMessages']}
+    conditions:
+      - { function: organizationHasAnyModule, parameters: ['TemplateMessages']}
+      - { function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'general-config'}]}
 
   billing_settings_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['BillingAdministration']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['BillingAdministration']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'billings-administration'}]}
 
   online_registration_settings_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'onlineregistration-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['IEL']}
-        - {function: isSchool}
+    conditions:
+      - {function: organizationIsSchool}
+      - {function: organizationHasAnyModule, parameters: ['IEL']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'onlineregistration-administration'}]}
 
   transition_next_year_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'pedagogics-administration'}]}
 
   course_duplication_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'manage', subject: 'pedagogics-administration'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'manage', subject: 'pedagogics-administration'}]}
 
   import_page:
     action: 'display'
-    services:
-      access:
-        - function: hasAbility
-          parameters:
-            - {action: 'manage', subject: 'user'}
-            - {action: 'manage', subject: 'equipments'}
-      organization:
-        - function: hasModule
-          parameters:
-            - 'Users'
-            - 'Equipments'
+    conditions:
+      - function: organizationHasAnyModule
+        parameters:
+          - 'Users'
+          - 'Equipments'
+      - function: accessHasAnyRoleAbility
+        parameters:
+          - {action: 'manage', subject: 'user'}
+          - {action: 'manage', subject: 'equipments'}

+ 25 - 35
config/abilities/pages/schedule.yaml

@@ -1,49 +1,39 @@
   agenda_page:
     action: 'display'
-    services:
-      access:
-        - function: hasAbility
-          parameters:
-            - {action: 'read', subject: 'events'}
-            - {action: 'read', subject: 'examens'}
-            - {action: 'read', subject: 'educationalprojects'}
-            - {action: 'read', subject: 'courses'}
-      organization:
-        - function: hasModule
-          parameters:
-            - 'Events'
-            - 'Courses'
-            - 'Examens'
-            - 'EducationalProjects'
+    conditions:
+      - function: organizationHasAnyModule
+        parameters:
+          - 'Events'
+          - 'Courses'
+          - 'Examens'
+          - 'EducationalProjects'
+      - function: accessHasAnyRoleAbility
+        parameters:
+          - {action: 'read', subject: 'events'}
+          - {action: 'read', subject: 'examens'}
+          - {action: 'read', subject: 'educationalprojects'}
+          - {action: 'read', subject: 'courses'}
 
   attendance_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'attendances'}]}
-      organization:
-        - {function: hasModule, parameters: ['Attendances']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Attendances']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'attendances'}]}
 
   course_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'courses' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'Courses' ] }
+    conditions:
+      - { function: organizationHasAnyModule, parameters: [ 'Courses' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'courses' } ] }
 
   exam_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'examens' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'Examens' ] }
+    conditions:
+      - { function: organizationHasAnyModule, parameters: [ 'Examens' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'examens' } ] }
 
   pedagogics_project_page:
     action: 'display'
-    services:
-      access:
-        - { function: hasAbility, parameters: [ { action: 'read', subject: 'educationalprojects' } ] }
-      organization:
-        - { function: hasModule, parameters: [ 'EducationalProjects' ] }
+    conditions:
+      - { function: organizationHasAnyModule, parameters: [ 'EducationalProjects' ] }
+      - { function: accessHasAnyRoleAbility, parameters: [ { action: 'read', subject: 'educationalprojects' } ] }

+ 12 - 20
config/abilities/pages/stats.yaml

@@ -1,31 +1,23 @@
   report_activity_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'statistic'}]}
-      organization:
-        - {function: hasModule, parameters: ['Statistic']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['Statistic']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'statistic'}]}
 
   education_quotas_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
-      organization:
-        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['PedagogicsAdministation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'educationstudent'}]}
 
   fede_stats_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'statistic'}]}
-      organization:
-        - {function: hasModule, parameters: ['StatisticFederation']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['StatisticFederation']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'statistic'}]}
 
   structure_stats_page:
     action: 'display'
-    services:
-      access:
-        - {function: hasAbility, parameters: [{action: 'read', subject: 'statistic'}]}
-      organization:
-        - {function: hasModule, parameters: ['StatisticStructure']}
+    conditions:
+      - {function: organizationHasAnyModule, parameters: ['StatisticStructure']}
+      - {function: accessHasAnyRoleAbility, parameters: [{action: 'read', subject: 'statistic'}]}

+ 32 - 0
layouts/error.vue

@@ -0,0 +1,32 @@
+<template>
+  <v-app dark>
+    <h1 v-if="error.statusCode !== 404">
+      {{ otherError }}
+    </h1>
+  </v-app>
+</template>
+
+<script setup lang="ts">
+  import {navigateTo} from "#app";
+  import Url from "~/services/utils/url";
+
+  const props = defineProps({
+    error: {
+      type: Object,
+      default: null
+    }
+  })
+
+  if(process.client && props.error.statusCode === 404 && process.env.NODE_ENV === 'production') {
+    const runtimeConfig = useRuntimeConfig()
+    navigateTo(Url.join(runtimeConfig.baseUrlAdminLegacy, 'dashboard'), {external: true})
+  }
+
+  const otherError = ref('Une erreur est parvenue')
+</script>
+
+<style scoped>
+h1 {
+  font-size: 20px;
+}
+</style>

+ 29 - 0
models/Organization/DolibarrAccount.ts

@@ -0,0 +1,29 @@
+import ApiResource from "~/models/ApiResource";
+import {Attr, Num, Str, Uid} from "pinia-orm/dist/decorators";
+
+/**
+ * The Dolibarr account of an organization
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/ApiResources/Dolibarr/DolibarrAccount.php
+ */
+export class DolibarrAccount extends ApiResource {
+    static entity = 'dolibarr/account'
+
+    @Uid()
+    declare id: number | string | null
+
+    @Num(0, { notNullable: true })
+    declare organizationId: number
+
+    @Str(null)
+    declare clientNumber: string
+
+    @Str(null)
+    declare product: string
+
+    @Attr({})
+    declare contract: object
+
+    @Attr([])
+    declare bills: Array<object>
+}

+ 29 - 0
models/Organization/MobytUserStatus.ts

@@ -0,0 +1,29 @@
+import ApiResource from "~/models/ApiResource";
+import {Bool, Num, Uid} from "pinia-orm/dist/decorators";
+
+
+/**
+ * The Mobyt user status of an organization
+ *
+ * @see https://gitlab.2iopenservice.com/opentalent/ap2i/-/blob/develop/src/ApiResources/Mobyt/MobytUserStatus.php
+ */
+export class MobytUserStatus extends ApiResource {
+    static entity = 'mobyt/status'
+
+    @Uid()
+    declare id: number | string | null
+
+    @Num(0, { notNullable: true })
+    declare organizationId: number
+
+    @Bool(false, { notNullable: true })
+    declare active: boolean
+
+    @Num(0, { notNullable: true })
+    declare amount: number
+
+    @Num(0, { notNullable: true })
+    declare money: number
+}
+
+

+ 35 - 1
nuxt.config.ts

@@ -28,7 +28,19 @@ export default defineNuxtConfig({
             baseUrlTypo3: '',
             baseUrlMercure: '',
             supportUrl: '',
+            school_product: 'school',
+            school_premium_product: 'school-premium',
+            artist_product: 'artist',
+            artist_premium_product: 'artist-premium',
+            manager_product: 'manager',
+            cmf_network: 'CMF',
+            ffec_network: 'FFEC',
+            OPENTALENT_MANAGER_ID: 93931,
+            CMF_ID: 12097
         }
+    },
+    env: {
+
     },
     hooks: {
       'builder:watch': console.log
@@ -71,6 +83,7 @@ export default defineNuxtConfig({
         '@pinia-orm/nuxt',
         'nuxt-lodash',
         '@nuxtjs/i18n',
+        '@nuxt/image-edge',
     ],
     typescript: {
         strict: true
@@ -98,7 +111,27 @@ export default defineNuxtConfig({
         detectBrowserLanguage: false,
         vueI18n: {
             legacy: false,
-        }
+            datetimeFormats: {
+                'fr-FR': {
+                    short: {
+                        year: 'numeric', month: 'numeric', day: 'numeric'
+                    },
+                    long: {
+                        year: 'numeric', month: 'numeric', day: 'numeric',
+                        hour: 'numeric', minute: 'numeric'
+                    }
+                },
+                'en': {
+                    short: {
+                        year: 'numeric', month: 'numeric', day: 'numeric'
+                    },
+                    long: {
+                        year: 'numeric', month: 'numeric', day: 'numeric',
+                        hour: 'numeric', minute: 'numeric'
+                    }
+                }
+            }
+        },
     } as I18nOptions,
     build: {
         transpile: ['vuetify'],
@@ -119,5 +152,6 @@ export default defineNuxtConfig({
                 protocol: 'wss'
             }
         },
+        ssr: { noExternal: ["moment"], }
     }
 })

+ 3 - 2
package.json

@@ -53,7 +53,7 @@
     "@casl/vue": "^2.2.0",
     "@fortawesome/fontawesome-free": "^6.2.1",
     "@mdi/font": "^7.0.96",
-    "@nuxt/image": "^0.7.1",
+    "@nuxt/image-edge": "^1.0.0-27840416.dc1ed65",
     "@nuxtjs/i18n": "^8.0.0-beta.7",
     "@pinia-orm/nuxt": "^1.1.4",
     "@pinia/nuxt": "^0.4.3",
@@ -64,6 +64,7 @@
     "event-source-polyfill": "^1.0.31",
     "js-yaml": "^4.1.0",
     "libphonenumber-js": "^1.10.14",
+    "moment": "^2.29.4",
     "nuxt": "^3.0.0",
     "nuxt-lodash": "^2.4.1",
     "pinia": "^2.0.28",
@@ -72,7 +73,7 @@
     "uuid": "^9.0.0",
     "vue-tel-input-vuetify": "^1.5.3",
     "vue-the-mask": "^0.11.1",
-    "vuetify": "^3.0.7",
+    "vuetify": "^3.1.1",
     "yaml-import": "^2.0.0"
   }
 }

+ 376 - 0
pages/subscription.vue

@@ -0,0 +1,376 @@
+<!--
+Page 'Mon abonnement'
+
+@see https://ressources.opentalent.fr/display/SPEC/Mon+abonnement
+-->
+<template>
+  <LayoutContainer>
+    <v-col cols="12" sm="12" md="12">
+      <v-expansion-panels :multiple="true" v-model="openedPanels">
+        <UiExpansionPanel title="informations" icon="fas fa-info">
+          <v-container fluid class="container">
+            <v-row>
+              <v-table>
+                <tbody>
+                <tr>
+                  <td v-if="smAndUp">{{ $t('client_id') }}</td>
+                  <td class="py-2">
+                    <h5 v-if="!smAndUp" class="text-decoration-underline py-2">{{ $t('client_id') }}</h5>
+                    <span>{{ dolibarrAccount ? dolibarrAccount.clientNumber : '-' }}</span>
+                  </td>
+                </tr>
+                <tr>
+                  <td v-if="smAndUp">{{ $t('version') }}</td>
+                  <td class="py-2">
+                    <h5 v-if="!smAndUp" class="text-decoration-underline py-2">{{ $t('version') }}</h5>
+                    <span>{{ dolibarrAccount ? $t(dolibarrAccount.product) : '-' }}</span>
+                  </td>
+                </tr>
+                <tr v-if="dolibarrAccount && dolibarrAccount.contract">
+                  <td v-if="smAndUp">{{ $t('services') }}</td>
+                  <td class="py-2">
+                    <h5 v-if="!smAndUp" class="text-decoration-underline py-2">{{ $t('services') }}</h5>
+                    <div
+                        v-for="line in dolibarrAccount.contract.lines"
+                        :key="line.id"
+                    >
+                      {{ line.serviceLabel }} - {{ $t('until') }} :
+                      {{ $d(line.dateEnd) }}
+                    </div>
+                  </td>
+                </tr>
+                <tr v-if="$can('manage', 'texto')">
+                  <td v-if="smAndUp">{{ $t('remaining_sms_credit') }}</td>
+                  <td class="py-2">
+                    <h5 v-if="!smAndUp" class="text-decoration-underline py-2">{{ $t('remaining_sms_credit') }}</h5>
+                    <span v-if="!mobytPending && mobytStatus !== null && mobytStatus.active">
+                      {{ mobytStatus.money.toLocaleString($i18n.locale, { style: 'currency', currency: 'EUR' }) }}
+                      {{ $t('convert_price_to_sms', { nb_sms: mobytStatus.amount }) }}
+                    </span>
+                  </td>
+                </tr>
+                </tbody>
+              </v-table>
+            </v-row>
+          </v-container>
+        </UiExpansionPanel>
+
+        <UiExpansionPanel v-if="showDolibarrPanel" title="bills" icon="fas fa-file">
+          <v-container fluid class="container">
+            <v-row>
+              <v-table>
+                <thead>
+                <tr>
+                  <th>{{ $t('reference') }}</th>
+                  <th>{{ $t('date') }}</th>
+                  <th>{{ $t('taxExcludedAmount') }}</th>
+                  <th>{{ $t('status') }}</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr
+                    v-for="bill in dolibarrAccount.bills"
+                    :key="bill.id"
+                >
+                  <td>{{ bill.ref }}</td>
+                  <td>{{ $d(bill.date) }}</td>
+                  <td>{{ bill.taxExcludedAmount.toLocaleString($i18n.locale, { style: 'currency', currency: 'EUR' }) }}</td>
+                  <td>{{ bill.paid === true ? $t('paid') : $t('unpaid') }}</td>
+                </tr>
+                </tbody>
+              </v-table>
+            </v-row>
+          </v-container>
+        </UiExpansionPanel>
+
+        <UiExpansionPanel title="more_features" icon="fas fa-plus">
+          <v-container id="products-section" fluid class="container">
+            <v-row>
+              <!-- Opentalent Artist Premium -->
+              <v-col v-if="1 || organizationProfile.isArtistProduct" cols="3">
+                <v-row>
+                  {{ $t('PRODUCT_ARTIST_PREMIUM') }}
+                </v-row>
+                <v-row>
+                  <nuxt-img src="/images/Artist-Square.jpg" />
+                </v-row>
+                <v-row>
+                  <p>
+                    {{ $t('get_more_functionalities_with_version') }} <b>{{ $t('PRODUCT_ARTIST_PREMIUM') }}</b>
+                  </p>
+
+                  <!-- Cmf member -->
+                  <div v-if="organizationProfile.isCmf" cols="3">
+                    <p>
+                      <b>{{ $t('for_only_x_eur_ttc_by_month', { price: formatCurrency(7.25, 'EUR') }) }}&nbsp*</b>
+                    </p>
+                    <div><i>*&nbsp{{ $t('yearly_paid_giving_x_eur_ttc_per_year', { price: formatCurrency(87.00, 'EUR') }) }}</i></div>
+                    <div><i>{{ $t('only_for_cmf_members') }} ({{ $t('public_price_x_ttc_a_year', { price: formatCurrency(168.00, 'EUR') }) }})</i></div>
+                  </div>
+
+                  <!-- Not a cmf member -->
+                  <div v-else>
+                    <p>
+                      <b>{{ $t('for_only_x_eur_ttc_by_month', { price: formatCurrency(14.00, 'EUR') }) }}&nbsp*</b>
+                    </p>
+                    <p>
+                      <i>
+                        *&nbsp{{ $t('yearly_paid_giving_x_eur_ttc_per_year', { price: formatCurrency(168.00, 'EUR') }) }}
+                      </i>
+                    </p>
+                  </div>
+
+                  <p class="mt-3">
+                    <a href="/resources/Fiche_produit_Opentalent_Artist.pdf" target="_blank">
+                      {{ $t('product_sheet') }} {{ $t('PRODUCT_ARTIST_PREMIUM') }}
+                    </a>
+                  </p>
+
+                  <p v-if="organizationProfile.isCmf" class="mt-3">
+                    <a href="/resources/BDC_Artist_Premium_CMF.pdf">
+                      <b>{{ $t('download_cmf_order_form') }}</b>
+                    </a>
+                  </p>
+                  <p v-else class="mt-3">
+                    <a href="/resources/BDC_Artist_Premium_Public.pdf">
+                      <b>{{ $t('download_order_form') }}</b>
+                    </a>
+                  </p>
+                </v-row>
+              </v-col>
+
+              <!-- Opentalent School Premium -->
+              <v-col v-if="1 || organizationProfile.isArtist" cols="3">
+                <v-row>
+                  {{ $t('PRODUCT_SCHOOL') }}
+                </v-row>
+                <v-row>
+                  <nuxt-img src="/images/School-Square.jpg" />
+                </v-row>
+                <v-row>
+                  <p>
+                    {{ $t('switch_to_version') }} <b>{{ $t('PRODUCT_SCHOOL_PREMIUM') }}</b> {{ $t('and_benefit') }} :
+                  </p>
+
+                  <ul class="mb-2">
+                    <li>{{ $t('of_accounts_for_teachers_and_students') }}</li>
+                    <li>{{ $t('of_a_complete_website') }}</li>
+                  </ul>
+
+                  <!-- Cmf member -->
+                  <div v-if="organizationProfile.isCmf">
+                    <p><b>{{ $t('starting_from_x_eur_ttc_per_month', { price: formatCurrency(26.50, 'EUR') }) }}&nbsp*</b></p>
+                    <div><i>* {{ $t('yearly_paid_giving_x_eur_ttc_per_year', { price: formatCurrency(318.00, 'EUR') }) }}</i></div>
+                    <div><i>{{ $t('version_x_up_to_x_students', { product: $t('PRODUCT_SCHOOL_PREMIUM'), max_students: '69' }) }}</i></div>
+                    <div><i>{{ $t('excluding_license_and_training_fees') }}.</i></div>
+                    <div><i>{{ $t('only_for_cmf_members') }} ({{ $t('public_price_x_ttc_a_year', { price: formatCurrency(529.20, 'EUR') }) }})</i></div>
+                  </div>
+                  <!-- Not cmf member -->
+                  <div v-else>
+                    <p>{{ $t('starting_from_x_eur_ttc_per_month', { price: formatCurrency(44.10, 'EUR') }) }}&nbsp*</p>
+                    <div><i>*&nbsp{{ $t('yearly_paid_giving_x_eur_ttc_per_year', { price: formatCurrency(529.20, 'EUR') }) }}</i></div>
+                    <div><i>{{ $t('version_x_up_to_x_students', { product: $t('PRODUCT_SCHOOL_PREMIUM'), max_students: '69' }) }}</i></div>
+                    <div><i>{{ $t('excluding_license_and_training_fees') }}.</i></div>
+                  </div>
+
+                  <p class="mt-4">
+                    <a href="/resources/Fiche_produit_Opentalent_School.pdf" target="_blank">
+                      {{ $t('product_sheet') }} {{ $t('PRODUCT_SCHOOL') }}
+                    </a>
+                  </p>
+
+                  <p>
+                    {{ $t('contact_us_at') }} <a href="tel:+33972126017">0 972 126 017</a>, {{ $t('or_by_mail_at') }}
+                    <a href="mailto:contact@opentalent.fr">contact@opentalent.fr</a>
+                  </p>
+                </v-row>
+              </v-col>
+
+              <!-- SMS -->
+              <v-col cols="3">
+                <v-row>
+                  {{ $t('sms') }}
+                </v-row>
+                <v-row>
+                  <nuxt-img src="/images/sms_big.png" :height="100" :width="100"/>
+                </v-row>
+                <v-row>
+                  <p><b>{{ $t('send_sms') }} {{ $t('to_your_members_from_app') }}</b></p>
+
+                  <!-- Cmf member -->
+                  <div v-if="organizationProfile.isCmf">
+                    <p><b>{{ $t('starting_from_x_eur_ttc_per_sms', { price: formatCurrency(0.10, 'EUR') }) }}&nbsp*</b></p>
+                    <p><i>*&nbsp{{ $t('for_x_sms', { amount: '5000' }) }}</i></p>
+
+                    <p>
+                      <b>
+                        <a href="/resources/BDC_SMS_CMF.pdf" target="_blank">
+                          {{ $t('download_cmf_order_form') }}
+                        </a>
+                      </b>
+                    </p>
+                  </div>
+                  <!-- Not cmf member -->
+                  <div v-else>
+                    <p><b>{{ $t('starting_from_x_eur_ttc_per_sms', { price: formatCurrency(0.12, 'EUR') }) }}&nbsp*</b></p>
+                    <p><i>*&nbsp{{ $t('for_x_sms', { amount: '5000' }) }}</i></p>
+
+                    <p>
+                      <a href="/resources/BDC_SMS_Public.pdf" target="_blank">
+                        <b>{{ $t('download_order_form') }}</b>
+                      </a>
+                    </p>
+                  </div>
+                </v-row>
+              </v-col>
+
+              <!-- Custom domain -->
+              <v-col cols="3">
+                <v-row>
+                  {{ $t('website') }}
+                </v-row>
+                <v-row>
+                  <nuxt-img src="/images/nom-de-domaine.jpg" />
+                </v-row>
+                <v-row>
+                  <p>
+                    <b>{{ $t('get_your_own_domain_and_up_to_five_emails_for_only_x_eur_ttc_per_month', { price: formatCurrency(34.80, 'EUR') }) }}</b>
+                  </p>
+
+                  <p>{{ $t('example') }} :</p>
+                  <table>
+                    <tbody>
+                    <tr>
+                      <td style="width: 190px;">
+                        {{ $t('domain_name') }} :
+                      </td>
+                      <td>
+                        <i>{{ $t('dummy_domain_name') }}</i>
+                      </td>
+                    </tr>
+                    <tr>
+                      <td>{{ $t('associated_mail_address') }} : </td>
+                      <td>
+                        <i>{{ $t('dummy_email_address') }}</i>
+                      </td>
+                    </tr>
+                    </tbody>
+                  </table>
+
+                  <p>
+                    <a href="/resources/BDC_Nom_de_domaine.pdf" target="_blank">
+                      <b>{{ $t('download_order_form') }}</b>
+                    </a>
+                  </p>
+                </v-row>
+              </v-col>
+            </v-row>
+          </v-container>
+        </UiExpansionPanel>
+      </v-expansion-panels>
+    </v-col>
+  </LayoutContainer>
+</template>
+
+<script setup lang="ts">
+  import {useAbility} from "@casl/vue";
+  import {useOrganizationProfileStore} from "~/stores/organizationProfile";
+  import {useEntityFetch} from "~/composables/data/useEntityFetch";
+  import {DolibarrAccount} from "~/models/Organization/DolibarrAccount";
+  import {MobytUserStatus} from "~/models/Organization/MobytUserStatus";
+  import {Ref} from "@vue/reactivity";
+  import {useDisplay} from "vuetify";
+
+  const ability = useAbility()
+
+  onBeforeMount(() => {
+    if(!ability.can('display', 'subscription_page'))
+      return navigateTo('/error')
+  })
+
+  const showDolibarrPanel = computed(() => !dolibarrPending.value && dolibarrAccount.value && dolibarrAccount.value.bills.length > 0)
+
+  const { smAndUp } = useDisplay()
+
+  // On déplie les expansion panels dans le onMounted en attendant la résolution du bug : https://github.com/vuetifyjs/vuetify/issues/16427#issuecomment-1380927133
+  // TODO: quand le bug ci dessus est résolu, remplacer par `const openedPanels: Ref<Array<string>> = ref(['informations', 'bills', 'more_features'])`
+  const openedPanels: Ref<Array<string>> = ref([])
+  onMounted(() => {
+    openedPanels.value = ['informations', 'bills', 'more_features']
+  })
+
+  const i18n = useI18n()
+  const organizationProfile = useOrganizationProfileStore()
+  if (organizationProfile.id === null) {
+    throw new Error("Missing organization's id")
+  }
+
+  const { fetch } = useEntityFetch()
+
+  const { data: dolibarrAccount, pending: dolibarrPending } = fetch(DolibarrAccount, organizationProfile.id)
+
+  if (ability.can('manage', 'texto')) {
+    const {data: mobytStatus, pending: mobytPending} = fetch(MobytUserStatus, organizationProfile.id)
+  } else {
+    const mobytStatus = ref(null)
+    const mobytPending = ref(false)
+  }
+
+  const formatCurrency = (value: Number, currency: string): string => {
+    return value.toLocaleString(i18n.locale.value, { style: 'currency', currency: currency })
+  }
+</script>
+
+<style scoped lang="scss">
+  #products-section {
+    width: 100%;
+
+    .v-col {
+      min-width: 260px;
+      border: solid 1px #e0e0e0;
+
+      .v-row:nth-child(1) {
+        background: rgb(var(--v-theme-ot-green));
+        height: 64px;
+        color: white;
+        font-size: 15px;
+        font-weight: bold;
+      }
+
+      .v-row:nth-child(2) {
+        height: 230px;
+        display: flex;
+        justify-content: center;
+      }
+
+      .v-row:nth-child(3) {
+      }
+    }
+
+    .v-col:not(:first-child) {
+      border-left: none;
+    }
+
+    img {
+      max-height: 90%;
+      max-width: 90%;
+    }
+
+    .v-row {
+      padding: 12px 18px;
+      vertical-align: top;
+      border-bottom: solid 1px #e0e0e0;
+    }
+    .v-row:last-child {
+      border: none;
+    }
+
+    p {
+      margin-bottom: 12px;
+    }
+
+    ul {
+      padding-left: 24px;
+    }
+  }
+</style>

+ 2 - 2
plugins/ability.ts

@@ -1,5 +1,5 @@
 import { createMongoAbility } from '@casl/ability'
-import AbilityUtils from '~/services/rights/abilityUtils'
+import AbilityBuilder from '~/services/rights/abilityBuilder'
 import {defineNuxtPlugin} from "nuxt/app";
 import {useAccessProfileStore} from "~/stores/accessProfile";
 import {useOrganizationProfileStore} from "~/stores/organizationProfile";
@@ -10,7 +10,7 @@ export default defineNuxtPlugin(() => {
     const accessProfile = useAccessProfileStore()
     const organizationProfile = useOrganizationProfileStore()
 
-    const abilityUtils = new AbilityUtils(ability, accessProfile, organizationProfile)
+    const abilityUtils = new AbilityBuilder(ability, accessProfile, organizationProfile)
 
     abilityUtils.setupAbilities()
 })

+ 8 - 7
plugins/init.server.ts

@@ -12,19 +12,20 @@ export default defineNuxtPlugin(async ({ssrContext}) => {
 
     const accessProfile = useAccessProfileStore()
 
-    if (accessId.value !== null) {
-        accessProfile.$patch({
-            bearer: bearer.value,
-            id: parseInt(accessId.value)
-        })
-    } else {
+    if (accessId.value === null) {
         redirectToLogin()
+        return
     }
 
+    accessProfile.$patch({
+        bearer: bearer.value,
+        id: parseInt(accessId.value)
+    })
+
     const {em} = useEntityManager()
 
     try {
-        await em.refreshProfile()
+        await em.refreshProfile(parseInt(accessId.value))
     } catch (error) {
         if (error instanceof UnauthorizedError) {
             redirectToLogin()

+ 4 - 12
services/data/entityManager.ts

@@ -74,6 +74,8 @@ class EntityManager {
         const repository = this.getRepository(model)
 
         let entity = repository.make(properties)
+
+        // Keep track of the entitie's model
         entity.setModel(model)
 
         // @ts-ignore
@@ -284,18 +286,8 @@ class EntityManager {
      *
      * Re-fetch the user profile and update the store
      */
-    public async refreshProfile() {
-        const response = await this.apiRequestService.get('api/my_profile')
-
-        // deserialize the response
-        const hydraResponse = await HydraDenormalizer.denormalize(response)
-
-        const profileData = hydraResponse.data
-
-        // On n'aura jamais 2 profils stockés, et on a besoin d'un id pour retrouver le profil dans le store :
-        profileData['id'] = 1
-
-        const profile = this.newInstance(MyProfile, hydraResponse.data)
+    public async refreshProfile(accessId: number) {
+        const profile = await this.fetch(MyProfile, accessId)
 
         // On met à jour le store accessProfile
         const accessProfileStore = useAccessProfileStore()

+ 0 - 1
services/data/imageManager.ts

@@ -50,7 +50,6 @@ class ImageManager {
         const query = [new Date().getTime().toString()]
 
         const response: any = await this.apiRequestService.get(imageUrl, query)
-        // console.log(response)
 
         if(!response || response.size === 0) {
             console.error('Error: image ' + id + ' not found or invalid')

+ 308 - 0
services/rights/abilityBuilder.ts

@@ -0,0 +1,308 @@
+import RoleUtils from '~/services/rights/roleUtils'
+import {AbilitiesType} from '~/types/interfaces'
+import YamlDenormalizer from "~/services/data/serializer/denormalizer/yamlDenormalizer";
+import {MongoAbility} from "@casl/ability/dist/types/Ability";
+import {useEach} from "#imports";
+import {ABILITIES} from "~/types/enum/enums";
+
+interface Condition {
+    function: string
+    parameters?: Array<any>
+    expectedResult?: any
+}
+
+
+/**
+ * Classe permettant de mener des opérations sur les habilités
+ */
+class AbilityBuilder {
+    private readonly ability: MongoAbility = {} as MongoAbility
+    private readonly accessProfile: any
+    private readonly organizationProfile: any
+
+    private readonly configDir = './config/abilities/config.yaml'
+
+    private abilities: Array<AbilitiesType> = []
+
+    /**
+     * @constructor
+     */
+    constructor(
+        ability: MongoAbility,
+        accessProfile: any,
+        organizationProfile: any,
+    ) {
+        this.ability = ability
+        this.accessProfile = accessProfile
+        this.organizationProfile = organizationProfile
+    }
+
+    /**
+     * Construit les habilités de l'utilisateur selon son profil et met à jour MongoAbility en fonction
+     */
+    setupAbilities() {
+        // Nécessaire pour que l'update des habilités soit correcte après la phase SSR
+        this.ability.update(this.accessProfile.abilities)
+
+        // Au moment où l'on effectue une action organizationProfileStore.setProfile, il faut aller récupérer
+        // les différentes habilités que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
+        const unsubscribe = this.organizationProfile.$onAction(({
+                                                                    name, // name of the action
+                                                                    store, // store instance, same as `someStore`
+                                                                    args, // array of parameters passed to the action
+                                                                    after, // hook after the action returns or resolves
+                                                                    onError, // hook if the action throws or rejects
+                                                                }: any) => {
+            after((result: any)=>{
+                if (name === 'setProfile'){
+                    //On construit les habilités
+                    this.buildAbilities();
+
+                    //On les store puis on update le service ability pour le mettre à jour.
+                    this.accessProfile.abilities = this.abilities
+
+                    // Unsubscribe pour éviter les memory leaks
+                    unsubscribe()
+                }
+            })
+        })
+    }
+
+    /**
+     * Construit et renvoie l'ensemble des habilités de l'utilisateur, qu'elles soient issues de ses roles
+     * ou de la configuration
+     *
+     * @return {Array<AbilitiesType>}
+     */
+    buildAbilities() {
+        // Build from roles
+        this.abilities = this.buildAbilitiesFromRoles()
+        this.ability.update(this.abilities)
+
+        // Build from config
+        this.abilities = this.abilities.concat(this.buildAbilitiesFromConfig())
+        this.ability.update(this.abilities)
+    }
+
+    /**
+     * Adaptation et transformations des roles symfony en abilities Casl
+     */
+    buildAbilitiesFromRoles() {
+        return RoleUtils.rolesToAbilities(this.accessProfile.roles)
+    }
+
+    /**
+     * Charge les habilités depuis les fichiers de configuration
+     */
+    buildAbilitiesFromConfig() {
+        const abilitiesByConfig: Array<AbilitiesType> = []
+
+        const doc = YamlDenormalizer.denormalize({path: this.configDir})
+        const fromConfig = doc.abilities
+
+        useEach(fromConfig, (ability: { action: ABILITIES, conditions: Array<Condition> }, subject: string) => {
+            let { action, conditions } = ability
+
+            if (!Array.isArray(conditions)) {
+                // Special: la denormalization ne produit pas une array s'il n'y a qu'un seul élément
+                conditions = [conditions]
+            }
+
+            if (this.hasConfigAbility(conditions as Array<Condition>, subject)) {
+                abilitiesByConfig.push({ action, subject })
+            }
+        })
+
+        return abilitiesByConfig
+    }
+
+    /**
+     * Parcourt les services définis dans la configuration, et établit si oui ou non l'habilité est autorisée
+     *
+     * @return {boolean}
+     * @param conditions  Les conditions à l'obtention de l'habileté, telles que définies dans les fichiers de config
+     * @param subject  For debugging purpose only
+     */
+    hasConfigAbility(conditions: Array<Condition>, subject: string = '') {
+        return conditions.every((condition) => this.execAndValidateCondition(condition, subject))
+    }
+
+    /**
+     * Correspondances entre les noms des fonctions définies dans les conditions des fichiers de configuration et
+     * les méthodes correspondantes
+     *
+     * TODO: voir pourquoi on a besoin d'accepter un param null pour le hasProfile?
+     */
+    handlerMap: any = {
+        accessHasAllRoleAbilities: (parameters: any) => this.hasAllRoleAbilities(parameters),
+        accessHasAnyRoleAbility: (parameters: any) => this.hasAnyRoleAbility(parameters),
+        accessHasAnyProfile: (parameters: any) => parameters === null || this.hasAnyProfile(parameters),
+        accessHasAllModules: (parameters: any) => this.hasAllModules(parameters),
+        organizationHasAnyModule: (parameters: any) => this.hasAnyModule(parameters),
+        accessIsAdminAccount: (parameters: any) => this.accessProfile.isAdminAccount,
+        organizationIsSchool: (parameters: any) => this.organizationProfile.isSchool,
+        organizationIsArtist: (parameters: any) => this.organizationProfile.isArtist,
+        organizationIsManagerProduct: (parameters: any) => this.organizationProfile.isManagerProduct,
+        organizationHasChildren: (parameters: any) => this.organizationProfile.hasChildren,
+        organizationIsAssociation: (parameters: any) => this.organizationProfile.isAssociation,
+        organizationIsShowAdherentList: (parameters: any) => this.organizationProfile.isShowAdherentList,
+        organizationIsCmf: (parameters: any) => this.organizationProfile.isCmf,
+        organizationHasWebsite: (parameters: any) => this.organizationProfile.getWebsite,
+    }
+
+    /**
+     * Exécute la fonction associée à la condition, et compare le résultat obtenu au résultat attendu (true par défaut)
+     *
+     * @param condition  Un condition à la possession d'une habilité, telle que définie dans les fichiers de config
+     * @param subject  For debugging purpose only
+     * @private
+     */
+    private execAndValidateCondition(
+        condition: Condition,
+        subject: string = ''
+    ) {
+        const expectedResult: boolean = condition.expectedResult ?? true;
+        const parameters = condition.parameters ?? []
+
+        if (!(condition.function in this.handlerMap)) {
+            throw new Error('unknown condition function : ' + condition.function)
+        }
+        const actualResult = this.handlerMap[condition.function](parameters ?? null)
+
+        return actualResult === expectedResult
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède l'habilité en paramètre
+     *
+     * @return {boolean}
+     * @param ability
+     */
+    hasRoleAbility(ability: AbilitiesType): boolean {
+        return this.ability.can(ability.action, ability.subject)
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède toutes les habilités passées en paramètre
+     *
+     * @param {Array<AbilitiesType>} abilities Habilités à tester
+     * @return {boolean}
+     */
+    hasAllRoleAbilities(abilities: Array<AbilitiesType>): boolean {
+        return abilities.every(ability => this.hasRoleAbility(ability))
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède au moins l'une des habilités passées en paramètre
+     *
+     * @param {Array<AbilitiesType>} abilities Habilités à tester
+     * @return {boolean}
+     */
+    hasAnyRoleAbility(abilities: Array<AbilitiesType>): boolean {
+        return abilities.some(ability => this.hasRoleAbility(ability))
+    }
+
+    /**
+     * Teste si l'utilisateur possède le profil donné
+     *
+     * @param {string} profile Profil à tester
+     * @return {boolean}
+     */
+    hasProfile(profile: string): boolean {
+        return {
+            'admin': this.accessProfile.isAdmin,
+            'administratifManager': this.accessProfile.isAdministratifManager,
+            'pedagogicManager': this.accessProfile.isPedagogicManager,
+            'financialManager': this.accessProfile.isFinancialManager,
+            'caMember': this.accessProfile.isCaMember,
+            'student': this.accessProfile.isStudent,
+            'teacher': this.accessProfile.isTeacher,
+            'member': this.accessProfile.isMember,
+            'other': this.accessProfile.isOther,
+            'guardian': this.accessProfile.isGuardian,
+            'payor': this.accessProfile.isPayer,
+        }[profile] ?? false
+    }
+
+    /**
+     * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre
+     *
+     * @param {Array<string>} profiles Profils à tester
+     * @return {boolean}
+     */
+    hasAnyProfile (profiles: Array<string>): boolean {
+        return profiles.some(p => this.hasProfile(p))
+    }
+
+    /**
+     * Retourne vrai si l'utilisateur connecté possède tous les profils passés en paramètre
+     *
+     * @param {Array<string>} profiles Profils à tester
+     * @return {boolean}
+     */
+    hasAllProfiles (profiles: Array<string>): boolean {
+        return profiles.every(p => this.hasProfile(p))
+    }
+
+    /**
+     * Est-ce que l'utilisateur possède le rôle donné ?
+     *
+     * @return {boolean}
+     * @param role
+     */
+    hasRole(role: string): boolean {
+        return this.accessProfile.hasRole(role)
+    }
+
+    /**
+     * L'utilisateur possède-t-il au moins l'un des rôles donnés
+     *
+     * @return {boolean}
+     * @param roles
+     */
+    hasAnyRole(roles: Array<string>): boolean {
+        return roles.some(r => this.hasRole(r))
+    }
+
+    /**
+     * L'utilisateur possède-t-il tous les rôles donnés
+     *
+     * @return {boolean}
+     * @param roles
+     */
+    hasAllRoles(roles: Array<string>): boolean {
+        return roles.every(r => this.hasRole(r))
+    }
+
+    /**
+     * Est-ce que l'organisation possède le module donné
+     *
+     * @return {boolean}
+     * @param module
+     */
+    hasModule(module: string): boolean {
+        return this.organizationProfile.hasModule(module)
+    }
+
+    /**
+     * Est-ce que l'organisation possède au moins un des modules donnés
+     *
+     * @param modules
+     * @return {boolean}
+     */
+    hasAnyModule(modules: Array<string>): boolean {
+        return modules.some(r => this.hasModule(r))
+    }
+
+    /**
+     * Est-ce que l'organisation possède-t-il tous les modules donnés
+     *
+     * @param modules
+     * @return {boolean}
+     */
+    hasAllModules(modules: Array<string>): boolean {
+        return modules.every(r => this.hasModule(r))
+    }
+}
+
+export default AbilityBuilder

+ 0 - 249
services/rights/abilityUtils.ts

@@ -1,249 +0,0 @@
-import RoleUtils from '~/services/rights/roleUtils'
-import {AbilitiesType} from '~/types/interfaces'
-import YamlDenormalizer from "~/services/data/serializer/denormalizer/yamlDenormalizer";
-import {MongoAbility} from "@casl/ability/dist/types/Ability";
-import {AnyJson} from "~/types/data";
-import {useEach} from "#imports";
-import {ABILITIES} from "~/types/enum/enums";
-
-/**
- * Classe permettant de mener des opérations sur les habilités
- */
-class AbilityUtils {
-    private readonly ability: MongoAbility = {} as MongoAbility
-    private readonly accessProfile: any
-    private readonly organizationProfile: any
-
-    /**
-     * @constructor
-     */
-    constructor(
-        ability: MongoAbility,
-        accessProfile: any,
-        organizationProfile: any,
-    ) {
-        this.ability = ability
-        this.accessProfile = accessProfile
-        this.organizationProfile = organizationProfile
-    }
-
-    /**
-     * Définit les abilities de l'utilisateur selon son profil
-     */
-    setupAbilities() {
-        // Nécessaire pour que l'update des habilités soit correcte après la phase SSR
-        this.ability.update(this.accessProfile.abilities)
-
-        // const abilities: Array<AbilitiesType> = this.buildAbilities();
-        // this.accessProfile.abilities = abilities
-        // this.ability.update(abilities)
-
-        // Au moment où l'on effectue une action organizationProfileStore.setProfile, il faut aller récupérer
-        // les différentes habilités que l'utilisateur peut effectuer. (Tout cela se passe en SSR)
-        const unsubscribe = this.organizationProfile.$onAction(({
-                                                                    name, // name of the action
-                                                                    store, // store instance, same as `someStore`
-                                                                    args, // array of parameters passed to the action
-                                                                    after, // hook after the action returns or resolves
-                                                                    onError, // hook if the action throws or rejects
-                                                                }: any) => {
-            after((result: any)=>{
-                if (name === 'setProfile'){
-                    //On récupère les habilités
-                    const abilities: Array<AbilitiesType> = this.buildAbilities();
-
-                    //On les store puis on update le service ability pour le mettre à jour.
-                    this.accessProfile.abilities = abilities
-                    this.ability.update(abilities)
-
-                    // Unsubscribe pour éviter les memory leaks
-                    unsubscribe()
-                }
-            })
-        })
-    }
-
-    /**
-     * Récupération de l'ensemble des habilités de l'utilisateur, qu'elles soient par Roles ou par Config
-     *
-     * @return {Array<AbilitiesType>}
-     */
-    buildAbilities(): Array<AbilitiesType> {
-        const abilitiesByRoles: Array<AbilitiesType> = this.buildAbilitiesFromRoles(this.accessProfile.roles)
-        const abilitiesByConfig = this.buildAbilitiesFromConfig('./config/abilities/config.yaml')
-        return abilitiesByRoles.concat(abilitiesByConfig)
-    }
-
-    /**
-     * Adaptation et transformations des roles symfony en abilities Casl
-     *
-     * @param {Array<string>} roles
-     * @return {Array<AbilitiesType>}
-     */
-    buildAbilitiesFromRoles(roles: Array<string>): Array<AbilitiesType> {
-        return RoleUtils.rolesToAbilities(roles)
-    }
-
-    /**
-     * Charge les habilités depuis les fichiers de configuration
-     *
-     * @param {string} configPath
-     * @return {Array<AbilitiesType>}
-     */
-    buildAbilitiesFromConfig(configPath: string): Array<AbilitiesType> {
-        const doc = YamlDenormalizer.denormalize({path: configPath})
-        const fromConfig = doc.abilities
-
-        const abilities: Array<AbilitiesType> = []
-
-        useEach(fromConfig, (ability: { action: ABILITIES, services: object }, subject: string) => {
-            const { action, services } = ability
-            if (this.hasConfigAbility(services)) {
-                abilities.push({ action, subject })
-            }
-        })
-
-        return abilities
-    }
-
-    /**
-     * Parcourt les services définis dans la configuration, et établit si oui ou non l'habilité est autorisée
-     *
-     * @return {boolean}
-     * @param services
-     */
-    hasConfigAbility(services: AnyJson) {
-        const handlerMap: any = {
-            hasRole: (parameters: any) => this.hasRoles(parameters),
-            hasAbility: (parameters: any) => this.hasAbilities(parameters),
-            hasProfile: (parameters: any) => this.hasProfileAmong(parameters),
-            isAdminAccount: (parameters: any) => this.accessProfile.isAdminAccount,
-            hasModule: (parameters: any) => this.hasModule(parameters),
-            isSchool: (parameters: any) => this.organizationProfile.isSchool,
-            isArtist: (parameters: any) => this.organizationProfile.isArtist,
-            isManagerProduct: (parameters: any) => this.organizationProfile.isManagerProduct,
-            isOrganizationWithChildren: (parameters: any) => this.organizationProfile.hasChildren,
-            isAssociation: (parameters: any) => this.organizationProfile.isAssociation,
-            isShowAdherentList: (parameters: any) => this.organizationProfile.isShowAdherentList,
-            isCmf: (parameters: any) => this.organizationProfile.isCmf,
-            getWebsite: (parameters: any) => this.organizationProfile.getWebsite,
-        }
-
-        let hasAbility = true
-
-        useEach(services, (handlers: Array<{ function: string, parameters?: Array<any>, result?: any }>, service: string) => {
-
-            useEach(handlers, (handler: { function: string, parameters?: Array<any>, result?: any }) => {
-
-                const expectedResult: boolean = handler.result ?? true;
-                const parametersArray = handler.parameters ?? []
-
-                useEach(parametersArray, (parameters: any) => {
-                    const actualResult = handlerMap[handler.function](parameters ?? null)
-
-                    if (actualResult !== expectedResult) {
-                        hasAbility = false
-                        return false
-                    }
-                })
-                if (!hasAbility) { return false }
-            })
-            if (!hasAbility) { return false }
-        })
-        return hasAbility
-    }
-
-    /**
-     * Est-ce que l'utilisateur possède la ou les habilités
-     *
-     * @param {Array<AbilitiesType>} abilities Habilités à tester
-     * @return {boolean}
-     */
-    hasAbilities(abilities: Array<AbilitiesType>|null): boolean{
-        useEach(abilities ?? [], (ability) => {
-            if (!this.ability.can(ability.action, ability.subject)) {
-                return false
-            }
-        })
-        return true
-    }
-
-
-    /**
-     * Teste le profil d'un utilisateur
-     *
-     * @param {string} profile : profile à tester
-     * @return {boolean}
-     */
-    private testProfile(profile: string): boolean {
-        const factory: {[key: string]: boolean|null} = {
-            'admin': this.accessProfile.isAdmin,
-            'administratifManager': this.accessProfile.isAdministratifManager,
-            'pedagogicManager': this.accessProfile.isPedagogicManager,
-            'financialManager': this.accessProfile.isFinancialManager,
-            'caMember': this.accessProfile.isCaMember,
-            'student': this.accessProfile.isStudent,
-            'teacher': this.accessProfile.isTeacher,
-            'member': this.accessProfile.isMember,
-            'other': this.accessProfile.isOther,
-            'guardian': this.accessProfile.isGuardian,
-            'payor': this.accessProfile.isPayer,
-        }
-        return factory[profile] ?? false
-    }
-
-    /**
-     * Retourne vrai si l'utilisateur connecté possède l'un des profils passés en paramètre
-     *
-     * @param {Array<string>} profiles Profils à tester
-     * @return {boolean}
-     */
-    hasProfileAmong (profiles: Array<string>|null): boolean {
-        if (null === profiles)
-            return true;
-
-        useEach(profiles, (profile) => {
-            if (this.testProfile(profile)) {
-                return true
-            }
-        })
-        return false
-    }
-
-    /**
-     * Est-ce que l'utilisateur possède le rôle donné ?
-     *
-     * @return {boolean}
-     * @param role
-     */
-    hasRole(role: string|null): boolean {
-        return role === null || this.accessProfile.roles.includes(role)
-    }
-
-    /**
-     * Est-ce que l'utilisateur possède tous les rôles donnés ?
-     *
-     * @return {boolean}
-     * @param roles
-     */
-    hasRoles(roles: Array<string>): boolean {
-        useEach(roles, (r: string) =>  {
-            if (!this.accessProfile.roles.includes(r)) {
-                return false
-            }
-        })
-        return true
-    }
-
-    /**
-     * Est-ce que l'organisation possède le module donné
-     *
-     * @return {boolean}
-     * @param module
-     */
-    hasModule(module: string): boolean {
-        return this.organizationProfile.modules.includes(module)
-    }
-}
-
-export default AbilityUtils

+ 4 - 3
services/rights/roleUtils.ts

@@ -125,11 +125,12 @@ class RoleUtils {
     useEach(roles, (role) => {
       if ((match = regex.exec(role)) !== null) {
         const subject = match[2]
-        const action = match[3] ?? ''
+        const actionName = match[3] ?? ''
+        const action = actionMap[actionName]
 
-        if (subject) {
+        if (subject && typeof action !== 'undefined') {
           abilities.push({
-            action: actionMap[action],
+            action: action,
             subject: subject.toLowerCase()
           })
         }

+ 10 - 0
stores/accessProfile.ts

@@ -87,6 +87,16 @@ export const useAccessProfileStore = defineStore('accessProfile', () => {
     })
   }
 
+  /**
+   * Est-ce que l'utilisateur possède le rôle donné ?
+   *
+   * @return {boolean}
+   * @param role
+   */
+  const hasRole = (role: string): boolean => {
+    return roles.value.includes(role)
+  }
+
   const setProfile = (profile: any) => {
     const profileRoles: Array<string> = Object.values(profile.roles)
 

+ 17 - 11
stores/organizationProfile.ts

@@ -6,12 +6,12 @@ import {useEach} from "#imports";
 export const useOrganizationProfileStore = defineStore('organizationProfile', () => {
 
   // State
-  const id = ref(null)
+  const id: Ref<number | null> = ref(null)
   const parametersId = ref(null)
   const name = ref(null)
   const product = ref(null)
   const currentActivityYear = ref(null)
-  const modules = ref([])
+  const modules: Ref<Array<string>> = ref([])
   const hasChildren = ref(false)
   const legalStatus = ref(null)
   const showAdherentList = ref(false)
@@ -19,6 +19,8 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
   const website = ref(null)
   const parents: Ref<Array<BaseOrganizationProfile>> = ref([])
 
+  const runtimeConfig = useRuntimeConfig()
+
   // Getters
   /**
    * L'organization fait-elle partie du réseau CMF?
@@ -27,7 +29,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    */
   const isCmf = computed( (): boolean => {
     return networks.value.filter((network: string) => {
-      return network === process.env.cmf_network
+      return network === runtimeConfig.cmf_network
     }).length > 0
   })
 
@@ -38,7 +40,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    */
   const isFfec = computed( (): boolean => {
     return networks.value.filter((network: string) => {
-      return network === process.env.ffec_network
+      return network === runtimeConfig.ffec_network
     }).length > 0
   })
 
@@ -56,7 +58,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    * @return {boolean}
    */
   const isArtistProduct = computed( (): boolean => {
-    return product.value === process.env.artist_product
+    return product.value === runtimeConfig.artist_product
   })
 
   /**
@@ -64,7 +66,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    * @return {boolean}
    */
   const isArtistPremiumProduct = computed( (): boolean => {
-    return product.value === process.env.artist_premium_product
+    return product.value === runtimeConfig.artist_premium_product
   })
 
   /**
@@ -81,7 +83,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    * @return {boolean}
    */
   const isSchoolProduct = computed( (): boolean => {
-    return product.value === process.env.school_product
+    return product.value === runtimeConfig.school_product
   })
 
   /**
@@ -90,7 +92,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    * @return {boolean}
    */
   const isSchoolPremiumProduct = computed( (): boolean => {
-    return product.value === process.env.school_premium_product
+    return product.value === runtimeConfig.school_premium_product
   })
 
   /**
@@ -106,7 +108,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    * @return {boolean}
    */
   const isManagerProduct = computed( (): boolean => {
-    return product.value === process.env.manager_product
+    return product.value === runtimeConfig.manager_product
   })
 
   /**
@@ -131,8 +133,8 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
    * @return {boolean}
    */
   const isCMFCentralService = computed((): boolean => {
-    if(process.env.CMF_ID)
-      return id.value === parseInt(process.env.CMF_ID)
+    if(runtimeConfig.CMF_ID)
+      return id.value === parseInt(runtimeConfig.CMF_ID)
     return false
   })
 
@@ -140,6 +142,9 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
     return website.value ?? null
   })
 
+  const hasModule = (module: string): boolean => {
+    return modules.value.includes(module)
+  }
 
   // Actions
   const setProfile = (profile: any) => {
@@ -197,6 +202,7 @@ export const useOrganizationProfileStore = defineStore('organizationProfile', ()
     isShowAdherentList,
     isAssociation,
     getWebsite,
+    hasModule,
     setProfile,
     refreshProfile
   }