Browse Source

config + rights + docs + tests + i18n

Vincent GUFFON 4 years ago
parent
commit
3144f4862a
51 changed files with 2647 additions and 413 deletions
  1. 3 0
      README.md
  2. 3 0
      components/Actions/EditButtonComponent.vue
  3. 2 1
      components/Form/DataTableComponent.vue
  4. 1 1
      components/Form/InputComponent.vue
  5. 46 0
      components/Layout/HeaderComponent.vue
  6. 111 0
      components/Layout/MenuComponent.vue
  7. 3 0
      config/abilities/config.yaml
  8. 49 0
      config/abilities/pages/addressBook.yaml
  9. 47 0
      config/abilities/pages/educational.yaml
  10. 7 0
      config/abilities/pages/equipment.yaml
  11. 25 0
      config/abilities/pages/schedule.yaml
  12. 37 0
      config/nuxtConfig/build.js
  13. 22 0
      config/nuxtConfig/env.js
  14. 21 0
      config/nuxtConfig/head.js
  15. 13 0
      config/nuxtConfig/i18n.js
  16. 16 0
      config/nuxtConfig/modules.js
  17. 10 0
      config/nuxtConfig/plugins.js
  18. 39 0
      config/nuxtConfig/vuetify.js
  19. 47 0
      jsdoc.json
  20. 8 0
      lang/fr-FR.js
  21. 22 0
      lang/layout/fr-FR.js
  22. 33 72
      layouts/default.vue
  23. 1 1
      middleware/auth.ts
  24. 14 110
      nuxt.config.js
  25. 540 86
      package-lock.json
  26. 14 4
      package.json
  27. 0 6
      pages/index.vue
  28. 2 2
      plugins/Queries/http.js
  29. 5 4
      plugins/Queries/rest.ts
  30. 17 23
      plugins/Rights/ability.ts
  31. 71 0
      services/profile/accessProfile.ts
  32. 113 0
      services/profile/organizationProfile.ts
  33. 131 0
      services/rights/abilitiesUtils.ts
  34. 130 0
      services/rights/roleUtils.ts
  35. 18 10
      services/utils/hydraParser.ts
  36. 0 0
      services/utils/objectProperties.js
  37. 14 0
      services/utils/yamlParser.ts
  38. 6 7
      store/index.js
  39. 0 29
      store/myProfile.js
  40. 80 0
      store/profile/access.ts
  41. 32 0
      store/profile/organization.ts
  42. 0 9
      test/Logo.spec.js
  43. 58 0
      test/services/rights/abilitiesUtils.spec.js
  44. 56 0
      test/services/rights/roleUtils.spec.js
  45. 16 0
      test/services/utils/files/test.yaml
  46. 33 0
      test/services/utils/hydraParser.spec.js
  47. 53 0
      test/services/utils/yamlParser.spec.js
  48. 2 1
      tsconfig.json
  49. 51 0
      types/types.d.ts
  50. 153 0
      use/template/menu.ts
  51. 472 47
      yarn.lock

+ 3 - 0
README.md

@@ -15,6 +15,9 @@ $ yarn start
 
 # generate static project
 $ yarn generate
+
+# generate doc
+$ yarn docs
 ```
 
 For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).

+ 3 - 0
components/Actions/EditButtonComponent.vue

@@ -7,6 +7,9 @@
 <script lang="ts">
   import {defineComponent} from '@nuxtjs/composition-api'
 
+  /**
+   * @component
+   */
   export default defineComponent({
     setup() {
 

+ 2 - 1
components/Form/DataTableComponent.vue

@@ -18,6 +18,7 @@
   import {defineComponent, ref, useContext, useFetch} from '@nuxtjs/composition-api'
   import {Collection, Model, Repository} from "@vuex-orm/core";
   import personActivitiesData from '@/data/personActivitiesData'
+  import {AnyJson} from "~/types/types";
 
   export default defineComponent({
     props: {
@@ -43,7 +44,7 @@
       const {$http} = useContext()
 
       const {fetch, fetchState} = useFetch(async ()=>{
-        const collection:{ [key:string]: any} = await $http.$get(`https://local.new.api.opentalent.fr/api/${uri}`)
+        const collection:AnyJson = await $http.$get(`https://local.new.api.opentalent.fr/api/${uri}`)
         repository.insert(collection['hydra:member']);
         entries.value = repository.all()
         totalEntries.value = parseInt(collection['hydra:totalItems'])

+ 1 - 1
components/Form/InputComponent.vue

@@ -16,7 +16,7 @@
 <script lang="ts">
   import {unref, defineComponent, ref, useFetch, watch, onUnmounted, useContext} from '@nuxtjs/composition-api'
   import {Query} from "@vuex-orm/core";
-  import {cloneAndFlatten, cloneAndNest} from "~/services/objectProperties";
+  import {cloneAndFlatten, cloneAndNest} from "~/services/utils/objectProperties";
 
   export default defineComponent({
     props: {

+ 46 - 0
components/Layout/HeaderComponent.vue

@@ -0,0 +1,46 @@
+<template>
+  <v-app-bar
+    :clipped-left="properties.clipped"
+    fixed
+    app
+    class="ot_green ot_white--text"
+  >
+    <v-btn
+      class="menu-btn"
+      icon
+      @click.stop="properties.miniVariant = !properties.miniVariant"
+    >
+      <v-icon class="ot_white--text">mdi-menu{{ `${properties.miniVariant ? '' : '-open'}` }}</v-icon>
+    </v-btn>
+
+    <v-toolbar-title v-text="properties.title" />
+
+    <v-spacer />
+
+  </v-app-bar>
+</template>
+
+<script lang="ts">
+  import {defineComponent, reactive, useContext} from '@nuxtjs/composition-api'
+
+  export default defineComponent({
+    setup() {
+      const {store} = useContext();
+
+      const properties = reactive({
+        clipped: true,
+        fixed: true,
+        miniVariant: false,
+        title: store.state.profile.organization.name
+      })
+
+      return {
+        properties
+      }
+    }
+  })
+</script>
+
+<style scoped>
+
+</style>

+ 111 - 0
components/Layout/MenuComponent.vue

@@ -0,0 +1,111 @@
+<template>
+  <v-navigation-drawer
+    :mini-variant="miniVariant"
+    :clipped="clipped"
+    class="ot_dark_grey ot_menu_color--text"
+    fixed
+    app
+  >
+    <v-list>
+      <div v-for="(item, i) in menu" :key="i">
+        <v-list-item
+          v-if="!item.children"
+          :href="item.old ? item.to : undefined"
+          :to="!item.old ? item.to : undefined"
+          router
+          exact
+        >
+          <v-list-item-action>
+            <v-icon class="ot_menu_color--text">{{ item.icon }}</v-icon>
+          </v-list-item-action>
+          <v-list-item-content>
+            <v-list-item-title class="ot_menu_color--text" v-text="$t(item.title)"/>
+          </v-list-item-content>
+        </v-list-item>
+
+        <v-list-group
+          v-else
+          v-model="item.active"
+          no-action
+        >
+          <template v-slot:activator>
+            <v-list-item-action>
+              <v-icon class="ot_menu_color--text">{{ item.icon }}</v-icon>
+            </v-list-item-action>
+            <v-list-item-content>
+              <v-list-item-title class="ot_menu_color--text" v-text="$t(item.title)"/>
+            </v-list-item-content>
+          </template>
+
+          <v-list-item
+            v-for="child in item.children"
+            :key="child.title"
+            :href="child.old ? child.to : undefined"
+            :to="!child.old ? child.to : undefined"
+            router
+            exact
+          >
+            <v-list-item-action>
+              <v-icon class="ot_menu_color--text">{{ child.icon }}</v-icon>
+            </v-list-item-action>
+            <v-list-item-content>
+              <v-list-item-title class="ot_menu_color--text" v-text="$t(child.title)"/>
+            </v-list-item-content>
+          </v-list-item>
+        </v-list-group>
+      </div>
+
+    </v-list>
+
+  </v-navigation-drawer>
+</template>
+
+<script lang="ts">
+  import Menu from '@/use/template/menu'
+  import {defineComponent} from '@nuxtjs/composition-api'
+  import {AnyJson} from "~/types/types";
+
+  export default defineComponent({
+    props: {
+      miniVariant: {
+        type: Boolean,
+        required: true
+      },
+      clipped: {
+        type: Boolean,
+        required: true
+      }
+    },
+    setup() {
+      const menu: AnyJson = new Menu().useMenuConstruct()
+
+      return {
+        menu
+      }
+    }
+  })
+</script>
+
+<style scoped>
+  .v-list-item__action, .v-list-group__header__prepend-icon {
+    margin-right: 10px !important;
+  }
+  .v-application--is-ltr .v-list-group--no-action > .v-list-group__items > .v-list-item {
+    padding-left: 30px;
+  }
+  .v-list-item__title{
+    font-size: 14px;
+  }
+  .v-icon.v-icon{
+    font-size: 16px;
+  }
+  .v-list-item{
+    min-height: 10px;
+  }
+  .v-list-item__action {
+    margin: 8px 0;
+  }
+  .v-list-item__content {
+    padding: 8px 0;
+  }
+</style>

+ 3 - 0
config/abilities/config.yaml

@@ -0,0 +1,3 @@
+abilities:
+  !!import/shallow
+    - pages/

+ 49 - 0
config/abilities/pages/addressBook.yaml

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

+ 47 - 0
config/abilities/pages/educational.yaml

@@ -0,0 +1,47 @@
+  criteria_notations_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+
+  seizure_period_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+
+  test_seizure_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsSeizure']}
+
+  test_validation_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+
+  examen_results_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-administration'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsAdministation']}
+
+  education_by_student_validation_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'pedagogics-seizure'}]}
+      organization:
+        - {function: hasModule, parameters: ['PedagogicsSeizure']}

