Merge remote-tracking branch 'upstream/master' into update_deps
commit
13e6793c9f
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
sso = Blueprint('sso', __name__, static_folder=None, template_folder='templates')
|
||||
|
||||
from mailu.sso.views import *
|
@ -0,0 +1,11 @@
|
||||
from wtforms import validators, fields
|
||||
from flask_babel import lazy_gettext as _
|
||||
import flask_wtf
|
||||
|
||||
class LoginForm(flask_wtf.FlaskForm):
|
||||
class Meta:
|
||||
csrf = False
|
||||
email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()])
|
||||
pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
|
||||
submitAdmin = fields.SubmitField(_('Sign in'))
|
||||
submitWebmail = fields.SubmitField(_('Sign in'))
|
@ -0,0 +1,86 @@
|
||||
{%- import "macros.html" as macros %}
|
||||
{%- import "bootstrap/utils.html" as utils %}
|
||||
<!doctype html>
|
||||
<html lang="{{ session['language'] }}" data-static="/static/">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="{% trans %}Admin page for{% endtrans %} {{ config["SITENAME"] }}">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<title>Mailu-Admin | {{ config["SITENAME"] }}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<nav class="main-header navbar navbar-expand navbar-white navbar-light">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars" title="{% trans %}toggle sidebar{% endtrans %}" aria-expanded="false"></i><span class="sr-only">{% trans %}toggle sidebar{% endtrans %}</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{%- for page, url in path %}
|
||||
{%- if loop.index > 1 %}
|
||||
<i class="fas fa-greater-than text-xs text-gray" aria-hidden="true"></i>
|
||||
{%- endif %}
|
||||
{%- if url %}
|
||||
<a class="nav-link d-inline-block" href="{{ url }}" role="button">{{ page }}</a>
|
||||
{%- else %}
|
||||
<span class="nav-link d-inline-block">{{ page }}</span>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#" aria-expanded="false">
|
||||
<i class="fas fa-language text-xl" aria-hidden="true" title="{% trans %}change language{% endtrans %}"></i><span class="sr-only">Language</span>
|
||||
<span class="badge badge-primary navbar-badge">{{ session['language'] }}</span></a>
|
||||
<div class="dropdown-menu dropdown-menu-right p-0" id="mailu-languages">
|
||||
{%- for locale in config.translations.values() %}
|
||||
<a class="dropdown-item{% if locale|string() == session['language'] %} active{% endif %}" href="{{ url_for('sso.set_language', language=locale) }}">{{ locale.get_language_name().title() }}</a>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<aside class="main-sidebar sidebar-dark-primary nav-compact elevation-4">
|
||||
<a class="brand-link bg-mailu-logo"{% if config["LOGO_BACKGROUND"] %} style="background-color:{{ config["LOGO_BACKGROUND"] }}!important;"{% endif %}>
|
||||
<img src="{{ config["LOGO_URL"] if config["LOGO_URL"] else '/static/mailu.png' }}" width="33" height="33" alt="Mailu" class="brand-image mailu-logo img-circle elevation-3">
|
||||
<span class="brand-text font-weight-light">{{ config["SITENAME"] }}</span>
|
||||
</a>
|
||||
{%- include "sidebar_sso.html" %}
|
||||
</aside>
|
||||
<div class="content-wrapper text-sm">
|
||||
<section class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0">{%- block title %}{%- endblock %}</h1>
|
||||
<small>{% block subtitle %}{% endblock %}</small>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{%- block main_action %}{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="content">
|
||||
{{ utils.flashed_messages(container=False, default_category='success') }}
|
||||
{%- block content %}{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="main-footer">
|
||||
Built with <i class="fa fa-heart text-danger" aria-hidden="true"></i><span class="sr-only">love</span>
|
||||
using <a href="https://flask.palletsprojects.com/">Flask</a>
|
||||
and <a href="https://adminlte.io/themes/v3/index3.html">AdminLTE</a>.
|
||||
<span class="fa-pull-right">
|
||||
<i class="fa fa-code-branch" aria-hidden="true"></i><span class="sr-only">fork</span>
|
||||
on <a href="https://github.com/Mailu/Mailu">Github</a>
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='vendor.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,11 @@
|
||||
{%- extends "base_sso.html" %}
|
||||
|
||||
{%- block content %}
|
||||
{%- call macros.card() %}
|
||||
<form class="form" method="post" role="form">
|
||||
{{ macros.form_field(form.email) }}
|
||||
{{ macros.form_field(form.pw) }}
|
||||
{{ macros.form_fields(fields, label=False, class="btn btn-default", spacing=False) }}
|
||||
</form>
|
||||
{%- endcall %}
|
||||
{%- endblock %}
|
@ -0,0 +1,5 @@
|
||||
{%- extends "form_sso.html" %}
|
||||
|
||||
{%- block title %}
|
||||
{% trans %}Sign in{% endtrans %}
|
||||
{%- endblock %}
|
@ -0,0 +1,55 @@
|
||||
<div class="sidebar text-sm">
|
||||
<nav class="mt-2">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" role="menu">
|
||||
<li class="nav-header text-uppercase text-primary" role="none">{% trans %}Go to{% endtrans %}</li>
|
||||
{%- if config['ADMIN'] %}
|
||||
<li class="nav-item">
|
||||
<a href="{{ url_for('ui.client') }}" class="nav-link">
|
||||
<i class="nav-icon fa fa-laptop"></i>
|
||||
<p class="text">{% trans %}Client setup{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
{%- endif %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ config["WEBSITE"] }}" target="_blank" class="nav-link" role="menuitem" rel="noreferrer">
|
||||
<i class="nav-icon fa fa-globe"></i>
|
||||
<p>{% trans %}Website{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" role="none">
|
||||
<a href="https://mailu.io" target="_blank" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-life-ring"></i>
|
||||
<p class="text">{% trans %}Help{% endtrans %} <i class="fas fa-external-link-alt text-xs"></i></p>
|
||||
</a>
|
||||
</li>
|
||||
{#-
|
||||
Domain self-registration is only available when
|
||||
- Admin is available
|
||||
- Domain Self-registration is enabled
|
||||
- The current user is not logged on
|
||||
#}
|
||||
{%- if config['DOMAIN_REGISTRATION'] and not current_user.is_authenticated and config['ADMIN'] %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('ui.domain_signup') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-plus-square"></i>
|
||||
<p class="text">{% trans %}Register a domain{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
{%- endif %}
|
||||
{#-
|
||||
User self-registration is only available when
|
||||
- Admin is available
|
||||
- Self-registration is enabled
|
||||
- The current user is not logged on
|
||||
#}
|
||||
{%- if not current_user.is_authenticated and signup_domains and config['ADMIN'] %}
|
||||
<li class="nav-item" role="none">
|
||||
<a href="{{ url_for('ui.user_signup') }}" class="nav-link" role="menuitem">
|
||||
<i class="nav-icon fa fa-user-plus"></i>
|
||||
<p class="text">{% trans %}Sign up{% endtrans %}</p>
|
||||
</a>
|
||||
</li>
|
||||
{%- endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
__all__ = [
|
||||
'base', 'languages'
|
||||
]
|
@ -0,0 +1,56 @@
|
||||
from werkzeug.utils import redirect
|
||||
from mailu import models, utils
|
||||
from mailu.sso import sso, forms
|
||||
from mailu.ui import access
|
||||
|
||||
from flask import current_app as app
|
||||
import flask
|
||||
import flask_login
|
||||
|
||||
@sso.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
|
||||
form = forms.LoginForm()
|
||||
form.submitAdmin.label.text = form.submitAdmin.label.text + ' Admin'
|
||||
form.submitWebmail.label.text = form.submitWebmail.label.text + ' Webmail'
|
||||
|
||||
fields = []
|
||||
if str(app.config["ADMIN"]).upper() != "FALSE":
|
||||
fields.append(form.submitAdmin)
|
||||
if str(app.config["WEBMAIL"]).upper() != "NONE":
|
||||
fields.append(form.submitWebmail)
|
||||
|
||||
if form.validate_on_submit():
|
||||
if form.submitAdmin.data:
|
||||
destination = app.config['WEB_ADMIN']
|
||||
elif form.submitWebmail.data:
|
||||
destination = app.config['WEB_WEBMAIL']
|
||||
device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit'))
|
||||
username = form.email.data
|
||||
if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip):
|
||||
flask.flash('Too many attempts from your IP (rate-limit)', 'error')
|
||||
return flask.render_template('login.html', form=form)
|
||||
if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username):
|
||||
flask.flash('Too many attempts for this user (rate-limit)', 'error')
|
||||
return flask.render_template('login.html', form=form)
|
||||
user = models.User.login(username, form.pw.data)
|
||||
if user:
|
||||
flask.session.regenerate()
|
||||
flask_login.login_user(user)
|
||||
response = flask.redirect(destination)
|
||||
response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'))
|
||||
flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.')
|
||||
return response
|
||||
else:
|
||||
utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip)
|
||||
flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.')
|
||||
flask.flash('Wrong e-mail or password', 'error')
|
||||
return flask.render_template('login.html', form=form, fields=fields)
|
||||
|
||||
@sso.route('/logout', methods=['GET'])
|
||||
@access.authenticated
|
||||
def logout():
|
||||
flask_login.logout_user()
|
||||
flask.session.destroy()
|
||||
return flask.redirect(flask.url_for('.login'))
|
||||
|
@ -0,0 +1,7 @@
|
||||
from mailu.sso import sso
|
||||
import flask
|
||||
|
||||
@sso.route('/language/<language>', methods=['POST'])
|
||||
def set_language(language=None):
|
||||
flask.session['language'] = language
|
||||
return flask.Response(status=200)
|
@ -1,6 +1,6 @@
|
||||
from flask import Blueprint
|
||||
|
||||
|
||||
ui = Blueprint('ui', __name__, static_folder='static', template_folder='templates')
|
||||
ui = Blueprint('ui', __name__, static_folder=None, template_folder='templates')
|
||||
|
||||
from mailu.ui.views import *
|
||||
|
@ -1,18 +0,0 @@
|
||||
{%- extends "form.html" %}
|
||||
|
||||
{%- block title %}
|
||||
{% trans %}Sign in{% endtrans %}
|
||||
{%- endblock %}
|
||||
|
||||
{%- block subtitle %}
|
||||
{% trans %}to access the administration tools{% endtrans %}
|
||||
{%- endblock %}
|
||||
|
||||
{%- block content %}
|
||||
{% if config["SESSION_COOKIE_SECURE"] %}
|
||||
<div id="login_needs_https" class="alert alert-danger d-none" role="alert">
|
||||
{% trans %}The login form has been disabled as <b>SESSION_COOKIE_SECURE</b> is on but you are accessing Mailu over HTTP.{% endtrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ super() }}
|
||||
{%- endblock %}
|
@ -0,0 +1,3 @@
|
||||
{% if RELAYNETS %}
|
||||
local_networks = [{{ RELAYNETS }}];
|
||||
{% endif %}
|
@ -0,0 +1,4 @@
|
||||
apply {
|
||||
# see https://github.com/Mailu/Mailu/issues/1705
|
||||
RCVD_NO_TLS_LAST = 0;
|
||||
}
|
@ -0,0 +1 @@
|
||||
Ensure that RCVD_NO_TLS_LAST doesn't add to the spam score (as TLS usage can't be determined)
|
@ -0,0 +1,10 @@
|
||||
Improved the SSO page. Warning! The new endpoints /sso and /static are introduced.
|
||||
These endpoints are now used for handling sign on requests and shared static files.
|
||||
You may want to update your reverse proxy to proxy /sso and /static to Mailu (to the front service).
|
||||
The example section of using a reverse proxy is updated with this information.
|
||||
- New SSO page is used for logging in Admin or Webmail.
|
||||
- Made SSO page available separately. SSO page can now be used without Admin accessible (ADMIN=false).
|
||||
- Introduced stub /static which is used by all sites for accessing static files.
|
||||
- Removed the /admin/ prefix to reduce complexity of routing with Mailu. Admin is accessible directly via /admin instead of /admin/ui
|
||||
Note: Failed logon attempts are logged in the logs of admin. You can watch this with fail2ban.
|
||||
|
@ -0,0 +1 @@
|
||||
RELAYNETS should be a comma separated list of networks
|
Loading…
Reference in New Issue