<template> <div id="login-page"> <div class="row"> <div class="fourteen wide column"> <img class="ui centered small image" :src="appLogo" alt="Logo de l'application" > <h2 class="ui center aligned icon header"> <div class="content"> {{ appName }} <div class="sub header"> {{ appAbstract }} </div> </div> </h2> </div> </div> <div class="row"> <div v-if="$route.name === 'login'" class="six wide column" > <h3 class="ui horizontal divider header"> CONNEXION </h3> <div :class="['ui warning message', {'closed': !errors.global}]"> <div class="header"> Les informations d'identification sont incorrectes. </div> NB: Seuls les comptes actifs peuvent se connecter. </div> <form class="ui form" role="form" type="post" @submit.prevent="login" > <div class="ui secondary segment"> <div class="six field"> <div class="ui left icon input"> <i class="user icon" aria-hidden="true" /> <input v-model="loginForm.username" type="text" name="username" placeholder="Utilisateur" > </div> </div> <div class="six field"> <div class="ui left icon input"> <i class="lock icon" aria-hidden="true" /> <input v-model="loginForm.password" type="password" name="password" placeholder="Mot de passe" > </div> </div> <button class="ui fluid large teal submit button" type="submit" > Login </button> </div> </form> </div> <div v-else-if="$route.name === 'signup'" class="six wide column" > <h3 class="ui horizontal divider header"> INSCRIPTION </h3> <div :class="['ui warning message', {'closed': !error}]"> {{ error }} </div> <form class="ui form" role="form" type="post" @submit.prevent="signup" > <div class="ui secondary segment"> <div class="six field"> <div class="ui left icon input"> <i class="user outline icon" aria-hidden="true" /> <input v-model="signupForm.first_name" type="text" name="first_name" placeholder="Prénom" required > </div> </div> <div class="six field"> <div class="ui left icon input"> <i class="id card icon" aria-hidden="true" /> <input v-model="signupForm.last_name" type="text" name="last_name" placeholder="Nom" required > </div> </div> <div class="six field"> <div class="ui left icon input"> <i class="envelope icon" aria-hidden="true" /> <input v-model="signupForm.email" type="email" name="email" placeholder="Adresse courriel" required > </div> </div> <div class="six field"> <div class="ui left icon input"> <i class="user icon" aria-hidden="true" /> <input v-model="signupForm.username" type="text" name="username" placeholder="Utilisateur" disabled > </div> </div> <div :class="['six field', {'error': errors.passwd}]"> <div class="ui action left icon input"> <i class="lock icon" aria-hidden="true" /> <input 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', {'error': errors.comments}]"> <div class="ui left icon input"> <i class="pencil icon" aria-hidden="true" /> <input v-model="signupForm.comments" type="text" name="comments" :placeholder="commentsFieldLabel || `Commentaires`" :required="commentsFieldRequired" > </div> </div> <div v-if="usersGroupsOptions.length > 0" class="six field" > <div class="ui divider" /> <Multiselect v-model="usersGroupsSelections" :options="usersGroupsOptions" :multiple="true" track-by="value" label="name" select-label="" selected-label="" deselect-label="" :searchable="false" :placeholder="'Sélectionez un ou plusieurs groupe de la liste ...'" /> <p v-if="adminMail"> Si le groupe d'utilisateurs recherché n'apparaît pas, vous pouvez demander à <a :href="'mailto:'+adminMail">{{ adminMail }}</a> de le créer </p> </div> <button :class="['ui fluid large teal submit button']" type="submit" > Valider </button> </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, mapMutations } from 'vuex'; import Multiselect from 'vue-multiselect'; import userAPI from '../services/user-api'; export default { name: 'Login', components: { Multiselect }, props: { username: { type: String, default: null } }, data() { return { logged: false, loginForm: { username: null, password: null, }, signupForm: { username: null, password: null, first_name: null, last_name: null, email: null, comments: null, usersgroups: [], }, errors: { global: null, passwd: null, comments: null, }, showPwd: false, }; }, computed: { ...mapState({ 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: { '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; } }, username(newValue, oldValue) { if (newValue !== oldValue) { this.loginForm.username = newValue; } } }, created() { if (this.$route.name === 'signup') { this.$store.dispatch('GET_USERS_GROUPS'); // récupére les groupes d'utilisateurs pour extra_forms } }, mounted() { 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.loginForm.username, password: this.loginForm.password, }) .then((status) => { if (status === 200) { this.errors.global = null; } else if (status === 'error') { this.errors.global = status; } }) .catch(); }, 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, // Ajout du label personnalisé pour affichage plus précis dans admin OGS comments: `{"${this.commentsFieldLabel}":"${this.signupForm.comments}"}`, // Pour permettre la visualisation dans OGS Maps, l'utilisateur doit être ajouté à un groupe OGS, mis en dur pour aller vite pour l'instant usergroup_roles:[{ organisation: { id: 1 } }] }, this.ssoSignupUrl); 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'}`; } 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); } }, 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(); } }, }; </script> <style lang="less" scoped> #login-page { 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> <style> .multiselect__placeholder { position: absolute; width: calc(100% - 48px); overflow: hidden; text-overflow: ellipsis; } .multiselect__tags { position: relative; } /* keep font-weight from overide of semantic classes */ .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; margin-bottom: 9px; } </style>