+ 7 - 0
config/abilities/pages/equipment.yaml

@@ -0,0 +1,7 @@
+  equipment_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'equipment'}]}
+      organization:
+        - {function: hasModule, parameters: ['Equipments']}

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

@@ -0,0 +1,25 @@
+  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'
+            - 'EducationnalProjects'
+
+  attendance_page:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'attendances'}]}
+      organization:
+        - {function: hasModule, parameters: ['Attendances']}

+ 37 - 0
config/nuxtConfig/build.js

@@ -0,0 +1,37 @@
+import webpack from 'webpack'
+
+export default {
+  // Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
+  ssr: true,
+
+  // Auto import components (https://go.nuxtjs.dev/config-components)
+  components: true,
+
+  // Build Configuration (https://go.nuxtjs.dev/config-build)
+  build: {
+    extend (config, { isDev, isClient }) {
+      config.node = {
+        fs: "empty"
+      }
+    },
+    plugins: [
+      new webpack.ProvidePlugin({
+        _: 'lodash'
+      })
+    ]
+  },
+
+  //Port and local host
+  server: {
+    port: 3002,
+    host: '0.0.0.0', // default: localhost,
+  },
+
+  //Poll for hot reloading with docker
+  watchers: {
+    webpack: {
+      aggregateTimeout: 300,
+      poll: 1000
+    }
+  }
+}

+ 22 - 0
config/nuxtConfig/env.js

@@ -0,0 +1,22 @@
+export default {
+  env: {
+    school_product: 'school',
+    school_premium_product: 'school-premium',
+    artist_product: 'artist',
+    artist_premium_product: 'artist-premium',
+    manager_product: 'manager',
+  },
+  publicRuntimeConfig: {
+    http: {
+      browserBaseURL: process.env.NODE_ENV !== 'production' ? 'https://local.new.api.opentalent.fr' : 'https://local.new.api.opentalent.fr'
+    },
+    baseURL_Legacy: process.env.NODE_ENV !== 'production' ? 'https://local.api.opentalent.fr' : 'https://local.api.opentalent.fr',
+    baseURL_adminLegacy: process.env.NODE_ENV !== 'production' ? 'https://local.admin.opentalent.fr/#' : 'https://admin.opentalent.fr/#',
+  },
+  privateRuntimeConfig: {
+    http: {
+      baseURL: process.env.NODE_ENV !== 'production' ? 'http://nginx_new' : 'https://local.api.opentalent.fr'
+    },
+    baseURL_Legacy: process.env.NODE_ENV !== 'production' ? 'http://nginx' : 'https://local.api.opentalent.fr'
+  }
+}

+ 21 - 0
config/nuxtConfig/head.js

@@ -0,0 +1,21 @@
+export default {
+  // Global page headers (https://go.nuxtjs.dev/config-head)
+  head: {
+    titleTemplate: '%s - admin',
+    title: 'admin',
+    meta: [
+      {charset: 'utf-8'},
+      {name: 'viewport', content: 'width=device-width, initial-scale=1'},
+      {hid: 'description', name: 'description', content: ''}
+    ],
+    link: [
+      {rel: 'icon', type: 'image/x-icon', href: '/favicon.ico'}
+    ]
+  },
+
+  // Global CSS (https://go.nuxtjs.dev/config-css)
+  css: [
+    '@/assets/css/import.scss',
+    '@fortawesome/fontawesome-free/css/all.css'
+  ]
+}

+ 13 - 0
config/nuxtConfig/i18n.js

@@ -0,0 +1,13 @@
+export default {
+  i18n: {
+    locales: [
+      {
+        code: 'fr',
+        file: 'fr-FR.js'
+      }
+    ],
+    lazy: true,
+    langDir: 'lang/',
+    defaultLocale: 'fr'
+  }
+}

+ 16 - 0
config/nuxtConfig/modules.js

@@ -0,0 +1,16 @@
+export default {
+  // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
+  buildModules: [
+    // https://go.nuxtjs.dev/typescript
+    '@nuxt/typescript-build',
+    // https://go.nuxtjs.dev/vuetify
+    '@nuxtjs/vuetify',
+    '@nuxtjs/composition-api'
+  ],
+
+  // Modules (https://go.nuxtjs.dev/config-modules)
+  modules: [
+    '@nuxt/http',
+    'nuxt-i18n'
+  ]
+}

+ 10 - 0
config/nuxtConfig/plugins.js

@@ -0,0 +1,10 @@
+export default {
+  // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
+  plugins: [
+    '~/plugins/grid',
+    '~/plugins/Rights/ability',
+    './plugins/Rights/casl.js',
+    '~/plugins/Queries/http',
+    '~/plugins/Queries/rest'
+  ]
+}

+ 39 - 0
config/nuxtConfig/vuetify.js

@@ -0,0 +1,39 @@
+import colors from 'vuetify/es5/util/colors'
+
+export default {
+  // Vuetify module configuration (https://go.nuxtjs.dev/config-vuetify)
+  vuetify: {
+    icons: {
+      iconfont: 'fa' || 'mdi',
+    },
+    customVariables: ['~/assets/css/variables.scss'],
+    theme: {
+      dark: false,
+      themes: {
+        dark: {
+          primary: colors.blue.darken2,
+          accent: colors.grey.darken3,
+          secondary: colors.amber.darken3,
+          info: colors.teal.lighten1,
+          warning: colors.amber.base,
+          error: colors.deepOrange.accent4,
+          success: colors.green.accent3
+        },
+        light: {
+          ot_green: '#00ad8e',
+          ot_light_green: '#a9e0d6',
+          ot_dark_grey: '#2c3a48',
+          ot_grey: '#777777',
+          ot_light_grey: '#f5f5f5',
+          ot_super_light_grey: '#ecf0f5',
+          ot_danger: '#f56954',
+          ot_success: '#00a65a',
+          ot_warning: '#f39c12',
+          ot_info: '#3c8dbc',
+          ot_menu_color: '#b8c7ce',
+          ot_white: '#ffffff'
+        },
+      }
+    }
+  }
+}

+ 47 - 0
jsdoc.json

@@ -0,0 +1,47 @@
+{
+  "opts": {
+    "encoding": "utf8",
+    "destination": "../admin-doc/",
+    "recurse": true,
+    "verbose": true,
+    "readme": "./readme.md",
+    "template": "node_modules/better-docs"
+  },
+  "tags": {
+    "allowUnknownTags": true
+  },
+  "plugins": [
+    "node_modules/better-docs/typescript",
+    "node_modules/better-docs/component",
+    "node_modules/better-docs/category",
+    "plugins/markdown"
+  ],
+  "source": {
+    "exclude": [
+      "./.nuxt",
+      "./coverage",
+      "./node_modules",
+      "./nuxt.config.js",
+      "./.eslintrc.js",
+      "./jest.config.js",
+      "./jest.config.js"
+    ],
+    "includePattern": "\\.(js|ts)$"
+  },
+  "templates": {
+    "better-docs": {
+      "name": "Documentation",
+      "navigation": [
+        {
+          "label": "FAQ",
+          "href": "https://ressources.opentalent.fr/"
+        },
+        {
+          "label": "SPEC",
+          "href": "https://assistance.opentalent.fr/"
+        }
+      ]
+    },
+    "search": true
+  }
+}

