Skip to content
Snippets Groups Projects
Commit 984818d5 authored by Timothee P's avatar Timothee P :sunflower:
Browse files

Merge branch 'redmine-issues/25582' into 'develop'

REDMINE_ISSUE-25582 | Mise en place d’un système de vérification lors de la création d’un compte utilisateur

See merge request !870
parents ebf06e92 0700fd25
No related branches found
No related tags found
1 merge request!870REDMINE_ISSUE-25582 | Mise en place d’un système de vérification lors de la création d’un compte utilisateur
......@@ -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
......
......@@ -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;
......
......@@ -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',
......
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;
......@@ -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
......
......@@ -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;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment