diff --git a/app/auth/email.py b/app/auth/email.py index d067bef27a..9ac8a0173d 100644 --- a/app/auth/email.py +++ b/app/auth/email.py @@ -7,7 +7,7 @@ from app.email import send_email def send_password_reset_email(user): token = user.get_reset_password_token() send_email( - "[ScoDoc] Reset Your Password", + "[ScoDoc] Réinitialisation de votre mot de passe", sender=current_app.config["ADMINS"][0], recipients=[user.email], text_body=render_template("email/reset_password.txt", user=user, token=token), diff --git a/app/auth/forms.py b/app/auth/forms.py index bb201e26db..e374781776 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -53,3 +53,8 @@ class ResetPasswordForm(FlaskForm): _l("Repeat Password"), validators=[DataRequired(), EqualTo("password")] ) submit = SubmitField(_l("Request Password Reset")) + + +class DeactivateUserForm(FlaskForm): + submit = SubmitField("Modifier l'utilisateur") + cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True}) diff --git a/app/auth/models.py b/app/auth/models.py index 314b683303..02b0c5e727 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -5,7 +5,6 @@ import base64 from datetime import datetime, timedelta -import json import os import re from time import time @@ -115,7 +114,7 @@ class User(UserMixin, db.Model): {"reset_password": self.id, "exp": time() + expires_in}, current_app.config["SECRET_KEY"], algorithm="HS256", - ).decode("utf-8") + ) @staticmethod def verify_reset_password_token(token): diff --git a/app/auth/routes.py b/app/auth/routes.py index d13f3e6e25..42cdc8e69d 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -3,7 +3,9 @@ auth.routes.py """ -from flask import render_template, redirect, url_for, current_app, flash, request +from app.scodoc.sco_exceptions import ScoValueError +from flask import current_app, g, flash, render_template +from flask import redirect, url_for, request from flask_login.utils import login_required from werkzeug.urls import url_parse from flask_login import login_user, logout_user, current_user @@ -15,12 +17,13 @@ from app.auth.forms import ( UserCreationForm, ResetPasswordRequestForm, ResetPasswordForm, + DeactivateUserForm, ) from app.auth.models import Permission from app.auth.models import User from app.auth.email import send_password_reset_email from app.decorators import admin_required - +from app.decorators import permission_required _ = lambda x: x # sans babel _l = _ @@ -69,13 +72,23 @@ def create_user(): @bp.route("/reset_password_request", methods=["GET", "POST"]) def reset_password_request(): + """Form demande renvoi de mot de passe par mail + Si l'utilisateur est déjà authentifié, le renvoie simplement sur + la page d'accueil. + """ if current_user.is_authenticated: return redirect(url_for("scodoc.index")) form = ResetPasswordRequestForm() if form.validate_on_submit(): - user = User.query.filter_by(email=form.email.data).first() - if user: - send_password_reset_email(user) + users = User.query.filter_by(email=form.email.data).all() + if len(users) == 1: + send_password_reset_email(users[0]) + elif len(users) > 1: + current_app.logger.info( + "reset_password_request: multiple users with email '{}' (ignoring)".format( + form.email.data + ) + ) else: current_app.logger.info( "reset_password_request: for unkown user '{}'".format(form.email.data) diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py index 27acef3d77..b324d61ec0 100644 --- a/app/scodoc/sco_synchro_etuds.py +++ b/app/scodoc/sco_synchro_etuds.py @@ -255,7 +255,7 @@ def formsemestre_synchro_etuds( url_for("scolar.affectGroups", scodoc_dept=g.scodoc_dept, partition_id=partitions[0]["partition_id"] - )}">Répartir les groupes de partitions[0]["partition_name"] + )}">Répartir les groupes de {partitions[0]["partition_name"]} """ ) diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index 65bdfe2385..6cbaab2cd0 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -343,11 +343,14 @@ def user_info_page(user_name=None): ) if current_user.has_permission(Permission.ScoUsersAdmin, dept): H.append( - """ -
  • modifier ou désactiver ce compte
    - (pour "supprimer" un utilisateur, le rendre inactif via le formulaire) + f""" +
  • modifier ce compte +
  • +
  • {"désactiver" if user.active else "activer"} ce compte
  • - """ % info ) diff --git a/app/templates/auth/toogle_active_user.html b/app/templates/auth/toogle_active_user.html new file mode 100644 index 0000000000..d5ec678d4c --- /dev/null +++ b/app/templates/auth/toogle_active_user.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

    {{ "Désactiver" if u.active else "Activer" }} l'utilisateur {{ u.get_nomplogin() }} ?

    +
    + Dans ScoDoc on ne supprime pas les utilisateurs mais on les rend inactifs: + ils n'apparaissent plus dans les listes et ne peuvent plus se connecter. +
    + Ces utilisateurs peuvent être réactivés à tout moment. +
    +
    +
    + {{ wtf.quick_form(form) }} +
    +
    +{% endblock %} \ No newline at end of file diff --git a/app/views/users.py b/app/views/users.py index b025e870e7..1d150fe201 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -38,11 +38,13 @@ import re from xml.etree import ElementTree import flask -from flask import g, url_for +from flask import g, url_for, request +from flask import redirect, render_template from flask_login import current_user from app import db +from app.auth.forms import DeactivateUserForm from app.auth.models import Permission from app.auth.models import User from app.auth.models import Role @@ -210,7 +212,8 @@ def create_user_form(REQUEST, user_name=None, edit=0): "title": "Mot de passe", "input_type": "password", "size": 14, - "allow_null": False, + "allow_null": True, + "explanation": "optionnel, l'utilisateur pourra le saisir avec son mail", }, ), ( @@ -219,7 +222,7 @@ def create_user_form(REQUEST, user_name=None, edit=0): "title": "Confirmer mot de passe", "input_type": "password", "size": 14, - "allow_null": False, + "allow_null": True, }, ), ] @@ -237,9 +240,9 @@ def create_user_form(REQUEST, user_name=None, edit=0): { "title": "e-mail", "input_type": "text", - "explanation": "vivement recommandé: utilisé pour contacter l'utilisateur", + "explanation": "requis, doit fonctionner", "size": 20, - "allow_null": True, + "allow_null": False, }, ) ] @@ -437,14 +440,17 @@ def create_user_form(REQUEST, user_name=None, edit=0): ) return "\n".join(H) + msg + "\n" + tf[1] + F # check passwords - if vals["passwd"] != vals["passwd2"]: - msg = tf_error_message( - """Les deux mots de passes ne correspondent pas !""" - ) - return "\n".join(H) + msg + "\n" + tf[1] + F - if not sco_users.is_valid_password(vals["passwd"]): - msg = tf_error_message("""Mot de passe trop simple, recommencez !""") - return "\n".join(H) + msg + "\n" + tf[1] + F + if vals["passwd"]: + if vals["passwd"] != vals["passwd2"]: + msg = tf_error_message( + """Les deux mots de passes ne correspondent pas !""" + ) + return "\n".join(H) + msg + "\n" + tf[1] + F + if not sco_users.is_valid_password(vals["passwd"]): + msg = tf_error_message( + """Mot de passe trop simple, recommencez !""" + ) + return "\n".join(H) + msg + "\n" + tf[1] + F if not can_choose_dept: vals["dept"] = auth_dept # ok, go @@ -457,8 +463,12 @@ def create_user_form(REQUEST, user_name=None, edit=0): db.session.add(u) db.session.commit() return flask.redirect( - "user_info_page?user_name=%s&head_message=Nouvel utilisateur créé" - % (user_name) + url_for( + "users.user_info_page", + scodoc_dept=g.scodoc_dept, + user_name=user_name, + head_message="Nouvel utilisateur créé", + ) ) @@ -611,7 +621,9 @@ def form_change_password(REQUEST, user_name=None):

    -

    Vous pouvez aussi: renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur +

    Note: en ScoDoc 9, les utilisateurs peuvent changer eux-même leur mot de passe + en indiquant l'adresse mail associée à leur compte. +

    """ % {"nomplogin": u.get_nomplogin(), "user_name": user_name} ) @@ -676,3 +688,25 @@ def change_password(user_name, password, password2, REQUEST): % scu.ScoURL() ) return html_sco_header.sco_header() + "\n".join(H) + F + + +@bp.route("/toggle_active_user/", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoUsersAdmin) +def toggle_active_user(user_name: str = None): + """Change active status of a user account""" + u = User.query.filter_by(user_name=user_name).first() + if not u: + raise ScoValueError("invalid user_name") + form = DeactivateUserForm() + if ( + request.method == "POST" and form.cancel.data + ): # if cancel button is clicked, the form.cancel.data will be True + # flash + return redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept)) + if form.validate_on_submit(): + u.active = not u.active + db.session.add(u) + db.session.commit() + return redirect(url_for("users.index_html", scodoc_dept=g.scodoc_dept)) + return render_template("auth/toogle_active_user.html", form=form, u=u) diff --git a/requirements-3.9.txt b/requirements-3.9.txt index bfba884cba..0ba9dcbf70 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -32,7 +32,6 @@ iniconfig==1.1.1 isort==5.9.3 itsdangerous==2.0.1 Jinja2==3.0.1 -jwt==1.2.0 lazy-object-proxy==1.6.0 Mako==1.1.4 MarkupSafe==2.0.1 @@ -45,6 +44,7 @@ psycopg2==2.9.1 py==1.10.0 pycparser==2.20 pydot==1.4.2 +PyJWT==2.1.0 pylint==2.9.6 pyOpenSSL==20.0.1 pyparsing==2.4.7