mirror of
https://github.com/outbackdingo/Mailu.git
synced 2026-01-27 18:19:36 +00:00
Implement a 'force-password-change' feature
This commit is contained in:
@@ -523,6 +523,7 @@ class User(Base, Email):
|
||||
spam_enabled = db.Column(db.Boolean, nullable=False, default=True)
|
||||
spam_mark_as_read = db.Column(db.Boolean, nullable=False, default=True)
|
||||
spam_threshold = db.Column(db.Integer, nullable=False, default=lambda:int(app.config.get("DEFAULT_SPAM_THRESHOLD", 80)))
|
||||
change_pw_next_login = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
# Flask-login attributes
|
||||
is_authenticated = True
|
||||
|
||||
@@ -10,3 +10,10 @@ class LoginForm(flask_wtf.FlaskForm):
|
||||
pwned = fields.HiddenField(label='', default=-1)
|
||||
submitWebmail = fields.SubmitField(_('Sign in'))
|
||||
submitAdmin = fields.SubmitField(_('Sign in'))
|
||||
|
||||
class PWChangeForm(flask_wtf.FlaskForm):
|
||||
oldpw = fields.PasswordField(_('Current password'), [validators.DataRequired()])
|
||||
pw = fields.PasswordField(_('New password'), [validators.DataRequired()])
|
||||
pw2 = fields.PasswordField(_('New password (again)'), [validators.DataRequired()])
|
||||
pwned = fields.HiddenField(label='', default=-1)
|
||||
submit = fields.SubmitField(_('Change password'))
|
||||
|
||||
13
core/admin/mailu/sso/templates/pw_change.html
Normal file
13
core/admin/mailu/sso/templates/pw_change.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{%- extends "base_sso.html" %}
|
||||
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ macros.form_field(form.oldpw) }}
|
||||
{{ macros.form_field(form.pw) }}
|
||||
{{ macros.form_field(form.pw2) }}
|
||||
{{ macros.form_field(form.submit) }}
|
||||
</form>
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
||||
@@ -3,7 +3,7 @@ from mailu import models, utils
|
||||
from mailu.sso import sso, forms
|
||||
from mailu.ui import access
|
||||
|
||||
from flask import current_app as app
|
||||
from flask import current_app as app, session
|
||||
import flask
|
||||
import flask_login
|
||||
import secrets
|
||||
@@ -54,6 +54,9 @@ def login():
|
||||
if user:
|
||||
flask.session.regenerate()
|
||||
flask_login.login_user(user)
|
||||
if user.change_pw_next_login:
|
||||
session['redirect_to'] = destination
|
||||
destination = flask.url_for('sso.pw_change')
|
||||
response = flask.redirect(destination)
|
||||
response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True)
|
||||
flask.current_app.logger.info(f'Login attempt for: {username}/sso/{flask.request.headers.get("X-Forwarded-Proto")} from: {client_ip}/{client_port}: success: password: {form.pwned.data}')
|
||||
@@ -66,6 +69,41 @@ def login():
|
||||
flask.flash('Wrong e-mail or password', 'error')
|
||||
return flask.render_template('login.html', form=form, fields=fields)
|
||||
|
||||
@sso.route('/pw_change', methods=['GET', 'POST'])
|
||||
@access.authenticated
|
||||
def pw_change():
|
||||
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
|
||||
client_port = flask.request.headers.get('X-Real-Port', None)
|
||||
form = forms.PWChangeForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
if msg := utils.isBadOrPwned(form):
|
||||
flask.flash(msg, "error")
|
||||
return flask.redirect(flask.url_for('sso.pw_change'))
|
||||
if form.oldpw.data == form.pw2.data:
|
||||
# TODO: fuzzy match?
|
||||
flask.flash("The new password can't be the same as the old password", "error")
|
||||
return flask.redirect(flask.url_for('sso.pw_change'))
|
||||
if form.pw.data != form.pw2.data:
|
||||
flask.flash("The new passwords don't match", "error")
|
||||
return flask.redirect(flask.url_for('sso.pw_change'))
|
||||
user = models.User.login(flask_login.current_user.email, form.oldpw.data)
|
||||
if user:
|
||||
flask.session.regenerate()
|
||||
flask_login.login_user(user)
|
||||
user.set_password(form.pw.data)
|
||||
user.change_pw_next_login = False
|
||||
models.db.session.commit()
|
||||
flask.current_app.logger.info(f'Forced password change by {user} from: {client_ip}/{client_port}: success: password: {form.pwned.data}')
|
||||
destination = app.config['WEB_ADMIN']
|
||||
if 'redir_to' in session:
|
||||
destination = session['redir_to']
|
||||
del session['redir_to']
|
||||
return flask.redirect(destination)
|
||||
flask.flash("The current password is incorrect!", "error")
|
||||
|
||||
return flask.render_template('pw_change.html', form=form)
|
||||
|
||||
@sso.route('/logout', methods=['GET'])
|
||||
@access.authenticated
|
||||
def logout():
|
||||
|
||||
@@ -99,6 +99,7 @@ class UserForm(flask_wtf.FlaskForm):
|
||||
displayed_name = fields.StringField(_('Displayed name'))
|
||||
comment = fields.StringField(_('Comment'))
|
||||
enabled = fields.BooleanField(_('Enabled'), default=True)
|
||||
change_pw_next_login = fields.BooleanField(_('Force password change at next login'), default=True)
|
||||
submit = fields.SubmitField(_('Save'))
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
{{ macros.form_field(form.displayed_name) }}
|
||||
{{ macros.form_field(form.comment) }}
|
||||
{{ macros.form_field(form.enabled) }}
|
||||
{{ macros.form_field(form.change_pw_next_login) }}
|
||||
{%- endcall %}
|
||||
|
||||
{%- call macros.card(_("Features and quotas"), theme="success") %}
|
||||
|
||||
Reference in New Issue
Block a user