Améliore formulaires gestion utilisateurs

This commit is contained in:
Emmanuel Viennet 2021-08-28 16:01:41 +02:00
parent dcd4d3bcbd
commit c48c52f7aa
9 changed files with 101 additions and 30 deletions

View File

@ -7,7 +7,7 @@ from app.email import send_email
def send_password_reset_email(user): def send_password_reset_email(user):
token = user.get_reset_password_token() token = user.get_reset_password_token()
send_email( send_email(
"[ScoDoc] Reset Your Password", "[ScoDoc] Réinitialisation de votre mot de passe",
sender=current_app.config["ADMINS"][0], sender=current_app.config["ADMINS"][0],
recipients=[user.email], recipients=[user.email],
text_body=render_template("email/reset_password.txt", user=user, token=token), text_body=render_template("email/reset_password.txt", user=user, token=token),

View File

@ -53,3 +53,8 @@ class ResetPasswordForm(FlaskForm):
_l("Repeat Password"), validators=[DataRequired(), EqualTo("password")] _l("Repeat Password"), validators=[DataRequired(), EqualTo("password")]
) )
submit = SubmitField(_l("Request Password Reset")) submit = SubmitField(_l("Request Password Reset"))
class DeactivateUserForm(FlaskForm):
submit = SubmitField("Modifier l'utilisateur")
cancel = SubmitField(label="Annuler", render_kw={"formnovalidate": True})

View File

@ -5,7 +5,6 @@
import base64 import base64
from datetime import datetime, timedelta from datetime import datetime, timedelta
import json
import os import os
import re import re
from time import time from time import time
@ -115,7 +114,7 @@ class User(UserMixin, db.Model):
{"reset_password": self.id, "exp": time() + expires_in}, {"reset_password": self.id, "exp": time() + expires_in},
current_app.config["SECRET_KEY"], current_app.config["SECRET_KEY"],
algorithm="HS256", algorithm="HS256",
).decode("utf-8") )
@staticmethod @staticmethod
def verify_reset_password_token(token): def verify_reset_password_token(token):

View File

@ -3,7 +3,9 @@
auth.routes.py 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 flask_login.utils import login_required
from werkzeug.urls import url_parse from werkzeug.urls import url_parse
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
@ -15,12 +17,13 @@ from app.auth.forms import (
UserCreationForm, UserCreationForm,
ResetPasswordRequestForm, ResetPasswordRequestForm,
ResetPasswordForm, ResetPasswordForm,
DeactivateUserForm,
) )
from app.auth.models import Permission from app.auth.models import Permission
from app.auth.models import User from app.auth.models import User
from app.auth.email import send_password_reset_email from app.auth.email import send_password_reset_email
from app.decorators import admin_required from app.decorators import admin_required
from app.decorators import permission_required
_ = lambda x: x # sans babel _ = lambda x: x # sans babel
_l = _ _l = _
@ -69,13 +72,23 @@ def create_user():
@bp.route("/reset_password_request", methods=["GET", "POST"]) @bp.route("/reset_password_request", methods=["GET", "POST"])
def reset_password_request(): 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: if current_user.is_authenticated:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
form = ResetPasswordRequestForm() form = ResetPasswordRequestForm()
if form.validate_on_submit(): if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first() users = User.query.filter_by(email=form.email.data).all()
if user: if len(users) == 1:
send_password_reset_email(user) 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: else:
current_app.logger.info( current_app.logger.info(
"reset_password_request: for unkown user '{}'".format(form.email.data) "reset_password_request: for unkown user '{}'".format(form.email.data)

View File

@ -255,7 +255,7 @@ def formsemestre_synchro_etuds(
url_for("scolar.affectGroups", url_for("scolar.affectGroups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=partitions[0]["partition_id"] partition_id=partitions[0]["partition_id"]
)}">Répartir les groupes de partitions[0]["partition_name"]</a></li> )}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>
""" """
) )

View File

@ -343,11 +343,14 @@ def user_info_page(user_name=None):
) )
if current_user.has_permission(Permission.ScoUsersAdmin, dept): if current_user.has_permission(Permission.ScoUsersAdmin, dept):
H.append( H.append(
""" f"""
<li><a class="stdlink" href="create_user_form?user_name=%(user_name)s&edit=1">modifier ou désactiver ce compte</a><br/> <li><a class="stdlink" href="{url_for('users.create_user_form', scodoc_dept=g.scodoc_dept,
<em>(pour "supprimer" un utilisateur, le rendre inactif via le formulaire)</em> user_name=user.user_name, edit=1)}">modifier ce compte</a>
</li>
<li><a class="stdlink" href="{url_for('users.toggle_active_user', scodoc_dept=g.scodoc_dept,
user_name=user.user_name)
}">{"désactiver" if user.active else "activer"} ce compte</a>
</li> </li>
""" """
% info % info
) )

