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 %} - -
-

- Account - {% if create %}New account{% else %}{{ account.name }}{% endif %} -

- -
+
+
+
+
+

+ {% if create %}Add Account{% else %}Edit Account{% endif %} + {% if create %}Account{% else %}{{ account.name }}{% endif %} +

+
+
+ +
+
+
+
{% endblock %} {% block content %}
+
-
-
-
-

{% if create %}Add{% else %}Edit{% endif %} account

+
+
+
+

{% if create %}Add{% else %}Edit{% endif %} Account

+
+
+ + +
+ {% if error %} +
+ +

Error!

+ {{ error }}
- - - - - -
- {% if error %} -
- -

Error!

- {{ error }} -
- {{ error }} - {% endif %} -
- - - - {% if invalid_accountname %} - Cannot be blank and must only contain alphanumeric - characters{% if SETTING.get('account_name_extra_chars') %}, dots, hyphens or underscores{% endif %}. - {% elif duplicate_accountname %} - Account name already in use. - {% endif %} -
-
- - - -
-
- - - -
-
- - - -
-
-
-

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.

-
- -
-
- - + {{ error }} + {% endif %} +
+ + + + {% if invalid_accountname %} + Cannot be blank and must only contain alphanumeric + characters{% if SETTING.get('account_name_extra_chars') %}, dots, hyphens or underscores{% endif %}. + + {% elif duplicate_accountname %} + Account name already in use. + {% endif %} +
+
+ + + +
+
+ + + +
+
+ + + +
-
-
-
-
-

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. +

+
+
+
+
{% endblock %} {% block extrascripts %} {% endblock %} diff --git a/powerdnsadmin/templates/admin_edit_key.html b/powerdnsadmin/templates/admin_edit_key.html index 6a94340..23f58d3 100644 --- a/powerdnsadmin/templates/admin_edit_key.html +++ b/powerdnsadmin/templates/admin_edit_key.html @@ -1,80 +1,92 @@ {% extends "base.html" %} + {% set active_page = "admin_keys" %} + {% if (key is not none and key.role.name != "User") %}{% set hide_opts = True %}{%else %}{% set hide_opts = False %}{% endif %} + {% block title %} -Edit Key - {{ SITE_NAME }} + + Edit Key - {{ SITE_NAME }} + {% endblock %} + {% block dashboard_stat %} - -
-

- Key - {% if create %}New key{% else %}{{ key.id }}{% endif %} -

- -
+
+
+
+
+

+ API Keys + {% if create %}Add API Key{% else %}Edit API Key - {{ key.id }}{% endif %} +

+
+
+ +
+
+
+
{% endblock %} {% block content %}
-
-
-
-
-

{% if create %}Add{% else %}Edit{% endif %} key

-
- - -
- - -
-
- - -
-
- - -
-
- - - -
{% endblock %} + {% block extrascripts %} - - + {% endblock %} diff --git a/powerdnsadmin/templates/admin_history.html b/powerdnsadmin/templates/admin_history.html index aa16c4a..f2a8619 100644 --- a/powerdnsadmin/templates/admin_history.html +++ b/powerdnsadmin/templates/admin_history.html @@ -1,81 +1,108 @@ {% extends "base.html" %} + {% set active_page = "admin_history" %} + {% block title %} -History - {{ SITE_NAME }} -{% endblock %} {% block dashboard_stat %} - -
-

- History Recent events -

- -
+ + History - {{ SITE_NAME }} + +{% endblock %} + +{% block dashboard_stat %} +
+
+
+
+

+ History + Recent Events +

+
+
+ +
+
+
+
{% endblock %} + {% block content %} {% import 'applied_change_macro.html' as applied_change_macro %} - - -
+
-
-
-
-

History Management

+
+
+
+

History Management

+ {% if current_user.role.name != 'User' %} + + {% endif %} +
+
+
+ +
+
+
+
+ + + + +
+
-
- -
- -
- - - - -
-
{% endblock %} {% block extrascripts %} @@ -471,49 +485,45 @@ {% endblock %} {% block modals %} - -