mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-04 19:15:30 +00:00
Merge pull request #1212 from corubba/feature/privacy-first
Privacy first
This commit is contained in:
commit
51249aecd3
@ -7,7 +7,6 @@ SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
|||||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||||
BIND_ADDRESS = '0.0.0.0'
|
BIND_ADDRESS = '0.0.0.0'
|
||||||
PORT = 9191
|
PORT = 9191
|
||||||
OFFLINE_MODE = False
|
|
||||||
|
|
||||||
### DATABASE CONFIG
|
### DATABASE CONFIG
|
||||||
SQLA_DB_USER = 'pda'
|
SQLA_DB_USER = 'pda'
|
||||||
|
@ -50,7 +50,6 @@ legal_envvars = (
|
|||||||
'SAML_LOGOUT',
|
'SAML_LOGOUT',
|
||||||
'SAML_LOGOUT_URL',
|
'SAML_LOGOUT_URL',
|
||||||
'SAML_ASSERTION_ENCRYPTED',
|
'SAML_ASSERTION_ENCRYPTED',
|
||||||
'OFFLINE_MODE',
|
|
||||||
'REMOTE_USER_LOGOUT_URL',
|
'REMOTE_USER_LOGOUT_URL',
|
||||||
'REMOTE_USER_COOKIES',
|
'REMOTE_USER_COOKIES',
|
||||||
'SIGNUP_ENABLED',
|
'SIGNUP_ENABLED',
|
||||||
@ -77,7 +76,6 @@ legal_envvars_bool = (
|
|||||||
'SAML_WANT_MESSAGE_SIGNED',
|
'SAML_WANT_MESSAGE_SIGNED',
|
||||||
'SAML_LOGOUT',
|
'SAML_LOGOUT',
|
||||||
'SAML_ASSERTION_ENCRYPTED',
|
'SAML_ASSERTION_ENCRYPTED',
|
||||||
'OFFLINE_MODE',
|
|
||||||
'REMOTE_USER_ENABLED',
|
'REMOTE_USER_ENABLED',
|
||||||
'SIGNUP_ENABLED',
|
'SIGNUP_ENABLED',
|
||||||
'LOCAL_DB_ENABLED',
|
'LOCAL_DB_ENABLED',
|
||||||
|
@ -15,4 +15,3 @@ services:
|
|||||||
- GUNICORN_TIMEOUT=60
|
- GUNICORN_TIMEOUT=60
|
||||||
- GUNICORN_WORKERS=2
|
- GUNICORN_WORKERS=2
|
||||||
- GUNICORN_LOGLEVEL=DEBUG
|
- GUNICORN_LOGLEVEL=DEBUG
|
||||||
- OFFLINE_MODE=False # True for offline, False for external resources
|
|
||||||
|
@ -74,8 +74,6 @@ def create_app(config=None):
|
|||||||
app.jinja_env.filters['display_record_name'] = utils.display_record_name
|
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_master_name'] = utils.display_master_name
|
||||||
app.jinja_env.filters['display_second_to_time'] = utils.display_time
|
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[
|
app.jinja_env.filters[
|
||||||
'display_setting_state'] = utils.display_setting_state
|
'display_setting_state'] = utils.display_setting_state
|
||||||
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
|
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
|
||||||
@ -93,9 +91,4 @@ def create_app(config=None):
|
|||||||
setting = Setting()
|
setting = Setting()
|
||||||
return dict(SETTING=setting)
|
return dict(SETTING=setting)
|
||||||
|
|
||||||
@app.context_processor
|
|
||||||
def inject_mode():
|
|
||||||
setting = app.config.get('OFFLINE_MODE', False)
|
|
||||||
return dict(OFFLINE_MODE=setting)
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -8,7 +8,6 @@ SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
|||||||
BIND_ADDRESS = '0.0.0.0'
|
BIND_ADDRESS = '0.0.0.0'
|
||||||
PORT = 9191
|
PORT = 9191
|
||||||
HSTS_ENABLED = False
|
HSTS_ENABLED = False
|
||||||
OFFLINE_MODE = False
|
|
||||||
FILESYSTEM_SESSIONS_ENABLED = False
|
FILESYSTEM_SESSIONS_ENABLED = False
|
||||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||||
CSRF_COOKIE_HTTPONLY = True
|
CSRF_COOKIE_HTTPONLY = True
|
||||||
|
@ -2,14 +2,12 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
import hashlib
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import idna
|
import idna
|
||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
|
|
||||||
def auth_from_url(url):
|
def auth_from_url(url):
|
||||||
@ -186,17 +184,6 @@ def pdns_api_extended_uri(version):
|
|||||||
return ""
|
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):
|
def display_setting_state(value):
|
||||||
if value == 1:
|
if value == 1:
|
||||||
return "ON"
|
return "ON"
|
||||||
|
@ -193,7 +193,8 @@ class Setting(db.Model):
|
|||||||
'otp_force': False,
|
'otp_force': False,
|
||||||
'max_history_records': 1000,
|
'max_history_records': 1000,
|
||||||
'deny_domain_override': False,
|
'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):
|
def __init__(self, id=None, name=None, value=None):
|
||||||
|
@ -1259,17 +1259,38 @@ def history_table(): # ajax call data
|
|||||||
@login_required
|
@login_required
|
||||||
@operator_role_required
|
@operator_role_required
|
||||||
def setting_basic():
|
def setting_basic():
|
||||||
if request.method == 'GET':
|
|
||||||
settings = [
|
settings = [
|
||||||
'maintenance', 'fullscreen_layout', 'record_helper',
|
'account_name_extra_chars',
|
||||||
'login_ldap_first', 'default_record_table_size',
|
'allow_user_create_domain',
|
||||||
'default_domain_table_size', 'auto_ptr', 'record_quick_edit',
|
'allow_user_remove_domain',
|
||||||
'pretty_ipv6_ptr', 'dnssec_admins_only',
|
'allow_user_view_history',
|
||||||
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
|
'auto_ptr',
|
||||||
'session_timeout', 'warn_session_timeout', 'ttl_options',
|
'bg_domain_updates',
|
||||||
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
|
'custom_css',
|
||||||
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force',
|
'default_domain_table_size',
|
||||||
'deny_domain_override', 'enforce_api_ttl', 'account_name_extra_chars'
|
'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)
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app
|
import hashlib
|
||||||
|
import imghdr
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
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 flask_login import current_user, login_required, login_manager
|
||||||
|
|
||||||
from ..models.user import User, Anonymous
|
from ..models.user import User, Anonymous
|
||||||
@ -97,3 +102,54 @@ def qrcode():
|
|||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
'Expires': '0'
|
'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_
|
||||||
|
|
||||||
|
def return_image(content, content_type=None):
|
||||||
|
"""Return the given binary image content. Guess the type if not given."""
|
||||||
|
if not content_type:
|
||||||
|
guess = mimetypes.guess_type('example.' + imghdr.what(None, h=content))
|
||||||
|
if guess and guess[0]:
|
||||||
|
content_type = guess[0]
|
||||||
|
|
||||||
|
return content, 200, {'Content-Type': content_type}
|
||||||
|
|
||||||
|
# To prevent "cache poisoning", the username query parameter is required
|
||||||
|
if request.args.get('username', None) != current_user.username:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
setting = Setting()
|
||||||
|
|
||||||
|
if session['authentication_type'] == 'LDAP':
|
||||||
|
search_filter = '(&({0}={1}){2})'.format(setting.get('ldap_filter_username'),
|
||||||
|
current_user.username,
|
||||||
|
setting.get('ldap_filter_basic'))
|
||||||
|
result = User().ldap_search(search_filter, setting.get('ldap_base_dn'))
|
||||||
|
if result and result[0] and result[0][0] and result[0][0][1]:
|
||||||
|
user_obj = result[0][0][1]
|
||||||
|
for key in ['jpegPhoto', 'thumbnailPhoto']:
|
||||||
|
if key in user_obj and user_obj[key] and user_obj[key][0]:
|
||||||
|
current_app.logger.debug(f'Return {key} from ldap as user image')
|
||||||
|
return return_image(user_obj[key][0])
|
||||||
|
|
||||||
|
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')
|
||||||
|
@ -42,15 +42,6 @@ table td {
|
|||||||
background-position: center;
|
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 {
|
.search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
BIN
powerdnsadmin/static/img/user_image.png
Normal file
BIN
powerdnsadmin/static/img/user_image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
@ -35,7 +35,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for statistic in statistics %}
|
{% for statistic in statistics %}
|
||||||
<tr class="odd gradeX">
|
<tr class="odd gradeX">
|
||||||
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ statistic['name'] }}"
|
<td><a href="https://doc.powerdns.com/authoritative/search.html?q={{ statistic['name'] }}"
|
||||||
target="_blank" class="btn btn-flat btn-xs blue"><i
|
target="_blank" class="btn btn-flat btn-xs blue"><i
|
||||||
class="fa fa-search"></i></a></td>
|
class="fa fa-search"></i></a></td>
|
||||||
<td>{{ statistic['name'] }}</td>
|
<td>{{ statistic['name'] }}</td>
|
||||||
@ -70,7 +70,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for config in configs %}
|
{% for config in configs %}
|
||||||
<tr class="odd gradeX">
|
<tr class="odd gradeX">
|
||||||
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ config['name'] }}"
|
<td><a href="https://doc.powerdns.com/authoritative/search.html?q={{ config['name'] }}"
|
||||||
target="_blank" class="btn btn-flat btn-xs blue"><i
|
target="_blank" class="btn btn-flat btn-xs blue"><i
|
||||||
class="fa fa-search"></i></a></td>
|
class="fa fa-search"></i></a></td>
|
||||||
<td>{{ config['name'] }}</td>
|
<td>{{ config['name'] }}</td>
|
||||||
|
@ -8,13 +8,8 @@
|
|||||||
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
|
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
|
||||||
<!-- Get Google Fonts we like -->
|
<!-- Get Google Fonts we like -->
|
||||||
{% if OFFLINE_MODE %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/roboto_mono.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/roboto_mono.css') }}">
|
||||||
{% else %}
|
|
||||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
|
|
||||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
|
|
||||||
{% endif %}
|
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
<!-- Tell Safari to not recognise telephone numbers -->
|
<!-- Tell Safari to not recognise telephone numbers -->
|
||||||
@ -25,20 +20,10 @@
|
|||||||
{% if SETTING.get('custom_css') %}
|
{% if SETTING.get('custom_css') %}
|
||||||
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
|
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
|
||||||
{% if OFFLINE_MODE %}
|
{% set user_image_url = url_for('user.image', username=current_user.username) %}
|
||||||
{% 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 %}
|
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
{% block pageheader %}
|
{% block pageheader %}
|
||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
@ -62,14 +47,14 @@
|
|||||||
<!-- User Account: style can be found in dropdown.less -->
|
<!-- User Account: style can be found in dropdown.less -->
|
||||||
<li class="dropdown user user-menu">
|
<li class="dropdown user user-menu">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
<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">
|
<span class="hidden-xs">
|
||||||
{{ current_user.firstname }}
|
{{ current_user.firstname }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li class="user-header">
|
<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>
|
<p>
|
||||||
{{ current_user.firstname }} {{ current_user.lastname }}
|
{{ current_user.firstname }} {{ current_user.lastname }}
|
||||||
<small>{{ current_user.role.name }}</small>
|
<small>{{ current_user.role.name }}</small>
|
||||||
@ -100,7 +85,7 @@
|
|||||||
{% if current_user.id is defined %}
|
{% if current_user.id is defined %}
|
||||||
<div class="user-panel">
|
<div class="user-panel">
|
||||||
<div class="pull-left image">
|
<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>
|
||||||
<div class="pull-left info">
|
<div class="pull-left info">
|
||||||
<p>{{ current_user.firstname }} {{ current_user.lastname }}</p>
|
<p>{{ current_user.firstname }} {{ current_user.lastname }}</p>
|
||||||
|
@ -15,12 +15,6 @@
|
|||||||
{% if SETTING.get('custom_css') %}
|
{% if SETTING.get('custom_css') %}
|
||||||
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="hold-transition login-page">
|
<body class="hold-transition login-page">
|
||||||
|
@ -11,13 +11,6 @@
|
|||||||
{% assets "css_login" -%}
|
{% assets "css_login" -%}
|
||||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
{%- endassets %}
|
{%- endassets %}
|
||||||
|
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="hold-transition register-page">
|
<body class="hold-transition register-page">
|
||||||
|
Loading…
Reference in New Issue
Block a user