+ 8 - 0
lang/fr-FR.js

@@ -0,0 +1,8 @@
+import layout from '@/lang/layout/fr-FR'
+
+export default (context, locale) => {
+
+  return {
+    ...layout()
+  }
+}

+ 22 - 0
lang/layout/fr-FR.js

@@ -0,0 +1,22 @@
+export default (context, locale) => {
+  return ({
+    welcome: 'Accueil',
+    address_book: 'Répertoire',
+    person: 'Personnes',
+    family_view: 'Vue famille',
+    education_student_next_year: 'Gestion des inscriptions',
+    commissions: 'Commissions',
+    my_network: 'Mon réseau',
+    network: 'Réseau',
+    schedule: 'Agenda',
+    attendances: 'Absences',
+    equipment: 'Parc matériel',
+    education_state: 'Suivi pédagogique',
+    criteria_notations: "Critère d'évaluation",
+    seizure_period: 'Périodes de saisie',
+    test_seizure: 'Saisie des évaluations',
+    test_validation: 'Validation par évaluation',
+    examen_results: 'Résultats des examens',
+    education_by_student_validation: 'Validation par enseignement',
+  })
+}

File diff suppressed because it is too large
+ 33 - 72
layouts/default.vue


+ 1 - 1
middleware/auth.ts

@@ -2,7 +2,7 @@ import { Middleware } from '@nuxt/types'
 
 const auth: Middleware = async ({ store, redirect }) => {
   // If the user is not authenticated
-  if (!store.state.myProfile) {
+  if (!store.state.profile.access) {
     return redirect('/login')
   }
 }

+ 14 - 110
nuxt.config.js

@@ -1,113 +1,17 @@
-import colors from 'vuetify/es5/util/colors'
+import build from './config/nuxtConfig/build'
+import env from './config/nuxtConfig/env'
+import head from './config/nuxtConfig/head'
+import modules from './config/nuxtConfig/modules'
+import plugins from './config/nuxtConfig/plugins'
+import i18n from './config/nuxtConfig/i18n'
+import vuetify from './config/nuxtConfig/vuetify'
 
 export default {
-  publicRuntimeConfig: {
-    http:{
-      browserBaseURL : process.env.NODE_ENV !== 'production' ? 'https://local.new.api.opentalent.fr' : 'https://local.new.api.opentalent.fr'
-    },
-    baseURL_Legacy : process.env.NODE_ENV !== 'production' ? 'https://local.api.opentalent.fr' : 'https://local.api.opentalent.fr'
-  },
-  privateRuntimeConfig: {
-    http:{
-      baseURL : process.env.NODE_ENV !== 'production' ? 'http://nginx_new' : 'https://local.api.opentalent.fr'
-    },
-    baseURL_Legacy : process.env.NODE_ENV !== 'production' ? 'http://nginx' : 'https://local.api.opentalent.fr'
-  },
-
-  // Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode)
-  ssr: true,
-
-  // Global page headers (https://go.nuxtjs.dev/config-head)
-  head: {
-    titleTemplate: '%s - admin',
-    title: 'admin',
-    meta: [
-      { charset: 'utf-8' },
-      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
-      { hid: 'description', name: 'description', content: '' }
-    ],
-    link: [
-      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
-    ]
-  },
-
-  // Global CSS (https://go.nuxtjs.dev/config-css)
-  css: [
-    '@/assets/css/import.scss'
-  ],
-
-  // Plugins to run before rendering page (https://go.nuxtjs.dev/config-plugins)
-  plugins: [
-    '~/plugins/grid',
-    '~/plugins/Rights/ability',
-    './plugins/Rights/casl.js',
-    '~/plugins/Queries/http',
-    '~/plugins/Queries/rest'
-  ],
-
-  // Auto import components (https://go.nuxtjs.dev/config-components)
-  components: true,
-
-  // Modules for dev and build (recommended) (https://go.nuxtjs.dev/config-modules)
-  buildModules: [
-    // https://go.nuxtjs.dev/typescript
-    '@nuxt/typescript-build',
-    // https://go.nuxtjs.dev/vuetify
-    '@nuxtjs/vuetify',
-    '@nuxtjs/composition-api'
-  ],
-
-  // Modules (https://go.nuxtjs.dev/config-modules)
-  modules: [
-    '@nuxt/http'
-  ],
-
-  // Vuetify module configuration (https://go.nuxtjs.dev/config-vuetify)
-  vuetify: {
-    customVariables: ['~/assets/css/variables.scss'],
-    theme: {
-      dark: false,
-      themes: {
-        dark: {
-          primary: colors.blue.darken2,
-          accent: colors.grey.darken3,
-          secondary: colors.amber.darken3,
-          info: colors.teal.lighten1,
-          warning: colors.amber.base,
-          error: colors.deepOrange.accent4,
-          success: colors.green.accent3
-        },
-        light: {
-          ot_green: '#00ad8e',
-          ot_light_green: '#a9e0d6',
-          ot_dark_grey: '#2c3a48',
-          ot_grey: '#777777',
-          ot_light_grey: '#f5f5f5',
-          ot_super_light_grey: '#ecf0f5',
-          ot_danger: '#f56954',
-          ot_success: '#00a65a',
-          ot_warning: '#f39c12',
-          ot_info: '#3c8dbc'
-        },
-      }
-    }
-  },
-
-  // Build Configuration (https://go.nuxtjs.dev/config-build)
-  build: {
-  },
-
-  //Port and local host
-  server: {
-    port: 3002,
-    host: '0.0.0.0', // default: localhost,
-  },
-
-  //Poll for hot reloading with docker
-  watchers: {
-    webpack: {
-      aggregateTimeout: 300,
-      poll: 1000
-    }
-  }
+  ...build,
+  ...env,
+  ...head,
+  ...modules,
+  ...plugins,
+  ...i18n,
+  ...vuetify
 }

File diff suppressed because it is too large
+ 540 - 86
package-lock.json


+ 14 - 4
package.json

@@ -9,7 +9,8 @@
     "generate": "nuxt-ts generate",
     "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .",
     "lint": "yarn lint:js",
-    "test": "jest"
+    "test": "jest",
+    "docs": "jsdoc -c 'jsdoc.json' ./"
   },
   "dependencies": {
     "@casl/ability": "^5.1.0",
@@ -18,25 +19,34 @@
     "@nuxt/typescript-runtime": "^2.0.0",
     "@nuxtjs/composition-api": "^0.17.0",
     "@syncfusion/ej2-vue-grids": "^18.4.30",
-    "@vuex-orm/core": "1.0.0-draft.6",
+    "@types/lodash": "^4.14.168",
+    "@vuex-orm/core": "1.0.0-draft.7",
     "cookieparser": "^0.1.0",
     "core-js": "^3.6.5",
-    "nuxt": "^2.14.6"
+    "js-yaml": "^4.0.0",
+    "lodash": "^4.17.20",
+    "marked": "^1.2.7",
+    "nuxt": "^2.14.6",
+    "nuxt-i18n": "^6.18.0",
+    "yaml-import": "^2.0.0"
   },
   "devDependencies": {
+    "@fortawesome/fontawesome-free": "^5.15.2",
     "@nuxt/types": "^2.14.6",
     "@nuxt/typescript-build": "^2.0.3",
     "@nuxtjs/eslint-config": "^3.1.0",
     "@nuxtjs/eslint-config-typescript": "^3.0.0",
     "@nuxtjs/eslint-module": "^2.0.0",
-    "@nuxtjs/vuetify": "^1.11.2",
+    "@nuxtjs/vuetify": "1.11.3",
     "@vue/test-utils": "^1.1.0",
     "babel-core": "7.0.0-bridge.0",
     "babel-eslint": "^10.1.0",
     "babel-jest": "^26.5.0",
+    "better-docs": "^2.3.2",
     "eslint": "^7.10.0",
     "eslint-plugin-nuxt": "^1.0.0",
     "jest": "^26.5.0",
+    "jsdoc": "^3.6.6",
     "ts-jest": "^26.4.1",
     "vue-jest": "^3.0.4"
   }

