Rework user image handling

Moved all the logic out of the template into a separate endpoint. This
makes it easy to extend to also support images from different sources
like LDAP/SAML/OIDC. Session-based caching is hard to do, so to allow
time-based caching in the browser, the url needs to be unique for every
user by using a query parameter.

Replaced the default/fallback user image with a new one. It is based on
the old one, but does not need css to be visible. And removed said css.

Gravatar has now its own setting named `gravatar_enabled`, which is
disabled by default.
This commit is contained in:
corubba 2022-05-27 13:01:46 +02:00
parent b795f1eadf
commit 607caa1a2d
9 changed files with 75 additions and 48 deletions

View File

@ -100,8 +100,6 @@ def create_app(config=None):
app.jinja_env.filters['display_record_name'] = utils.display_record_name
app.jinja_env.filters['display_master_name'] = utils.display_master_name
app.jinja_env.filters['display_second_to_time'] = utils.display_time
app.jinja_env.filters[
'email_to_gravatar_url'] = utils.email_to_gravatar_url
app.jinja_env.filters[
'display_setting_state'] = utils.display_setting_state
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name

View File

@ -2,14 +2,12 @@ import logging
import re
import json
import requests
import hashlib
import ipaddress
import idna
from collections.abc import Iterable
from distutils.version import StrictVersion
from urllib.parse import urlparse
from datetime import datetime, timedelta
def auth_from_url(url):
@ -186,17 +184,6 @@ def pdns_api_extended_uri(version):
return ""
def email_to_gravatar_url(email="", size=100):
"""
AD doesn't necessarily have email
"""
if email is None:
email = ""
hash_string = hashlib.md5(email.encode('utf-8')).hexdigest()
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size)
def display_setting_state(value):
if value == 1:
return "ON"

View File

@ -193,7 +193,8 @@ class Setting(db.Model):
'otp_force': False,
'max_history_records': 1000,
'deny_domain_override': False,
'account_name_extra_chars': False
'account_name_extra_chars': False,
'gravatar_enabled': False,
}
def __init__(self, id=None, name=None, value=None):

View File

@ -1259,20 +1259,41 @@ def history_table(): # ajax call data
@login_required
@operator_role_required
def setting_basic():
if request.method == 'GET':
settings = [
'maintenance', 'fullscreen_layout', 'record_helper',
'login_ldap_first', 'default_record_table_size',
'default_domain_table_size', 'auto_ptr', 'record_quick_edit',
'pretty_ipv6_ptr', 'dnssec_admins_only',
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
'session_timeout', 'warn_session_timeout', 'ttl_options',
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force',
'deny_domain_override', 'enforce_api_ttl', 'account_name_extra_chars'
]
settings = [
'account_name_extra_chars',
'allow_user_create_domain',
'allow_user_remove_domain',
'allow_user_view_history',
'auto_ptr',
'bg_domain_updates',
'custom_css',
'default_domain_table_size',
'default_record_table_size',
'delete_sso_accounts',
'deny_domain_override',
'dnssec_admins_only',
'enable_api_rr_history',
'enforce_api_ttl',
'fullscreen_layout',
'gravatar_enabled',
'login_ldap_first',
'maintenance',
'max_history_records',
'otp_field_enabled',
'otp_force',
'pdns_api_timeout',
'pretty_ipv6_ptr',
'record_helper',
'record_quick_edit',
'session_timeout',
'site_name',
'ttl_options',
'verify_ssl_connections',
'verify_user_email',
'warn_session_timeout',
]
return render_template('admin_setting_basic.html', settings=settings)
return render_template('admin_setting_basic.html', settings=settings)
@admin_bp.route('/setting/basic/<path:setting>/edit', methods=['POST'])

View File

@ -1,5 +1,8 @@
import datetime
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app
import hashlib
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, \
current_app, after_this_request, abort
from flask_login import current_user, login_required, login_manager
from ..models.user import User, Anonymous
@ -96,4 +99,34 @@ def qrcode():
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}
@user_bp.route('/image', methods=['GET'])
@login_required
def image():
"""Returns the user profile image or avatar."""
@after_this_request
def add_cache_headers(response_):
"""When the response is ok, add cache headers."""
if 200 <= response_.status_code <= 399:
response_.cache_control.private = True
response_.cache_control.max_age = int(datetime.timedelta(days=1).total_seconds())
return response_
# To prevent "cache poisoning", the username query parameter is required
if request.args.get('username', None) != current_user.username:
abort(400)
setting = Setting()
email = current_user.email
if email and setting.get('gravatar_enabled'):
hash_ = hashlib.md5(email.encode('utf-8')).hexdigest()
url = f'https://s.gravatar.com/avatar/{hash_}?s=100'
current_app.logger.debug('Redirect user image request to gravatar')
return redirect(url, 307)
# Fallback to the local default image
return current_app.send_static_file('img/user_image.png')

View File

@ -42,15 +42,6 @@ table td {
background-position: center;
}
.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img.img-circle.offline {
filter: brightness(0);
border-color: black;
}
.navbar-nav>.user-menu .user-image.offline {
filter: brightness(0);
}
.search-input {
width: 100%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -23,11 +23,7 @@
{% endblock %}
</head>
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
{% if OFFLINE_MODE %}
{% set gravatar_url = url_for('static', filename='img/gravatar.png') %}
{% elif current_user.email is defined %}
{% set gravatar_url = current_user.email|email_to_gravatar_url(size=80) %}
{% endif %}
{% set user_image_url = url_for('user.image', username=current_user.username) %}
<div class="wrapper">
{% block pageheader %}
<header class="main-header">
@ -51,14 +47,14 @@
<!-- User Account: style can be found in dropdown.less -->
<li class="dropdown user user-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<img src="{{ gravatar_url }}" class="user-image {{ 'offline' if OFFLINE_MODE }}" alt="User Image"/>
<img src="{{ user_image_url }}" class="user-image" alt="User Image"/>
<span class="hidden-xs">
{{ current_user.firstname }}
</span>
</a>
<ul class="dropdown-menu">
<li class="user-header">
<img src="{{ gravatar_url }}" class="img-circle {{ 'offline' if OFFLINE_MODE }}" alt="User Image"/>
<img src="{{ user_image_url }}" class="img-circle" alt="User Image"/>
<p>
{{ current_user.firstname }} {{ current_user.lastname }}
<small>{{ current_user.role.name }}</small>
@ -89,7 +85,7 @@
{% if current_user.id is defined %}
<div class="user-panel">
<div class="pull-left image">
<img src="{{ gravatar_url }}" class="img-circle" alt="User Image"/>
<img src="{{ user_image_url }}" class="img-circle" alt="User Image"/>
</div>
<div class="pull-left info">
<p>{{ current_user.firstname }} {{ current_user.lastname }}</p>