diff --git a/src/assets/resources/semantic-ui-2.4.2/semantic.min.css b/src/assets/resources/semantic-ui-2.4.2/semantic.min.css index 9b65ec01786f777114e4a3e90901b19738630a56..a9922b28f713187e679568a345d4295d764c9fcd 100644 --- a/src/assets/resources/semantic-ui-2.4.2/semantic.min.css +++ b/src/assets/resources/semantic-ui-2.4.2/semantic.min.css @@ -20033,10 +20033,10 @@ ol.ui.list li[value]:before { padding: 1em 1.5em; line-height: 1.4285em; color: #252525; - -webkit-transition: opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease; - transition: opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease; - transition: opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease; - transition: opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease, -webkit-box-shadow .5s ease; + -webkit-transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease; + transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, -webkit-box-shadow .5s ease; + transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease; + transition: padding .5s ease, max-height .5s ease, opacity .5s ease, color .5s ease, background .5s ease, box-shadow .5s ease, -webkit-box-shadow .5s ease; border-radius: .07142857rem; -webkit-box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent; box-shadow: 0 0 0 1px rgba(34, 36, 38, .22) inset, 0 0 0 0 transparent diff --git a/src/components/MessageInfo.vue b/src/components/MessageInfo.vue index ec61554a06157953f61df6b37666379457b1498d..5744791616bce1359473eafa73c3956e354cf463 100644 --- a/src/components/MessageInfo.vue +++ b/src/components/MessageInfo.vue @@ -12,10 +12,10 @@ /> <div class="header"> <i - class="info circle icon" + :class="[headerIcon, 'circle icon']" aria-hidden="true" /> - Informations + {{ message.header || 'Informations' }} </div> <ul class="list"> {{ @@ -49,6 +49,17 @@ export default { computed: { ...mapState(['messages']), + + headerIcon() { + switch (this.message.level) { + case 'positive': + return 'check'; + case 'negative': + return 'times'; + default: + return 'info'; + } + } }, mounted() { @@ -83,7 +94,12 @@ export default { transition: all 0.6s ease-out; } .list-container.show{ - height: 6em; + height: 7.5em; +} +@media screen and (min-width: 726px) { + .list-container.show{ + height: 6em; + } } .list-container.show:not(:first-child){ margin-top: 10px; @@ -103,9 +119,14 @@ export default { ul.list{ overflow: auto; - height: 2.2em; + height: 3.5em; margin-bottom: .5em !important; } +@media screen and (min-width: 726px) { + ul.list{ + height: 2.2em; + } +} .ui.message { overflow: hidden; diff --git a/src/router/index.js b/src/router/index.js index d0f2883bfd63d52f0853f36603e92b060aab4241..0323e1290168f84878132500a54413755e9ca45e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -20,6 +20,7 @@ const routes = [ { path: `${projectBase === 'projet' ? '': `/${projectBase}/:slug`}/connexion/`, name: 'login', + props: true, component: () => import('../views/Login.vue') }, { @@ -27,6 +28,12 @@ const routes = [ name: 'signup', component: () => import('../views/Login.vue') }, + { + path: `${projectBase === 'projet' ? '': `/${projectBase}/:slug`}/inscription/succes`, + name: 'sso-signup-success', + props: true, + component: () => import('../views/Login.vue') + }, { path: `${projectBase === 'projet' ? '': `/${projectBase}/:slug`}/my_account/`, name: 'my_account', diff --git a/src/services/user-api.js b/src/services/user-api.js new file mode 100644 index 0000000000000000000000000000000000000000..015a55105802b35a21c6976458650bc51fd8c128 --- /dev/null +++ b/src/services/user-api.js @@ -0,0 +1,20 @@ +import axios from '@/axios-client.js'; +import store from '../store'; + + +const baseUrl = store.state.configuration.VUE_APP_DJANGO_API_BASE; + +const userAPI = { + async signup(data, url) { + try { + const response = await axios.post(url || `${baseUrl}v2/users/`, data); + return response; // Retourne directement la réponse si succès + } catch (err) { + console.error('Erreur lors de l\'inscription :', err.response || err); + return err.response || { status: 500, data: { detail: 'Erreur inconnue' } }; // 👈 Retourne la réponse d'erreur si disponible + } + }, +}; + + +export default userAPI; diff --git a/src/store/index.js b/src/store/index.js index 0f35dd98b56e2153a54b90714b37104435035003..ff20b3c0d7a59b47b97f40634b52b212d0caf29d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -148,33 +148,6 @@ export default new Vuex.Store({ }); }, - SIGNUP({ commit }, payload) { - if (payload.first_name && payload.last_name && payload.username && - payload.email && payload.password && payload.siret) { - console.log('payload', payload); - return axios - .post('https://fenigs.neogeo.fr/fr/login/signup/', { - first_name: payload.first_name, - last_name: payload.last_name, - username: payload.username, - email: payload.email, - password: payload.password, - comments: payload.siret, - }) - .then((response) => { - console.log('response', response); - if (response.status === 201 && response.data) { - console.log('response.data', response.data); - } - }) - .catch((err) => { - console.error(err) - commit('SET_USER', false); - return 'error'; - }); - } - }, - LOGIN({ commit, dispatch }, payload) { if (payload.username && payload.password) { return axios diff --git a/src/views/Login.vue b/src/views/Login.vue index bc5d2aa97228cd8a8d2c270670429c70a549a98f..35ab7b8a06714f335c88237889cd1faba0decec3 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -26,10 +26,7 @@ CONNEXION </h3> - <div - v-if="form.errors" - class="ui warning message" - > + <div :class="['ui warning message', {'closed': !errors.global}]"> <div class="header"> Les informations d'identification sont incorrectes. </div> @@ -43,28 +40,28 @@ @submit.prevent="login" > <div class="ui secondary segment"> - <div class="six field required"> + <div class="six field"> <div class="ui left icon input"> <i class="user icon" aria-hidden="true" /> <input - v-model="form.username" + v-model="loginForm.username" type="text" name="username" placeholder="Utilisateur" > </div> </div> - <div class="six field required"> + <div class="six field"> <div class="ui left icon input"> <i class="lock icon" aria-hidden="true" /> <input - v-model="form.password" + v-model="loginForm.password" type="password" name="password" placeholder="Mot de passe" @@ -81,21 +78,15 @@ </form> </div> <div - v-if="$route.name === 'signup'" + v-else-if="$route.name === 'signup'" class="six wide column" > <h3 class="ui horizontal divider header"> INSCRIPTION </h3> - <div - v-if="form.errors" - class="ui warning message" - > - <div class="header"> - Un compte associé à cette adresse courriel existe déjà . - </div> - Veuillez utiliser une adresse email différente ou vous connecter avec le compte associé à cette adresse courriel. + <div :class="['ui warning message', {'closed': !error}]"> + {{ error }} </div> <form @@ -105,60 +96,62 @@ @submit.prevent="signup" > <div class="ui secondary segment"> - - <div class="six field required"> + <div class="six field"> <div class="ui left icon input"> <i class="user outline icon" aria-hidden="true" /> <input - v-model="form.first_name" + v-model="signupForm.first_name" type="text" name="first_name" placeholder="Prénom" + required > </div> </div> - <div class="six field required"> + <div class="six field"> <div class="ui left icon input"> <i class="id card icon" aria-hidden="true" /> <input - v-model="form.last_name" + v-model="signupForm.last_name" type="text" name="last_name" placeholder="Nom" + required > </div> </div> - <div class="six field required"> + <div class="six field"> <div class="ui left icon input"> <i class="envelope icon" aria-hidden="true" /> <input - v-model="form.email" + v-model="signupForm.email" type="email" name="email" placeholder="Adresse courriel" + required > </div> </div> - <div class="six field required"> + <div class="six field"> <div class="ui left icon input"> <i class="user icon" aria-hidden="true" /> <input - v-model="form.username" + v-model="signupForm.username" type="text" name="username" placeholder="Utilisateur" @@ -167,37 +160,52 @@ </div> </div> - <div class="six field required"> - <div class="ui left icon input"> + <div :class="['six field', {'error': errors.passwd}]"> + <div class="ui action left icon input"> <i class="lock icon" aria-hidden="true" /> <input - v-model="form.password" - type="password" + v-model="signupForm.password" + :type="showPwd ? 'text' : 'password'" name="password" placeholder="Mot de passe" + required + @blur="isValidPwd" + > + <button + class="ui icon button" + @click="showPwd = !showPwd" > + <i :class="[showPwd ? 'eye slash' : 'eye', 'icon']" /> + </button> </div> </div> - <div class="six field required"> - <div class="ui labeled left icon input"> - <label for="amount" class="ui label">SIRET</label> + <div :class="['six field', {'error': errors.comments}]"> + <div class="ui left icon input"> + <i + class="pencil icon" + aria-hidden="true" + /> <input - v-model="form.siret" - type="number" - name="siret" + v-model="signupForm.comments" + type="text" + name="comments" + :placeholder="commentsFieldLabel || `Commentaires`" + :required="commentsFieldRequired" > </div> </div> - - <div class="six field required"> + <div + v-if="usersGroupsOptions.length > 0" + class="six field" + > + <div class="ui divider" /> <Multiselect - v-if="usersGroupsOptions" - v-model="form.usersgroups" - :options="usersGroupsOptions || []" + v-model="usersGroupsSelections" + :options="usersGroupsOptions" :multiple="true" track-by="value" label="name" @@ -206,21 +214,15 @@ deselect-label="" :searchable="false" :placeholder="'Sélectionez un ou plusieurs groupe de la liste ...'" - /> - </div> - <!-- - :options-limit="10" - :reset-after="false" - :loading="loadingPrerecordedListValues" - :show-no-results="true" - :clear-on-select="false" - :preserve-search="false" - @search-change="search" - @select="selectPrerecordedValue" - --> + /> + <p v-if="adminMail"> + Si le groupe d'utilisateurs recherché n'apparaît pas, vous pouvez demander à un + <a :href="'mailto:'+adminMail">administrateur</a> de le créer + </p> + </div> <button - class="ui fluid large teal submit button" + :class="['ui fluid large teal submit button']" type="submit" > Valider @@ -228,13 +230,38 @@ </div> </form> </div> + <div + v-else-if="$route.name === 'sso-signup-success'" + class="six wide column" + > + <h3 class="ui horizontal divider header"> + INSCRIPTION RÉUSSIE + </h3> + <h4 class="ui center aligned icon header"> + <div class="content"> + <p + v-if="username" + class="sub header" + > + Le compte pour le nom d'utilisateur <strong>{{ username }}</strong> a été créé + </p> + <p> + Un e-mail de confirmation vient d'être envoyé à l'adresse indiquée. + </p> + <p class="sub header"> + Merci de bien vouloir suivre les instructions données afin de finaliser la création de votre compte. + </p> + </div> + </h4> + </div> </div> </div> </template> <script> -import { mapState, mapGetters } from 'vuex'; +import { mapState, mapGetters, mapMutations } from 'vuex'; import Multiselect from 'vue-multiselect'; +import userAPI from '../services/user-api'; export default { name: 'Login', @@ -243,20 +270,35 @@ export default { Multiselect }, + props: { + username: { + type: String, + default: null + } + }, + data() { return { logged: false, - form: { - errors: null, + loginForm: { + username: null, + password: null, + }, + signupForm: { + username: null, + password: null, first_name: null, last_name: null, - username: null, email: null, - password: null, - password2: null, - siret: null, - usersgroups: null, + comments: null, + usersgroups: [], }, + errors: { + global: null, + passwd: null, + comments: null, + }, + showPwd: false, }; }, @@ -265,19 +307,50 @@ export default { appLogo: state => state.configuration.VUE_APP_LOGO_PATH, appName: state => state.configuration.VUE_APP_APPLICATION_NAME, appAbstract: state => state.configuration.VUE_APP_APPLICATION_ABSTRACT, + adminMail: state => state.configuration.VUE_APP_ADMIN_MAIL, + ssoSignupUrl: state => state.configuration.VUE_APP_SSO_SIGNUP_URL, + commentsFieldLabel: state => state.configuration.VUE_APP_SIGNUP_COMMENTS_FIELD_LABEL, + commentsFieldRequired: state => state.configuration.VUE_APP_SIGNUP_COMMENTS_FIELD_REQUIRED, }), ...mapGetters(['usersGroupsOptions']), + + usersGroupsSelections: { + get() { + return this.usersGroupsOptions.filter((el) => this.signupForm.usersgroups?.includes(el.value)); + }, + set(newValue) { + this.signupForm.usersgroups = newValue.map(el => el.value); + } + }, + + error() { + return this.errors.global || this.errors.passwd || this.errors.comments; + } }, watch: { - 'form.first_name': function(newValue, oldValue) { - if (newValue && newValue !== oldValue && this.form.last_name) { - this.form.username = `${newValue.charAt(0)}${this.form.last_name}`.toLowerCase(); + 'signupForm.first_name': function (newValue, oldValue) { + if (newValue !== oldValue) { + this.signupForm.username = `${newValue.charAt(0)}${this.signupForm.last_name}`.toLowerCase().replace(/\s/g, ''); + } + }, + 'signupForm.last_name': function (newValue, oldValue) { + if (newValue !== oldValue) { + this.signupForm.username = `${this.signupForm.first_name.charAt(0)}${newValue}`.toLowerCase().replace(/\s/g, ''); + } + }, + 'signupForm.password': function (newValue, oldValue) { + if (newValue.length >= 8) { + if (newValue !== oldValue) { + this.isValidPwd(); + } + } else { + this.errors.passwd = null; } }, - 'form.last_name': function(newValue, oldValue) { - if (newValue && newValue !== oldValue && this.form.first_name) { - this.form.username = `${this.form.first_name.charAt(0)}${newValue}`.toLowerCase(); + username(newValue, oldValue) { + if (newValue !== oldValue) { + this.loginForm.username = newValue; } } }, @@ -289,55 +362,110 @@ export default { }, mounted() { - if (this.$store.state.user) { - this.$store.commit( - 'DISPLAY_MESSAGE', - { comment: 'Vous êtes déjà connecté, vous allez être redirigé vers la page précédente.' } - ); - setTimeout(() => this.$store.dispatch('REDIRECT_AFTER_LOGIN'), 3100); + if (this.$route.name === 'login') { + if (this.$store.state.user) { + this.DISPLAY_MESSAGE({ header: 'Vous êtes déjà connecté', comment: 'Vous allez être redirigé vers la page précédente.' }); + setTimeout(() => this.$store.dispatch('REDIRECT_AFTER_LOGIN'), 3100); + } } }, - + methods: { + ...mapMutations(['DISPLAY_MESSAGE']), + login() { this.$store .dispatch('LOGIN', { - username: this.form.username, - password: this.form.password, + username: this.loginForm.username, + password: this.loginForm.password, }) .then((status) => { if (status === 200) { - this.form.errors = null; + this.errors.global = null; } else if (status === 'error') { - this.form.errors = status; + this.errors.global = status; } }) .catch(); }, - signup() { - this.form.errors = null; - this.$store - .dispatch('SIGNUP', { - first_name: this.form.first_name, - last_name: this.form.last_name, - username: this.form.username, - email: this.form.email, - password: this.form.password, - siret: this.form.siret, - }) - .then((status) => { - if (status === 200) { - this.form.errors = null; - } else if (status === 'error') { - this.form.errors = status; + + async signup() { + if (this.hasUnvalidFields()) return; + + // Étape 1 : Création de l'utilisateur auprès du service d'authentification SSO si nécessaire + if (this.ssoSignupUrl) { + const ssoResponse = await userAPI.signup(this.signupForm, this.ssoSignupUrl); + + console.log(ssoResponse); + if (ssoResponse.status !== 201) { + if (ssoResponse.status === 400) { + this.errors.global = 'Un compte associé à ce courriel existe déjà '; + } else { + this.errors.global = `Erreur lors de l'inscription: ${ssoResponse.data?.detail || 'Problème inconnu'}`; } - }) - .catch(); + return; // Stoppe la fonction si l'inscription SSO échoue + } else { + this.signupForm.username = ssoResponse.data.username; + this.signupForm.first_name = ssoResponse.data.first_name; + this.signupForm.last_name = ssoResponse.data.last_name; + } + } + + // Étape 2 : Création de l'utilisateur dans Geocontrib + const response = await userAPI.signup(this.signupForm); + + if (response.status !== 201) { + const errorMessage = response.data + ? Object.values(response.data)?.[0]?.[0] || 'Problème inconnu' + : 'Problème inconnu'; + this.errors.global = `Erreur lors de l'inscription: ${errorMessage}`; + return; + } + + this.DISPLAY_MESSAGE({ header: 'Inscription réussie !', comment: `Bienvenue sur la plateforme ${this.signupForm.username}.`, level: 'positive' }); + if (this.ssoSignupUrl) { + setTimeout(() => { + this.$router.push({ name: 'sso-signup-success', params: { username: this.signupForm.username } }); + }, 3100); + } else { + setTimeout(() => { + this.$router.push({ name: 'login', params: { username: this.signupForm.username } }); + }, 3100); + } }, - checkPwdCreation() { - if (this.form.password !== this.form.password2) { - this.form.errors = 'Les mots de passe doivent être identique' + + + isValidPwd() { + const regPwd = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d/$&+,:;=?#|'<>.^*()%!-]{8,}$/; + + if (!regPwd.test(this.signupForm.password)) { + this.errors.passwd = `Le mot de passe doit comporter au moins 8 caractères, dont 1 majuscule, 1 minuscule et 1 chiffre. + Vous pouvez utiliser les caractères spéciaux suivants : /$ & + , : ; = ? # | ' < > . ^ * ( ) % ! -.`; + return false; + } + + this.errors.passwd = null; + return true; + }, + + hasUnvalidFields() { + const { last_name, email, first_name, comments } = this.signupForm; + + if (this.commentsFieldRequired && !comments) { + this.errors.comments = `Le champ ${ this.commentsFieldLabel || 'Commentaires'} est requis`; + return true; + } else { + this.errors.comments = null; + } + + if (email && last_name && first_name) { + this.errors.global = null; + } else { + this.errors.global = 'Certains champs requis ne sont pas renseignés'; + return true; } + + return !this.isValidPwd(); } }, }; @@ -348,6 +476,27 @@ export default { max-width: 500px; min-width: 200px; margin: 3em auto; + + .ui.message { + min-height: 0px; + + &.closed { + overflow: hidden; + opacity: 0; + padding: 0; + max-height: 0px; + } + } + + input[required] { + background-image: linear-gradient(45deg, transparent, transparent 50%, rgb(209, 0, 0) 50%, rgb(209, 0, 0) 100%); + background-position: top right; + background-size: .5em .5em; + background-repeat: no-repeat; + } +} +p { + margin: 1em 0 !important; } </style> @@ -364,18 +513,23 @@ export default { } /* keep font-weight from overide of semantic classes */ -.multiselect__placeholder, .multiselect__content, .multiselect__tags { +.multiselect__placeholder, +.multiselect__content, +.multiselect__tags { font-weight: initial !important; } + /* keep placeholder eigth */ .multiselect .multiselect__placeholder { margin-bottom: 9px !important; padding-top: 1px; } + /* keep placeholder height when opening dropdown without selection */ input.multiselect__input { padding: 3px 0 0 0 !important; } + /* keep placeholder height when opening dropdown with already a value selected */ .multiselect__tags .multiselect__single { padding: 1px 0 0 0 !important;