View File

@ -0,0 +1,17 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>{{ "Désactiver" if u.active else "Activer" }} l'utilisateur {{ u.get_nomplogin() }} ?</h1>
<div class="help">
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.
<br />
Ces utilisateurs peuvent être réactivés à tout moment.
</div>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -38,11 +38,13 @@ import re
from xml.etree import ElementTree from xml.etree import ElementTree
import flask 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 flask_login import current_user
from app import db from app import db
from app.auth.forms import DeactivateUserForm
from app.auth.models import Permission from app.auth.models import Permission
from app.auth.models import User from app.auth.models import User
from app.auth.models import Role from app.auth.models import Role
@ -210,7 +212,8 @@ def create_user_form(REQUEST, user_name=None, edit=0):
"title": "Mot de passe", "title": "Mot de passe",
"input_type": "password", "input_type": "password",
"size": 14, "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", "title": "Confirmer mot de passe",
"input_type": "password", "input_type": "password",
"size": 14, "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", "title": "e-mail",
"input_type": "text", "input_type": "text",
"explanation": "vivement recommandé: utilisé pour contacter l'utilisateur", "explanation": "requis, doit fonctionner",
"size": 20, "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 return "\n".join(H) + msg + "\n" + tf[1] + F
# check passwords # check passwords
if vals["passwd"] != vals["passwd2"]: if vals["passwd"]:
msg = tf_error_message( if vals["passwd"] != vals["passwd2"]:
"""Les deux mots de passes ne correspondent pas !""" 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"]): return "\n".join(H) + msg + "\n" + tf[1] + F
msg = tf_error_message("""Mot de passe trop simple, recommencez !""") if not sco_users.is_valid_password(vals["passwd"]):
return "\n".join(H) + msg + "\n" + tf[1] + F msg = tf_error_message(
"""Mot de passe trop simple, recommencez !"""
)
return "\n".join(H) + msg + "\n" + tf[1] + F
if not can_choose_dept: if not can_choose_dept:
vals["dept"] = auth_dept vals["dept"] = auth_dept
# ok, go # ok, go
@ -457,8 +463,12 @@ def create_user_form(REQUEST, user_name=None, edit=0):
db.session.add(u) db.session.add(u)
db.session.commit() db.session.commit()
return flask.redirect( return flask.redirect(
"user_info_page?user_name=%s&head_message=Nouvel utilisateur créé" url_for(
% (user_name) "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):
<input type="hidden" value="%(user_name)s" name="user_name"> <input type="hidden" value="%(user_name)s" name="user_name">
<input type="submit" value="Changer"> <input type="submit" value="Changer">
</p> </p>
<p>Vous pouvez aussi: <a class="stdlink" href="reset_password_form?user_name=%(user_name)s">renvoyer un mot de passe aléatoire temporaire par mail à l'utilisateur</a> <p class="help">Note: en ScoDoc 9, les utilisateurs peuvent changer eux-même leur mot de passe
en indiquant l'adresse mail associée à leur compte.
</p>
""" """
% {"nomplogin": u.get_nomplogin(), "user_name": user_name} % {"nomplogin": u.get_nomplogin(), "user_name": user_name}
) )
@ -676,3 +688,25 @@ def change_password(user_name, password, password2, REQUEST):
% scu.ScoURL() % scu.ScoURL()
) )
return html_sco_header.sco_header() + "\n".join(H) + F return html_sco_header.sco_header() + "\n".join(H) + F
@bp.route("/toggle_active_user/<user_name>", 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)

View File

@ -32,7 +32,6 @@ iniconfig==1.1.1
isort==5.9.3 isort==5.9.3
itsdangerous==2.0.1 itsdangerous==2.0.1
Jinja2==3.0.1 Jinja2==3.0.1
jwt==1.2.0
lazy-object-proxy==1.6.0 lazy-object-proxy==1.6.0
Mako==1.1.4 Mako==1.1.4
MarkupSafe==2.0.1 MarkupSafe==2.0.1
@ -45,6 +44,7 @@ psycopg2==2.9.1
py==1.10.0 py==1.10.0
pycparser==2.20 pycparser==2.20
pydot==1.4.2 pydot==1.4.2
PyJWT==2.1.0
pylint==2.9.6 pylint==2.9.6
pyOpenSSL==20.0.1 pyOpenSSL==20.0.1
pyparsing==2.4.7 pyparsing==2.4.7