From 1a7c84ae5574df2eae4bc28fe153f36d62103dfa Mon Sep 17 00:00:00 2001 From: Florent Lavelle <flavelle@neogeo.fr> Date: Thu, 30 Sep 2021 14:13:05 +0200 Subject: [PATCH] Initialize source code (beta version) Co-authored-by: Florent <florent@Air-de-neogeo.neogeo.local> Co-authored-by: m431m <mmeliani@neogeo.fr> Co-authored-by: Kevin Rakotobe <krakotobe@neogeo.fr> --- .browserslistrc | 3 + .editorconfig | 7 + .eslintrc.js | 30 + .gitignore | 31 + .gitlab-ci.yml | 69 ++ README.md | 65 +- babel.config.js | 5 + cypress.json | 3 + jest.config.js | 3 + package.json | 48 ++ public/config/config.json.sample | 13 + public/favicon.ico | Bin 0 -> 4286 bytes public/index.html | 18 + src/App.vue | 11 + src/api/loginAPI.js | 165 ++++ src/api/organisationsAPI.js | 63 ++ src/api/usergroupsAPI.js | 45 ++ src/api/usersAPI.js | 35 + src/app.less | 125 ++++ src/app.scss | 4 + src/assets/icons/file_document_sheet.svg | 10 + src/assets/logo_pigma.png | Bin 0 -> 69765 bytes src/components/ImportImage.vue | 173 +++++ src/components/OrganisationCreation.vue | 353 +++++++++ src/components/OrganisationSelector.vue | 214 ++++++ src/components/SearchUsergroups.vue | 117 +++ src/main.js | 25 + src/router/index.js | 79 ++ src/services/error-service.js | 55 ++ src/store/index.js | 23 + src/store/modules/forgotten-pwd.store.js | 105 +++ src/store/modules/index.js | 16 + src/store/modules/organisations.store.js | 97 +++ src/store/modules/sign-in.store.js | 78 ++ src/store/modules/sign-out.store.js | 57 ++ src/store/modules/sign-up.store.js | 115 +++ src/store/modules/terms-of-use.store.js | 77 ++ src/store/modules/user.store.js | 91 +++ src/store/modules/usergroups.store.js | 113 +++ src/store/modules/validation-email.store.js | 37 + .../modules/validation-registration.store.js | 45 ++ src/views/ForgottenPassword.vue | 146 ++++ src/views/NotFound.vue | 11 + src/views/ReinitPassword.vue | 292 ++++++++ src/views/SignIn.vue | 251 +++++++ src/views/SignOut.vue | 95 +++ src/views/SignOutFailed.vue | 107 +++ src/views/SignUp.vue | 598 +++++++++++++++ src/views/SignUpSuccess.vue | 70 ++ src/views/TermsOfUse.vue | 166 ++++ src/views/UserProfile.vue | 708 ++++++++++++++++++ src/views/ValidationEmail.vue | 98 +++ src/views/ValidationRegistration.vue | 126 ++++ tests/e2e/.eslintrc.js | 12 + tests/e2e/plugins/index.js | 26 + tests/e2e/specs/test.js | 8 + tests/e2e/support/commands.js | 25 + tests/e2e/support/index.js | 20 + tests/unit/example.spec.js | 12 + vue.config.js | 15 + 60 files changed, 5408 insertions(+), 1 deletion(-) create mode 100644 .browserslistrc create mode 100644 .editorconfig create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 babel.config.js create mode 100644 cypress.json create mode 100644 jest.config.js create mode 100644 package.json create mode 100644 public/config/config.json.sample create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 src/App.vue create mode 100644 src/api/loginAPI.js create mode 100644 src/api/organisationsAPI.js create mode 100644 src/api/usergroupsAPI.js create mode 100644 src/api/usersAPI.js create mode 100644 src/app.less create mode 100644 src/app.scss create mode 100644 src/assets/icons/file_document_sheet.svg create mode 100644 src/assets/logo_pigma.png create mode 100644 src/components/ImportImage.vue create mode 100644 src/components/OrganisationCreation.vue create mode 100644 src/components/OrganisationSelector.vue create mode 100644 src/components/SearchUsergroups.vue create mode 100644 src/main.js create mode 100644 src/router/index.js create mode 100644 src/services/error-service.js create mode 100644 src/store/index.js create mode 100644 src/store/modules/forgotten-pwd.store.js create mode 100644 src/store/modules/index.js create mode 100644 src/store/modules/organisations.store.js create mode 100644 src/store/modules/sign-in.store.js create mode 100644 src/store/modules/sign-out.store.js create mode 100644 src/store/modules/sign-up.store.js create mode 100644 src/store/modules/terms-of-use.store.js create mode 100644 src/store/modules/user.store.js create mode 100644 src/store/modules/usergroups.store.js create mode 100644 src/store/modules/validation-email.store.js create mode 100644 src/store/modules/validation-registration.store.js create mode 100644 src/views/ForgottenPassword.vue create mode 100644 src/views/NotFound.vue create mode 100644 src/views/ReinitPassword.vue create mode 100644 src/views/SignIn.vue create mode 100644 src/views/SignOut.vue create mode 100644 src/views/SignOutFailed.vue create mode 100644 src/views/SignUp.vue create mode 100644 src/views/SignUpSuccess.vue create mode 100644 src/views/TermsOfUse.vue create mode 100644 src/views/UserProfile.vue create mode 100644 src/views/ValidationEmail.vue create mode 100644 src/views/ValidationRegistration.vue create mode 100644 tests/e2e/.eslintrc.js create mode 100644 tests/e2e/plugins/index.js create mode 100644 tests/e2e/specs/test.js create mode 100644 tests/e2e/support/commands.js create mode 100644 tests/e2e/support/index.js create mode 100644 tests/unit/example.spec.js create mode 100644 vue.config.js diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 0000000..214388f --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,3 @@ +> 1% +last 2 versions +not dead diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c24743d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 100 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..68aa83f --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,30 @@ +const config = require('./public/config/config.json'); + +module.exports = { + root: true, + env: { + node: true, + }, + extends: [ + 'plugin:vue/essential', + '@vue/airbnb', + ], + parserOptions: { + parser: 'babel-eslint', + }, + rules: { + 'no-console': config.NODE_ENV === 'production' ? 'warn' : 'off', + 'no-debugger': config.NODE_ENV === 'production' ? 'warn' : 'off', + }, + overrides: [ + { + files: [ + '**/__tests__/*.{j,t}s?(x)', + '**/tests/unit/**/*.spec.{j,t}s?(x)', + ], + env: { + jest: true, + }, + }, + ], +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d451809 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +.DS_Store +node_modules +/dist + +/tests/e2e/videos/ +/tests/e2e/screenshots/ + +# local env files +.env +.env.* +.env.local +.env.*.local +dist +package-lock.json +jsconfig.json +config.json + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..291f700 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,69 @@ +variables: + SONAR_TOKEN: "$SONAR_TOKEN" + SONAR_PROJECTKEY: "$CI_PROJECT_NAME" + SONAR_HOST_URL: "https://sonarqube.neogeo.fr" + GIT_DEPTH: 0 + +cache: + paths: + - node_modules/ + +stages: + - sonarqube + - build + - deploy + +sonarqube-check: + image: + name: sonarsource/sonar-scanner-cli:latest + entrypoint: [""] + stage: sonarqube + script: + - sonar-scanner -Dsonar.qualitygate.wait=true -Dsonar.projectKey=$CI_PROJECT_NAME -Dsonar.projectName=$CI_PROJECT_NAME -Dsonar.projectVersion=$CI_COMMIT_BRANCH + allow_failure: true + only: + - develop + - master + - draft + +build_development: + stage: build + tags: + - build + image: node:14.16.0 + script: + - npm install --unsafe-perm + - echo -e " + NODE_ENV=development\n + VUE_APP_LOCALE=fr-FR\n + DOMAIN=https://dev.pigma.neogeo.fr/fr\n + VUE_APP_DOMAIN=https://dev.pigma.neogeo.fr/\n + BASE_PATH=/\n + VUE_APP_NEXT_DEFAULT=/\n + VUE_APP_BASE_PATH=${BASE_PATH}\n + VUE_APP_LOGIN_API_PATH=/fr/login/\n + VUE_APP_ORGANISATION_API_PATH=/fr/organisation/\n + VUE_APP_USERGROUP_API_PATH=/fr/usergroup/\n + VUE_APP_LOGIN_API_USERNAME=admin\n + VUE_APP_LOGIN_API_PASSWORD=Neogeo2020\n" > .env + - npm run build + artifacts: + paths: + - dist + expire_in: 1 week + +deploy-pigma-dev: + image: alpine + tags: + - deploy-dev + stage: deploy + when: manual + script: + - apk add --no-cache rsync openssh + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" >> ~/.ssh/id_dsa + - chmod 600 ~/.ssh/id_dsa + - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config + - ls -lrth + - scp -o "ProxyCommand ssh debian@10.0.30.17 -W %h:%p" -r dist/* debian@10.9.10.13:/opt/neogeo/onegeo-login + - ssh -J debian@10.0.30.17 debian@10.9.10.13 "echo test" diff --git a/README.md b/README.md index 1075c60..85ab922 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,65 @@ -# Onegeo-Suite Site LogIn +# login-site +## Project setup + +```shell +npm install +``` + +### Set environment variables + +```ìni +NODE_ENV=development +VUE_APP_LOCALE=fr-FR + +DOMAIN=https://dev.pigma.neogeo.fr/fr +VUE_APP_DOMAIN=https://dev.pigma.neogeo.fr/ + +BASE_PATH=/ +VUE_APP_NEXT_DEFAULT=/ + +VUE_APP_BASE_PATH=${BASE_PATH} + +# API +VUE_APP_LOGIN_API_PATH=/fr/login +VUE_APP_ORGANISATION_API_PATH=/fr/organisation/ +VUE_APP_USERGROUP_API_PATH=/fr/usergroup/ + +# AUTH +VUE_APP_LOGIN_API_USERNAME=admin +VUE_APP_LOGIN_API_PASSWORD=Neogeo2020 +``` + +### Compiles and hot-reloads for development + +```shell +npm run serve +``` + +### Compiles and minifies for production + +```shell +npm run build +``` + +### Run your unit tests + +```shell +npm run test:unit +``` + +### Run your end-to-end tests + +```shell +npm run test:e2e +``` + +### Lints and fixes files + +```shell +npm run lint +``` + +### Customize configuration + +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..757ff9b --- /dev/null +++ b/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/cli-plugin-babel/preset', + ], +}; diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..470c720 --- /dev/null +++ b/cypress.json @@ -0,0 +1,3 @@ +{ + "pluginsFile": "tests/e2e/plugins/index.js" +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..3ce878d --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: '@vue/cli-plugin-unit-jest', +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..54dc764 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "onegeo-suite-site-login-vuejs", + "version": "1.0.0dev0", + "private": true, + "scripts": { + "dev": "cross-env NODE_ENV=development vue-cli-service serve --open --host localhost", + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "test:unit": "vue-cli-service test:unit", + "test:e2e": "vue-cli-service test:e2e", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "axios": "^0.21.1", + "bootstrap-vue": "^2.21.2", + "core-js": "^3.6.5", + "corejs-typeahead": "^1.3.1", + "lodash": "^4.17.21", + "sweetalert2": "^11.0.18", + "vee-validate": "^3.4.9", + "vue": "^2.6.11", + "vue-multiselect": "^2.1.6", + "vue-recaptcha": "^1.3.0", + "vue-router": "^3.2.0", + "vuex": "^3.4.0" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "~4.5.0", + "@vue/cli-plugin-e2e-cypress": "~4.5.0", + "@vue/cli-plugin-eslint": "~4.5.0", + "@vue/cli-plugin-router": "~4.5.0", + "@vue/cli-plugin-unit-jest": "~4.5.0", + "@vue/cli-plugin-vuex": "~4.5.0", + "@vue/cli-service": "~4.5.0", + "@vue/eslint-config-airbnb": "^5.0.2", + "@vue/test-utils": "^1.0.3", + "babel-eslint": "^10.1.0", + "cross-env": "^7.0.3", + "eslint": "^6.7.2", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-vue": "^6.2.2", + "less": "^3.0.4", + "less-loader": "^5.0.0", + "sass": "^1.34.0", + "sass-loader": "^10.2.0", + "vue-template-compiler": "^2.6.11" + } +} diff --git a/public/config/config.json.sample b/public/config/config.json.sample new file mode 100644 index 0000000..3f459af --- /dev/null +++ b/public/config/config.json.sample @@ -0,0 +1,13 @@ +{ + "NODE_ENV": "development", + "LOCALE": "fr-FR", + "DOMAIN": "http://127.0.0.1", + "BASE_PATH": "/", + "ROOT_PATH": "", + "NEXT_DEFAULT": "/", + "VUE_APP_LOGIN_API": "http://127.0.0.1/login", + "VUE_APP_ORGANISATION_API": "http://127.0.0.1/organisation/", + "VUE_APP_USERGROUP_API": "http://127.0.0.1/usergroup/", + "VUE_APP_LOGIN_API_USERNAME": "admin", + "VUE_APP_LOGIN_API_PASSWORD": "CHANGE_ME" +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kev<ISBgE$F{SFy+(=9Z)f)De0Se}ZDZW}Z3B zElCeVrw;K0Fdl_Cg=gZOFXXc3pL)Q05CAuT+XucQ<8g~3dteP~|7s7c6QYP;fy;mF zMN;>tV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?<QnEgvj4i?s}Yk=qA2z`-^*<eK3c)MS4JOdbsTQEOa0) z0NWqlna2rzs>5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7<X*Er!BfRbvU93$DH%#v6dRt^6HBxz1xBNHx=$&_Gv<&J}Ljk zJN<Fzx(`Oe@KgQ0F$<14=XV#WK`o#6Ku>z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{T<?%b6i9IjI)Ls)S{-*mq<@~R{?$}ZKjf;^k75i_}(2MXt}^SEBVg7AI@28 zo_uPg2V)_e-`2Ois=PYoe%9u*n9({PFR)OnHJPi{dNx>Kx<YG`4QQ>D#iCLfl2<BD h7L=-;Q>vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f1a8b2b --- /dev/null +++ b/public/index.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang=""> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <link rel="icon" href="<%= BASE_URL %>favicon.ico"> + <link rel="stylesheet" href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css"> + <title><%= htmlWebpackPlugin.options.title %></title> + </head> + <body> + <noscript> + <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> + </noscript> + <div id="app"></div> + <!-- built files will be auto injected --> + </body> +</html> diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..2390341 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,11 @@ +<template> + <div id="app"> + <router-view id="page"/> + </div> +</template> + +<script> +export default { + +}; +</script> diff --git a/src/api/loginAPI.js b/src/api/loginAPI.js new file mode 100644 index 0000000..69e982b --- /dev/null +++ b/src/api/loginAPI.js @@ -0,0 +1,165 @@ +import axios from 'axios'; + +const DEV_AUTH = process.env.NODE_ENV === 'development' ? true : false; + +const AUTH = { + username: process.env.VUE_APP_LOGIN_API_USERNAME, + password: process.env.VUE_APP_LOGIN_API_PASSWORD +}; + +if (!DEV_AUTH) { + axios.defaults.headers.common['X-CSRFToken'] = (name => { + var re = new RegExp(name + "=([^;]+)"); + var value = re.exec(document.cookie); + return (value != null) ? unescape(value[1]) : null; + })('csrftoken'); +} + +const path = require('path'); +const DOMAIN = process.env.VUE_APP_DOMAIN; +const LOGIN_API_PATH = process.env.VUE_APP_LOGIN_API_PATH; + +const loginAPI = { + + async signUp(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/signup/`), DOMAIN); + const response = await axios.post(url, data); + if (response.status === 201) { + return response.data; + } else { + console.error(response); + return false; + } + }, + + async signIn(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/signin/`), DOMAIN); + const response = await axios.post(url, data); + if (response.status === 200) { + return response.data; + } else { + console.error(response); + return false; + } + }, + + async signOut(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/signout/`), DOMAIN); + const response = await axios.get(url, data); + if (response.status === 200) { + return response.data; + } else { + console.error(response); + return false; + } + }, + + async validationRegistration(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/signup-confirmation/`), DOMAIN); + const response = await axios.post(url, data); + if (response.status === 200) { + return true; + } else { + console.error(response); + return false; + } + }, + + async getOrganisationsList() { + const url = new URL(path.join(LOGIN_API_PATH, `/organisations/`), DOMAIN); + const response = await axios.get(url); + if (response.status === 200) { + return response.data; + } else { + console.error(response); + return false; + } + }, + + async getUserDetail() { + const url = new URL(path.join(LOGIN_API_PATH, `/user/`), DOMAIN); + const response = await axios.get(url, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response.data; + } else { + console.error(response); + return false; + } + }, + + async updateUserDetail(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/user/`), DOMAIN); + const response = await axios.put(url, data, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response.data; + } else { + console.error(response); + return false; + } + }, + + async forgottenPasswordRequest(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/forgotten-password-request/`), DOMAIN); + try { + const response = await axios.post(url, data); + if (response.status === 200) { + return response; + } + } catch (err) { + return err.response; + } + }, + + async forgottenPasswordConfirm(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/forgotten-password-confirmation/`), DOMAIN); + try { + const response = await axios.post(url, data); + if (response.status === 200) { + return response; + } + } catch (err) { + return err.response; + } + }, + + async newEmailConfirm(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/email-update-confirmation/`), DOMAIN); + try { + const response = await axios.post(url, data); + if (response.status === 200) { + return true; + } else { + return false; + } + } catch { + return false; + } + }, + + async getTermsOfUse() { + const url = new URL(path.join(LOGIN_API_PATH, `/term-of-use/`), DOMAIN); + try { + const response = await axios.get(url, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response; + } + } catch (err) { + return err.response; + } + }, + + async postTermsOfUseAgreement(data) { + const url = new URL(path.join(LOGIN_API_PATH, `/term-of-use/`), DOMAIN); + try { + const response = await axios.post(url, data, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response; + } + } catch (err) { + return err.response; + } + } + +}; + +export default loginAPI; diff --git a/src/api/organisationsAPI.js b/src/api/organisationsAPI.js new file mode 100644 index 0000000..b41964a --- /dev/null +++ b/src/api/organisationsAPI.js @@ -0,0 +1,63 @@ +import axios from 'axios'; + +const DEV_AUTH = process.env.NODE_ENV === 'development' ? true : false; + +const AUTH = { + username: process.env.VUE_APP_LOGIN_API_USERNAME, + password: process.env.VUE_APP_LOGIN_API_PASSWORD +}; + +if (!DEV_AUTH) { + axios.defaults.headers.common['X-CSRFToken'] = (name => { + var re = new RegExp(name + "=([^;]+)"); + var value = re.exec(document.cookie); + return (value != null) ? unescape(value[1]) : null; + })('csrftoken'); +} + +const path = require('path'); +const DOMAIN = process.env.VUE_APP_DOMAIN; +const ORGANISATION_API_PATH = process.env.VUE_APP_ORGANISATION_API_PATH; +const USERGROUP_API_PATH = process.env.VUE_APP_USERGROUP_API_PATH; + +const organisationsAPI = { + + async getOrganisationsTypes() { + const url = new URL(path.join(ORGANISATION_API_PATH, `organisation-types/`), DOMAIN); + let response; + try { + response = await axios.get(url); + if (response.status === 200) { + return response.data; + } + } catch (err) { + console.error(err); + return false; + } + }, + + async getOrganisationsRoles() { + const url = new URL(path.join(USERGROUP_API_PATH, `user-group-roles/`), DOMAIN); + let response; + try { + response = await axios.get(url); + if (response.status === 200) { + return response.data; + } + } catch (err) { + console.error(err); + return false; + } + }, + + async setOrganisationThumbnail(id, data) { + const url = new URL(path.join(ORGANISATION_API_PATH, `organisations/${id}/thumbnail/`), DOMAIN); + const response = await axios.put(url, data, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response.data; + } + return false; + }, +}; + +export default organisationsAPI; diff --git a/src/api/usergroupsAPI.js b/src/api/usergroupsAPI.js new file mode 100644 index 0000000..55cf151 --- /dev/null +++ b/src/api/usergroupsAPI.js @@ -0,0 +1,45 @@ +import axios from 'axios'; + +const DEV_AUTH = process.env.NODE_ENV === 'development' ? true : false; + +const AUTH = { + username: process.env.VUE_APP_LOGIN_API_USERNAME, + password: process.env.VUE_APP_LOGIN_API_PASSWORD +}; + + +const path = require('path'); +const DOMAIN = process.env.VUE_APP_DOMAIN; +const USERGROUP_API_PATH = process.env.VUE_APP_USERGROUP_API_PATH; + +if (!DEV_AUTH) { + axios.defaults.headers.common['X-CSRFToken'] = (name => { + var re = new RegExp(name + "=([^;]+)"); + var value = re.exec(document.cookie); + return (value != null) ? unescape(value[1]) : null; + })('csrftoken'); +} + +const usergroupsAPI = { + + async getFilteredUsergroupsList(type = 'group-of-organisation', page = 1) { + const url = new URL(path.join(USERGROUP_API_PATH, `user-groups/?page=${page}&usergroup_types=${type}`), DOMAIN); + const response = await axios.get(url, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response.data; + } + return false; + }, + + async updateUsergroup(id, data) { + const url = new URL(path.join(USERGROUP_API_PATH, `user-groups/${id}/`), DOMAIN); + const response = await axios.put(url, data, { ...DEV_AUTH && { auth: AUTH } }); + if (response.status === 200) { + return response.data; + } + return false; + }, + +} + +export default usergroupsAPI; diff --git a/src/api/usersAPI.js b/src/api/usersAPI.js new file mode 100644 index 0000000..2363366 --- /dev/null +++ b/src/api/usersAPI.js @@ -0,0 +1,35 @@ +import axios from 'axios'; + +const DEV_AUTH = process.env.NODE_ENV === 'development' ? true : false; + +const AUTH = { + username: process.env.VUE_APP_LOGIN_API_USERNAME, + password: process.env.VUE_APP_LOGIN_API_PASSWORD +}; + +if (!DEV_AUTH) { + axios.defaults.headers.common['X-CSRFToken'] = (name => { + var re = new RegExp(name + "=([^;]+)"); + var value = re.exec(document.cookie); + return (value != null) ? unescape(value[1]) : null; + })('csrftoken'); +} + +const USERGROUP_API_URL = process.env.VUE_APP_USERGROUP_API; + + +const usersAPI = { + async findUsername(username, page = 1) { + const url = `${USERGROUP_API_URL}users/?page=${page}&username=${username}`; + const response = await axios.get( + url, + { ...DEV_AUTH && { auth: AUTH } } + ); + if (response.status === 200) { + return response.data; + } + return false; + } +} + +export default usersAPI; diff --git a/src/app.less b/src/app.less new file mode 100644 index 0000000..6702c1a --- /dev/null +++ b/src/app.less @@ -0,0 +1,125 @@ +html { + height: 100%; +} + +body { + height: 100%; +} + +#app { + height: 100%; +} + +#page { + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: column; + align-items: center; +} + +form { + margin: 1em 0 1em; + .form-row { + p { + width: 100%; + color: grey; + font-style: italic; + font-size: 0.7em; + margin-bottom: 0.2em; + } + .form-group { + input { + // font-size: 0.9em; + color: rgb(29, 29, 29); + font-weight: 400; + } + input::placeholder { + color: rgb(192, 192, 192); + } + } + .form-group.disabled { + opacity: 0.4; + pointer-events: none; + } + } + .form-row.disabled { + opacity: 0.4; + pointer-events: none; + } +} + +label { + font-size: 0.8em; + font-weight: bold; + margin-bottom: 0.1em; +} + +label.required:after { + content: ' *'; + color: rgb(209, 0, 0); +} + +.form-control { + border-width: 2px; +} + +.control { + width: 100%; + .form-errors { + display: block; + font-size: 0.8em; + font-style: italic; + } +} +.control.is-invalid { + .form-errors { + color: #EB0600 !important; + font-size: 0.75rem; + position: absolute; + padding-top: 1px; + } + input { + border: 1px #EB0600 solid; + } +} +.control.is-valid { + .form-errors { + color: #045929; + } + input { + border: 1px #045929 solid; + } +} + +// Multiselect +.multiselect__tags { + border: 2px solid #ced4da; +} + +.multiselect__placeholder { + color: #838383; +} + +.multiselect__option--highlight { + background-color: @blue; +} + +.multiselect__option--selected.multiselect__option--highlight { + background-color: @blue; +} + +.search-usergroups { + .multiselect__select { + display: none !important; + } + .multiselect__clear { + position: absolute; + right: 0; + top: 3px; + width: 40px; + display: block; + cursor: pointer; + z-index: 2; + } +} diff --git a/src/app.scss b/src/app.scss new file mode 100644 index 0000000..4e2d6c4 --- /dev/null +++ b/src/app.scss @@ -0,0 +1,4 @@ +$primary: #187CC6; + +@import 'node_modules/bootstrap/scss/bootstrap.scss'; +@import 'node_modules/bootstrap-vue/src/index.scss'; diff --git a/src/assets/icons/file_document_sheet.svg b/src/assets/icons/file_document_sheet.svg new file mode 100644 index 0000000..f755b4f --- /dev/null +++ b/src/assets/icons/file_document_sheet.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#187CC6;} +</style> +<path class="st0" d="M59.85,0.77H20.46c-5.42,0-9.8,4.43-9.8,9.85l-0.05,78.77c0,5.42,4.38,9.85,9.8,9.85h59.13 + c5.42,0,9.85-4.43,9.85-9.85V30.31L59.85,0.77z M20.46,89.39V10.61h34.46v24.62h24.62v54.16H20.46z"/> +</svg> diff --git a/src/assets/logo_pigma.png b/src/assets/logo_pigma.png new file mode 100644 index 0000000000000000000000000000000000000000..d51900857e1fc949fbee7addfdd24309d481f1d9 GIT binary patch literal 69765 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%0Dff})dp?&z_K$Wq8Sz`3PeU%cBB-* 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 JPDHLkV1k}W<M;po literal 0 HcmV?d00001 diff --git a/src/components/ImportImage.vue b/src/components/ImportImage.vue new file mode 100644 index 0000000..ac1b294 --- /dev/null +++ b/src/components/ImportImage.vue @@ -0,0 +1,173 @@ +<template> + <div :class="{disabled: disabled}"> + <img + :id="previewId" + :hidden="!filePreview" + style="max-width: 400px; margin: 1em 1em 0.3em;" + alt="Image preview" + /><br v-if="filePreview"> + <div v-if="fileExist || filePreview"> + <div v-if="fileExist && !filePreview"> + <img + :src="fileURL" + style="max-width: 400px; margin: 1em 1em 0.3em;" + alt="Image preview" + /> + <p style="margin: 0 0 0.5em 1.3em"> + {{ title.split('/').slice(-1)[0] }} + </p> + </div> + </div> + <div + v-else + id="import-file" + @click="uploadFile" + > + <img + src="@/assets/icons/file_document_sheet.svg" + alt="Icône fichier" + /> + <div v-if="type === 'logo'"><b-icon-plus />Ajouter un logo</div> + <div v-if="type === 'image'"><b-icon-plus />Ajouter une image</div> + </div> + <div> + <input + :id="`upload-file-${this.name}`" + :hidden="!(fileExist || filePreview)" + type="file" + accept="image/*" + placeholder="Importer un fichier" + @change="previewFile" + > + </div> + </div> +</template> + +<script> +import { mapState } from 'vuex'; + +export default { + name: 'ImportImage', + + props: { + name: { + type: String, + default: '' + }, + title: { + type: String, + default: '' + }, + type: { + type: String, + default: 'logo' + }, + file: { + type: String, + default: null + }, + disabled: { + type: Boolean, + default: false + } + }, + + data() { + return { + fileExist: false, + filePreview: false + } + }, + + computed: { + previewId() { + return `preview-file-${this.name}`; + }, + fileURL() { + if (this.file) { + return `${process.env.VUE_APP_DOMAIN}${this.file}`; + } else { + return '' + } + } + }, + + watch: { + accept(newValue, oldValue) { + if (newValue || (!newValue && oldValue)) { + this.filePreview = false; + } + } + }, + + created() { + if (this.file) { + this.fileExist = true; + } + }, + + methods: { + uploadFile() { + const elem = document.getElementById(`upload-file-${this.name}`); + elem.click(); + }, + previewFile(e) { + const previewEl = document.getElementById(this.previewId); + if (e.target.files && e.target.files[0]) { + this.fileExist = false; + this.filePreview = true; + previewEl.src = URL.createObjectURL(e.target.files[0]); + previewEl.onload = function() { + URL.revokeObjectURL(previewEl.src) // free memory + } + this.$emit('update', e.target.files[0]); + } + } + } +} +</script> + +<style lang="less" scoped> + +#import-file { + cursor: pointer; + margin-top: 0.5em; + border: 1px dashed @blue; + border-radius: 3px; + width: 240px; + height: 130px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-evenly; + img { + height: 2.5em; + } + div { + color: @blue; + font-size: 1.1em; + font-weight: bold; + font-style: normal; + } +} +#import-file:hover { + border: 2px dashed @blue; + img { + height: 2.7em; + } + div { + font-size: 1.3em; + + } +} + +#preview-file { + max-width: 15em; +} + +.disabled { + pointer-events: none; + opacity: 0.6; +} + +</style> diff --git a/src/components/OrganisationCreation.vue b/src/components/OrganisationCreation.vue new file mode 100644 index 0000000..9706792 --- /dev/null +++ b/src/components/OrganisationCreation.vue @@ -0,0 +1,353 @@ +<template> + <div> + <b-button-close @click="$emit('cancel')" class="close"/> + <h6>Créer une nouvelle organisation</h6> + <br> + <form> + <div class="form-row"> + <div class="form-group col-11"> + <ValidationProvider rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <label class="required">Nom de l'organisation</label> + <p>Inscrivez le nom complet et en toutes lettres ; par ex. « Communauté des Communes Rurales de l'Entre-Deux-Mers »</p> + <input + v-model="formData.name" + class="form-control" + type="text" + placeholder="" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="form-row"> + <div class="form-group col-6"> + <div class="control"> + <label>Sigle</label> + <input + v-model="formData.sigle" + class="form-control" + type="text" + placeholder="" + > + </div> + </div> + </div> + <div class="form-row"> + <div class="form-group col-6"> + <label>Numéro SIRET</label> + <input + v-model="formData.siret" + class="form-control" + type="text" + placeholder="xxx xxx xxx xxxxx" + > + </div> + </div> + <div class="form-row"> + <div class="form-group col-6"> + <ValidationProvider rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <label class="required">Type d'organisation</label> + <Multiselect + v-model="formData.type" + :options="organisationsTypes" + track-by="codename" + label="display_name" + selectLabel="" + selectedLabel="" + deselectLabel="" + :searchable="false" + placeholder="Sélectionnez un type" + /> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="form-row"> + <div class="form-group col-6"> + <label>Logo de l'organisation</label> + <p>Importez un logo au format JPG ou PNG (taille maximale : 2 Mo)</p> + <ValidationProvider ref="thumbnail" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <ImportImage + name="logo" + :title="null" + :file="null" + @update="setThumbnail" + /> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="form-row"> + <div class="form-group col-9"> + <label>Site internet</label> + <input + v-model="formData.web" + class="form-control" + type="text" + placeholder="https://" + @focus="formData.web === null ? formData.web = 'https://' : null" + @blur="formData.web === 'https://' ? formData.web = null : null" + > + </div> + </div> + <div class="form-row"> + <div class="form-group col-6"> + <label>Numéro de téléphone</label> + <input + v-model="formData.tel" + class="form-control" + type="text" + placeholder="" + > + </div> + </div> + <div class="form-row"> + <div class="form-group col-11"> + <label>Adresse postale</label> + <textarea + v-model="formData.postalAddress" + class="form-control" + /> + </div> + </div> + <div class="form-row"> + <div class="form-group col-11"> + <label>Description de l'organisation</label> + <textarea + v-model="formData.description" + class="form-control" + /> + </div> + </div> + <!-- <div class="form-row"> + <div class="form-group col-11"> + <label> + Ajouter cette organisation à des groupe d'organisations + </label> + <p> + L'ajout de cette organisation à un groupe d'organisation donnera accès aux données et collections de données associées à ce groupe d'organisation. + </p> + <SearchUsergroups + :type="'group-of-organisation'" + @select="addOrgToSphere" + /> + <div + v-if="spheres.length > 0" + id="orga-spheres-container" + > + <div + v-for="sphere of spheres" + :key="sphere.id" + class="orga-sphere" + > + {{ sphere.display_name }} + <b-icon-x + font-scale="1.5" + @click="removeOrgFromSphere" + /> + </div> + </div> + </div> + </div> --> + </form> + <hr class="divider" /> + </div> +</template> + +<script> +import { + mapState, + mapActions +} from 'vuex'; + +import ImportImage from '@/components/ImportImage'; +// import SearchUsergroups from '@/components/SearchUsergroups'; + +import { + ValidationObserver, + ValidationProvider, + extend, + configure +} from 'vee-validate'; +import { + required +} from 'vee-validate/dist/rules'; + + +extend('required', { + ...required, + message: 'Ce champ est requis' +}); + +configure({ + classes: { + valid: 'is-valid', + invalid: 'is-invalid' + } +}); + +export default { + name: 'OrganisationCreation', + + components: { + ValidationObserver, + ValidationProvider, + ImportImage, + // SearchUsergroups + }, + + data() { + return { + formData: { + name: null, + sigle: null, + siret: null, + type: null, + web: null, + postalAddress: null, + tel: null, + description: null + }, + thumbnail: null, + spheres: [] + }; + }, + + computed: { + ...mapState('organisations', [ + 'organisationsTypes' + ]), + ...mapState('sign-up', [ + 'error' + ]) + }, + + watch: { + formData: { + deep: true, + handler(newValue) { + this.$emit('update', { + form: newValue, + thumbnail: this.thumbnail, + spheres: this.spheres + }); + } + }, + thumbnail(newValue) { + this.$emit('update', { + form: this.formData, + thumbnail: newValue, + spheres: this.spheres + }); + }, + spheres: { + deep: true, + handler(newValue) { + this.$emit('update', { + form: this.formData, + thumbnail: this.thumbnail, + spheres: newValue + }); + } + }, + error(newValue) { + if (newValue) { + for (const [key, value] of Object.entries(newValue)) { + this.$refs[key].applyResult({ + errors: value, + valid: false, + failedRules: {}, + }); + } + } + } + }, + + created() { + if (this.organisationsTypes.length === 0) { + this.GET_ORGANISATIONS_TYPES(); + } + this.$emit('update', { + form: this.formData, + thumbnail: this.thumbnail, + spheres: this.spheres + }); + }, + + methods: { + ...mapActions('organisations', ['GET_ORGANISATIONS_TYPES']), + + setThumbnail(e) { + const formData = new FormData(); + formData.append('file', e); + this.thumbnail = formData; + }, + + addOrgToSphere(e) { + this.spheres.push(e); + }, + removeOrgFromSphere(e) { + const index = this.spheres.findIndex(el => el.id === e.id); + this.spheres.splice(index, 1); + } + } +} +</script> + +<style lang="less" scoped> +form { + min-width: 800px; + margin-bottom: 3em; +} + +h2 { + color: @blue; + margin-top: 0.5em; + margin-left: 0.5em; +} + +h4 { + color: @blue; + margin-top: 0.8em; +} + +button { + margin: 1em 2em 0 0; + font-size: 1em; +} + +.close { + font-size: 2rem; + position: relative; + display: block; + float: right; + top: -2.5rem; + right: -2.5rem; +} + +#orga-spheres-container { + display: flex; + flex-flow: row wrap; + padding: 0.1em; + margin: 0.5em 0; + .orga-sphere { + display: flex; + margin: 0.2em 1em 0.2em 0; + padding: 0.5em; + border-radius: 10px; + font-size: 0.9em; + background-color: @blue; + color: white; + .b-icon { + cursor: pointer; + } + } +} + +</style> diff --git a/src/components/OrganisationSelector.vue b/src/components/OrganisationSelector.vue new file mode 100644 index 0000000..717679c --- /dev/null +++ b/src/components/OrganisationSelector.vue @@ -0,0 +1,214 @@ +<template> + <div> + <h5>Organisation</h5> + <hr class="divider" /> + <div v-if="!showCreationForm"> + <label>Sélectionnez votre organisation dans la liste ci-dessous.</label> + <Multiselect + v-model="organisation" + style="margin-top: 0.5em;" + class="search-usergroups" + :options="suggestions.local" + :optionsLimit="10" + :allow-empty="false" + track-by="id" + label="display_name" + :resetAfter="false" + selectLabel="" + selectedLabel="" + deselectLabel="" + :searchable="true" + placeholder="Recherchez une organisation ..." + :showNoResults="true" + :loading="loading" + :clearOnSelect="false" + :preserveSearch="false" + @search-change="search" + > + <template slot="clear"> + <div + v-if="organisation" + class="multiselect__clear" + @click.prevent.stop="organisation = null" + > + <b-icon-x font-scale="2"/> + </div> + </template> + <span slot="noResult"> + Aucun résultat. + </span> + <span slot="noOptions"> + Saississez les premiers caractères d'une organisation pour lancer la recherche + </span> + </Multiselect> + <label> + Si celle-ci n'est pas dans la liste, vous pouvez + <b-link @click="showCreationForm = true"> + indiquer une nouvelle organisation. + </b-link> + </label> + </div> + <OrganisationCreation + v-if="showCreationForm" + @update="updateOrganisationToCreate" + @cancel="showCreationForm = false" + /> + </div> +</template> + +<script> +import { mapState, mapActions } from 'vuex'; +import Bloodhound from 'corejs-typeahead/dist/bloodhound'; + +import OrganisationCreation from '@/components/OrganisationCreation.vue'; + +export default { + name: 'OrganisationSelector', + components: { + OrganisationCreation + }, + data() { + return { + loading: false, + text: null, + suggestions: { + local: [] + }, + results: [], + organisation: null, + role: null, + showCreationForm: false, + organisationToCreate: { + codename: null, + display_name: null, + description: null, + organisation_type: null, + registration_number: null, + phone_number: null, + website_url: null, + postal_address: null, + }, + organisationThumbnail: null, + organisationSpheres: null, + selectedOrganisation: null, + }; + }, + computed: { + ...mapState('organisations', [ + 'organisationsList', + 'organisationsRoles' + ]) + }, + watch: { + text(newValue) { + this.suggestions.search(newValue, data => { + this.results = data.map(function(item) { + return item.id; + }); + }); + }, + + organisationsList: { + deep: true, + handler(newValue) { + const SuggestionsEngine = Bloodhound.noConflict(); + let local = []; + newValue.map(function(item) { + local.push({ + id: item.id, + display_name: item.display_name + }); + }); + this.suggestions = new SuggestionsEngine({ + local: local, + initialize: true, + datumTokenizer: Bloodhound.tokenizers.obj.nonword('title'), + queryTokenizer: Bloodhound.tokenizers.nonword + }); + } + }, + + organisation(newValue) { + if (newValue) { + this.selectedOrganisation = newValue; + } + }, + organisationToCreate: { + deep: true, + handler(newValue) { + if (newValue) { + this.selectedOrganisation = newValue; + } + } + }, + organisationThumbnail(newValue) { + if (newValue) { + this.$emit('select', { + selected: true, + orga: this.selectedOrganisation, + thumbnail: newValue, + spheres: this.organisationSpheres + }); + } + }, + organisationSpheres(newValue) { + if (newValue) { + this.$emit('select', { + selected: true, + orga: this.selectedOrganisation, + thumbnail: this.organisationThumbnail, + spheres: newValue + }); + } + }, + selectedOrganisation(newValue) { + if (newValue) { + this.$emit('select', { + selected: true, + orga: newValue, + thumbnail: this.organisationThumbnail, + spheres: this.organisationSpheres + }); + } + }, + }, + + created() { + this.GET_ORGANISATIONS_LIST(); + }, + + methods: { + ...mapActions('organisations', [ + 'GET_ORGANISATIONS_LIST', + 'GET_ORGANISATIONS_ROLES', + 'SEARCH_ORGANISATIONS_LIST' + ]), + + search(text) { + this.text = text; + }, + + updateOrganisationToCreate(e) { + this.organisationToCreate.codename = e.form.name ? e.form.name.replace(/[^A-Z0-9]/ig, "_") : null; + this.organisationToCreate.acronym = e.form.sigle; + this.organisationToCreate.display_name = e.form.name; + this.organisationToCreate.description = e.form.description; + this.organisationToCreate.organisation_type = e.form.type; + this.organisationToCreate.registration_number = e.form.siret; + this.organisationToCreate.phone_number = e.form.tel; + this.organisationToCreate.website_url = e.form.web; + this.organisationToCreate.postal_address = e.form.postalAddress; + + this.organisationSpheres = e.spheres; + this.organisationThumbnail = e.thumbnail; + }, + }, +}; +</script> + +<style lang="less" scoped> +label { + font-size: 1rem; + font-weight: normal; +} +</style> diff --git a/src/components/SearchUsergroups.vue b/src/components/SearchUsergroups.vue new file mode 100644 index 0000000..58a4164 --- /dev/null +++ b/src/components/SearchUsergroups.vue @@ -0,0 +1,117 @@ +<template> + <div> + <Multiselect + v-model="selection" + style="margin-top: 0.5em;" + class="search-usergroups" + :options="results" + :optionsLimit="10" + :allow-empty="false" + track-by="id" + label="display_name" + :resetAfter="true" + selectLabel="" + selectedLabel="" + deselectLabel="" + :searchable="true" + :placeholder="placeholder" + :showNoResults="true" + :loading="loading" + :clearOnSelect="false" + :preserveSearch="true" + @search-change="search" + @select="select" + > + <template slot="clear"> + <div + v-if="selection" + class="multiselect__clear" + @click.prevent.stop="selection = null" + > + <b-icon-x font-scale="2"/> + </div> + </template> + <span slot="noResult"> + Aucun résultat. + </span> + <span slot="noOptions"> + Saisissez les premiers caractères ... + </span> + </Multiselect> + </div> +</template> + +<script> +import Multiselect from 'vue-multiselect'; + +import { mapState, mapActions } from 'vuex'; + +export default { + name: 'SearchUsergroups', + + components: { + Multiselect + }, + + props: { + placeholder: { + type: String, + default: 'Ajouter cette organisation à des groupes d\'organisations' + } + }, + + data() { + return { + selection: null, + loading: false, + text: null, + results: [] + }; + }, + + computed: { + ...mapState('usergroups', [ + 'spheres' + ]) + }, + + watch: { + text: function(newValue) { + this.loading = true; + this.SEARCH_SPHERES(newValue) + .then(() => { + if (newValue) { + this.results = this.spheres; + } else { + // this.results.splice(0); + this.results = this.spheres; + } + this.loading = false; + }); + } + }, + + created() { + this.GET_SPHERES_LIST() + .then(() => { + this.results = this.spheres; + }) + }, + + methods: { + ...mapActions('usergroups', [ + 'GET_SPHERES_LIST', + 'SEARCH_SPHERES' + ]), + + search(text) { + this.text = text; + }, + + select(e) { + this.$emit('select', e); + this.selection = null; + } + } +} +</script> diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..2a6a249 --- /dev/null +++ b/src/main.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; + +import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'; +import 'bootstrap-vue/dist/bootstrap-vue-icons.min.css'; +import Multiselect from 'vue-multiselect'; + +// Custom css +import '@/app.scss'; +import '@/app.less'; + +Vue.use(BootstrapVue); +Vue.use(BootstrapVueIcons); +Vue.component('Multiselect', Multiselect); + +import App from '@/App.vue'; +import router from '@/router'; +import store from '@/store'; + +Vue.config.productionTip = false; + +new Vue({ + router, + store, + render: (h) => h(App), +}).$mount('#app') diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..9e6e3ec --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,79 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; + +Vue.use(VueRouter); + +const routes = [ + { + path: '/', + redirect: { name: 'SignIn' }, + }, + { + path: '/signin', + name: 'SignIn', + component: () => import('@/views/SignIn.vue'), + }, + { + path: '/signup', + name: 'SignUp', + component: () => import('@/views/SignUp.vue'), + }, + { + path: '/terms-of-use', + name: 'TermsOfUse', + component: () => import('@/views/TermsOfUse.vue'), + }, + { + path: '/signout', + name: 'SignOut', + component: () => import('@/views/SignOut.vue'), + }, + { + path: '/signout-failed', + name: 'SignOutFailed', + component: () => import('@/views/SignOutFailed.vue'), + }, + { + path: '/signupsuccess', + name: 'SignUpSuccess', + component: () => import('@/views/SignUpSuccess.vue'), + }, + { + path: '/validateregistration', + name: 'ValidationRegistration', + component: () => import('@/views/ValidationRegistration.vue'), + }, + { + path: '/validate-email', + name: 'ValidationEmail', + component: () => import('@/views/ValidationEmail.vue'), + }, + { + path: '/forgottenpwd', + name: 'ForgottenPassword', + component: () => import('@/views/ForgottenPassword.vue'), + }, + { + path: '/reinitpwd', + name: 'ReinitPassword', + component: () => import('@/views/ReinitPassword.vue'), + }, + { + path: '/profile', + name: 'UserProfile', + component: () => import('@/views/UserProfile.vue'), + }, + { + path: '/*', + name: 'NotFound', + component: () => import('@/views/NotFound.vue'), + }, +]; + +const router = new VueRouter({ + routes, + mode: 'history', + base: process.env.VUE_APP_BASE_PATH || '', +}); + +export default router; diff --git a/src/services/error-service.js b/src/services/error-service.js new file mode 100644 index 0000000..4a1d29e --- /dev/null +++ b/src/services/error-service.js @@ -0,0 +1,55 @@ +import Swal from "sweetalert2"; +import "sweetalert2/dist/sweetalert2.min.css"; + +import router from '@/router'; + +export class ErrorService { + + static onError(error) { + const response = error; + if (response && response.status === 403) { + router.push({ name: '403Page'}); + } + if (response && response.status >= 400 && response.status < 405) { + + const errorObj = response.data.detail ? response.data.detail : response.data; + const messages = []; + function recurse(obj) { + for (const [key, value] of Object.entries(obj)) { + if (typeof key === 'string' && typeof value === 'string') { + messages.push(`<b>${key}:</b> ${value}`); + } else if (typeof value === 'string') { + messages.push(`${value}`); + } else { + recurse(value); + } + } + } + recurse(errorObj); + + Swal.fire({ + position: 'top-end', + icon: 'error', + title: `Erreur ${response.status}`, + html: messages.join('\n'), + showConfirmButton: false, + timer: 3000 + }); + } + return false; + } + + static onSuccess(response, message) { + if (response) { + Swal.fire({ + position: 'top-end', + heightAuto: false, + icon: 'success', + title: `${message}`, + showConfirmButton: false, + timer: 3000 + }); + } + return false; + } +} diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..e844d05 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import modules from './modules'; + +Vue.use(Vuex); + +const state = { + cancellableSearchRequest: null +}; + +export const SET_CANCELLABLE_SEARCH_REQUEST = 'SET_CANCELLABLE_SEARCH_REQUEST'; + +const mutations = { + [SET_CANCELLABLE_SEARCH_REQUEST]: (state, payload) => { + state.cancellableSearchRequest = payload; + } +}; + +export default new Vuex.Store({ + modules, + state, + mutations +}); diff --git a/src/store/modules/forgotten-pwd.store.js b/src/store/modules/forgotten-pwd.store.js new file mode 100644 index 0000000..bb39585 --- /dev/null +++ b/src/store/modules/forgotten-pwd.store.js @@ -0,0 +1,105 @@ +import client from '@/api/loginAPI.js'; +import { ErrorService } from '@/services/error-service.js'; +import Swal from "sweetalert2"; +import "sweetalert2/dist/sweetalert2.min.css"; +import router from '@/router'; + +const state = { + error: null, + success: null +}; + +const getters = { +}; + +export const REQUEST_FORGOTTEN_PASSWORD = 'REQUEST_FORGOTTEN_PASSWORD'; +export const CONFIRM_NEW_PASSWORD = 'CONFIRM_NEW_PASSWORD'; + +const actions = { + [REQUEST_FORGOTTEN_PASSWORD]: async ({ commit }, data) => { + const response = await client.forgottenPasswordRequest(data); + if (response.status === 200) { + Swal.fire({ + position: 'center', + heightAuto: false, + icon: 'success', + text: ` + Un e-mail vous a été envoyé sur votre messagerie pour réinitialiser votre mot de passe. + `, + showConfirmButton: true, + confirmButtonText: 'OK', + confirmButtonColor: '#187CC6' + }).then((result) => { + if (result.isConfirmed) { + router.push({ name: 'SignIn' }); + } + }); + } + if (response.status === 404) { + commit('SET_ERROR', { + response: response, + message: 'Aucun compte trouvé pour cette adresse e-mail.' + }); + } + }, + + [CONFIRM_NEW_PASSWORD]: async ({ commit }, data) => { + const response = await client.forgottenPasswordConfirm(data); + if (response.status === 200) { + Swal.fire({ + position: 'center', + heightAuto: false, + icon: 'success', + text: `Votre mot de passe a bien été réinitialisé. + Vous allez être redirigé vers la page de connexion. + `, + showConfirmButton: true, + confirmButtonText: 'OK', + confirmButtonColor: '#187CC6' + }).then((result) => { + if (result.isConfirmed) { + router.push({ name: 'SignIn' }); + } + }); + } else if (response.status === 400) { + Swal.fire({ + position: 'center', + heightAuto: false, + icon: 'error', + text: `Votre token a expiré. Vous devez en demander un nouveau + pour changer votre mot de passe. + `, + showConfirmButton: true, + confirmButtonText: 'OK', + confirmButtonColor: '#187CC6' + }).then((result) => { + if (result.isConfirmed) { + router.push({ name: 'SignIn' }); + } + }); + } + } +}; + +export const SET_ERROR = 'SET_ERROR'; +export const SET_SUCCESS = 'SET_SUCCESS'; + +const mutations = { + [SET_ERROR]: (state, payload) => { + state.success = null; + ErrorService.onError(payload.response); + state.error = payload.message; + }, + [SET_SUCCESS]: (state, payload) => { + state.error = null; + state.success = payload.message; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/index.js b/src/store/modules/index.js new file mode 100644 index 0000000..2bbb9be --- /dev/null +++ b/src/store/modules/index.js @@ -0,0 +1,16 @@ +/** + * Automatically imports all the modules and exports as a single module object +*/ + +const requireModule = require.context('.', false, /\.store\.js$/); +const modules = {}; + +requireModule.keys().forEach((filename) => { + // create the module name from fileName + // remove the store.js extension + const moduleName = filename.replace(/(\.\/|\.store\.js)/g, ''); + + modules[moduleName] = requireModule(filename).default || requireModule(filename); +}); + +export default modules; diff --git a/src/store/modules/organisations.store.js b/src/store/modules/organisations.store.js new file mode 100644 index 0000000..1d165e1 --- /dev/null +++ b/src/store/modules/organisations.store.js @@ -0,0 +1,97 @@ +import client from '@/api/loginAPI.js'; +import organisationAPI from '@/api/organisationsAPI.js'; +import axios from 'axios'; + +const state = { + organisationsList: [], + organisationsTypes: [], + organisationsRoles: [], +}; + +const getters = { }; + +export const GET_ORGANISATIONS_LIST = 'GET_ORGANISATIONS_LIST'; +export const GET_ORGANISATIONS_TYPES = 'GET_ORGANISATIONS_TYPES'; +export const GET_ORGANISATIONS_ROLES = 'GET_ORGANISATIONS_ROLES'; +export const SEARCH_ORGANISATIONS_LIST = 'SEARCH_ORGANISATIONS_LIST'; + +const actions = { + [GET_ORGANISATIONS_LIST]: async ({ commit }) => { + const organisations = await client.getOrganisationsList(); + commit('SET_ORGANISATIONS_LIST', organisations); + }, + [SEARCH_ORGANISATIONS_LIST]: async ({ rootState, commit, dispatch }, text) => { + + if (text) { + if (rootState.cancellableSearchRequest) { + rootState.cancellableSearchRequest.cancel(); + commit('SET_CANCELLABLE_SEARCH_REQUEST', null, { root: true }); + } + + const cancelToken = axios.CancelToken.source(); + commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); + const url = `${process.env.VUE_APP_LOGIN_API}/organisations/?page=1&search=${text}`; + + try { + const response = await axios.get( + url, + { + cancelToken: cancelToken.token, + ...process.env.NODE_ENV === 'development' && { + auth: { + username: process.env.VUE_APP_LOGIN_API_USERNAME, + password: process.env.VUE_APP_LOGIN_API_PASSWORD + } + } + } + ); + if (response.status === 200) { + commit('SET_CANCELLABLE_SEARCH_REQUEST', null, { root: true }); + const organisations = response.data; + if (organisations) { + commit('SET_ORGANISATIONS_LIST', organisations); + } + } + } catch(err) { + commit('SET_CANCELLABLE_SEARCH_REQUEST', null, { root: true }); + } + } else { + dispatch('GET_ORGANISATIONS_LIST', { + direction: null, + field: null + }); + } + }, + [GET_ORGANISATIONS_TYPES]: async ({ commit }) => { + const types = await organisationAPI.getOrganisationsTypes(); + commit('SET_ORGANISATIONS_TYPES', types); + }, + [GET_ORGANISATIONS_ROLES]: async ({ commit }) => { + const roles = await organisationAPI.getOrganisationsRoles(); + commit('SET_ORGANISATIONS_ROLES', roles); + } +}; + +export const SET_ORGANISATIONS_LIST = 'SET_ORGANISATIONS_LIST'; +export const SET_ORGANISATIONS_TYPES = 'SET_ORGANISATIONS_TYPES'; +export const SET_ORGANISATIONS_ROLES = 'SET_ORGANISATIONS_ROLES'; + +const mutations = { + [SET_ORGANISATIONS_LIST]: (state, payload) => { + state.organisationsList = payload ? payload.results ? payload.results : payload : []; + }, + [SET_ORGANISATIONS_TYPES]: (state, payload) => { + state.organisationsTypes = payload; + }, + [SET_ORGANISATIONS_ROLES]: (state, payload) => { + state.organisationsRoles = payload; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/sign-in.store.js b/src/store/modules/sign-in.store.js new file mode 100644 index 0000000..fb3bd4c --- /dev/null +++ b/src/store/modules/sign-in.store.js @@ -0,0 +1,78 @@ +import client from '@/api/loginAPI.js'; + +const state = { + username: null, + password: null, + logged: false, + error: null, + next: null, +}; + +const getters = { +}; + +export const POST_SIGNIN = 'POST_SIGNIN'; + +const actions = { + [POST_SIGNIN]: async ({ commit }) => { + const data = { + username: state.username, + password: state.password, + }; + await client.signIn(data) + .then( + () => { + commit('SET_ERROR', null); + commit('SET_LOGGED', true); + }, + ) + .catch( + (error) => { + if (error.response.status === 403) { + commit( + 'SET_ERROR', + error.response.data.detail + || 'Nom d\'utilisateur et/ou mot de passe incorrect(s)', + ); + commit('SET_LOGGED', false); + } + }, + ); + }, +}; + +export const SET_LOGGED = 'SET_LOGGED'; +export const SET_USERNAME = 'SET_USERNAME'; +export const SET_PASSWORD = 'SET_PASSWORD'; +export const SET_ERROR = 'SET_ERROR'; +export const SET_NEXT = 'SET_NEXT'; + +const mutations = { + [SET_LOGGED]: (state, value) => { + if (value === true) { + state.logged = true; + } else { + state.logged = false; + } + }, + [SET_USERNAME]: (state, value) => { + state.username = value; + }, + [SET_PASSWORD]: (state, value) => { + state.password = value; + }, + [SET_ERROR]: (state, value) => { + state.error = value; + }, + [SET_NEXT]: (state, value) => { + state.next = value; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/sign-out.store.js b/src/store/modules/sign-out.store.js new file mode 100644 index 0000000..f9377c8 --- /dev/null +++ b/src/store/modules/sign-out.store.js @@ -0,0 +1,57 @@ +import client from '@/api/loginAPI.js'; + +const state = { + logged: null, + error: null +}; + +const getters = { +}; + +export const GET_SIGNOUT = 'GET_SIGNOUT'; + +const actions = { + [GET_SIGNOUT]: async ({ commit }) => { + await client.signOut() + .then( + () => { + commit('SET_ERROR', undefined); + commit('SET_LOGGED', false); + }, + ) + .catch( + (error) => { + commit( + 'SET_ERROR', + error.response.data + || 'Une erreur est survenue', + ); + commit('SET_LOGGED', true); + }, + ); + }, +}; + +export const SET_ERROR = 'SET_ERROR'; +export const SET_LOGGED = 'SET_LOGGED'; + +const mutations = { + [SET_LOGGED]: (state, value) => { + if (value === true) { + state.logged = true; + } else { + state.logged = false; + } + }, + [SET_ERROR]: (state, payload) => { + state.error = payload; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/sign-up.store.js b/src/store/modules/sign-up.store.js new file mode 100644 index 0000000..98e999b --- /dev/null +++ b/src/store/modules/sign-up.store.js @@ -0,0 +1,115 @@ +import client from '@/api/loginAPI.js'; +import organisationAPI from '@/api/organisationsAPI.js'; +import usergroupsAPI from '@/api/usergroupsAPI.js'; + +import { ErrorService } from '@/services/error-service.js'; + +const state = { + form: { + first_name: null, + last_name: null, + email: null, + phone_number: null, + comments: null, + username: null, + password1: null, + password2: null + }, + organisationThumbnail: null, + organisationSpheres: [], + signed: false, + error: null, +}; + +const getters = { + getForm: state => state.form, + getSigned: state => state.signed, + getError: state => state.error, +}; + +export const POST_SIGNUP = 'POST_SIGNUP'; + +const actions = { + [POST_SIGNUP]: async ({ state, commit }) => { + await client.signUp(state.form) + .then( + async (resp) => { + if (state.organisationThumbnail) { + await organisationAPI.setOrganisationThumbnail(resp.usergroup_roles[0].organisation.id, state.organisationThumbnail) + .then(() => { + commit('SET_ERROR', undefined); + }) + .catch((error) => { + commit( + 'SET_ERROR', + error.response + || 'Une erreur est survenue', + ); + commit('SET_SIGNED', false); + }); + } + if (state.organisationSpheres.length > 0) { + const data = { + ...resp.usergroup_roles[0].organisation, + parents: state.organisationSpheres.map((el) => { return el.id; }) + }; + await usergroupsAPI.updateUsergroup(resp.usergroup_roles[0].organisation.id, data) + .then(() => { + commit('SET_ERROR', undefined); + }) + .catch((error) => { + commit( + 'SET_ERROR', + error.response + || 'Une erreur est survenue', + ); + commit('SET_SIGNED', false); + }); + } + commit('SET_ERROR', undefined); + commit('SET_SIGNED', true); + }, + ) + .catch( + (error) => { + commit( + 'SET_ERROR', + error.response + || 'Une erreur est survenue', + ); + commit('SET_SIGNED', false); + }, + ); + }, +}; + +export const SET_FORM = 'SET_FORM'; +export const SET_SIGNED = 'SET_SIGNED'; +export const SET_ERROR = 'SET_ERROR'; + +const mutations = { + [SET_FORM]: (state, data) => { + state.form = data.form; + state.organisationThumbnail = data.thumbnail; + state.organisationSpheres = data.spheres; + }, + [SET_SIGNED]: (state, value) => { + if (value === true) { + state.signed = true; + } else { + state.signed = false; + } + }, + [SET_ERROR]: (state, value) => { + ErrorService.onError(value); + state.error = value && value.data ? value.data : value; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/terms-of-use.store.js b/src/store/modules/terms-of-use.store.js new file mode 100644 index 0000000..076d29f --- /dev/null +++ b/src/store/modules/terms-of-use.store.js @@ -0,0 +1,77 @@ +import client from '@/api/loginAPI.js'; +import { ErrorService } from '@/services/error-service.js'; +import router from '@/router'; + +const state = { + terms: null, + error: null, + success: null +}; + +const getters = { +}; + +export const GET_TERMS_OF_USE = 'GET_TERMS_OF_USE'; +export const AGREE_TO_TERMS_OF_USE = 'AGREE_TO_TERMS_OF_USE'; + +const actions = { + [GET_TERMS_OF_USE]: async ({ commit }) => { + const response = await client.getTermsOfUse(); + if (response.status === 200) { + commit('SET_TERMS', response.data); + commit('SET_SUCCESS', { + response: response, + message: '' + }); + } + if (response.status === 404) { + commit('SET_ERROR', { + response: response, + message: 'Une erreur est survenue.' + }); + } + }, + [AGREE_TO_TERMS_OF_USE]: async ({ commit }, data) => { + const response = await client.postTermsOfUseAgreement(data); + if (response.status === 200) { + commit('SET_SUCCESS', { + response: response, + message: '' + }); + window.location.href = router.currentRoute.query.next; + } + if (response.status === 404) { + commit('SET_ERROR', { + response: response, + message: 'Une erreur est survenue.' + }); + } + } +}; + +export const SET_TERMS = 'SET_TERMS'; +export const SET_ERROR = 'SET_ERROR'; +export const SET_SUCCESS = 'SET_SUCCESS'; + +const mutations = { + [SET_TERMS]: (state, payload) => { + state.terms = payload; + }, + [SET_ERROR]: (state, payload) => { + state.success = null; + ErrorService.onError(payload.response); + state.error = payload.message; + }, + [SET_SUCCESS]: (state, payload) => { + state.error = null; + state.success = payload.message; + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/user.store.js b/src/store/modules/user.store.js new file mode 100644 index 0000000..e5b7ea6 --- /dev/null +++ b/src/store/modules/user.store.js @@ -0,0 +1,91 @@ +import loginAPI from '@/api/loginAPI.js'; + +import { ErrorService } from '@/services/error-service.js'; + +// MUTATIONS +export const SET_ERROR = 'SET_ERROR'; +export const SET_SUCCESS = 'SET_SUCCESS'; +export const SET_USER_DETAIL = 'SET_USER_DETAIL'; + +// ACTIONS +export const GET_USER_DETAIL = 'GET_USER_DETAIL'; +export const UPDATE_USER_DETAIL = 'UPDATE_USER_DETAIL'; + +/**************** STATE *******************/ +const state = { + userData: null, + userError: null, + error: null, + success: null +}; + +/**************** GETTERS *****************/ +const getters = { + +}; + +/*************** MUTATIONS ****************/ +const mutations = { + [SET_USER_DETAIL]: (state, payload) => { + state.userData = payload; + }, + + [SET_ERROR]: (state, error) => { + if (error) { + ErrorService.onError(error); + state.userError = error.response.data.detail; + } else { + state.userError = error; + } + }, + + [SET_SUCCESS]: (state, payload) => { + state.error = null; + state.success = payload.message; + }, +}; +/**************** ACTIONS *****************/ +const actions = { + + + [GET_USER_DETAIL]: async ({ commit }) => { + await loginAPI.getUserDetail() + .then((resp) => { + if (resp) { + commit('SET_ERROR', null); + commit('SET_USER_DETAIL', resp); + } + }) + .catch((error) => { + commit('SET_ERROR', error); + }); + }, + + [UPDATE_USER_DETAIL]: async ({ commit }, data) => { + await loginAPI.updateUserDetail(data) + .then((resp) => { + if (resp) { + commit('SET_ERROR', null); + commit('SET_SUCCESS', { + response: resp, + message: 'Votre mot de passe a bien été modifié.' + }); + } + }) + .catch((error) => { + commit('SET_SUCCESS', { + response: null, + message: null + }); + commit('SET_ERROR', error); + }); + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; \ No newline at end of file diff --git a/src/store/modules/usergroups.store.js b/src/store/modules/usergroups.store.js new file mode 100644 index 0000000..f5ae420 --- /dev/null +++ b/src/store/modules/usergroups.store.js @@ -0,0 +1,113 @@ +import { ErrorService } from '@/services/error-service.js'; +import usergroupsAPI from '@/api/usergroupsAPI'; +import axios from 'axios'; + +const DEV_AUTH = process.env.NODE_ENV === 'development' ? true : false; +const AUTH = { + username: process.env.VUE_APP_LOGIN_API_USERNAME, + password: process.env.VUE_APP_LOGIN_API_PASSWORD +}; +const path = require('path'); +const DOMAIN = process.env.VUE_APP_DOMAIN; +const USERGROUP_API_PATH = process.env.VUE_APP_USERGROUP_API_PATH; + +// MUTATIONS +export const SET_USERGROUPS_SPHERES_LIST = 'SET_USERGROUPS_SPHERES_LIST'; +export const SET_ERROR = 'SET_ERROR'; +export const SET_IS_SPHERES_SEARCHED = 'SET_IS_SPHERES_SEARCHED'; + +// ACTIONS +export const GET_SPHERES_LIST = 'GET_SPHERES_LIST'; +export const SEARCH_SPHERES = 'SEARCH_SPHERES'; + +/**************** STATE *******************/ +const state = { + spheresCount: 0, + spheres: [], + usergroupsError: null, + isSpheresSearched: false +}; + +/**************** GETTERS *****************/ +const getters = { +}; + +/*************** MUTATIONS ****************/ +const mutations = { + + [SET_USERGROUPS_SPHERES_LIST]: (state, payload) => { + if (payload) { + state.spheresCount = payload.count; + state.spheres = payload.results; + } + }, + + [SET_ERROR]: (state, error) => { + if (error) { + ErrorService.onError(error); + state.usergroupsError = error; + } else { + state.usergroupsError = error; + } + }, + + [SET_IS_SPHERES_SEARCHED]: (state, payload) => { + state.isSpheresSearched = payload; + } + +}; +/**************** ACTIONS *****************/ +const actions = { + + [GET_SPHERES_LIST]: async ({ commit }) => { + try { + const spheres = await usergroupsAPI.getFilteredUsergroupsList() + if (spheres) { + commit('SET_ERROR', null); + commit('SET_USERGROUPS_SPHERES_LIST', spheres); + } + } catch (error) { + commit('SET_ERROR', error); + } + }, + + [SEARCH_SPHERES]: async ({ rootState, commit }, text) => { + if (rootState.cancellableSearchRequest) { + rootState.cancellableSearchRequest.cancel(); + commit('SET_CANCELLABLE_SEARCH_REQUEST', null, { root: true }); + } + + const cancelToken = axios.CancelToken.source(); + commit('SET_CANCELLABLE_SEARCH_REQUEST', cancelToken, { root: true }); + const url = new URL(path.join(USERGROUP_API_PATH, `user-groups/?page=1&search=${text}&usergroup_types=group-of-organisation`), DOMAIN); + + try { + const response = await axios.get( + url, + { + cancelToken: cancelToken.token, + ...DEV_AUTH && { auth: AUTH } + } + ); + if (response.status === 200) { + commit('SET_CANCELLABLE_SEARCH_REQUEST', null, { root: true }); + const usergroups = response.data; + if (usergroups) { + commit('SET_ERROR', null); + commit('SET_USERGROUPS_SPHERES_LIST', usergroups); + commit('SET_IS_SPHERES_SEARCHED', true); + } + } + } catch(err) { + commit('SET_CANCELLABLE_SEARCH_REQUEST', null, { root: true }); + } + } +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/store/modules/validation-email.store.js b/src/store/modules/validation-email.store.js new file mode 100644 index 0000000..e17831b --- /dev/null +++ b/src/store/modules/validation-email.store.js @@ -0,0 +1,37 @@ +import client from '@/api/loginAPI.js'; + +const state = { + status: undefined, +}; + +export const POST_TOKEN = 'POST_TOKEN'; + +const actions = { + [POST_TOKEN]: async ({ commit }, token) => { + const data = { + token: token + }; + await client.newEmailConfirm(data).then(response => { + commit('SET_STATUS', response); + }); + }, +}; + +export const SET_STATUS = 'SET_STATUS'; + +const mutations = { + [SET_STATUS]: (state, response) => { + if (response === true) { + state.status = true; + } else { + state.status = false; + } + }, +}; + +export default { + namespaced: true, + state, + actions, + mutations, +}; diff --git a/src/store/modules/validation-registration.store.js b/src/store/modules/validation-registration.store.js new file mode 100644 index 0000000..ff4b6d9 --- /dev/null +++ b/src/store/modules/validation-registration.store.js @@ -0,0 +1,45 @@ +import client from '@/api/loginAPI.js'; + +const state = { + token: null, + status: undefined, +}; + +const getters = { + getStatus: state => state.status, +}; + +export const POST_TOKEN = 'POST_TOKEN'; + +const actions = { + [POST_TOKEN]: async ({ state, commit }) => { + const data = { token: state.token }; + await client.validationRegistration(data).then(response => { + commit('SET_STATUS', response); + }); + }, +}; + +export const SET_STATUS = 'SET_STATUS'; +export const SET_TOKEN = 'SET_TOKEN'; + +const mutations = { + [SET_TOKEN]: (state, value) => { + state.token = value; + }, + [SET_STATUS]: (state, response) => { + if (response === true) { + state.status = true; + } else { + state.status = false; + } + }, +}; + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +}; diff --git a/src/views/ForgottenPassword.vue b/src/views/ForgottenPassword.vue new file mode 100644 index 0000000..069e878 --- /dev/null +++ b/src/views/ForgottenPassword.vue @@ -0,0 +1,146 @@ +<template> + <div> + <div class="container"> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="form"> + <h5 class="title">Réinitialisez votre mot de passe</h5> + <form> + <div class="form-row"> + <label> + Veuillez indiquer l'adresse e-mail de votre compte + </label> + <input + v-model="email" + class="form-control" + type="text" + placeholder="Adresse e-mail" + > + </div> + <button + type="submit" + class="btn btn-primary" + @click.prevent="reinitPassword" + > + Envoyer un e-mail de réinitialisation + </button> + <button + class="btn btn-outline-secondary" + @click="$router.push({ name: 'SignIn' })"> + Annuler + </button> + </form> + </div> + <div class="messages"> + <div v-if="error"> + <p class="form-errors">{{ error }}</p> + </div> + </div> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> +import { mapState, mapActions } from 'vuex'; + +export default { + name: 'ForgottenPassword', + data() { + return { + email: null, + }; + }, + + computed: { + ...mapState('forgotten-pwd', [ + 'error', + 'success' + ]) + }, + + methods: { + ...mapActions('forgotten-pwd', [ + 'REQUEST_FORGOTTEN_PASSWORD' + ]), + reinitPassword() { + this.REQUEST_FORGOTTEN_PASSWORD({ + email: this.email + }) + } + } +}; +</script> + +<style lang="less" scoped> + +.container { + margin: auto; + width: 480px; + height: fit-content; + + .header { + margin: 0 0 5rem 0; + img { + width: 440px; + } + } + + .form { + + .form-row { + margin: 0 0 40px 0; + } + + button.btn { + float: right; + position: relative; + margin-left: 7px; + margin-right: 0; + } + + button.btn-primary { + border: 2px solid #9BD0FF; + border-radius: 8px; + } + button.btn-outline-secondary { + background-color: #F7F8FA; + border: 2px solid #A9B2B9; + border-radius: 8px; + color: #2F3234; + } + button.btn-outline-secondary:hover { + color: white; + background-color: #4b4b4b; + } + + } + .messages { + display: inline-block; + position: relative; + top: 1.5em; + } +} + +.form-errors { + color: #EB0600 !important; +} + +.form-success { + color: #11ac45 !important; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} + +</style> diff --git a/src/views/NotFound.vue b/src/views/NotFound.vue new file mode 100644 index 0000000..1388ee9 --- /dev/null +++ b/src/views/NotFound.vue @@ -0,0 +1,11 @@ +<template> + <div> + + </div> +</template> + +<script> +export default { + name: 'NotFound', +}; +</script> diff --git a/src/views/ReinitPassword.vue b/src/views/ReinitPassword.vue new file mode 100644 index 0000000..0505791 --- /dev/null +++ b/src/views/ReinitPassword.vue @@ -0,0 +1,292 @@ +<template> + <div> + <div class="container"> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <b-overlay + id="overlay-background" + :show="loading" + :variant="'white'" + :opacity="0.7" + :blur="'2px'" + rounded="sm" + no-wrap + > + </b-overlay> + <div class="form"> + <h5 class="title">Choississez un nouveau mot de passe</h5> + <ValidationObserver v-slot="{ handleSubmit }"> + <form> + <div class="form-row"> + <ValidationProvider + ref="password" + :rules="{ required: true, regex: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/ }" + v-slot="{ classes, errors }" + vid="confirmation" + > + <div class="control" :class="classes"> + <label class="required"> + Nouveau de mot de passe + </label> + <div class="input-group flex-nowrap"> + <input + v-model="password1" + class="form-control" + :type="showPassword ? 'text' : 'password'" + placeholder="Mot de passe" + > + <span + v-if="!showPassword" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showPassword = !showPassword" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showPassword = !showPassword" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + <div class="form-row"> + <ValidationProvider ref="password" rules="required|confirmed:confirmation" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <label class="required"> + Confirmez le mot de passe + </label> + <div class="input-group flex-nowrap"> + <input + v-model="password2" + class="form-control" + :type="showPassword ? 'text' : 'password'" + placeholder="Mot de passe" + > + <span + v-if="!showPassword" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showPassword = !showPassword" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showPassword = !showPassword" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + <div class="infos"> + <ul> + <li>Votre mot de passe ne peut pas trop ressembler à vos autres informations personnelles.</li> + <li>Votre mot de passe doit contenir au minimum 8 caractères.</li> + <li>Votre mot de passe ne peut pas être un mot de passe couramment utilisé.</li> + <li>Votre mot de passe ne peut pas être entièrement numérique.</li> + </ul> + </div> + <button + type="button" + class="btn btn-primary" + @click.prevent="handleSubmit(sendNewPassword)" + > + Sauvegarder + </button> + <button + type="button" + class="btn btn-outline-secondary" + @click="$router.push({ name: 'SignIn' })" + > + Annuler + </button> + </form> + </ValidationObserver> + </div> + <div class="messages"> + <div v-if="error"> + <p class="form-errors">{{ error }}</p> + </div> + <div v-if="success"> + <p class="form-success">{{ success }}</p> + </div> + </div> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> +import { mapState, mapActions } from 'vuex'; + +import { + ValidationObserver, + ValidationProvider, + extend, + configure, +} from 'vee-validate'; + +import { required, confirmed, regex } from 'vee-validate/dist/rules'; + +extend('required', { + ...required, + message: 'Ce champ est requis', +}); + +extend('confirmed', { + ...confirmed, + message: 'Les mots de passe doivent être identiques', +}); + +configure({ + classes: { + valid: 'is-valid', + invalid: 'is-invalid', + }, +}); + +extend('regex', { + ...regex, + message: 'Votre mot de passe doit comporter au moins 8 caractères, dont au moins une majuscule, une minuscule et 1 chiffre.', +}); + +export default { + name: 'ReinitPassword', + + components: { + ValidationObserver, + ValidationProvider + }, + + data() { + return { + loading: false, + password1: null, + password2: null, + showPassword: false + } + }, + + computed: { + ...mapState('forgotten-pwd', [ + 'error', + 'success' + ]) + }, + + methods: { + ...mapActions('forgotten-pwd', [ + 'CONFIRM_NEW_PASSWORD' + ]), + + sendNewPassword() { + this.loading = true; + this.CONFIRM_NEW_PASSWORD({ + token: this.$route.query.token, + password: this.password1 + }).then(() => { + this.loading = false; + }) + } + } +}; +</script> + +<style lang="less" scoped> + +.container { + margin: auto; + width: 480px; + height: fit-content; + + .header { + margin: 0 0 5rem 0; + img { + width: 440px; + } + } + + .form { + + .form-row { + margin: 0 0 40px 0; + display: block; + } + + button.btn { + float: right; + position: relative; + margin-left: 7px; + margin-right: 0; + margin-top: 1rem; + } + + button.btn-primary { + border: 2px solid #9BD0FF; + border-radius: 8px; + } + button.btn-outline-secondary { + background-color: #F7F8FA; + border: 2px solid #A9B2B9; + border-radius: 8px; + color: #2F3234; + } + button.btn-outline-secondary:hover { + color: white; + background-color: #4b4b4b; + } + + } + .infos { + font-size: 0.75em; + text-align: justify; + ul { + padding-left: 1rem; + } + } + .messages { + display: inline-block; + position: relative; + top: 1.5em; + } +} + +.form-errors { + color: #EB0600 !important; + max-width: 40em; +} + +.form-success { + color: #11ac45 !important; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} + +</style> diff --git a/src/views/SignIn.vue b/src/views/SignIn.vue new file mode 100644 index 0000000..6f68b47 --- /dev/null +++ b/src/views/SignIn.vue @@ -0,0 +1,251 @@ +<template> + <div> + <div class="sign-in-container"> + <div class="sign-in-header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="sign-in-form"> + <h5 class="title">Saississez vos identifiants PIGMA</h5> + <form> + <div class="form-row"> + <input + v-model="form.username" + class="form-control" + type="text" + placeholder="Nom d'utilisateur" + v-on:keydown.enter.prevent="submit" + > + </div> + <div class="form-row"> + <div class="input-group flex-nowrap"> + <input + v-model="form.password" + class="form-control" + :type="showPassword ? 'text' : 'password'" + placeholder="Mot de passe" + v-on:keydown.enter.prevent="submit" + > + <span + v-if="!showPassword" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showPassword = !showPassword" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showPassword = !showPassword" + /> + </span> + </div> + </div> + <div class="form-row"> + <div v-if="error" class="form-row"> + <span class="form-errors">{{ error }}</span> + </div> + </div> + <div class="form-row"> + <b-button + :pressed="btnPressed" + :disabled="(!username || !password)" + @click.prevent="submit" + variant="primary" + block + > + Se connecter + </b-button> + </div> + <div class="form-row"> + <div class="btn-group-vertical"> + <router-link + to="/signup" + custom + > + Pas encore de compte ? + </router-link> + <router-link + to="/forgottenpwd" + custom + > + Mot de passe oublié ? + </router-link> + </div> + </div> + </form> + </div> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> + +import { + mapState, + mapActions, + mapMutations, +} from 'vuex'; + +const signInActions = [ + 'POST_SIGNIN' +]; +const signInMutations = [ + 'SET_LOGGED', + 'SET_NEXT', + 'SET_USERNAME', + 'SET_PASSWORD', +]; + +export default { + name: 'SignIn', + + data() { + return { + showPassword: false, + btnPressed: false, + form: { + username: null, + password: null + } + }; + }, + + computed: { + ...mapState('sign-in', [ + 'username', + 'password', + 'logged', + 'error', + 'next', + ]) + }, + + watch: { + form: { + deep: true, + handler(newValue) { + this.SET_USERNAME(newValue.username); + this.SET_PASSWORD(newValue.password); + } + }, + + logged() { + if (this.logged && this.next) { + window.location.href = this.next; + } + } + }, + + created() { + this.SET_NEXT(this.$route.query.next || process.env.VUE_APP_NEXT_DEFAULT); + }, + + methods: { + ...mapActions('sign-in', signInActions), + ...mapMutations('sign-in', signInMutations), + async submit() { + this.btnPressed = true; + await this.POST_SIGNIN(); + }, + }, +}; +</script> + +<style lang="less" scoped> +.sign-in-container { + margin: auto; + width: 480px; + height: fit-content; + + .sign-in-header { + margin: 0 1rem 1rem 1rem; + img { + width: 440px; + } + } + + .sign-in-form { + margin: 5rem 1rem; + + h5.title { + color: #373b3d; + } + + form { + margin: 2rem 0.25rem; + + .form-row { + margin-bottom: 1.25em; + + .input-group { + span { + cursor: pointer; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-left: none + } + } + } + + .row { + .col-7 { + display: flex; + flex-direction: column; + justify-content: flex-end; + + .checkbox { + label { + small { + cursor: pointer !important; + input { + cursor: pointer !important; + } + } + } + } + } + + .col-5 { + button { + float: right; + } + } + } + + .btn-group-vertical { + margin-top: 2rem; + + a { + text-decoration: none; + } + } + } + } +} + +.form-errors { + color: #EB0600 !important; +} + +.form-success { + color: #30C963 !important; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/src/views/SignOut.vue b/src/views/SignOut.vue new file mode 100644 index 0000000..47ccac3 --- /dev/null +++ b/src/views/SignOut.vue @@ -0,0 +1,95 @@ +<template> + <div> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="center"> + <b-container class="msg"> + <p> + Vous êtes maintenant déconnecté.<br> + </p> + <b-button type="button" variant="outline-primary" @click.prevent="$router.push({ name: 'SignIn' })"> + Ouvrir la page de connexion + </b-button> + </b-container> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> +import { mapState, mapActions } from 'vuex'; + +export default { + name: 'SignOut', + + data: () => { + return { }; + }, + + computed: { + ...mapState('sign-out', [ + 'logged', + 'error' + ]) + }, + + beforeRouteEnter(to, from, next) { + next((vm) => { + vm.GET_SIGNOUT().then(() => { + if (vm.logged) { + vm.$router.push({ name: 'SignOutFailed' }) + } + }) + }); + }, + + methods: { + ...mapActions('sign-out', [ + 'GET_SIGNOUT' + ]) + } +}; +</script> + +<style lang="less" scoped> +.header { + position: absolute; + top: 0; + left: 0; +} + +.header > img { + width: 440px; +} + +.center { + position: initial; +} + +.center .msg.container { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.center .msg.container, +.center .msg.container button { + font-size: large; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/src/views/SignOutFailed.vue b/src/views/SignOutFailed.vue new file mode 100644 index 0000000..febef67 --- /dev/null +++ b/src/views/SignOutFailed.vue @@ -0,0 +1,107 @@ +<template> + <div> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <b-overlay + id="overlay-background" + :show="loading" + :variant="'white'" + :opacity="0.7" + :blur="'2px'" + rounded="sm" + no-wrap + > + </b-overlay> + <div class="center"> + <b-container class="msg"> + <p> + Une erreur est survenue lors de la déconnexion.<br> + </p> + <b-button type="button" variant="outline-primary" @click.prevent="retrySignout"> + Réessayer + </b-button> + </b-container> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> +import { mapState, mapActions } from 'vuex'; + +export default { + name: 'SignOutFailed', + + data: () => { + return { + loading: false + }; + }, + + computed: { + ...mapState('sign-out', [ + 'logged', + 'error' + ]) + }, + + methods: { + ...mapActions('sign-out', [ + 'GET_SIGNOUT' + ]), + + retrySignout() { + this.loading = false; + this.GET_SIGNOUT().then(() => { + this.loading = false; + if (!this.logged) { + this.$router.push({ name: 'SignOut' }); + } + }); + } + } +}; +</script> + +<style lang="less" scoped> +.header { + position: absolute; + top: 0; + left: 0; +} + +.header > img { + width: 440px; +} + +.center { + position: initial; +} + +.center .msg.container { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.center .msg.container, +.center .msg.container button { + font-size: large; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/src/views/SignUp.vue b/src/views/SignUp.vue new file mode 100644 index 0000000..2900747 --- /dev/null +++ b/src/views/SignUp.vue @@ -0,0 +1,598 @@ +<template> + <div> + <div class="signup-container"> + <div class="signup-header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <b-overlay + id="overlay-background" + :show="loading" + :variant="'white'" + :opacity="0.7" + :blur="'2px'" + rounded="sm" + no-wrap + > + </b-overlay> + <div class="signup-form"> + <h4 class="title"> + Nouveau compte + <span class="sub-title">Sollicitez la création d'un compte</span> + </h4> + <hr class="divider"> + <h5>Un e-mail de confirmation sera envoyé à l'adresse indiquée</h5> + <ValidationObserver v-slot="{ handleSubmit }"> + <form> + <h5>Vos informations personnelles</h5> + <hr class="divider"> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label required">Prénom</label> + </div> + <div class="col"> + <ValidationProvider ref="first_name" rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="form.first_name" + type="text" + class="form-control" + placeholder="Prénom" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label required">Nom</label> + </div> + <div class="col"> + <ValidationProvider ref="last_name" rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="form.last_name" + type="text" + class="form-control" + placeholder="Nom" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label required">Adresse e-mail</label> + </div> + <div class="col"> + <ValidationProvider ref="email" rules="required|email" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="form.email" + type="mail" + class="form-control" + placeholder="Adresse e-mail" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label">Numéro de téléphone</label> + </div> + <div class="col"> + <ValidationProvider ref="phone_number" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="form.phone_number" + type="text" + class="form-control" + placeholder="" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label">Motif de l'inscription</label> + </div> + <div class="col"> + <textarea + v-model="form.comments" + class="form-control" + /> + </div> + </div> + <h5>Vos identifiants</h5> + <hr class="divider"> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label">Nom d'utilisateur</label> + </div> + <div class="col"> + <ValidationProvider ref="username" rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <div class="input-group flex-nowrap"> + <input + v-model="form.username" + type="text" + class="form-control" + disabled + > + <span class="input-group-text"> + <b-icon icon="person-fill" /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center" style="margin-bottom"> + <div class="col-3"> + </div> + <div class="col"> + <p class="infos"> + Le nom d'utilisateur est généré automatiquement à partir de la + première lettre de votre prénom et de votre nom. + Il n'est pas modifiable. + </p> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label + class="col-form-label required" + style="padding: 0;" + >Mot de passe + </label> + </div> + <div class="col"> + <ValidationProvider + ref="password" + :rules="{ required: true, regex: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/ }" + v-slot="{ classes, errors }" + vid="confirmation" + > + <div class="control" :class="classes"> + <div class="input-group flex-nowrap"> + <input + v-model="form.password1" + class="form-control" + :type="showPassword ? 'text' : 'password'" + placeholder="Mot de passe" + > + <span + v-if="!showPassword" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showPassword = !showPassword" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showPassword = !showPassword" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"></div> + <div class="col"> + <ValidationProvider ref="password" rules="required|confirmed:confirmation" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <div class="input-group flex-nowrap"> + <input + v-model="form.password2" + class="form-control" + :type="showPassword ? 'text' : 'password'" + placeholder="Mot de passe" + > + <span + v-if="!showPassword" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showPassword = !showPassword" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showPassword = !showPassword" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + </div> + <div class="col"> + <div class="infos"> + <ul> + <li>Votre mot de passe ne peut pas ressembler à vos informations personnelles.</li> + <li>Votre mot de passe doit contenir au minimum 8 caractères.</li> + <li>Votre mot de passe ne peut pas être un mot de passe couramment utilisé.</li> + <li>Votre mot de passe ne peut pas être entièrement numérique.</li> + </ul> + </div> + </div> + </div> + <OrganisationSelector @select="handleOrganisationSelection"/> + <b-button + :disabled="(!form.username || !form.password1 || !form.password2 || !form.first_name || !form.last_name || !form.email)" + :pressed="btnPressed" + @click.prevent="handleSubmit(submit)" + variant="primary" + > + Valider + </b-button> + <b-button type="button" variant="outline-primary" @click.prevent="$router.push({ name: 'SignIn' })"> + Annuler + </b-button> + </form> + </ValidationObserver> + </div> + </div> + <small class="footer"> + <p> + Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a> + </p> + </small> + </div> +</template> + +<script> + +import { + mapState, + mapActions, + mapMutations, +} from 'vuex'; + +const signUpActions = [ + 'POST_SIGNUP', +]; + +const signOutActions = [ + 'GET_SIGNOUT', +]; + +import OrganisationSelector from '@/components/OrganisationSelector'; + +// import usersAPI from '@/api/usersAPI.js'; + +// import Swal from "sweetalert2"; +// import "sweetalert2/dist/sweetalert2.min.css"; + +import { deburr } from 'lodash'; + +import { + ValidationObserver, + ValidationProvider, + extend, + configure, +} from 'vee-validate'; + +import { required, email, confirmed, regex } from 'vee-validate/dist/rules'; + +extend('required', { + ...required, + message: 'Ce champ est requis', +}); + +extend('email', { + ...email, + message: 'Veuillez entrer une adresse e-mail valide', +}); + +extend('confirmed', { + ...confirmed, + message: 'Les mots de passe doivent être identiques', +}); + +extend('regex', { + ...regex, + message: 'Votre mot de passe doit comporter au moins 8 caractères, dont au moins une majuscule, une minuscule et 1 chiffre.', +}); + +configure({ + classes: { + valid: 'is-valid', + invalid: 'is-invalid', + }, +}); + +export default { + name: 'SignUp', + components: { + ValidationObserver, + ValidationProvider, + OrganisationSelector, + }, + data() { + return { + loading: false, + form: { + first_name: null, + last_name: null, + email: null, + phone_number: null, + comments: null, + username: null, + password1: null, + password2: null + }, + isOrganisationSelected: false, + organisation: null, + organisationThumbnail: null, + organisationSpheres: [], + btnPressed: false, + showPassword: false, + }; + }, + computed: { + ...mapState('sign-up', [ + 'error', + 'signed' + ]), + ...mapState('sign-out', [ + 'logged' + ]) + }, + watch: { + 'form.first_name': { + deep: true, + handler(newValue) { + if (newValue !== null && this.form.last_name !== null) { + this.form.username = + deburr( + newValue + .replace(/\s/g, '') + .charAt(0) + .toLowerCase() + .concat('', this.form.last_name.replace(/\s/g, '').toLowerCase()) + ).replace(/[^a-z0-9]/g,''); + } + } + }, + 'form.last_name': { + deep: true, + handler(newValue) { + if (this.form.first_name !== null && newValue !== null) { + this.form.username = + deburr( + this.form.first_name + .replace(/\s/g, '') + .charAt(0) + .toLowerCase() + .concat('', newValue.replace(/\s/g, '').toLowerCase()) + ).replace(/[^a-z0-9]/g,''); + } + } + }, + signed() { + this.$router.push({ name: 'SignUpSuccess' }); + }, + error(newValue) { + if (newValue) { + for (const [key, value] of Object.entries(newValue)) { + if (this.$refs[key]) { + this.$refs[key].applyResult({ + errors: value, + valid: false, + failedRules: {}, + }); + } + } + } + }, + }, + created() { + if (this.logged) { + this.GET_SIGNOUT(); + } + if (this.signed) { + this.SET_SIGNED(true); + } + }, + methods: { + ...mapActions('sign-out', signOutActions), + ...mapActions('sign-up', signUpActions), + ...mapMutations('sign-up', [ + 'SET_FORM', + 'SET_SIGNED' + ]), + handleOrganisationSelection(e) { + this.isOrganisationSelected = e.selected; + this.organisation = e.orga; + this.organisationThumbnail = e.thumbnail; + this.organisationSpheres = e.spheres; + }, + submit() { + this.loading = true; + this.btnPressed = true; + // const isUsernameAvailable = await usersAPI.findUsername(this.form.username); + // if (isUsernameAvailable.results.length > 0) { + // const currentUsername = this.form.username; + // let newUsername; + // let suffix = '1'; + // // Check if username last character is a number already + // // and apply suffix accordingly + // if (!isNaN(parseInt(currentUsername.slice(-1)))) { + // suffix = (parseInt(currentUsername.slice(-1)) + 1).toString(); + // newUsername = currentUsername.replace(/.$/, suffix); + // } else { + // newUsername = currentUsername.concat(suffix); + // } + // Swal.fire({ + // position: 'center', + // heightAuto: false, + // icon: 'warning', + // html: `Le nom d'utilisateur <b>${currentUsername}</b> est déjà pris. + // Le compte sera créé avec le nom d'utilisateur suivant: <b>${newUsername}</b>`, + // showCancelButton: true, + // cancelButtonText: 'Annuler', + // showConfirmButton: true, + // confirmButtonText: 'Confirmer', + // confirmButtonColor: '#187CC6' + // }).then((result) => { + // if (result.isConfirmed) { + // this.form.username = newUsername; + // this.submit(); + // } + // }); + // } else { + this.signUp(); + // } + // this.loading = false; + }, + async signUp() { + // this.loading = true; + this.SET_FORM({ + form: { + first_name: this.form.first_name, + last_name: this.form.last_name, + email: this.form.email, + username: this.form.username, + password: this.form.password1, + phone_number: this.form.phone_number ? this.form.phone_number : '', + comments: this.form.comments ? this.form.comments : '', + ...this.isOrganisationSelected && { + usergroup_roles: [ + { + organisation: this.organisation, + }, + ], + } + }, + thumbnail: this.organisationThumbnail, + spheres: this.organisationSpheres + }); + await this.POST_SIGNUP(); + this.loading = false; + } + } +}; +</script> + +<style lang="less" scoped> +.signup-container { + margin: auto; + width: 800px; + height: fit-content; + + .signup-header { + margin: 0 1rem 1rem 1rem; + img { + width: 440px; + } + } + + .signup-form { + margin: 5rem 1rem; + + h4.title { + color: #373b3d; + + .sub-title { + font-size: 75%; + color: #6b7479; + } + } + + hr.solid { + border-top: 2px solid #373b3d; + } + + h5 { + color: #6b7479; + } + + form { + margin-top: 32px; + + h5 { + margin-bottom: 20px; + margin-top: 40px; + color: #373b3d; + } + + .row { + margin-bottom: 1.6rem; + } + + .input-group { + span { + cursor: pointer; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-left: none + } + } + + button { + float: right; + position: relative; + margin-left: 7px; + margin-top: 30px; + } + + .infos { + font-size: 0.8em; + font-style: italic; + margin-right: 1em; + ul { + padding-left: 1rem; + } + } + + } + } +} + +.form-errors { + color: #EB0600 !important; + margin-right: 2em; + line-height: 1; +} + +.form-success { + color: #30C963 !important; +} + +.footer { + position: relative; + bottom: 0; + font-size: small; + margin-top: 2rem; +} + +.footer a { + text-decoration: none; +} + +</style> diff --git a/src/views/SignUpSuccess.vue b/src/views/SignUpSuccess.vue new file mode 100644 index 0000000..7e15394 --- /dev/null +++ b/src/views/SignUpSuccess.vue @@ -0,0 +1,70 @@ +<template> + <div> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="center"> + <b-container class="msg"> + <p> + Un e-mail de confirmation vient d'être envoyé à l'adresse indiquée.<br> + Merci de bien vouloir suivre les instructions données afin de finaliser la création de votre compte. + </p> + <!-- <b-button type="button" variant="outline-primary" @click.prevent="$router.push({ name: 'SignIn' })"> + Ouvrir la page de connexion + </b-button> --> + </b-container> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> +export default { + name: 'SignUpSuccess', + data: () => { + return { }; + }, +}; +</script> + +<style lang="less" scoped> +.header { + position: absolute; + top: 0; + left: 0; +} + +.header > img { + width: 440px; +} + +.center { + position: initial; +} + +.center .msg.container { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.center .msg.container, +.center .msg.container button { + font-size: large; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/src/views/TermsOfUse.vue b/src/views/TermsOfUse.vue new file mode 100644 index 0000000..40e475d --- /dev/null +++ b/src/views/TermsOfUse.vue @@ -0,0 +1,166 @@ +<template> + <div> + <div class="terms-container"> + <div class="terms-header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div v-if="!error"> + <b-overlay + :show="loading" + :variant="'white'" + rounded="lg" + opacity="0.9" + > + <template #overlay> + <div class="d-flex align-items-center"> + <b-spinner variant="secondary"></b-spinner> + </div> + </template> + <div class="terms-title"> + {{ terms ? terms.title : '' }} + </div> + <div class="terms-content"> + <div class="terms-date"> + {{ terms ? new Date(terms.issue_date.slice(0, 10)).toLocaleString('fr-FR', { + year: "numeric", + month: "long", + day: "numeric" + }) : '' }}<br> + </div> + <div v-html="terms ? terms.body : ''"></div> + <a v-if="terms && terms.file" :href="terms.file" target="_blank">Télécharger les conditions d'utilisation</a> + </div> + <div class="terms-footer"> + <button + type="button" + class="btn btn-primary" + @click.prevent="acceptTermsOfUse" + > + Accepter + </button> + <button + type="button" + class="btn btn-outline-secondary" + @click="$router.push({ name: 'SignIn' })" + > + Refuser + </button> + </div> + </b-overlay> + </div> + <div v-else class="terms-container"> + <b-container class="msg"> + <p> + Une erreur est survenue. Veuillez contacter l'administrateur du site. + </p> + </b-container> + </div> + </div> + </div> +</template> + +<script> +import { mapState, mapMutations, mapActions } from 'vuex'; + +export default { + name: 'TermsOfUse', + + data() { + return { + loading: false + } + }, + + computed: { + ...mapState('terms-of-use', [ + 'error', + 'terms' + ]) + }, + + created() { + this.loading = true; + this.GET_TERMS_OF_USE().then(() => { + this.loading = false; + }); + }, + + methods: { + ...mapMutations('terms-of-use', [ + 'SET_ERROR' + ]), + ...mapActions('terms-of-use', [ + 'GET_TERMS_OF_USE', + 'AGREE_TO_TERMS_OF_USE' + ]), + + acceptTermsOfUse() { + this.AGREE_TO_TERMS_OF_USE({ + has_agreed: true + }); + }, + + retry() { + this.SET_ERROR({ + message: null + }); + } + } +} +</script> + +<style lang="less" scoped> +.terms-container { + margin: auto; + width: 600px; + height: fit-content; + max-height: 80vh; + + .terms-header { + margin: 0 1rem 1rem 1rem; + img { + width: 560px; + } + } + + .terms-title { + text-align: center; + font-size: 2rem; + font-weight: 600; + } + .terms-content { + text-align: justify; + margin: 1em 20px; + min-height: 10rem; + .terms-date { + text-align: right; + margin-bottom: 1rem; + } + } + .terms-footer { + height: 7rem; + margin: 2em 40px; + button.btn { + float: right; + position: relative; + margin-left: 7px; + margin-right: 0; + } + + button.btn-primary { + border: 2px solid #9BD0FF; + border-radius: 8px; + } + button.btn-outline-secondary { + background-color: #F7F8FA; + border: 2px solid #A9B2B9; + border-radius: 8px; + color: #2F3234; + } + button.btn-outline-secondary:hover { + color: white; + background-color: #4b4b4b; + } + } +} +</style> \ No newline at end of file diff --git a/src/views/UserProfile.vue b/src/views/UserProfile.vue new file mode 100644 index 0000000..7d94510 --- /dev/null +++ b/src/views/UserProfile.vue @@ -0,0 +1,708 @@ +<template> + <div> + <b-button + id="back-button" + variant="primary" + @click="goBackToNext" + > + <b-icon-arrow-bar-left/> + Revenir + </b-button> + <div class="signup-container"> + <div class="signup-header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="signup-form"> + <h4 class="title"> + Compte existant + <span class="sub-title">Modifiez les informations relatives à votre compte</span> + </h4> + <hr class="divider"> + <ValidationObserver v-slot="{ handleSubmit }"> + <b-overlay + :show="loadingUserInformation" + rounded="lg" + :style="'padding: 5px;'" + variant="white" + > + <form> + <h5>Vos informations personnelles</h5> + <hr class="divider"> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label">Nom d'utilisateur</label> + </div> + <div class="col"> + <div class="input-group flex-nowrap"> + <input + v-model="formUser.username" + type="text" + class="form-control" + placeholder="Nom d'utilisateur" + disabled + > + </div> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label required">Prénom</label> + </div> + <div class="col"> + <ValidationProvider ref="first_name" rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="formUser.first_name" + type="text" + class="form-control" + placeholder="Prénom" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label required">Nom</label> + </div> + <div class="col"> + <ValidationProvider ref="last_name" rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="formUser.last_name" + type="text" + class="form-control" + placeholder="Nom" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label"> + Adresse e-mail + </label> + </div> + <div class="col"> + <ValidationProvider ref="email" rules="required|email" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="formUser.email" + type="mail" + class="form-control" + placeholder="Adresse e-mail" + disabled + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label">Numéro de téléphone</label> + </div> + <div class="col"> + <ValidationProvider ref="phone_number" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="formUser.phone_number" + type="text" + class="form-control" + placeholder="" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + <label class="col-form-label">Motif de l'inscription</label> + </div> + <div class="col"> + <textarea + v-model="formUser.comments" + class="form-control" + /> + </div> + </div> + <div class="row g-2 align-items-center" style="margin-bottom: 0.5em;"> + <div class="col-3"> + <label class="col-form-label">Organisation(s)</label> + </div> + <div v-if="userData" class="col"> + <div v-if="userData.usergroup_roles.length === 0"> + Vous n'êtes rattaché à aucune organisation. + </div> + <b-list-group + v-else + > + <b-list-group-item + v-for="usergroup of userData.usergroup_roles" + :key="usergroup.organisation.id" + disabled + > + {{ usergroup.organisation.display_name }} <em>en tant que</em> {{ organisationsRoles.find(el => el.choice === usergroup.role).label }} + </b-list-group-item> + </b-list-group> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + </div> + <div v-if="userData" class="col"> + <div + style="margin: 0 0.5em 0.5em 0.1em; font-size: 0.9em; font-style: italic;" + > + Pour toute demande de modification, merci de contacter <a href="mailto:admin@example.com">admin@example.com</a>. + </div> + </div> + </div> + </form> + <div class="form-footer"> + <b-button + :disabled="(!formUser.first_name || !formUser.last_name || !formUser.email)" + @click.prevent="handleSubmit(submitUserInformations)" + variant="primary" + > + Valider + </b-button> + </div> + </b-overlay> + </ValidationObserver> + + <ValidationObserver v-slot="{ handleSubmit }"> + <b-overlay + :show="loadingUserEmail" + rounded="lg" + variant="white" + :style="'padding: 5px;'" + > + <form> + <h5>Changer votre adresse e-mail</h5> + <hr class="divider"> + <div class="row g-2 align-items-center"> + <div class="col-3" style="padding-right: 0;"> + <label class="col-form-label required" style="font-size: 0.9em;">Nouvelle adresse e-mail</label> + </div> + <div class="col"> + <ValidationProvider ref="email" rules="email" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <input + v-model="formEmail.new_email" + type="mail" + class="form-control" + placeholder="Adresse e-mail" + > + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + </div> + <div class="col"> + <div + style="margin: 0 0.5em 0.5em 0.5em; font-size: 0.8em; font-style: italic;" + > + Vous pouvez demander le changement de votre adresse mail en renseignant une nouvelle adresse. + Un lien de confirmation vous sera envoyé sur la nouvelle adresse. + </div> + </div> + </div> + </form> + <div class="form-footer"> + <b-button + :disabled="!formEmail.new_email" + :pressed="btnPressed" + @click.prevent="handleSubmit(submitNewEmail)" + variant="primary" + > + Valider + </b-button> + </div> + </b-overlay> + </ValidationObserver> + + <ValidationObserver v-slot="{ handleSubmit }"> + <b-overlay + :show="loadingUserPassword" + rounded="lg" + variant="white" + :style="'padding: 5px;'" + > + <form> + <h5>Changer votre mot de passe</h5> + <hr class="divider"> + <div class="row g-2 align-items-center"> + <div class="col-3" style="padding-right: 0;"> + <label class="col-form-label required" style="font-size: 0.9em;">Ancien mot de passe</label> + </div> + <div class="col"> + <ValidationProvider ref="password" rules="required" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <div class="input-group flex-nowrap"> + <input + v-model="formPassword.password" + class="form-control" + :type="showPassword ? 'text' : 'password'" + placeholder="Ancien mot de passe" + > + <span + v-if="!showPassword" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showPassword = !showPassword" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showPassword = !showPassword" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3" style="padding-right: 0;"> + <label + class="col-form-label required" + style="font-size: 0.9em;">Nouveau mot de passe</label> + </div> + <div class="col"> + <ValidationProvider ref="newPassword1" v-slot="{ classes, errors }" vid="confirmation"> + <div class="control" :class="classes"> + <div class="input-group flex-nowrap"> + <input + v-model="formPassword.newPassword1" + :type="showNewPassword2 ? 'text' : 'password'" + class="form-control" + placeholder="Nouveau mot de passe" + > + <span + v-if="!showNewPassword2" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showNewPassword2 = !showNewPassword2" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showNewPassword2 = !showNewPassword2" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3" style="padding-right: 0;"> + <label + class="col-form-label" + style="font-size: 0.9em;"></label> + </div> + <div class="col"> + <ValidationProvider ref="newPassword2" rules="confirmed:confirmation" v-slot="{ classes, errors }"> + <div class="control" :class="classes"> + <div class="input-group flex-nowrap"> + <input + v-model="formPassword.newPassword2" + class="form-control" + :type="showNewPassword2 ? 'text' : 'password'" + placeholder="Confirmez le mot de passe" + > + <span + v-if="!showNewPassword2" + class="input-group-text" + > + <b-icon + icon="eye-fill" + @click="showNewPassword2 = !showNewPassword2" + /> + </span> + <span + v-else + class="input-group-text" + > + <b-icon + icon="eye-slash-fill" + @click="showNewPassword2 = !showNewPassword2" + /> + </span> + </div> + <span class="form-errors">{{ errors[0] }}</span> + </div> + </ValidationProvider> + </div> + </div> + <div class="row g-2 align-items-center"> + <div class="col-3"> + </div> + <div class="col"> + <div class="infos"> + <ul> + <li>Votre mot de passe ne peut pas trop ressembler à vos autres informations personnelles.</li> + <li>Votre mot de passe doit contenir au minimum 8 caractères.</li> + <li>Votre mot de passe ne peut pas être un mot de passe couramment utilisé.</li> + <li>Votre mot de passe ne peut pas être entièrement numérique.</li> + </ul> + </div> + </div> + </div> + </form> + <div class="form-footer"> + <b-button + :disabled="(!formPassword.password || !formPassword.newPassword1 || !formPassword.newPassword2)" + :pressed="btnPressed" + @click.prevent="handleSubmit(submitNewPassword)" + variant="primary" + > + Valider + </b-button> + </div> + </b-overlay> + </ValidationObserver> + </div> + </div> + <small class="footer"> + <p> + Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a> + </p> + </small> + </div> +</template> + +<script> +import { mapState, mapMutations, mapActions } from 'vuex'; + +import Swal from "sweetalert2"; +import "sweetalert2/dist/sweetalert2.min.css"; + +import { + ValidationObserver, + ValidationProvider, + extend, + configure, +} from 'vee-validate'; + +import { required, email, confirmed } from 'vee-validate/dist/rules'; + +extend('required', { + ...required, + message: 'Ce champ est requis', +}); + +extend('email', { + ...email, + message: 'Veuillez entrer une adresse e-mail valide', +}); + +extend('confirmed', { + ...confirmed, + message: 'Les mots de passe doivent être identiques', +}); + +configure({ + classes: { + valid: 'is-valid', + invalid: 'is-invalid', + }, +}); + +export default { + name: 'UserProfile', + + components: { + ValidationObserver, + ValidationProvider, + }, + + data() { + return { + loadingUserInformation: false, + loadingUserPassword: false, + loadingUserEmail: false, + formUser: { + first_name: null, + last_name: null, + email: null, + phone_number: null, + comments: null, + username: null + }, + formEmail: { + new_email: null + }, + formPassword: { + password: null, + newPassword1: null, + newPassword2: null + }, + isOrganisationSelected: false, + organisation: null, + btnPressed: false, + showPassword: false, + showNewPassword2: false, + }; + }, + + computed: { + ...mapState('user', ['userData', 'success']), + ...mapState('organisations', ['organisationsRoles']), + ...mapState('sign-in', [ + 'next' + ]) + }, + + created() { + this.SET_NEXT(this.$route.query.next || process.env.VUE_APP_NEXT_DEFAULT); + if (!this.userData) { + this.loadingUserInformation = true; + this.GET_USER_DETAIL() + .then(() => { + this.formUser = { + ...this.formUser, + ...this.userData + }; + this.loadingUserInformation = false; + }); + } + if (this.organisationsRoles.length === 0) { + this.GET_ORGANISATIONS_ROLES(); + } + }, + + methods: { + ...mapMutations('sign-in', [ + 'SET_NEXT' + ]), + ...mapActions('user', [ + 'GET_USER_DETAIL', + 'UPDATE_USER_DETAIL' + ]), + ...mapActions('organisations', [ + 'GET_ORGANISATIONS_ROLES' + ]), + + goBackToNext() { + this.$router.push(this.$route.path); + window.location.pathname = this.next; + }, + + submitUserInformations() { + this.loadingUserInformation = true + this.UPDATE_USER_DETAIL(this.formUser) + .then(() => { + this.GET_USER_DETAIL() + .then(() => { + this.formUser = { + ...this.formUser, + ...this.userData + }; + this.loadingUserInformation = false; + }); + }) + .catch(() => { + this.loadingUserInformation = false; + }); + }, + + submitNewEmail() { + const data = { + new_email: this.formEmail.new_email, + }; + this.loadingUserEmail = true; + this.UPDATE_USER_DETAIL(data) + .then(() => { + this.loadingUserEmail = false; + Swal.fire({ + position: 'center', + heightAuto: false, + icon: 'success', + text: `Un e-mail est envoyé à votre nouvelle adresse. + Il contient un lien de validation sur lequel vous devez + cliquer pour confirmer le changement. + `, + showConfirmButton: true, + confirmButtonText: 'OK', + confirmButtonColor: '#187CC6' + }); + }); + }, + + submitNewPassword() { + const data = { + password: this.formPassword.password, + new_password: this.formPassword.newPassword2 + }; + this.loadingUserPassword = true; + this.UPDATE_USER_DETAIL(data) + .then(() => { + this.loadingUserPassword = false; + if (this.success && this.success.length) { + Swal.fire({ + position: 'center', + heightAuto: false, + icon: 'success', + text: `Votre mot de passe a bien été modifié. + Vous allez être redirigé vers la page de connexion. + `, + showConfirmButton: true, + confirmButtonText: 'OK', + confirmButtonColor: '#187CC6' + }).then((result) => { + if (result.isConfirmed) { + this.$router.push({ name: 'SignIn' }); + } + }); + } + }); + } + } + +} +</script> + +<style lang="less" scoped> + +#back-button { + font-size: 1.5em; + position: -webkit-sticky; /* Safari */ + position: sticky; + top: 5%; + left: 40px; + align-self: flex-start; + border: 2px solid #9BD0FF; + border-radius: 8px; + letter-spacing: 1px; +} + +.signup-container { + margin: auto; + width: 800px; + height: fit-content; + + .signup-header { + margin: 0 1rem 1rem 1rem; + img { + width: 440px; + } + } + + .signup-form { + margin: 5rem 1rem; + + h4.title { + color: #373b3d; + + .sub-title { + font-size: 75%; + color: #6b7479; + } + } + + hr.solid { + border-top: 2px solid #373b3d; + } + + h5 { + color: #6b7479; + } + + form { + margin-top: 32px; + + h5 { + margin-bottom: 20px; + margin-top: 40px; + color: #373b3d; + } + + .row { + margin-bottom: 1.6rem; + } + + .input-group { + span { + cursor: pointer; + border-bottom-left-radius: 0; + border-top-left-radius: 0; + border-left: none + } + } + } + .infos { + font-size: 0.7em; + font-style: italic; + margin-right: 1em; + ul { + padding-left: 1rem; + } + } + .form-footer { + display: flex; + justify-content: flex-end; + margin-left: 7px; + margin-top: 30px; + button { + margin-left: 2em; + } + button.btn-primary { + border: 2px solid #9BD0FF; + border-radius: 8px; + } + button.btn-outline-secondary { + background-color: #F7F8FA; + border: 2px solid #A9B2B9; + border-radius: 8px; + color: #2F3234; + } + button.btn-outline-secondary:hover { + color: white; + background-color: #4b4b4b; + } + } + } +} + +.form-errors { + color: #EB0600 !important; +} + +.form-success { + color: #30C963 !important; +} + +.footer { + position: relative; + bottom: 0; + font-size: small; + margin-top: 2rem; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/src/views/ValidationEmail.vue b/src/views/ValidationEmail.vue new file mode 100644 index 0000000..b73a7b1 --- /dev/null +++ b/src/views/ValidationEmail.vue @@ -0,0 +1,98 @@ +<template> + <div> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="center"> + <b-container class="msg" v-if="status"> + <p> + Votre nouvelle adresse e-mail est validée. + </p> + <b-button + type="button" + variant="outline-primary" + @click.prevent="$router.push({ name: 'SignIn' })" + > + Ouvrir la page de connexion + </b-button> + </b-container> + <b-container class="msg" v-else> + <p> + Une erreur est survenue. Veuillez contacter l'administrateur du site. + </p> + </b-container> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> + +import { + mapState, + mapActions, +} from 'vuex'; + +export default { + name: 'ValidationEmail', + data: () => { + return { + }; + }, + computed: { + ...mapState('validation-email', [ + 'status' + ]) + }, + created() { + this.POST_TOKEN(this.$route.query.token); + }, + methods: { + ...mapActions('validation-email', [ + 'POST_TOKEN' + ]) + } +}; +</script> + +<style lang="less" scoped> +.header { + position: absolute; + top: 0; + left: 0; +} + +.header > img { + width: 440px; +} + +.center { + position: initial; +} + +.center .msg.container { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.center .msg.container, +.center .msg.container button { + font-size: large; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/src/views/ValidationRegistration.vue b/src/views/ValidationRegistration.vue new file mode 100644 index 0000000..53dbcee --- /dev/null +++ b/src/views/ValidationRegistration.vue @@ -0,0 +1,126 @@ +<template> + <div> + <div class="header"> + <img alt="logo" src="@/assets/logo_pigma.png"/> + </div> + <div class="center"> + <b-container class="msg" v-if="btnVariant=='success'"> + <p> + Merci pour votre inscription. + </p> + <p> + Vous recevrez une notification lors de l'activation de votre compte par l'administrateur de la plateforme. + </p> + <!-- <b-button type="button" variant="outline-primary" @click.prevent="$router.push({ name: 'SignIn' })"> + Ouvrir la page de connexion + </b-button> --> + </b-container> + <b-container class="msg" v-else-if="btnVariant=='danger'"> + <p> + Une erreur est survenue. Veuillez contacter l'administrateur du site. + </p> + </b-container> + <b-container class="msg" v-else> + <p> + Pour confirmer votre inscription <b-button :pressed="btnPressed" :variant="btnVariant" @click.prevent="submit">Cliquez ici</b-button> + </p> + </b-container> + </div> + <small class="footer"> + <p>Propulsé par <a href="https://www.neogeo.fr/" target="_blank" rel="noopener">Neogeo-Technologies</a></p> + </small> + </div> +</template> + +<script> + +import { + mapGetters, + mapState, + mapActions, + mapMutations +} from 'vuex'; + +const validationRegistrationStoreName = 'validation-registration'; + +const validationRegistrationGetters = { + status: 'getStatus', +}; + +const validationRegistrationState = ['accountStatus']; +const validationRegistrationMutations = ['SET_TOKEN']; +const validationRegistrationActions = ['POST_TOKEN']; + +export default { + name: 'ValidationRegistration', + data: () => { + return { + btnPressed: false, + }; + }, + computed: { + ...mapState(validationRegistrationStoreName, validationRegistrationState), + ...mapGetters(validationRegistrationStoreName, validationRegistrationGetters), + btnVariant() { + if (this.status === true) { + return 'success'; + } else if (this.status === false) { + return 'danger'; + } else { + return ''; + } + }, + }, + created() { + this.$store.commit('validation-registration/SET_TOKEN', this.$route.query.token); + }, + methods: { + ...mapActions(validationRegistrationStoreName, validationRegistrationActions), + ...mapMutations(validationRegistrationStoreName, validationRegistrationMutations), + async submit() { + this.btnPressed = true; + await this.$store.dispatch('validation-registration/POST_TOKEN'); + }, + }, +}; +</script> + +<style lang="less" scoped> +.header { + position: absolute; + top: 0; + left: 0; +} + +.header > img { + width: 440px; +} + +.center { + position: initial; +} + +.center .msg.container { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +.center .msg.container, +.center .msg.container button { + font-size: large; +} + +.footer { + position: absolute; + bottom: 0; + font-size: small; +} + +.footer a { + text-decoration: none; +} +</style> diff --git a/tests/e2e/.eslintrc.js b/tests/e2e/.eslintrc.js new file mode 100644 index 0000000..25e20e8 --- /dev/null +++ b/tests/e2e/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + plugins: [ + 'cypress', + ], + env: { + mocha: true, + 'cypress/globals': true, + }, + rules: { + strict: 'off', + }, +}; diff --git a/tests/e2e/plugins/index.js b/tests/e2e/plugins/index.js new file mode 100644 index 0000000..8be9bc3 --- /dev/null +++ b/tests/e2e/plugins/index.js @@ -0,0 +1,26 @@ +/* eslint-disable arrow-body-style */ +// https://docs.cypress.io/guides/guides/plugins-guide.html + +// if you need a custom webpack configuration you can uncomment the following import +// and then use the `file:preprocessor` event +// as explained in the cypress docs +// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples + +// /* eslint-disable import/no-extraneous-dependencies, global-require */ +// const webpack = require('@cypress/webpack-preprocessor') + +module.exports = (on, config) => { + // on('file:preprocessor', webpack({ + // webpackOptions: require('@vue/cli-service/webpack.config'), + // watchOptions: {} + // })) + + return { + ...config, + fixturesFolder: 'tests/e2e/fixtures', + integrationFolder: 'tests/e2e/specs', + screenshotsFolder: 'tests/e2e/screenshots', + videosFolder: 'tests/e2e/videos', + supportFile: 'tests/e2e/support/index.js', + }; +}; diff --git a/tests/e2e/specs/test.js b/tests/e2e/specs/test.js new file mode 100644 index 0000000..e6c9471 --- /dev/null +++ b/tests/e2e/specs/test.js @@ -0,0 +1,8 @@ +// https://docs.cypress.io/api/introduction/api.html + +describe('My First Test', () => { + it('Visits the app root url', () => { + cy.visit('/'); + cy.contains('h1', 'Welcome to Your Vue.js App'); + }); +}); diff --git a/tests/e2e/support/commands.js b/tests/e2e/support/commands.js new file mode 100644 index 0000000..c1f5a77 --- /dev/null +++ b/tests/e2e/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/tests/e2e/support/index.js b/tests/e2e/support/index.js new file mode 100644 index 0000000..37a498f --- /dev/null +++ b/tests/e2e/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/tests/unit/example.spec.js b/tests/unit/example.spec.js new file mode 100644 index 0000000..6fa693d --- /dev/null +++ b/tests/unit/example.spec.js @@ -0,0 +1,12 @@ +import { shallowMount } from '@vue/test-utils'; +import HelloWorld from '@/components/HelloWorld.vue'; + +describe('HelloWorld.vue', () => { + it('renders props.msg when passed', () => { + const msg = 'new message'; + const wrapper = shallowMount(HelloWorld, { + propsData: { msg }, + }); + expect(wrapper.text()).toMatch(msg); + }); +}); diff --git a/vue.config.js b/vue.config.js new file mode 100644 index 0000000..0aeb41b --- /dev/null +++ b/vue.config.js @@ -0,0 +1,15 @@ +module.exports = { + publicPath: process.env.VUE_APP_BASE_PATH || '/', + transpileDependencies: ['vuetify'], + lintOnSave: false, + css: { + loaderOptions: { + less: { + globalVars: { + blue: '#187CC6', + lightBlue: '#9BD0FF', + }, + }, + }, + }, +}; -- GitLab