mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2025-01-07 19:05:39 +00:00
Merge pull request #156 from petersipos/feature/automatic-reverse-domain-creation
Feature/automatic reverse domain creation
This commit is contained in:
commit
b6ed658cbd
125
app/models.py
125
app/models.py
@ -7,8 +7,11 @@ import urlparse
|
|||||||
import itertools
|
import itertools
|
||||||
import traceback
|
import traceback
|
||||||
import pyotp
|
import pyotp
|
||||||
|
import re
|
||||||
|
import dns.reversename
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from distutils.util import strtobool
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from flask_login import AnonymousUserMixin
|
from flask_login import AnonymousUserMixin
|
||||||
|
|
||||||
@ -31,7 +34,6 @@ else:
|
|||||||
if 'PRETTY_IPV6_PTR' in app.config.keys():
|
if 'PRETTY_IPV6_PTR' in app.config.keys():
|
||||||
import dns.inet
|
import dns.inet
|
||||||
import dns.name
|
import dns.name
|
||||||
import dns.reversename
|
|
||||||
PRETTY_IPV6_PTR = app.config['PRETTY_IPV6_PTR']
|
PRETTY_IPV6_PTR = app.config['PRETTY_IPV6_PTR']
|
||||||
else:
|
else:
|
||||||
PRETTY_IPV6_PTR = False
|
PRETTY_IPV6_PTR = False
|
||||||
@ -450,7 +452,7 @@ class Domain(db.Model):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logging.error('Can not create settting %s for domain %s. %s' % (setting, self.name, str(e)))
|
logging.error('Can not create setting %s for domain %s. %s' % (setting, self.name, str(e)))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_domains(self):
|
def get_domains(self):
|
||||||
@ -481,8 +483,11 @@ class Domain(db.Model):
|
|||||||
"""
|
"""
|
||||||
Return domain id
|
Return domain id
|
||||||
"""
|
"""
|
||||||
domain = Domain.query.filter(Domain.name==name).first()
|
try:
|
||||||
return domain.id
|
domain = Domain.query.filter(Domain.name==name).first()
|
||||||
|
return domain.id
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
"""
|
"""
|
||||||
@ -507,6 +512,10 @@ class Domain(db.Model):
|
|||||||
if domain_user:
|
if domain_user:
|
||||||
domain_user.delete()
|
domain_user.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
domain_setting = DomainSetting.query.filter(DomainSetting.domain_id==domain.id)
|
||||||
|
if domain_setting:
|
||||||
|
domain_setting.delete()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
# then remove domain
|
# then remove domain
|
||||||
Domain.query.filter(Domain.name == d).delete()
|
Domain.query.filter(Domain.name == d).delete()
|
||||||
@ -600,6 +609,60 @@ class Domain(db.Model):
|
|||||||
logging.debug(str(e))
|
logging.debug(str(e))
|
||||||
return {'status': 'error', 'msg': 'Cannot add this domain.'}
|
return {'status': 'error', 'msg': 'Cannot add this domain.'}
|
||||||
|
|
||||||
|
def create_reverse_domain(self, domain_name, domain_reverse_name):
|
||||||
|
"""
|
||||||
|
Check the existing reverse lookup domain,
|
||||||
|
if not exists create a new one automatically
|
||||||
|
"""
|
||||||
|
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
|
domain_auto_ptr = DomainSetting.query.filter(DomainSetting.domain == domain_obj).filter(DomainSetting.setting == 'auto_ptr').first()
|
||||||
|
domain_auto_ptr = strtobool(domain_auto_ptr.value) if domain_auto_ptr else False
|
||||||
|
system_auto_ptr = Setting.query.filter(Setting.name == 'auto_ptr').first()
|
||||||
|
system_auto_ptr = strtobool(system_auto_ptr.value)
|
||||||
|
self.name = domain_name
|
||||||
|
domain_id = self.get_id_by_name(domain_reverse_name)
|
||||||
|
if None == domain_id and \
|
||||||
|
(
|
||||||
|
system_auto_ptr or \
|
||||||
|
domain_auto_ptr
|
||||||
|
):
|
||||||
|
result = self.add(domain_reverse_name, 'Master', 'INCEPTION-INCREMENT', '', '')
|
||||||
|
self.update()
|
||||||
|
if result['status'] == 'ok':
|
||||||
|
history = History(msg='Add reverse lookup domain %s' % domain_reverse_name, detail=str({'domain_type': 'Master', 'domain_master_ips': ''}), created_by='System')
|
||||||
|
history.add()
|
||||||
|
else:
|
||||||
|
return {'status': 'error', 'msg': 'Adding reverse lookup domain failed'}
|
||||||
|
domain_user_ids = self.get_user()
|
||||||
|
domain_users = []
|
||||||
|
u = User()
|
||||||
|
for uid in domain_user_ids:
|
||||||
|
u.id = uid
|
||||||
|
tmp = u.get_user_info_by_id()
|
||||||
|
domain_users.append(tmp.username)
|
||||||
|
if 0 != len(domain_users):
|
||||||
|
self.name = domain_reverse_name
|
||||||
|
self.grant_privielges(domain_users)
|
||||||
|
return {'status': 'ok', 'msg': 'New reverse lookup domain created with granted privilages'}
|
||||||
|
return {'status': 'ok', 'msg': 'New reverse lookup domain created without users'}
|
||||||
|
return {'status': 'ok', 'msg': 'Reverse lookup domain already exists'}
|
||||||
|
|
||||||
|
def get_reverse_domain_name(self, reverse_host_address):
|
||||||
|
c = 1
|
||||||
|
if re.search('ip6.arpa', reverse_host_address):
|
||||||
|
for i in range(1,32,1):
|
||||||
|
address = re.search('((([a-f0-9]\.){'+ str(i) +'})(?P<ipname>.+6.arpa)\.?)', reverse_host_address)
|
||||||
|
if None != self.get_id_by_name(address.group('ipname')):
|
||||||
|
c = i
|
||||||
|
break
|
||||||
|
return re.search('((([a-f0-9]\.){'+ str(c) +'})(?P<ipname>.+6.arpa)\.?)', reverse_host_address).group('ipname')
|
||||||
|
else:
|
||||||
|
for i in range(1,4,1):
|
||||||
|
address = re.search('((([0-9]+\.){'+ str(i) +'})(?P<ipname>.+r.arpa)\.?)', reverse_host_address)
|
||||||
|
if None != self.get_id_by_name(address.group('ipname')):
|
||||||
|
c = i
|
||||||
|
break
|
||||||
|
return re.search('((([0-9]+\.){'+ str(c) +'})(?P<ipname>.+r.arpa)\.?)', reverse_host_address).group('ipname')
|
||||||
|
|
||||||
def delete(self, domain_name):
|
def delete(self, domain_name):
|
||||||
"""
|
"""
|
||||||
@ -769,7 +832,7 @@ class Record(object):
|
|||||||
if NEW_SCHEMA:
|
if NEW_SCHEMA:
|
||||||
data = {"rrsets": [
|
data = {"rrsets": [
|
||||||
{
|
{
|
||||||
"name": self.name + '.',
|
"name": self.name.rstrip('.') + '.',
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
"changetype": "REPLACE",
|
"changetype": "REPLACE",
|
||||||
"ttl": self.ttl,
|
"ttl": self.ttl,
|
||||||
@ -861,7 +924,7 @@ class Record(object):
|
|||||||
|
|
||||||
records = []
|
records = []
|
||||||
for r in deleted_records:
|
for r in deleted_records:
|
||||||
r_name = r['name'] + '.' if NEW_SCHEMA else r['name']
|
r_name = r['name'].rstrip('.') + '.' if NEW_SCHEMA else r['name']
|
||||||
r_type = r['type']
|
r_type = r['type']
|
||||||
if PRETTY_IPV6_PTR: # only if activated
|
if PRETTY_IPV6_PTR: # only if activated
|
||||||
if NEW_SCHEMA: # only if new schema
|
if NEW_SCHEMA: # only if new schema
|
||||||
@ -883,7 +946,7 @@ class Record(object):
|
|||||||
records = []
|
records = []
|
||||||
for r in new_records:
|
for r in new_records:
|
||||||
if NEW_SCHEMA:
|
if NEW_SCHEMA:
|
||||||
r_name = r['name'] + '.'
|
r_name = r['name'].rstrip('.') + '.'
|
||||||
r_type = r['type']
|
r_type = r['type']
|
||||||
if PRETTY_IPV6_PTR: # only if activated
|
if PRETTY_IPV6_PTR: # only if activated
|
||||||
if r_type == 'PTR': # only ptr
|
if r_type == 'PTR': # only ptr
|
||||||
@ -988,12 +1051,54 @@ class Record(object):
|
|||||||
logging.debug(jdata2['error'])
|
logging.debug(jdata2['error'])
|
||||||
return {'status': 'error', 'msg': jdata2['error']}
|
return {'status': 'error', 'msg': jdata2['error']}
|
||||||
else:
|
else:
|
||||||
|
self.auto_ptr(domain, new_records, deleted_records)
|
||||||
logging.info('Record was applied successfully.')
|
logging.info('Record was applied successfully.')
|
||||||
return {'status': 'ok', 'msg': 'Record was applied successfully'}
|
return {'status': 'ok', 'msg': 'Record was applied successfully'}
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
logging.error("Cannot apply record changes to domain %s. DETAIL: %s" % (str(e), domain))
|
logging.error("Cannot apply record changes to domain %s. DETAIL: %s" % (str(e), domain))
|
||||||
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
|
return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'}
|
||||||
|
|
||||||
|
def auto_ptr(self, domain, new_records, deleted_records):
|
||||||
|
"""
|
||||||
|
Add auto-ptr records
|
||||||
|
"""
|
||||||
|
domain_obj = Domain.query.filter(Domain.name == domain).first()
|
||||||
|
domain_auto_ptr = DomainSetting.query.filter(DomainSetting.domain == domain_obj).filter(DomainSetting.setting == 'auto_ptr').first()
|
||||||
|
domain_auto_ptr = strtobool(domain_auto_ptr.value) if domain_auto_ptr else False
|
||||||
|
|
||||||
|
system_auto_ptr = Setting.query.filter(Setting.name == 'auto_ptr').first()
|
||||||
|
system_auto_ptr = strtobool(system_auto_ptr.value)
|
||||||
|
|
||||||
|
if system_auto_ptr or domain_auto_ptr:
|
||||||
|
try:
|
||||||
|
d = Domain()
|
||||||
|
for r in new_records:
|
||||||
|
if r['type'] in ['A', 'AAAA']:
|
||||||
|
r_name = r['name'] + '.'
|
||||||
|
r_content = r['content']
|
||||||
|
reverse_host_address = dns.reversename.from_address(r_content).to_text()
|
||||||
|
domain_reverse_name = d.get_reverse_domain_name(reverse_host_address)
|
||||||
|
d.create_reverse_domain(domain, domain_reverse_name)
|
||||||
|
self.name = dns.reversename.from_address(r_content).to_text().rstrip('.')
|
||||||
|
self.type = 'PTR'
|
||||||
|
self.status = r['disabled']
|
||||||
|
self.ttl = r['ttl']
|
||||||
|
self.data = r_name
|
||||||
|
self.add(domain_reverse_name)
|
||||||
|
for r in deleted_records:
|
||||||
|
if r['type'] in ['A', 'AAAA']:
|
||||||
|
r_name = r['name'] + '.'
|
||||||
|
r_content = r['content']
|
||||||
|
reverse_host_address = dns.reversename.from_address(r_content).to_text()
|
||||||
|
domain_reverse_name = d.get_reverse_domain_name(reverse_host_address)
|
||||||
|
self.name = reverse_host_address
|
||||||
|
self.type = 'PTR'
|
||||||
|
self.data = r_content
|
||||||
|
self.delete(domain_reverse_name)
|
||||||
|
return {'status': 'ok', 'msg': 'Auto-PTR record was updated successfully'}
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("Cannot update auto-ptr record changes to domain %s. DETAIL: %s" % (str(e), domain))
|
||||||
|
return {'status': 'error', 'msg': 'Auto-PTR creation failed. There was something wrong, please contact administrator.'}
|
||||||
|
|
||||||
def delete(self, domain):
|
def delete(self, domain):
|
||||||
"""
|
"""
|
||||||
@ -1003,14 +1108,10 @@ class Record(object):
|
|||||||
headers['X-API-Key'] = PDNS_API_KEY
|
headers['X-API-Key'] = PDNS_API_KEY
|
||||||
data = {"rrsets": [
|
data = {"rrsets": [
|
||||||
{
|
{
|
||||||
"name": self.name,
|
"name": self.name.rstrip('.') + '.',
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
"changetype": "DELETE",
|
"changetype": "DELETE",
|
||||||
"records": [
|
"records": [
|
||||||
{
|
|
||||||
"name": self.name,
|
|
||||||
"type": self.type
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -168,7 +168,7 @@
|
|||||||
{{ domain.type }}
|
{{ domain.type }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ domain.serial }}
|
{% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{domain.serial}}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if domain.master == '[]'%}N/A {% else %}{{ domain.master|display_master_name }}{% endif %}
|
{% if domain.master == '[]'%}N/A {% else %}{{ domain.master|display_master_name }}{% endif %}
|
||||||
|
@ -223,7 +223,8 @@
|
|||||||
$("#tbl_records").DataTable().search('').columns().search('').draw();
|
$("#tbl_records").DataTable().search('').columns().search('').draw();
|
||||||
|
|
||||||
// add new row
|
// add new row
|
||||||
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', 'A', 'Active', 3600, '', '', '', '0']);
|
var default_type = records_allow_edit[0]
|
||||||
|
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', 3600, '', '', '', '0']);
|
||||||
editRow($("#tbl_records").DataTable(), nRow);
|
editRow($("#tbl_records").DataTable(), nRow);
|
||||||
document.getElementById("edit-row-focus").focus();
|
document.getElementById("edit-row-focus").focus();
|
||||||
nEditing = nRow;
|
nEditing = nRow;
|
||||||
@ -268,16 +269,16 @@
|
|||||||
var modal = $("#modal_custom_record");
|
var modal = $("#modal_custom_record");
|
||||||
if (record_data.val() == "") {
|
if (record_data.val() == "") {
|
||||||
var form = " <label for=\"mx_priority\">MX Priority</label> \
|
var form = " <label for=\"mx_priority\">MX Priority</label> \
|
||||||
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"10\"> \
|
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"eg. 10\"> \
|
||||||
<label for=\"mx_server\">MX Server</label> \
|
<label for=\"mx_server\">MX Server</label> \
|
||||||
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"postfix.example.com\"> \
|
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"eg. postfix.example.com\"> \
|
||||||
";
|
";
|
||||||
} else {
|
} else {
|
||||||
var parts = record_data.val().split(" ");
|
var parts = record_data.val().split(" ");
|
||||||
var form = " <label for=\"mx_priority\">MX Priority</label> \
|
var form = " <label for=\"mx_priority\">MX Priority</label> \
|
||||||
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"10\" value=\"" + parts[0] + "\"> \
|
<input type=\"text\" class=\"form-control\" name=\"mx_priority\" id=\"mx_priority\" placeholder=\"eg. 10\" value=\"" + parts[0] + "\"> \
|
||||||
<label for=\"mx_server\">MX Server</label> \
|
<label for=\"mx_server\">MX Server</label> \
|
||||||
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"postfix.example.com\" value=\"" + parts[1] + "\"> \
|
<input type=\"text\" class=\"form-control\" name=\"mx_server\" id=\"mx_server\" placeholder=\"eg. postfix.example.com\" value=\"" + parts[1] + "\"> \
|
||||||
";
|
";
|
||||||
}
|
}
|
||||||
modal.find('.modal-body p').html(form);
|
modal.find('.modal-body p').html(form);
|
||||||
|
@ -55,6 +55,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-header">
|
||||||
|
<h3 class="box-title">Auto PTR creation</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<p><input type="checkbox" id="{{ domain.name }}" class="auto_ptr_toggle"
|
||||||
|
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %} {% if auto_ptr_setting %}disabled="True"{% endif %}>
|
||||||
|
Allow automatic reverse pointer creation on record updates?{% if
|
||||||
|
auto_ptr_setting %}</br><code>Auto-ptr is enabled globally on the PDA system!</code>{% endif %}</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@ -94,6 +110,10 @@ $('.dyndns_on_demand_toggle').iCheck({
|
|||||||
checkboxClass : 'icheckbox_square-blue',
|
checkboxClass : 'icheckbox_square-blue',
|
||||||
increaseArea : '20%' // optional
|
increaseArea : '20%' // optional
|
||||||
});
|
});
|
||||||
|
$('.auto_ptr_toggle').iCheck({
|
||||||
|
checkboxClass : 'icheckbox_square-blue',
|
||||||
|
increaseArea : '20%' // optional
|
||||||
|
});
|
||||||
|
|
||||||
$("#domain_multi_user").multiSelect();
|
$("#domain_multi_user").multiSelect();
|
||||||
|
|
||||||
@ -110,6 +130,18 @@ $('.dyndns_on_demand_toggle').on('ifToggled', function(event) {
|
|||||||
};
|
};
|
||||||
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
|
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
|
||||||
});
|
});
|
||||||
|
$('.auto_ptr_toggle').on('ifToggled', function(event) {
|
||||||
|
var is_checked = $(this).prop('checked');
|
||||||
|
var domain = $(this).prop('id');
|
||||||
|
postdata = {
|
||||||
|
'action' : 'set_setting',
|
||||||
|
'data' : {
|
||||||
|
'setting' : 'auto_ptr',
|
||||||
|
'value' : is_checked
|
||||||
|
}
|
||||||
|
};
|
||||||
|
applyChanges(postdata, $SCRIPT_ROOT + '/domain/' + domain + '/managesetting', true);
|
||||||
|
});
|
||||||
|
|
||||||
// handle deletion of domain
|
// handle deletion of domain
|
||||||
$(document.body).on('click', '.delete_domain', function() {
|
$(document.body).on('click', '.delete_domain', function() {
|
||||||
|
13
app/views.py
13
app/views.py
@ -2,6 +2,7 @@ import base64
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
import re
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
@ -58,6 +59,11 @@ def inject_default_domain_table_size_setting():
|
|||||||
default_domain_table_size_setting = Setting.query.filter(Setting.name == 'default_domain_table_size').first()
|
default_domain_table_size_setting = Setting.query.filter(Setting.name == 'default_domain_table_size').first()
|
||||||
return dict(default_domain_table_size_setting=default_domain_table_size_setting.value)
|
return dict(default_domain_table_size_setting=default_domain_table_size_setting.value)
|
||||||
|
|
||||||
|
@app.context_processor
|
||||||
|
def inject_auto_ptr_setting():
|
||||||
|
auto_ptr_setting = Setting.query.filter(Setting.name == 'auto_ptr').first()
|
||||||
|
return dict(auto_ptr_setting=strtobool(auto_ptr_setting.value))
|
||||||
|
|
||||||
# START USER AUTHENTICATION HANDLER
|
# START USER AUTHENTICATION HANDLER
|
||||||
@app.before_request
|
@app.before_request
|
||||||
def before_request():
|
def before_request():
|
||||||
@ -312,8 +318,11 @@ def domain(domain_name):
|
|||||||
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
if jr['type'] in app.config['RECORDS_ALLOW_EDIT']:
|
||||||
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content'])
|
record = Record(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content'])
|
||||||
records.append(record)
|
records.append(record)
|
||||||
|
if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name):
|
||||||
return render_template('domain.html', domain=domain, records=records, editable_records=app.config['RECORDS_ALLOW_EDIT'])
|
editable_records = app.config['RECORDS_ALLOW_EDIT']
|
||||||
|
else:
|
||||||
|
editable_records = ['PTR']
|
||||||
|
return render_template('domain.html', domain=domain, records=records, editable_records=editable_records)
|
||||||
else:
|
else:
|
||||||
return redirect(url_for('error', code=404))
|
return redirect(url_for('error', code=404))
|
||||||
|
|
||||||
|
@ -77,7 +77,8 @@ def init_records():
|
|||||||
Setting('record_helper', 'True'),
|
Setting('record_helper', 'True'),
|
||||||
Setting('login_ldap_first', 'True'),
|
Setting('login_ldap_first', 'True'),
|
||||||
Setting('default_record_table_size', '15'),
|
Setting('default_record_table_size', '15'),
|
||||||
Setting('default_domain_table_size', '10')
|
Setting('default_domain_table_size', '10'),
|
||||||
|
Setting('auto_ptr','False')
|
||||||
])
|
])
|
||||||
|
|
||||||
db_commit = db.session.commit()
|
db_commit = db.session.commit()
|
||||||
|
Loading…
Reference in New Issue
Block a user