diff --git a/configs/development.py b/configs/development.py
index d4bd24f..b848d0c 100644
--- a/configs/development.py
+++ b/configs/development.py
@@ -15,6 +15,17 @@ SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
+#CAPTCHA Config
+CAPTCHA_ENABLE = True
+CAPTCHA_LENGTH = 6
+CAPTCHA_WIDTH = 160
+CAPTCHA_HEIGHT = 60
+CAPTCHA_SESSION_KEY = 'captcha_image'
+
+#Server side sessions tracking
+#Set to TRUE for CAPTCHA, or enable another stateful session tracking system
+FILESYSTEM_SESSIONS_ENABLED = True
+
### DATABASE - MySQL
#SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
# urllib.parse.quote_plus(SQLA_DB_USER),
diff --git a/migrations/versions/b24bf17725d2_add_unique_index_to_settings_table_keys.py b/migrations/versions/b24bf17725d2_add_unique_index_to_settings_table_keys.py
new file mode 100644
index 0000000..48cfbe9
--- /dev/null
+++ b/migrations/versions/b24bf17725d2_add_unique_index_to_settings_table_keys.py
@@ -0,0 +1,24 @@
+"""Add unique index to settings table keys
+
+Revision ID: b24bf17725d2
+Revises: 0967658d9c0d
+Create Date: 2021-12-12 20:29:17.103441
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b24bf17725d2'
+down_revision = '0967658d9c0d'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ op.create_index(op.f('ix_setting_name'), 'setting', ['name'], unique=True)
+
+
+def downgrade():
+ op.drop_index(op.f('ix_setting_name'), table_name='setting')
diff --git a/package.json b/package.json
index 76982c8..f6724c3 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,17 @@
{
"dependencies": {
- "admin-lte": "2.4.9",
- "bootstrap": "^3.4.1",
- "bootstrap-datepicker": "^1.8.0",
+ "@fortawesome/fontawesome-free": "6.3.0",
+ "admin-lte": "3.2.0",
+ "bootstrap": "4.6.2",
+ "bootstrap-datepicker": "^1.9.0",
"bootstrap-validator": "^0.11.9",
- "datatables.net-plugins": "^1.10.19",
+ "datatables.net-plugins": "^1.13.1",
"icheck": "^1.0.2",
"jquery-slimscroll": "^1.3.8",
- "jquery-ui-dist": "^1.12.1",
+ "jquery-sparkline": "^2.4.0",
+ "jquery-ui-dist": "^1.13.2",
"jquery.quicksearch": "^2.4.0",
- "jtimeout": "^3.1.0",
+ "jtimeout": "^3.2.0",
"multiselect": "^0.9.12"
}
}
diff --git a/powerdnsadmin/assets.py b/powerdnsadmin/assets.py
index 233e23a..7ad973f 100644
--- a/powerdnsadmin/assets.py
+++ b/powerdnsadmin/assets.py
@@ -2,67 +2,65 @@ from flask_assets import Bundle, Environment, Filter
class ConcatFilter(Filter):
- """
- Filter that merges files, placing a semicolon between them.
+ """
+ Filter that merges files, placing a semicolon between them.
- Fixes issues caused by missing semicolons at end of JS assets, for example
- with last statement of jquery.pjax.js.
- """
- def concat(self, out, hunks, **kw):
- out.write(';'.join([h.data() for h, info in hunks]))
+ Fixes issues caused by missing semicolons at end of JS assets, for example
+ with last statement of jquery.pjax.js.
+ """
+ def concat(self, out, hunks, **kw):
+ out.write(';'.join([h.data() for h, info in hunks]))
+css_login = Bundle(
+ 'node_modules/@fortawesome/fontawesome-free/css/all.min.css',
+ 'node_modules/icheck/skins/square/blue.css',
+ 'node_modules/admin-lte/dist/css/adminlte.css',
+ filters=('cssmin', 'cssrewrite'),
+ output='generated/login.css')
-css_login = Bundle('node_modules/bootstrap/dist/css/bootstrap.css',
- 'node_modules/font-awesome/css/font-awesome.css',
- 'node_modules/ionicons/dist/css/ionicons.css',
- 'node_modules/icheck/skins/square/blue.css',
- 'node_modules/admin-lte/dist/css/AdminLTE.css',
- filters=('cssmin', 'cssrewrite'),
- output='generated/login.css')
+js_login = Bundle(
+ 'node_modules/jquery/dist/jquery.js',
+ 'node_modules/bootstrap/dist/js/bootstrap.js',
+ 'node_modules/icheck/icheck.js',
+ 'custom/js/custom.js',
+ filters=(ConcatFilter, 'rjsmin'),
+ output='generated/login.js')
-js_login = Bundle('node_modules/jquery/dist/jquery.js',
- 'node_modules/bootstrap/dist/js/bootstrap.js',
- 'node_modules/icheck/icheck.js',
- 'custom/js/custom.js',
- filters=(ConcatFilter, 'rjsmin'),
- output='generated/login.js')
-
-js_validation = Bundle('node_modules/bootstrap-validator/dist/validator.js',
- output='generated/validation.js')
+js_validation = Bundle(
+ 'node_modules/bootstrap-validator/dist/validator.js',
+ output='generated/validation.js')
css_main = Bundle(
- 'node_modules/bootstrap/dist/css/bootstrap.css',
- 'node_modules/font-awesome/css/font-awesome.css',
- 'node_modules/ionicons/dist/css/ionicons.css',
- 'node_modules/datatables.net-bs/css/dataTables.bootstrap.css',
- 'node_modules/icheck/skins/square/blue.css',
- 'node_modules/multiselect/css/multi-select.css',
- 'node_modules/admin-lte/dist/css/AdminLTE.css',
- 'node_modules/admin-lte/dist/css/skins/_all-skins.css',
- 'custom/css/custom.css',
- 'node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css',
- filters=('cssmin', 'cssrewrite'),
- output='generated/main.css')
+ 'node_modules/@fortawesome/fontawesome-free/css/all.min.css',
+ 'node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css',
+ 'node_modules/icheck/skins/square/blue.css',
+ 'node_modules/multiselect/css/multi-select.css',
+ 'node_modules/admin-lte/dist/css/adminlte.css',
+ 'custom/css/custom.css',
+ 'node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css',
+ filters=('cssmin', 'cssrewrite'),
+ output='generated/main.css')
-js_main = Bundle('node_modules/jquery/dist/jquery.js',
- 'node_modules/jquery-ui-dist/jquery-ui.js',
- 'node_modules/bootstrap/dist/js/bootstrap.js',
- 'node_modules/datatables.net/js/jquery.dataTables.js',
- 'node_modules/datatables.net-bs/js/dataTables.bootstrap.js',
- 'node_modules/jquery-sparkline/jquery.sparkline.js',
- 'node_modules/jquery-slimscroll/jquery.slimscroll.js',
- 'node_modules/icheck/icheck.js',
- 'node_modules/fastclick/lib/fastclick.js',
- 'node_modules/moment/moment.js',
- 'node_modules/admin-lte/dist/js/adminlte.js',
- 'node_modules/multiselect/js/jquery.multi-select.js',
- 'node_modules/datatables.net-plugins/sorting/natural.js',
- 'node_modules/jtimeout/src/jTimeout.js',
- 'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
- 'custom/js/custom.js',
- 'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js',
- filters=(ConcatFilter, 'rjsmin'),
- output='generated/main.js')
+js_main = Bundle(
+ 'node_modules/jquery/dist/jquery.js',
+ 'node_modules/jquery-ui-dist/jquery-ui.js',
+ 'node_modules/bootstrap/dist/js/bootstrap.bundle.js',
+ 'node_modules/datatables.net/js/jquery.dataTables.js',
+ 'node_modules/datatables.net-bs4/js/dataTables.bootstrap4.js',
+ 'node_modules/jquery-sparkline/jquery.sparkline.js',
+ 'node_modules/jquery-slimscroll/jquery.slimscroll.js',
+ 'node_modules/icheck/icheck.js',
+ 'node_modules/fastclick/lib/fastclick.js',
+ 'node_modules/moment/moment.js',
+ 'node_modules/admin-lte/dist/js/adminlte.js',
+ 'node_modules/multiselect/js/jquery.multi-select.js',
+ 'node_modules/datatables.net-plugins/sorting/natural.js',
+ 'node_modules/jtimeout/src/jTimeout.js',
+ 'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
+ 'custom/js/custom.js',
+ 'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js',
+ filters=(ConcatFilter, 'rjsmin'),
+ output='generated/main.js')
assets = Environment()
assets.register('js_login', js_login)
diff --git a/powerdnsadmin/models/domain.py b/powerdnsadmin/models/domain.py
index 4610adb..500ad74 100644
--- a/powerdnsadmin/models/domain.py
+++ b/powerdnsadmin/models/domain.py
@@ -549,11 +549,12 @@ class Domain(db.Model):
domain.apikeys[:] = []
# Remove history for domain
- domain_history = History.query.filter(
- History.domain_id == domain.id
- )
- if domain_history:
- domain_history.delete()
+ if not Setting().get('preserve_history'):
+ domain_history = History.query.filter(
+ History.domain_id == domain.id
+ )
+ if domain_history:
+ domain_history.delete()
# then remove domain
Domain.query.filter(Domain.name == domain_name).delete()
diff --git a/powerdnsadmin/models/record.py b/powerdnsadmin/models/record.py
index b6c3f54..ae70a9e 100644
--- a/powerdnsadmin/models/record.py
+++ b/powerdnsadmin/models/record.py
@@ -422,6 +422,25 @@ class Record(object):
]
d = Domain()
+ for r in del_rrsets:
+ for record in r['records']:
+ # Format the reverse record name
+ # It is the reverse of forward record's content.
+ reverse_host_address = dns.reversename.from_address(
+ record['content']).to_text()
+
+ # Create the reverse domain name in PDNS
+ domain_reverse_name = d.get_reverse_domain_name(
+ reverse_host_address)
+ d.create_reverse_domain(domain_name,
+ domain_reverse_name)
+
+ # Delete the reverse zone
+ self.name = reverse_host_address
+ self.type = 'PTR'
+ self.data = record['content']
+ self.delete(domain_reverse_name)
+
for r in new_rrsets:
for record in r['records']:
# Format the reverse record name
@@ -455,25 +474,6 @@ class Record(object):
# Format the rrset
rrset = {"rrsets": rrset_data}
self.add(domain_reverse_name, rrset)
-
- for r in del_rrsets:
- for record in r['records']:
- # Format the reverse record name
- # It is the reverse of forward record's content.
- reverse_host_address = dns.reversename.from_address(
- record['content']).to_text()
-
- # Create the reverse domain name in PDNS
- domain_reverse_name = d.get_reverse_domain_name(
- reverse_host_address)
- d.create_reverse_domain(domain_name,
- domain_reverse_name)
-
- # Delete the reverse zone
- self.name = reverse_host_address
- self.type = 'PTR'
- self.data = record['content']
- self.delete(domain_reverse_name)
return {
'status': 'ok',
'msg': 'Auto-PTR record was updated successfully'
diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py
index a4016bf..71fec98 100644
--- a/powerdnsadmin/models/setting.py
+++ b/powerdnsadmin/models/setting.py
@@ -11,7 +11,7 @@ from .base import db
class Setting(db.Model):
id = db.Column(db.Integer, primary_key=True)
- name = db.Column(db.String(64))
+ name = db.Column(db.String(64), unique=True, index=True)
value = db.Column(db.Text())
defaults = {
@@ -31,6 +31,7 @@ class Setting(db.Model):
'delete_sso_accounts': False,
'bg_domain_updates': False,
'enable_api_rr_history': True,
+ 'preserve_history': False,
'site_name': 'PowerDNS-Admin',
'site_url': 'http://localhost:9191',
'session_timeout': 10,
diff --git a/powerdnsadmin/routes/__init__.py b/powerdnsadmin/routes/__init__.py
index 7d8aa9a..598b17a 100644
--- a/powerdnsadmin/routes/__init__.py
+++ b/powerdnsadmin/routes/__init__.py
@@ -1,5 +1,5 @@
from .base import (
- csrf, login_manager, handle_bad_request, handle_unauthorized_access,
+ captcha, csrf, login_manager, handle_bad_request, handle_unauthorized_access,
handle_access_forbidden, handle_page_not_found, handle_internal_server_error
)
@@ -14,6 +14,7 @@ from .api import api_bp, apilist_bp
def init_app(app):
login_manager.init_app(app)
csrf.init_app(app)
+ captcha.init_app(app)
app.register_blueprint(index_bp)
app.register_blueprint(user_bp)
diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py
index d4298a0..98f4ef9 100644
--- a/powerdnsadmin/routes/admin.py
+++ b/powerdnsadmin/routes/admin.py
@@ -795,7 +795,7 @@ class DetailedHistory():
if 'domain_type' in detail_dict and 'account_id' in detail_dict: # this is a domain creation
self.detailed_msg = render_template_string("""
-
Domain type:
{{ domaintype }}
+
Domain Type:
{{ domaintype }}
Account:
{{ account }}
""",
@@ -804,22 +804,23 @@ class DetailedHistory():
elif 'authenticator' in detail_dict: # this is a user authentication
self.detailed_msg = render_template_string("""
-
-
-
-
-
User {{ username }} authentication {{ auth_result }}
-
-
-
+
-
Authenticator Type:
-
{{ authenticator }}
+
Username:
+
{{ username }}
-
IP Address
-
{{ ip_address }}
+
Authentication Result:
+
{{ auth_result }}
+
+
+
Authenticator Type:
+
{{ authenticator }}
+
+
+
IP Address:
+
{{ ip_address }}
@@ -963,6 +964,13 @@ def history():
'msg': 'You do not have permission to remove history.'
}), 401)
+ if Setting().get('preserve_history'):
+ return make_response(
+ jsonify({
+ 'status': 'error',
+ 'msg': 'History removal is not allowed (toggle preserve_history in settings).'
+ }), 401)
+
h = History()
result = h.remove_all()
if result:
@@ -1318,6 +1326,7 @@ def setting_basic():
'otp_field_enabled',
'otp_force',
'pdns_api_timeout',
+ 'preserve_history',
'pretty_ipv6_ptr',
'record_helper',
'record_quick_edit',
diff --git a/powerdnsadmin/routes/base.py b/powerdnsadmin/routes/base.py
index 16ed00a..7af342c 100644
--- a/powerdnsadmin/routes/base.py
+++ b/powerdnsadmin/routes/base.py
@@ -3,10 +3,12 @@ import base64
from flask import render_template, url_for, redirect, session, request, current_app
from flask_login import LoginManager
from flask_seasurf import SeaSurf
+from flask_session_captcha import FlaskSessionCaptcha
from ..models.user import User
+captcha = FlaskSessionCaptcha()
csrf = SeaSurf()
login_manager = LoginManager()
diff --git a/powerdnsadmin/routes/domain.py b/powerdnsadmin/routes/domain.py
index 87d0be4..9742760 100644
--- a/powerdnsadmin/routes/domain.py
+++ b/powerdnsadmin/routes/domain.py
@@ -89,14 +89,14 @@ def domain(domain_name):
# - Find a way to make it consistent, or
# - Only allow one comment for that case
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
+ pretty_v6 = Setting().get('pretty_ipv6_ptr')
for r in rrsets:
if r['type'] in records_allow_to_edit:
r_name = r['name'].rstrip('.')
# If it is reverse zone and pretty_ipv6_ptr setting
# is enabled, we reformat the name for ipv6 records.
- if Setting().get('pretty_ipv6_ptr') and r[
- 'type'] == 'PTR' and 'ip6.arpa' in r_name and '*' not in r_name:
+ if pretty_v6 and r['type'] == 'PTR' and 'ip6.arpa' in r_name and '*' not in r_name:
r_name = dns.reversename.to_address(
dns.name.from_text(r_name))
diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py
index 117fe14..14ad275 100644
--- a/powerdnsadmin/routes/index.py
+++ b/powerdnsadmin/routes/index.py
@@ -10,7 +10,7 @@ from yaml import Loader, load
from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort
from flask_login import login_user, logout_user, login_required, current_user
-from .base import csrf, login_manager
+from .base import captcha, csrf, login_manager
from ..lib import utils
from ..decorators import dyndns_login_required
from ..models.base import db
@@ -400,7 +400,7 @@ def login():
desc_prop = Setting().get('oidc_oauth_account_description_property')
account_to_add = []
- #If the name_property and desc_property exist in me (A variable that contains all the userinfo from the IdP).
+ #If the name_property and desc_property exist in me (A variable that contains all the userinfo from the IdP).
if name_prop in me and desc_prop in me:
accounts_name_prop = [me[name_prop]] if type(me[name_prop]) is not list else me[name_prop]
accounts_desc_prop = [me[desc_prop]] if type(me[desc_prop]) is not list else me[desc_prop]
@@ -415,7 +415,7 @@ def login():
account_to_add.append(account)
user_accounts = user.get_accounts()
- # Add accounts
+ # Add accounts
for account in account_to_add:
if account not in user_accounts:
account.add_user(user)
@@ -651,50 +651,73 @@ def logout():
@index_bp.route('/register', methods=['GET', 'POST'])
def register():
- if Setting().get('signup_enabled'):
- if request.method == 'GET':
- return render_template('register.html')
- elif request.method == 'POST':
- username = request.form.get('username', '').strip()
- password = request.form.get('password', '')
- firstname = request.form.get('firstname', '').strip()
- lastname = request.form.get('lastname', '').strip()
- email = request.form.get('email', '').strip()
- rpassword = request.form.get('rpassword', '')
+ CAPTCHA_ENABLE = current_app.config.get('CAPTCHA_ENABLE')
+ if Setting().get('signup_enabled'):
+ if current_user.is_authenticated:
+ return redirect(url_for('index.index'))
+ if request.method == 'GET':
+ return render_template('register.html', captcha_enable=CAPTCHA_ENABLE)
+ elif request.method == 'POST':
+ username = request.form.get('username', '').strip()
+ password = request.form.get('password', '')
+ firstname = request.form.get('firstname', '').strip()
+ lastname = request.form.get('lastname', '').strip()
+ email = request.form.get('email', '').strip()
+ rpassword = request.form.get('rpassword', '')
- if not username or not password or not email:
- return render_template(
- 'register.html', error='Please input required information')
+ is_valid_email = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
- if password != rpassword:
- return render_template(
- 'register.html',
- error="Password confirmation does not match")
+ error_messages = {}
+ if not firstname:
+ error_messages['firstname'] = 'First Name is required'
+ if not lastname:
+ error_messages['lastname'] = 'Last Name is required'
+ if not username:
+ error_messages['username'] = 'Username is required'
+ if not password:
+ error_messages['password'] = 'Password is required'
+ if not rpassword:
+ error_messages['rpassword'] = 'Password confirmation is required'
+ if not email:
+ error_messages['email'] = 'Email is required'
+ if not is_valid_email.match(email):
+ error_messages['email'] = 'Invalid email address'
+ if password != rpassword:
+ error_messages['password'] = 'Password confirmation does not match'
+ error_messages['rpassword'] = 'Password confirmation does not match'
- user = User(username=username,
- plain_text_password=password,
- firstname=firstname,
- lastname=lastname,
- email=email)
+ if not captcha.validate():
+ return render_template(
+ 'register.html', error='Invalid CAPTCHA answer', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE)
- try:
- result = user.create_local_user()
- if result and result['status']:
- if Setting().get('verify_user_email'):
- send_account_verification(email)
- if Setting().get('otp_force') and Setting().get('otp_field_enabled'):
- user.update_profile(enable_otp=True)
- prepare_welcome_user(user.id)
- return redirect(url_for('index.welcome'))
- else:
- return redirect(url_for('index.login'))
- else:
- return render_template('register.html',
- error=result['msg'])
- except Exception as e:
- return render_template('register.html', error=e)
+ if error_messages:
+ return render_template('register.html', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE)
+
+ user = User(username=username,
+ plain_text_password=password,
+ firstname=firstname,
+ lastname=lastname,
+ email=email
+ )
+
+ try:
+ result = user.create_local_user()
+ if result and result['status']:
+ if Setting().get('verify_user_email'):
+ send_account_verification(email)
+ if Setting().get('otp_force') and Setting().get('otp_field_enabled'):
+ user.update_profile(enable_otp=True)
+ prepare_welcome_user(user.id)
+ return redirect(url_for('index.welcome'))
+ else:
+ return redirect(url_for('index.login'))
+ else:
+ return render_template('register.html',
+ error=result['msg'], captcha_enable=CAPTCHA_ENABLE)
+ except Exception as e:
+ return render_template('register.html', error=e, captcha_enable=CAPTCHA_ENABLE)
else:
- return render_template('errors/404.html'), 404
+ return render_template('errors/404.html'), 404
# Show welcome page on first login if otp_force is enabled
diff --git a/powerdnsadmin/static/custom/js/custom.js b/powerdnsadmin/static/custom/js/custom.js
index e4b72b1..0f096bf 100644
--- a/powerdnsadmin/static/custom/js/custom.js
+++ b/powerdnsadmin/static/custom/js/custom.js
@@ -287,4 +287,18 @@ function copy_otp_secret_to_clipboard() {
navigator.clipboard.writeText(copyBox.value);
$("#copy_tooltip").css("visibility", "visible");
setTimeout(function(){ $("#copy_tooltip").css("visibility", "collapse"); }, 2000);
- }
\ No newline at end of file
+ }
+
+// Side menu nav bar active selection
+/** add active class and stay opened when selected */
+var url = window.location;
+
+// for sidebar menu entirely but not cover treeview
+$('ul.nav-sidebar a').filter(function() {
+ return this.href == url;
+}).addClass('active');
+
+// for treeview
+$('ul.nav-treeview a').filter(function() {
+ return this.href == url;
+}).parentsUntil(".nav-sidebar > .nav-treeview").addClass('menu-open').prev('a').addClass('active');
diff --git a/powerdnsadmin/templates/admin_edit_account.html b/powerdnsadmin/templates/admin_edit_account.html
index ee8d1be..046866c 100644
--- a/powerdnsadmin/templates/admin_edit_account.html
+++ b/powerdnsadmin/templates/admin_edit_account.html
@@ -1,192 +1,211 @@
{% extends "base.html" %}
+
{% set active_page = "admin_accounts" %}
-{% block title %}Edit Account - {{ SITE_NAME }}{% endblock %}
+
+{% block title %}
+
+ Edit Account - {{ SITE_NAME }}
+
+{% endblock %}
{% block dashboard_stat %}
-
-
-
{% if create %}Add{% else %}Edit{% endif %} account
+
+
+
+
{% if create %}Add{% else %}Edit{% endif %} Account
+
+
-
-
-
-
Help with creating a new account
-
-
-
- An account allows grouping of domains belonging to a particular entity, such as a customer or
- department.
- A domain can be assigned to an account upon domain creation or through the domain administration
- page.
-
-
Fill in all the fields to the in the form to the left.
-
- Name is an account identifier. It will be lowercased and can contain alphanumeric
- characters{% if SETTING.get('account_name_extra_chars') %}, dots, hyphens and underscores (no space or other special character is allowed)
- {% else %} (no extra character is allowed){% endif %}.
- Description is a user friendly name for this account.
- Contact person is the name of a contact person at the account.
- Mail Address is an e-mail address for the contact person.
-
-
+
+
Access Control
+
+
Users on the right have access to manage records in all domains
+ associated with the account.
+
+
Click on users to move between columns.
+
+
+
+
+
+
Domains on the right are associated with the account. Red marked domain names are already associated with other accounts.
+ Moving already associated domains to this account will overwrite the previous associated account.
+
+
Hover over the red domain names to show the associated account. Click on domains to move between columns.
+
+
+
+
+
+
+
+
+
+
+
Help with creating a new account
+
+
+
+ An account allows grouping of domains belonging to a particular entity, such as a customer or
+ department.
+
+ A domain can be assigned to an account upon domain creation or through the domain administration
+ page.
+
+
Fill in all the fields to the in the form to the left.
+
+ Name is an account identifier. It will be lowercased and can contain alphanumeric
+ characters{% if SETTING.get('account_name_extra_chars') %}, dots, hyphens and underscores (no space or other special character is allowed)
+ {% else %} (no extra character is allowed){% endif %}.
+ Description is a user friendly name for this account.
+ Contact person is the name of a contact person at the account.
+ Mail Address is an e-mail address for the contact person.
+
+
+ Edit User
+
+ {% if not user.username==current_user.username or (current_user.role.name=='Operator' and user.role.name=='Administrator') %}
+
+
+
+ Delete User
+
+
+ {% endif %}
+
Make sure you add PDA redirection URI (e.g http://localhost:9191/google/authorized) to your Google App Credentials Restriction.
+
+
+
+
Help
+
+
+
Fill in all the fields in the left form.
+
Make sure you add PDA redirection URI (e.g http://localhost:9191/google/authorized) to your Google App Credentials Restriction.
+
+
-
+
@@ -440,7 +462,7 @@
-
+
Fill in all the fields in the left form.
@@ -448,7 +470,7 @@
-
+
@@ -562,7 +584,7 @@
-
+
Fill in all the fields in the left form.
You first need to define an Application Registration in your Azure Active Directory, with the appropriate HTTPS URL for this endpoint, and with the appropriate rights, as explained in the documentation.
Select record types you allow user to edit in the forward zone and reverse zone. Take a look at
- PowerDNS docs for
- full list of supported record types.
-
+
+
+
+
+
+
+
Help
+
+
+
Select record types you allow user to edit in the forward zone and reverse zone. Take a look at
+ PowerDNS docs for
+ full list of supported record types.
+