+ 0 - 6
pages/index.vue

@@ -2,12 +2,6 @@
   <v-row justify="center" align="center">
     <v-col cols="12" sm="12" md="12">
       <h3>Bienvenue !</h3>
-      <div v-if="$can('read', 'books')">
-        books
-      </div>
-      <div v-else>
-        no_permission
-      </div>
     </v-col>
   </v-row>
 </template>

+ 2 - 2
plugins/Queries/http.js

@@ -1,8 +1,8 @@
 export default function ({ $http, redirect, store }) {
 
   $http.onRequest(config => {
-    config.headers.set('Authorization', `Bearer ${store.state.myProfile.bearer}`)
-    config.headers.set('x-accessid', `${store.state.myProfile.accessId}`)
+    config.headers.set('Authorization', `Bearer ${store.state.profile.access.bearer}`)
+    config.headers.set('x-accessid', `${store.state.profile.access.accessId}`)
   })
 
   $http.onResponse(async (request, options, response) => {

+ 5 - 4
plugins/Queries/rest.ts

@@ -1,10 +1,10 @@
 import {Plugin} from '@nuxt/types'
-
-import Hydra from '../../services/hydra'
+import HydraParser from '../../services/utils/hydraParser'
+import {AnyJson} from "~/types/types";
 
 declare module '@nuxt/types' {
   interface Context {
-    $rest: { [key: string]: any }
+    $rest: AnyJson
   }
 }
 
@@ -23,7 +23,8 @@ const restPlugin: Plugin = (ctx) => {
   const queries = async (responseQuery: any) => {
     try {
       let response = await responseQuery.json();
-      return Hydra.parse(response);
+      const hydra = new HydraParser();
+      return hydra.parse(response);
     } catch (err) {
       console.log(err)
     }

+ 17 - 23
plugins/Rights/ability.ts

@@ -1,38 +1,32 @@
 import { Plugin } from '@nuxt/types'
-import { defineAbility } from '@casl/ability'
+import { Ability } from '@casl/ability'
+import { $abilitiesUtils } from "~/services/rights/abilitiesUtils";
 
 declare module '@nuxt/types' {
   interface Context {
-    $ability(): void
+    $ability(): Ability
   }
 }
 
-export const ability = defineAbility(can => can('read', 'nothing'))
+export const ability = new Ability();
 
 const abilityPlugin: Plugin = (ctx) => {
-  ability.update(ctx.store.state.myProfile.permissions);
+  ability.update(ctx.store.state.profile.access.abilities);
 
-  ctx.store.subscribe((mutation) => {
-    switch (mutation.type) {
-      case 'myProfile/setProfile':
-        const permissions = getPermissionByRoles(ctx.store.state.myProfile.roles)
-        ctx.store.commit('myProfile/setPermissions', permissions)
-        ability.update(permissions);
-        break;
-    }
-  });
+  const unsubscribe = ctx.store.subscribeAction({
+    after: (action, state) => {
+      switch (action.type) {
+        case 'profile/organization/setProfile':
+          const abilitiesUtils = $abilitiesUtils(ctx.store, ability)
+          const abilities = abilitiesUtils.getAbilities();
 
-  const getPermissionByRoles = (roles:[]) =>{
-    for(const key in roles){
-      const role = roles[key];
+          ctx.store.commit('profile/access/setAbilities', abilities)
+          ability.update(abilities);
+          unsubscribe()
+          break;
+      }
     }
-    return [
-      { action: 'read', subject: 'books' },
-      { action: 'read', subject: 'book' },
-    ]
-  }
-
+  })
   ctx.$ability = () => {return ability}
 }
-
 export default abilityPlugin

+ 71 - 0
services/profile/accessProfile.ts

@@ -0,0 +1,71 @@
+import {AbilitiesType, accessState, AccessStore, AnyJson} from "~/types/types";
+import {Ability} from "@casl/ability";
+
+/**
+ * @category Services/profiles
+ * @class AccessProfile
+ * Classe répondant aux différentes questions que l'on peut se poser sur l'access connecté
+ */
+class AccessProfile{
+  private accessProfile: accessState
+
+  private $ability:Ability = {} as Ability
+
+  /**
+   * @constructor
+   * @param {AccessStore} store State Access du Store contenant les informations de l'utilisateur
+   * @param {Ability} ability Plugin $ability
+   */
+  constructor(store:AccessStore, ability:Ability) {
+    this.accessProfile = store.state.profile.access
+    this.$ability = ability
+  }
+
+  /**
+   * Est ce que l'utilisateur possède le role.
+   * @param {Array<string>} roles roles à tester
+   * @returns {boolean}
+   */
+  hasRole(roles:Array<string>): boolean{
+    if (null === roles)
+      return true;
+
+    let hasRole = false;
+    roles.map((r) => {
+      if (this.accessProfile.roles.indexOf(r) > -1)
+        hasRole = true;
+    });
+    return hasRole;
+  }
+
+  /**
+   * Est-ce que l'utilisateur possède l'abilité
+   * @param {Array<AbilitiesType>} ability abilité à tester
+   * @returns {boolean}
+   */
+  hasAbility(ability:Array<AbilitiesType>): boolean{
+    if(ability === null)
+      return true;
+
+    let hasAbility= false;
+    ability.map((ability) => {
+      if (this.$ability.can(ability.action, ability.subject))
+        hasAbility = true;
+    });
+    return hasAbility;
+  }
+
+  /**
+   * Factory
+   * @returns {AnyJson} retourne les fonction rendues publiques
+   */
+  handler():AnyJson{
+    return {
+      hasRole: this.hasRole.bind(this),
+      hasAbility: this.hasAbility.bind(this),
+    }
+  }
+}
+
+export const accessProfile = (store:AccessStore, ability:Ability) => new AccessProfile(store, ability);
+

+ 113 - 0
services/profile/organizationProfile.ts

@@ -0,0 +1,113 @@
+import {AnyJson, organizationState, OrganizationStore} from "~/types/types";
+
+/**
+ * @category Services/profiles
+ * @class OrganizationProfile
+ * Classe répondant aux différentes questions que l'on peut se poser sur l'organization de l'access connecté
+ */
+class OrganizationProfile{
+  private organizationProfile:organizationState
+
+  /**
+   * @constructor
+   * @param {OrganizationStore} store State organization du store contenant les informations de l'organisation
+   */
+  constructor(store:OrganizationStore) {
+    this.organizationProfile = store.state.profile.organization
+  }
+
+  /**
+   * Est-ce que l'organisation possède le module
+   * @param {Array<string>} modules Modules à tester
+   * @returns {boolean}
+   */
+  hasModule(modules:Array<string>) {
+    let hasModule = false;
+    modules.map((module) => {
+      if (this.organizationProfile.modules.indexOf(module) > -1)
+        hasModule = true;
+    });
+    return hasModule;
+  }
+
+  /**
+   * L'organization possède t'elle un produit school ou school premium
+   * @returns {boolean}
+   */
+  isSchool():boolean {
+    return this.isSchoolProduct() || this.isSchoolPremiumProduct();
+  }
+
+  /**
+   * L'organization possède t'elle un produit artiste ou artiste premium
+   * @returns {boolean}
+   */
+  isArtist():boolean {
+    return this.isArtistProduct() || this.isArtistPremiumProduct();
+  }
+
+  /**
+   * L'organization possède t'elle un produit school
+   * @returns {boolean}
+   */
+  isSchoolProduct() {
+    return this.organizationProfile.product === process.env.school_product
+  }
+
+  /**
+   * L'organization possède t'elle un produit school premium
+   * @returns {boolean}
+   */
+  isSchoolPremiumProduct() {
+    return this.organizationProfile.product === process.env.school_premium_product
+  }
+
+  /**
+   * L'organization possède t'elle un produit premium
+   * @returns {boolean}
+   */
+  isArtistProduct() {
+    return this.organizationProfile.product === process.env.artist_product
+  }
+
+  /**
+   * L'organization possède t'elle un produit artiste premium
+   * @returns {boolean}
+   */
+  isArtistPremiumProduct() {
+    return this.organizationProfile.product === process.env.artist_premium_product
+  }
+
+  /**
+   * L'organization possède t'elle un produit manager
+   * @returns {boolean}
+   */
+  isManagerProduct() {
+    return this.organizationProfile.product === process.env.manager_product
+  }
+
+  /**
+   * L'organization possède t'elledes enfants
+   * @returns {boolean}
+   */
+  isOrganizationWithChildren(){
+    return this.organizationProfile.hasChildren;
+  }
+
+  /**
+   * Factory
+   * @returns {AnyJson} retourne les fonction rendues publiques
+   */
+  handler():AnyJson{
+    return {
+      hasModule: this.hasModule.bind(this),
+      isSchool: this.isSchool.bind(this),
+      isArtist: this.isArtist.bind(this),
+      isManagerProduct: this.isManagerProduct.bind(this),
+      isOrganizationWithChildren: this.isOrganizationWithChildren.bind(this)
+    }
+  }
+}
+
+export const organizationProfile = (store:OrganizationStore) => new OrganizationProfile(store);
+

+ 131 - 0
services/rights/abilitiesUtils.ts

@@ -0,0 +1,131 @@
+import {accessProfile} from "@/services/profile/accessProfile"
+import {organizationProfile} from "@/services/profile/organizationProfile"
+import {$roleUtils} from "~/services/rights/roleUtils";
+import {AbilitiesType, AccessStore, AnyJson, AnyStore} from "~/types/types";
+import {Ability} from "@casl/ability";
+import YamlParser from "~/services/utils/yamlParser";
+import * as _ from "lodash";
+
+/**
+ * @category Services/droits
+ * @class AbilitiesUtils
+ * Classe permettant de mener des opérations sur les abilités
+ */
+class AbilitiesUtils {
+  private $store: AnyStore = {} as AnyStore
+  private $ability: Ability = {} as Ability
+  private factory: AnyJson = {}
+
+  /**
+   * @constructor
+   */
+  constructor(store: AnyStore, ability: Ability) {
+    this.$store = store
+    this.$ability = ability
+  }
+
+  /**
+   * Initialise les services factories
+   */
+  initFactory() {
+    this.factory = {
+      access: accessProfile(this.$store, this.$ability),
+      organization: organizationProfile(this.$store)
+    }
+  }
+
+  /**
+   * Récupération de l'ensemble des abilities quelles soient par Roles ou par Config.
+   * @returns {Array<AbilitiesType>}
+   */
+  getAbilities():Array<AbilitiesType> {
+    const abilitiesByRoles = this.getAbilitiesByRoles(this.$store.state.profile.access.roles)
+    this.$ability.update(abilitiesByRoles);
+    this.initFactory();
+    return abilitiesByRoles.concat(this.getAbilitiesByConfig('./config/abilities/config.yaml'))
+  }
+
+  /**
+   * Adaptation et transformations des roles en abilities
+   * @param {Array<string>} roles
+   * @returns {Array<AbilitiesType>}
+   */
+  getAbilitiesByRoles(roles: Array<string>): Array<AbilitiesType> {
+    roles = $roleUtils.transformUnderscoreToHyphenBeforeCompleteMigration(roles);
+    return $roleUtils.transformRoleToAbilities(roles);
+  }
+
+  /**
+   * - Parcours la config d'abilities en Yaml
+   * - filtres la config pour ne garder que les abilities autorisées
+   * - transform la config restante en Object Abilities
+   * @param {string} configPath
+   * @returns {Array<AbilitiesType>}
+   */
+  getAbilitiesByConfig(configPath:string): Array<AbilitiesType> {
+    let abilitiesByConfig: Array<AbilitiesType> = []
+   try {
+      const doc = YamlParser.parse(configPath);
+      const abilitiesAvailable = doc['abilities']
+      const abilitiesFiltered = this.abilitiesAvailableFilter(abilitiesAvailable)
+      abilitiesByConfig = this.transformAbilitiesConfigToAbility(abilitiesFiltered)
+    } catch (e) {
+      console.debug(e)
+    }
+    return abilitiesByConfig;
+  }
+
+  /**
+   * Filtre toutes les abilities possible suivant si l'utilisateur est autorisé ou non à les posséder
+   * @param {AnyJson} abilitiesAvailable
+   * @returns {AnyJson}
+   */
+  abilitiesAvailableFilter(abilitiesAvailable:AnyJson):AnyJson{
+    return _.pickBy(abilitiesAvailable, (ability:any) =>{
+      const services = ability['services']
+      return this.canHaveTheAbility(services)
+    })
+  }
+
+  /**
+   * Transform une config d'abilities en un tableau d'Abilities
+   * @param {AnyJson} abilitiesAvailable
+   * @returns {Array<AbilitiesType>}
+   */
+  transformAbilitiesConfigToAbility(abilitiesAvailable:AnyJson):Array<AbilitiesType>{
+    let abilitiesByConfig: Array<AbilitiesType> = []
+    _.each(abilitiesAvailable, (ability, subject) => {
+      let myAbility: AbilitiesType = {
+        action: ability['action'],
+        subject: subject
+      }
+      abilitiesByConfig.push(myAbility)
+    })
+    return abilitiesByConfig;
+  }
+
+  /**
+   * Parcours les fonctions par services et établit si oui ou non l'abilité est autorisée
+   * @param {AnyJson} functionByservices
+   * @returns {boolean}
+   */
+  canHaveTheAbility(functionByservices: AnyJson) {
+    let hasAbility = true;
+    _.each(functionByservices, (functions, service) => {
+      if (hasAbility) {
+        const nbFunctions = functions.length
+        let cmpt = 0
+        while (hasAbility && nbFunctions > cmpt) {
+          const f = functions[cmpt]['function'];
+          const parameters = functions[cmpt]['parameters'] ?? null;
+          const result = functions[cmpt]['result'] ?? null;
+          hasAbility = result !== null ? this.factory[service].handler()[f](parameters) == result : this.factory[service].handler()[f](parameters)
+          cmpt++
+        }
+      }
+    })
+    return hasAbility;
+  }
+}
+
+export const $abilitiesUtils = (store: AnyStore, ability:Ability) => new AbilitiesUtils(store, ability);

+ 130 - 0
services/rights/roleUtils.ts

@@ -0,0 +1,130 @@
+import {AbilitiesType, AnyJson} from "~/types/types";
+import * as _ from "lodash";
+
+const roles_by_function = [
+  'ROLE_SUPER_ADMIN',
+  'ROLE_ADMIN',
+  'ROLE_ADMIN_CORE',
+  'ROLE_ADMINISTRATIF_MANAGER',
+  'ROLE_ADMINISTRATIF_MANAGER_CORE',
+  'ROLE_PEDAGOGICS_MANAGER',
+  'ROLE_PEDAGOGICS_MANAGER_CORE',
+  'ROLE_FINANCIAL_MANAGER',
+  'ROLE_FINANCIAL_MANAGER_CORE',
+  'ROLE_CA',
+  'ROLE_CA_CORE',
+  'ROLE_STUDENT',
+  'ROLE_STUDENT_CORE',
+  'ROLE_TEACHER',
+  'ROLE_TEACHER_CORE',
+  'ROLE_MEMBER',
+  'ROLE_MEMBER_CORE',
+  'ROLE_OTHER',
+  'ROLE_OTHER_CORE'
+];
+
+const roles_to_change = [
+  'ROLE_GENERAL_CONFIG',
+  'ROLE_GENERAL_CONFIG_VIEW',
+  'ROLE_TAGG_ADVANCED',
+  'ROLE_TAGG_ADVANCED_VIEW',
+  'ROLE_PEDAGOGICS_ADMINISTRATION',
+  'ROLE_PEDAGOGICS_ADMINISTRATION_VIEW',
+  'ROLE_PEDAGOGICS_SEIZURE',
+  'ROLE_PEDAGOGICS_SEIZURE_VIEW',
+  'ROLE_BILLINGS_ADMINISTRATION',
+  'ROLE_BILLINGS_ADMINISTRATION_VIEW',
+  'ROLE_BILLINGS_SEIZURE',
+  'ROLE_BILLINGS_SEIZURE_VIEW',
+  'ROLE_ONLINEREGISTRATION_ADMINISTRATION',
+  'ROLE_ONLINEREGISTRATION_ADMINISTRATION_VIEW'
+];
+
+const action_map: AnyJson = {
+  '': 'manage',
+  '_VIEW': 'read'
+}
+
+/**
+ * @category Services/droits
+ * @class RoleUtils
+ * Classe permettant de mener des opérations sur les roles
+ */
+class RoleUtils {
+
+  /**
+   * @constructor
+   */
+  constructor() {
+  }
+
+  /**
+   * Test si une personne possède le role lui permettant d'acquérir la fonction
+   * @param {string} function_name
+   * @param {Array<string>} roles
+   * @returns {boolean}
+   */
+  isA(function_name:string, roles:Array<string>): boolean {
+    return roles.indexOf('ROLE_' + function_name + '_CORE') >= 0
+  }
+
+  /**
+   * Filtre les roles afin d'en exclure les "Roles fonctions"
+   * @param {Array<string>} roles
+   * @returns {Array<string>}
+   */
+  filterFunctionRoles(roles:Array<string>):Array<string>{
+    return roles.filter(role => {
+      return roles_by_function.indexOf(role) < 0
+    })
+  }
+
+  /**
+   * Avant la migration complète, quelque role disposent d'underscore en trop, on corrige cela...
+   * @param {Array<string>} roles
+   * @returns {Array<string>}
+   */
+  transformUnderscoreToHyphenBeforeCompleteMigration(roles: Array<string>): Array<string> {
+    const regex = /(ROLE\_)([A-Z]*\_[A-Z]*)([A-Z\_]*)*/i;
+    let match;
+    roles = roles.map((role) => {
+      if (roles_to_change.indexOf(role) >= 0) {
+        if ((match = regex.exec(role)) !== null) {
+          let role = match[1]
+          let subject = match[2].replace('_', '-')
+          let action = match[3]
+          return role + subject + (action ? action : '')
+        }
+      }
+      return role;
+    });
+    return roles
+  }
+
+  /**
+   * On transforme les ROLES Symfony en Abilities
+   * @param {Array<string>} roles
+   * @returns {Array<AbilitiesType>}
+   */
+  transformRoleToAbilities(roles: Array<string>): [] | Array<AbilitiesType> {
+    let abilities:any = [];
+
+    const regex = /(ROLE\_)([A-Z\-]*)([\_A-Z]*)/i;
+    let match;
+
+    _.each(roles, role =>{
+      if ((match = regex.exec(role)) !== null) {
+        let subject = match[2]
+        let action = match[3]
+        abilities.push({
+          action: action_map[action],
+          subject: subject.toLowerCase()
+        })
+      }
+    })
+
+    return abilities;
+  }
+}
+
+export const $roleUtils = new RoleUtils();

+ 18 - 10
services/hydra.js → services/utils/hydraParser.ts

@@ -1,20 +1,26 @@
-export default {
+import {AnyJson} from "~/types/types";
 
-  parse(response) {
+export default class HydraParser {
+  constructor() {
+  }
+
+  parse(response: AnyJson): AnyJson {
     if (response['hydra:member']) {
       response.totalCount = response['hydra:totalItems'];
       return this.parseCollection(response);
     } else {
       return this.parseItem(response);
     }
-  },
-  populateId(data) {
+  }
+
+  populateId(data: AnyJson) {
     if (data['@id'] && data['@id'] instanceof String) {
       var iriParts = data['@id'].split('/');
       data.id = iriParts[iriParts.length - 1];
     }
-  },
-  populateAllData(data) {
+  }
+
+  populateAllData(data: AnyJson):void {
     this.populateId(data);
     for (const key in data) {
       const value = data[key];
@@ -22,8 +28,9 @@ export default {
         this.populateAllData(value);
       }
     }
-  },
-  parseItem(data) {
+  }
+
+  parseItem(data: AnyJson): AnyJson {
     this.populateId(data);
 
     if (data['hydra:previous']) {
@@ -41,8 +48,9 @@ export default {
       data.itemPosition = data['hydra:itemPosition'];
     }
     return data;
-  },
-  parseCollection(data) {
+  }
+
+  parseCollection(data: AnyJson): AnyJson {
     let collectionResponse = data['hydra:member'];
     collectionResponse.metadata = {};
     collectionResponse.order = {};

+ 0 - 0
services/objectProperties.js → services/utils/objectProperties.js


+ 14 - 0
services/utils/yamlParser.ts

@@ -0,0 +1,14 @@
+import {AnyJson} from "~/types/types";
+import { read } from 'yaml-import';
+const yaml = require('js-yaml');
+
+export default class YamlParser {
+  static parse(inPath: string): AnyJson {
+    try {
+      return yaml.load(yaml.dump(read(inPath)));
+    }catch (e) {
+      console.debug(e);
+      return {}
+    }
+  }
+}

+ 6 - 7
store/index.js

@@ -21,17 +21,16 @@ export const actions = {
       try {
         accessId = parsed.AccessId
       } catch (err) {
-        // No valid cookie found
+        // No valid Access Id found
       }
     }
-    commit('myProfile/setBearer', bearer)
-    commit('myProfile/setAccessId', accessId)
-
+    commit('profile/access/setBearer', bearer)
+    commit('profile/access/setAccessId', accessId)
     await dispatch('updateProfile')
   },
 
-  async updateProfile({ commit, state }) {
-    const my_profile = await this.$http.$get(`/api/my_profile/${state.myProfile.accessId}`)
-   commit('myProfile/setProfile', my_profile)
+  async updateProfile({dispatch, state}) {
+    const my_profile = await this.$http.$get(`/api/my_profile/${state.profile.access.accessId}`)
+    dispatch('profile/access/setProfile', my_profile)
   },
 }

+ 0 - 29
store/myProfile.js

@@ -1,29 +0,0 @@
-export const state = () => ({
-  bearer: null,
-  accessId: null,
-  roles: [],
-  modules: [],
-  permissions: [],
-})
-
-export const mutations = {
-  setBearer(state, bearer){
-    state.bearer = bearer
-  },
-  setAccessId(state, accessId){
-    state.accessId = accessId
-  },
-  setProfile(state, profile){
-    state.modules = profile.modules
-    state.roles = profile.roles
-  },
-  setRoles(state, roles){
-    state.roles = roles
-  },
-  setModules(state, modules){
-    state.modules = modules
-  },
-  setPermissions(state, permissions){
-    state.permissions = permissions
-  }
-}

+ 80 - 0
store/profile/access.ts

@@ -0,0 +1,80 @@
+import {$roleUtils} from '~/services/rights/roleUtils'
+import {AbilitiesType, accessState} from "~/types/types";
+
+export const state = () => ({
+  bearer: null,
+  accessId: null,
+  roles: [],
+  abilities: [],
+  isAdmin: false,
+  isAdministratifManager: false,
+  isPedagogicManager: false,
+  isFinancialManager: false,
+  isCaMember: false,
+  isStudent: false,
+  isTeacher: false,
+  isMember: false,
+  isOther: false
+})
+
+export const mutations = {
+  setBearer(state:accessState, bearer:string){
+    state.bearer = bearer
+  },
+  setAccessId(state:accessState, accessId:number){
+    state.accessId = accessId
+  },
+  setRoles(state:accessState, roles:Array<string>){
+    state.roles = roles
+  },
+  setAbilities(state:accessState, abilities:Array<AbilitiesType>){
+    state.abilities = abilities
+  },
+  setIsAdmin(state:accessState, isRole:boolean){
+    state.isAdmin = isRole
+  },
+  setIsAdministratifManager(state:accessState, isRole:boolean){
+    state.isAdministratifManager = isRole
+  },
+  setIsPedagogicManager(state:accessState, isRole:boolean){
+    state.isPedagogicManager = isRole
+  },
+  setIsFinancialManager(state:accessState, isRole:boolean){
+    state.isFinancialManager = isRole
+  },
+  setIsCaMember(state:accessState, isRole:boolean){
+    state.isCaMember = isRole
+  },
+  setIsStudent(state:accessState, isRole:boolean){
+    state.isStudent = isRole
+  },
+  setIsTeacher(state:accessState, isRole:boolean){
+    state.isTeacher = isRole
+  },
+  setIsMember(state:accessState, isRole:boolean){
+    state.isMember = isRole
+  },
+  setIsOther(state:accessState, isRole:boolean){
+    state.isOther = isRole
+  }
+}
+
+export const actions = {
+  setProfile(context:any, profile:any){
+    let roles_to_array:Array<string> = Object.values(profile.roles)
+
+    context.commit('setIsAdmin', $roleUtils.isA('ADMIN', roles_to_array))
+    context.commit('setIsAdministratifManager', $roleUtils.isA('ADMINISTRATIF_MANAGER', roles_to_array))
+    context.commit('setIsPedagogicManager', $roleUtils.isA('PEDAGOGICS_MANAGER', roles_to_array))
+    context.commit('setIsFinancialManager', $roleUtils.isA('FINANCIAL_MANAGER', roles_to_array))
+    context.commit('setIsCaMember', $roleUtils.isA('CA', roles_to_array))
+    context.commit('setIsStudent', $roleUtils.isA('STUDENT', roles_to_array))
+    context.commit('setIsTeacher', $roleUtils.isA('TEACHER', roles_to_array))
+    context.commit('setIsMember', $roleUtils.isA('MEMBER', roles_to_array))
+    context.commit('setIsOther', $roleUtils.isA('OTHER', roles_to_array))
+    context.commit('setRoles', $roleUtils.filterFunctionRoles(roles_to_array))
+
+    //Time to set Oganization Profile
+    context.dispatch('profile/organization/setProfile', profile.organization, {root:true})
+  },
+}

+ 32 - 0
store/profile/organization.ts

@@ -0,0 +1,32 @@
+import {organizationState} from "~/types/types";
+
+export const state = () => ({
+  name: '',
+  product: '',
+  modules: [],
+  hasChildren: false
+})
+
+export const mutations = {
+  setName(state:organizationState, name:string){
+    state.name = name
+  },
+  setProduct(state:organizationState, product:string){
+    state.product = product
+  },
+  setModules(state:organizationState, modules:Array<string>) {
+    state.modules = modules
+  },
+  setHasChildren(state:organizationState, hasChildren:boolean) {
+    state.hasChildren = hasChildren
+  }
+}
+
+export const actions = {
+  setProfile(context:any, profile:any){
+    context.commit('setName', profile.name)
+    context.commit('setProduct', profile.product)
+    context.commit('setModules', profile.modules)
+    context.commit('setHasChildren', profile.hasChildren)
+  }
+}

+ 0 - 9
test/Logo.spec.js

@@ -1,9 +0,0 @@
-import { mount } from '@vue/test-utils'
-import Logo from '@/components/Logo.vue'
-
-describe('Logo', () => {
-  test('is a Vue instance', () => {
-    const wrapper = mount(Logo)
-    expect(wrapper.vm).toBeTruthy()
-  })
-})

+ 58 - 0
test/services/rights/abilitiesUtils.spec.js

@@ -0,0 +1,58 @@
+import {$abilitiesUtils} from "~/services/rights/abilitiesUtils";
+
+test('test transformAbilitiesConfigToAbility', () => {
+  const abilitiesConfig = {
+    "accesses": {
+      "action": "display",
+      "services": {
+        "access": [
+          {
+            "function": "hasAbility",
+            "parameters": {
+              "action": "read",
+              "subject": "user"
+            }
+          }
+        ],
+        "organization": [
+          {
+            "function": "hasModule",
+            "parameters": [
+              "Users"
+            ]
+          }
+        ]
+      }
+    },
+    "student_registration": {
+      "action": "display",
+      "services": {
+        "access": [
+          {
+            "function": "hasAbility",
+            "parameters": {
+              "action": "read",
+              "subject": "student-registration"
+            }
+          }
+        ],
+        "organization": [
+          {
+            "function": "hasModule",
+            "parameters": [
+              "UsersSchool"
+            ]
+          }
+        ]
+      }
+    }
+  };
+
+  const abilitiesUtils = $abilitiesUtils()
+  let abilities_to_have = [
+    {action: 'display', subject: 'accesses'},
+    {action: 'display', subject: 'student_registration'}
+  ]
+
+  expect(abilitiesUtils.transformAbilitiesConfigToAbility(abilitiesConfig)).toStrictEqual(abilities_to_have);
+})

+ 56 - 0
test/services/rights/roleUtils.spec.js

@@ -0,0 +1,56 @@
+import {$roleUtils} from "~/services/rights/roleUtils";
+
+const roles = [
+  'ROLE_BOOK_CONFIG_VIEW',
+  'ROLE_ROOM_CONFIG',
+  'ROLE_USER',
+  'ROLE_ADMIN',
+  'ROLE_ADMIN_CORE',
+  'ROLE_PLACE_VIEW',
+  'ROLE_ADMINISTRATIF_MANAGER_CORE',
+];
+
+const final_role = [
+  'ROLE_GENERAL-CONFIG_VIEW',
+  'ROLE_TAGG-ADVANCED',
+  'ROLE_ROOM',
+  'ROLE_USER_EDIT'
+];
+
+test('test isA', () => {
+  expect($roleUtils.isA('ADMINISTRATIF_MANAGER', roles)).toBeTruthy();
+  expect($roleUtils.isA('TEACHER', roles)).toBeFalsy();
+})
+
+test('test filterFunctionRoles', () => {
+  let filter_roles = [
+    'ROLE_BOOK_CONFIG_VIEW',
+    'ROLE_ROOM_CONFIG',
+    'ROLE_USER',
+    'ROLE_PLACE_VIEW'
+  ];
+
+  expect($roleUtils.filterFunctionRoles(roles)).toStrictEqual(filter_roles);
+});
+
+test('test transformUnderscoreToHyphenBeforeCompleteMigration', () => {
+  let roles_to_array = [
+    'ROLE_GENERAL_CONFIG_VIEW',
+    'ROLE_TAGG_ADVANCED',
+    'ROLE_ROOM',
+    'ROLE_USER_EDIT'
+  ];
+
+  expect($roleUtils.transformUnderscoreToHyphenBeforeCompleteMigration(roles_to_array)).toStrictEqual(final_role);
+});
+
+test('test transformRoleToAbilities', () => {
+  let abilities_to_have = [
+    {action: 'read', subject: 'book-config'},
+    {action: 'read', subject: 'place'},
+    {action: 'manage', subject: 'room-config'},
+    {action: 'manage', subject: 'user'}
+  ]
+
+  expect($roleUtils.transformRoleToAbilities(final_role)).toStrictEqual(abilities_to_have);
+})

+ 16 - 0
test/services/utils/files/test.yaml

@@ -0,0 +1,16 @@
+abilities:
+  accesses:
+    action: 'display'
+    services:
+      access :
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'user'}]}
+      organization  :
+        - {function: hasModule, parameters: ['Users']}
+
+  student_registration:
+    action: 'display'
+    services:
+      access:
+        - {function: hasAbility, parameters: [{action: 'read', subject: 'student-registration'}]}
+      organization:
+        - {function: hasModule, parameters: ['UsersSchool']}

+ 33 - 0
test/services/utils/hydraParser.spec.js

@@ -0,0 +1,33 @@
+import HydraParser from "~/services/utils/hydraParser";
+
+test('test parseItem', () => {
+  const hydra = new HydraParser()
+
+  let serverResponse = {
+    "@context": "\/api\/contexts\/Access",
+    "@id": "\/api\/accesses\/7351",
+    "@type": "Access",
+    "organization": "\/api\/organizations\/37306",
+    "id": 7351,
+    "person": {
+      "@type": "Person",
+      "id": 11344,
+      "name": "BRUEL",
+      "givenName": "Patrick",
+    }
+  }
+
+  expect(hydra.parse(serverResponse)).toStrictEqual({
+    "@context": "/api/contexts/Access",
+    "@id": "/api/accesses/7351",
+    "@type": "Access",
+    "id": 7351,
+    "organization": "/api/organizations/37306",
+    "person": {
+      "@type": "Person",
+      "givenName": "Patrick",
+      "id": 11344,
+      "name": "BRUEL"
+    }
+  });
+});

+ 53 - 0
test/services/utils/yamlParser.spec.js

@@ -0,0 +1,53 @@
+import YamlParser from "~/services/utils/yamlParser";
+
+test('test parse', () => {
+  const path = './test/services/utils/files/test.yaml'
+  expect(YamlParser.parse(path)).toStrictEqual({
+    "abilities": {
+      "accesses": {
+        "action": "display",
+        "services": {
+          "access": [
+            {
+              "function": "hasAbility",
+              "parameters": [{
+                "action": "read",
+                "subject": "user"
+              }]
+            }
+          ],
+          "organization": [
+            {
+              "function": "hasModule",
+              "parameters": [
+                "Users"
+              ]
+            }
+          ]
+        }
+      },
+      "student_registration": {
+        "action": "display",
+        "services": {
+          "access": [
+            {
+              "function": "hasAbility",
+              "parameters": [{
+                "action": "read",
+                "subject": "student-registration"
+              }]
+            }
+          ],
+          "organization": [
+            {
+              "function": "hasModule",
+              "parameters": [
+                "UsersSchool"
+              ]
+            }
+          ]
+        }
+      }
+    }
+  });
+});

+ 2 - 1
tsconfig.json

@@ -27,7 +27,8 @@
       "@types/node",
       "@nuxt/types",
       "@nuxt/http",
-      "@nuxtjs/vuetify"
+      "@nuxtjs/vuetify",
+      "nuxt-i18n"
     ]
   },
   "exclude": [

+ 51 - 0
types/types.d.ts

@@ -0,0 +1,51 @@
+import {Store} from "vuex";
+interface ItemsMenu extends Array<ItemMenu> {}
+
+interface ItemMenu {
+  icon: string,
+  title: string,
+  to?: string,
+  old?: boolean,
+  children?: ItemsMenu
+}
+
+interface AbilitiesType {
+  action: 'display' | 'read' | 'manage',
+  subject: string,
+  /** an array of fields to which user has (or not) access */
+  fields?: string[]
+  /** an object of conditions which restricts the rule scope */
+  conditions?: any
+  /** indicates whether rule allows or forbids something */
+  inverted?: boolean
+  /** message which explains why rule is forbidden */
+  reason?: string
+}
+
+interface accessState {
+  bearer: string,
+  accessId: number,
+  roles: Array<string>,
+  abilities: Array<AbilitiesType>,
+  isAdmin: boolean,
+  isAdministratifManager: boolean,
+  isPedagogicManager: boolean,
+  isFinancialManager: boolean,
+  isCaMember: boolean,
+  isStudent: boolean,
+  isTeacher: boolean,
+  isMember: boolean,
+  isOther: boolean
+}
+interface AccessStore extends Store<{profile:{access:accessState}}> {}
+
+interface organizationState {
+  name: string,
+  product: string,
+  modules: Array<string>,
+  hasChildren: boolean,
+}
+interface OrganizationStore extends Store<{profile:{organization:organizationState}}> {}
+
+interface AnyJson extends Record<string, any> {}
+interface AnyStore extends Store<any> {}

+ 153 - 0
use/template/menu.ts

@@ -0,0 +1,153 @@
+import {ref, useContext} from "@nuxtjs/composition-api";
+import {ItemMenu, ItemsMenu} from "~/types/types";
+import {organizationProfile} from "~/services/profile/organizationProfile";
+
+export default class Menu{
+  private $ability:any;
+  private $config:any;
+  private $store:any;
+
+  constructor() {
+    const {$ability, $config, store} = useContext();
+    this.$ability = $ability;
+    this.$config = $config;
+    this.$store = store;
+  }
+
+  useMenuConstruct(){
+    let menu:ItemsMenu = [ this.constructMenu('fa-home', 'welcome', '/dashboard', true) ]
+
+    const accessMenu = this.accessMenu()
+    if(accessMenu) menu.push(accessMenu)
+
+    const agendaMenu = this.agendaMenu()
+    if(agendaMenu) menu.push(agendaMenu)
+
+    const equipmentMenu = this.equipmentMenu()
+    if(equipmentMenu) menu.push(equipmentMenu)
+
+    const educationalMenu = this.educationalMenu()
+    if(educationalMenu) menu.push(educationalMenu)
+
+    return ref(menu)
+  }
+
+  /**
+   * Menu Répertoire
+   */
+  accessMenu():ItemMenu | null {
+    const children:ItemsMenu = [];
+
+    if (this.$ability().can('display', 'accesses_page')) {
+      const organization = organizationProfile(this.$store)
+      let to = organization.isSchool() ? `/students/list/` : `/adherent/list/`
+      children.push(this.constructMenu('fa-user', 'person', to, true))
+    }
+
+    if (this.$ability().can('display', 'student_registration_page')) {
+      children.push(this.constructMenu('fa-users', 'family_view', '/student_registration/new', true))
+    }
+
+    if (this.$ability().can('display', 'education_student_next_year_page')) {
+      children.push(this.constructMenu('fa-list-alt', 'education_student_next_year', '/education_student_next_year/list/', true))
+    }
+    if (this.$ability().can('display', 'commissions_page')) {
+      children.push(this.constructMenu('fa-street-view', 'commissions', '/commissions/list/', true))
+    }
+    if (this.$ability().can('display', 'network_children_page')) {
+      children.push(this.constructMenu('fa-sitemap', 'network', 'networks/list/', true))
+    }
+    if (this.$ability().can('display', 'network_parents_page')) {
+      children.push(this.constructMenu('fa-sitemap', 'my_network', '/network_artist_schools/list/', true))
+    }
+
+    if(children.length === 1){
+      return children[0];
+    }
+    return children.length > 0 ? this.constructMenu('fa-address-book', 'address_book', undefined, undefined, children) : null;
+  }
+
+  /**
+   * Menu agenda
+   */
+  agendaMenu():ItemMenu | null {
+    const children:ItemsMenu = [];
+
+    if (this.$ability().can('display', 'agenda_page')) {
+      children.push(this.constructMenu('fa-calendar-alt', 'schedule', '/calendar', true))
+    }
+
+    if (this.$ability().can('display', 'attendance_page')) {
+      children.push(this.constructMenu('fa-calendar-check', 'attendances', '/attendances/list/', true))
+    }
+
+    if(children.length === 1){
+      return children[0];
+    }
+    return children.length > 0 ? this.constructMenu('fa-calendar-alt', 'schedule', undefined, undefined, children) : null;
+  }
+
+  /**
+   * Menu equipment
+   */
+  equipmentMenu():ItemMenu | null {
+    return this.constructMenu('fa-cube', 'equipment', '/equipment/list', true)
+  }
+
+  /**
+   * Menu suivi pédagogique
+   */
+  educationalMenu():ItemMenu | null {
+    const children:ItemsMenu = [];
+
+    if (this.$ability().can('display', 'criteria_notations_page')) {
+      children.push(this.constructMenu('fa-bars', 'criteria_notations', '/criteria_notations/list/', true))
+    }
+
+    if (this.$ability().can('display', 'seizure_period_page')) {
+      children.push(this.constructMenu('fa-calendar-alt', 'seizure_period', '/education_teachers/list/', true))
+    }
+
+    if (this.$ability().can('display', 'test_seizure_page')) {
+      children.push(this.constructMenu('fa-pencil-alt', 'test_seizure', '/education_input/list/', true))
+    }
+
+    if (this.$ability().can('display', 'test_validation_page')) {
+      children.push(this.constructMenu('fa-check', 'test_validation', '/education_notations/list/', true))
+    }
+
+    if (this.$ability().can('display', 'examen_results_page')) {
+      children.push(this.constructMenu('fa-graduation-cap', 'examen_results', '/examen_convocations/list/', true))
+    }
+
+    if (this.$ability().can('display', 'education_by_student_validation_page')) {
+      children.push(this.constructMenu('fa-check-square', 'education_by_student_validation', '/education_by_student/list/', true))
+    }
+
+    if(children.length === 1){
+      return children[0];
+    }
+    return children.length > 0 ? this.constructMenu('fa-graduation-cap', 'education_state', undefined, undefined, children) : null;
+  }
+
+  /**
+   * Construit un ItemMenu
+   * @param icon
+   * @param title
+   * @param link
+   * @param isOldLink
+   * @param children
+   */
+  constructMenu(icon: string, title: string, link?: string, isOldLink?: boolean, children?: Array<ItemMenu>): ItemMenu{
+    return children ? {
+      icon: icon,
+      title: title,
+      children: children,
+    } : {
+      icon: icon,
+      title: title,
+      to: `${isOldLink ? this.$config.baseURL_adminLegacy : ''}${link}`,
+      old: isOldLink,
+    }
+  }
+}

File diff suppressed because it is too large
+ 472 - 47
yarn.lock


Some files were not shown because too many files changed in this diff