From 8ea00b948410bd6a63c971d068c7b64131ac8eda Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Mon, 2 Dec 2019 10:32:03 +0700 Subject: [PATCH 1/4] Refactoring the code - Use Flask blueprint - Split model and views into smaller parts - Bug fixes - API adjustment --- app/__init__.py | 51 - app/customboxes.py | 5 - app/lib/log.py | 46 - app/models.py | 2391 -------- app/oauth.py | 108 - app/templates/admin_editaccount.html | 120 - app/templates/admin_edituser.html | 155 - app/templates/admin_setting_pdns.html | 86 - app/templates/admin_setting_records.html | 79 - app/templates/domain_add.html | 163 - app/templates/domain_management.html | 272 - app/templates/login.html | 138 - app/templates/maintenance.html | 18 - app/templates/register.html | 99 - app/templates/template.html | 124 - app/views.py | 1886 ------ .../0fb6d23a4863_remove_user_avatar.py | 28 + powerdnsadmin/__init__.py | 125 + {app => powerdnsadmin}/assets.py | 0 {app => powerdnsadmin}/decorators.py | 129 +- powerdnsadmin/default_config.py | 102 + {app => powerdnsadmin}/lib/__init__.py | 0 {app => powerdnsadmin}/lib/certutil.py | 0 {app => powerdnsadmin/lib}/errors.py | 11 +- {app => powerdnsadmin}/lib/helper.py | 22 +- {app => powerdnsadmin/lib}/schema.py | 0 {app => powerdnsadmin}/lib/utils.py | 229 +- powerdnsadmin/models.py | 26 + powerdnsadmin/models/__init__.py | 23 + powerdnsadmin/models/account.py | 200 + powerdnsadmin/models/account_user.py | 17 + powerdnsadmin/models/api_key.py | 114 + powerdnsadmin/models/base.py | 7 + powerdnsadmin/models/domain.py | 805 +++ powerdnsadmin/models/domain_setting.py | 33 + powerdnsadmin/models/domain_template.py | 65 + .../models/domain_template_record.py | 42 + powerdnsadmin/models/domain_user.py | 17 + powerdnsadmin/models/history.py | 48 + powerdnsadmin/models/record.py | 607 ++ powerdnsadmin/models/record_entry.py | 25 + powerdnsadmin/models/role.py | 23 + powerdnsadmin/models/server.py | 64 + powerdnsadmin/models/setting.py | 268 + powerdnsadmin/models/user.py | 581 ++ powerdnsadmin/routes/__init__.py | 25 + powerdnsadmin/routes/admin.py | 994 ++++ .../routes}/api.py | 318 +- powerdnsadmin/routes/base.py | 65 + powerdnsadmin/routes/dashboard.py | 166 + powerdnsadmin/routes/domain.py | 525 ++ powerdnsadmin/routes/index.py | 686 +++ powerdnsadmin/routes/user.py | 89 + powerdnsadmin/services/__init__.py | 4 + powerdnsadmin/services/base.py | 3 + powerdnsadmin/services/github.py | 42 + powerdnsadmin/services/google.py | 44 + powerdnsadmin/services/oidc.py | 41 + .../static/custom/css/custom.css | 0 .../static/custom/js/custom.js | 0 powerdnsadmin/static/favicon.ico | Bin 0 -> 894 bytes powerdnsadmin/static/generated/login.css | 1 + powerdnsadmin/static/generated/login.js | 1603 +++++ powerdnsadmin/static/generated/main.css | 1 + powerdnsadmin/static/generated/main.js | 5227 +++++++++++++++++ powerdnsadmin/static/generated/validation.js | 394 ++ {app => powerdnsadmin}/swagger-spec.yaml | 0 .../templates/admin_edit_account.html | 131 + powerdnsadmin/templates/admin_edit_user.html | 166 + .../templates/admin_history.html | 62 +- .../templates/admin_manage_account.html | 53 +- .../templates/admin_manage_user.html | 94 +- .../templates/admin_pdns_stats.html | 63 +- .../admin_setting_authentication.html | 6 +- .../templates/admin_setting_basic.html | 33 +- .../templates/admin_setting_pdns.html | 93 + .../templates/admin_setting_records.html | 83 + {app => powerdnsadmin}/templates/base.html | 42 +- .../templates/dashboard.html | 18 +- .../templates/dashboard_domain.html | 8 +- {app => powerdnsadmin}/templates/domain.html | 2 +- powerdnsadmin/templates/domain_add.html | 180 + powerdnsadmin/templates/domain_setting.html | 284 + {app => powerdnsadmin}/templates/dyndns.html | 0 .../templates/errors/400.html | 6 +- .../templates/errors/403.html | 12 +- .../templates/errors/404.html | 6 +- .../templates/errors/500.html | 6 +- .../templates/errors/SAML.html | 6 +- powerdnsadmin/templates/login.html | 147 + powerdnsadmin/templates/maintenance.html | 43 + powerdnsadmin/templates/register.html | 109 + powerdnsadmin/templates/template.html | 125 + .../templates/template_add.html | 81 +- .../templates/template_edit.html | 4 +- .../templates/user_profile.html | 118 +- {app => powerdnsadmin}/validators.py | 0 requirements.txt | 1 + run.py | 7 +- 99 files changed, 15183 insertions(+), 6386 deletions(-) delete mode 100755 app/__init__.py delete mode 100755 app/customboxes.py delete mode 100644 app/lib/log.py delete mode 100644 app/models.py delete mode 100644 app/oauth.py delete mode 100644 app/templates/admin_editaccount.html delete mode 100644 app/templates/admin_edituser.html delete mode 100644 app/templates/admin_setting_pdns.html delete mode 100644 app/templates/admin_setting_records.html delete mode 100644 app/templates/domain_add.html delete mode 100644 app/templates/domain_management.html delete mode 100644 app/templates/login.html delete mode 100644 app/templates/maintenance.html delete mode 100644 app/templates/register.html delete mode 100644 app/templates/template.html delete mode 100755 app/views.py create mode 100644 migrations/versions/0fb6d23a4863_remove_user_avatar.py create mode 100755 powerdnsadmin/__init__.py rename {app => powerdnsadmin}/assets.py (100%) rename {app => powerdnsadmin}/decorators.py (58%) create mode 100644 powerdnsadmin/default_config.py rename {app => powerdnsadmin}/lib/__init__.py (100%) rename {app => powerdnsadmin}/lib/certutil.py (100%) rename {app => powerdnsadmin/lib}/errors.py (86%) rename {app => powerdnsadmin}/lib/helper.py (65%) rename {app => powerdnsadmin/lib}/schema.py (100%) rename {app => powerdnsadmin}/lib/utils.py (53%) create mode 100644 powerdnsadmin/models.py create mode 100644 powerdnsadmin/models/__init__.py create mode 100644 powerdnsadmin/models/account.py create mode 100644 powerdnsadmin/models/account_user.py create mode 100644 powerdnsadmin/models/api_key.py create mode 100644 powerdnsadmin/models/base.py create mode 100644 powerdnsadmin/models/domain.py create mode 100644 powerdnsadmin/models/domain_setting.py create mode 100644 powerdnsadmin/models/domain_template.py create mode 100644 powerdnsadmin/models/domain_template_record.py create mode 100644 powerdnsadmin/models/domain_user.py create mode 100644 powerdnsadmin/models/history.py create mode 100644 powerdnsadmin/models/record.py create mode 100644 powerdnsadmin/models/record_entry.py create mode 100644 powerdnsadmin/models/role.py create mode 100644 powerdnsadmin/models/server.py create mode 100644 powerdnsadmin/models/setting.py create mode 100644 powerdnsadmin/models/user.py create mode 100644 powerdnsadmin/routes/__init__.py create mode 100644 powerdnsadmin/routes/admin.py rename {app/blueprints => powerdnsadmin/routes}/api.py (54%) create mode 100644 powerdnsadmin/routes/base.py create mode 100644 powerdnsadmin/routes/dashboard.py create mode 100644 powerdnsadmin/routes/domain.py create mode 100644 powerdnsadmin/routes/index.py create mode 100644 powerdnsadmin/routes/user.py create mode 100644 powerdnsadmin/services/__init__.py create mode 100644 powerdnsadmin/services/base.py create mode 100644 powerdnsadmin/services/github.py create mode 100644 powerdnsadmin/services/google.py create mode 100644 powerdnsadmin/services/oidc.py rename {app => powerdnsadmin}/static/custom/css/custom.css (100%) rename {app => powerdnsadmin}/static/custom/js/custom.js (100%) create mode 100644 powerdnsadmin/static/favicon.ico create mode 100644 powerdnsadmin/static/generated/login.css create mode 100644 powerdnsadmin/static/generated/login.js create mode 100644 powerdnsadmin/static/generated/main.css create mode 100644 powerdnsadmin/static/generated/main.js create mode 100644 powerdnsadmin/static/generated/validation.js rename {app => powerdnsadmin}/swagger-spec.yaml (100%) create mode 100644 powerdnsadmin/templates/admin_edit_account.html create mode 100644 powerdnsadmin/templates/admin_edit_user.html rename {app => powerdnsadmin}/templates/admin_history.html (76%) rename app/templates/admin_manageaccount.html => powerdnsadmin/templates/admin_manage_account.html (77%) rename app/templates/admin_manageuser.html => powerdnsadmin/templates/admin_manage_user.html (68%) rename app/templates/admin.html => powerdnsadmin/templates/admin_pdns_stats.html (67%) rename {app => powerdnsadmin}/templates/admin_setting_authentication.html (99%) rename {app => powerdnsadmin}/templates/admin_setting_basic.html (80%) create mode 100644 powerdnsadmin/templates/admin_setting_pdns.html create mode 100644 powerdnsadmin/templates/admin_setting_records.html rename {app => powerdnsadmin}/templates/base.html (81%) rename {app => powerdnsadmin}/templates/dashboard.html (95%) rename {app => powerdnsadmin}/templates/dashboard_domain.html (82%) rename {app => powerdnsadmin}/templates/domain.html (99%) create mode 100644 powerdnsadmin/templates/domain_add.html create mode 100644 powerdnsadmin/templates/domain_setting.html rename {app => powerdnsadmin}/templates/dyndns.html (100%) rename {app => powerdnsadmin}/templates/errors/400.html (75%) rename app/templates/errors/401.html => powerdnsadmin/templates/errors/403.html (66%) rename {app => powerdnsadmin}/templates/errors/404.html (74%) rename {app => powerdnsadmin}/templates/errors/500.html (74%) rename {app => powerdnsadmin}/templates/errors/SAML.html (78%) create mode 100644 powerdnsadmin/templates/login.html create mode 100644 powerdnsadmin/templates/maintenance.html create mode 100644 powerdnsadmin/templates/register.html create mode 100644 powerdnsadmin/templates/template.html rename {app => powerdnsadmin}/templates/template_add.html (57%) rename {app => powerdnsadmin}/templates/template_edit.html (99%) rename {app => powerdnsadmin}/templates/user_profile.html (55%) rename {app => powerdnsadmin}/validators.py (100%) diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100755 index e47b466..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -from werkzeug.contrib.fixers import ProxyFix -from flask import Flask, request, session, redirect, url_for -from flask_login import LoginManager -from flask_sqlalchemy import SQLAlchemy as SA -from flask_migrate import Migrate -from authlib.flask.client import OAuth as AuthlibOAuth -from sqlalchemy.exc import OperationalError -from flask_seasurf import SeaSurf - -### SYBPATCH ### -from app.customboxes import customBoxes -### SYBPATCH ### - -# subclass SQLAlchemy to enable pool_pre_ping -class SQLAlchemy(SA): - def apply_pool_defaults(self, app, options): - SA.apply_pool_defaults(self, app, options) - options["pool_pre_ping"] = True - - -from app.assets import assets - -app = Flask(__name__) -app.config.from_object('config') -app.wsgi_app = ProxyFix(app.wsgi_app) -csrf = SeaSurf(app) - -assets.init_app(app) - -#### CONFIGURE LOGGER #### -from app.lib.log import logger -logging = logger('powerdns-admin', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() - -login_manager = LoginManager() -login_manager.init_app(app) -db = SQLAlchemy(app) # database -migrate = Migrate(app, db) # flask-migrate -authlib_oauth_client = AuthlibOAuth(app) # authlib oauth - -if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'): - from app.lib import certutil - if not certutil.check_certificate(): - certutil.create_self_signed_cert() - -from app import models - -from app.blueprints.api import api_blueprint - -app.register_blueprint(api_blueprint, url_prefix='/api/v1') - -from app import views diff --git a/app/customboxes.py b/app/customboxes.py deleted file mode 100755 index 7b92f49..0000000 --- a/app/customboxes.py +++ /dev/null @@ -1,5 +0,0 @@ - -# "boxId":("title","filter") -class customBoxes: - boxes = {"reverse": (" ", " "), "ip6arpa": ("ip6","%.ip6.arpa"), "inaddrarpa": ("in-addr","%.in-addr.arpa")} - order = ["reverse", "ip6arpa", "inaddrarpa"] diff --git a/app/lib/log.py b/app/lib/log.py deleted file mode 100644 index 973c277..0000000 --- a/app/lib/log.py +++ /dev/null @@ -1,46 +0,0 @@ -import logging - -class logger(object): - def __init__(self, name, level, logfile): - self.name = name - self.level = level - self.logfile = logfile - - def config(self): - # define logger and set logging level - logger = logging.getLogger() - - if self.level == 'CRITICAL': - level = logging.CRITICAL - elif self.level == 'ERROR': - level = logging.ERROR - elif self.level == 'WARNING': - level = logging.WARNING - elif self.level == 'DEBUG': - level = logging.DEBUG - else: - level = logging.INFO - - logger.setLevel(level) - - # set request requests module log level - logging.getLogger("requests").setLevel(logging.CRITICAL) - - if self.logfile: - # define handler to log into file - file_log_handler = logging.FileHandler(self.logfile) - logger.addHandler(file_log_handler) - - # define logging format for file - file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_log_handler.setFormatter(file_formatter) - - # define handler to log into console - stderr_log_handler = logging.StreamHandler() - logger.addHandler(stderr_log_handler) - - # define logging format for console - console_formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] | %(message)s') - stderr_log_handler.setFormatter(console_formatter) - - return logging.getLogger(self.name) diff --git a/app/models.py b/app/models.py deleted file mode 100644 index 721231e..0000000 --- a/app/models.py +++ /dev/null @@ -1,2391 +0,0 @@ -import sys -import os -import re -import ldap -import ldap.filter -import base64 -import bcrypt -import itertools -import traceback -import pyotp -import dns.reversename -import dns.inet -import dns.name -import pytimeparse -import random -import string - -from ast import literal_eval -from datetime import datetime -from urllib.parse import urljoin -from distutils.util import strtobool -from distutils.version import StrictVersion -from flask_login import AnonymousUserMixin -from app import db, app -from app.lib import utils -from app.lib.log import logging - - -class Anonymous(AnonymousUserMixin): - - def __init__(self): - self.username = 'Anonymous' - - -class User(db.Model): - - id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String(64), index=True, unique=True) - password = db.Column(db.String(64)) - firstname = db.Column(db.String(64)) - lastname = db.Column(db.String(64)) - email = db.Column(db.String(128)) - avatar = db.Column(db.String(128)) - otp_secret = db.Column(db.String(16)) - role_id = db.Column(db.Integer, db.ForeignKey('role.id')) - - def __init__(self, id=None, username=None, password=None, plain_text_password=None, firstname=None, lastname=None, role_id=None, email=None, avatar=None, otp_secret=None, reload_info=True): - self.id = id - self.username = username - self.password = password - self.plain_text_password = plain_text_password - self.firstname = firstname - self.lastname = lastname - self.role_id = role_id - self.email = email - self.avatar = avatar - self.otp_secret = otp_secret - - if reload_info: - user_info = self.get_user_info_by_id() if id else self.get_user_info_by_username() - - if user_info: - self.id = user_info.id - self.username = user_info.username - self.firstname = user_info.firstname - self.lastname = user_info.lastname - self.email = user_info.email - self.role_id = user_info.role_id - self.otp_secret = user_info.otp_secret - - def is_authenticated(self): - return True - - def is_active(self): - return True - - def is_anonymous(self): - return False - - def get_id(self): - try: - return unicode(self.id) # python 2 - except NameError: - return str(self.id) # python 3 - - def __repr__(self): - return ''.format(self.username) - - def get_totp_uri(self): - return "otpauth://totp/PowerDNS-Admin:{0}?secret={1}&issuer=PowerDNS-Admin".format(self.username, self.otp_secret) - - def verify_totp(self, token): - totp = pyotp.TOTP(self.otp_secret) - return totp.verify(token) - - def get_hashed_password(self, plain_text_password=None): - # Hash a password for the first time - # (Using bcrypt, the salt is saved into the hash itself) - if plain_text_password is None: - return plain_text_password - - pw = plain_text_password if plain_text_password else self.plain_text_password - return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) - - def check_password(self, hashed_password): - # Check hased password. Using bcrypt, the salt is saved into the hash itself - if (self.plain_text_password): - return bcrypt.checkpw(self.plain_text_password.encode('utf-8'), hashed_password.encode('utf-8')) - return False - - def get_user_info_by_id(self): - user_info = User.query.get(int(self.id)) - return user_info - - def get_user_info_by_username(self): - user_info = User.query.filter(User.username == self.username).first() - return user_info - - def ldap_init_conn(self): - ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) - conn = ldap.initialize(Setting().get('ldap_uri')) - conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF) - conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3) - conn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) - conn.set_option(ldap.OPT_X_TLS_DEMAND, True) - conn.set_option(ldap.OPT_DEBUG_LEVEL, 255) - conn.protocol_version = ldap.VERSION3 - return conn - - def ldap_search(self, searchFilter, baseDN): - searchScope = ldap.SCOPE_SUBTREE - retrieveAttributes = None - - try: - conn = self.ldap_init_conn() - if Setting().get('ldap_type') == 'ad': - conn.simple_bind_s( - "{0}@{1}".format(self.username, Setting().get('ldap_domain')), self.password) - else: - conn.simple_bind_s( - Setting().get('ldap_admin_username'), Setting().get('ldap_admin_password')) - ldap_result_id = conn.search(baseDN, searchScope, searchFilter, retrieveAttributes) - result_set = [] - - while 1: - result_type, result_data = conn.result(ldap_result_id, 0) - if (result_data == []): - break - else: - if result_type == ldap.RES_SEARCH_ENTRY: - result_set.append(result_data) - return result_set - - except ldap.LDAPError as e: - logging.error(e) - logging.debug('baseDN: {0}'.format(baseDN)) - logging.debug(traceback.format_exc()) - - def ldap_auth(self, ldap_username, password): - try: - conn = self.ldap_init_conn() - conn.simple_bind_s(ldap_username, password) - return True - except ldap.LDAPError as e: - logging.error(e) - return False - - def ad_recursive_groups(self, groupDN): - """ - Recursively list groups belonging to a group. It will allow checking deep in the Active Directory - whether a user is allowed to enter or not - """ - LDAP_BASE_DN = Setting().get('ldap_base_dn') - groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap.filter.escape_filter_chars(groupDN) - result = [groupDN] - try: - groups = self.ldap_search(groupSearchFilter, LDAP_BASE_DN) - for group in groups: - result += [group[0][0]] - if 'memberOf' in group[0][1]: - for member in group[0][1]['memberOf']: - result += self.ad_recursive_groups(member.decode("utf-8")) - return result - except ldap.LDAPError as e: - logging.exception("Recursive AD Group search error") - return result - - def is_validate(self, method, src_ip=''): - """ - Validate user credential - """ - role_name = 'User' - - if method == 'LOCAL': - user_info = User.query.filter(User.username == self.username).first() - - if user_info: - if user_info.password and self.check_password(user_info.password): - logging.info( - 'User "{0}" logged in successfully. Authentication request from {1}'.format(self.username, src_ip)) - return True - logging.error( - 'User "{0}" inputted a wrong password. Authentication request from {1}'.format(self.username, src_ip)) - return False - - logging.warning( - 'User "{0}" does not exist. Authentication request from {1}'.format(self.username, src_ip)) - return False - - if method == 'LDAP': - LDAP_TYPE = Setting().get('ldap_type') - LDAP_BASE_DN = Setting().get('ldap_base_dn') - LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic') - LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username') - LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group') - LDAP_OPERATOR_GROUP = Setting().get('ldap_operator_group') - LDAP_USER_GROUP = Setting().get('ldap_user_group') - LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled') - - # validate AD user password - if Setting().get('ldap_type') == 'ad': - ldap_username = "{0}@{1}".format(self.username, Setting().get('ldap_domain')) - if not self.ldap_auth(ldap_username, self.password): - logging.error( - 'User "{0}" input a wrong LDAP password. Authentication request from {1}'.format(self.username, src_ip)) - return False - - searchFilter = "(&({0}={1}){2})".format( - LDAP_FILTER_USERNAME, self.username, LDAP_FILTER_BASIC) - logging.debug('Ldap searchFilter {0}'.format(searchFilter)) - - ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN) - logging.debug('Ldap search result: {0}'.format(ldap_result)) - - if not ldap_result: - logging.warning( - 'LDAP User "{0}" does not exist. Authentication request from {1}'.format(self.username, src_ip)) - return False - else: - try: - ldap_username = ldap.filter.escape_filter_chars(ldap_result[0][0][0]) - - if Setting().get('ldap_type') != 'ad': - # validate ldap user password - if not self.ldap_auth(ldap_username, self.password): - logging.error( - 'User "{0}" input a wrong LDAP password. Authentication request from {1}'.format(self.username, src_ip)) - return False - - # check if LDAP_GROUP_SECURITY_ENABLED is True - # user can be assigned to ADMIN or USER role. - if LDAP_GROUP_SECURITY_ENABLED: - try: - if LDAP_TYPE == 'ldap': - if (self.ldap_search(searchFilter, LDAP_ADMIN_GROUP)): - role_name = 'Administrator' - logging.info( - 'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP)) - elif (self.ldap_search(searchFilter, LDAP_OPERATOR_GROUP)): - role_name = 'Operator' - logging.info('User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'.format( - self.username, LDAP_OPERATOR_GROUP)) - elif (self.ldap_search(searchFilter, LDAP_USER_GROUP)): - logging.info( - 'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP)) - else: - logging.error('User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'.format( - self.username, LDAP_ADMIN_GROUP, LDAP_OPERATOR_GROUP, LDAP_USER_GROUP)) - return False - elif LDAP_TYPE == 'ad': - user_ldap_groups = [] - user_ad_member_of = ldap_result[0][0][1].get('memberOf') - - if not user_ad_member_of: - logging.error( - 'User {0} does not belong to any group while LDAP_GROUP_SECURITY_ENABLED is ON'.format(self.username)) - return False - - for group in [g.decode("utf-8") for g in user_ad_member_of]: - user_ldap_groups += self.ad_recursive_groups(group) - - if (LDAP_ADMIN_GROUP in user_ldap_groups): - role_name = 'Administrator' - logging.info( - 'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP)) - elif (LDAP_OPERATOR_GROUP in user_ldap_groups): - role_name = 'Operator' - logging.info('User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'.format( - self.username, LDAP_OPERATOR_GROUP)) - elif (LDAP_USER_GROUP in user_ldap_groups): - logging.info( - 'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP)) - else: - logging.error('User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'.format( - self.username, LDAP_ADMIN_GROUP, LDAP_OPERATOR_GROUP, LDAP_USER_GROUP)) - return False - else: - logging.error('Invalid LDAP type') - return False - except Exception as e: - logging.error( - 'LDAP group lookup for user "{0}" has failed. Authentication request from {1}'.format(self.username, src_ip)) - logging.debug(traceback.format_exc()) - return False - - except Exception as e: - logging.error('Wrong LDAP configuration. {0}'.format(e)) - logging.debug(traceback.format_exc()) - return False - - # create user if not exist in the db - if not User.query.filter(User.username == self.username).first(): - self.firstname = self.username - self.lastname = '' - try: - # try to get user's firstname, lastname and email address from LDAP attributes - if LDAP_TYPE == 'ldap': - self.firstname = ldap_result[0][0][1]['givenName'][0].decode("utf-8") - self.lastname = ldap_result[0][0][1]['sn'][0].decode("utf-8") - self.email = ldap_result[0][0][1]['mail'][0].decode("utf-8") - elif LDAP_TYPE == 'ad': - self.firstname = ldap_result[0][0][1]['name'][0].decode("utf-8") - self.email = ldap_result[0][0][1]['userPrincipalName'][0].decode("utf-8") - except Exception as e: - logging.warning("Reading ldap data threw an exception {0}".format(e)) - logging.debug(traceback.format_exc()) - - # first register user will be in Administrator role - if User.query.count() == 0: - self.role_id = Role.query.filter_by(name='Administrator').first().id - else: - self.role_id = Role.query.filter_by(name=role_name).first().id - - self.create_user() - logging.info('Created user "{0}" in the DB'.format(self.username)) - - # user already exists in database, set their role based on group membership (if enabled) - if LDAP_GROUP_SECURITY_ENABLED: - self.set_role(role_name) - - return True - else: - logging.error('Unsupported authentication method') - return False - - def get_apikeys(self, domain_name=None): - info = [] - apikey_query = db.session.query(ApiKey) \ - .join(Domain.apikeys) \ - .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ - .outerjoin(Account, Domain.account_id == Account.id) \ - .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ - .filter( - db.or_( - DomainUser.user_id == User.id, - AccountUser.user_id == User.id - ) - ) \ - .filter(User.id == self.id) - - if domain_name: - info = apikey_query.filter(Domain.name == domain_name).all() - else: - info = apikey_query.all() - - return info - - def create_user(self): - """ - If user logged in successfully via LDAP in the first time - We will create a local user (in DB) in order to manage user - profile such as name, roles,... - """ - - # Set an invalid password hash for non local users - self.password = '*' - - db.session.add(self) - db.session.commit() - - def create_local_user(self): - """ - Create local user witch stores username / password in the DB - """ - # check if username existed - user = User.query.filter(User.username == self.username).first() - if user: - return {'status': False, 'msg': 'Username is already in use'} - - # check if email existed - user = User.query.filter(User.email == self.email).first() - if user: - return {'status': False, 'msg': 'Email address is already in use'} - - # first register user will be in Administrator role - self.role_id = Role.query.filter_by(name='User').first().id - if User.query.count() == 0: - self.role_id = Role.query.filter_by(name='Administrator').first().id - - self.password = self.get_hashed_password( - self.plain_text_password) if self.plain_text_password else '*' - - if self.password and self.password != '*': - self.password = self.password.decode("utf-8") - - db.session.add(self) - db.session.commit() - return {'status': True, 'msg': 'Created user successfully'} - - def update_local_user(self): - """ - Update local user - """ - # Sanity check - account name - if self.username == "": - return {'status': False, 'msg': 'No user name specified'} - - # read user and check that it exists - user = User.query.filter(User.username == self.username).first() - if not user: - return {'status': False, 'msg': 'User does not exist'} - - # check if new email exists (only if changed) - if user.email != self.email: - checkuser = User.query.filter(User.email == self.email).first() - if checkuser: - return {'status': False, 'msg': 'New email address is already in use'} - - user.firstname = self.firstname - user.lastname = self.lastname - user.email = self.email - - # store new password hash (only if changed) - if self.plain_text_password != "": - user.password = self.get_hashed_password(self.plain_text_password).decode("utf-8") - - db.session.commit() - return {'status': True, 'msg': 'User updated successfully'} - - def update_profile(self, enable_otp=None): - """ - Update user profile - """ - - user = User.query.filter(User.username == self.username).first() - if not user: - return False - - user.firstname = self.firstname if self.firstname else user.firstname - user.lastname = self.lastname if self.lastname else user.lastname - user.email = self.email if self.email else user.email - user.password = self.get_hashed_password(self.plain_text_password).decode( - "utf-8") if self.plain_text_password else user.password - user.avatar = self.avatar if self.avatar else user.avatar - - if enable_otp is not None: - user.otp_secret = "" - - if enable_otp == True: - # generate the opt secret key - user.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') - - try: - db.session.add(user) - db.session.commit() - return True - except Exception: - db.session.rollback() - return False - - def get_account_query(self): - """ - Get query for account to which the user is associated. - """ - return db.session.query(Account) \ - .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ - .filter(AccountUser.user_id == self.id) - - def get_account(self): - """ - Get all accounts to which the user is associated. - """ - return self.get_account_query() - - def get_domain_query(self): - """ - Get query for domain to which the user has access permission. - This includes direct domain permission AND permission through - account membership - """ - return db.session.query(Domain) \ - .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ - .outerjoin(Account, Domain.account_id == Account.id) \ - .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ - .filter( - db.or_( - DomainUser.user_id == User.id, - AccountUser.user_id == User.id - ) - ) \ - .filter(User.id == self.id) - - def get_domain(self): - """ - Get domains which user has permission to - access - """ - return self.get_domain_query() - - def get_domains(self): - return self.get_domain_query().all() - - def delete(self): - """ - Delete a user - """ - # revoke all user privileges and account associations first - self.revoke_privilege() - for a in self.get_account(): - a.revoke_privileges_by_id(self.id) - - try: - User.query.filter(User.username == self.username).delete() - db.session.commit() - return True - except Exception as e: - db.session.rollback() - logging.error('Cannot delete user {0} from DB. DETAIL: {1}'.format(self.username, e)) - return False - - def revoke_privilege(self): - """ - Revoke all privileges from a user - """ - user = User.query.filter(User.username == self.username).first() - - if user: - user_id = user.id - try: - DomainUser.query.filter(DomainUser.user_id == user_id).delete() - db.session.commit() - return True - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot revoke user {0} privileges. DETAIL: {1}'.format(self.username, e)) - return False - return False - - def set_role(self, role_name): - role = Role.query.filter(Role.name == role_name).first() - if role: - user = User.query.filter(User.username == self.username).first() - user.role_id = role.id - db.session.commit() - return {'status': True, 'msg': 'Set user role successfully'} - else: - return {'status': False, 'msg': 'Role does not exist'} - - -class Account(db.Model): - __tablename__ = 'account' - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(40), index=True, unique=True, nullable=False) - description = db.Column(db.String(128)) - contact = db.Column(db.String(128)) - mail = db.Column(db.String(128)) - domains = db.relationship("Domain", back_populates="account") - - def __init__(self, name=None, description=None, contact=None, mail=None): - self.name = name - self.description = description - self.contact = contact - self.mail = mail - - if self.name is not None: - self.name = ''.join( - c for c in self.name.lower() if c in "abcdefghijklmnopqrstuvwxyz0123456789") - - def __repr__(self): - return ''.format(self.name) - - def get_name_by_id(self, account_id): - """ - Convert account_id to account_name - """ - account = Account.query.filter(Account.id == account_id).first() - if account is None: - return '' - - return account.name - - def get_id_by_name(self, account_name): - """ - Convert account_name to account_id - """ - # Skip actual database lookup for empty queries - if account_name is None or account_name == "": - return None - - account = Account.query.filter(Account.name == account_name).first() - if account is None: - return None - - return account.id - - def unassociate_domains(self): - """ - Remove associations to this account from all domains - """ - account = Account.query.filter(Account.name == self.name).first() - for domain in account.domains: - Domain(name=domain.name).assoc_account(None) - - def create_account(self): - """ - Create a new account - """ - # Sanity check - account name - if self.name == "": - return {'status': False, 'msg': 'No account name specified'} - - # check that account name is not already used - account = Account.query.filter(Account.name == self.name).first() - if account: - return {'status': False, 'msg': 'Account already exists'} - - db.session.add(self) - db.session.commit() - return {'status': True, 'msg': 'Account created successfully'} - - def update_account(self): - """ - Update an existing account - """ - # Sanity check - account name - if self.name == "": - return {'status': False, 'msg': 'No account name specified'} - - # read account and check that it exists - account = Account.query.filter(Account.name == self.name).first() - if not account: - return {'status': False, 'msg': 'Account does not exist'} - - account.description = self.description - account.contact = self.contact - account.mail = self.mail - - db.session.commit() - return {'status': True, 'msg': 'Account updated successfully'} - - def delete_account(self): - """ - Delete an account - """ - # unassociate all domains and users first - self.unassociate_domains() - self.grant_privileges([]) - - try: - Account.query.filter(Account.name == self.name).delete() - db.session.commit() - return True - - except Exception as e: - db.session.rollback() - logging.error('Cannot delete account {0} from DB. DETAIL: {1}'.format(self.username, e)) - return False - - def get_user(self): - """ - Get users (id) associated with this account - """ - user_ids = [] - query = db.session.query(AccountUser, Account).filter(User.id == AccountUser.user_id).filter( - Account.id == AccountUser.account_id).filter(Account.name == self.name).all() - for q in query: - user_ids.append(q[0].user_id) - return user_ids - - def grant_privileges(self, new_user_list): - """ - Reconfigure account_user table - """ - account_id = self.get_id_by_name(self.name) - - account_user_ids = self.get_user() - new_user_ids = [u.id for u in User.query.filter( - User.username.in_(new_user_list)).all()] if new_user_list else [] - - removed_ids = list(set(account_user_ids).difference(new_user_ids)) - added_ids = list(set(new_user_ids).difference(account_user_ids)) - - try: - for uid in removed_ids: - AccountUser.query.filter(AccountUser.user_id == uid).filter( - AccountUser.account_id == account_id).delete() - db.session.commit() - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot revoke user privileges on account {0}. DETAIL: {1}'.format(self.name, e)) - - try: - for uid in added_ids: - au = AccountUser(account_id, uid) - db.session.add(au) - db.session.commit() - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot grant user privileges to account {0}. DETAIL: {1}'.format(self.name, e)) - - def revoke_privileges_by_id(self, user_id): - """ - Remove a single user from privilege list based on user_id - """ - new_uids = [u for u in self.get_user() if u != user_id] - users = [] - for uid in new_uids: - users.append(User(id=uid).get_user_info_by_id().username) - - self.grant_privileges(users) - - def add_user(self, user): - """ - Add a single user to Account by User - """ - try: - au = AccountUser(self.id, user.id) - db.session.add(au) - db.session.commit() - return True - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot add user privileges on account {0}. DETAIL: {1}'.format(self.name, e)) - return False - - def remove_user(self, user): - """ - Remove a single user from Account by User - """ - try: - AccountUser.query.filter(AccountUser.user_id == user.id).filter( - AccountUser.account_id == self.id).delete() - db.session.commit() - return True - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot revoke user privileges on account {0}. DETAIL: {1}'.format(self.name, e)) - return False - - -class Role(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64), index=True, unique=True) - description = db.Column(db.String(128)) - users = db.relationship('User', backref='role', lazy=True) - apikeys = db.relationship('ApiKey', back_populates='role', lazy=True) - - def __init__(self, id=None, name=None, description=None): - self.id = id - self.name = name - self.description = description - - # allow database autoincrement to do its own ID assignments - def __init__(self, name=None, description=None): - self.id = None - self.name = name - self.description = description - - def __repr__(self): - return ''.format(self.name) - - -class DomainSetting(db.Model): - __tablename__ = 'domain_setting' - id = db.Column(db.Integer, primary_key=True) - domain_id = db.Column(db.Integer, db.ForeignKey('domain.id')) - domain = db.relationship('Domain', back_populates='settings') - setting = db.Column(db.String(255), nullable=False) - value = db.Column(db.String(255)) - - def __init__(self, id=None, setting=None, value=None): - self.id = id - self.setting = setting - self.value = value - - def __repr__(self): - return ''.format(setting, self.domain.name) - - def __eq__(self, other): - return type(self) == type(other) and self.setting == other.setting - - def set(self, value): - try: - self.value = value - db.session.commit() - return True - except Exception as e: - logging.error('Unable to set DomainSetting value. DETAIL: {0}'.format(e)) - logging.debug(traceback.format_exc()) - db.session.rollback() - return False - -domain_apikey = db.Table('domain_apikey', - db.Column('domain_id', db.Integer, db.ForeignKey('domain.id')), - db.Column('apikey_id', db.Integer, db.ForeignKey('apikey.id')) -) - - -class Domain(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), index=True, unique=True) - master = db.Column(db.String(128)) - type = db.Column(db.String(6), nullable=False) - serial = db.Column(db.Integer) - notified_serial = db.Column(db.Integer) - last_check = db.Column(db.Integer) - dnssec = db.Column(db.Integer) - account_id = db.Column(db.Integer, db.ForeignKey('account.id')) - account = db.relationship("Account", back_populates="domains") - settings = db.relationship('DomainSetting', back_populates='domain') - apikeys = db.relationship( - "ApiKey", - secondary=domain_apikey, - back_populates="domains" - ) - - def __init__(self, id=None, name=None, master=None, type='NATIVE', serial=None, notified_serial=None, last_check=None, dnssec=None, account_id=None): - self.id = id - self.name = name - self.master = master - self.type = type - self.serial = serial - self.notified_serial = notified_serial - self.last_check = last_check - self.dnssec = dnssec - self.account_id = account_id - # PDNS configs - self.PDNS_STATS_URL = Setting().get('pdns_api_url') - self.PDNS_API_KEY = Setting().get('pdns_api_key') - self.PDNS_VERSION = Setting().get('pdns_version') - self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) - - if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'): - self.NEW_SCHEMA = True - else: - self.NEW_SCHEMA = False - - def __repr__(self): - return ''.format(self.name) - - def add_setting(self, setting, value): - try: - self.settings.append(DomainSetting(setting=setting, value=value)) - db.session.commit() - return True - except Exception as e: - logging.error( - 'Can not create setting {0} for domain {1}. {2}'.format(setting, self.name, e)) - return False - - def get_domain_info(self, domain_name): - """ - Get all domains which has in PowerDNS - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers) - return jdata - - def get_domains(self): - """ - Get all domains which has in PowerDNS - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) - return jdata - - def get_id_by_name(self, name): - """ - Return domain id - """ - try: - domain = Domain.query.filter(Domain.name == name).first() - return domain.id - except Exception as e: - logging.error('Domain does not exist. ERROR: {0}'.format(e)) - return None - - def update(self): - """ - Fetch zones (domains) from PowerDNS and update into DB - """ - db_domain = Domain.query.all() - list_db_domain = [d.name for d in db_domain] - dict_db_domain = dict((x.name, x) for x in db_domain) - logging.info("{} Entrys in pdnsADMIN".format(len(list_db_domain))) - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers) - list_jdomain = [d['name'].rstrip('.') for d in jdata] - logging.info("{} Entrys in PDNSApi".format(len(list_jdomain))) - - try: - # domains should remove from db since it doesn't exist in powerdns anymore - should_removed_db_domain = list(set(list_db_domain).difference(list_jdomain)) - for domain_name in should_removed_db_domain: - self.delete_domain_from_pdnsadmin(domain_name) - except Exception as e: - logging.error('Can not delete domain from DB. DETAIL: {0}'.format(e)) - logging.debug(traceback.format_exc()) - - # update/add new domain - for data in jdata: - if 'account' in data: - account_id = Account().get_id_by_name(data['account']) - else: - logging.debug( - "No 'account' data found in API result - Unsupported PowerDNS version?") - account_id = None - domain = dict_db_domain.get(data['name'].rstrip('.'), None) - if domain: - self.update_pdns_admin_domain(domain, account_id, data) - else: - # add new domain - self.add_domain_to_powerdns_admin(domain=data) - - logging.info('Update finished') - return {'status': 'ok', 'msg': 'Domain table has been updated successfully'} - except Exception as e: - logging.error('Can not update domain table. Error: {0}'.format(e)) - return {'status': 'error', 'msg': 'Can not update domain table'} - - def update_pdns_admin_domain(self, domain, account_id, data): - # existing domain, only update if something actually has changed - if (domain.master != str(data['masters']) - or domain.type != data['kind'] - or domain.serial != data['serial'] - or domain.notified_serial != data['notified_serial'] - or domain.last_check != (1 if data['last_check'] else 0) - or domain.dnssec != data['dnssec'] - or domain.account_id != account_id): - - domain.master = str(data['masters']) - domain.type = data['kind'] - domain.serial = data['serial'] - domain.notified_serial = data['notified_serial'] - domain.last_check = 1 if data['last_check'] else 0 - domain.dnssec = 1 if data['dnssec'] else 0 - domain.account_id = account_id - try: - db.session.commit() - logging.info("updated PDNS-Admin Domain {0}".format(domain)) - except Exception as e: - db.session.rollback() - logging.info("Rolledback Domain {0} {1}".format(domain.name, e)) - raise - - def add(self, domain_name, domain_type, soa_edit_api, domain_ns=[], domain_master_ips=[], account_name=None): - """ - Add a domain to power dns - """ - - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - if self.NEW_SCHEMA: - domain_name = domain_name + '.' - domain_ns = [ns + '.' for ns in domain_ns] - - if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]: - soa_edit_api = 'DEFAULT' - - elif soa_edit_api == 'OFF': - soa_edit_api = '' - - post_data = { - "name": domain_name, - "kind": domain_type, - "masters": domain_master_ips, - "nameservers": domain_ns, - "soa_edit_api": soa_edit_api, - "account": account_name - } - - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones'), headers=headers, method='POST', data=post_data) - if 'error' in jdata.keys(): - logging.error(jdata['error']) - return {'status': 'error', 'msg': jdata['error']} - else: - logging.info('Added domain successfully to PowerDNS: {0}'.format(domain_name)) - self.add_domain_to_powerdns_admin(domain_dict=post_data) - return {'status': 'ok', 'msg': 'Added domain successfully'} - except Exception as e: - logging.error('Cannot add domain {0} {1}'.format(domain_name, e)) - logging.debug(traceback.format_exc()) - return {'status': 'error', 'msg': 'Cannot add this domain.'} - - def add_domain_to_powerdns_admin(self, domain=None, domain_dict=None): - """ - Read Domain from PowerDNS and add into PDNS-Admin - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - if not domain: - try: - domain = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_dict['name'])), headers=headers) - except Exception as e: - logging.error('Can not read Domain from PDNS') - logging.error(e) - logging.debug(traceback.format_exc()) - - if 'account' in domain: - account_id = Account().get_id_by_name(domain['account']) - else: - logging.debug( - "No 'account' data found in API result - Unsupported PowerDNS version?") - account_id = None - # add new domain - d = Domain() - d.name = domain['name'].rstrip('.') - d.master = str(domain['masters']) - d.type = domain['kind'] - d.serial = domain['serial'] - d.notified_serial = domain['notified_serial'] - d.last_check = domain['last_check'] - d.dnssec = 1 if domain['dnssec'] else 0 - d.account_id = account_id - db.session.add(d) - try: - db.session.commit() - logging.info("Synched PowerDNS Domain to PDNS-Admin: {0}".format(d.name)) - return {'status': 'ok', 'msg': 'Added Domain successfully to PowerDNS-Admin'} - except Exception as e: - db.session.rollback() - logging.info("Rolledback Domain {0}".format(d.name)) - raise - - def update_soa_setting(self, domain_name, soa_edit_api): - domain = Domain.query.filter(Domain.name == domain_name).first() - if not domain: - return {'status': 'error', 'msg': 'Domain doesnt exist.'} - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]: - soa_edit_api = 'DEFAULT' - - elif soa_edit_api == 'OFF': - soa_edit_api = '' - - post_data = { - "soa_edit_api": soa_edit_api, - "kind": domain.type - } - - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, - method='PUT', data=post_data) - if 'error' in jdata.keys(): - logging.error(jdata['error']) - return {'status': 'error', 'msg': jdata['error']} - else: - logging.info('soa-edit-api changed for domain {0} successfully'.format(domain_name)) - return {'status': 'ok', 'msg': 'soa-edit-api changed successfully'} - except Exception as e: - logging.debug(e) - logging.debug(traceback.format_exc()) - logging.error('Cannot change soa-edit-api for domain {0}'.format(domain_name)) - return {'status': 'error', 'msg': 'Cannot change soa-edit-api 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().get('auto_ptr') - 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', 'DEFAULT', '', '') - self.update() - if result['status'] == 'ok': - history = History(msg='Add reverse lookup domain {0}'.format(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_privileges(domain_users) - return {'status': 'ok', 'msg': 'New reverse lookup domain created with granted privileges'} - 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.+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.+6.arpa)\.?)', reverse_host_address).group('ipname') - else: - for i in range(1, 4, 1): - address = re.search( - '((([0-9]+\.){' + str(i) + '})(?P.+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.+r.arpa)\.?)', reverse_host_address).group('ipname') - - def delete(self, domain_name): - """ - Delete a single domain name from powerdns - """ - try: - self.delete_domain_from_powerdns(domain_name) - self.delete_domain_from_pdnsadmin(domain_name) - return {'status': 'ok', 'msg': 'Delete domain successfully'} - except Exception as e: - logging.error('Cannot delete domain {0}'.format(domain_name)) - logging.error(e) - logging.debug(traceback.format_exc()) - return {'status': 'error', 'msg': 'Cannot delete domain'} - - def delete_domain_from_powerdns(self, domain_name): - """ - Delete a single domain name from powerdns - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + - '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers, method='DELETE') - logging.info('Deleted domain successfully from PowerDNS-Entity: {0}'.format(domain_name)) - return {'status': 'ok', 'msg': 'Delete domain successfully'} - - def delete_domain_from_pdnsadmin(self, domain_name): - # Revoke permission before deleting domain - domain = Domain.query.filter(Domain.name == domain_name).first() - domain_user = DomainUser.query.filter(DomainUser.domain_id == domain.id) - if domain_user: - domain_user.delete() - db.session.commit() - domain_setting = DomainSetting.query.filter( - DomainSetting.domain_id == domain.id) - if domain_setting: - domain_setting.delete() - db.session.commit() - domain.apikeys[:] = [] - db.session.commit() - - # then remove domain - Domain.query.filter(Domain.name == domain_name).delete() - db.session.commit() - logging.info("Deleted Domain successfully from pdnsADMIN: {}".format(domain_name)) - - def get_user(self): - """ - Get users (id) who have access to this domain name - """ - user_ids = [] - query = db.session.query(DomainUser, Domain).filter(User.id == DomainUser.user_id).filter( - Domain.id == DomainUser.domain_id).filter(Domain.name == self.name).all() - for q in query: - user_ids.append(q[0].user_id) - return user_ids - - def grant_privileges(self, new_user_list): - """ - Reconfigure domain_user table - """ - - domain_id = self.get_id_by_name(self.name) - - domain_user_ids = self.get_user() - new_user_ids = [u.id for u in User.query.filter( - User.username.in_(new_user_list)).all()] if new_user_list else [] - - removed_ids = list(set(domain_user_ids).difference(new_user_ids)) - added_ids = list(set(new_user_ids).difference(domain_user_ids)) - - try: - for uid in removed_ids: - DomainUser.query.filter(DomainUser.user_id == uid).filter( - DomainUser.domain_id == domain_id).delete() - db.session.commit() - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot revoke user privileges on domain {0}. DETAIL: {1}'.format(self.name, e)) - - try: - for uid in added_ids: - du = DomainUser(domain_id, uid) - db.session.add(du) - db.session.commit() - except Exception as e: - db.session.rollback() - logging.error( - 'Cannot grant user privileges to domain {0}. DETAIL: {1}'.format(self.name, e)) - - def update_from_master(self, domain_name): - """ - Update records from Master DNS server - """ - domain = Domain.query.filter(Domain.name == domain_name).first() - if domain: - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - try: - utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/axfr-retrieve'.format(domain.name)), headers=headers, method='PUT') - return {'status': 'ok', 'msg': 'Update from Master successfully'} - except Exception as e: - logging.error('Cannot update from master. DETAIL: {0}'.format(e)) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} - else: - return {'status': 'error', 'msg': 'This domain doesnot exist'} - - def get_domain_dnssec(self, domain_name): - """ - Get domain DNSSEC information - """ - domain = Domain.query.filter(Domain.name == domain_name).first() - if domain: - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='GET') - if 'error' in jdata: - return {'status': 'error', 'msg': 'DNSSEC is not enabled for this domain'} - else: - return {'status': 'ok', 'dnssec': jdata} - except Exception as e: - logging.error('Cannot get domain dnssec. DETAIL: {0}'.format(e)) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} - else: - return {'status': 'error', 'msg': 'This domain doesnot exist'} - - def enable_domain_dnssec(self, domain_name): - """ - Enable domain DNSSEC - """ - domain = Domain.query.filter(Domain.name == domain_name).first() - if domain: - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - try: - # Enable API-RECTIFY for domain, BEFORE activating DNSSEC - post_data = { - "api_rectify": True - } - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) - if 'error' in jdata: - return {'status': 'error', 'msg': 'API-RECTIFY could not be enabled for this domain', 'jdata': jdata} - - # Activate DNSSEC - post_data = { - "keytype": "ksk", - "active": True - } - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys'.format(domain.name)), headers=headers, method='POST', data=post_data) - if 'error' in jdata: - return {'status': 'error', 'msg': 'Cannot enable DNSSEC for this domain. Error: {0}'.format(jdata['error']), 'jdata': jdata} - - return {'status': 'ok'} - - except Exception as e: - logging.error('Cannot enable dns sec. DETAIL: {}'.format(e)) - logging.debug(traceback.format_exc()) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} - - else: - return {'status': 'error', 'msg': 'This domain does not exist'} - - def delete_dnssec_key(self, domain_name, key_id): - """ - Remove keys DNSSEC - """ - domain = Domain.query.filter(Domain.name == domain_name).first() - if domain: - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - try: - # Deactivate DNSSEC - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}/cryptokeys/{1}'.format(domain.name, key_id)), headers=headers, method='DELETE') - if jdata != True: - return {'status': 'error', 'msg': 'Cannot disable DNSSEC for this domain. Error: {0}'.format(jdata['error']), 'jdata': jdata} - - # Disable API-RECTIFY for domain, AFTER deactivating DNSSEC - post_data = { - "api_rectify": False - } - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain.name)), headers=headers, method='PUT', data=post_data) - if 'error' in jdata: - return {'status': 'error', 'msg': 'API-RECTIFY could not be disabled for this domain', 'jdata': jdata} - - return {'status': 'ok'} - - except Exception as e: - logging.error('Cannot delete dnssec key. DETAIL: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator', 'domain': domain.name, 'id': key_id} - - else: - return {'status': 'error', 'msg': 'This domain doesnot exist'} - - def assoc_account(self, account_id): - """ - Associate domain with a domain, specified by account id - """ - domain_name = self.name - - # Sanity check - domain name - if domain_name == "": - return {'status': False, 'msg': 'No domain name specified'} - - # read domain and check that it exists - domain = Domain.query.filter(Domain.name == domain_name).first() - if not domain: - return {'status': False, 'msg': 'Domain does not exist'} - - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - account_name = Account().get_name_by_id(account_id) - - post_data = { - "account": account_name - } - - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain_name)), headers=headers, - method='PUT', data=post_data) - - if 'error' in jdata.keys(): - logging.error(jdata['error']) - return {'status': 'error', 'msg': jdata['error']} - else: - self.update() - msg_str = 'Account changed for domain {0} successfully' - logging.info(msg_str.format(domain_name)) - return {'status': 'ok', 'msg': 'account changed successfully'} - - except Exception as e: - logging.debug(e) - logging.debug(traceback.format_exc()) - msg_str = 'Cannot change account for domain {0}' - logging.error(msg_str.format(domain_name)) - return { - 'status': 'error', - 'msg': 'Cannot change account for this domain.' - } - - def get_account(self): - """ - Get current account associated with this domain - """ - domain = Domain.query.filter(Domain.name == self.name).first() - - return domain.account - - -class DomainUser(db.Model): - __tablename__ = 'domain_user' - id = db.Column(db.Integer, primary_key=True) - domain_id = db.Column(db.Integer, db.ForeignKey('domain.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - def __init__(self, domain_id, user_id): - self.domain_id = domain_id - self.user_id = user_id - - def __repr__(self): - return ''.format(self.domain_id, self.user_id) - - -class AccountUser(db.Model): - __tablename__ = 'account_user' - id = db.Column(db.Integer, primary_key=True) - account_id = db.Column(db.Integer, db.ForeignKey('account.id'), nullable=False) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) - - def __init__(self, account_id, user_id): - self.account_id = account_id - self.user_id = user_id - - def __repr__(self): - return ''.format(self.account_id, self.user_id) - - -class RecordEntry(object): - """ - This is not a model, it's just an object - which will store records entries from PowerDNS API - """ - - def __init__(self, name=None, type=None, status=None, ttl=None, data=None, - is_allowed_edit=False): - self.name = name - self.type = type - self.status = status - self.ttl = ttl - self.data = data - self._is_allowed_edit = is_allowed_edit - self._is_allowed_delete = is_allowed_edit and self.type != 'SOA' - - def is_allowed_edit(self): - return self._is_allowed_edit - - def is_allowed_delete(self): - return self._is_allowed_delete - - -class Record(object): - - """ - This is not a model, it's just an object - which be assigned data from PowerDNS API - """ - def __init__(self, name=None, type=None, status=None, ttl=None, data=None): - self.name = name - self.type = type - self.status = status - self.ttl = ttl - self.data = data - # PDNS configs - self.PDNS_STATS_URL = Setting().get('pdns_api_url') - self.PDNS_API_KEY = Setting().get('pdns_api_key') - self.PDNS_VERSION = Setting().get('pdns_version') - self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) - self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr') - - if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'): - self.NEW_SCHEMA = True - else: - self.NEW_SCHEMA = False - - def get_record_data(self, domain): - """ - Query domain's DNS records via API - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers) - except Exception as e: - logging.error( - "Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}".format(e)) - return False - - if self.NEW_SCHEMA: - rrsets = jdata['rrsets'] - for rrset in rrsets: - r_name = rrset['name'].rstrip('.') - if self.PRETTY_IPV6_PTR: # only if activated - if rrset['type'] == 'PTR': # only ptr - if 'ip6.arpa' in r_name: # only if v6-ptr - r_name = dns.reversename.to_address(dns.name.from_text(r_name)) - - rrset['name'] = r_name - rrset['content'] = rrset['records'][0]['content'] - rrset['disabled'] = rrset['records'][0]['disabled'] - return {'records': rrsets} - - return jdata - - def add(self, domain): - """ - Add a record to domain - """ - # validate record first - r = self.get_record_data(domain) - records = r['records'] - check = list(filter(lambda check: check['name'] == self.name, records)) - if check: - r = check[0] - if r['type'] in ('A', 'AAAA', 'CNAME'): - return {'status': 'error', 'msg': 'Record already exists with type "A", "AAAA" or "CNAME"'} - - # continue if the record is ready to be added - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - if self.NEW_SCHEMA: - data = {"rrsets": [ - { - "name": self.name.rstrip('.') + '.', - "type": self.type, - "changetype": "REPLACE", - "ttl": self.ttl, - "records": [ - { - "content": self.data, - "disabled": self.status, - } - ] - } - ] - } - else: - data = {"rrsets": [ - { - "name": self.name, - "type": self.type, - "changetype": "REPLACE", - "records": [ - { - "content": self.data, - "disabled": self.status, - "name": self.name, - "ttl": self.ttl, - "type": self.type - } - ] - } - ] - } - - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) - logging.debug(jdata) - return {'status': 'ok', 'msg': 'Record was added successfully'} - except Exception as e: - logging.error("Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".format( - self.name, self.type, self.data, domain, e)) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} - - def compare(self, domain_name, new_records): - """ - Compare new records with current powerdns record data - Input is a list of hashes (records) - """ - # get list of current records we have in powerdns - current_records = self.get_record_data(domain_name)['records'] - - # convert them to list of list (just has [name, type]) instead of list of hash - # to compare easier - list_current_records = [[x['name'], x['type']] for x in current_records] - list_new_records = [[x['name'], x['type']] for x in new_records] - - # get list of deleted records - # they are the records which exist in list_current_records but not in list_new_records - list_deleted_records = [x for x in list_current_records if x not in list_new_records] - - # convert back to list of hash - deleted_records = [x for x in current_records if [x['name'], x['type']] in list_deleted_records and ( - x['type'] in Setting().get_records_allow_to_edit() and x['type'] != 'SOA')] - - # return a tuple - return deleted_records, new_records - - def apply(self, domain, post_records): - """ - Apply record changes to domain - """ - records = [] - for r in post_records: - r_name = domain if r['record_name'] in ['@', ''] else r['record_name'] + '.' + domain - r_type = r['record_type'] - if self.PRETTY_IPV6_PTR: # only if activated - if self.NEW_SCHEMA: # only if new schema - if r_type == 'PTR': # only ptr - if ':' in r['record_name']: # dirty ipv6 check - r_name = r['record_name'] - - r_data = domain if r_type == 'CNAME' and r[ - 'record_data'] in ['@', ''] else r['record_data'] - - record = { - "name": r_name, - "type": r_type, - "content": r_data, - "disabled": True if r['record_status'] == 'Disabled' else False, - "ttl": int(r['record_ttl']) if r['record_ttl'] else 3600, - } - records.append(record) - - deleted_records, new_records = self.compare(domain, records) - - records = [] - for r in deleted_records: - r_name = r['name'].rstrip('.') + '.' if self.NEW_SCHEMA else r['name'] - r_type = r['type'] - if self.PRETTY_IPV6_PTR: # only if activated - if self.NEW_SCHEMA: # only if new schema - if r_type == 'PTR': # only ptr - if ':' in r['name']: # dirty ipv6 check - r_name = dns.reversename.from_address(r['name']).to_text() - - record = { - "name": r_name, - "type": r_type, - "changetype": "DELETE", - "records": [ - ] - } - records.append(record) - - postdata_for_delete = {"rrsets": records} - - records = [] - for r in new_records: - if self.NEW_SCHEMA: - r_name = r['name'].rstrip('.') + '.' - r_type = r['type'] - if self.PRETTY_IPV6_PTR: # only if activated - if r_type == 'PTR': # only ptr - if ':' in r['name']: # dirty ipv6 check - r_name = r['name'] - - record = { - "name": r_name, - "type": r_type, - "changetype": "REPLACE", - "ttl": r['ttl'], - "records": [ - { - "content": r['content'], - "disabled": r['disabled'], - } - ] - } - else: - record = { - "name": r['name'], - "type": r['type'], - "changetype": "REPLACE", - "records": [ - { - "content": r['content'], - "disabled": r['disabled'], - "name": r['name'], - "ttl": r['ttl'], - "type": r['type'], - "priority": 10, # priority field for pdns 3.4.1. https://doc.powerdns.com/md/authoritative/upgrading/ - } - ] - } - - records.append(record) - - # Adjustment to add multiple records which described in - # https://github.com/ngoduykhanh/PowerDNS-Admin/issues/5#issuecomment-181637576 - final_records = [] - records = sorted(records, key=lambda item: ( - item["name"], item["type"], item["changetype"])) - for key, group in itertools.groupby(records, lambda item: (item["name"], item["type"], item["changetype"])): - if self.NEW_SCHEMA: - r_name = key[0] - r_type = key[1] - r_changetype = key[2] - - if self.PRETTY_IPV6_PTR: # only if activated - if r_type == 'PTR': # only ptr - if ':' in r_name: # dirty ipv6 check - r_name = dns.reversename.from_address(r_name).to_text() - - new_record = { - "name": r_name, - "type": r_type, - "changetype": r_changetype, - "ttl": None, - "records": [] - } - for item in group: - temp_content = item['records'][0]['content'] - temp_disabled = item['records'][0]['disabled'] - if key[1] in ['MX', 'CNAME', 'SRV', 'NS']: - if temp_content.strip()[-1:] != '.': - temp_content += '.' - - if new_record['ttl'] is None: - new_record['ttl'] = item['ttl'] - new_record['records'].append({ - "content": temp_content, - "disabled": temp_disabled - }) - final_records.append(new_record) - - else: - - final_records.append({ - "name": key[0], - "type": key[1], - "changetype": key[2], - "records": [ - { - "content": item['records'][0]['content'], - "disabled": item['records'][0]['disabled'], - "name": key[0], - "ttl": item['records'][0]['ttl'], - "type": key[1], - "priority": 10, - } for item in group - ] - }) - - postdata_for_new = {"rrsets": final_records} - logging.debug(postdata_for_new) - logging.debug(postdata_for_delete) - logging.info( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain))) - try: - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - jdata1 = utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format( - domain)), headers=headers, method='PATCH', data=postdata_for_delete) - jdata2 = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=postdata_for_new) - - if 'error' in jdata1.keys(): - logging.error('Cannot apply record changes.') - logging.debug(jdata1['error']) - return {'status': 'error', 'msg': jdata1['error']} - elif 'error' in jdata2.keys(): - logging.error('Cannot apply record changes.') - logging.debug(jdata2['error']) - return {'status': 'error', 'msg': jdata2['error']} - else: - self.auto_ptr(domain, new_records, deleted_records) - self.update_db_serial(domain) - logging.info('Record was applied successfully.') - return {'status': 'ok', 'msg': 'Record was applied successfully'} - except Exception as e: - logging.error("Cannot apply record changes to domain {0}. Error: {1}".format(domain, e)) - logging.debug(traceback.format_exc()) - 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().get('auto_ptr') - - 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_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 {0}. DETAIL: {1}".format(domain, e)) - return {'status': 'error', 'msg': 'Auto-PTR creation failed. There was something wrong, please contact administrator.'} - - def delete(self, domain): - """ - Delete a record from domain - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - data = {"rrsets": [ - { - "name": self.name.rstrip('.') + '.', - "type": self.type, - "changetype": "DELETE", - "records": [ - ] - } - ] - } - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) - logging.debug(jdata) - return {'status': 'ok', 'msg': 'Record was removed successfully'} - except Exception as e: - logging.error("Cannot remove record {0}/{1}/{2} from domain {3}. DETAIL: {4}".format( - self.name, self.type, self.data, domain, e)) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} - - def is_allowed_edit(self): - """ - Check if record is allowed to edit - """ - return self.type in Setting().get_records_allow_to_edit() - - def is_allowed_delete(self): - """ - Check if record is allowed to removed - """ - return (self.type in Setting().get_records_allow_to_edit() and self.type != 'SOA') - - def exists(self, domain): - """ - Check if record is present within domain records, and if it's present set self to found record - """ - jdata = self.get_record_data(domain) - jrecords = jdata['records'] - - for jr in jrecords: - if jr['name'] == self.name and jr['type'] == self.type: - self.name = jr['name'] - self.type = jr['type'] - self.status = jr['disabled'] - self.ttl = jr['ttl'] - self.data = jr['content'] - self.priority = 10 - return True - return False - - def update(self, domain, content): - """ - Update single record - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - if self.NEW_SCHEMA: - data = {"rrsets": [ - { - "name": self.name + '.', - "type": self.type, - "ttl": self.ttl, - "changetype": "REPLACE", - "records": [ - { - "content": content, - "disabled": self.status, - } - ] - } - ] - } - else: - data = {"rrsets": [ - { - "name": self.name, - "type": self.type, - "changetype": "REPLACE", - "records": [ - { - "content": content, - "disabled": self.status, - "name": self.name, - "ttl": self.ttl, - "type": self.type, - "priority": 10 - } - ] - } - ] - } - try: - utils.fetch_json(urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + - '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='PATCH', data=data) - logging.debug("dyndns data: {0}".format(data)) - return {'status': 'ok', 'msg': 'Record was updated successfully'} - except Exception as e: - logging.error("Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}".format( - self.name, self.type, self.data, domain, e)) - return {'status': 'error', 'msg': 'There was something wrong, please contact administrator'} - - def update_db_serial(self, domain): - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/localhost/zones/{0}'.format(domain)), headers=headers, method='GET') - serial = jdata['serial'] - - domain = Domain.query.filter(Domain.name == domain).first() - if domain: - domain.serial = serial - db.session.commit() - return {'status': True, 'msg': 'Synced local serial for domain name {0}'.format(domain)} - else: - return {'status': False, 'msg': 'Could not find domain name {0} in local db'.format(domain)} - - -class Server(object): - - """ - This is not a model, it's just an object - which be assigned data from PowerDNS API - """ - - def __init__(self, server_id=None, server_config=None): - self.server_id = server_id - self.server_config = server_config - # PDNS configs - self.PDNS_STATS_URL = Setting().get('pdns_api_url') - self.PDNS_API_KEY = Setting().get('pdns_api_key') - self.PDNS_VERSION = Setting().get('pdns_version') - self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) - - def get_config(self): - """ - Get server config - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/{0}/config'.format(self.server_id)), headers=headers, method='GET') - return jdata - except Exception as e: - logging.error("Can not get server configuration. DETAIL: {0}".format(e)) - logging.debug(traceback.format_exc()) - return [] - - def get_statistic(self): - """ - Get server statistics - """ - headers = {} - headers['X-API-Key'] = self.PDNS_API_KEY - - try: - jdata = utils.fetch_json( - urljoin(self.PDNS_STATS_URL, self.API_EXTENDED_URL + '/servers/{0}/statistics'.format(self.server_id)), headers=headers, method='GET') - return jdata - except Exception as e: - logging.error("Can not get server statistics. DETAIL: {0}".format(e)) - logging.debug(traceback.format_exc()) - return [] - - -class History(db.Model): - id = db.Column(db.Integer, primary_key=True) - msg = db.Column(db.String(256)) - # detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql')) - detail = db.Column(db.Text()) - created_by = db.Column(db.String(128)) - created_on = db.Column(db.DateTime, default=datetime.utcnow) - - def __init__(self, id=None, msg=None, detail=None, created_by=None): - self.id = id - self.msg = msg - self.detail = detail - self.created_by = created_by - - def __repr__(self): - return ''.format(self.msg) - - def add(self): - """ - Add an event to history table - """ - h = History() - h.msg = self.msg - h.detail = self.detail - h.created_by = self.created_by - db.session.add(h) - db.session.commit() - - def remove_all(self): - """ - Remove all history from DB - """ - try: - db.session.query(History).delete() - db.session.commit() - logging.info("Removed all history") - return True - except Exception as e: - db.session.rollback() - logging.error("Cannot remove history. DETAIL: {0}".format(e)) - logging.debug(traceback.format_exc()) - return False - - -class Setting(db.Model): - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(64)) - value = db.Column(db.Text()) - - defaults = { - 'maintenance': False, - 'fullscreen_layout': True, - 'record_helper': True, - 'login_ldap_first': True, - 'default_record_table_size': 15, - 'default_domain_table_size': 10, - 'auto_ptr': False, - 'record_quick_edit': True, - 'pretty_ipv6_ptr': False, - 'dnssec_admins_only': False, - 'allow_user_create_domain': False, - 'bg_domain_updates': False, - 'site_name': 'PowerDNS-Admin', - 'session_timeout': 10, - 'pdns_api_url': '', - 'pdns_api_key': '', - 'pdns_version': '4.1.1', - 'local_db_enabled': True, - 'signup_enabled': True, - 'ldap_enabled': False, - 'ldap_type': 'ldap', - 'ldap_uri': '', - 'ldap_base_dn': '', - 'ldap_admin_username': '', - 'ldap_admin_password': '', - 'ldap_filter_basic': '', - 'ldap_filter_username': '', - 'ldap_sg_enabled': False, - 'ldap_admin_group': '', - 'ldap_operator_group': '', - 'ldap_user_group': '', - 'ldap_domain': '', - 'github_oauth_enabled': False, - 'github_oauth_key': '', - 'github_oauth_secret': '', - 'github_oauth_scope': 'email', - 'github_oauth_api_url': 'https://api.github.com/user', - 'github_oauth_token_url': 'https://github.com/login/oauth/access_token', - 'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize', - 'google_oauth_enabled': False, - 'google_oauth_client_id': '', - 'google_oauth_client_secret': '', - 'google_token_url': 'https://oauth2.googleapis.com/token', - 'google_oauth_scope': 'openid email profile', - 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', - 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', - 'oidc_oauth_enabled': False, - 'oidc_oauth_key': '', - 'oidc_oauth_secret': '', - 'oidc_oauth_scope': 'email', - 'oidc_oauth_api_url': '', - 'oidc_oauth_token_url': '', - 'oidc_oauth_authorize_url': '', - 'forward_records_allow_edit': {'A': True, 'AAAA': True, 'AFSDB': False, 'ALIAS': False, 'CAA': True, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': True, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'LUA': False, 'MX': True, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': True, 'SSHFP': False, 'SRV': True, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}, - 'reverse_records_allow_edit': {'A': False, 'AAAA': False, 'AFSDB': False, 'ALIAS': False, 'CAA': False, 'CERT': False, 'CDNSKEY': False, 'CDS': False, 'CNAME': False, 'DNSKEY': False, 'DNAME': False, 'DS': False, 'HINFO': False, 'KEY': False, 'LOC': True, 'LUA': False, 'MX': False, 'NAPTR': False, 'NS': True, 'NSEC': False, 'NSEC3': False, 'NSEC3PARAM': False, 'OPENPGPKEY': False, 'PTR': True, 'RP': False, 'RRSIG': False, 'SOA': False, 'SPF': False, 'SSHFP': False, 'SRV': False, 'TKEY': False, 'TSIG': False, 'TLSA': False, 'SMIMEA': False, 'TXT': True, 'URI': False}, - 'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours', - } - - def __init__(self, id=None, name=None, value=None): - self.id = id - self.name = name - self.value = value - - # allow database autoincrement to do its own ID assignments - def __init__(self, name=None, value=None): - self.id = None - self.name = name - self.value = value - - def set_maintenance(self, mode): - maintenance = Setting.query.filter(Setting.name == 'maintenance').first() - - if maintenance is None: - value = self.defaults['maintenance'] - maintenance = Setting(name='maintenance', value=str(value)) - db.session.add(maintenance) - - mode = str(mode) - - try: - if maintenance.value != mode: - maintenance.value = mode - db.session.commit() - return True - except Exception as e: - logging.error('Cannot set maintenance to {0}. DETAIL: {1}'.format(mode, e)) - logging.debug(traceback.format_exec()) - db.session.rollback() - return False - - def toggle(self, setting): - current_setting = Setting.query.filter(Setting.name == setting).first() - - if current_setting is None: - value = self.defaults[setting] - current_setting = Setting(name=setting, value=str(value)) - db.session.add(current_setting) - - try: - if current_setting.value == "True": - current_setting.value = "False" - else: - current_setting.value = "True" - db.session.commit() - return True - except Exception as e: - logging.error('Cannot toggle setting {0}. DETAIL: {1}'.format(setting, e)) - logging.debug(traceback.format_exec()) - db.session.rollback() - return False - - def set(self, setting, value): - current_setting = Setting.query.filter(Setting.name == setting).first() - - if current_setting is None: - current_setting = Setting(name=setting, value=None) - db.session.add(current_setting) - - value = str(value) - - try: - current_setting.value = value - db.session.commit() - return True - except Exception as e: - logging.error('Cannot edit setting {0}. DETAIL: {1}'.format(setting, e)) - logging.debug(traceback.format_exec()) - db.session.rollback() - return False - - def get(self, setting): - if setting in self.defaults: - result = self.query.filter(Setting.name == setting).first() - if result is not None: - return strtobool(result.value) if result.value in ['True', 'False'] else result.value - else: - return self.defaults[setting] - else: - logging.error('Unknown setting queried: {0}'.format(setting)) - - def get_records_allow_to_edit(self): - return list(set(self.get_forward_records_allow_to_edit() + self.get_reverse_records_allow_to_edit())) - - def get_forward_records_allow_to_edit(self): - records = self.get('forward_records_allow_edit') - f_records = literal_eval(records) if isinstance(records, str) else records - r_name = [r for r in f_records if f_records[r]] - # Sort alphabetically if python version is smaller than 3.6 - if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 6): - r_name.sort() - return r_name - - def get_reverse_records_allow_to_edit(self): - records = self.get('reverse_records_allow_edit') - r_records = literal_eval(records) if isinstance(records, str) else records - r_name = [r for r in r_records if r_records[r]] - # Sort alphabetically if python version is smaller than 3.6 - if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 6): - r_name.sort() - return r_name - - def get_ttl_options(self): - return [(pytimeparse.parse(ttl), ttl) for ttl in self.get('ttl_options').split(',')] - - -class DomainTemplate(db.Model): - __tablename__ = "domain_template" - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255), index=True, unique=True) - description = db.Column(db.String(255)) - records = db.relationship( - 'DomainTemplateRecord', back_populates='template', cascade="all, delete-orphan") - - def __repr__(self): - return ''.format(self.name) - - def __init__(self, name=None, description=None): - self.id = None - self.name = name - self.description = description - - def replace_records(self, records): - try: - self.records = [] - for record in records: - self.records.append(record) - db.session.commit() - return {'status': 'ok', 'msg': 'Template records have been modified'} - except Exception as e: - logging.error('Cannot create template records Error: {0}'.format(e)) - db.session.rollback() - return {'status': 'error', 'msg': 'Can not create template records'} - - def create(self): - try: - db.session.add(self) - db.session.commit() - return {'status': 'ok', 'msg': 'Template has been created'} - except Exception as e: - logging.error('Can not update domain template table. Error: {0}'.format(e)) - db.session.rollback() - return {'status': 'error', 'msg': 'Can not update domain template table'} - - def delete_template(self): - try: - self.records = [] - db.session.delete(self) - db.session.commit() - return {'status': 'ok', 'msg': 'Template has been deleted'} - except Exception as e: - logging.error('Can not delete domain template. Error: {0}'.format(e)) - db.session.rollback() - return {'status': 'error', 'msg': 'Can not delete domain template'} - - -class DomainTemplateRecord(db.Model): - __tablename__ = "domain_template_record" - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(255)) - type = db.Column(db.String(64)) - ttl = db.Column(db.Integer) - data = db.Column(db.Text) - status = db.Column(db.Boolean) - template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id')) - template = db.relationship('DomainTemplate', back_populates='records') - - def __repr__(self): - return ''.format(self.id) - - def __init__(self, id=None, name=None, type=None, ttl=None, data=None, status=None): - self.id = id - self.name = name - self.type = type - self.ttl = ttl - self.data = data - self.status = status - - def apply(self): - try: - db.session.commit() - except Exception as e: - logging.error('Can not update domain template table. Error: {0}'.format(e)) - db.session.rollback() - return {'status': 'error', 'msg': 'Can not update domain template table'} - - -class ApiKey(db.Model): - __tablename__ = "apikey" - id = db.Column(db.Integer, primary_key=True) - key = db.Column(db.String(255), unique=True, nullable=False) - description = db.Column(db.String(255)) - role_id = db.Column(db.Integer, db.ForeignKey('role.id')) - role = db.relationship('Role', back_populates="apikeys", lazy=True) - domains = db.relationship( - "Domain", - secondary=domain_apikey, - back_populates="apikeys" - ) - - def __init__(self, key=None, desc=None, role_name=None, domains=[]): - self.id = None - self.description = desc - self.role_name = role_name - self.domains[:] = domains - if not key: - rand_key = ''.join( - random.choice( - string.ascii_letters + string.digits - ) for _ in range(15) - ) - self.plain_key = rand_key - self.key = self.get_hashed_password(rand_key).decode('utf-8') - logging.debug("Hashed key: {0}".format(self.key)) - else: - self.key = key - - def create(self): - try: - self.role = Role.query.filter(Role.name == self.role_name).first() - db.session.add(self) - db.session.commit() - except Exception as e: - logging.error('Can not update api key table. Error: {0}'.format(e)) - db.session.rollback() - raise e - - def delete(self): - try: - db.session.delete(self) - db.session.commit() - except Exception as e: - msg_str = 'Can not delete api key template. Error: {0}' - logging.error(msg_str.format(e)) - db.session.rollback() - raise e - - def update(self, role_name=None, description=None, domains=None): - try: - if role_name: - role = Role.query.filter(Role.name == role_name).first() - self.role_id = role.id - - if description: - self.description = description - - if domains: - domain_object_list = Domain.query \ - .filter(Domain.name.in_(domains)) \ - .all() - self.domains[:] = domain_object_list - - db.session.commit() - except Exception as e: - msg_str = 'Update of apikey failed. Error: {0}' - logging.error(msg_str.format(e)) - db.session.rollback - raise e - - def get_hashed_password(self, plain_text_password=None): - # Hash a password for the first time - # (Using bcrypt, the salt is saved into the hash itself) - if plain_text_password is None: - return plain_text_password - - if plain_text_password: - pw = plain_text_password - else: - pw = self.plain_text_password - - return bcrypt.hashpw( - pw.encode('utf-8'), - app.config.get('SALT').encode('utf-8') - ) - - def check_password(self, hashed_password): - # Check hased password. Using bcrypt, - # the salt is saved into the hash itself - if (self.plain_text_password): - return bcrypt.checkpw( - self.plain_text_password.encode('utf-8'), - hashed_password.encode('utf-8')) - return False - - def is_validate(self, method, src_ip=''): - """ - Validate user credential - """ - if method == 'LOCAL': - passw_hash = self.get_hashed_password(self.plain_text_password) - apikey = ApiKey.query \ - .filter(ApiKey.key == passw_hash.decode('utf-8')) \ - .first() - - if not apikey: - raise Exception("Unauthorized") - - return apikey diff --git a/app/oauth.py b/app/oauth.py deleted file mode 100644 index e42bbfb..0000000 --- a/app/oauth.py +++ /dev/null @@ -1,108 +0,0 @@ -from flask import request, session, redirect, url_for - -from app import app, authlib_oauth_client -from app.models import Setting - -# TODO: -# - Fix github/google enabling (Currently need to reload the flask app) - -def github_oauth(): - if not Setting().get('github_oauth_enabled'): - return None - - def fetch_github_token(): - return session.get('github_token') - - github = authlib_oauth_client.register( - 'github', - client_id = Setting().get('github_oauth_key'), - client_secret = Setting().get('github_oauth_secret'), - request_token_params = {'scope': Setting().get('github_oauth_scope')}, - api_base_url = Setting().get('github_oauth_api_url'), - request_token_url = None, - access_token_url = Setting().get('github_oauth_token_url'), - authorize_url = Setting().get('github_oauth_authorize_url'), - client_kwargs={'scope': Setting().get('github_oauth_scope')}, - fetch_token=fetch_github_token, - ) - - @app.route('/github/authorized') - def github_authorized(): - session['github_oauthredir'] = url_for('.github_authorized', _external=True) - token = github.authorize_access_token() - if token is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error'], - request.args['error_description'] - ) - session['github_token'] = (token) - return redirect(url_for('.login')) - - return github - - -def google_oauth(): - if not Setting().get('google_oauth_enabled'): - return None - - def fetch_google_token(): - return session.get('google_token') - - google = authlib_oauth_client.register( - 'google', - client_id=Setting().get('google_oauth_client_id'), - client_secret=Setting().get('google_oauth_client_secret'), - api_base_url=Setting().get('google_base_url'), - request_token_url=None, - access_token_url=Setting().get('google_token_url'), - authorize_url=Setting().get('google_authorize_url'), - client_kwargs={'scope': Setting().get('google_oauth_scope')}, - fetch_token=fetch_google_token, - ) - - @app.route('/google/authorized') - def google_authorized(): - session['google_oauthredir'] = url_for('.google_authorized', _external=True) - token = google.authorize_access_token() - if token is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error_reason'], - request.args['error_description'] - ) - session['google_token'] = (token) - return redirect(url_for('.login')) - - return google - -def oidc_oauth(): - if not Setting().get('oidc_oauth_enabled'): - return None - - def fetch_oidc_token(): - return session.get('oidc_token') - - oidc = authlib_oauth_client.register( - 'oidc', - client_id = Setting().get('oidc_oauth_key'), - client_secret = Setting().get('oidc_oauth_secret'), - api_base_url = Setting().get('oidc_oauth_api_url'), - request_token_url = None, - access_token_url = Setting().get('oidc_oauth_token_url'), - authorize_url = Setting().get('oidc_oauth_authorize_url'), - client_kwargs={'scope': Setting().get('oidc_oauth_scope')}, - fetch_token=fetch_oidc_token, - ) - - @app.route('/oidc/authorized') - def oidc_authorized(): - session['oidc_oauthredir'] = url_for('.oidc_authorized', _external=True) - token = oidc.authorize_access_token() - if token is None: - return 'Access denied: reason=%s error=%s' % ( - request.args['error'], - request.args['error_description'] - ) - session['oidc_token'] = (token) - return redirect(url_for('.login')) - - return oidc \ No newline at end of file diff --git a/app/templates/admin_editaccount.html b/app/templates/admin_editaccount.html deleted file mode 100644 index 409fec4..0000000 --- a/app/templates/admin_editaccount.html +++ /dev/null @@ -1,120 +0,0 @@ -{% extends "base.html" %} -{% set active_page = "admin_accounts" %} -{% block title %}Edit Account - {{ SITE_NAME }}{% endblock %} - -{% block dashboard_stat %} - -
-

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

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

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

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

Error!

- {{ error }} -
- {{ error }} - {% endif %} -
- - - - {% if invalid_accountname %} - Cannot be blank and must only contain alphanumeric characters. - {% 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.

-
- -
-
- -
-
-
-
-
-
-

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 stored as all lowercase letters (no spaces, special characters etc).
- 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/app/templates/admin_edituser.html b/app/templates/admin_edituser.html deleted file mode 100644 index 8053251..0000000 --- a/app/templates/admin_edituser.html +++ /dev/null @@ -1,155 +0,0 @@ -{% extends "base.html" %} -{% set active_page = "admin_users" %} -{% block title %}Edit User - {{ SITE_NAME }}{% endblock %} - -{% block dashboard_stat %} - -
-

- User - {% if create %}New user{% else %}{{ user.username }}{% endif %} -

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

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

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

Error!

- {{ error }} -
- {{ error }} - {% endif %} -
- - -
-
- - -
-
- - -
- -
- - -
-
- - - {% if blank_password %} - The password cannot be blank. - {% endif %} -
-
- -
-
- {% if not create %} -
-
-

Two Factor Authentication

-
-
-

If two factor authentication was configured and is causing problems due to a lost device or technical issue, it can be disabled here.

-

The user will need to reconfigure two factor authentication, to re-enable it.

-

Beware: This could compromise security!

-
- -
- {% endif %} -
-
-
-
-

Help with {% if create %}creating a new{% else%}updating a{% endif %} user

-
-
-

Fill in all the fields to the in the form to the left.

- {% if create %} -

Newly created users do not have access to any domains. You will need to grant access to the user once it is created via the domain management buttons on the dashboard.

- {% else %} -

Password can be left empty to keep the current password.

-

Username cannot be changed.

- {% endif %} -
-
-
-
-
-{% endblock %} -{% block extrascripts %} - -{% endblock %} -{% block modals %} - -{% endblock %} diff --git a/app/templates/admin_setting_pdns.html b/app/templates/admin_setting_pdns.html deleted file mode 100644 index 37a1852..0000000 --- a/app/templates/admin_setting_pdns.html +++ /dev/null @@ -1,86 +0,0 @@ -{% extends "base.html" %} -{% set active_page = "admin_settings" %} -{% block title %} -PDNS Settings - {{ SITE_NAME }} -{% endblock %} {% block dashboard_stat %} - -
-

- Settings PowerDNS-Admin settings -

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

PDNS Settings

-
- - -
- -
- {% if not SETTING.get('pdns_api_url') or not SETTING.get('pdns_api_key') or not SETTING.get('pdns_version') %} -
- -

Error!

- Please complete your PowerDNS API configuration before continuing -
- {% endif %} -
- - - -
-
- - - -
-
- - - -
-
- -
-
-
-
-
-
-

Help

-
-
-
-

You must configure the API connection information before PowerDNS-Admin can query your PowerDNS data. Following fields are required:

-
PDNS API URL
-
Your PowerDNS API URL (eg. http://127.0.0.1:8081/).
-
PDNS API KEY
-
Your PowerDNS API key.
-
PDNS VERSION
-
Your PowerDNS version number (eg. 4.1.1).
-
-

Find more details at https://doc.powerdns.com/md/httpapi/README/

-
-
-
-
-
-{% endblock %} -{% block extrascripts %} - {% assets "js_validation" -%} - - {%- endassets %} -{% endblock %} diff --git a/app/templates/admin_setting_records.html b/app/templates/admin_setting_records.html deleted file mode 100644 index 7366e8c..0000000 --- a/app/templates/admin_setting_records.html +++ /dev/null @@ -1,79 +0,0 @@ -{% extends "base.html" %} -{% set active_page = "admin_settings" %} -{% block title %} -DNS Records Settings - {{ SITE_NAME }} -{% endblock %} {% block dashboard_stat %} - -
-

- Settings PowerDNS-Admin settings -

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

DNS record Settings

-
- - -
- - -
- - - - - - - - {% for record in f_records %} - - - - - - - {% endfor %} -
#RecordForward ZoneReverse Zone
{{ loop.index }}{{ record }} - - - -
-
- -
-
-
-
-
-
-

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.

-
-
-
-
-
-{% endblock %} -{% block extrascripts %} - -{% endblock %} diff --git a/app/templates/domain_add.html b/app/templates/domain_add.html deleted file mode 100644 index b57f658..0000000 --- a/app/templates/domain_add.html +++ /dev/null @@ -1,163 +0,0 @@ -{% extends "base.html" %} -{% set active_page = "new_domain" %} -{% block title %}Add Domain - {{ SITE_NAME }}{% endblock %} - -{% block dashboard_stat %} - -
-

- Domain - Create new -

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

Create new domain

-
- - -
- -
-
- -
-
-
- -
- -     - -     - -
-
-
- - -
- -
- -
- -
-
- -
-
- -
-
- -
-
-
- - - -
-
- -
-
-
-
-

Help with creating a new domain

-
-
-
-
Domain name
-
Enter your domain name in the format of name.tld (eg. powerdns-admin.com). You can also enter sub-domains to create a sub-root zone (eg. sub.powerdns-admin.com) in case you want to delegate sub-domain management to specific users.
-
Type
-
The type decides how the domain will be replicated across multiple DNS servers. -
    -
  • - Native - PowerDNS will not perform any replication. Use this if you only have one PowerDNS server or you handle replication via your backend (MySQL). -
  • -
  • - Master - This PowerDNS server will serve as the master and will send zone transfers (AXFRs) to other servers configured as slaves. -
  • -
  • - Slave - This PowerDNS server will serve as the slave and will request and receive zone transfers (AXFRs) from other servers configured as masters. -
  • -
-
-
SOA-EDIT-API
-
The SOA-EDIT-API setting defines how the SOA serial number will be updated after a change is made to the domain. -
    -
  • - DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than the generated serial, use the generated serial. If the current serial is higher or equal to the generated serial, increase the current serial by 1. -
  • -
  • - INCREASE - Increase the current serial by 1. -
  • -
  • - EPOCH - Change the serial to the number of seconds since the EPOCH, aka unixtime. -
  • -
  • - OFF - Disable automatic updates of the SOA serial. -
  • -
-
-
-

Find more details at https://docs.powerdns.com/md/

-
-
-
-
-
-{% endblock %} -{% block extrascripts %} - -{% endblock %} diff --git a/app/templates/domain_management.html b/app/templates/domain_management.html deleted file mode 100644 index c8900c2..0000000 --- a/app/templates/domain_management.html +++ /dev/null @@ -1,272 +0,0 @@ -{% extends "base.html" %} -{% block title %}Domain Management - {{ SITE_NAME }}{% endblock %} - -{% block dashboard_stat %} - {% if status %} - {% if status.get('status') == 'ok' %} -
- Success! {{ status.get('msg') }} -
- {% elif status.get('status') == 'error' %} -
- {% if status.get('msg') != None %} - Error! {{ status.get('msg') }} - {% else %} - Error! An undefined error occurred. - {% endif %} -
- {% endif %} - {% endif %} -
-

- Manage domain {{ domain.name }} -

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

Domain Access Control

-
-
-
-
-

Users on the right have access to manage the records in - the {{ domain.name }} domain.

-

Click on users to move from between columns.

-

- Users in red are Administrators - and already have access to ALL domains. -

-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-

Account

-
-
-
-
-
- -
- -
-
-
-
-
-
-
-
-
-
-
-

Auto PTR creation

-
-
-

-  Allow automatic reverse pointer creation on record updates?{% if - SETTING.get('auto_ptr') %}
Auto-ptr is enabled globally on the PDA system!{% endif %}

-
-
-
-
-
-
-
-
-

DynDNS 2 Settings

-
-
-

-  Allow on-demand creation of records via DynDNS updates?

-
-
-
-
-
-
-
-
-

Change SOA-EDIT-API

-
-
-

The SOA-EDIT-API setting defines how the SOA serial number will be updated after a change is made to the domain.

-
    -
  • - DEFAULT - Generate a soa serial of YYYYMMDD01. If the current serial is lower than the generated serial, use the generated serial. If the current serial is higher or equal to the generated serial, increase the current serial by 1. -
  • -
  • - INCREASE - Increase the current serial by 1. -
  • -
  • - EPOCH - Change the serial to the number of seconds since the EPOCH, aka unixtime. -
  • -
  • - OFF - Disable automatic updates of the SOA serial. -
  • -
- New SOA-EDIT-API Setting: -
- -
- -
-
-
-
-
-
-
-
-
-

Domain Deletion

-
-
-

This function is used to remove a domain from PowerDNS-Admin AND PowerDNS. All records and user privileges associated with this domain will also be removed. This change cannot be reverted.

- -
-
-
-
-
-{% endblock %} -{% block extrascripts %} - -{% endblock %} -{% block modals %} - -{% endblock %} diff --git a/app/templates/login.html b/app/templates/login.html deleted file mode 100644 index 2208d50..0000000 --- a/app/templates/login.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - Log In - {{ SITE_NAME }} - - - {% assets "css_login" -%} - - {%- endassets %} - - - - - - - - - -{% assets "js_login" -%} - -{%- endassets %} -{% assets "js_validation" -%} - -{%- endassets %} - - - - diff --git a/app/templates/maintenance.html b/app/templates/maintenance.html deleted file mode 100644 index 4478aeb..0000000 --- a/app/templates/maintenance.html +++ /dev/null @@ -1,18 +0,0 @@ - -Site Maintenance - - -
-

We’ll be back soon!

-
-

Sorry for the inconvenience but we’re performing some maintenance at the moment. Please contact the System Administrator if you need more information, otherwise we’ll be back online shortly!

-

— Team

-
-
\ No newline at end of file diff --git a/app/templates/register.html b/app/templates/register.html deleted file mode 100644 index 2507008..0000000 --- a/app/templates/register.html +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - Register - {{ SITE_NAME }} - - - {% assets "css_login" -%} - - {%- endassets %} - - - - - - -
- -
- {% if error %} -
- - {{ error }} -
- {% endif %} - -
- -
- - - -
-
- - - -
-
- - - -
- -
- - - -
-
- - -
-
- - - -
-
-
- -
-
- -
- -
-
-
- - -
- - -{% assets "js_login" -%} - -{%- endassets %} -{% assets "js_validation" -%} - -{%- endassets %} - - - diff --git a/app/templates/template.html b/app/templates/template.html deleted file mode 100644 index 0739488..0000000 --- a/app/templates/template.html +++ /dev/null @@ -1,124 +0,0 @@ -{% extends "base.html" %} -{% set active_page = "admin_domain_template" %} -{% block title %}Templates - {{ SITE_NAME }}{% endblock %} - -{% block dashboard_stat %} - -
-

- Templates - List -

- -
-{% endblock %} -{% block content %} - -
- {% with errors = get_flashed_messages(category_filter=["error"]) %} {% if errors %} -
-
-
- -

- Error! -

-
- x -
    - {%- for msg in errors %} -
  • {{ msg }}
  • {% endfor -%} -
-
-
-
-
-{% endif %} {% endwith %} -
-
-
-
-

Templates

-
- -
- - - - - - - - - - - {% for template in templates %} - - - - - - - {% endfor %} - -
NameDescriptionNumber of RecordsAction
- {{ template.name }} - - {{ template.description }} - - {{ template.records|count }} - - - - - -
-
- -
- -
- -
- -
- -{% endblock %} -{% block extrascripts %} - -{% endblock %} -{% block modals %} -{% endblock %} diff --git a/app/views.py b/app/views.py deleted file mode 100755 index b94cbff..0000000 --- a/app/views.py +++ /dev/null @@ -1,1886 +0,0 @@ -import base64 -import logging as logger -import os -import traceback -import re -import datetime -import json -import ipaddress -from distutils.util import strtobool -from distutils.version import StrictVersion -from functools import wraps -from io import BytesIO -from ast import literal_eval - -import qrcode as qrc -import qrcode.image.svg as qrc_svg -from flask import g, request, make_response, jsonify, render_template, session, redirect, url_for, send_from_directory, abort, flash -from flask_login import login_user, logout_user, current_user, login_required -from werkzeug import secure_filename - -from .models import User, Account, AccountUser, Domain, Record, RecordEntry, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord -from app import app, login_manager, csrf -from app.lib import utils -from app.oauth import github_oauth, google_oauth, oidc_oauth -from app.decorators import admin_role_required, operator_role_required, can_access_domain, can_configure_dnssec, can_create_domain -from yaml import Loader, load - -if app.config['SAML_ENABLED']: - from onelogin.saml2.utils import OneLogin_Saml2_Utils - -google = None -github = None -logging = logger.getLogger(__name__) - - -# FILTERS -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.context_processor -def inject_sitename(): - setting = Setting().get('site_name') - return dict(SITE_NAME=setting) - -@app.context_processor -def inject_setting(): - setting = Setting() - return dict(SETTING=setting) - - -@app.before_first_request -def register_modules(): - global google - global github - global oidc - google = google_oauth() - github = github_oauth() - oidc = oidc_oauth() - - -# START USER AUTHENTICATION HANDLER -@app.before_request -def before_request(): - # check if user is anonymous - g.user = current_user - login_manager.anonymous_user = Anonymous - - # check site maintenance mode - maintenance = Setting().get('maintenance') - if maintenance and current_user.is_authenticated and current_user.role.name not in ['Administrator', 'Operator']: - return render_template('maintenance.html') - - # Manage session timeout - session.permanent = True - app.permanent_session_lifetime = datetime.timedelta(minutes=int(Setting().get('session_timeout'))) - session.modified = True - g.user = current_user - -@login_manager.user_loader -def load_user(id): - """ - This will be current_user - """ - return User.query.get(int(id)) - -def dyndns_login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - if current_user.is_authenticated is False: - return render_template('dyndns.html', response='badauth'), 200 - return f(*args, **kwargs) - return decorated_function - - -@login_manager.request_loader -def login_via_authorization_header(request): - auth_header = request.headers.get('Authorization') - if auth_header: - auth_header = auth_header.replace('Basic ', '', 1) - try: - auth_header = str(base64.b64decode(auth_header), 'utf-8') - username,password = auth_header.split(":") - except TypeError as e: - return None - user = User(username=username, password=password, plain_text_password=password) - try: - auth_method = request.args.get('auth_method', 'LOCAL') - auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL' - auth = user.is_validate(method=auth_method, src_ip=request.remote_addr) - if auth == False: - return None - else: - login_user(user, remember = False) - return user - except Exception as e: - logging.error('Error: {0}'.format(e)) - return None - return None -# END USER AUTHENTICATION HANDLER - - -# START VIEWS -@app.errorhandler(400) -def http_bad_request(e): - return redirect(url_for('error', code=400)) - - -@app.errorhandler(401) -def http_unauthorized(e): - return redirect(url_for('error', code=401)) - -@app.errorhandler(404) -@app.errorhandler(405) -def _handle_api_error(ex): - if request.path.startswith('/api/'): - return json.dumps({"msg": "NotFound"}), 404 - else: - return redirect(url_for('error', code=404)) - -@app.errorhandler(500) -def http_page_not_found(e): - return redirect(url_for('error', code=500)) - - -@app.route('/error/') -def error(code, msg=None): - supported_code = ('400', '401', '404', '500') - if code in supported_code: - return render_template('errors/{0}.html'.format(code), msg=msg), int(code) - else: - return render_template('errors/404.html'), 404 - -@app.route('/swagger', methods=['GET']) -def swagger_spec(): - try: - dir_path = os.path.dirname(os.path.abspath(__file__)) - spec_path = os.path.join(dir_path, "swagger-spec.yaml") - spec = open(spec_path,'r') - loaded_spec = load(spec.read(), Loader) - except Exception as e: - logging.error('Cannot view swagger spec. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return redirect(url_for('error', code=500)) - - resp = make_response(json.dumps(loaded_spec), 200) - resp.headers['Content-Type'] = 'application/json' - - return resp - -@app.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['username'] - password = request.form['password'] - firstname = request.form.get('firstname') - lastname = request.form.get('lastname') - email = request.form.get('email') - rpassword = request.form.get('rpassword') - - if not username or not password or not email: - return render_template('register.html', error='Please input required information') - - if password != rpassword: - return render_template('register.html', error = "Password confirmation does not match") - - 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']: - return redirect(url_for('login')) - else: - return render_template('register.html', error=result['msg']) - except Exception as e: - return render_template('register.html', error=e) - else: - return render_template('errors/404.html'), 404 - - -@app.route('/google/login') -def google_login(): - if not Setting().get('google_oauth_enabled') or google is None: - logging.error('Google OAuth is disabled or you have not yet reloaded the pda application after enabling.') - return abort(400) - else: - redirect_uri = url_for('google_authorized', _external=True) - return google.authorize_redirect(redirect_uri) - - -@app.route('/github/login') -def github_login(): - if not Setting().get('github_oauth_enabled') or github is None: - logging.error('Github OAuth is disabled or you have not yet reloaded the pda application after enabling.') - return abort(400) - else: - redirect_uri = url_for('github_authorized', _external=True) - return github.authorize_redirect(redirect_uri) - -@app.route('/oidc/login') -def oidc_login(): - if not Setting().get('oidc_oauth_enabled') or oidc is None: - logging.error('OIDC OAuth is disabled or you have not yet reloaded the pda application after enabling.') - return abort(400) - else: - redirect_uri = url_for('oidc_authorized', _external=True) - return oidc.authorize_redirect(redirect_uri) - -@app.route('/saml/login') -def saml_login(): - if not app.config.get('SAML_ENABLED'): - return abort(400) - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) - redirect_url=OneLogin_Saml2_Utils.get_self_url(req) + url_for('saml_authorized') - return redirect(auth.login(return_to=redirect_url)) - - -@app.route('/saml/metadata') -def saml_metadata(): - if not app.config.get('SAML_ENABLED'): - return abort(400) - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) - settings = auth.get_settings() - metadata = settings.get_sp_metadata() - errors = settings.validate_metadata(metadata) - - if len(errors) == 0: - resp = make_response(metadata, 200) - resp.headers['Content-Type'] = 'text/xml' - else: - resp = make_response(errors.join(', '), 500) - return resp - - -@app.route('/saml/authorized', methods=['GET', 'POST']) -@csrf.exempt -def saml_authorized(): - errors = [] - if not app.config.get('SAML_ENABLED'): - return abort(400) - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) - auth.process_response() - errors = auth.get_errors() - if len(errors) == 0: - session['samlUserdata'] = auth.get_attributes() - session['samlNameId'] = auth.get_nameid() - session['samlSessionIndex'] = auth.get_session_index() - self_url = OneLogin_Saml2_Utils.get_self_url(req) - self_url = self_url+req['script_name'] - if 'RelayState' in request.form and self_url != request.form['RelayState']: - return redirect(auth.redirect_to(request.form['RelayState'])) - if app.config.get('SAML_ATTRIBUTE_USERNAME', False): - username = session['samlUserdata'][app.config['SAML_ATTRIBUTE_USERNAME']][0].lower() - else: - username = session['samlNameId'].lower() - user = User.query.filter_by(username=username).first() - if not user: - # create user - user = User(username=username, - plain_text_password = None, - email=session['samlNameId']) - user.create_local_user() - session['user_id'] = user.id - email_attribute_name = app.config.get('SAML_ATTRIBUTE_EMAIL', 'email') - givenname_attribute_name = app.config.get('SAML_ATTRIBUTE_GIVENNAME', 'givenname') - surname_attribute_name = app.config.get('SAML_ATTRIBUTE_SURNAME', 'surname') - name_attribute_name = app.config.get('SAML_ATTRIBUTE_NAME', None) - account_attribute_name = app.config.get('SAML_ATTRIBUTE_ACCOUNT', None) - admin_attribute_name = app.config.get('SAML_ATTRIBUTE_ADMIN', None) - group_attribute_name = app.config.get('SAML_ATTRIBUTE_GROUP', None) - admin_group_name = app.config.get('SAML_GROUP_ADMIN_NAME', None) - group_to_account_mapping = create_group_to_account_mapping() - - if email_attribute_name in session['samlUserdata']: - user.email = session['samlUserdata'][email_attribute_name][0].lower() - if givenname_attribute_name in session['samlUserdata']: - user.firstname = session['samlUserdata'][givenname_attribute_name][0] - if surname_attribute_name in session['samlUserdata']: - user.lastname = session['samlUserdata'][surname_attribute_name][0] - if name_attribute_name in session['samlUserdata']: - name = session['samlUserdata'][name_attribute_name][0].split(' ') - user.firstname = name[0] - user.lastname = ' '.join(name[1:]) - - if group_attribute_name: - user_groups = session['samlUserdata'].get(group_attribute_name, []) - else: - user_groups = [] - if admin_attribute_name or group_attribute_name: - user_accounts = set(user.get_account()) - saml_accounts = [] - for group_mapping in group_to_account_mapping: - mapping = group_mapping.split('=') - group = mapping[0] - account_name = mapping[1] - - if group in user_groups: - account = handle_account(account_name) - saml_accounts.append(account) - - for account_name in session['samlUserdata'].get(account_attribute_name, []): - account = handle_account(account_name) - saml_accounts.append(account) - saml_accounts = set(saml_accounts) - for account in saml_accounts - user_accounts: - account.add_user(user) - history = History(msg='Adding {0} to account {1}'.format(user.username, account.name), created_by='SAML Assertion') - history.add() - for account in user_accounts - saml_accounts: - account.remove_user(user) - history = History(msg='Removing {0} from account {1}'.format(user.username, account.name), created_by='SAML Assertion') - history.add() - if admin_attribute_name and 'true' in session['samlUserdata'].get(admin_attribute_name, []): - uplift_to_admin(user) - elif admin_group_name in user_groups: - uplift_to_admin(user) - elif admin_attribute_name or group_attribute_name: - if user.role.name != 'User': - user.role_id = Role.query.filter_by(name='User').first().id - history = History(msg='Demoting {0} to user'.format(user.username), created_by='SAML Assertion') - history.add() - user.plain_text_password = None - user.update_profile() - session['authentication_type'] = 'SAML' - login_user(user, remember=False) - return redirect(url_for('index')) - else: - return render_template('errors/SAML.html', errors=errors) - - -def create_group_to_account_mapping(): - group_to_account_mapping_string = app.config.get('SAML_GROUP_TO_ACCOUNT_MAPPING', None) - if group_to_account_mapping_string and len(group_to_account_mapping_string.strip()) > 0: - group_to_account_mapping = group_to_account_mapping_string.split(',') - else: - group_to_account_mapping = [] - return group_to_account_mapping - - -def handle_account(account_name): - clean_name = ''.join(c for c in account_name.lower() if c in "abcdefghijklmnopqrstuvwxyz0123456789") - if len(clean_name) > Account.name.type.length: - logging.error("Account name {0} too long. Truncated.".format(clean_name)) - account = Account.query.filter_by(name=clean_name).first() - if not account: - account = Account(name=clean_name.lower(), description='', contact='', mail='') - account.create_account() - history = History(msg='Account {0} created'.format(account.name), created_by='SAML Assertion') - history.add() - return account - - -def uplift_to_admin(user): - if user.role.name != 'Administrator': - user.role_id = Role.query.filter_by(name='Administrator').first().id - history = History(msg='Promoting {0} to administrator'.format(user.username), created_by='SAML Assertion') - history.add() - - -@login_manager.unauthorized_handler -def unauthorized_callback(): - session['next'] = request.script_root + request.path - return redirect(url_for('login')) - - -@app.route('/login', methods=['GET', 'POST']) -def login(): - SAML_ENABLED = app.config.get('SAML_ENABLED') - - if g.user is not None and current_user.is_authenticated: - return redirect(url_for('dashboard')) - - if 'google_token' in session: - user_data = json.loads(google.get('userinfo').text) - first_name = user_data['given_name'] - surname = user_data['family_name'] - email = user_data['email'] - user = User.query.filter_by(username=email).first() - if user is None: - user = User.query.filter_by(email=email).first() - if not user: - user = User(username=email, - firstname=first_name, - lastname=surname, - plain_text_password=None, - email=email) - - result = user.create_local_user() - if not result['status']: - session.pop('google_token', None) - return redirect(url_for('login')) - - session['user_id'] = user.id - login_user(user, remember = False) - session['authentication_type'] = 'OAuth' - return redirect(url_for('index')) - - if 'github_token' in session: - me = json.loads(github.get('user').text) - github_username = me['login'] - github_name = me['name'] - github_email = me['email'] - - user = User.query.filter_by(username=github_username).first() - if user is None: - user = User.query.filter_by(email=github_email).first() - if not user: - user = User(username=github_username, - plain_text_password=None, - firstname=github_name, - lastname='', - email=github_email) - - result = user.create_local_user() - if not result['status']: - session.pop('github_token', None) - return redirect(url_for('login')) - - session['user_id'] = user.id - session['authentication_type'] = 'OAuth' - login_user(user, remember = False) - return redirect(url_for('index')) - - if 'oidc_token' in session: - me = json.loads(oidc.get('userinfo').text) - oidc_username = me["preferred_username"] - oidc_givenname = me["name"] - oidc_familyname = "" - oidc_email = me["email"] - - user = User.query.filter_by(username=oidc_username).first() - if not user: - user = User(username=oidc_username, - plain_text_password=None, - firstname=oidc_givenname, - lastname=oidc_familyname, - email=oidc_email) - - result = user.create_local_user() - if not result['status']: - session.pop('oidc_token', None) - return redirect(url_for('login')) - - session['user_id'] = user.id - session['authentication_type'] = 'OAuth' - login_user(user, remember = False) - return redirect(url_for('index')) - - if request.method == 'GET': - return render_template('login.html', saml_enabled=SAML_ENABLED) - - # process Local-DB authentication - username = request.form['username'] - password = request.form['password'] - otp_token = request.form.get('otptoken') - auth_method = request.form.get('auth_method', 'LOCAL') - session['authentication_type'] = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL' - remember_me = True if 'remember' in request.form else False - - user = User(username=username, password=password, plain_text_password=password) - - try: - auth = user.is_validate(method=auth_method, src_ip=request.remote_addr) - if auth == False: - return render_template('login.html', saml_enabled=SAML_ENABLED, error='Invalid credentials') - except Exception as e: - return render_template('login.html', saml_enabled=SAML_ENABLED, error=e) - - # check if user enabled OPT authentication - if user.otp_secret: - if otp_token and otp_token.isdigit(): - good_token = user.verify_totp(otp_token) - if not good_token: - return render_template('login.html', saml_enabled=SAML_ENABLED, error='Invalid credentials') - else: - return render_template('login.html', saml_enabled=SAML_ENABLED, error='Token required') - - login_user(user, remember=remember_me) - return redirect(session.get('next', url_for('index'))) - - -def clear_session(): - session.pop('user_id', None) - session.pop('github_token', None) - session.pop('google_token', None) - session.pop('authentication_type', None) - session.clear() - logout_user() - - -@app.route('/logout') -def logout(): - if app.config.get('SAML_ENABLED') and 'samlSessionIndex' in session and app.config.get('SAML_LOGOUT'): - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) - if app.config.get('SAML_LOGOUT_URL'): - return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - return_to = app.config.get('SAML_LOGOUT_URL'), - session_index = session['samlSessionIndex'], name_id=session['samlNameId'])) - return redirect(auth.logout(name_id_format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", - session_index = session['samlSessionIndex'], - name_id=session['samlNameId'])) - clear_session() - return redirect(url_for('login')) - - -@app.route('/saml/sls') -def saml_logout(): - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) - url = auth.process_slo() - errors = auth.get_errors() - if len(errors) == 0: - clear_session() - if url is not None: - return redirect(url) - elif app.config.get('SAML_LOGOUT_URL') is not None: - return redirect(app.config.get('SAML_LOGOUT_URL')) - else: - return redirect(url_for('login')) - else: - return render_template('errors/SAML.html', errors=errors) - -# SYBPATCH -######################################################### -from app import customBoxes -from sqlalchemy import not_ - -@app.route('/dashboard_domains_custom/', methods=['GET']) -@login_required -def dashboard_domains_custom(boxId): - if current_user.role.name in ['Administrator', 'Operator']: - domains = Domain.query - else: - domains = User(id=current_user.id).get_domain_query() - - template = app.jinja_env.get_template("dashboard_domain.html") - render = template.make_module(vars={"current_user": current_user}) - - columns = [Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master, Domain.account] - # History.created_on.desc() - order_by = [] - for i in range(len(columns)): - column_index = request.args.get("order[{0}][column]".format(i)) - sort_direction = request.args.get("order[{0}][dir]".format(i)) - if column_index is None: - break - if sort_direction != "asc" and sort_direction != "desc": - sort_direction = "asc" - - column = columns[int(column_index)] - order_by.append(getattr(column, sort_direction)()) - - if order_by: - domains = domains.order_by(*order_by) - - if boxId == "reverse": - for boxId in customBoxes.order: - if boxId == "reverse": continue - domains = domains.filter(not_(Domain.name.ilike(customBoxes.boxes[boxId][1]))) - else: - domains = domains.filter(Domain.name.ilike(customBoxes.boxes[boxId][1])) - - total_count = domains.count() - - search = request.args.get("search[value]") - if search: - start = "" if search.startswith("^") else "%" - end = "" if search.endswith("$") else "%" - - if current_user.role.name in ['Administrator', 'Operator']: - domains = domains.outerjoin(Account).filter(Domain.name.ilike(start + search.strip("^$") + end) | - Account.name.ilike(start + search.strip("^$") + end) | - Account.description.ilike(start + search.strip("^$") + end)) - else: - domains = domains.filter(Domain.name.ilike(start + search.strip("^$") + end)) - - filtered_count = domains.count() - - start = int(request.args.get("start", 0)) - length = min(int(request.args.get("length", 0)), 100) - - if length != -1: - domains = domains[start:start + length] - - data = [] - for domain in domains: - data.append([ - render.name(domain), - render.dnssec(domain), - render.type(domain), - render.serial(domain), - render.master(domain), - render.account(domain), - render.actions(domain), - ]) - - response_data = { - "draw": int(request.args.get("draw", 0)), - "recordsTotal": total_count, - "recordsFiltered": filtered_count, - "data": data, - } - return jsonify(response_data) - -# add custom boxes -@app.route('/dashboard', methods=['GET', 'POST']) -@login_required -def dashboard(): - if not Setting().get('pdns_api_url') or not Setting().get('pdns_api_key') or not Setting().get('pdns_version'): - return redirect(url_for('admin_setting_pdns')) - - BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates') - if not BG_DOMAIN_UPDATE: - logging.debug('Update domains in foreground') - Domain().update() - else: - logging.debug('Update domains in background') - - # stats for dashboard - domain_count = Domain.query.count() - users = User.query.all() - history_number = History.query.count() - history = History.query.order_by(History.created_on.desc()).limit(4) - server = Server(server_id='localhost') - statistics = server.get_statistic() - if statistics: - uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value'] - else: - uptime = 0 - - # add custom boxes to render_template - return render_template('dashboard.html', custom_boxes=customBoxes, domain_count=domain_count, users=users, history_number=history_number, uptime=uptime, histories=history, show_bg_domain_button=BG_DOMAIN_UPDATE) - -# SYBPATCH END -######################################################### - - -@app.route('/dashboard-domains', methods=['GET']) -@login_required -def dashboard_domains(): - if current_user.role.name in ['Administrator', 'Operator']: - domains = Domain.query - else: - domains = User(id=current_user.id).get_domain_query() - - template = app.jinja_env.get_template("dashboard_domain.html") - render = template.make_module(vars={"current_user": current_user}) - - columns = [Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master, Domain.account] - # History.created_on.desc() - order_by = [] - for i in range(len(columns)): - column_index = request.args.get("order[{0}][column]".format(i)) - sort_direction = request.args.get("order[{0}][dir]".format(i)) - if column_index is None: - break - if sort_direction != "asc" and sort_direction != "desc": - sort_direction = "asc" - - column = columns[int(column_index)] - order_by.append(getattr(column, sort_direction)()) - - if order_by: - domains = domains.order_by(*order_by) - - total_count = domains.count() - - search = request.args.get("search[value]") - if search: - start = "" if search.startswith("^") else "%" - end = "" if search.endswith("$") else "%" - - if current_user.role.name in ['Administrator', 'Operator']: - domains = domains.outerjoin(Account).filter(Domain.name.ilike(start + search.strip("^$") + end) | - Account.name.ilike(start + search.strip("^$") + end) | - Account.description.ilike(start + search.strip("^$") + end)) - else: - domains = domains.filter(Domain.name.ilike(start + search.strip("^$") + end)) - - filtered_count = domains.count() - - start = int(request.args.get("start", 0)) - length = min(int(request.args.get("length", 0)), 100) - - if length != -1: - domains = domains[start:start + length] - - data = [] - for domain in domains: - data.append([ - render.name(domain), - render.dnssec(domain), - render.type(domain), - render.serial(domain), - render.master(domain), - render.account(domain), - render.actions(domain), - ]) - - response_data = { - "draw": int(request.args.get("draw", 0)), - "recordsTotal": total_count, - "recordsFiltered": filtered_count, - "data": data, - } - return jsonify(response_data) - -@app.route('/dashboard-domains-updater', methods=['GET', 'POST']) -@login_required -def dashboard_domains_updater(): - logging.debug('Update domains in background') - d = Domain().update() - - response_data = { - "result": d, - } - return jsonify(response_data) - - -@app.route('/domain/', methods=['GET']) -@login_required -@can_access_domain -def domain(domain_name): - r = Record() - domain = Domain.query.filter(Domain.name == domain_name).first() - if not domain: - return redirect(url_for('error', code=404)) - - # query domain info from PowerDNS API - zone_info = r.get_record_data(domain.name) - if zone_info: - jrecords = zone_info['records'] - else: - # can not get any record, API server might be down - return redirect(url_for('error', code=500)) - - quick_edit = Setting().get('record_quick_edit') - records_allow_to_edit = Setting().get_records_allow_to_edit() - forward_records_allow_to_edit = Setting().get_forward_records_allow_to_edit() - reverse_records_allow_to_edit = Setting().get_reverse_records_allow_to_edit() - ttl_options = Setting().get_ttl_options() - records = [] - - if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'): - for jr in jrecords: - if jr['type'] in records_allow_to_edit: - for subrecord in jr['records']: - record = RecordEntry(name=jr['name'], type=jr['type'], status='Disabled' if subrecord['disabled'] else 'Active', ttl=jr['ttl'], data=subrecord['content'], is_allowed_edit=True) - records.append(record) - if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): - editable_records = forward_records_allow_to_edit - else: - editable_records = reverse_records_allow_to_edit - return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit, ttl_options=ttl_options) - else: - for jr in jrecords: - if jr['type'] in records_allow_to_edit: - record = RecordEntry(name=jr['name'], type=jr['type'], status='Disabled' if jr['disabled'] else 'Active', ttl=jr['ttl'], data=jr['content'], is_allowed_edit=True) - records.append(record) - if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): - editable_records = forward_records_allow_to_edit - else: - editable_records = reverse_records_allow_to_edit - return render_template('domain.html', domain=domain, records=records, editable_records=editable_records, quick_edit=quick_edit, ttl_options=ttl_options) - - -@app.route('/admin/domain/add', methods=['GET', 'POST']) -@login_required -@can_create_domain -def domain_add(): - templates = DomainTemplate.query.all() - if request.method == 'POST': - try: - domain_name = request.form.getlist('domain_name')[0] - domain_type = request.form.getlist('radio_type')[0] - domain_template = request.form.getlist('domain_template')[0] - soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0] - account_id = request.form.getlist('accountid')[0] - - if ' ' in domain_name or not domain_name or not domain_type: - return render_template('errors/400.html', msg="Please correct your input"), 400 - - if domain_type == 'slave': - if request.form.getlist('domain_master_address'): - domain_master_string = request.form.getlist('domain_master_address')[0] - domain_master_string = domain_master_string.replace(' ','') - domain_master_ips = domain_master_string.split(',') - else: - domain_master_ips = [] - - account_name = Account().get_name_by_id(account_id) - - d = Domain() - result = d.add(domain_name=domain_name, domain_type=domain_type, soa_edit_api=soa_edit_api, domain_master_ips=domain_master_ips, account_name=account_name) - if result['status'] == 'ok': - history = History(msg='Add domain {0}'.format(domain_name), detail=str({'domain_type': domain_type, 'domain_master_ips': domain_master_ips, 'account_id': account_id}), created_by=current_user.username) - history.add() - - # grant user access to the domain - Domain(name=domain_name).grant_privileges([current_user.username]) - - # apply template if needed - if domain_template != '0': - template = DomainTemplate.query.filter(DomainTemplate.id == domain_template).first() - template_records = DomainTemplateRecord.query.filter(DomainTemplateRecord.template_id == domain_template).all() - record_data = [] - for template_record in template_records: - record_row = {'record_data': template_record.data, 'record_name': template_record.name, 'record_status': template_record.status, 'record_ttl': template_record.ttl, 'record_type': template_record.type} - record_data.append(record_row) - r = Record() - result = r.apply(domain_name, record_data) - if result['status'] == 'ok': - history = History(msg='Applying template {0} to {1}, created records successfully.'.format(template.name, domain_name), detail=str(result), created_by=current_user.username) - history.add() - else: - history = History(msg='Applying template {0} to {1}, FAILED to created records.'.format(template.name, domain_name), detail=str(result), created_by=current_user.username) - history.add() - return redirect(url_for('dashboard')) - else: - return render_template('errors/400.html', msg=result['msg']), 400 - except Exception as e: - logging.error('Cannot add domain. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return redirect(url_for('error', code=500)) - - else: - accounts = Account.query.all() - return render_template('domain_add.html', templates=templates, accounts=accounts) - - -@app.route('/admin/domain//delete', methods=['POST']) -@login_required -@operator_role_required -def domain_delete(domain_name): - d = Domain() - result = d.delete(domain_name) - - if result['status'] == 'error': - return redirect(url_for('error', code=500)) - - history = History(msg='Delete domain {0}'.format(domain_name), created_by=current_user.username) - history.add() - - return redirect(url_for('dashboard')) - - -@app.route('/admin/domain//manage', methods=['GET', 'POST']) -@login_required -@operator_role_required -def domain_management(domain_name): - if request.method == 'GET': - domain = Domain.query.filter(Domain.name == domain_name).first() - if not domain: - return redirect(url_for('error', code=404)) - users = User.query.all() - accounts = Account.query.all() - - # get list of user ids to initialize selection data - d = Domain(name=domain_name) - domain_user_ids = d.get_user() - account = d.get_account() - - return render_template('domain_management.html', domain=domain, users=users, domain_user_ids=domain_user_ids, accounts=accounts, domain_account=account) - - if request.method == 'POST': - # username in right column - new_user_list = request.form.getlist('domain_multi_user[]') - - # grant/revoke user privileges - d = Domain(name=domain_name) - d.grant_privileges(new_user_list) - - history = History(msg='Change domain {0} access control'.format(domain_name), detail=str({'user_has_access': new_user_list}), created_by=current_user.username) - history.add() - - return redirect(url_for('domain_management', domain_name=domain_name)) - - -@app.route('/admin/domain//change_soa_setting', methods=['POST']) -@login_required -@operator_role_required -def domain_change_soa_edit_api(domain_name): - domain = Domain.query.filter(Domain.name == domain_name).first() - if not domain: - return redirect(url_for('error', code=404)) - new_setting = request.form.get('soa_edit_api') - if new_setting is None: - return redirect(url_for('error', code=500)) - if new_setting == '0': - return redirect(url_for('domain_management', domain_name=domain_name)) - - d = Domain() - status = d.update_soa_setting(domain_name=domain_name, soa_edit_api=new_setting) - if status['status'] != None: - users = User.query.all() - accounts = Account.query.all() - d = Domain(name=domain_name) - domain_user_ids = d.get_user() - account = d.get_account() - return render_template('domain_management.html', domain=domain, users=users, domain_user_ids=domain_user_ids, accounts=accounts, domain_account=account) - else: - return redirect(url_for('error', code=500)) - - -@app.route('/admin/domain//change_account', methods=['POST']) -@login_required -@operator_role_required -def domain_change_account(domain_name): - domain = Domain.query.filter(Domain.name == domain_name).first() - if not domain: - return redirect(url_for('error', code=404)) - - account_id = request.form.get('accountid') - status = Domain(name=domain.name).assoc_account(account_id) - if status['status']: - return redirect(url_for('domain_management', domain_name=domain.name)) - else: - return redirect(url_for('error', code=500)) - - -@app.route('/domain//apply', methods=['POST'], strict_slashes=False) -@login_required -@can_access_domain -def record_apply(domain_name): - """ - example jdata: {u'record_ttl': u'1800', u'record_type': u'CNAME', u'record_name': u'test4', u'record_status': u'Active', u'record_data': u'duykhanh.me'} - """ - #TODO: filter removed records / name modified records. - - try: - jdata = request.json - - submitted_serial = jdata['serial'] - submitted_record = jdata['record'] - - domain = Domain.query.filter(Domain.name==domain_name).first() - - logging.debug('Your submitted serial: {0}'.format(submitted_serial)) - logging.debug('Current domain serial: {0}'.format(domain.serial)) - - if domain: - if int(submitted_serial) != domain.serial: - return make_response(jsonify( {'status': 'error', 'msg': 'The zone has been changed by another session or user. Please refresh this web page to load updated records.'} ), 500) - else: - return make_response(jsonify( {'status': 'error', 'msg': 'Domain name {0} does not exist'.format(domain_name)} ), 404) - - r = Record() - result = r.apply(domain_name, submitted_record) - if result['status'] == 'ok': - jdata.pop('_csrf_token', None) # don't store csrf token in the history. - history = History(msg='Apply record changes to domain {0}'.format(domain_name), detail=str(json.dumps(jdata)), created_by=current_user.username) - history.add() - return make_response(jsonify( result ), 200) - else: - return make_response(jsonify( result ), 400) - except Exception as e: - logging.error('Cannot apply record changes. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify( {'status': 'error', 'msg': 'Error when applying new changes'} ), 500) - - -@app.route('/domain//update', methods=['POST'], strict_slashes=False) -@login_required -@can_access_domain -def record_update(domain_name): - """ - This route is used for domain work as Slave Zone only - Pulling the records update from its Master - """ - try: - jdata = request.json - - domain_name = jdata['domain'] - d = Domain() - result = d.update_from_master(domain_name) - if result['status'] == 'ok': - return make_response(jsonify( {'status': 'ok', 'msg': result['msg']} ), 200) - else: - return make_response(jsonify( {'status': 'error', 'msg': result['msg']} ), 500) - except Exception as e: - logging.error('Cannot update record. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify( {'status': 'error', 'msg': 'Error when applying new changes'} ), 500) - - -@app.route('/domain//info', methods=['GET']) -@login_required -@can_access_domain -def domain_info(domain_name): - domain = Domain() - domain_info = domain.get_domain_info(domain_name) - return make_response(jsonify(domain_info), 200) - - -@app.route('/domain//dnssec', methods=['GET']) -@login_required -@can_access_domain -def domain_dnssec(domain_name): - domain = Domain() - dnssec = domain.get_domain_dnssec(domain_name) - return make_response(jsonify(dnssec), 200) - - -@app.route('/domain//dnssec/enable', methods=['POST']) -@login_required -@can_access_domain -@can_configure_dnssec -def domain_dnssec_enable(domain_name): - domain = Domain() - dnssec = domain.enable_domain_dnssec(domain_name) - return make_response(jsonify(dnssec), 200) - - -@app.route('/domain//dnssec/disable', methods=['POST']) -@login_required -@can_access_domain -@can_configure_dnssec -def domain_dnssec_disable(domain_name): - domain = Domain() - dnssec = domain.get_domain_dnssec(domain_name) - - for key in dnssec['dnssec']: - domain.delete_dnssec_key(domain_name,key['id']); - - return make_response(jsonify( { 'status': 'ok', 'msg': 'DNSSEC removed.' } )) - - -@app.route('/domain//managesetting', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_setdomainsetting(domain_name): - if request.method == 'POST': - # - # post data should in format - # {'action': 'set_setting', 'setting': 'default_action, 'value': 'True'} - # - try: - jdata = request.json - data = jdata['data'] - - if jdata['action'] == 'set_setting': - new_setting = data['setting'] - new_value = str(data['value']) - domain = Domain.query.filter(Domain.name == domain_name).first() - setting = DomainSetting.query.filter(DomainSetting.domain == domain).filter(DomainSetting.setting == new_setting).first() - - if setting: - if setting.set(new_value): - history = History(msg='Setting {0} changed value to {1} for {2}'.format(new_setting, new_value, domain.name), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'Setting updated.' } )) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to set value of setting.' } )) - else: - if domain.add_setting(new_setting, new_value): - history = History(msg='New setting {0} with value {1} for {2} has been created'.format(new_setting, new_value, domain.name), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'New setting created and updated.' } )) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to create new setting.' } )) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400) - except Exception as e: - logging.error('Cannot change domain setting. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400) - - -@app.route('/templates', methods=['GET', 'POST']) -@app.route('/templates/list', methods=['GET', 'POST']) -@login_required -@operator_role_required -def templates(): - templates = DomainTemplate.query.all() - return render_template('template.html', templates=templates) - - -@app.route('/template/create', methods=['GET', 'POST']) -@login_required -@operator_role_required -def create_template(): - if request.method == 'GET': - return render_template('template_add.html') - if request.method == 'POST': - try: - name = request.form.getlist('name')[0] - description = request.form.getlist('description')[0] - - if ' ' in name or not name or not type: - flash("Please correct your input", 'error') - return redirect(url_for('create_template')) - - if DomainTemplate.query.filter(DomainTemplate.name == name).first(): - flash("A template with the name {0} already exists!".format(name), 'error') - return redirect(url_for('create_template')) - - t = DomainTemplate(name=name, description=description) - result = t.create() - if result['status'] == 'ok': - history = History(msg='Add domain template {0}'.format(name), detail=str({'name': name, 'description': description}), created_by=current_user.username) - history.add() - return redirect(url_for('templates')) - else: - flash(result['msg'], 'error') - return redirect(url_for('create_template')) - except Exception as e: - logging.error('Cannot create domain template. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return redirect(url_for('error', code=500)) - - -@app.route('/template/createfromzone', methods=['POST']) -@login_required -@operator_role_required -def create_template_from_zone(): - try: - jdata = request.json - name = jdata['name'] - description = jdata['description'] - domain_name = jdata['domain'] - - if ' ' in name or not name or not type: - return make_response(jsonify({'status': 'error', 'msg': 'Please correct template name'}), 500) - - if DomainTemplate.query.filter(DomainTemplate.name == name).first(): - return make_response(jsonify({'status': 'error', 'msg': 'A template with the name {0} already exists!'.format(name)}), 500) - - t = DomainTemplate(name=name, description=description) - result = t.create() - if result['status'] == 'ok': - history = History(msg='Add domain template {0}'.format(name), detail=str({'name': name, 'description': description}), created_by=current_user.username) - history.add() - - records = [] - r = Record() - domain = Domain.query.filter(Domain.name == domain_name).first() - if domain: - # query domain info from PowerDNS API - zone_info = r.get_record_data(domain.name) - if zone_info: - jrecords = zone_info['records'] - - if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'): - for jr in jrecords: - if jr['type'] in Setting().get_records_allow_to_edit(): - name = '@' if jr['name'] == domain_name else re.sub('\.{}$'.format(domain_name), '', jr['name']) - for subrecord in jr['records']: - record = DomainTemplateRecord(name=name, type=jr['type'], status=True if subrecord['disabled'] else False, ttl=jr['ttl'], data=subrecord['content']) - records.append(record) - else: - for jr in jrecords: - if jr['type'] in Setting().get_records_allow_to_edit(): - name = '@' if jr['name'] == domain_name else re.sub('\.{}$'.format(domain_name), '', jr['name']) - record = DomainTemplateRecord(name=name, type=jr['type'], status=True if jr['disabled'] else False, ttl=jr['ttl'], data=jr['content']) - records.append(record) - - result_records = t.replace_records(records) - - if result_records['status'] == 'ok': - return make_response(jsonify({'status': 'ok', 'msg': result['msg']}), 200) - else: - t.delete_template() - return make_response(jsonify({'status': 'error', 'msg': result_records['msg']}), 500) - - else: - return make_response(jsonify({'status': 'error', 'msg': result['msg']}), 500) - except Exception as e: - logging.error('Cannot create template from zone. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500) - - -@app.route('/template//edit', methods=['GET']) -@login_required -@operator_role_required -def edit_template(template): - try: - t = DomainTemplate.query.filter(DomainTemplate.name == template).first() - records_allow_to_edit = Setting().get_records_allow_to_edit() - quick_edit = Setting().get('record_quick_edit') - ttl_options = Setting().get_ttl_options() - if t is not None: - records = [] - for jr in t.records: - if jr.type in records_allow_to_edit: - record = DomainTemplateRecord(name=jr.name, type=jr.type, status='Disabled' if jr.status else 'Active', ttl=jr.ttl, data=jr.data) - records.append(record) - - return render_template('template_edit.html', template=t.name, records=records, editable_records=records_allow_to_edit, quick_edit=quick_edit, ttl_options=ttl_options) - except Exception as e: - logging.error('Cannot open domain template page. DETAIL: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return redirect(url_for('error', code=500)) - return redirect(url_for('templates')) - - -@app.route('/template//apply', methods=['POST'], strict_slashes=False) -@login_required -def apply_records(template): - try: - jdata = request.json - records = [] - - for j in jdata['records']: - name = '@' if j['record_name'] in ['@', ''] else j['record_name'] - type = j['record_type'] - data = j['record_data'] - disabled = True if j['record_status'] == 'Disabled' else False - ttl = int(j['record_ttl']) if j['record_ttl'] else 3600 - - dtr = DomainTemplateRecord(name=name, type=type, data=data, status=disabled, ttl=ttl) - records.append(dtr) - - t = DomainTemplate.query.filter(DomainTemplate.name == template).first() - result = t.replace_records(records) - if result['status'] == 'ok': - jdata.pop('_csrf_token', None) # don't store csrf token in the history. - history = History(msg='Apply domain template record changes to domain template {0}'.format(template), detail=str(json.dumps(jdata)), created_by=current_user.username) - history.add() - return make_response(jsonify(result), 200) - else: - return make_response(jsonify(result), 400) - except Exception as e: - logging.error('Cannot apply record changes to the template. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify({'status': 'error', 'msg': 'Error when applying new changes'}), 500) - - -@app.route('/template//delete', methods=['POST']) -@login_required -@operator_role_required -def delete_template(template): - try: - t = DomainTemplate.query.filter(DomainTemplate.name == template).first() - if t is not None: - result = t.delete_template() - if result['status'] == 'ok': - history = History(msg='Deleted domain template {0}'.format(template), detail=str({'name': template}), created_by=current_user.username) - history.add() - return redirect(url_for('templates')) - else: - flash(result['msg'], 'error') - return redirect(url_for('templates')) - except Exception as e: - logging.error('Cannot delete template. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return redirect(url_for('error', code=500)) - return redirect(url_for('templates')) - - -@app.route('/admin/pdns', methods=['GET']) -@login_required -@operator_role_required -def admin_pdns(): - if not Setting().get('pdns_api_url') or not Setting().get('pdns_api_key') or not Setting().get('pdns_version'): - return redirect(url_for('admin_setting_pdns')) - - domains = Domain.query.all() - users = User.query.all() - - server = Server(server_id='localhost') - configs = server.get_config() - statistics = server.get_statistic() - history_number = History.query.count() - - if statistics: - uptime = list([uptime for uptime in statistics if uptime['name'] == 'uptime'])[0]['value'] - else: - uptime = 0 - - return render_template('admin.html', domains=domains, users=users, configs=configs, statistics=statistics, uptime=uptime, history_number=history_number) - - -@app.route('/admin/user/edit/', methods=['GET', 'POST']) -@app.route('/admin/user/edit', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_edituser(user_username=None): - if user_username: - user = User.query.filter(User.username == user_username).first() - create = False - - if not user: - return render_template('errors/404.html'), 404 - - if user.role.name == 'Administrator' and current_user.role.name != 'Administrator': - return render_template('errors/401.html'), 401 - else: - user = None - create = True - - if request.method == 'GET': - return render_template('admin_edituser.html', user=user, create=create) - - elif request.method == 'POST': - fdata = request.form - - if create: - user_username = fdata['username'] - - user = User(username=user_username, plain_text_password=fdata['password'], firstname=fdata['firstname'], lastname=fdata['lastname'], email=fdata['email'], reload_info=False) - - if create: - if fdata['password'] == "": - return render_template('admin_edituser.html', user=user, create=create, blank_password=True) - - result = user.create_local_user() - history = History(msg='Created user {0}'.format(user.username), created_by=current_user.username) - - else: - result = user.update_local_user() - history = History(msg='Updated user {0}'.format(user.username), created_by=current_user.username) - - if result['status']: - history.add() - return redirect(url_for('admin_manageuser')) - - return render_template('admin_edituser.html', user=user, create=create, error=result['msg']) - - -@app.route('/admin/manageuser', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_manageuser(): - if request.method == 'GET': - roles = Role.query.all() - users = User.query.order_by(User.username).all() - return render_template('admin_manageuser.html', users=users, roles=roles) - - if request.method == 'POST': - # - # post data should in format - # {'action': 'delete_user', 'data': 'username'} - # - try: - jdata = request.json - data = jdata['data'] - - if jdata['action'] == 'user_otp_disable': - user = User(username=data) - result = user.update_profile(enable_otp=False) - if result: - history = History(msg='Two factor authentication disabled for user {0}'.format(data), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'Two factor authentication has been disabled for user.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Cannot disable two factor authentication for user.' } ), 500) - - if jdata['action'] == 'delete_user': - user = User(username=data) - if user.username == current_user.username: - return make_response(jsonify( { 'status': 'error', 'msg': 'You cannot delete yourself.' } ), 400) - result = user.delete() - if result: - history = History(msg='Delete username {0}'.format(data), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'User has been removed.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Cannot remove user.' } ), 500) - - elif jdata['action'] == 'revoke_user_privileges': - user = User(username=data) - result = user.revoke_privilege() - if result: - history = History(msg='Revoke {0} user privileges'.format(data), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'Revoked user privileges.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Cannot revoke user privilege.' } ), 500) - - elif jdata['action'] == 'update_user_role': - username = data['username'] - role_name = data['role_name'] - - if username == current_user.username: - return make_response(jsonify( { 'status': 'error', 'msg': 'You cannot change you own roles.' } ), 400) - - user = User.query.filter(User.username==username).first() - if not user: - return make_response(jsonify( { 'status': 'error', 'msg': 'User does not exist.' } ), 404) - - if user.role.name == 'Administrator' and current_user.role.name != 'Administrator': - return make_response(jsonify( { 'status': 'error', 'msg': 'You do not have permission to change Administrator users role.' } ), 400) - - if role_name == 'Administrator' and current_user.role.name != 'Administrator': - return make_response(jsonify( { 'status': 'error', 'msg': 'You do not have permission to promote a user to Administrator role.' } ), 400) - - user = User(username=username) - result = user.set_role(role_name) - if result['status']: - history = History(msg='Change user role of {0} to {1}'.format(username, role_name), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'Changed user role successfully.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Cannot change user role. {0}'.format(result['msg']) } ), 500) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400) - except Exception as e: - logging.error('Cannot update user. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400) - - -@app.route('/admin/account/edit/', methods=['GET', 'POST']) -@app.route('/admin/account/edit', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_editaccount(account_name=None): - users = User.query.all() - - if request.method == 'GET': - if account_name is None: - return render_template('admin_editaccount.html', users=users, create=1) - - else: - account = Account.query.filter(Account.name == account_name).first() - account_user_ids = account.get_user() - return render_template('admin_editaccount.html', account=account, account_user_ids=account_user_ids, users=users, create=0) - - if request.method == 'POST': - fdata = request.form - new_user_list = request.form.getlist('account_multi_user') - - # on POST, synthesize account and account_user_ids from form data - if not account_name: - account_name = fdata['accountname'] - - account = Account(name=account_name, description=fdata['accountdescription'], contact=fdata['accountcontact'], mail=fdata['accountmail']) - account_user_ids = [] - for username in new_user_list: - userid = User(username=username).get_user_info_by_username().id - account_user_ids.append(userid) - - create = int(fdata['create']) - if create: - # account __init__ sanitizes and lowercases the name, so to manage expectations - # we let the user reenter the name until it's not empty and it's valid (ignoring the case) - if account.name == "" or account.name != account_name.lower(): - return render_template('admin_editaccount.html', account=account, account_user_ids=account_user_ids, users=users, create=create, invalid_accountname=True) - - if Account.query.filter(Account.name == account.name).first(): - return render_template('admin_editaccount.html', account=account, account_user_ids=account_user_ids, users=users, create=create, duplicate_accountname=True) - - result = account.create_account() - history = History(msg='Create account {0}'.format(account.name), created_by=current_user.username) - - else: - result = account.update_account() - history = History(msg='Update account {0}'.format(account.name), created_by=current_user.username) - - if result['status']: - account.grant_privileges(new_user_list) - history.add() - return redirect(url_for('admin_manageaccount')) - - return render_template('admin_editaccount.html', account=account, account_user_ids=account_user_ids, users=users, create=create, error=result['msg']) - - -@app.route('/admin/manageaccount', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_manageaccount(): - if request.method == 'GET': - accounts = Account.query.order_by(Account.name).all() - for account in accounts: - account.user_num = AccountUser.query.filter(AccountUser.account_id==account.id).count() - return render_template('admin_manageaccount.html', accounts=accounts) - - if request.method == 'POST': - # - # post data should in format - # {'action': 'delete_account', 'data': 'accountname'} - # - try: - jdata = request.json - data = jdata['data'] - - if jdata['action'] == 'delete_account': - account = Account(name=data) - result = account.delete_account() - if result: - history = History(msg='Delete account {0}'.format(data), created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'Account has been removed.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Cannot remove account.' } ), 500) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Action not supported.' } ), 400) - except Exception as e: - logging.error('Cannot update account. Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return make_response(jsonify( { 'status': 'error', 'msg': 'There is something wrong, please contact Administrator.' } ), 400) - - -@app.route('/admin/history', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_history(): - if request.method == 'POST': - if current_user.role.name != 'Administrator': - return make_response(jsonify( { 'status': 'error', 'msg': 'You do not have permission to remove history.' } ), 401) - - h = History() - result = h.remove_all() - if result: - history = History(msg='Remove all histories', created_by=current_user.username) - history.add() - return make_response(jsonify( { 'status': 'ok', 'msg': 'Changed user role successfully.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Can not remove histories.' } ), 500) - - if request.method == 'GET': - histories = History.query.all() - return render_template('admin_history.html', histories=histories) - - -@app.route('/admin/setting/basic', methods=['GET']) -@login_required -@operator_role_required -def admin_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', - 'bg_domain_updates', - 'site_name', - 'session_timeout', - 'ttl_options' ] - - return render_template('admin_setting_basic.html', settings=settings) - - -@app.route('/admin/setting/basic//edit', methods=['POST']) -@login_required -@operator_role_required -def admin_setting_basic_edit(setting): - jdata = request.json - new_value = jdata['value'] - result = Setting().set(setting, new_value) - - if (result): - return make_response(jsonify( { 'status': 'ok', 'msg': 'Toggled setting successfully.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) - - -@app.route('/admin/setting/basic//toggle', methods=['POST']) -@login_required -@operator_role_required -def admin_setting_basic_toggle(setting): - result = Setting().toggle(setting) - if (result): - return make_response(jsonify( { 'status': 'ok', 'msg': 'Toggled setting successfully.' } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'Unable to toggle setting.' } ), 500) - - -@app.route('/admin/setting/pdns', methods=['GET', 'POST']) -@login_required -@admin_role_required -def admin_setting_pdns(): - if request.method == 'GET': - pdns_api_url = Setting().get('pdns_api_url') - pdns_api_key = Setting().get('pdns_api_key') - pdns_version = Setting().get('pdns_version') - return render_template('admin_setting_pdns.html', pdns_api_url=pdns_api_url, pdns_api_key=pdns_api_key, pdns_version=pdns_version) - elif request.method == 'POST': - pdns_api_url = request.form.get('pdns_api_url') - pdns_api_key = request.form.get('pdns_api_key') - pdns_version = request.form.get('pdns_version') - - Setting().set('pdns_api_url', pdns_api_url) - Setting().set('pdns_api_key', pdns_api_key) - Setting().set('pdns_version', pdns_version) - - return render_template('admin_setting_pdns.html', pdns_api_url=pdns_api_url, pdns_api_key=pdns_api_key, pdns_version=pdns_version) - - -@app.route('/admin/setting/dns-records', methods=['GET', 'POST']) -@login_required -@operator_role_required -def admin_setting_records(): - if request.method == 'GET': - _fr = Setting().get('forward_records_allow_edit') - _rr = Setting().get('reverse_records_allow_edit') - f_records = literal_eval(_fr) if isinstance(_fr, str) else _fr - r_records = literal_eval(_rr) if isinstance(_rr, str) else _rr - - return render_template('admin_setting_records.html', f_records=f_records, r_records=r_records) - elif request.method == 'POST': - fr = {} - rr = {} - records = Setting().defaults['forward_records_allow_edit'] - for r in records: - fr[r] = True if request.form.get('fr_{0}'.format(r.lower())) else False - rr[r] = True if request.form.get('rr_{0}'.format(r.lower())) else False - - Setting().set('forward_records_allow_edit', str(fr)) - Setting().set('reverse_records_allow_edit', str(rr)) - return redirect(url_for('admin_setting_records')) - - -@app.route('/admin/setting/authentication', methods=['GET', 'POST']) -@login_required -@admin_role_required -def admin_setting_authentication(): - if request.method == 'GET': - return render_template('admin_setting_authentication.html') - elif request.method == 'POST': - conf_type = request.form.get('config_tab') - result = None - - if conf_type == 'general': - local_db_enabled = True if request.form.get('local_db_enabled') else False - signup_enabled = True if request.form.get('signup_enabled', ) else False - - if not local_db_enabled and not Setting().get('ldap_enabled'): - result = {'status': False, 'msg': 'Local DB and LDAP Authentication can not be disabled at the same time.'} - else: - Setting().set('local_db_enabled', local_db_enabled) - Setting().set('signup_enabled', signup_enabled) - result = {'status': True, 'msg': 'Saved successfully'} - elif conf_type == 'ldap': - ldap_enabled = True if request.form.get('ldap_enabled') else False - - if not ldap_enabled and not Setting().get('local_db_enabled'): - result = {'status': False, 'msg': 'Local DB and LDAP Authentication can not be disabled at the same time.'} - else: - Setting().set('ldap_enabled', ldap_enabled) - Setting().set('ldap_type', request.form.get('ldap_type')) - Setting().set('ldap_uri', request.form.get('ldap_uri')) - Setting().set('ldap_base_dn', request.form.get('ldap_base_dn')) - Setting().set('ldap_admin_username', request.form.get('ldap_admin_username')) - Setting().set('ldap_admin_password', request.form.get('ldap_admin_password')) - Setting().set('ldap_filter_basic', request.form.get('ldap_filter_basic')) - Setting().set('ldap_filter_username', request.form.get('ldap_filter_username')) - Setting().set('ldap_sg_enabled', True if request.form.get('ldap_sg_enabled')=='ON' else False) - Setting().set('ldap_admin_group', request.form.get('ldap_admin_group')) - Setting().set('ldap_operator_group', request.form.get('ldap_operator_group')) - Setting().set('ldap_user_group', request.form.get('ldap_user_group')) - Setting().set('ldap_domain', request.form.get('ldap_domain')) - result = {'status': True, 'msg': 'Saved successfully'} - elif conf_type == 'google': - Setting().set('google_oauth_enabled', True if request.form.get('google_oauth_enabled') else False) - Setting().set('google_oauth_client_id', request.form.get('google_oauth_client_id')) - Setting().set('google_oauth_client_secret', request.form.get('google_oauth_client_secret')) - Setting().set('google_token_url', request.form.get('google_token_url')) - Setting().set('google_oauth_scope', request.form.get('google_oauth_scope')) - Setting().set('google_authorize_url', request.form.get('google_authorize_url')) - Setting().set('google_base_url', request.form.get('google_base_url')) - result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'} - elif conf_type == 'github': - Setting().set('github_oauth_enabled', True if request.form.get('github_oauth_enabled') else False) - Setting().set('github_oauth_key', request.form.get('github_oauth_key')) - Setting().set('github_oauth_secret', request.form.get('github_oauth_secret')) - Setting().set('github_oauth_scope', request.form.get('github_oauth_scope')) - Setting().set('github_oauth_api_url', request.form.get('github_oauth_api_url')) - Setting().set('github_oauth_token_url', request.form.get('github_oauth_token_url')) - Setting().set('github_oauth_authorize_url', request.form.get('github_oauth_authorize_url')) - result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'} - elif conf_type == 'oidc': - Setting().set('oidc_oauth_enabled', True if request.form.get('oidc_oauth_enabled') else False) - Setting().set('oidc_oauth_key', request.form.get('oidc_oauth_key')) - Setting().set('oidc_oauth_secret', request.form.get('oidc_oauth_secret')) - Setting().set('oidc_oauth_scope', request.form.get('oidc_oauth_scope')) - Setting().set('oidc_oauth_api_url', request.form.get('oidc_oauth_api_url')) - Setting().set('oidc_oauth_token_url', request.form.get('oidc_oauth_token_url')) - Setting().set('oidc_oauth_authorize_url', request.form.get('oidc_oauth_authorize_url')) - result = {'status': True, 'msg': 'Saved successfully. Please reload PDA to take effect.'} - else: - return abort(400) - - return render_template('admin_setting_authentication.html', result=result) - - -@app.route('/user/profile', methods=['GET', 'POST']) -@login_required -def user_profile(): - if request.method == 'GET': - return render_template('user_profile.html') - if request.method == 'POST': - if session['authentication_type'] == 'LOCAL': - firstname = request.form['firstname'] if 'firstname' in request.form else '' - lastname = request.form['lastname'] if 'lastname' in request.form else '' - email = request.form['email'] if 'email' in request.form else '' - new_password = request.form['password'] if 'password' in request.form else '' - else: - firstname = lastname = email = new_password = '' - logging.warning('Authenticated externally. User {0} information will not allowed to update the profile'.format(current_user.username)) - - if request.data: - jdata = request.json - data = jdata['data'] - if jdata['action'] == 'enable_otp': - if session['authentication_type'] in ['LOCAL', 'LDAP']: - enable_otp = data['enable_otp'] - user = User(username=current_user.username) - user.update_profile(enable_otp=enable_otp) - return make_response(jsonify( { 'status': 'ok', 'msg': 'Change OTP Authentication successfully. Status: {0}'.format(enable_otp) } ), 200) - else: - return make_response(jsonify( { 'status': 'error', 'msg': 'User {0} is externally. You are not allowed to update the OTP'.format(current_user.username) } ), 400) - - # get new avatar - save_file_name = None - if 'file' in request.files: - if session['authentication_type'] in ['LOCAL', 'LDAP']: - file = request.files['file'] - if file: - filename = secure_filename(file.filename) - file_extension = filename.rsplit('.', 1)[1] - - if file_extension.lower() in ['jpg', 'jpeg', 'png']: - save_file_name = current_user.username + '.' + file_extension - file.save(os.path.join(app.config['UPLOAD_DIR'], 'avatar', save_file_name)) - else: - logging.error('Authenticated externally. User {0} is not allowed to update the avatar') - abort(400) - - user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, avatar=save_file_name, reload_info=False) - user.update_profile() - - return render_template('user_profile.html') - - -@app.route('/user/avatar/') -def user_avatar(filename): - return send_from_directory(os.path.join(app.config['UPLOAD_DIR'], 'avatar'), filename) - - -@app.route('/qrcode') -@login_required -def qrcode(): - if not current_user: - return redirect(url_for('index')) - - # render qrcode for FreeTOTP - img = qrc.make(current_user.get_totp_uri(), image_factory=qrc_svg.SvgPathImage) - stream = BytesIO() - img.save(stream) - return stream.getvalue(), 200, { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0'} - - -@app.route('/nic/checkip.html', methods=['GET', 'POST']) -@csrf.exempt -def dyndns_checkip(): - # route covers the default ddclient 'web' setting for the checkip service - return render_template('dyndns.html', response=request.environ.get('HTTP_X_REAL_IP', request.remote_addr)) - - -@app.route('/nic/update', methods=['GET', 'POST']) -@dyndns_login_required -@csrf.exempt -def dyndns_update(): - # dyndns protocol response codes in use are: - # good: update successful - # nochg: IP address already set to update address - # nohost: hostname does not exist for this user account - # 911: server error - # have to use 200 HTTP return codes because ddclient does not read the return string if the code is other than 200 - # reference: https://help.dyn.com/remote-access-api/perform-update/ - # reference: https://help.dyn.com/remote-access-api/return-codes/ - hostname = request.args.get('hostname') - myip = request.args.get('myip') - - if not hostname: - history = History(msg="DynDNS update: missing hostname parameter", created_by=current_user.username) - history.add() - return render_template('dyndns.html', response='nohost'), 200 - - try: - # get all domains owned by the current user - domains = User(id=current_user.id).get_domain() - except Exception as e: - logging.error('DynDNS Error: {0}'.format(e)) - logging.debug(traceback.format_exc()) - return render_template('dyndns.html', response='911'), 200 - - domain = None - domain_segments = hostname.split('.') - for index in range(len(domain_segments)): - full_domain = '.'.join(domain_segments) - potential_domain = Domain.query.filter(Domain.name == full_domain).first() - if potential_domain in domains: - domain = potential_domain - break - domain_segments.pop(0) - - if not domain: - history = History(msg="DynDNS update: attempted update of {0} but it does not exist for this user".format(hostname), created_by=current_user.username) - history.add() - return render_template('dyndns.html', response='nohost'), 200 - - myip_addr = [] - if myip: - for address in myip.split(','): - myip_addr += utils.validate_ipaddress(address) - - remote_addr = utils.validate_ipaddress(request.headers.get('X-Forwarded-For', request.remote_addr).split(', ')[:1]) - - response='nochg' - for ip in myip_addr or remote_addr: - if isinstance(ip, ipaddress.IPv4Address): - rtype='A' - else: - rtype='AAAA' - - r = Record(name=hostname,type=rtype) - # check if the user requested record exists within this domain - if r.exists(domain.name) and r.is_allowed_edit(): - if r.data == str(ip): - # record content did not change, return 'nochg' - history = History(msg="DynDNS update: attempted update of {0} but record did not change".format(hostname), created_by=current_user.username) - history.add() - else: - oldip = r.data - result = r.update(domain.name, str(ip)) - if result['status'] == 'ok': - history = History(msg='DynDNS update: updated {0} record {1} in zone {2}, it changed from {3} to {4}'.format(rtype,hostname,domain.name,oldip,str(ip)), detail=str(result), created_by=current_user.username) - history.add() - response='good' - else: - response='911' - break - elif r.is_allowed_edit(): - ondemand_creation = DomainSetting.query.filter(DomainSetting.domain == domain).filter(DomainSetting.setting == 'create_via_dyndns').first() - if (ondemand_creation is not None) and (strtobool(ondemand_creation.value) == True): - record = Record(name=hostname,type=rtype,data=str(ip),status=False,ttl=3600) - result = record.add(domain.name) - if result['status'] == 'ok': - history = History(msg='DynDNS update: created record {0} in zone {1}, it now represents {2}'.format(hostname,domain.name,str(ip)), detail=str(result), created_by=current_user.username) - history.add() - response='good' - else: - history = History(msg='DynDNS update: attempted update of {0} but it does not exist for this user'.format(hostname), created_by=current_user.username) - history.add() - - return render_template('dyndns.html', response=response), 200 - -@app.route('/', methods=['GET', 'POST']) -@login_required -def index(): - return redirect(url_for('dashboard')) - -# END VIEWS diff --git a/migrations/versions/0fb6d23a4863_remove_user_avatar.py b/migrations/versions/0fb6d23a4863_remove_user_avatar.py new file mode 100644 index 0000000..c04ec57 --- /dev/null +++ b/migrations/versions/0fb6d23a4863_remove_user_avatar.py @@ -0,0 +1,28 @@ +"""Remove user avatar + +Revision ID: 0fb6d23a4863 +Revises: 654298797277 +Create Date: 2019-12-02 10:29:41.945044 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '0fb6d23a4863' +down_revision = '654298797277' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'avatar') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('avatar', mysql.VARCHAR(length=128), nullable=True)) + # ### end Alembic commands ### diff --git a/powerdnsadmin/__init__.py b/powerdnsadmin/__init__.py new file mode 100755 index 0000000..7c402e5 --- /dev/null +++ b/powerdnsadmin/__init__.py @@ -0,0 +1,125 @@ +import os +from werkzeug.contrib.fixers import ProxyFix +from flask import Flask +from flask_seasurf import SeaSurf +from flask_sslify import SSLify + +from .lib import utils + +# from flask_login import LoginManager +# from flask_sqlalchemy import SQLAlchemy as SA +# from flask_migrate import Migrate +# from authlib.flask.client import OAuth as AuthlibOAuth +# from sqlalchemy.exc import OperationalError + +# from app.assets import assets + +# ### SYBPATCH ### +# from app.customboxes import customBoxes +### SYBPATCH ### + +# subclass SQLAlchemy to enable pool_pre_ping +# class SQLAlchemy(SA): +# def apply_pool_defaults(self, app, options): +# SA.apply_pool_defaults(self, app, options) +# options["pool_pre_ping"] = True + +# app = Flask(__name__) +# app.config.from_object('config') +# app.wsgi_app = ProxyFix(app.wsgi_app) +# csrf = SeaSurf(app) + +# assets.init_app(app) + +# #### CONFIGURE LOGGER #### +# from app.lib.log import logger +# logging = logger('powerdns-admin', app.config['LOG_LEVEL'], app.config['LOG_FILE']).config() + +# login_manager = LoginManager() +# login_manager.init_app(app) +# db = SQLAlchemy(app) # database +# migrate = Migrate(app, db) # flask-migrate +# authlib_oauth_client = AuthlibOAuth(app) # authlib oauth + +# if app.config.get('SAML_ENABLED') and app.config.get('SAML_ENCRYPT'): +# from app.lib import certutil +# if not certutil.check_certificate(): +# certutil.create_self_signed_cert() + +# from app import models + +# from app.blueprints.api import api_blueprint + +# app.register_blueprint(api_blueprint, url_prefix='/api/v1') + +# from app import views + + +def create_app(config=None): + from . import models, routes, services + from .assets import assets + app = Flask(__name__) + + # Proxy + app.wsgi_app = ProxyFix(app.wsgi_app) + + # HSTS enabled + _sslify = SSLify(app) + + # CSRF protection + csrf = SeaSurf(app) + csrf.exempt(routes.index.dyndns_checkip) + csrf.exempt(routes.index.dyndns_update) + csrf.exempt(routes.index.saml_authorized) + csrf.exempt(routes.api.api_login_create_zone) + csrf.exempt(routes.api.api_login_delete_zone) + csrf.exempt(routes.api.api_generate_apikey) + csrf.exempt(routes.api.api_delete_apikey) + csrf.exempt(routes.api.api_update_apikey) + csrf.exempt(routes.api.api_zone_subpath_forward) + csrf.exempt(routes.api.api_zone_forward) + csrf.exempt(routes.api.api_create_zone) + + # Load default configuration + app.config.from_object('powerdnsadmin.default_config') + + # Load environment configuration + if 'FLASK_CONF' in os.environ: + app.config.from_envvar('FLASK_CONF') + + # Load app sepecified configuration + if config is not None: + if isinstance(config, dict): + app.config.update(config) + elif config.endswith('.py'): + app.config.from_pyfile(config) + + # Load app's components + assets.init_app(app) + models.init_app(app) + routes.init_app(app) + services.init_app(app) + + # Register filters + 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 + + # Register context proccessors + from .models.setting import Setting + + @app.context_processor + def inject_sitename(): + setting = Setting().get('site_name') + return dict(SITE_NAME=setting) + + @app.context_processor + def inject_setting(): + setting = Setting() + return dict(SETTING=setting) + + return app \ No newline at end of file diff --git a/app/assets.py b/powerdnsadmin/assets.py similarity index 100% rename from app/assets.py rename to powerdnsadmin/assets.py diff --git a/app/decorators.py b/powerdnsadmin/decorators.py similarity index 58% rename from app/decorators.py rename to powerdnsadmin/decorators.py index 97cf294..e06deff 100644 --- a/app/decorators.py +++ b/powerdnsadmin/decorators.py @@ -1,12 +1,12 @@ -from functools import wraps -from flask import g, redirect, url_for, request, abort - -from app.models import Setting -from .models import User, ApiKey import base64 -from app.lib.log import logging -from app.errors import RequestIsNotJSON, NotEnoughPrivileges -from app.errors import DomainAccessForbidden +import binascii +from functools import wraps +from flask import g, redirect, url_for, request, abort, current_app, render_template +from flask_login import current_user + +from .models import User, ApiKey, Setting, Domain +from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges +from .lib.errors import DomainAccessForbidden def admin_role_required(f): @@ -15,9 +15,10 @@ def admin_role_required(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name != 'Administrator': - return redirect(url_for('error', code=401)) + if current_user.role.name != 'Administrator': + abort(403) return f(*args, **kwargs) + return decorated_function @@ -27,9 +28,10 @@ def operator_role_required(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name not in ['Administrator', 'Operator']: - return redirect(url_for('error', code=401)) + if current_user.role.name not in ['Administrator', 'Operator']: + abort(403) return f(*args, **kwargs) + return decorated_function @@ -42,14 +44,21 @@ def can_access_domain(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name not in ['Administrator', 'Operator']: + if current_user.role.name not in ['Administrator', 'Operator']: domain_name = kwargs.get('domain_name') - user_domain = [d.name for d in g.user.get_domain()] + domain = Domain.query.filter(Domain.name == domain_name).first() - if domain_name not in user_domain: - return redirect(url_for('error', code=401)) + if not domain: + abort(404) + + valid_access = Domain(id=domain.id).is_valid_access( + current_user.id) + + if not valid_access: + abort(403) return f(*args, **kwargs) + return decorated_function @@ -61,10 +70,13 @@ def can_configure_dnssec(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name not in ['Administrator', 'Operator'] and Setting().get('dnssec_admins_only'): - return redirect(url_for('error', code=401)) + if current_user.role.name not in [ + 'Administrator', 'Operator' + ] and Setting().get('dnssec_admins_only'): + abort(403) return f(*args, **kwargs) + return decorated_function @@ -76,9 +88,12 @@ def can_create_domain(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name not in ['Administrator', 'Operator'] and not Setting().get('allow_user_create_domain'): - return redirect(url_for('error', code=401)) + if current_user.role.name not in [ + 'Administrator', 'Operator' + ] and not Setting().get('allow_user_create_domain'): + abort(403) return f(*args, **kwargs) + return decorated_function @@ -92,38 +107,40 @@ def api_basic_auth(f): try: auth_header = str(base64.b64decode(auth_header), 'utf-8') username, password = auth_header.split(":") + except binascii.Error as e: + current_app.logger.error( + 'Invalid base64-encoded of credential. Error {0}'.format( + e)) + abort(401) except TypeError as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(401) - user = User( - username=username, - password=password, - plain_text_password=password - ) + user = User(username=username, + password=password, + plain_text_password=password) try: auth_method = request.args.get('auth_method', 'LOCAL') auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL' - auth = user.is_validate( - method=auth_method, - src_ip=request.remote_addr - ) + auth = user.is_validate(method=auth_method, + src_ip=request.remote_addr) if not auth: - logging.error('Checking user password failed') + current_app.logger.error('Checking user password failed') abort(401) else: user = User.query.filter(User.username == username).first() - g.user = user + current_user = user except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(401) else: - logging.error('Error: Authorization header missing!') + current_app.logger.error('Error: Authorization header missing!') abort(401) return f(*args, **kwargs) + return decorated_function @@ -137,6 +154,7 @@ def is_json(f): return decorated_function + def api_can_create_domain(f): """ Grant access if: @@ -145,11 +163,14 @@ def api_can_create_domain(f): """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name not in ['Administrator', 'Operator'] and not Setting().get('allow_user_create_domain'): + if current_user.role.name not in [ + 'Administrator', 'Operator' + ] and not Setting().get('allow_user_create_domain'): msg = "User {0} does not have enough privileges to create domain" - logging.error(msg.format(g.user.username)) + current_app.logger.error(msg.format(current_user.username)) raise NotEnoughPrivileges() return f(*args, **kwargs) + return decorated_function @@ -161,9 +182,10 @@ def apikey_is_admin(f): def decorated_function(*args, **kwargs): if g.apikey.role.name != 'Administrator': msg = "Apikey {0} does not have enough privileges to create domain" - logging.error(msg.format(g.apikey.id)) + current_app.logger.error(msg.format(g.apikey.id)) raise NotEnoughPrivileges() return f(*args, **kwargs) + return decorated_function @@ -179,6 +201,7 @@ def apikey_can_access_domain(f): if zone_id not in domain_names: raise DomainAccessForbidden() return f(*args, **kwargs) + return decorated_function @@ -189,29 +212,41 @@ def apikey_auth(f): if auth_header: try: apikey_val = str(base64.b64decode(auth_header), 'utf-8') + except binascii.Error as e: + current_app.logger.error( + 'Invalid base64-encoded of credential. Error {0}'.format( + e)) + abort(401) except TypeError as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(401) - apikey = ApiKey( - key=apikey_val - ) + apikey = ApiKey(key=apikey_val) apikey.plain_text_password = apikey_val try: auth_method = 'LOCAL' - auth = apikey.is_validate( - method=auth_method, - src_ip=request.remote_addr - ) + auth = apikey.is_validate(method=auth_method, + src_ip=request.remote_addr) g.apikey = auth except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(401) else: - logging.error('Error: API key header missing!') + current_app.logger.error('Error: API key header missing!') abort(401) return f(*args, **kwargs) + + return decorated_function + + +def dyndns_login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if current_user.is_authenticated is False: + return render_template('dyndns.html', response='badauth'), 200 + return f(*args, **kwargs) + return decorated_function diff --git a/powerdnsadmin/default_config.py b/powerdnsadmin/default_config.py new file mode 100644 index 0000000..8a6b283 --- /dev/null +++ b/powerdnsadmin/default_config.py @@ -0,0 +1,102 @@ +import os +basedir = os.path.abspath(os.path.dirname(__file__)) + +SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu' + +# BASIC APP CONFIG +SECRET_KEY = 'We are the world' +BIND_ADDRESS = '0.0.0.0' +PORT = 9191 + +# TIMEOUT - for large zones +TIMEOUT = 10 + +# LOG CONFIG +# - For docker, LOG_FILE='' +LOG_LEVEL = 'DEBUG' + +# DATABASE CONFIG +SQLA_DB_USER = 'pda' +SQLA_DB_PASSWORD = 'changeme' +SQLA_DB_HOST = '127.0.0.1' +SQLA_DB_NAME = 'pda' +SQLALCHEMY_TRACK_MODIFICATIONS = True + +# DATBASE - MySQL +SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME + +# DATABSE - SQLite +#SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db') + +# SAML Authnetication +SAML_ENABLED = False +# SAML_DEBUG = True +# SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml') +# ##Example for ADFS Metadata-URL +# SAML_METADATA_URL = 'https:///FederationMetadata/2007-06/FederationMetadata.xml' +# #Cache Lifetime in Seconds +# SAML_METADATA_CACHE_LIFETIME = 1 + +# # SAML SSO binding format to use +# ## Default: library default (urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect) +# #SAML_IDP_SSO_BINDING = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + +# ## EntityID of the IdP to use. Only needed if more than one IdP is +# ## in the SAML_METADATA_URL +# ### Default: First (only) IdP in the SAML_METADATA_URL +# ### Example: https://idp.example.edu/idp +# #SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp' +# ## NameID format to request +# ### Default: The SAML NameID Format in the metadata if present, +# ### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified +# ### Example: urn:oid:0.9.2342.19200300.100.1.1 +# #SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1' + +# ## Attribute to use for Email address +# ### Default: email +# ### Example: urn:oid:0.9.2342.19200300.100.1.3 +# #SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3' + +# ## Attribute to use for Given name +# ### Default: givenname +# ### Example: urn:oid:2.5.4.42 +# #SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42' + +# ## Attribute to use for Surname +# ### Default: surname +# ### Example: urn:oid:2.5.4.4 +# #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4' + +# ## Attribute to use for username +# ### Default: Use NameID instead +# ### Example: urn:oid:0.9.2342.19200300.100.1.1 +# #SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1' + +# ## Attribute to get admin status from +# ### Default: Don't control admin with SAML attribute +# ### Example: https://example.edu/pdns-admin +# ### If set, look for the value 'true' to set a user as an administrator +# ### If not included in assertion, or set to something other than 'true', +# ### the user is set as a non-administrator user. +# #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin' + +# ## Attribute to get account names from +# ### Default: Don't control accounts with SAML attribute +# ### If set, the user will be added and removed from accounts to match +# ### what's in the login assertion. Accounts that don't exist will +# ### be created and the user added to them. +# SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account' + +# SAML_SP_ENTITY_ID = 'http://' +# SAML_SP_CONTACT_NAME = '' +# SAML_SP_CONTACT_MAIL = '' +# #Cofigures if SAML tokens should be encrypted. +# #If enabled a new app certificate will be generated on restart +# SAML_SIGN_REQUEST = False +# #Use SAML standard logout mechanism retreived from idp metadata +# #If configured false don't care about SAML session on logout. +# #Logout from PowerDNS-Admin only and keep SAML session authenticated. +# SAML_LOGOUT = False +# #Configure to redirect to a different url then PowerDNS-Admin login after SAML logout +# #for example redirect to google.com after successful saml logout +# #SAML_LOGOUT_URL = 'https://google.com' diff --git a/app/lib/__init__.py b/powerdnsadmin/lib/__init__.py similarity index 100% rename from app/lib/__init__.py rename to powerdnsadmin/lib/__init__.py diff --git a/app/lib/certutil.py b/powerdnsadmin/lib/certutil.py similarity index 100% rename from app/lib/certutil.py rename to powerdnsadmin/lib/certutil.py diff --git a/app/errors.py b/powerdnsadmin/lib/errors.py similarity index 86% rename from app/errors.py rename to powerdnsadmin/lib/errors.py index 413595a..a074e66 100644 --- a/app/errors.py +++ b/powerdnsadmin/lib/errors.py @@ -27,6 +27,15 @@ class DomainNotExists(StructuredException): self.name = name +class DomainAlreadyExists(StructuredException): + status_code = 409 + + def __init__(self, name=None, message="Domain already exists"): + StructuredException.__init__(self) + self.message = message + self.name = name + + class DomainAccessForbidden(StructuredException): status_code = 403 @@ -49,7 +58,7 @@ class ApiKeyNotUsable(StructuredException): status_code = 400 def __init__(self, name=None, message="Api key must have domains or have \ - administrative role"): + administrative role" ): StructuredException.__init__(self) self.message = message self.name = name diff --git a/app/lib/helper.py b/powerdnsadmin/lib/helper.py similarity index 65% rename from app/lib/helper.py rename to powerdnsadmin/lib/helper.py index cd09c07..544ca47 100644 --- a/app/lib/helper.py +++ b/powerdnsadmin/lib/helper.py @@ -1,10 +1,8 @@ -from app.models import Setting import requests -from flask import request -import logging as logger from urllib.parse import urljoin +from flask import request, current_app -logging = logger.getLogger(__name__) +from ..models import Setting def forward_request(): @@ -17,13 +15,13 @@ def forward_request(): if request.method != 'GET' and request.method != 'DELETE': msg = msg_str.format(request.get_json(force=True)) - logging.debug(msg) + current_app.logger.debug(msg) data = request.get_json(force=True) verify = False headers = { - 'user-agent': 'powerdnsadmin/0', + 'user-agent': 'powerdns-admin/api', 'pragma': 'no-cache', 'cache-control': 'no-cache', 'accept': 'application/json; q=1', @@ -32,12 +30,10 @@ def forward_request(): url = urljoin(pdns_api_url, request.path) - resp = requests.request( - request.method, - url, - headers=headers, - verify=verify, - json=data - ) + resp = requests.request(request.method, + url, + headers=headers, + verify=verify, + json=data) return resp diff --git a/app/schema.py b/powerdnsadmin/lib/schema.py similarity index 100% rename from app/schema.py rename to powerdnsadmin/lib/schema.py diff --git a/app/lib/utils.py b/powerdnsadmin/lib/utils.py similarity index 53% rename from app/lib/utils.py rename to powerdnsadmin/lib/utils.py index 5154b71..b009728 100644 --- a/app/lib/utils.py +++ b/powerdnsadmin/lib/utils.py @@ -5,60 +5,51 @@ import hashlib import ipaddress import os -from app import app +# from app import app from distutils.version import StrictVersion from urllib.parse import urlparse from datetime import datetime, timedelta from threading import Thread from .certutil import KEY_FILE, CERT_FILE -import logging as logger +# import logging as logger -logging = logger.getLogger(__name__) +# logging = logger.getLogger(__name__) +# if app.config['SAML_ENABLED']: +# from onelogin.saml2.auth import OneLogin_Saml2_Auth +# from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser +# idp_timestamp = datetime(1970, 1, 1) +# idp_data = None +# if 'SAML_IDP_ENTITY_ID' in app.config: +# idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING']) +# else: +# idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None)) +# if idp_data is None: +# print('SAML: IDP Metadata initial load failed') +# exit(-1) +# idp_timestamp = datetime.now() -if app.config['SAML_ENABLED']: - from onelogin.saml2.auth import OneLogin_Saml2_Auth - from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser - idp_timestamp = datetime(1970, 1, 1) - idp_data = None - if 'SAML_IDP_ENTITY_ID' in app.config: - idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING']) - else: - idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None)) - if idp_data is None: - print('SAML: IDP Metadata initial load failed') - exit(-1) - idp_timestamp = datetime.now() +# def get_idp_data(): +# global idp_data, idp_timestamp +# lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME']) +# if idp_timestamp+lifetime < datetime.now(): +# background_thread = Thread(target=retrieve_idp_data) +# background_thread.start() +# return idp_data - -def get_idp_data(): - global idp_data, idp_timestamp - lifetime = timedelta(minutes=app.config['SAML_METADATA_CACHE_LIFETIME']) - if idp_timestamp+lifetime < datetime.now(): - background_thread = Thread(target=retrieve_idp_data) - background_thread.start() - return idp_data - - -def retrieve_idp_data(): - global idp_data, idp_timestamp - if 'SAML_IDP_SSO_BINDING' in app.config: - new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING']) - else: - new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None)) - if new_idp_data is not None: - idp_data = new_idp_data - idp_timestamp = datetime.now() - print("SAML: IDP Metadata successfully retrieved from: " + app.config['SAML_METADATA_URL']) - else: - print("SAML: IDP Metadata could not be retrieved") - - -if 'TIMEOUT' in app.config.keys(): - TIMEOUT = app.config['TIMEOUT'] -else: - TIMEOUT = 10 +# def retrieve_idp_data(): +# global idp_data, idp_timestamp +# if 'SAML_IDP_SSO_BINDING' in app.config: +# new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=app.config['SAML_IDP_SSO_BINDING']) +# else: +# new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None)) +# if new_idp_data is not None: +# idp_data = new_idp_data +# idp_timestamp = datetime.now() +# print("SAML: IDP Metadata successfully retrieved from: " + app.config['SAML_METADATA_URL']) +# else: +# print("SAML: IDP Metadata could not be retrieved") def auth_from_url(url): @@ -70,13 +61,16 @@ def auth_from_url(url): return auth -def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None, timeout=None, headers=None): +def fetch_remote(remote_url, + method='GET', + data=None, + accept=None, + params=None, + timeout=None, + headers=None): if data is not None and type(data) != str: data = json.dumps(data) - if timeout is None: - timeout = TIMEOUT - verify = False our_headers = { @@ -89,29 +83,31 @@ def fetch_remote(remote_url, method='GET', data=None, accept=None, params=None, if headers is not None: our_headers.update(headers) - r = requests.request( - method, - remote_url, - headers=headers, - verify=verify, - auth=auth_from_url(remote_url), - timeout=timeout, - data=data, - params=params - ) + r = requests.request(method, + remote_url, + headers=headers, + verify=verify, + auth=auth_from_url(remote_url), + timeout=timeout, + data=data, + params=params) try: - if r.status_code not in (200, 201, 204, 400, 422): + if r.status_code not in (200, 201, 204, 400, 409, 422): r.raise_for_status() except Exception as e: msg = "Returned status {0} and content {1}" - logging.error(msg.format(r.status_code, r.content)) raise RuntimeError('Error while fetching {0}'.format(remote_url)) return r -def fetch_json(remote_url, method='GET', data=None, params=None, headers=None): - r = fetch_remote(remote_url, method=method, data=data, params=params, headers=headers, +def fetch_json(remote_url, method='GET', data=None, params=None, headers=None, timeout=None): + r = fetch_remote(remote_url, + method=method, + data=data, + params=params, + headers=headers, + timeout=timeout, accept='application/json; q=1') if method == "DELETE": @@ -121,9 +117,10 @@ def fetch_json(remote_url, method='GET', data=None, params=None, headers=None): return {} try: - assert('json' in r.headers['content-type']) + assert ('json' in r.headers['content-type']) except Exception as e: - raise RuntimeError('Error while fetching {0}'.format(remote_url)) from e + raise RuntimeError( + 'Error while fetching {0}'.format(remote_url)) from e # don't use r.json here, as it will read from r.text, which will trigger # content encoding auto-detection in almost all cases, WHICH IS EXTREMELY @@ -132,7 +129,8 @@ def fetch_json(remote_url, method='GET', data=None, params=None, headers=None): try: data = json.loads(r.content.decode('utf-8')) except Exception as e: - raise RuntimeError('Error while loading JSON data from {0}'.format(remote_url)) from e + raise RuntimeError( + 'Error while loading JSON data from {0}'.format(remote_url)) from e return data @@ -157,14 +155,14 @@ def display_time(amount, units='s', remove_seconds=True): Convert timestamp to normal time format """ amount = int(amount) - INTERVALS = [(lambda mlsec:divmod(mlsec, 1000), 'ms'), - (lambda seconds:divmod(seconds, 60), 's'), - (lambda minutes:divmod(minutes, 60), 'm'), - (lambda hours:divmod(hours, 24), 'h'), - (lambda days:divmod(days, 7), 'D'), - (lambda weeks:divmod(weeks, 4), 'W'), - (lambda years:divmod(years, 12), 'M'), - (lambda decades:divmod(decades, 10), 'Y')] + INTERVALS = [(lambda mlsec: divmod(mlsec, 1000), 'ms'), + (lambda seconds: divmod(seconds, 60), 's'), + (lambda minutes: divmod(minutes, 60), 'm'), + (lambda hours: divmod(hours, 24), 'h'), + (lambda days: divmod(days, 7), 'D'), + (lambda weeks: divmod(weeks, 4), 'W'), + (lambda years: divmod(years, 12), 'M'), + (lambda decades: divmod(decades, 10), 'Y')] for index_start, (interval, unit) in enumerate(INTERVALS): if unit == units: @@ -173,13 +171,14 @@ def display_time(amount, units='s', remove_seconds=True): amount_abrev = [] last_index = 0 amount_temp = amount - for index, (formula, abrev) in enumerate(INTERVALS[index_start: len(INTERVALS)]): + for index, (formula, + abrev) in enumerate(INTERVALS[index_start:len(INTERVALS)]): divmod_result = formula(amount_temp) amount_temp = divmod_result[0] amount_abrev.append((divmod_result[1], abrev)) if divmod_result[1] > 0: last_index = index - amount_abrev_partial = amount_abrev[0: last_index + 1] + amount_abrev_partial = amount_abrev[0:last_index + 1] amount_abrev_partial.reverse() final_string = '' @@ -241,50 +240,65 @@ def init_saml_auth(req): settings = {} settings['sp'] = {} if 'SAML_NAMEID_FORMAT' in app.config: - settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT'] + settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT'] else: - settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get('NameIDFormat', 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified') + settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get( + 'NameIDFormat', + 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified') settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID'] if os.path.isfile(CERT_FILE): - cert = open(CERT_FILE, "r").readlines() - settings['sp']['x509cert'] = "".join(cert) + cert = open(CERT_FILE, "r").readlines() + settings['sp']['x509cert'] = "".join(cert) if os.path.isfile(KEY_FILE): - key = open(KEY_FILE, "r").readlines() - settings['sp']['privateKey'] = "".join(key) + key = open(KEY_FILE, "r").readlines() + settings['sp']['privateKey'] = "".join(key) settings['sp']['assertionConsumerService'] = {} - settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized' + settings['sp']['assertionConsumerService'][ + 'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + settings['sp']['assertionConsumerService'][ + 'url'] = own_url + '/saml/authorized' settings['sp']['attributeConsumingService'] = {} settings['sp']['singleLogoutService'] = {} - settings['sp']['singleLogoutService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - settings['sp']['singleLogoutService']['url'] = own_url+'/saml/sls' + settings['sp']['singleLogoutService'][ + 'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + settings['sp']['singleLogoutService']['url'] = own_url + '/saml/sls' settings['idp'] = metadata['idp'] settings['strict'] = True settings['debug'] = app.config['SAML_DEBUG'] settings['security'] = {} - settings['security']['digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + settings['security'][ + 'digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' settings['security']['metadataCacheDuration'] = None settings['security']['metadataValidUntil'] = None settings['security']['requestedAuthnContext'] = True - settings['security']['signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + settings['security'][ + 'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' settings['security']['wantAssertionsEncrypted'] = False settings['security']['wantAttributeStatement'] = True settings['security']['wantNameId'] = True - settings['security']['authnRequestsSigned'] = app.config['SAML_SIGN_REQUEST'] - settings['security']['logoutRequestSigned'] = app.config['SAML_SIGN_REQUEST'] - settings['security']['logoutResponseSigned'] = app.config['SAML_SIGN_REQUEST'] + settings['security']['authnRequestsSigned'] = app.config[ + 'SAML_SIGN_REQUEST'] + settings['security']['logoutRequestSigned'] = app.config[ + 'SAML_SIGN_REQUEST'] + settings['security']['logoutResponseSigned'] = app.config[ + 'SAML_SIGN_REQUEST'] settings['security']['nameIdEncrypted'] = False settings['security']['signMetadata'] = True settings['security']['wantAssertionsSigned'] = True - settings['security']['wantMessagesSigned'] = app.config.get('SAML_WANT_MESSAGE_SIGNED', True) + settings['security']['wantMessagesSigned'] = app.config.get( + 'SAML_WANT_MESSAGE_SIGNED', True) settings['security']['wantNameIdEncrypted'] = False settings['contactPerson'] = {} settings['contactPerson']['support'] = {} - settings['contactPerson']['support']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME'] - settings['contactPerson']['support']['givenName'] = app.config['SAML_SP_CONTACT_MAIL'] + settings['contactPerson']['support']['emailAddress'] = app.config[ + 'SAML_SP_CONTACT_NAME'] + settings['contactPerson']['support']['givenName'] = app.config[ + 'SAML_SP_CONTACT_MAIL'] settings['contactPerson']['technical'] = {} - settings['contactPerson']['technical']['emailAddress'] = app.config['SAML_SP_CONTACT_NAME'] - settings['contactPerson']['technical']['givenName'] = app.config['SAML_SP_CONTACT_MAIL'] + settings['contactPerson']['technical']['emailAddress'] = app.config[ + 'SAML_SP_CONTACT_NAME'] + settings['contactPerson']['technical']['givenName'] = app.config[ + 'SAML_SP_CONTACT_MAIL'] settings['organization'] = {} settings['organization']['en-US'] = {} settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin' @@ -304,11 +318,20 @@ def display_setting_state(value): def validate_ipaddress(address): - try: - ip = ipaddress.ip_address(address) - except ValueError: - pass - else: - if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)): - return [ip] - return [] + try: + ip = ipaddress.ip_address(address) + except ValueError: + pass + else: + if isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)): + return [ip] + return [] + + +class customBoxes: + boxes = { + "reverse": (" ", " "), + "ip6arpa": ("ip6", "%.ip6.arpa"), + "inaddrarpa": ("in-addr", "%.in-addr.arpa") + } + order = ["reverse", "ip6arpa", "inaddrarpa"] diff --git a/powerdnsadmin/models.py b/powerdnsadmin/models.py new file mode 100644 index 0000000..7858b7e --- /dev/null +++ b/powerdnsadmin/models.py @@ -0,0 +1,26 @@ +import sys +import os +import re +import ldap +import ldap.filter +import base64 +import bcrypt +import itertools +import traceback +import pyotp +import dns.reversename +import dns.inet +import dns.name +import pytimeparse +import random +import string + +from ast import literal_eval +from datetime import datetime +from urllib.parse import urljoin +from distutils.util import strtobool +from distutils.version import StrictVersion +from flask_login import AnonymousUserMixin +from app import db, app +from app.lib import utils +from app.lib.log import logging diff --git a/powerdnsadmin/models/__init__.py b/powerdnsadmin/models/__init__.py new file mode 100644 index 0000000..62eccc3 --- /dev/null +++ b/powerdnsadmin/models/__init__.py @@ -0,0 +1,23 @@ +from flask_migrate import Migrate + +from .base import db +from .user import User +from .role import Role +from .account import Account +from .account_user import AccountUser +from .server import Server +from .history import History +from .api_key import ApiKey +from .setting import Setting +from .domain import Domain +from .domain_setting import DomainSetting +from .domain_user import DomainUser +from .domain_template import DomainTemplate +from .domain_template_record import DomainTemplateRecord +from .record import Record +from .record_entry import RecordEntry + + +def init_app(app): + db.init_app(app) + _migrate = Migrate(app, db) diff --git a/powerdnsadmin/models/account.py b/powerdnsadmin/models/account.py new file mode 100644 index 0000000..0fb3c3c --- /dev/null +++ b/powerdnsadmin/models/account.py @@ -0,0 +1,200 @@ +from .base import db +from .user import User +from .account_user import AccountUser + + +class Account(db.Model): + __tablename__ = 'account' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(40), index=True, unique=True, nullable=False) + description = db.Column(db.String(128)) + contact = db.Column(db.String(128)) + mail = db.Column(db.String(128)) + domains = db.relationship("Domain", back_populates="account") + + def __init__(self, name=None, description=None, contact=None, mail=None): + self.name = name + self.description = description + self.contact = contact + self.mail = mail + + if self.name is not None: + self.name = ''.join(c for c in self.name.lower() + if c in "abcdefghijklmnopqrstuvwxyz0123456789") + + def __repr__(self): + return ''.format(self.name) + + def get_name_by_id(self, account_id): + """ + Convert account_id to account_name + """ + account = Account.query.filter(Account.id == account_id).first() + if account is None: + return '' + + return account.name + + def get_id_by_name(self, account_name): + """ + Convert account_name to account_id + """ + # Skip actual database lookup for empty queries + if account_name is None or account_name == "": + return None + + account = Account.query.filter(Account.name == account_name).first() + if account is None: + return None + + return account.id + + def create_account(self): + """ + Create a new account + """ + # Sanity check - account name + if self.name == "": + return {'status': False, 'msg': 'No account name specified'} + + # check that account name is not already used + account = Account.query.filter(Account.name == self.name).first() + if account: + return {'status': False, 'msg': 'Account already exists'} + + db.session.add(self) + db.session.commit() + return {'status': True, 'msg': 'Account created successfully'} + + def update_account(self): + """ + Update an existing account + """ + # Sanity check - account name + if self.name == "": + return {'status': False, 'msg': 'No account name specified'} + + # read account and check that it exists + account = Account.query.filter(Account.name == self.name).first() + if not account: + return {'status': False, 'msg': 'Account does not exist'} + + account.description = self.description + account.contact = self.contact + account.mail = self.mail + + db.session.commit() + return {'status': True, 'msg': 'Account updated successfully'} + + def delete_account(self): + """ + Delete an account + """ + # unassociate all users first + self.grant_privileges([]) + + try: + Account.query.filter(Account.name == self.name).delete() + db.session.commit() + return True + except Exception as e: + db.session.rollback() + logging.error( + 'Cannot delete account {0} from DB. DETAIL: {1}'.format( + self.username, e)) + return False + + def get_user(self): + """ + Get users (id) associated with this account + """ + user_ids = [] + query = db.session.query( + AccountUser, + Account).filter(User.id == AccountUser.user_id).filter( + Account.id == AccountUser.account_id).filter( + Account.name == self.name).all() + for q in query: + user_ids.append(q[0].user_id) + return user_ids + + def grant_privileges(self, new_user_list): + """ + Reconfigure account_user table + """ + account_id = self.get_id_by_name(self.name) + + account_user_ids = self.get_user() + new_user_ids = [ + u.id + for u in User.query.filter(User.username.in_(new_user_list)).all() + ] if new_user_list else [] + + removed_ids = list(set(account_user_ids).difference(new_user_ids)) + added_ids = list(set(new_user_ids).difference(account_user_ids)) + + try: + for uid in removed_ids: + AccountUser.query.filter(AccountUser.user_id == uid).filter( + AccountUser.account_id == account_id).delete() + db.session.commit() + except Exception as e: + db.session.rollback() + logging.error( + 'Cannot revoke user privileges on account {0}. DETAIL: {1}'. + format(self.name, e)) + + try: + for uid in added_ids: + au = AccountUser(account_id, uid) + db.session.add(au) + db.session.commit() + except Exception as e: + db.session.rollback() + logging.error( + 'Cannot grant user privileges to account {0}. DETAIL: {1}'. + format(self.name, e)) + + def revoke_privileges_by_id(self, user_id): + """ + Remove a single user from privilege list based on user_id + """ + new_uids = [u for u in self.get_user() if u != user_id] + users = [] + for uid in new_uids: + users.append(User(id=uid).get_user_info_by_id().username) + + self.grant_privileges(users) + + def add_user(self, user): + """ + Add a single user to Account by User + """ + try: + au = AccountUser(self.id, user.id) + db.session.add(au) + db.session.commit() + return True + except Exception as e: + db.session.rollback() + logging.error( + 'Cannot add user privileges on account {0}. DETAIL: {1}'. + format(self.name, e)) + return False + + def remove_user(self, user): + """ + Remove a single user from Account by User + """ + # TODO: This func is currently used by SAML feature in a wrong way. Fix it + try: + AccountUser.query.filter(AccountUser.user_id == user.id).filter( + AccountUser.account_id == self.id).delete() + db.session.commit() + return True + except Exception as e: + db.session.rollback() + logging.error( + 'Cannot revoke user privileges on account {0}. DETAIL: {1}'. + format(self.name, e)) + return False diff --git a/powerdnsadmin/models/account_user.py b/powerdnsadmin/models/account_user.py new file mode 100644 index 0000000..a582884 --- /dev/null +++ b/powerdnsadmin/models/account_user.py @@ -0,0 +1,17 @@ +from .base import db + + +class AccountUser(db.Model): + __tablename__ = 'account_user' + id = db.Column(db.Integer, primary_key=True) + account_id = db.Column(db.Integer, + db.ForeignKey('account.id'), + nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def __init__(self, account_id, user_id): + self.account_id = account_id + self.user_id = user_id + + def __repr__(self): + return ''.format(self.account_id, self.user_id) \ No newline at end of file diff --git a/powerdnsadmin/models/api_key.py b/powerdnsadmin/models/api_key.py new file mode 100644 index 0000000..52602c0 --- /dev/null +++ b/powerdnsadmin/models/api_key.py @@ -0,0 +1,114 @@ +import random +import string +import bcrypt +from flask import current_app + +from .base import db, domain_apikey +from ..models.role import Role +from ..models.domain import Domain + + +class ApiKey(db.Model): + __tablename__ = "apikey" + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.String(255), unique=True, nullable=False) + description = db.Column(db.String(255)) + role_id = db.Column(db.Integer, db.ForeignKey('role.id')) + role = db.relationship('Role', back_populates="apikeys", lazy=True) + domains = db.relationship("Domain", + secondary=domain_apikey, + back_populates="apikeys") + + def __init__(self, key=None, desc=None, role_name=None, domains=[]): + self.id = None + self.description = desc + self.role_name = role_name + self.domains[:] = domains + if not key: + rand_key = ''.join( + random.choice(string.ascii_letters + string.digits) + for _ in range(15)) + self.plain_key = rand_key + self.key = self.get_hashed_password(rand_key).decode('utf-8') + current_app.logger.debug("Hashed key: {0}".format(self.key)) + else: + self.key = key + + def create(self): + try: + self.role = Role.query.filter(Role.name == self.role_name).first() + db.session.add(self) + db.session.commit() + except Exception as e: + current_app.logger.error('Can not update api key table. Error: {0}'.format(e)) + db.session.rollback() + raise e + + def delete(self): + try: + db.session.delete(self) + db.session.commit() + except Exception as e: + msg_str = 'Can not delete api key template. Error: {0}' + current_app.logger.error(msg_str.format(e)) + db.session.rollback() + raise e + + def update(self, role_name=None, description=None, domains=None): + try: + if role_name: + role = Role.query.filter(Role.name == role_name).first() + self.role_id = role.id + + if description: + self.description = description + + if domains: + domain_object_list = Domain.query \ + .filter(Domain.name.in_(domains)) \ + .all() + self.domains[:] = domain_object_list + + db.session.commit() + except Exception as e: + msg_str = 'Update of apikey failed. Error: {0}' + current_app.logger.error(msg_str.format(e)) + db.session.rollback + raise e + + def get_hashed_password(self, plain_text_password=None): + # Hash a password for the first time + # (Using bcrypt, the salt is saved into the hash itself) + if plain_text_password is None: + return plain_text_password + + if plain_text_password: + pw = plain_text_password + else: + pw = self.plain_text_password + + return bcrypt.hashpw(pw.encode('utf-8'), + current_app.config.get('SALT').encode('utf-8')) + + def check_password(self, hashed_password): + # Check hased password. Using bcrypt, + # the salt is saved into the hash itself + if (self.plain_text_password): + return bcrypt.checkpw(self.plain_text_password.encode('utf-8'), + hashed_password.encode('utf-8')) + return False + + def is_validate(self, method, src_ip=''): + """ + Validate user credential + """ + if method == 'LOCAL': + passw_hash = self.get_hashed_password(self.plain_text_password) + apikey = ApiKey.query \ + .filter(ApiKey.key == passw_hash.decode('utf-8')) \ + .first() + + if not apikey: + raise Exception("Unauthorized") + + return apikey diff --git a/powerdnsadmin/models/base.py b/powerdnsadmin/models/base.py new file mode 100644 index 0000000..7ade5db --- /dev/null +++ b/powerdnsadmin/models/base.py @@ -0,0 +1,7 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() +domain_apikey = db.Table( + 'domain_apikey', + db.Column('domain_id', db.Integer, db.ForeignKey('domain.id')), + db.Column('apikey_id', db.Integer, db.ForeignKey('apikey.id'))) diff --git a/powerdnsadmin/models/domain.py b/powerdnsadmin/models/domain.py new file mode 100644 index 0000000..4f249a4 --- /dev/null +++ b/powerdnsadmin/models/domain.py @@ -0,0 +1,805 @@ +import re +import traceback +from flask import current_app +from urllib.parse import urljoin +from distutils.util import strtobool +from distutils.version import StrictVersion + +from ..lib import utils +from .base import db, domain_apikey +from .setting import Setting +from .user import User +from .account import Account +from .account import AccountUser +from .domain_user import DomainUser +from .domain_setting import DomainSetting +from .history import History + + +class Domain(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), index=True, unique=True) + master = db.Column(db.String(128)) + type = db.Column(db.String(6), nullable=False) + serial = db.Column(db.Integer) + notified_serial = db.Column(db.Integer) + last_check = db.Column(db.Integer) + dnssec = db.Column(db.Integer) + account_id = db.Column(db.Integer, db.ForeignKey('account.id')) + account = db.relationship("Account", back_populates="domains") + settings = db.relationship('DomainSetting', back_populates='domain') + apikeys = db.relationship("ApiKey", + secondary=domain_apikey, + back_populates="domains") + + def __init__(self, + id=None, + name=None, + master=None, + type='NATIVE', + serial=None, + notified_serial=None, + last_check=None, + dnssec=None, + account_id=None): + self.id = id + self.name = name + self.master = master + self.type = type + self.serial = serial + self.notified_serial = notified_serial + self.last_check = last_check + self.dnssec = dnssec + self.account_id = account_id + # PDNS configs + self.PDNS_STATS_URL = Setting().get('pdns_api_url') + self.PDNS_API_KEY = Setting().get('pdns_api_key') + self.PDNS_VERSION = Setting().get('pdns_version') + self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) + + if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'): + self.NEW_SCHEMA = True + else: + self.NEW_SCHEMA = False + + def __repr__(self): + return ''.format(self.name) + + def add_setting(self, setting, value): + try: + self.settings.append(DomainSetting(setting=setting, value=value)) + db.session.commit() + return True + except Exception as e: + current_app.logger.error( + 'Can not create setting {0} for domain {1}. {2}'.format( + setting, self.name, e)) + return False + + def get_domain_info(self, domain_name): + """ + Get all domains which has in PowerDNS + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain_name)), + headers=headers, + timeout=int( + Setting().get('pdns_api_timeout'))) + return jdata + + def get_domains(self): + """ + Get all domains which has in PowerDNS + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + jdata = utils.fetch_json( + urljoin(self.PDNS_STATS_URL, + self.API_EXTENDED_URL + '/servers/localhost/zones'), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout'))) + return jdata + + def get_id_by_name(self, name): + """ + Return domain id + """ + try: + domain = Domain.query.filter(Domain.name == name).first() + return domain.id + except Exception as e: + current_app.logger.error( + 'Domain does not exist. ERROR: {0}'.format(e)) + return None + + def update(self): + """ + Fetch zones (domains) from PowerDNS and update into DB + """ + db_domain = Domain.query.all() + list_db_domain = [d.name for d in db_domain] + dict_db_domain = dict((x.name, x) for x in db_domain) + current_app.logger.info("Found {} entrys in PowerDNS-Admin".format( + len(list_db_domain))) + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + try: + jdata = utils.fetch_json( + urljoin(self.PDNS_STATS_URL, + self.API_EXTENDED_URL + '/servers/localhost/zones'), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout'))) + list_jdomain = [d['name'].rstrip('.') for d in jdata] + current_app.logger.info( + "Found {} entrys in PowerDNS server".format(len(list_jdomain))) + + try: + # domains should remove from db since it doesn't exist in powerdns anymore + should_removed_db_domain = list( + set(list_db_domain).difference(list_jdomain)) + for domain_name in should_removed_db_domain: + self.delete_domain_from_pdnsadmin(domain_name) + except Exception as e: + current_app.logger.error( + 'Can not delete domain from DB. DETAIL: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + + # update/add new domain + for data in jdata: + if 'account' in data: + account_id = Account().get_id_by_name(data['account']) + else: + current_app.logger.debug( + "No 'account' data found in API result - Unsupported PowerDNS version?" + ) + account_id = None + domain = dict_db_domain.get(data['name'].rstrip('.'), None) + if domain: + self.update_pdns_admin_domain(domain, account_id, data) + else: + # add new domain + self.add_domain_to_powerdns_admin(domain=data) + + current_app.logger.info('Update domain finished') + return { + 'status': 'ok', + 'msg': 'Domain table has been updated successfully' + } + except Exception as e: + current_app.logger.error( + 'Can not update domain table. Error: {0}'.format(e)) + return {'status': 'error', 'msg': 'Can not update domain table'} + + def update_pdns_admin_domain(self, domain, account_id, data): + # existing domain, only update if something actually has changed + if (domain.master != str(data['masters']) + or domain.type != data['kind'] + or domain.serial != data['serial'] + or domain.notified_serial != data['notified_serial'] + or domain.last_check != (1 if data['last_check'] else 0) + or domain.dnssec != data['dnssec'] + or domain.account_id != account_id): + + domain.master = str(data['masters']) + domain.type = data['kind'] + domain.serial = data['serial'] + domain.notified_serial = data['notified_serial'] + domain.last_check = 1 if data['last_check'] else 0 + domain.dnssec = 1 if data['dnssec'] else 0 + domain.account_id = account_id + try: + db.session.commit() + current_app.logger.info("Updated PDNS-Admin domain {0}".format( + domain.name)) + except Exception as e: + db.session.rollback() + current_app.logger.info("Rolledback Domain {0} {1}".format( + domain.name, e)) + raise + + def add(self, + domain_name, + domain_type, + soa_edit_api, + domain_ns=[], + domain_master_ips=[], + account_name=None): + """ + Add a domain to power dns + """ + + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + if self.NEW_SCHEMA: + domain_name = domain_name + '.' + domain_ns = [ns + '.' for ns in domain_ns] + + if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]: + soa_edit_api = 'DEFAULT' + + elif soa_edit_api == 'OFF': + soa_edit_api = '' + + post_data = { + "name": domain_name, + "kind": domain_type, + "masters": domain_master_ips, + "nameservers": domain_ns, + "soa_edit_api": soa_edit_api, + "account": account_name + } + + try: + jdata = utils.fetch_json( + urljoin(self.PDNS_STATS_URL, + self.API_EXTENDED_URL + '/servers/localhost/zones'), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='POST', + data=post_data) + if 'error' in jdata.keys(): + current_app.logger.error(jdata['error']) + return {'status': 'error', 'msg': jdata['error']} + else: + current_app.logger.info( + 'Added domain successfully to PowerDNS: {0}'.format( + domain_name)) + self.add_domain_to_powerdns_admin(domain_dict=post_data) + return {'status': 'ok', 'msg': 'Added domain successfully'} + except Exception as e: + current_app.logger.error('Cannot add domain {0} {1}'.format( + domain_name, e)) + current_app.logger.debug(traceback.format_exc()) + return {'status': 'error', 'msg': 'Cannot add this domain.'} + + def add_domain_to_powerdns_admin(self, domain=None, domain_dict=None): + """ + Read Domain from PowerDNS and add into PDNS-Admin + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + if not domain: + try: + domain = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format( + domain_dict['name'])), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout'))) + except Exception as e: + current_app.logger.error('Can not read Domain from PDNS') + current_app.logger.error(e) + current_app.logger.debug(traceback.format_exc()) + + if 'account' in domain: + account_id = Account().get_id_by_name(domain['account']) + else: + current_app.logger.debug( + "No 'account' data found in API result - Unsupported PowerDNS version?" + ) + account_id = None + # add new domain + d = Domain() + d.name = domain['name'].rstrip('.') + d.master = str(domain['masters']) + d.type = domain['kind'] + d.serial = domain['serial'] + d.notified_serial = domain['notified_serial'] + d.last_check = domain['last_check'] + d.dnssec = 1 if domain['dnssec'] else 0 + d.account_id = account_id + db.session.add(d) + try: + db.session.commit() + current_app.logger.info( + "Synched PowerDNS Domain to PDNS-Admin: {0}".format(d.name)) + return { + 'status': 'ok', + 'msg': 'Added Domain successfully to PowerDNS-Admin' + } + except Exception as e: + db.session.rollback() + current_app.logger.info("Rolledback Domain {0}".format(d.name)) + raise + + def update_soa_setting(self, domain_name, soa_edit_api): + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + return {'status': 'error', 'msg': 'Domain doesnt exist.'} + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]: + soa_edit_api = 'DEFAULT' + + elif soa_edit_api == 'OFF': + soa_edit_api = '' + + post_data = {"soa_edit_api": soa_edit_api, "kind": domain.type} + + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain.name)), + headers=headers, + timeout=int( + Setting().get('pdns_api_timeout')), + method='PUT', + data=post_data) + if 'error' in jdata.keys(): + current_app.logger.error(jdata['error']) + return {'status': 'error', 'msg': jdata['error']} + else: + current_app.logger.info( + 'soa-edit-api changed for domain {0} successfully'.format( + domain_name)) + return { + 'status': 'ok', + 'msg': 'soa-edit-api changed successfully' + } + except Exception as e: + current_app.logger.debug(e) + current_app.logger.debug(traceback.format_exc()) + current_app.logger.error( + 'Cannot change soa-edit-api for domain {0}'.format( + domain_name)) + return { + 'status': 'error', + 'msg': 'Cannot change soa-edit-api 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().get('auto_ptr') + 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', 'DEFAULT', '', '') + self.update() + if result['status'] == 'ok': + history = History(msg='Add reverse lookup domain {0}'.format( + 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() + if len(domain_user_ids) > 0: + self.name = domain_reverse_name + self.grant_privileges(domain_user_ids) + return { + 'status': + 'ok', + 'msg': + 'New reverse lookup domain created with granted privileges' + } + 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.+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.+6.arpa)\.?)', + reverse_host_address).group('ipname') + else: + for i in range(1, 4, 1): + address = re.search( + '((([0-9]+\.){' + str(i) + '})(?P.+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.+r.arpa)\.?)', + reverse_host_address).group('ipname') + + def delete(self, domain_name): + """ + Delete a single domain name from powerdns + """ + try: + self.delete_domain_from_powerdns(domain_name) + self.delete_domain_from_pdnsadmin(domain_name) + return {'status': 'ok', 'msg': 'Delete domain successfully'} + except Exception as e: + current_app.logger.error( + 'Cannot delete domain {0}'.format(domain_name)) + current_app.logger.error(e) + current_app.logger.debug(traceback.format_exc()) + return {'status': 'error', 'msg': 'Cannot delete domain'} + + def delete_domain_from_powerdns(self, domain_name): + """ + Delete a single domain name from powerdns + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain_name)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='DELETE') + current_app.logger.info( + 'Deleted domain successfully from PowerDNS-Entity: {0}'.format( + domain_name)) + return {'status': 'ok', 'msg': 'Delete domain successfully'} + + def delete_domain_from_pdnsadmin(self, domain_name): + # Revoke permission before deleting domain + domain = Domain.query.filter(Domain.name == domain_name).first() + domain_user = DomainUser.query.filter( + DomainUser.domain_id == domain.id) + if domain_user: + domain_user.delete() + db.session.commit() + domain_setting = DomainSetting.query.filter( + DomainSetting.domain_id == domain.id) + if domain_setting: + domain_setting.delete() + db.session.commit() + domain.apikeys[:] = [] + db.session.commit() + + # then remove domain + Domain.query.filter(Domain.name == domain_name).delete() + db.session.commit() + current_app.logger.info( + "Deleted Domain successfully from pdnsADMIN: {}".format( + domain_name)) + + def get_user(self): + """ + Get users (id) who have access to this domain name + """ + user_ids = [] + query = db.session.query( + DomainUser, Domain).filter(User.id == DomainUser.user_id).filter( + Domain.id == DomainUser.domain_id).filter( + Domain.name == self.name).all() + for q in query: + user_ids.append(q[0].user_id) + return user_ids + + def grant_privileges(self, new_user_ids): + """ + Reconfigure domain_user table + """ + + domain_id = self.get_id_by_name(self.name) + domain_user_ids = self.get_user() + + removed_ids = list(set(domain_user_ids).difference(new_user_ids)) + added_ids = list(set(new_user_ids).difference(domain_user_ids)) + + try: + for uid in removed_ids: + DomainUser.query.filter(DomainUser.user_id == uid).filter( + DomainUser.domain_id == domain_id).delete() + db.session.commit() + except Exception as e: + db.session.rollback() + current_app.logger.error( + 'Cannot revoke user privileges on domain {0}. DETAIL: {1}'. + format(self.name, e)) + + try: + for uid in added_ids: + du = DomainUser(domain_id, uid) + db.session.add(du) + db.session.commit() + except Exception as e: + db.session.rollback() + print(traceback.format_exc()) + current_app.logger.error( + 'Cannot grant user privileges to domain {0}. DETAIL: {1}'. + format(self.name, e)) + + def update_from_master(self, domain_name): + """ + Update records from Master DNS server + """ + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + try: + utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}/axfr-retrieve'.format( + domain.name)), + headers=headers, + timeout=int( + Setting().get('pdns_api_timeout')), + method='PUT') + return { + 'status': 'ok', + 'msg': 'Update from Master successfully' + } + except Exception as e: + current_app.logger.error( + 'Cannot update from master. DETAIL: {0}'.format(e)) + return { + 'status': + 'error', + 'msg': + 'There was something wrong, please contact administrator' + } + else: + return {'status': 'error', 'msg': 'This domain doesnot exist'} + + def get_domain_dnssec(self, domain_name): + """ + Get domain DNSSEC information + """ + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + try: + jdata = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}/cryptokeys'.format( + domain.name)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='GET') + if 'error' in jdata: + return { + 'status': 'error', + 'msg': 'DNSSEC is not enabled for this domain' + } + else: + return {'status': 'ok', 'dnssec': jdata} + except Exception as e: + current_app.logger.error( + 'Cannot get domain dnssec. DETAIL: {0}'.format(e)) + return { + 'status': + 'error', + 'msg': + 'There was something wrong, please contact administrator' + } + else: + return {'status': 'error', 'msg': 'This domain doesnot exist'} + + def enable_domain_dnssec(self, domain_name): + """ + Enable domain DNSSEC + """ + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + try: + # Enable API-RECTIFY for domain, BEFORE activating DNSSEC + post_data = {"api_rectify": True} + jdata = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain.name)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='PUT', + data=post_data) + if 'error' in jdata: + return { + 'status': 'error', + 'msg': + 'API-RECTIFY could not be enabled for this domain', + 'jdata': jdata + } + + # Activate DNSSEC + post_data = {"keytype": "ksk", "active": True} + jdata = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}/cryptokeys'.format( + domain.name)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='POST', + data=post_data) + if 'error' in jdata: + return { + 'status': + 'error', + 'msg': + 'Cannot enable DNSSEC for this domain. Error: {0}'. + format(jdata['error']), + 'jdata': + jdata + } + + return {'status': 'ok'} + + except Exception as e: + current_app.logger.error( + 'Cannot enable dns sec. DETAIL: {}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return { + 'status': + 'error', + 'msg': + 'There was something wrong, please contact administrator' + } + + else: + return {'status': 'error', 'msg': 'This domain does not exist'} + + def delete_dnssec_key(self, domain_name, key_id): + """ + Remove keys DNSSEC + """ + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + try: + # Deactivate DNSSEC + jdata = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}/cryptokeys/{1}'.format( + domain.name, key_id)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='DELETE') + if jdata != True: + return { + 'status': + 'error', + 'msg': + 'Cannot disable DNSSEC for this domain. Error: {0}'. + format(jdata['error']), + 'jdata': + jdata + } + + # Disable API-RECTIFY for domain, AFTER deactivating DNSSEC + post_data = {"api_rectify": False} + jdata = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain.name)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='PUT', + data=post_data) + if 'error' in jdata: + return { + 'status': 'error', + 'msg': + 'API-RECTIFY could not be disabled for this domain', + 'jdata': jdata + } + + return {'status': 'ok'} + + except Exception as e: + current_app.logger.error( + 'Cannot delete dnssec key. DETAIL: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return { + 'status': 'error', + 'msg': + 'There was something wrong, please contact administrator', + 'domain': domain.name, + 'id': key_id + } + + else: + return {'status': 'error', 'msg': 'This domain doesnot exist'} + + def assoc_account(self, account_id): + """ + Associate domain with a domain, specified by account id + """ + domain_name = self.name + + # Sanity check - domain name + if domain_name == "": + return {'status': False, 'msg': 'No domain name specified'} + + # read domain and check that it exists + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + return {'status': False, 'msg': 'Domain does not exist'} + + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + account_name = Account().get_name_by_id(account_id) + + post_data = {"account": account_name} + + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain_name)), + headers=headers, + timeout=int( + Setting().get('pdns_api_timeout')), + method='PUT', + data=post_data) + + if 'error' in jdata.keys(): + current_app.logger.error(jdata['error']) + return {'status': 'error', 'msg': jdata['error']} + else: + self.update() + msg_str = 'Account changed for domain {0} successfully' + current_app.logger.info(msg_str.format(domain_name)) + return {'status': 'ok', 'msg': 'account changed successfully'} + + except Exception as e: + current_app.logger.debug(e) + current_app.logger.debug(traceback.format_exc()) + msg_str = 'Cannot change account for domain {0}' + current_app.logger.error(msg_str.format(domain_name)) + return { + 'status': 'error', + 'msg': 'Cannot change account for this domain.' + } + + def get_account(self): + """ + Get current account associated with this domain + """ + domain = Domain.query.filter(Domain.name == self.name).first() + + return domain.account + + def is_valid_access(self, user_id): + """ + Check if the user is allowed to access this + domain name + """ + return db.session.query(Domain) \ + .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ + .outerjoin(Account, Domain.account_id == Account.id) \ + .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ + .filter( + db.or_( + DomainUser.user_id == user_id, + AccountUser.user_id == user_id + )).filter(Domain.id == self.id).first() diff --git a/powerdnsadmin/models/domain_setting.py b/powerdnsadmin/models/domain_setting.py new file mode 100644 index 0000000..56dd6f1 --- /dev/null +++ b/powerdnsadmin/models/domain_setting.py @@ -0,0 +1,33 @@ +from .base import db + + +class DomainSetting(db.Model): + __tablename__ = 'domain_setting' + id = db.Column(db.Integer, primary_key=True) + domain_id = db.Column(db.Integer, db.ForeignKey('domain.id')) + domain = db.relationship('Domain', back_populates='settings') + setting = db.Column(db.String(255), nullable=False) + value = db.Column(db.String(255)) + + def __init__(self, id=None, setting=None, value=None): + self.id = id + self.setting = setting + self.value = value + + def __repr__(self): + return ''.format(setting, self.domain.name) + + def __eq__(self, other): + return type(self) == type(other) and self.setting == other.setting + + def set(self, value): + try: + self.value = value + db.session.commit() + return True + except Exception as e: + logging.error( + 'Unable to set DomainSetting value. DETAIL: {0}'.format(e)) + logging.debug(traceback.format_exc()) + db.session.rollback() + return False \ No newline at end of file diff --git a/powerdnsadmin/models/domain_template.py b/powerdnsadmin/models/domain_template.py new file mode 100644 index 0000000..1b3c6ff --- /dev/null +++ b/powerdnsadmin/models/domain_template.py @@ -0,0 +1,65 @@ +from flask import current_app +from .base import db + + +class DomainTemplate(db.Model): + __tablename__ = "domain_template" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255), index=True, unique=True) + description = db.Column(db.String(255)) + records = db.relationship('DomainTemplateRecord', + back_populates='template', + cascade="all, delete-orphan") + + def __repr__(self): + return ''.format(self.name) + + def __init__(self, name=None, description=None): + self.id = None + self.name = name + self.description = description + + def replace_records(self, records): + try: + self.records = [] + for record in records: + self.records.append(record) + db.session.commit() + return { + 'status': 'ok', + 'msg': 'Template records have been modified' + } + except Exception as e: + current_app.logger.error( + 'Cannot create template records Error: {0}'.format(e)) + db.session.rollback() + return { + 'status': 'error', + 'msg': 'Can not create template records' + } + + def create(self): + try: + db.session.add(self) + db.session.commit() + return {'status': 'ok', 'msg': 'Template has been created'} + except Exception as e: + current_app.logger.error( + 'Can not update domain template table. Error: {0}'.format(e)) + db.session.rollback() + return { + 'status': 'error', + 'msg': 'Can not update domain template table' + } + + def delete_template(self): + try: + self.records = [] + db.session.delete(self) + db.session.commit() + return {'status': 'ok', 'msg': 'Template has been deleted'} + except Exception as e: + current_app.logger.error( + 'Can not delete domain template. Error: {0}'.format(e)) + db.session.rollback() + return {'status': 'error', 'msg': 'Can not delete domain template'} \ No newline at end of file diff --git a/powerdnsadmin/models/domain_template_record.py b/powerdnsadmin/models/domain_template_record.py new file mode 100644 index 0000000..993c5ed --- /dev/null +++ b/powerdnsadmin/models/domain_template_record.py @@ -0,0 +1,42 @@ +from .base import db + + +class DomainTemplateRecord(db.Model): + __tablename__ = "domain_template_record" + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(255)) + type = db.Column(db.String(64)) + ttl = db.Column(db.Integer) + data = db.Column(db.Text) + status = db.Column(db.Boolean) + template_id = db.Column(db.Integer, db.ForeignKey('domain_template.id')) + template = db.relationship('DomainTemplate', back_populates='records') + + def __repr__(self): + return ''.format(self.id) + + def __init__(self, + id=None, + name=None, + type=None, + ttl=None, + data=None, + status=None): + self.id = id + self.name = name + self.type = type + self.ttl = ttl + self.data = data + self.status = status + + def apply(self): + try: + db.session.commit() + except Exception as e: + logging.error( + 'Can not update domain template table. Error: {0}'.format(e)) + db.session.rollback() + return { + 'status': 'error', + 'msg': 'Can not update domain template table' + } diff --git a/powerdnsadmin/models/domain_user.py b/powerdnsadmin/models/domain_user.py new file mode 100644 index 0000000..e556339 --- /dev/null +++ b/powerdnsadmin/models/domain_user.py @@ -0,0 +1,17 @@ +from .base import db + + +class DomainUser(db.Model): + __tablename__ = 'domain_user' + id = db.Column(db.Integer, primary_key=True) + domain_id = db.Column(db.Integer, + db.ForeignKey('domain.id'), + nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + + def __init__(self, domain_id, user_id): + self.domain_id = domain_id + self.user_id = user_id + + def __repr__(self): + return ''.format(self.domain_id, self.user_id) \ No newline at end of file diff --git a/powerdnsadmin/models/history.py b/powerdnsadmin/models/history.py new file mode 100644 index 0000000..70ef43a --- /dev/null +++ b/powerdnsadmin/models/history.py @@ -0,0 +1,48 @@ +from flask import current_app +from datetime import datetime + +from .base import db + + +class History(db.Model): + id = db.Column(db.Integer, primary_key=True) + msg = db.Column(db.String(256)) + # detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql')) + detail = db.Column(db.Text()) + created_by = db.Column(db.String(128)) + created_on = db.Column(db.DateTime, default=datetime.utcnow) + + def __init__(self, id=None, msg=None, detail=None, created_by=None): + self.id = id + self.msg = msg + self.detail = detail + self.created_by = created_by + + def __repr__(self): + return ''.format(self.msg) + + def add(self): + """ + Add an event to history table + """ + h = History() + h.msg = self.msg + h.detail = self.detail + h.created_by = self.created_by + db.session.add(h) + db.session.commit() + + def remove_all(self): + """ + Remove all history from DB + """ + try: + db.session.query(History).delete() + db.session.commit() + current_app.logger.info("Removed all history") + return True + except Exception as e: + db.session.rollback() + current_app.logger.error("Cannot remove history. DETAIL: {0}".format(e)) + current_app.logger.debug(traceback.format_exc()) + return False diff --git a/powerdnsadmin/models/record.py b/powerdnsadmin/models/record.py new file mode 100644 index 0000000..5723df2 --- /dev/null +++ b/powerdnsadmin/models/record.py @@ -0,0 +1,607 @@ +import traceback +import itertools +import dns.reversename +import dns.inet +import dns.name +from distutils.version import StrictVersion +from flask import current_app +from urllib.parse import urljoin +from distutils.util import strtobool + +from .. import utils +from .base import db +from .setting import Setting +from .domain import Domain +from .domain_setting import DomainSetting + + +class Record(object): + """ + This is not a model, it's just an object + which be assigned data from PowerDNS API + """ + def __init__(self, name=None, type=None, status=None, ttl=None, data=None): + self.name = name + self.type = type + self.status = status + self.ttl = ttl + self.data = data + # PDNS configs + self.PDNS_STATS_URL = Setting().get('pdns_api_url') + self.PDNS_API_KEY = Setting().get('pdns_api_key') + self.PDNS_VERSION = Setting().get('pdns_version') + self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) + self.PRETTY_IPV6_PTR = Setting().get('pretty_ipv6_ptr') + + if StrictVersion(self.PDNS_VERSION) >= StrictVersion('4.0.0'): + self.NEW_SCHEMA = True + else: + self.NEW_SCHEMA = False + + def get_record_data(self, domain): + """ + Query domain's DNS records via API + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + timeout=int(Setting().get('pdns_api_timeout')), + headers=headers) + except Exception as e: + current_app.logger.error( + "Cannot fetch domain's record data from remote powerdns api. DETAIL: {0}" + .format(e)) + return False + + if self.NEW_SCHEMA: + rrsets = jdata['rrsets'] + for rrset in rrsets: + r_name = rrset['name'].rstrip('.') + if self.PRETTY_IPV6_PTR: # only if activated + if rrset['type'] == 'PTR': # only ptr + if 'ip6.arpa' in r_name: # only if v6-ptr + r_name = dns.reversename.to_address( + dns.name.from_text(r_name)) + + rrset['name'] = r_name + rrset['content'] = rrset['records'][0]['content'] + rrset['disabled'] = rrset['records'][0]['disabled'] + return {'records': rrsets} + + return jdata + + def add(self, domain): + """ + Add a record to domain + """ + # validate record first + r = self.get_record_data(domain) + records = r['records'] + check = list(filter(lambda check: check['name'] == self.name, records)) + if check: + r = check[0] + if r['type'] in ('A', 'AAAA', 'CNAME'): + return { + 'status': 'error', + 'msg': + 'Record already exists with type "A", "AAAA" or "CNAME"' + } + + # continue if the record is ready to be added + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + if self.NEW_SCHEMA: + data = { + "rrsets": [{ + "name": + self.name.rstrip('.') + '.', + "type": + self.type, + "changetype": + "REPLACE", + "ttl": + self.ttl, + "records": [{ + "content": self.data, + "disabled": self.status, + }] + }] + } + else: + data = { + "rrsets": [{ + "name": + self.name, + "type": + self.type, + "changetype": + "REPLACE", + "records": [{ + "content": self.data, + "disabled": self.status, + "name": self.name, + "ttl": self.ttl, + "type": self.type + }] + }] + } + + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='PATCH', + data=data) + current_app.logger.debug(jdata) + return {'status': 'ok', 'msg': 'Record was added successfully'} + except Exception as e: + current_app.logger.error( + "Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}". + format(self.name, self.type, self.data, domain, e)) + return { + 'status': 'error', + 'msg': + 'There was something wrong, please contact administrator' + } + + def compare(self, domain_name, new_records): + """ + Compare new records with current powerdns record data + Input is a list of hashes (records) + """ + # get list of current records we have in powerdns + current_records = self.get_record_data(domain_name)['records'] + + # convert them to list of list (just has [name, type]) instead of list of hash + # to compare easier + list_current_records = [[x['name'], x['type']] + for x in current_records] + list_new_records = [[x['name'], x['type']] for x in new_records] + + # get list of deleted records + # they are the records which exist in list_current_records but not in list_new_records + list_deleted_records = [ + x for x in list_current_records if x not in list_new_records + ] + + # convert back to list of hash + deleted_records = [ + x for x in current_records + if [x['name'], x['type']] in list_deleted_records and ( + x['type'] in Setting().get_records_allow_to_edit() + and x['type'] != 'SOA') + ] + + # return a tuple + return deleted_records, new_records + + def apply(self, domain, post_records): + """ + Apply record changes to domain + """ + records = [] + for r in post_records: + r_name = domain if r['record_name'] in [ + '@', '' + ] else r['record_name'] + '.' + domain + r_type = r['record_type'] + if self.PRETTY_IPV6_PTR: # only if activated + if self.NEW_SCHEMA: # only if new schema + if r_type == 'PTR': # only ptr + if ':' in r['record_name']: # dirty ipv6 check + r_name = r['record_name'] + + r_data = domain if r_type == 'CNAME' and r['record_data'] in [ + '@', '' + ] else r['record_data'] + + record = { + "name": r_name, + "type": r_type, + "content": r_data, + "disabled": + True if r['record_status'] == 'Disabled' else False, + "ttl": int(r['record_ttl']) if r['record_ttl'] else 3600, + } + records.append(record) + + deleted_records, new_records = self.compare(domain, records) + + records = [] + for r in deleted_records: + r_name = r['name'].rstrip( + '.') + '.' if self.NEW_SCHEMA else r['name'] + r_type = r['type'] + if self.PRETTY_IPV6_PTR: # only if activated + if self.NEW_SCHEMA: # only if new schema + if r_type == 'PTR': # only ptr + if ':' in r['name']: # dirty ipv6 check + r_name = dns.reversename.from_address( + r['name']).to_text() + + record = { + "name": r_name, + "type": r_type, + "changetype": "DELETE", + "records": [] + } + records.append(record) + + postdata_for_delete = {"rrsets": records} + + records = [] + for r in new_records: + if self.NEW_SCHEMA: + r_name = r['name'].rstrip('.') + '.' + r_type = r['type'] + if self.PRETTY_IPV6_PTR: # only if activated + if r_type == 'PTR': # only ptr + if ':' in r['name']: # dirty ipv6 check + r_name = r['name'] + + record = { + "name": + r_name, + "type": + r_type, + "changetype": + "REPLACE", + "ttl": + r['ttl'], + "records": [{ + "content": r['content'], + "disabled": r['disabled'], + }] + } + else: + record = { + "name": + r['name'], + "type": + r['type'], + "changetype": + "REPLACE", + "records": [{ + "content": r['content'], + "disabled": r['disabled'], + "name": r['name'], + "ttl": r['ttl'], + "type": r['type'], + "priority": + 10, # priority field for pdns 3.4.1. https://doc.powerdns.com/md/authoritative/upgrading/ + }] + } + + records.append(record) + + # Adjustment to add multiple records which described in + # https://github.com/ngoduykhanh/PowerDNS-Admin/issues/5#issuecomment-181637576 + final_records = [] + records = sorted(records, + key=lambda item: + (item["name"], item["type"], item["changetype"])) + for key, group in itertools.groupby( + records, lambda item: + (item["name"], item["type"], item["changetype"])): + if self.NEW_SCHEMA: + r_name = key[0] + r_type = key[1] + r_changetype = key[2] + + if self.PRETTY_IPV6_PTR: # only if activated + if r_type == 'PTR': # only ptr + if ':' in r_name: # dirty ipv6 check + r_name = dns.reversename.from_address( + r_name).to_text() + + new_record = { + "name": r_name, + "type": r_type, + "changetype": r_changetype, + "ttl": None, + "records": [] + } + for item in group: + temp_content = item['records'][0]['content'] + temp_disabled = item['records'][0]['disabled'] + if key[1] in ['MX', 'CNAME', 'SRV', 'NS']: + if temp_content.strip()[-1:] != '.': + temp_content += '.' + + if new_record['ttl'] is None: + new_record['ttl'] = item['ttl'] + new_record['records'].append({ + "content": temp_content, + "disabled": temp_disabled + }) + final_records.append(new_record) + + else: + + final_records.append({ + "name": + key[0], + "type": + key[1], + "changetype": + key[2], + "records": [{ + "content": item['records'][0]['content'], + "disabled": item['records'][0]['disabled'], + "name": key[0], + "ttl": item['records'][0]['ttl'], + "type": key[1], + "priority": 10, + } for item in group] + }) + + postdata_for_new = {"rrsets": final_records} + current_app.logger.debug(postdata_for_new) + current_app.logger.debug(postdata_for_delete) + current_app.logger.info( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain))) + try: + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + jdata1 = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + headers=headers, + method='PATCH', + data=postdata_for_delete) + jdata2 = utils.fetch_json( + urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='PATCH', + data=postdata_for_new) + + if 'error' in jdata1.keys(): + current_app.logger.error('Cannot apply record changes.') + current_app.logger.debug(jdata1['error']) + return {'status': 'error', 'msg': jdata1['error']} + elif 'error' in jdata2.keys(): + current_app.logger.error('Cannot apply record changes.') + current_app.logger.debug(jdata2['error']) + return {'status': 'error', 'msg': jdata2['error']} + else: + self.auto_ptr(domain, new_records, deleted_records) + self.update_db_serial(domain) + current_app.logger.info('Record was applied successfully.') + return { + 'status': 'ok', + 'msg': 'Record was applied successfully' + } + except Exception as e: + current_app.logger.error( + "Cannot apply record changes to domain {0}. Error: {1}".format( + domain, e)) + current_app.logger.debug(traceback.format_exc()) + 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().get('auto_ptr') + + 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_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: + current_app.logger.error( + "Cannot update auto-ptr record changes to domain {0}. Error: {1}" + .format(domain, e)) + current_app.logger.debug(traceback.format_exc()) + return { + 'status': + 'error', + 'msg': + 'Auto-PTR creation failed. There was something wrong, please contact administrator.' + } + + def delete(self, domain): + """ + Delete a record from domain + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + data = { + "rrsets": [{ + "name": self.name.rstrip('.') + '.', + "type": self.type, + "changetype": "DELETE", + "records": [] + }] + } + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='PATCH', + data=data) + current_app.logger.debug(jdata) + return {'status': 'ok', 'msg': 'Record was removed successfully'} + except Exception as e: + current_app.logger.error( + "Cannot remove record {0}/{1}/{2} from domain {3}. DETAIL: {4}" + .format(self.name, self.type, self.data, domain, e)) + return { + 'status': 'error', + 'msg': + 'There was something wrong, please contact administrator' + } + + def is_allowed_edit(self): + """ + Check if record is allowed to edit + """ + return self.type in Setting().get_records_allow_to_edit() + + def is_allowed_delete(self): + """ + Check if record is allowed to removed + """ + return (self.type in Setting().get_records_allow_to_edit() + and self.type != 'SOA') + + def exists(self, domain): + """ + Check if record is present within domain records, and if it's present set self to found record + """ + jdata = self.get_record_data(domain) + jrecords = jdata['records'] + + for jr in jrecords: + if jr['name'] == self.name and jr['type'] == self.type: + self.name = jr['name'] + self.type = jr['type'] + self.status = jr['disabled'] + self.ttl = jr['ttl'] + self.data = jr['content'] + self.priority = 10 + return True + return False + + def update(self, domain, content): + """ + Update single record + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + if self.NEW_SCHEMA: + data = { + "rrsets": [{ + "name": + self.name + '.', + "type": + self.type, + "ttl": + self.ttl, + "changetype": + "REPLACE", + "records": [{ + "content": content, + "disabled": self.status, + }] + }] + } + else: + data = { + "rrsets": [{ + "name": + self.name, + "type": + self.type, + "changetype": + "REPLACE", + "records": [{ + "content": content, + "disabled": self.status, + "name": self.name, + "ttl": self.ttl, + "type": self.type, + "priority": 10 + }] + }] + } + try: + utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='PATCH', + data=data) + current_app.logger.debug("dyndns data: {0}".format(data)) + return {'status': 'ok', 'msg': 'Record was updated successfully'} + except Exception as e: + current_app.logger.error( + "Cannot add record {0}/{1}/{2} to domain {3}. DETAIL: {4}". + format(self.name, self.type, self.data, domain, e)) + return { + 'status': 'error', + 'msg': + 'There was something wrong, please contact administrator' + } + + def update_db_serial(self, domain): + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/localhost/zones/{0}'.format(domain)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='GET') + serial = jdata['serial'] + + domain = Domain.query.filter(Domain.name == domain).first() + if domain: + domain.serial = serial + db.session.commit() + return { + 'status': True, + 'msg': 'Synced local serial for domain name {0}'.format(domain) + } + else: + return { + 'status': False, + 'msg': + 'Could not find domain name {0} in local db'.format(domain) + } diff --git a/powerdnsadmin/models/record_entry.py b/powerdnsadmin/models/record_entry.py new file mode 100644 index 0000000..e129dc1 --- /dev/null +++ b/powerdnsadmin/models/record_entry.py @@ -0,0 +1,25 @@ +class RecordEntry(object): + """ + This is not a model, it's just an object + which will store records entries from PowerDNS API + """ + def __init__(self, + name=None, + type=None, + status=None, + ttl=None, + data=None, + is_allowed_edit=False): + self.name = name + self.type = type + self.status = status + self.ttl = ttl + self.data = data + self._is_allowed_edit = is_allowed_edit + self._is_allowed_delete = is_allowed_edit and self.type != 'SOA' + + def is_allowed_edit(self): + return self._is_allowed_edit + + def is_allowed_delete(self): + return self._is_allowed_delete \ No newline at end of file diff --git a/powerdnsadmin/models/role.py b/powerdnsadmin/models/role.py new file mode 100644 index 0000000..8833cbd --- /dev/null +++ b/powerdnsadmin/models/role.py @@ -0,0 +1,23 @@ +from .base import db + + +class Role(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64), index=True, unique=True) + description = db.Column(db.String(128)) + users = db.relationship('User', backref='role', lazy=True) + apikeys = db.relationship('ApiKey', back_populates='role', lazy=True) + + def __init__(self, id=None, name=None, description=None): + self.id = id + self.name = name + self.description = description + + # allow database autoincrement to do its own ID assignments + def __init__(self, name=None, description=None): + self.id = None + self.name = name + self.description = description + + def __repr__(self): + return ''.format(self.name) \ No newline at end of file diff --git a/powerdnsadmin/models/server.py b/powerdnsadmin/models/server.py new file mode 100644 index 0000000..7e32d33 --- /dev/null +++ b/powerdnsadmin/models/server.py @@ -0,0 +1,64 @@ +import traceback +from flask import current_app +from urllib.parse import urljoin + +from ..lib import utils +from .base import db +from .setting import Setting + + +class Server(object): + """ + This is not a model, it's just an object + which be assigned data from PowerDNS API + """ + def __init__(self, server_id=None, server_config=None): + self.server_id = server_id + self.server_config = server_config + # PDNS configs + self.PDNS_STATS_URL = Setting().get('pdns_api_url') + self.PDNS_API_KEY = Setting().get('pdns_api_key') + self.PDNS_VERSION = Setting().get('pdns_version') + self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION) + + def get_config(self): + """ + Get server config + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/{0}/config'.format(self.server_id)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='GET') + return jdata + except Exception as e: + current_app.logger.error( + "Can not get server configuration. DETAIL: {0}".format(e)) + current_app.logger.debug(traceback.format_exc()) + return [] + + def get_statistic(self): + """ + Get server statistics + """ + headers = {} + headers['X-API-Key'] = self.PDNS_API_KEY + + try: + jdata = utils.fetch_json(urljoin( + self.PDNS_STATS_URL, self.API_EXTENDED_URL + + '/servers/{0}/statistics'.format(self.server_id)), + headers=headers, + timeout=int(Setting().get('pdns_api_timeout')), + method='GET') + return jdata + except Exception as e: + current_app.logger.error( + "Can not get server statistics. DETAIL: {0}".format(e)) + current_app.logger.debug(traceback.format_exc()) + return [] diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py new file mode 100644 index 0000000..f28e3b0 --- /dev/null +++ b/powerdnsadmin/models/setting.py @@ -0,0 +1,268 @@ +import sys +import pytimeparse +from ast import literal_eval +from distutils.util import strtobool +from distutils.version import StrictVersion + +from .base import db + + +class Setting(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(64)) + value = db.Column(db.Text()) + + defaults = { + 'maintenance': False, + 'fullscreen_layout': True, + 'record_helper': True, + 'login_ldap_first': True, + 'default_record_table_size': 15, + 'default_domain_table_size': 10, + 'auto_ptr': False, + 'record_quick_edit': True, + 'pretty_ipv6_ptr': False, + 'dnssec_admins_only': False, + 'allow_user_create_domain': False, + 'bg_domain_updates': False, + 'site_name': 'PowerDNS-Admin', + 'session_timeout': 10, + 'pdns_api_url': '', + 'pdns_api_key': '', + 'pdns_api_timeout': 30, + 'pdns_version': '4.1.1', + 'local_db_enabled': True, + 'signup_enabled': True, + 'ldap_enabled': False, + 'ldap_type': 'ldap', + 'ldap_uri': '', + 'ldap_base_dn': '', + 'ldap_admin_username': '', + 'ldap_admin_password': '', + 'ldap_filter_basic': '', + 'ldap_filter_username': '', + 'ldap_sg_enabled': False, + 'ldap_admin_group': '', + 'ldap_operator_group': '', + 'ldap_user_group': '', + 'ldap_domain': '', + 'github_oauth_enabled': False, + 'github_oauth_key': '', + 'github_oauth_secret': '', + 'github_oauth_scope': 'email', + 'github_oauth_api_url': 'https://api.github.com/user', + 'github_oauth_token_url': + 'https://github.com/login/oauth/access_token', + 'github_oauth_authorize_url': + 'https://github.com/login/oauth/authorize', + 'google_oauth_enabled': False, + 'google_oauth_client_id': '', + 'google_oauth_client_secret': '', + 'google_token_url': 'https://oauth2.googleapis.com/token', + 'google_oauth_scope': 'openid email profile', + 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', + 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', + 'oidc_oauth_enabled': False, + 'oidc_oauth_key': '', + 'oidc_oauth_secret': '', + 'oidc_oauth_scope': 'email', + 'oidc_oauth_api_url': '', + 'oidc_oauth_token_url': '', + 'oidc_oauth_authorize_url': '', + 'forward_records_allow_edit': { + 'A': True, + 'AAAA': True, + 'AFSDB': False, + 'ALIAS': False, + 'CAA': True, + 'CERT': False, + 'CDNSKEY': False, + 'CDS': False, + 'CNAME': True, + 'DNSKEY': False, + 'DNAME': False, + 'DS': False, + 'HINFO': False, + 'KEY': False, + 'LOC': True, + 'LUA': False, + 'MX': True, + 'NAPTR': False, + 'NS': True, + 'NSEC': False, + 'NSEC3': False, + 'NSEC3PARAM': False, + 'OPENPGPKEY': False, + 'PTR': True, + 'RP': False, + 'RRSIG': False, + 'SOA': False, + 'SPF': True, + 'SSHFP': False, + 'SRV': True, + 'TKEY': False, + 'TSIG': False, + 'TLSA': False, + 'SMIMEA': False, + 'TXT': True, + 'URI': False + }, + 'reverse_records_allow_edit': { + 'A': False, + 'AAAA': False, + 'AFSDB': False, + 'ALIAS': False, + 'CAA': False, + 'CERT': False, + 'CDNSKEY': False, + 'CDS': False, + 'CNAME': False, + 'DNSKEY': False, + 'DNAME': False, + 'DS': False, + 'HINFO': False, + 'KEY': False, + 'LOC': True, + 'LUA': False, + 'MX': False, + 'NAPTR': False, + 'NS': True, + 'NSEC': False, + 'NSEC3': False, + 'NSEC3PARAM': False, + 'OPENPGPKEY': False, + 'PTR': True, + 'RP': False, + 'RRSIG': False, + 'SOA': False, + 'SPF': False, + 'SSHFP': False, + 'SRV': False, + 'TKEY': False, + 'TSIG': False, + 'TLSA': False, + 'SMIMEA': False, + 'TXT': True, + 'URI': False + }, + 'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours', + } + + def __init__(self, id=None, name=None, value=None): + self.id = id + self.name = name + self.value = value + + # allow database autoincrement to do its own ID assignments + def __init__(self, name=None, value=None): + self.id = None + self.name = name + self.value = value + + def set_maintenance(self, mode): + maintenance = Setting.query.filter( + Setting.name == 'maintenance').first() + + if maintenance is None: + value = self.defaults['maintenance'] + maintenance = Setting(name='maintenance', value=str(value)) + db.session.add(maintenance) + + mode = str(mode) + + try: + if maintenance.value != mode: + maintenance.value = mode + db.session.commit() + return True + except Exception as e: + logging.error('Cannot set maintenance to {0}. DETAIL: {1}'.format( + mode, e)) + logging.debug(traceback.format_exec()) + db.session.rollback() + return False + + def toggle(self, setting): + current_setting = Setting.query.filter(Setting.name == setting).first() + + if current_setting is None: + value = self.defaults[setting] + current_setting = Setting(name=setting, value=str(value)) + db.session.add(current_setting) + + try: + if current_setting.value == "True": + current_setting.value = "False" + else: + current_setting.value = "True" + db.session.commit() + return True + except Exception as e: + logging.error('Cannot toggle setting {0}. DETAIL: {1}'.format( + setting, e)) + logging.debug(traceback.format_exec()) + db.session.rollback() + return False + + def set(self, setting, value): + current_setting = Setting.query.filter(Setting.name == setting).first() + + if current_setting is None: + current_setting = Setting(name=setting, value=None) + db.session.add(current_setting) + + value = str(value) + + try: + current_setting.value = value + db.session.commit() + return True + except Exception as e: + logging.error('Cannot edit setting {0}. DETAIL: {1}'.format( + setting, e)) + logging.debug(traceback.format_exec()) + db.session.rollback() + return False + + def get(self, setting): + if setting in self.defaults: + result = self.query.filter(Setting.name == setting).first() + if result is not None: + return strtobool(result.value) if result.value in [ + 'True', 'False' + ] else result.value + else: + return self.defaults[setting] + else: + logging.error('Unknown setting queried: {0}'.format(setting)) + + def get_records_allow_to_edit(self): + return list( + set(self.get_forward_records_allow_to_edit() + + self.get_reverse_records_allow_to_edit())) + + def get_forward_records_allow_to_edit(self): + records = self.get('forward_records_allow_edit') + f_records = literal_eval(records) if isinstance(records, + str) else records + r_name = [r for r in f_records if f_records[r]] + # Sort alphabetically if python version is smaller than 3.6 + if sys.version_info[0] < 3 or (sys.version_info[0] == 3 + and sys.version_info[1] < 6): + r_name.sort() + return r_name + + def get_reverse_records_allow_to_edit(self): + records = self.get('reverse_records_allow_edit') + r_records = literal_eval(records) if isinstance(records, + str) else records + r_name = [r for r in r_records if r_records[r]] + # Sort alphabetically if python version is smaller than 3.6 + if sys.version_info[0] < 3 or (sys.version_info[0] == 3 + and sys.version_info[1] < 6): + r_name.sort() + return r_name + + def get_ttl_options(self): + return [(pytimeparse.parse(ttl), ttl) + for ttl in self.get('ttl_options').split(',')] diff --git a/powerdnsadmin/models/user.py b/powerdnsadmin/models/user.py new file mode 100644 index 0000000..864cc28 --- /dev/null +++ b/powerdnsadmin/models/user.py @@ -0,0 +1,581 @@ +import os +import base64 +import bcrypt +import ldap +import ldap.filter +from flask import current_app +from flask_login import AnonymousUserMixin + +from .base import db +from .role import Role +from .domain_user import DomainUser + + +class Anonymous(AnonymousUserMixin): + def __init__(self): + self.username = 'Anonymous' + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), index=True, unique=True) + password = db.Column(db.String(64)) + firstname = db.Column(db.String(64)) + lastname = db.Column(db.String(64)) + email = db.Column(db.String(128)) + otp_secret = db.Column(db.String(16)) + role_id = db.Column(db.Integer, db.ForeignKey('role.id')) + + def __init__(self, + id=None, + username=None, + password=None, + plain_text_password=None, + firstname=None, + lastname=None, + role_id=None, + email=None, + otp_secret=None, + reload_info=True): + self.id = id + self.username = username + self.password = password + self.plain_text_password = plain_text_password + self.firstname = firstname + self.lastname = lastname + self.role_id = role_id + self.email = email + self.otp_secret = otp_secret + + if reload_info: + user_info = self.get_user_info_by_id( + ) if id else self.get_user_info_by_username() + + if user_info: + self.id = user_info.id + self.username = user_info.username + self.firstname = user_info.firstname + self.lastname = user_info.lastname + self.email = user_info.email + self.role_id = user_info.role_id + self.otp_secret = user_info.otp_secret + + def is_authenticated(self): + return True + + def is_active(self): + return True + + def is_anonymous(self): + return False + + def get_id(self): + try: + return unicode(self.id) # python 2 + except NameError: + return str(self.id) # python 3 + + def __repr__(self): + return ''.format(self.username) + + def get_totp_uri(self): + return "otpauth://totp/PowerDNS-Admin:{0}?secret={1}&issuer=PowerDNS-Admin".format( + self.username, self.otp_secret) + + def verify_totp(self, token): + totp = pyotp.TOTP(self.otp_secret) + return totp.verify(token) + + def get_hashed_password(self, plain_text_password=None): + # Hash a password for the first time + # (Using bcrypt, the salt is saved into the hash itself) + if plain_text_password is None: + return plain_text_password + + pw = plain_text_password if plain_text_password else self.plain_text_password + return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt()) + + def check_password(self, hashed_password): + # Check hased password. Using bcrypt, the salt is saved into the hash itself + if (self.plain_text_password): + return bcrypt.checkpw(self.plain_text_password.encode('utf-8'), + hashed_password.encode('utf-8')) + return False + + def get_user_info_by_id(self): + user_info = User.query.get(int(self.id)) + return user_info + + def get_user_info_by_username(self): + user_info = User.query.filter(User.username == self.username).first() + return user_info + + def ldap_init_conn(self): + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + conn = ldap.initialize(Setting().get('ldap_uri')) + conn.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF) + conn.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + conn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) + conn.set_option(ldap.OPT_X_TLS_DEMAND, True) + conn.set_option(ldap.OPT_DEBUG_LEVEL, 255) + conn.protocol_version = ldap.VERSION3 + return conn + + def ldap_search(self, searchFilter, baseDN): + searchScope = ldap.SCOPE_SUBTREE + retrieveAttributes = None + + try: + conn = self.ldap_init_conn() + if Setting().get('ldap_type') == 'ad': + conn.simple_bind_s( + "{0}@{1}".format(self.username, + Setting().get('ldap_domain')), + self.password) + else: + conn.simple_bind_s(Setting().get('ldap_admin_username'), + Setting().get('ldap_admin_password')) + ldap_result_id = conn.search(baseDN, searchScope, searchFilter, + retrieveAttributes) + result_set = [] + + while 1: + result_type, result_data = conn.result(ldap_result_id, 0) + if (result_data == []): + break + else: + if result_type == ldap.RES_SEARCH_ENTRY: + result_set.append(result_data) + return result_set + + except ldap.LDAPError as e: + current_app.logger.error(e) + current_app.logger.debug('baseDN: {0}'.format(baseDN)) + current_app.logger.debug(traceback.format_exc()) + + def ldap_auth(self, ldap_username, password): + try: + conn = self.ldap_init_conn() + conn.simple_bind_s(ldap_username, password) + return True + except ldap.LDAPError as e: + current_app.logger.error(e) + return False + + def ad_recursive_groups(self, groupDN): + """ + Recursively list groups belonging to a group. It will allow checking deep in the Active Directory + whether a user is allowed to enter or not + """ + LDAP_BASE_DN = Setting().get('ldap_base_dn') + groupSearchFilter = "(&(objectcategory=group)(member=%s))" % ldap.filter.escape_filter_chars( + groupDN) + result = [groupDN] + try: + groups = self.ldap_search(groupSearchFilter, LDAP_BASE_DN) + for group in groups: + result += [group[0][0]] + if 'memberOf' in group[0][1]: + for member in group[0][1]['memberOf']: + result += self.ad_recursive_groups( + member.decode("utf-8")) + return result + except ldap.LDAPError as e: + current_app.logger.exception("Recursive AD Group search error") + return result + + def is_validate(self, method, src_ip=''): + """ + Validate user credential + """ + role_name = 'User' + + if method == 'LOCAL': + user_info = User.query.filter( + User.username == self.username).first() + + if user_info: + if user_info.password and self.check_password( + user_info.password): + current_app.logger.info( + 'User "{0}" logged in successfully. Authentication request from {1}' + .format(self.username, src_ip)) + return True + current_app.logger.error( + 'User "{0}" inputted a wrong password. Authentication request from {1}' + .format(self.username, src_ip)) + return False + + current_app.logger.warning( + 'User "{0}" does not exist. Authentication request from {1}'. + format(self.username, src_ip)) + return False + + if method == 'LDAP': + LDAP_TYPE = Setting().get('ldap_type') + LDAP_BASE_DN = Setting().get('ldap_base_dn') + LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic') + LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username') + LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group') + LDAP_OPERATOR_GROUP = Setting().get('ldap_operator_group') + LDAP_USER_GROUP = Setting().get('ldap_user_group') + LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled') + + # validate AD user password + if Setting().get('ldap_type') == 'ad': + ldap_username = "{0}@{1}".format(self.username, + Setting().get('ldap_domain')) + if not self.ldap_auth(ldap_username, self.password): + current_app.logger.error( + 'User "{0}" input a wrong LDAP password. Authentication request from {1}' + .format(self.username, src_ip)) + return False + + searchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_USERNAME, + self.username, + LDAP_FILTER_BASIC) + current_app.logger.debug('Ldap searchFilter {0}'.format(searchFilter)) + + ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN) + current_app.logger.debug('Ldap search result: {0}'.format(ldap_result)) + + if not ldap_result: + current_app.logger.warning( + 'LDAP User "{0}" does not exist. Authentication request from {1}' + .format(self.username, src_ip)) + return False + else: + try: + ldap_username = ldap.filter.escape_filter_chars( + ldap_result[0][0][0]) + + if Setting().get('ldap_type') != 'ad': + # validate ldap user password + if not self.ldap_auth(ldap_username, self.password): + current_app.logger.error( + 'User "{0}" input a wrong LDAP password. Authentication request from {1}' + .format(self.username, src_ip)) + return False + + # check if LDAP_GROUP_SECURITY_ENABLED is True + # user can be assigned to ADMIN or USER role. + if LDAP_GROUP_SECURITY_ENABLED: + try: + if LDAP_TYPE == 'ldap': + if (self.ldap_search(searchFilter, + LDAP_ADMIN_GROUP)): + role_name = 'Administrator' + current_app.logger.info( + 'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin' + .format(self.username, + LDAP_ADMIN_GROUP)) + elif (self.ldap_search(searchFilter, + LDAP_OPERATOR_GROUP)): + role_name = 'Operator' + current_app.logger.info( + 'User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin' + .format(self.username, + LDAP_OPERATOR_GROUP)) + elif (self.ldap_search(searchFilter, + LDAP_USER_GROUP)): + current_app.logger.info( + 'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin' + .format(self.username, + LDAP_USER_GROUP)) + else: + current_app.logger.error( + 'User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin' + .format(self.username, + LDAP_ADMIN_GROUP, + LDAP_OPERATOR_GROUP, + LDAP_USER_GROUP)) + return False + elif LDAP_TYPE == 'ad': + user_ldap_groups = [] + user_ad_member_of = ldap_result[0][0][1].get( + 'memberOf') + + if not user_ad_member_of: + current_app.logger.error( + 'User {0} does not belong to any group while LDAP_GROUP_SECURITY_ENABLED is ON' + .format(self.username)) + return False + + for group in [ + g.decode("utf-8") + for g in user_ad_member_of + ]: + user_ldap_groups += self.ad_recursive_groups( + group) + + if (LDAP_ADMIN_GROUP in user_ldap_groups): + role_name = 'Administrator' + current_app.logger.info( + 'User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin' + .format(self.username, + LDAP_ADMIN_GROUP)) + elif (LDAP_OPERATOR_GROUP in user_ldap_groups): + role_name = 'Operator' + current_app.logger.info( + 'User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin' + .format(self.username, + LDAP_OPERATOR_GROUP)) + elif (LDAP_USER_GROUP in user_ldap_groups): + current_app.logger.info( + 'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin' + .format(self.username, + LDAP_USER_GROUP)) + else: + current_app.logger.error( + 'User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin' + .format(self.username, + LDAP_ADMIN_GROUP, + LDAP_OPERATOR_GROUP, + LDAP_USER_GROUP)) + return False + else: + current_app.logger.error('Invalid LDAP type') + return False + except Exception as e: + current_app.logger.error( + 'LDAP group lookup for user "{0}" has failed. Authentication request from {1}' + .format(self.username, src_ip)) + current_app.logger.debug(traceback.format_exc()) + return False + + except Exception as e: + current_app.logger.error('Wrong LDAP configuration. {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return False + + # create user if not exist in the db + if not User.query.filter(User.username == self.username).first(): + self.firstname = self.username + self.lastname = '' + try: + # try to get user's firstname, lastname and email address from LDAP attributes + if LDAP_TYPE == 'ldap': + self.firstname = ldap_result[0][0][1]['givenName'][ + 0].decode("utf-8") + self.lastname = ldap_result[0][0][1]['sn'][0].decode( + "utf-8") + self.email = ldap_result[0][0][1]['mail'][0].decode( + "utf-8") + elif LDAP_TYPE == 'ad': + self.firstname = ldap_result[0][0][1]['name'][ + 0].decode("utf-8") + self.email = ldap_result[0][0][1]['userPrincipalName'][ + 0].decode("utf-8") + except Exception as e: + current_app.logger.warning( + "Reading ldap data threw an exception {0}".format(e)) + current_app.logger.debug(traceback.format_exc()) + + # first register user will be in Administrator role + if User.query.count() == 0: + self.role_id = Role.query.filter_by( + name='Administrator').first().id + else: + self.role_id = Role.query.filter_by( + name=role_name).first().id + + self.create_user() + current_app.logger.info('Created user "{0}" in the DB'.format( + self.username)) + + # user already exists in database, set their role based on group membership (if enabled) + if LDAP_GROUP_SECURITY_ENABLED: + self.set_role(role_name) + + return True + else: + current_app.logger.error('Unsupported authentication method') + return False + + # def get_apikeys(self, domain_name=None): + # info = [] + # apikey_query = db.session.query(ApiKey) \ + # .join(Domain.apikeys) \ + # .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ + # .outerjoin(Account, Domain.account_id == Account.id) \ + # .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ + # .filter( + # db.or_( + # DomainUser.user_id == User.id, + # AccountUser.user_id == User.id + # ) + # ) \ + # .filter(User.id == self.id) + + # if domain_name: + # info = apikey_query.filter(Domain.name == domain_name).all() + # else: + # info = apikey_query.all() + + # return info + + def create_user(self): + """ + If user logged in successfully via LDAP in the first time + We will create a local user (in DB) in order to manage user + profile such as name, roles,... + """ + + # Set an invalid password hash for non local users + self.password = '*' + + db.session.add(self) + db.session.commit() + + def create_local_user(self): + """ + Create local user witch stores username / password in the DB + """ + # check if username existed + user = User.query.filter(User.username == self.username).first() + if user: + return {'status': False, 'msg': 'Username is already in use'} + + # check if email existed + user = User.query.filter(User.email == self.email).first() + if user: + return {'status': False, 'msg': 'Email address is already in use'} + + # first register user will be in Administrator role + self.role_id = Role.query.filter_by(name='User').first().id + if User.query.count() == 0: + self.role_id = Role.query.filter_by( + name='Administrator').first().id + + self.password = self.get_hashed_password( + self.plain_text_password) if self.plain_text_password else '*' + + if self.password and self.password != '*': + self.password = self.password.decode("utf-8") + + db.session.add(self) + db.session.commit() + return {'status': True, 'msg': 'Created user successfully'} + + def update_local_user(self): + """ + Update local user + """ + # Sanity check - account name + if self.username == "": + return {'status': False, 'msg': 'No user name specified'} + + # read user and check that it exists + user = User.query.filter(User.username == self.username).first() + if not user: + return {'status': False, 'msg': 'User does not exist'} + + # check if new email exists (only if changed) + if user.email != self.email: + checkuser = User.query.filter(User.email == self.email).first() + if checkuser: + return { + 'status': False, + 'msg': 'New email address is already in use' + } + + user.firstname = self.firstname + user.lastname = self.lastname + user.email = self.email + + # store new password hash (only if changed) + if self.plain_text_password != "": + user.password = self.get_hashed_password( + self.plain_text_password).decode("utf-8") + + db.session.commit() + return {'status': True, 'msg': 'User updated successfully'} + + def update_profile(self, enable_otp=None): + """ + Update user profile + """ + + user = User.query.filter(User.username == self.username).first() + if not user: + return False + + user.firstname = self.firstname if self.firstname else user.firstname + user.lastname = self.lastname if self.lastname else user.lastname + user.email = self.email if self.email else user.email + user.password = self.get_hashed_password( + self.plain_text_password).decode( + "utf-8") if self.plain_text_password else user.password + + if enable_otp is not None: + user.otp_secret = "" + + if enable_otp == True: + # generate the opt secret key + user.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') + + try: + db.session.add(user) + db.session.commit() + return True + except Exception: + db.session.rollback() + return False + + def get_domains(self): + """ + Get list of domains which the user is granted to have + access. + + Note: This doesn't include the permission granting from Account + which user belong to + """ + + return self.get_domain_query().all() + + def delete(self): + """ + Delete a user + """ + # revoke all user privileges first + self.revoke_privilege() + + try: + User.query.filter(User.username == self.username).delete() + db.session.commit() + return True + except Exception as e: + db.session.rollback() + current_app.logger.error('Cannot delete user {0} from DB. DETAIL: {1}'.format( + self.username, e)) + return False + + def revoke_privilege(self): + """ + Revoke all privileges from a user + """ + user = User.query.filter(User.username == self.username).first() + + if user: + user_id = user.id + try: + DomainUser.query.filter(DomainUser.user_id == user_id).delete() + db.session.commit() + return True + except Exception as e: + db.session.rollback() + current_app.logger.error( + 'Cannot revoke user {0} privileges. DETAIL: {1}'.format( + self.username, e)) + return False + return False + + def set_role(self, role_name): + role = Role.query.filter(Role.name == role_name).first() + if role: + user = User.query.filter(User.username == self.username).first() + user.role_id = role.id + db.session.commit() + return {'status': True, 'msg': 'Set user role successfully'} + else: + return {'status': False, 'msg': 'Role does not exist'} \ No newline at end of file diff --git a/powerdnsadmin/routes/__init__.py b/powerdnsadmin/routes/__init__.py new file mode 100644 index 0000000..829f110 --- /dev/null +++ b/powerdnsadmin/routes/__init__.py @@ -0,0 +1,25 @@ +from .base import login_manager, handle_bad_request, handle_unauthorized_access, handle_access_forbidden, handle_page_not_found, handle_internal_server_error + +from .index import index_bp +from .user import user_bp +from .dashboard import dashboard_bp +from .domain import domain_bp +from .admin import admin_bp +from .api import api_bp + + +def init_app(app): + login_manager.init_app(app) + + app.register_blueprint(index_bp) + app.register_blueprint(user_bp) + app.register_blueprint(dashboard_bp) + app.register_blueprint(domain_bp) + app.register_blueprint(admin_bp) + app.register_blueprint(api_bp) + + app.register_error_handler(400, handle_bad_request) + app.register_error_handler(401, handle_unauthorized_access) + app.register_error_handler(403, handle_access_forbidden) + app.register_error_handler(404, handle_page_not_found) + app.register_error_handler(500, handle_internal_server_error) diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py new file mode 100644 index 0000000..7c5432f --- /dev/null +++ b/powerdnsadmin/routes/admin.py @@ -0,0 +1,994 @@ +import re +import json +import traceback +import datetime +from ast import literal_eval +from distutils.version import StrictVersion +from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, jsonify, abort +from flask_login import login_user, login_required, current_user + +from .base import login_manager +from ..decorators import operator_role_required, admin_role_required +from ..models.user import User, Anonymous +from ..models.account import Account +from ..models.account_user import AccountUser +from ..models.role import Role +from ..models.server import Server +from ..models.setting import Setting +from ..models.history import History +from ..models.domain import Domain +from ..models.record import Record +from ..models.domain_template import DomainTemplate +from ..models.domain_template_record import DomainTemplateRecord + +admin_bp = Blueprint('admin', + __name__, + template_folder='templates', + url_prefix='/admin') + + +@admin_bp.route('/pdns', methods=['GET']) +@login_required +@operator_role_required +def pdns_stats(): + if not Setting().get('pdns_api_url') or not Setting().get( + 'pdns_api_key') or not Setting().get('pdns_version'): + return redirect(url_for('admin.setting_pdns')) + + domains = Domain.query.all() + users = User.query.all() + + server = Server(server_id='localhost') + configs = server.get_config() + statistics = server.get_statistic() + history_number = History.query.count() + + if statistics: + uptime = list([ + uptime for uptime in statistics if uptime['name'] == 'uptime' + ])[0]['value'] + else: + uptime = 0 + + return render_template('admin_pdns_stats.html', + domains=domains, + users=users, + configs=configs, + statistics=statistics, + uptime=uptime, + history_number=history_number) + + +@admin_bp.route('/user/edit/', methods=['GET', 'POST']) +@admin_bp.route('/user/edit', methods=['GET', 'POST']) +@login_required +@operator_role_required +def edit_user(user_username=None): + if user_username: + user = User.query.filter(User.username == user_username).first() + create = False + + if not user: + return render_template('errors/404.html'), 404 + + if user.role.name == 'Administrator' and current_user.role.name != 'Administrator': + return render_template('errors/401.html'), 401 + else: + user = None + create = True + + if request.method == 'GET': + return render_template('admin_edit_user.html', + user=user, + create=create) + + elif request.method == 'POST': + fdata = request.form + + if create: + user_username = fdata['username'] + + user = User(username=user_username, + plain_text_password=fdata['password'], + firstname=fdata['firstname'], + lastname=fdata['lastname'], + email=fdata['email'], + reload_info=False) + + if create: + if fdata['password'] == "": + return render_template('admin_edit_user.html', + user=user, + create=create, + blank_password=True) + + result = user.create_local_user() + history = History(msg='Created user {0}'.format(user.username), + created_by=current_user.username) + + else: + result = user.update_local_user() + history = History(msg='Updated user {0}'.format(user.username), + created_by=current_user.username) + + if result['status']: + history.add() + return redirect(url_for('admin.manage_user')) + + return render_template('admin_edit_user.html', + user=user, + create=create, + error=result['msg']) + + +@admin_bp.route('/manage-user', methods=['GET', 'POST']) +@login_required +@operator_role_required +def manage_user(): + if request.method == 'GET': + roles = Role.query.all() + users = User.query.order_by(User.username).all() + return render_template('admin_manage_user.html', + users=users, + roles=roles) + + if request.method == 'POST': + # + # post data should in format + # {'action': 'delete_user', 'data': 'username'} + # + try: + jdata = request.json + data = jdata['data'] + + if jdata['action'] == 'user_otp_disable': + user = User(username=data) + result = user.update_profile(enable_otp=False) + if result: + history = History( + msg='Two factor authentication disabled for user {0}'. + format(data), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': + 'ok', + 'msg': + 'Two factor authentication has been disabled for user.' + }), 200) + else: + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'Cannot disable two factor authentication for user.' + }), 500) + + elif jdata['action'] == 'delete_user': + user = User(username=data) + if user.username == current_user.username: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'You cannot delete yourself.' + }), 400) + + # Remove account associations first + user_accounts = Account.query.join(AccountUser).join( + User).filter(AccountUser.user_id == user.id, + AccountUser.account_id == Account.id).all() + for uc in user_accounts: + uc.revoke_privileges_by_id(user.id) + + # Then delete the user + result = user.delete() + if result: + history = History(msg='Delete username {0}'.format(data), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'User has been removed.' + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Cannot remove user.' + }), 500) + + elif jdata['action'] == 'revoke_user_privileges': + user = User(username=data) + result = user.revoke_privilege() + if result: + history = History( + msg='Revoke {0} user privileges'.format(data), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Revoked user privileges.' + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Cannot revoke user privilege.' + }), 500) + + elif jdata['action'] == 'update_user_role': + username = data['username'] + role_name = data['role_name'] + + if username == current_user.username: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'You cannot change you own roles.' + }), 400) + + user = User.query.filter(User.username == username).first() + if not user: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'User does not exist.' + }), 404) + + if user.role.name == 'Administrator' and current_user.role.name != 'Administrator': + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'You do not have permission to change Administrator users role.' + }), 400) + + if role_name == 'Administrator' and current_user.role.name != 'Administrator': + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'You do not have permission to promote a user to Administrator role.' + }), 400) + + user = User(username=username) + result = user.set_role(role_name) + if result['status']: + history = History( + msg='Change user role of {0} to {1}'.format( + username, role_name), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Changed user role successfully.' + }), 200) + else: + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'Cannot change user role. {0}'.format( + result['msg']) + }), 500) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Action not supported.' + }), 400) + except Exception as e: + current_app.logger.error( + 'Cannot update user. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'There is something wrong, please contact Administrator.' + }), 400) + + +@admin_bp.route('/account/edit/', methods=['GET', 'POST']) +@admin_bp.route('/account/edit', methods=['GET', 'POST']) +@login_required +@operator_role_required +def edit_account(account_name=None): + users = User.query.all() + + if request.method == 'GET': + if account_name is None: + return render_template('admin_edit_account.html', + users=users, + create=1) + + else: + account = Account.query.filter( + Account.name == account_name).first() + account_user_ids = account.get_user() + return render_template('admin_edit_account.html', + account=account, + account_user_ids=account_user_ids, + users=users, + create=0) + + if request.method == 'POST': + fdata = request.form + new_user_list = request.form.getlist('account_multi_user') + + # on POST, synthesize account and account_user_ids from form data + if not account_name: + account_name = fdata['accountname'] + + account = Account(name=account_name, + description=fdata['accountdescription'], + contact=fdata['accountcontact'], + mail=fdata['accountmail']) + account_user_ids = [] + for username in new_user_list: + userid = User(username=username).get_user_info_by_username().id + account_user_ids.append(userid) + + create = int(fdata['create']) + if create: + # account __init__ sanitizes and lowercases the name, so to manage expectations + # we let the user reenter the name until it's not empty and it's valid (ignoring the case) + if account.name == "" or account.name != account_name.lower(): + return render_template('admin_edit_account.html', + account=account, + account_user_ids=account_user_ids, + users=users, + create=create, + invalid_accountname=True) + + if Account.query.filter(Account.name == account.name).first(): + return render_template('admin_edit_account.html', + account=account, + account_user_ids=account_user_ids, + users=users, + create=create, + duplicate_accountname=True) + + result = account.create_account() + history = History(msg='Create account {0}'.format(account.name), + created_by=current_user.username) + + else: + result = account.update_account() + history = History(msg='Update account {0}'.format(account.name), + created_by=current_user.username) + + if result['status']: + account.grant_privileges(new_user_list) + history.add() + return redirect(url_for('admin.manage_account')) + + return render_template('admin_edit_account.html', + account=account, + account_user_ids=account_user_ids, + users=users, + create=create, + error=result['msg']) + + +@admin_bp.route('/manage-account', methods=['GET', 'POST']) +@login_required +@operator_role_required +def manage_account(): + if request.method == 'GET': + accounts = Account.query.order_by(Account.name).all() + for account in accounts: + account.user_num = AccountUser.query.filter( + AccountUser.account_id == account.id).count() + return render_template('admin_manage_account.html', accounts=accounts) + + if request.method == 'POST': + # + # post data should in format + # {'action': 'delete_account', 'data': 'accountname'} + # + try: + jdata = request.json + data = jdata['data'] + + if jdata['action'] == 'delete_account': + account = Account.query.filter(Account.name == data).first() + if not account: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Account not found.' + }), 404) + # Remove account association from domains first + for domain in account.domains: + Domain(name=domain.name).assoc_account(None) + # Then delete the account + result = account.delete_account() + if result: + history = History(msg='Delete account {0}'.format(data), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Account has been removed.' + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Cannot remove account.' + }), 500) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Action not supported.' + }), 400) + except Exception as e: + current_app.logger.error( + 'Cannot update account. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'There is something wrong, please contact Administrator.' + }), 400) + + +@admin_bp.route('/history', methods=['GET', 'POST']) +@login_required +@operator_role_required +def history(): + if request.method == 'POST': + if current_user.role.name != 'Administrator': + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'You do not have permission to remove history.' + }), 401) + + h = History() + result = h.remove_all() + if result: + history = History(msg='Remove all histories', + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Changed user role successfully.' + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Can not remove histories.' + }), 500) + + if request.method == 'GET': + histories = History.query.all() + return render_template('admin_history.html', histories=histories) + + +@admin_bp.route('/setting/basic', methods=['GET']) +@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', 'bg_domain_updates', 'site_name', + 'session_timeout', 'ttl_options', 'pdns_api_timeout' + ] + + return render_template('admin_setting_basic.html', settings=settings) + + +@admin_bp.route('/setting/basic//edit', methods=['POST']) +@login_required +@operator_role_required +def setting_basic_edit(setting): + jdata = request.json + new_value = jdata['value'] + result = Setting().set(setting, new_value) + + if (result): + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Toggled setting successfully.' + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Unable to toggle setting.' + }), 500) + + +@admin_bp.route('/setting/basic//toggle', methods=['POST']) +@login_required +@operator_role_required +def setting_basic_toggle(setting): + result = Setting().toggle(setting) + if (result): + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Toggled setting successfully.' + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Unable to toggle setting.' + }), 500) + + +@admin_bp.route('/setting/pdns', methods=['GET', 'POST']) +@login_required +@admin_role_required +def setting_pdns(): + if request.method == 'GET': + pdns_api_url = Setting().get('pdns_api_url') + pdns_api_key = Setting().get('pdns_api_key') + pdns_version = Setting().get('pdns_version') + return render_template('admin_setting_pdns.html', + pdns_api_url=pdns_api_url, + pdns_api_key=pdns_api_key, + pdns_version=pdns_version) + elif request.method == 'POST': + pdns_api_url = request.form.get('pdns_api_url') + pdns_api_key = request.form.get('pdns_api_key') + pdns_version = request.form.get('pdns_version') + + Setting().set('pdns_api_url', pdns_api_url) + Setting().set('pdns_api_key', pdns_api_key) + Setting().set('pdns_version', pdns_version) + + return render_template('admin_setting_pdns.html', + pdns_api_url=pdns_api_url, + pdns_api_key=pdns_api_key, + pdns_version=pdns_version) + + +@admin_bp.route('/setting/dns-records', methods=['GET', 'POST']) +@login_required +@operator_role_required +def setting_records(): + if request.method == 'GET': + _fr = Setting().get('forward_records_allow_edit') + _rr = Setting().get('reverse_records_allow_edit') + f_records = literal_eval(_fr) if isinstance(_fr, str) else _fr + r_records = literal_eval(_rr) if isinstance(_rr, str) else _rr + + return render_template('admin_setting_records.html', + f_records=f_records, + r_records=r_records) + elif request.method == 'POST': + fr = {} + rr = {} + records = Setting().defaults['forward_records_allow_edit'] + for r in records: + fr[r] = True if request.form.get('fr_{0}'.format( + r.lower())) else False + rr[r] = True if request.form.get('rr_{0}'.format( + r.lower())) else False + + Setting().set('forward_records_allow_edit', str(fr)) + Setting().set('reverse_records_allow_edit', str(rr)) + return redirect(url_for('admin.setting_records')) + + +@admin_bp.route('/setting/authentication', methods=['GET', 'POST']) +@login_required +@admin_role_required +def setting_authentication(): + if request.method == 'GET': + return render_template('admin_setting_authentication.html') + elif request.method == 'POST': + conf_type = request.form.get('config_tab') + result = None + + if conf_type == 'general': + local_db_enabled = True if request.form.get( + 'local_db_enabled') else False + signup_enabled = True if request.form.get( + 'signup_enabled', ) else False + + if not local_db_enabled and not Setting().get('ldap_enabled'): + result = { + 'status': + False, + 'msg': + 'Local DB and LDAP Authentication can not be disabled at the same time.' + } + else: + Setting().set('local_db_enabled', local_db_enabled) + Setting().set('signup_enabled', signup_enabled) + result = {'status': True, 'msg': 'Saved successfully'} + elif conf_type == 'ldap': + ldap_enabled = True if request.form.get('ldap_enabled') else False + + if not ldap_enabled and not Setting().get('local_db_enabled'): + result = { + 'status': + False, + 'msg': + 'Local DB and LDAP Authentication can not be disabled at the same time.' + } + else: + Setting().set('ldap_enabled', ldap_enabled) + Setting().set('ldap_type', request.form.get('ldap_type')) + Setting().set('ldap_uri', request.form.get('ldap_uri')) + Setting().set('ldap_base_dn', request.form.get('ldap_base_dn')) + Setting().set('ldap_admin_username', + request.form.get('ldap_admin_username')) + Setting().set('ldap_admin_password', + request.form.get('ldap_admin_password')) + Setting().set('ldap_filter_basic', + request.form.get('ldap_filter_basic')) + Setting().set('ldap_filter_username', + request.form.get('ldap_filter_username')) + Setting().set( + 'ldap_sg_enabled', True + if request.form.get('ldap_sg_enabled') == 'ON' else False) + Setting().set('ldap_admin_group', + request.form.get('ldap_admin_group')) + Setting().set('ldap_operator_group', + request.form.get('ldap_operator_group')) + Setting().set('ldap_user_group', + request.form.get('ldap_user_group')) + Setting().set('ldap_domain', request.form.get('ldap_domain')) + result = {'status': True, 'msg': 'Saved successfully'} + elif conf_type == 'google': + Setting().set( + 'google_oauth_enabled', + True if request.form.get('google_oauth_enabled') else False) + Setting().set('google_oauth_client_id', + request.form.get('google_oauth_client_id')) + Setting().set('google_oauth_client_secret', + request.form.get('google_oauth_client_secret')) + Setting().set('google_token_url', + request.form.get('google_token_url')) + Setting().set('google_oauth_scope', + request.form.get('google_oauth_scope')) + Setting().set('google_authorize_url', + request.form.get('google_authorize_url')) + Setting().set('google_base_url', + request.form.get('google_base_url')) + result = { + 'status': True, + 'msg': 'Saved successfully. Please reload PDA to take effect.' + } + elif conf_type == 'github': + Setting().set( + 'github_oauth_enabled', + True if request.form.get('github_oauth_enabled') else False) + Setting().set('github_oauth_key', + request.form.get('github_oauth_key')) + Setting().set('github_oauth_secret', + request.form.get('github_oauth_secret')) + Setting().set('github_oauth_scope', + request.form.get('github_oauth_scope')) + Setting().set('github_oauth_api_url', + request.form.get('github_oauth_api_url')) + Setting().set('github_oauth_token_url', + request.form.get('github_oauth_token_url')) + Setting().set('github_oauth_authorize_url', + request.form.get('github_oauth_authorize_url')) + result = { + 'status': True, + 'msg': 'Saved successfully. Please reload PDA to take effect.' + } + elif conf_type == 'oidc': + Setting().set( + 'oidc_oauth_enabled', + True if request.form.get('oidc_oauth_enabled') else False) + Setting().set('oidc_oauth_key', request.form.get('oidc_oauth_key')) + Setting().set('oidc_oauth_secret', + request.form.get('oidc_oauth_secret')) + Setting().set('oidc_oauth_scope', + request.form.get('oidc_oauth_scope')) + Setting().set('oidc_oauth_api_url', + request.form.get('oidc_oauth_api_url')) + Setting().set('oidc_oauth_token_url', + request.form.get('oidc_oauth_token_url')) + Setting().set('oidc_oauth_authorize_url', + request.form.get('oidc_oauth_authorize_url')) + result = { + 'status': True, + 'msg': 'Saved successfully. Please reload PDA to take effect.' + } + else: + return abort(400) + + return render_template('admin_setting_authentication.html', + result=result) + + +@admin_bp.route('/templates', methods=['GET', 'POST']) +@admin_bp.route('/templates/list', methods=['GET', 'POST']) +@login_required +@operator_role_required +def templates(): + templates = DomainTemplate.query.all() + return render_template('template.html', templates=templates) + + +@admin_bp.route('/template/create', methods=['GET', 'POST']) +@login_required +@operator_role_required +def create_template(): + if request.method == 'GET': + return render_template('template_add.html') + if request.method == 'POST': + try: + name = request.form.getlist('name')[0] + description = request.form.getlist('description')[0] + + if ' ' in name or not name or not type: + flash("Please correct your input", 'error') + return redirect(url_for('admin.create_template')) + + if DomainTemplate.query.filter( + DomainTemplate.name == name).first(): + flash( + "A template with the name {0} already exists!".format( + name), 'error') + return redirect(url_for('admin.create_template')) + + t = DomainTemplate(name=name, description=description) + result = t.create() + if result['status'] == 'ok': + history = History(msg='Add domain template {0}'.format(name), + detail=str({ + 'name': name, + 'description': description + }), + created_by=current_user.username) + history.add() + return redirect(url_for('admin.templates')) + else: + flash(result['msg'], 'error') + return redirect(url_for('admin.create_template')) + except Exception as e: + current_app.logger.error( + 'Cannot create domain template. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + abort(500) + + +@admin_bp.route('/template/create-from-zone', methods=['POST']) +@login_required +@operator_role_required +def create_template_from_zone(): + try: + jdata = request.json + name = jdata['name'] + description = jdata['description'] + domain_name = jdata['domain'] + + if ' ' in name or not name or not type: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Please correct template name' + }), 400) + + if DomainTemplate.query.filter(DomainTemplate.name == name).first(): + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'A template with the name {0} already exists!'.format(name) + }), 409) + + t = DomainTemplate(name=name, description=description) + result = t.create() + if result['status'] == 'ok': + history = History(msg='Add domain template {0}'.format(name), + detail=str({ + 'name': name, + 'description': description + }), + created_by=current_user.username) + history.add() + + records = [] + r = Record() + domain = Domain.query.filter(Domain.name == domain_name).first() + if domain: + # query domain info from PowerDNS API + zone_info = r.get_record_data(domain.name) + if zone_info: + jrecords = zone_info['records'] + + if StrictVersion(Setting().get( + 'pdns_version')) >= StrictVersion('4.0.0'): + for jr in jrecords: + if jr['type'] in Setting().get_records_allow_to_edit(): + name = '@' if jr['name'] == domain_name else re.sub( + '\.{}$'.format(domain_name), '', jr['name']) + for subrecord in jr['records']: + record = DomainTemplateRecord( + name=name, + type=jr['type'], + status=True + if subrecord['disabled'] else False, + ttl=jr['ttl'], + data=subrecord['content']) + records.append(record) + else: + for jr in jrecords: + if jr['type'] in Setting().get_records_allow_to_edit(): + name = '@' if jr['name'] == domain_name else re.sub( + '\.{}$'.format(domain_name), '', jr['name']) + record = DomainTemplateRecord( + name=name, + type=jr['type'], + status=True if jr['disabled'] else False, + ttl=jr['ttl'], + data=jr['content']) + records.append(record) + + result_records = t.replace_records(records) + + if result_records['status'] == 'ok': + return make_response( + jsonify({ + 'status': 'ok', + 'msg': result['msg'] + }), 200) + else: + t.delete_template() + return make_response( + jsonify({ + 'status': 'error', + 'msg': result_records['msg'] + }), 500) + + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': result['msg'] + }), 500) + except Exception as e: + current_app.logger.error( + 'Cannot create template from zone. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Error when applying new changes' + }), 500) + + +@admin_bp.route('/template//edit', methods=['GET']) +@login_required +@operator_role_required +def edit_template(template): + try: + t = DomainTemplate.query.filter( + DomainTemplate.name == template).first() + records_allow_to_edit = Setting().get_records_allow_to_edit() + quick_edit = Setting().get('record_quick_edit') + ttl_options = Setting().get_ttl_options() + if t is not None: + records = [] + for jr in t.records: + if jr.type in records_allow_to_edit: + record = DomainTemplateRecord( + name=jr.name, + type=jr.type, + status='Disabled' if jr.status else 'Active', + ttl=jr.ttl, + data=jr.data) + records.append(record) + + return render_template('template_edit.html', + template=t.name, + records=records, + editable_records=records_allow_to_edit, + quick_edit=quick_edit, + ttl_options=ttl_options) + except Exception as e: + current_app.logger.error( + 'Cannot open domain template page. DETAIL: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + abort(500) + return redirect(url_for('admin.templates')) + + +@admin_bp.route('/template//apply', + methods=['POST'], + strict_slashes=False) +@login_required +def apply_records(template): + try: + jdata = request.json + records = [] + + for j in jdata['records']: + name = '@' if j['record_name'] in ['@', ''] else j['record_name'] + type = j['record_type'] + data = j['record_data'] + disabled = True if j['record_status'] == 'Disabled' else False + ttl = int(j['record_ttl']) if j['record_ttl'] else 3600 + + dtr = DomainTemplateRecord(name=name, + type=type, + data=data, + status=disabled, + ttl=ttl) + records.append(dtr) + + t = DomainTemplate.query.filter( + DomainTemplate.name == template).first() + result = t.replace_records(records) + if result['status'] == 'ok': + jdata.pop('_csrf_token', + None) # don't store csrf token in the history. + history = History( + msg='Apply domain template record changes to domain template {0}' + .format(template), + detail=str(json.dumps(jdata)), + created_by=current_user.username) + history.add() + return make_response(jsonify(result), 200) + else: + return make_response(jsonify(result), 400) + except Exception as e: + current_app.logger.error( + 'Cannot apply record changes to the template. Error: {0}'.format( + e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Error when applying new changes' + }), 500) + + +@admin_bp.route('/template//delete', methods=['POST']) +@login_required +@operator_role_required +def delete_template(template): + try: + t = DomainTemplate.query.filter( + DomainTemplate.name == template).first() + if t is not None: + result = t.delete_template() + if result['status'] == 'ok': + history = History( + msg='Deleted domain template {0}'.format(template), + detail=str({'name': template}), + created_by=current_user.username) + history.add() + return redirect(url_for('admin.templates')) + else: + flash(result['msg'], 'error') + return redirect(url_for('admin.templates')) + except Exception as e: + current_app.logger.error( + 'Cannot delete template. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + abort(500) + return redirect(url_for('admin.templates')) diff --git a/app/blueprints/api.py b/powerdnsadmin/routes/api.py similarity index 54% rename from app/blueprints/api.py rename to powerdnsadmin/routes/api.py index 6439d84..2ce1bd8 100644 --- a/app/blueprints/api.py +++ b/powerdnsadmin/routes/api.py @@ -1,77 +1,97 @@ import json -from flask import Blueprint, g, request, abort -from app.models import Domain, History, Setting, ApiKey -from app.lib import utils, helper -from app.decorators import api_basic_auth, api_can_create_domain, is_json -from app.decorators import apikey_auth, apikey_is_admin -from app.decorators import apikey_can_access_domain -from app import csrf -from app.errors import DomainNotExists, DomainAccessForbidden, RequestIsNotJSON -from app.errors import ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges -from app.schema import ApiKeySchema, DomainSchema, ApiPlainKeySchema from urllib.parse import urljoin -from app.lib.log import logging +from flask import Blueprint, g, request, abort, current_app +from flask_login import current_user -api_blueprint = Blueprint('api_blueprint', __name__) +from ..models.base import db +from ..models import Domain, DomainUser, Account, AccountUser, History, Setting, ApiKey +from ..lib import utils, helper +from ..lib.schema import ApiKeySchema, DomainSchema, ApiPlainKeySchema +from ..lib.errors import DomainNotExists, DomainAlreadyExists, DomainAccessForbidden, RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges +from ..decorators import api_basic_auth, api_can_create_domain, is_json, apikey_auth, apikey_is_admin, apikey_can_access_domain + +api_bp = Blueprint('api', __name__, url_prefix='/api/v1') apikey_schema = ApiKeySchema(many=True) domain_schema = DomainSchema(many=True) apikey_plain_schema = ApiPlainKeySchema(many=True) -@api_blueprint.errorhandler(400) +def get_user_domains(): + domains = db.session.query(Domain) \ + .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ + .outerjoin(Account, Domain.account_id == Account.id) \ + .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ + .filter( + db.or_( + DomainUser.user_id == current_user.id, + AccountUser.user_id == current_user.id + )).all() + return domains + + +@api_bp.errorhandler(400) def handle_400(err): return json.dumps({"msg": "Bad Request"}), 400 -@api_blueprint.errorhandler(401) +@api_bp.errorhandler(401) def handle_401(err): return json.dumps({"msg": "Unauthorized"}), 401 -@api_blueprint.errorhandler(500) +@api_bp.errorhandler(409) +def handle_409(err): + return json.dumps({"msg": "Conflict"}), 409 + + +@api_bp.errorhandler(500) def handle_500(err): return json.dumps({"msg": "Internal Server Error"}), 500 -@api_blueprint.errorhandler(DomainNotExists) +@api_bp.errorhandler(DomainNotExists) def handle_domain_not_exists(err): return json.dumps(err.to_dict()), err.status_code -@api_blueprint.errorhandler(DomainAccessForbidden) +@api_bp.errorhandler(DomainAlreadyExists) +def handle_domain_already_exists(err): + return json.dumps(err.to_dict()), err.status_code + + +@api_bp.errorhandler(DomainAccessForbidden) def handle_domain_access_forbidden(err): return json.dumps(err.to_dict()), err.status_code -@api_blueprint.errorhandler(ApiKeyCreateFail) +@api_bp.errorhandler(ApiKeyCreateFail) def handle_apikey_create_fail(err): return json.dumps(err.to_dict()), err.status_code -@api_blueprint.errorhandler(ApiKeyNotUsable) +@api_bp.errorhandler(ApiKeyNotUsable) def handle_apikey_not_usable(err): return json.dumps(err.to_dict()), err.status_code -@api_blueprint.errorhandler(NotEnoughPrivileges) +@api_bp.errorhandler(NotEnoughPrivileges) def handle_not_enough_privileges(err): return json.dumps(err.to_dict()), err.status_code -@api_blueprint.errorhandler(RequestIsNotJSON) +@api_bp.errorhandler(RequestIsNotJSON) def handle_request_is_not_json(err): return json.dumps(err.to_dict()), err.status_code -@api_blueprint.before_request +@api_bp.before_request @is_json def before_request(): pass -@csrf.exempt -@api_blueprint.route('/pdnsadmin/zones', methods=['POST']) +@api_bp.route('/pdnsadmin/zones', methods=['POST']) @api_basic_auth @api_can_create_domain def api_login_create_zone(): @@ -85,45 +105,49 @@ def api_login_create_zone(): msg_str = "Sending request to powerdns API {0}" msg = msg_str.format(request.get_json(force=True)) - logging.debug(msg) + current_app.logger.debug(msg) - resp = utils.fetch_remote( - urljoin(pdns_api_url, api_full_uri), - method='POST', - data=request.get_json(force=True), - headers=headers, - accept='application/json; q=1' - ) + try: + resp = utils.fetch_remote(urljoin(pdns_api_url, api_full_uri), + method='POST', + data=request.get_json(force=True), + headers=headers, + accept='application/json; q=1') + except Exception as e: + current_app.logger.error("Cannot create domain. Error: {}".format(e)) + abort(500) if resp.status_code == 201: - logging.debug("Request to powerdns API successful") + current_app.logger.debug("Request to powerdns API successful") data = request.get_json(force=True) - history = History( - msg='Add domain {0}'.format(data['name'].rstrip('.')), - detail=json.dumps(data), - created_by=g.user.username - ) + history = History(msg='Add domain {0}'.format( + data['name'].rstrip('.')), + detail=json.dumps(data), + created_by=current_user.username) history.add() - if g.user.role.name not in ['Administrator', 'Operator']: - logging.debug("User is ordinary user, assigning created domain") + if current_user.role.name not in ['Administrator', 'Operator']: + current_app.logger.debug( + "User is ordinary user, assigning created domain") domain = Domain(name=data['name'].rstrip('.')) domain.update() - domain.grant_privileges([g.user.username]) + domain.grant_privileges([current_user.username]) domain = Domain() domain.update() + if resp.status_code == 409: + raise(DomainAlreadyExists) + return resp.content, resp.status_code, resp.headers.items() -@csrf.exempt -@api_blueprint.route('/pdnsadmin/zones', methods=['GET']) +@api_bp.route('/pdnsadmin/zones', methods=['GET']) @api_basic_auth def api_login_list_zones(): - if g.user.role.name not in ['Administrator', 'Operator']: - domain_obj_list = g.user.get_domains() + if current_user.role.name not in ['Administrator', 'Operator']: + domain_obj_list = get_user_domains() else: domain_obj_list = Domain.query.all() @@ -131,11 +155,7 @@ def api_login_list_zones(): return json.dumps(domain_schema.dump(domain_obj_list)), 200 -@csrf.exempt -@api_blueprint.route( - '/pdnsadmin/zones/', - methods=['DELETE'] -) +@api_bp.route('/pdnsadmin/zones/', methods=['DELETE']) @api_basic_auth @api_can_create_domain def api_login_delete_zone(domain_name): @@ -153,45 +173,40 @@ def api_login_delete_zone(domain_name): if not domain: abort(404) - if g.user.role.name not in ['Administrator', 'Operator']: - user_domains_obj_list = g.user.get_domains() + if current_user.role.name not in ['Administrator', 'Operator']: + user_domains_obj_list = get_user_domains() user_domains_list = [item.name for item in user_domains_obj_list] if domain_name not in user_domains_list: raise DomainAccessForbidden() msg_str = "Sending request to powerdns API {0}" - logging.debug(msg_str.format(domain_name)) + current_app.logger.debug(msg_str.format(domain_name)) try: - resp = utils.fetch_remote( - urljoin(pdns_api_url, api_full_uri), - method='DELETE', - headers=headers, - accept='application/json; q=1' - ) + resp = utils.fetch_remote(urljoin(pdns_api_url, api_full_uri), + method='DELETE', + headers=headers, + accept='application/json; q=1') if resp.status_code == 204: - logging.debug("Request to powerdns API successful") + current_app.logger.debug("Request to powerdns API successful") - history = History( - msg='Delete domain {0}'.format(domain_name), - detail='', - created_by=g.user.username - ) + history = History(msg='Delete domain {0}'.format(domain_name), + detail='', + created_by=current_user.username) history.add() domain = Domain() domain.update() except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(500) return resp.content, resp.status_code, resp.headers.items() -@csrf.exempt -@api_blueprint.route('/pdnsadmin/apikeys', methods=['POST']) +@api_bp.route('/pdnsadmin/apikeys', methods=['POST']) @api_basic_auth def api_generate_apikey(): data = request.get_json() @@ -201,7 +216,7 @@ def api_generate_apikey(): domain_obj_list = [] abort(400) if 'domains' not in data else None - abort(400) if not isinstance(data['domains'], (list,)) else None + abort(400) if not isinstance(data['domains'], (list, )) else None abort(400) if 'role' not in data else None description = data['description'] if 'description' in data else None @@ -209,98 +224,95 @@ def api_generate_apikey(): domains = data['domains'] if role_name == 'User' and len(domains) == 0: - logging.error("Apikey with User role must have domains") + current_app.logger.error("Apikey with User role must have domains") raise ApiKeyNotUsable() elif role_name == 'User': domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all() if len(domain_obj_list) == 0: msg = "One of supplied domains does not exists" - logging.error(msg) + current_app.logger.error(msg) raise DomainNotExists(message=msg) - if g.user.role.name not in ['Administrator', 'Operator']: + if current_user.role.name not in ['Administrator', 'Operator']: # domain list of domain api key should be valid for # if not any domain error # role of api key, user cannot assign role above for api key if role_name != 'User': msg = "User cannot assign other role than User" - logging.error(msg) + current_app.logger.error(msg) raise NotEnoughPrivileges(message=msg) - user_domain_obj_list = g.user.get_domains() + user_domain_obj_list = get_user_domains() domain_list = [item.name for item in domain_obj_list] user_domain_list = [item.name for item in user_domain_obj_list] - logging.debug("Input domain list: {0}".format(domain_list)) - logging.debug("User domain list: {0}".format(user_domain_list)) + current_app.logger.debug("Input domain list: {0}".format(domain_list)) + current_app.logger.debug( + "User domain list: {0}".format(user_domain_list)) inter = set(domain_list).intersection(set(user_domain_list)) if not (len(inter) == len(domain_list)): msg = "You don't have access to one of domains" - logging.error(msg) + current_app.logger.error(msg) raise DomainAccessForbidden(message=msg) - apikey = ApiKey( - desc=description, - role_name=role_name, - domains=domain_obj_list - ) + apikey = ApiKey(desc=description, + role_name=role_name, + domains=domain_obj_list) try: apikey.create() except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) raise ApiKeyCreateFail(message='Api key create failed') return json.dumps(apikey_plain_schema.dump([apikey])), 201 -@csrf.exempt -@api_blueprint.route('/pdnsadmin/apikeys', defaults={'domain_name': None}) -@api_blueprint.route('/pdnsadmin/apikeys/') +@api_bp.route('/pdnsadmin/apikeys', defaults={'domain_name': None}) +@api_bp.route('/pdnsadmin/apikeys/') @api_basic_auth def api_get_apikeys(domain_name): apikeys = [] - logging.debug("Getting apikeys") + current_app.logger.debug("Getting apikeys") - if g.user.role.name not in ['Administrator', 'Operator']: + if current_user.role.name not in ['Administrator', 'Operator']: if domain_name: msg = "Check if domain {0} exists and \ - is allowed for user." . format(domain_name) - logging.debug(msg) - apikeys = g.user.get_apikeys(domain_name) + is allowed for user." .format(domain_name) + current_app.logger.debug(msg) + apikeys = current_user.get_apikeys(domain_name) if not apikeys: raise DomainAccessForbidden(name=domain_name) - logging.debug(apikey_schema.dump(apikeys)) + current_app.logger.debug(apikey_schema.dump(apikeys)) else: msg_str = "Getting all allowed domains for user {0}" - msg = msg_str . format(g.user.username) - logging.debug(msg) + msg = msg_str.format(current_user.username) + current_app.logger.debug(msg) try: - apikeys = g.user.get_apikeys() - logging.debug(apikey_schema.dump(apikeys)) + apikeys = current_user.get_apikeys() + current_app.logger.debug(apikey_schema.dump(apikeys)) except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(500) else: - logging.debug("Getting all domains for administrative user") + current_app.logger.debug("Getting all domains for administrative user") try: apikeys = ApiKey.query.all() - logging.debug(apikey_schema.dump(apikeys)) + current_app.logger.debug(apikey_schema.dump(apikeys)) except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(500) return json.dumps(apikey_schema.dump(apikeys)), 200 -@csrf.exempt -@api_blueprint.route('/pdnsadmin/apikeys/', methods=['DELETE']) +@api_bp.route('/pdnsadmin/apikeys/', methods=['DELETE']) @api_basic_auth def api_delete_apikey(apikey_id): apikey = ApiKey.query.get(apikey_id) @@ -308,11 +320,11 @@ def api_delete_apikey(apikey_id): if not apikey: abort(404) - logging.debug(g.user.role.name) + current_app.logger.debug(current_user.role.name) - if g.user.role.name not in ['Administrator', 'Operator']: - apikeys = g.user.get_apikeys() - user_domains_obj_list = g.user.get_domain().all() + if current_user.role.name not in ['Administrator', 'Operator']: + apikeys = current_user.get_apikeys() + user_domains_obj_list = current_user.get_domain().all() apikey_domains_obj_list = apikey.domains user_domains_list = [item.name for item in user_domains_obj_list] apikey_domains_list = [item.name for item in apikey_domains_obj_list] @@ -322,7 +334,7 @@ def api_delete_apikey(apikey_id): if not (len(inter) == len(apikey_domains_list)): msg = "You don't have access to some domains apikey belongs to" - logging.error(msg) + current_app.logger.error(msg) raise DomainAccessForbidden(message=msg) if apikey_id not in apikeys_ids: @@ -331,14 +343,13 @@ def api_delete_apikey(apikey_id): try: apikey.delete() except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(500) return '', 204 -@csrf.exempt -@api_blueprint.route('/pdnsadmin/apikeys/', methods=['PUT']) +@api_bp.route('/pdnsadmin/apikeys/', methods=['PUT']) @api_basic_auth def api_update_apikey(apikey_id): # if role different and user is allowed to change it, update @@ -355,80 +366,78 @@ def api_update_apikey(apikey_id): if not apikey: abort(404) - logging.debug('Updating apikey with id {0}'.format(apikey_id)) + current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id)) if role_name == 'User' and len(domains) == 0: - logging.error("Apikey with User role must have domains") + current_app.logger.error("Apikey with User role must have domains") raise ApiKeyNotUsable() elif role_name == 'User': domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all() if len(domain_obj_list) == 0: msg = "One of supplied domains does not exists" - logging.error(msg) + current_app.logger.error(msg) raise DomainNotExists(message=msg) - if g.user.role.name not in ['Administrator', 'Operator']: + if current_user.role.name not in ['Administrator', 'Operator']: if role_name != 'User': msg = "User cannot assign other role than User" - logging.error(msg) + current_app.logger.error(msg) raise NotEnoughPrivileges(message=msg) - apikeys = g.user.get_apikeys() + apikeys = current_user.get_apikeys() apikey_domains = [item.name for item in apikey.domains] apikeys_ids = [apikey_item.id for apikey_item in apikeys] - user_domain_obj_list = g.user.get_domain().all() + user_domain_obj_list = current_user.get_domain().all() domain_list = [item.name for item in domain_obj_list] user_domain_list = [item.name for item in user_domain_obj_list] - logging.debug("Input domain list: {0}".format(domain_list)) - logging.debug("User domain list: {0}".format(user_domain_list)) + current_app.logger.debug("Input domain list: {0}".format(domain_list)) + current_app.logger.debug( + "User domain list: {0}".format(user_domain_list)) inter = set(domain_list).intersection(set(user_domain_list)) if not (len(inter) == len(domain_list)): msg = "You don't have access to one of domains" - logging.error(msg) + current_app.logger.error(msg) raise DomainAccessForbidden(message=msg) if apikey_id not in apikeys_ids: msg = 'Apikey does not belong to domain to which user has access' - logging.error(msg) + current_app.logger.error(msg) raise DomainAccessForbidden() if set(domains) == set(apikey_domains): - logging.debug("Domains are same, apikey domains won't be updated") + current_app.logger.debug( + "Domains are same, apikey domains won't be updated") domains = None if role_name == apikey.role: - logging.debug("Role is same, apikey role won't be updated") + current_app.logger.debug("Role is same, apikey role won't be updated") role_name = None if description == apikey.description: msg = "Description is same, apikey description won't be updated" - logging.debug(msg) + current_app.logger.debug(msg) description = None try: apikey = ApiKey.query.get(apikey_id) - apikey.update( - role_name=role_name, - domains=domains, - description=description - ) + apikey.update(role_name=role_name, + domains=domains, + description=description) except Exception as e: - logging.error('Error: {0}'.format(e)) + current_app.logger.error('Error: {0}'.format(e)) abort(500) return '', 204 -@csrf.exempt -@api_blueprint.route( +@api_bp.route( '/servers//zones//', - methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] -) + methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE']) @apikey_auth @apikey_can_access_domain def api_zone_subpath_forward(server_id, zone_id, subpath): @@ -436,11 +445,8 @@ def api_zone_subpath_forward(server_id, zone_id, subpath): return resp.content, resp.status_code, resp.headers.items() -@csrf.exempt -@api_blueprint.route( - '/servers//zones/', - methods=['GET', 'PUT', 'PATCH', 'DELETE'] -) +@api_bp.route('/servers//zones/', + methods=['GET', 'PUT', 'PATCH', 'DELETE']) @apikey_auth @apikey_can_access_domain def api_zone_forward(server_id, zone_id): @@ -450,10 +456,7 @@ def api_zone_forward(server_id, zone_id): return resp.content, resp.status_code, resp.headers.items() -@api_blueprint.route( - '/servers', - methods=['GET'] -) +@api_bp.route('/servers', methods=['GET']) @apikey_auth @apikey_is_admin def api_server_forward(): @@ -461,10 +464,7 @@ def api_server_forward(): return resp.content, resp.status_code, resp.headers.items() -@api_blueprint.route( - '/servers/', - methods=['GET', 'PUT'] -) +@api_bp.route('/servers/', methods=['GET', 'PUT']) @apikey_auth @apikey_is_admin def api_server_sub_forward(subpath): @@ -472,25 +472,24 @@ def api_server_sub_forward(subpath): return resp.content, resp.status_code, resp.headers.items() -@csrf.exempt -@api_blueprint.route('/servers//zones', methods=['POST']) +@api_bp.route('/servers//zones', methods=['POST']) @apikey_auth def api_create_zone(server_id): resp = helper.forward_request() if resp.status_code == 201: - logging.debug("Request to powerdns API successful") + current_app.logger.debug("Request to powerdns API successful") data = request.get_json(force=True) - history = History( - msg='Add domain {0}'.format(data['name'].rstrip('.')), - detail=json.dumps(data), - created_by=g.apikey.description - ) + history = History(msg='Add domain {0}'.format( + data['name'].rstrip('.')), + detail=json.dumps(data), + created_by=g.apikey.description) history.add() if g.apikey.role.name not in ['Administrator', 'Operator']: - logging.debug("Apikey is user key, assigning created domain") + current_app.logger.debug( + "Apikey is user key, assigning created domain") domain = Domain(name=data['name'].rstrip('.')) g.apikey.domains.append(domain) @@ -500,8 +499,7 @@ def api_create_zone(server_id): return resp.content, resp.status_code, resp.headers.items() -@csrf.exempt -@api_blueprint.route('/servers//zones', methods=['GET']) +@api_bp.route('/servers//zones', methods=['GET']) @apikey_auth def api_get_zones(server_id): if g.apikey.role.name not in ['Administrator', 'Operator']: @@ -510,9 +508,9 @@ def api_get_zones(server_id): domain_obj_list = Domain.query.all() return json.dumps(domain_schema.dump(domain_obj_list)), 200 + #endpoint to snychronize Domains in background -@csrf.exempt -@api_blueprint.route('/sync_domains', methods=['GET']) +@api_bp.route('/sync_domains', methods=['GET']) @apikey_auth def sync_domains(): domain = Domain() diff --git a/powerdnsadmin/routes/base.py b/powerdnsadmin/routes/base.py new file mode 100644 index 0000000..d1df359 --- /dev/null +++ b/powerdnsadmin/routes/base.py @@ -0,0 +1,65 @@ +import base64 +from flask import render_template, url_for, redirect, session, request, current_app +from flask_login import LoginManager, login_user + +from ..models.user import User + +login_manager = LoginManager() + + +def handle_bad_request(e): + return render_template('errors/400.html', code=400, message=e), 400 + + +def handle_unauthorized_access(e): + session['next'] = request.script_root + request.path + return redirect(url_for('index.login')) + + +def handle_access_forbidden(e): + return render_template('errors/403.html', code=403, message=e), 403 + + +def handle_page_not_found(e): + return render_template('errors/404.html', code=404, message=e), 404 + + +def handle_internal_server_error(e): + return render_template('errors/500.html', code=500, message=e), 500 + + +@login_manager.user_loader +def load_user(id): + """ + This will be current_user + """ + return User.query.get(int(id)) + + +@login_manager.request_loader +def login_via_authorization_header(request): + auth_header = request.headers.get('Authorization') + if auth_header: + auth_header = auth_header.replace('Basic ', '', 1) + try: + auth_header = str(base64.b64decode(auth_header), 'utf-8') + username, password = auth_header.split(":") + except TypeError as e: + return None + user = User(username=username, + password=password, + plain_text_password=password) + try: + auth_method = request.args.get('auth_method', 'LOCAL') + auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL' + auth = user.is_validate(method=auth_method, + src_ip=request.remote_addr) + if auth == False: + return None + else: + # login_user(user, remember=False) + return User.query.filter(User.id==user.id).first() + except Exception as e: + current_app.logger.error('Error: {0}'.format(e)) + return None + return None diff --git a/powerdnsadmin/routes/dashboard.py b/powerdnsadmin/routes/dashboard.py new file mode 100644 index 0000000..ac1792b --- /dev/null +++ b/powerdnsadmin/routes/dashboard.py @@ -0,0 +1,166 @@ +from flask import Blueprint, render_template, make_response, url_for, current_app, request, jsonify +from flask_login import login_required, current_user +from sqlalchemy import not_, or_ + +from ..lib.utils import customBoxes +from ..models.user import User +from ..models.account import Account +from ..models.account_user import AccountUser +from ..models.domain import Domain +from ..models.domain_user import DomainUser +from ..models.setting import Setting +from ..models.history import History +from ..models.server import Server +from ..models.base import db + +dashboard_bp = Blueprint('dashboard', + __name__, + template_folder='templates', + url_prefix='/dashboard') + + +@dashboard_bp.route('/domains-custom/', methods=['GET']) +@login_required +def domains_custom(boxId): + if current_user.role.name in ['Administrator', 'Operator']: + domains = Domain.query + else: + # Get query for domain to which the user has access permission. + # This includes direct domain permission AND permission through + # account membership + domains = db.session.query(Domain) \ + .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ + .outerjoin(Account, Domain.account_id == Account.id) \ + .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ + .filter( + db.or_( + DomainUser.user_id == current_user.id, + AccountUser.user_id == current_user.id + )) + + template = current_app.jinja_env.get_template("dashboard_domain.html") + render = template.make_module(vars={"current_user": current_user}) + + columns = [ + Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master, + Domain.account + ] + # History.created_on.desc() + order_by = [] + for i in range(len(columns)): + column_index = request.args.get("order[{0}][column]".format(i)) + sort_direction = request.args.get("order[{0}][dir]".format(i)) + if column_index is None: + break + if sort_direction != "asc" and sort_direction != "desc": + sort_direction = "asc" + + column = columns[int(column_index)] + order_by.append(getattr(column, sort_direction)()) + + if order_by: + domains = domains.order_by(*order_by) + + if boxId == "reverse": + for boxId in customBoxes.order: + if boxId == "reverse": continue + domains = domains.filter( + not_(Domain.name.ilike(customBoxes.boxes[boxId][1]))) + else: + domains = domains.filter(Domain.name.ilike( + customBoxes.boxes[boxId][1])) + + total_count = domains.count() + + search = request.args.get("search[value]") + if search: + start = "" if search.startswith("^") else "%" + end = "" if search.endswith("$") else "%" + + if current_user.role.name in ['Administrator', 'Operator']: + domains = domains.outerjoin(Account).filter( + Domain.name.ilike(start + search.strip("^$") + end) + | Account.name.ilike(start + search.strip("^$") + end) + | Account.description.ilike(start + search.strip("^$") + end)) + else: + domains = domains.filter( + Domain.name.ilike(start + search.strip("^$") + end)) + + filtered_count = domains.count() + + start = int(request.args.get("start", 0)) + length = min(int(request.args.get("length", 0)), 100) + + if length != -1: + domains = domains[start:start + length] + + data = [] + for domain in domains: + data.append([ + render.name(domain), + render.dnssec(domain), + render.type(domain), + render.serial(domain), + render.master(domain), + render.account(domain), + render.actions(domain), + ]) + + response_data = { + "draw": int(request.args.get("draw", 0)), + "recordsTotal": total_count, + "recordsFiltered": filtered_count, + "data": data, + } + return jsonify(response_data) + + +@dashboard_bp.route('/', methods=['GET', 'POST']) +@login_required +def dashboard(): + if not Setting().get('pdns_api_url') or not Setting().get( + 'pdns_api_key') or not Setting().get('pdns_version'): + return redirect(url_for('admin_setting_pdns')) + + BG_DOMAIN_UPDATE = Setting().get('bg_domain_updates') + if not BG_DOMAIN_UPDATE: + current_app.logger.info('Updating domains in foreground...') + Domain().update() + else: + current_app.logger.info('Updating domains in background...') + + # Stats for dashboard + domain_count = Domain.query.count() + user_num = User.query.count() + history_number = History.query.count() + history = History.query.order_by(History.created_on.desc()).limit(4) + server = Server(server_id='localhost') + statistics = server.get_statistic() + if statistics: + uptime = list([ + uptime for uptime in statistics if uptime['name'] == 'uptime' + ])[0]['value'] + else: + uptime = 0 + + # Add custom boxes to render_template + return render_template('dashboard.html', + custom_boxes=customBoxes, + domain_count=domain_count, + user_num=user_num, + history_number=history_number, + uptime=uptime, + histories=history, + show_bg_domain_button=BG_DOMAIN_UPDATE) + + +@dashboard_bp.route('/domains-updater', methods=['GET', 'POST']) +@login_required +def domains_updater(): + current_app.logger.debug('Update domains in background') + d = Domain().update() + + response_data = { + "result": d, + } + return jsonify(response_data) diff --git a/powerdnsadmin/routes/domain.py b/powerdnsadmin/routes/domain.py new file mode 100644 index 0000000..84a7a2b --- /dev/null +++ b/powerdnsadmin/routes/domain.py @@ -0,0 +1,525 @@ +import re +import json +import traceback +import datetime +from distutils.version import StrictVersion +from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort, jsonify +from flask_login import login_user, login_required, current_user + +from .base import login_manager +from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec +from ..models.user import User, Anonymous +from ..models.account import Account +from ..models.setting import Setting +from ..models.history import History +from ..models.domain import Domain +from ..models.record import Record +from ..models.record_entry import RecordEntry +from ..models.domain_template import DomainTemplate +from ..models.domain_template_record import DomainTemplateRecord +from ..models.domain_setting import DomainSetting + +domain_bp = Blueprint('domain', + __name__, + template_folder='templates', + url_prefix='/domain') + + +@domain_bp.route('/', methods=['GET']) +@login_required +@can_access_domain +def domain(domain_name): + r = Record() + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + abort(404) + + # query domain info from PowerDNS API + zone_info = r.get_record_data(domain.name) + if zone_info: + jrecords = zone_info['records'] + else: + # can not get any record, API server might be down + abort(500) + + quick_edit = Setting().get('record_quick_edit') + records_allow_to_edit = Setting().get_records_allow_to_edit() + forward_records_allow_to_edit = Setting( + ).get_forward_records_allow_to_edit() + reverse_records_allow_to_edit = Setting( + ).get_reverse_records_allow_to_edit() + ttl_options = Setting().get_ttl_options() + records = [] + + if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'): + for jr in jrecords: + if jr['type'] in records_allow_to_edit: + for subrecord in jr['records']: + record = RecordEntry(name=jr['name'], + type=jr['type'], + status='Disabled' if + subrecord['disabled'] else 'Active', + ttl=jr['ttl'], + data=subrecord['content'], + is_allowed_edit=True) + records.append(record) + if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): + editable_records = forward_records_allow_to_edit + else: + editable_records = reverse_records_allow_to_edit + return render_template('domain.html', + domain=domain, + records=records, + editable_records=editable_records, + quick_edit=quick_edit, + ttl_options=ttl_options) + else: + for jr in jrecords: + if jr['type'] in records_allow_to_edit: + record = RecordEntry( + name=jr['name'], + type=jr['type'], + status='Disabled' if jr['disabled'] else 'Active', + ttl=jr['ttl'], + data=jr['content'], + is_allowed_edit=True) + records.append(record) + if not re.search('ip6\.arpa|in-addr\.arpa$', domain_name): + editable_records = forward_records_allow_to_edit + else: + editable_records = reverse_records_allow_to_edit + return render_template('domain.html', + domain=domain, + records=records, + editable_records=editable_records, + quick_edit=quick_edit, + ttl_options=ttl_options) + + +@domain_bp.route('/add', methods=['GET', 'POST']) +@login_required +@can_create_domain +def add(): + templates = DomainTemplate.query.all() + if request.method == 'POST': + try: + domain_name = request.form.getlist('domain_name')[0] + domain_type = request.form.getlist('radio_type')[0] + domain_template = request.form.getlist('domain_template')[0] + soa_edit_api = request.form.getlist('radio_type_soa_edit_api')[0] + account_id = request.form.getlist('accountid')[0] + + if ' ' in domain_name or not domain_name or not domain_type: + return render_template('errors/400.html', + msg="Please correct your input"), 400 + + if domain_type == 'slave': + if request.form.getlist('domain_master_address'): + domain_master_string = request.form.getlist( + 'domain_master_address')[0] + domain_master_string = domain_master_string.replace( + ' ', '') + domain_master_ips = domain_master_string.split(',') + else: + domain_master_ips = [] + + account_name = Account().get_name_by_id(account_id) + + d = Domain() + result = d.add(domain_name=domain_name, + domain_type=domain_type, + soa_edit_api=soa_edit_api, + domain_master_ips=domain_master_ips, + account_name=account_name) + if result['status'] == 'ok': + history = History(msg='Add domain {0}'.format(domain_name), + detail=str({ + 'domain_type': domain_type, + 'domain_master_ips': domain_master_ips, + 'account_id': account_id + }), + created_by=current_user.username) + history.add() + + # grant user access to the domain + Domain(name=domain_name).grant_privileges( + [current_user.id]) + + # apply template if needed + if domain_template != '0': + template = DomainTemplate.query.filter( + DomainTemplate.id == domain_template).first() + template_records = DomainTemplateRecord.query.filter( + DomainTemplateRecord.template_id == + domain_template).all() + record_data = [] + for template_record in template_records: + record_row = { + 'record_data': template_record.data, + 'record_name': template_record.name, + 'record_status': template_record.status, + 'record_ttl': template_record.ttl, + 'record_type': template_record.type + } + record_data.append(record_row) + r = Record() + result = r.apply(domain_name, record_data) + if result['status'] == 'ok': + history = History( + msg= + 'Applying template {0} to {1}, created records successfully.' + .format(template.name, domain_name), + detail=str(result), + created_by=current_user.username) + history.add() + else: + history = History( + msg= + 'Applying template {0} to {1}, FAILED to created records.' + .format(template.name, domain_name), + detail=str(result), + created_by=current_user.username) + history.add() + return redirect(url_for('dashboard.dashboard')) + else: + return render_template('errors/400.html', + msg=result['msg']), 400 + except Exception as e: + current_app.logger.error('Cannot add domain. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + abort(500) + + else: + accounts = Account.query.all() + return render_template('domain_add.html', + templates=templates, + accounts=accounts) + + +@domain_bp.route('/setting//delete', methods=['POST']) +@login_required +@operator_role_required +def delete(domain_name): + d = Domain() + result = d.delete(domain_name) + + if result['status'] == 'error': + abort(500) + + history = History(msg='Delete domain {0}'.format(domain_name), + created_by=current_user.username) + history.add() + + return redirect(url_for('dashboard.dashboard')) + + +@domain_bp.route('/setting//manage', methods=['GET', 'POST']) +@login_required +@operator_role_required +def setting(domain_name): + if request.method == 'GET': + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + abort(404) + users = User.query.all() + accounts = Account.query.all() + + # get list of user ids to initialize selection data + d = Domain(name=domain_name) + domain_user_ids = d.get_user() + account = d.get_account() + + return render_template('domain_setting.html', + domain=domain, + users=users, + domain_user_ids=domain_user_ids, + accounts=accounts, + domain_account=account) + + if request.method == 'POST': + # username in right column + new_user_list = request.form.getlist('domain_multi_user[]') + new_user_ids = [ + user.id for user in User.query.filter( + User.username.in_(new_user_list)).all() if user + ] + + # grant/revoke user privileges + d = Domain(name=domain_name) + d.grant_privileges(new_user_ids) + + history = History( + msg='Change domain {0} access control'.format(domain_name), + detail=str({'user_has_access': new_user_list}), + created_by=current_user.username) + history.add() + + return redirect(url_for('domain.setting', domain_name=domain_name)) + + +@domain_bp.route('/setting//change_soa_setting', + methods=['POST']) +@login_required +@operator_role_required +def change_soa_edit_api(domain_name): + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + abort(404) + new_setting = request.form.get('soa_edit_api') + if new_setting is None: + abort(500) + if new_setting == '0': + return redirect(url_for('domain.setting', domain_name=domain_name)) + + d = Domain() + status = d.update_soa_setting(domain_name=domain_name, + soa_edit_api=new_setting) + if status['status'] != None: + users = User.query.all() + accounts = Account.query.all() + d = Domain(name=domain_name) + domain_user_ids = d.get_user() + account = d.get_account() + return render_template('domain_setting.html', + domain=domain, + users=users, + domain_user_ids=domain_user_ids, + accounts=accounts, + domain_account=account) + else: + abort(500) + + +@domain_bp.route('/setting//change_account', + methods=['POST']) +@login_required +@operator_role_required +def change_account(domain_name): + domain = Domain.query.filter(Domain.name == domain_name).first() + if not domain: + abort(404) + + account_id = request.form.get('accountid') + status = Domain(name=domain.name).assoc_account(account_id) + if status['status']: + return redirect(url_for('domain.setting', domain_name=domain.name)) + else: + abort(500) + + +@domain_bp.route('//apply', + methods=['POST'], + strict_slashes=False) +@login_required +@can_access_domain +def record_apply(domain_name): + #TODO: filter removed records / name modified records. + + try: + jdata = request.json + submitted_serial = jdata['serial'] + submitted_record = jdata['record'] + domain = Domain.query.filter(Domain.name == domain_name).first() + current_app.logger.debug( + 'Your submitted serial: {0}'.format(submitted_serial)) + current_app.logger.debug('Current domain serial: {0}'.format( + domain.serial)) + + if domain: + if int(submitted_serial) != domain.serial: + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'The zone has been changed by another session or user. Please refresh this web page to load updated records.' + }), 500) + else: + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'Domain name {0} does not exist'.format(domain_name) + }), 404) + + r = Record() + result = r.apply(domain_name, submitted_record) + if result['status'] == 'ok': + jdata.pop('_csrf_token', + None) # don't store csrf token in the history. + history = History( + msg='Apply record changes to domain {0}'.format(domain_name), + detail=str(json.dumps(jdata)), + created_by=current_user.username) + history.add() + return make_response(jsonify(result), 200) + else: + return make_response(jsonify(result), 400) + except Exception as e: + current_app.logger.error( + 'Cannot apply record changes. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Error when applying new changes' + }), 500) + + +@domain_bp.route('//update', + methods=['POST'], + strict_slashes=False) +@login_required +@can_access_domain +def record_update(domain_name): + """ + This route is used for domain work as Slave Zone only + Pulling the records update from its Master + """ + try: + jdata = request.json + + domain_name = jdata['domain'] + d = Domain() + result = d.update_from_master(domain_name) + if result['status'] == 'ok': + return make_response( + jsonify({ + 'status': 'ok', + 'msg': result['msg'] + }), 200) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': result['msg'] + }), 500) + except Exception as e: + current_app.logger.error('Cannot update record. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Error when applying new changes' + }), 500) + + +@domain_bp.route('//info', methods=['GET']) +@login_required +@can_access_domain +def info(domain_name): + domain = Domain() + domain_info = domain.get_domain_info(domain_name) + return make_response(jsonify(domain_info), 200) + + +@domain_bp.route('//dnssec', methods=['GET']) +@login_required +@can_access_domain +def dnssec(domain_name): + domain = Domain() + dnssec = domain.get_domain_dnssec(domain_name) + return make_response(jsonify(dnssec), 200) + + +@domain_bp.route('//dnssec/enable', methods=['POST']) +@login_required +@can_access_domain +@can_configure_dnssec +def dnssec_enable(domain_name): + domain = Domain() + dnssec = domain.enable_domain_dnssec(domain_name) + return make_response(jsonify(dnssec), 200) + + +@domain_bp.route('//dnssec/disable', methods=['POST']) +@login_required +@can_access_domain +@can_configure_dnssec +def dnssec_disable(domain_name): + domain = Domain() + dnssec = domain.get_domain_dnssec(domain_name) + + for key in dnssec['dnssec']: + domain.delete_dnssec_key(domain_name, key['id']) + + return make_response(jsonify({'status': 'ok', 'msg': 'DNSSEC removed.'})) + + +@domain_bp.route('//manage-setting', methods=['GET', 'POST']) +@login_required +@operator_role_required +def admin_setdomainsetting(domain_name): + if request.method == 'POST': + # + # post data should in format + # {'action': 'set_setting', 'setting': 'default_action, 'value': 'True'} + # + try: + jdata = request.json + data = jdata['data'] + + if jdata['action'] == 'set_setting': + new_setting = data['setting'] + new_value = str(data['value']) + domain = Domain.query.filter( + Domain.name == domain_name).first() + setting = DomainSetting.query.filter( + DomainSetting.domain == domain).filter( + DomainSetting.setting == new_setting).first() + + if setting: + if setting.set(new_value): + history = History( + msg='Setting {0} changed value to {1} for {2}'. + format(new_setting, new_value, domain.name), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'Setting updated.' + })) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Unable to set value of setting.' + })) + else: + if domain.add_setting(new_setting, new_value): + history = History( + msg= + 'New setting {0} with value {1} for {2} has been created' + .format(new_setting, new_value, domain.name), + created_by=current_user.username) + history.add() + return make_response( + jsonify({ + 'status': 'ok', + 'msg': 'New setting created and updated.' + })) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Unable to create new setting.' + })) + else: + return make_response( + jsonify({ + 'status': 'error', + 'msg': 'Action not supported.' + }), 400) + except Exception as e: + current_app.logger.error( + 'Cannot change domain setting. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'There is something wrong, please contact Administrator.' + }), 400) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py new file mode 100644 index 0000000..71fc6c7 --- /dev/null +++ b/powerdnsadmin/routes/index.py @@ -0,0 +1,686 @@ +import os +import json +import traceback +import datetime +import ipaddress +from distutils.util import strtobool +from yaml import Loader, load +from onelogin.saml2.utils import OneLogin_Saml2_Utils +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 login_manager +from ..lib import utils +from ..decorators import dyndns_login_required +from ..models.base import db +from ..models.user import User, Anonymous +from ..models.role import Role +from ..models.account import Account +from ..models.account_user import AccountUser +from ..models.domain import Domain +from ..models.domain_user import DomainUser +from ..models.domain_setting import DomainSetting +from ..models.record import Record +from ..models.setting import Setting +from ..models.history import History +from ..services.google import google_oauth +from ..services.github import github_oauth +from ..services.oidc import oidc_oauth + +google = None +github = None +oidc = None + +index_bp = Blueprint('index', + __name__, + template_folder='templates', + url_prefix='/') + + +@index_bp.before_app_first_request +def register_modules(): + global google + global github + global oidc + google = google_oauth() + github = github_oauth() + oidc = oidc_oauth() + + +@index_bp.before_request +def before_request(): + # Check if user is anonymous + g.user = current_user + login_manager.anonymous_user = Anonymous + + # Check site is in maintenance mode + maintenance = Setting().get('maintenance') + if maintenance and current_user.is_authenticated and current_user.role.name not in [ + 'Administrator', 'Operator' + ]: + return render_template('maintenance.html') + + # Manage session timeout + session.permanent = True + current_app.permanent_session_lifetime = datetime.timedelta( + minutes=int(Setting().get('session_timeout'))) + session.modified = True + + +@index_bp.route('/', methods=['GET']) +@login_required +def index(): + return redirect(url_for('dashboard.dashboard')) + + +@index_bp.route('/google/login') +def google_login(): + if not Setting().get('google_oauth_enabled') or google is None: + current_app.logger.error( + 'Google OAuth is disabled or you have not yet reloaded the pda application after enabling.' + ) + abort(400) + else: + redirect_uri = url_for('google_authorized', _external=True) + return google.authorize_redirect(redirect_uri) + + +@index_bp.route('/github/login') +def github_login(): + if not Setting().get('github_oauth_enabled') or github is None: + current_app.logger.error( + 'Github OAuth is disabled or you have not yet reloaded the pda application after enabling.' + ) + abort(400) + else: + redirect_uri = url_for('github_authorized', _external=True) + return github.authorize_redirect(redirect_uri) + + +@index_bp.route('/oidc/login') +def oidc_login(): + if not Setting().get('oidc_oauth_enabled') or oidc is None: + current_app.logger.error( + 'OIDC OAuth is disabled or you have not yet reloaded the pda application after enabling.' + ) + abort(400) + else: + redirect_uri = url_for('oidc_authorized', _external=True) + return oidc.authorize_redirect(redirect_uri) + + +@index_bp.route('/login', methods=['GET', 'POST']) +def login(): + SAML_ENABLED = current_app.config.get('SAML_ENABLED') + + if g.user is not None and current_user.is_authenticated: + return redirect(url_for('dashboard.dashboard')) + + if 'google_token' in session: + user_data = json.loads(google.get('userinfo').text) + first_name = user_data['given_name'] + surname = user_data['family_name'] + email = user_data['email'] + user = User.query.filter_by(username=email).first() + if user is None: + user = User.query.filter_by(email=email).first() + if not user: + user = User(username=email, + firstname=first_name, + lastname=surname, + plain_text_password=None, + email=email) + + result = user.create_local_user() + if not result['status']: + session.pop('google_token', None) + return redirect(url_for('index.login')) + + session['user_id'] = user.id + login_user(user, remember=False) + session['authentication_type'] = 'OAuth' + return redirect(url_for('index.index')) + + if 'github_token' in session: + me = json.loads(github.get('user').text) + github_username = me['login'] + github_name = me['name'] + github_email = me['email'] + + user = User.query.filter_by(username=github_username).first() + if user is None: + user = User.query.filter_by(email=github_email).first() + if not user: + user = User(username=github_username, + plain_text_password=None, + firstname=github_name, + lastname='', + email=github_email) + + result = user.create_local_user() + if not result['status']: + session.pop('github_token', None) + return redirect(url_for('index.login')) + + session['user_id'] = user.id + session['authentication_type'] = 'OAuth' + login_user(user, remember=False) + return redirect(url_for('index.index')) + + if 'oidc_token' in session: + me = json.loads(oidc.get('userinfo').text) + oidc_username = me["preferred_username"] + oidc_givenname = me["name"] + oidc_familyname = "" + oidc_email = me["email"] + + user = User.query.filter_by(username=oidc_username).first() + if not user: + user = User(username=oidc_username, + plain_text_password=None, + firstname=oidc_givenname, + lastname=oidc_familyname, + email=oidc_email) + + result = user.create_local_user() + if not result['status']: + session.pop('oidc_token', None) + return redirect(url_for('index.login')) + + session['user_id'] = user.id + session['authentication_type'] = 'OAuth' + login_user(user, remember=False) + return redirect(url_for('index.index')) + + if request.method == 'GET': + return render_template('login.html', saml_enabled=SAML_ENABLED) + elif request.method == 'POST': + # process Local-DB authentication + username = request.form['username'] + password = request.form['password'] + otp_token = request.form.get('otptoken') + auth_method = request.form.get('auth_method', 'LOCAL') + session[ + 'authentication_type'] = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL' + remember_me = True if 'remember' in request.form else False + + user = User(username=username, + password=password, + plain_text_password=password) + + try: + auth = user.is_validate(method=auth_method, + src_ip=request.remote_addr) + if auth == False: + return render_template('login.html', + saml_enabled=SAML_ENABLED, + error='Invalid credentials') + except Exception as e: + current_app.logger.error( + "Cannot authenticate user. Error: {}".format(e)) + current_app.logger.debug(traceback.format_exc()) + return render_template('login.html', + saml_enabled=SAML_ENABLED, + error=e) + + # check if user enabled OPT authentication + if user.otp_secret: + if otp_token and otp_token.isdigit(): + good_token = user.verify_totp(otp_token) + if not good_token: + return render_template('login.html', + saml_enabled=SAML_ENABLED, + error='Invalid credentials') + else: + return render_template('login.html', + saml_enabled=SAML_ENABLED, + error='Token required') + + login_user(user, remember=remember_me) + return redirect(session.get('next', url_for('index.index'))) + + +def clear_session(): + session.pop('user_id', None) + session.pop('github_token', None) + session.pop('google_token', None) + session.pop('authentication_type', None) + session.clear() + logout_user() + + +@index_bp.route('/logout') +def logout(): + if current_app.config.get( + 'SAML_ENABLED' + ) and 'samlSessionIndex' in session and current_app.config.get( + 'SAML_LOGOUT'): + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + if current_app.config.get('SAML_LOGOUT_URL'): + return redirect( + auth.logout( + name_id_format= + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + return_to=current_app.config.get('SAML_LOGOUT_URL'), + session_index=session['samlSessionIndex'], + name_id=session['samlNameId'])) + return redirect( + auth.logout( + name_id_format= + "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + session_index=session['samlSessionIndex'], + name_id=session['samlNameId'])) + clear_session() + return redirect(url_for('index.login')) + + +@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['username'] + password = request.form['password'] + firstname = request.form.get('firstname') + lastname = request.form.get('lastname') + email = request.form.get('email') + rpassword = request.form.get('rpassword') + + if not username or not password or not email: + return render_template( + 'register.html', error='Please input required information') + + if password != rpassword: + return render_template( + 'register.html', + error="Password confirmation does not match") + + 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']: + 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) + else: + return render_template('errors/404.html'), 404 + + +@index_bp.route('/nic/checkip.html', methods=['GET', 'POST']) +def dyndns_checkip(): + # This route covers the default ddclient 'web' setting for the checkip service + return render_template('dyndns.html', + response=request.environ.get( + 'HTTP_X_REAL_IP', request.remote_addr)) + + +@index_bp.route('/nic/update', methods=['GET', 'POST']) +@dyndns_login_required +def dyndns_update(): + # dyndns protocol response codes in use are: + # good: update successful + # nochg: IP address already set to update address + # nohost: hostname does not exist for this user account + # 911: server error + # have to use 200 HTTP return codes because ddclient does not read the return string if the code is other than 200 + # reference: https://help.dyn.com/remote-access-api/perform-update/ + # reference: https://help.dyn.com/remote-access-api/return-codes/ + hostname = request.args.get('hostname') + myip = request.args.get('myip') + + if not hostname: + history = History(msg="DynDNS update: missing hostname parameter", + created_by=current_user.username) + history.add() + return render_template('dyndns.html', response='nohost'), 200 + + try: + if current_user.role.name in ['Administrator', 'Operator']: + domains = Domain.query.all() + else: + # Get query for domain to which the user has access permission. + # This includes direct domain permission AND permission through + # account membership + domains = db.session.query(Domain) \ + .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ + .outerjoin(Account, Domain.account_id == Account.id) \ + .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ + .filter( + db.or_( + DomainUser.user_id == current_user.id, + AccountUser.user_id == current_user.id + )).all() + except Exception as e: + current_app.logger.error('DynDNS Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + return render_template('dyndns.html', response='911'), 200 + + domain = None + domain_segments = hostname.split('.') + for index in range(len(domain_segments)): + full_domain = '.'.join(domain_segments) + potential_domain = Domain.query.filter( + Domain.name == full_domain).first() + if potential_domain in domains: + domain = potential_domain + break + domain_segments.pop(0) + + if not domain: + history = History( + msg= + "DynDNS update: attempted update of {0} but it does not exist for this user" + .format(hostname), + created_by=current_user.username) + history.add() + return render_template('dyndns.html', response='nohost'), 200 + + myip_addr = [] + if myip: + for address in myip.split(','): + myip_addr += utils.validate_ipaddress(address) + + remote_addr = utils.validate_ipaddress( + request.headers.get('X-Forwarded-For', + request.remote_addr).split(', ')[:1]) + + response = 'nochg' + for ip in myip_addr or remote_addr: + if isinstance(ip, ipaddress.IPv4Address): + rtype = 'A' + else: + rtype = 'AAAA' + + r = Record(name=hostname, type=rtype) + # Check if the user requested record exists within this domain + if r.exists(domain.name) and r.is_allowed_edit(): + if r.data == str(ip): + # Record content did not change, return 'nochg' + history = History( + msg= + "DynDNS update: attempted update of {0} but record did not change" + .format(hostname), + created_by=current_user.username) + history.add() + else: + oldip = r.data + result = r.update(domain.name, str(ip)) + if result['status'] == 'ok': + history = History( + msg= + 'DynDNS update: updated {0} record {1} in zone {2}, it changed from {3} to {4}' + .format(rtype, hostname, domain.name, oldip, str(ip)), + detail=str(result), + created_by=current_user.username) + history.add() + response = 'good' + else: + response = '911' + break + elif r.is_allowed_edit(): + ondemand_creation = DomainSetting.query.filter( + DomainSetting.domain == domain).filter( + DomainSetting.setting == 'create_via_dyndns').first() + if (ondemand_creation is not None) and (strtobool( + ondemand_creation.value) == True): + record = Record(name=hostname, + type=rtype, + data=str(ip), + status=False, + ttl=3600) + result = record.add(domain.name) + if result['status'] == 'ok': + history = History( + msg= + 'DynDNS update: created record {0} in zone {1}, it now represents {2}' + .format(hostname, domain.name, str(ip)), + detail=str(result), + created_by=current_user.username) + history.add() + response = 'good' + else: + history = History( + msg= + 'DynDNS update: attempted update of {0} but it does not exist for this user' + .format(hostname), + created_by=current_user.username) + history.add() + + return render_template('dyndns.html', response=response), 200 + + +### START SAML AUTHENTICATION ### +@index_bp.route('/saml/login') +def saml_login(): + if not current_app.config.get('SAML_ENABLED'): + abort(400) + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for( + 'saml_authorized') + return redirect(auth.login(return_to=redirect_url)) + + +@index_bp.route('/saml/metadata') +def saml_metadata(): + if not current_app.config.get('SAML_ENABLED'): + current_app.logger.error("SAML authentication is disabled.") + abort(400) + + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + settings = auth.get_settings() + metadata = settings.get_sp_metadata() + errors = settings.validate_metadata(metadata) + + if len(errors) == 0: + resp = make_response(metadata, 200) + resp.headers['Content-Type'] = 'text/xml' + else: + resp = make_response(errors.join(', '), 500) + return resp + + +@index_bp.route('/saml/authorized', methods=['GET', 'POST']) +def saml_authorized(): + errors = [] + if not current_app.config.get('SAML_ENABLED'): + current_app.logger.error("SAML authentication is disabled.") + abort(400) + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + auth.process_response() + errors = auth.get_errors() + if len(errors) == 0: + session['samlUserdata'] = auth.get_attributes() + session['samlNameId'] = auth.get_nameid() + session['samlSessionIndex'] = auth.get_session_index() + self_url = OneLogin_Saml2_Utils.get_self_url(req) + self_url = self_url + req['script_name'] + if 'RelayState' in request.form and self_url != request.form[ + 'RelayState']: + return redirect(auth.redirect_to(request.form['RelayState'])) + if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False): + username = session['samlUserdata'][ + current_app.config['SAML_ATTRIBUTE_USERNAME']][0].lower() + else: + username = session['samlNameId'].lower() + user = User.query.filter_by(username=username).first() + if not user: + # create user + user = User(username=username, + plain_text_password=None, + email=session['samlNameId']) + user.create_local_user() + session['user_id'] = user.id + email_attribute_name = current_app.config.get('SAML_ATTRIBUTE_EMAIL', + 'email') + givenname_attribute_name = current_app.config.get( + 'SAML_ATTRIBUTE_GIVENNAME', 'givenname') + surname_attribute_name = current_app.config.get( + 'SAML_ATTRIBUTE_SURNAME', 'surname') + name_attribute_name = current_app.config.get('SAML_ATTRIBUTE_NAME', + None) + account_attribute_name = current_app.config.get( + 'SAML_ATTRIBUTE_ACCOUNT', None) + admin_attribute_name = current_app.config.get('SAML_ATTRIBUTE_ADMIN', + None) + group_attribute_name = current_app.config.get('SAML_ATTRIBUTE_GROUP', + None) + admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME', + None) + group_to_account_mapping = create_group_to_account_mapping() + + if email_attribute_name in session['samlUserdata']: + user.email = session['samlUserdata'][email_attribute_name][ + 0].lower() + if givenname_attribute_name in session['samlUserdata']: + user.firstname = session['samlUserdata'][givenname_attribute_name][ + 0] + if surname_attribute_name in session['samlUserdata']: + user.lastname = session['samlUserdata'][surname_attribute_name][0] + if name_attribute_name in session['samlUserdata']: + name = session['samlUserdata'][name_attribute_name][0].split(' ') + user.firstname = name[0] + user.lastname = ' '.join(name[1:]) + + if group_attribute_name: + user_groups = session['samlUserdata'].get(group_attribute_name, []) + else: + user_groups = [] + if admin_attribute_name or group_attribute_name: + user_accounts = set(user.get_account()) + saml_accounts = [] + for group_mapping in group_to_account_mapping: + mapping = group_mapping.split('=') + group = mapping[0] + account_name = mapping[1] + + if group in user_groups: + account = handle_account(account_name) + saml_accounts.append(account) + + for account_name in session['samlUserdata'].get( + account_attribute_name, []): + account = handle_account(account_name) + saml_accounts.append(account) + saml_accounts = set(saml_accounts) + for account in saml_accounts - user_accounts: + account.add_user(user) + history = History(msg='Adding {0} to account {1}'.format( + user.username, account.name), + created_by='SAML Assertion') + history.add() + for account in user_accounts - saml_accounts: + account.remove_user(user) + history = History(msg='Removing {0} from account {1}'.format( + user.username, account.name), + created_by='SAML Assertion') + history.add() + if admin_attribute_name and 'true' in session['samlUserdata'].get( + admin_attribute_name, []): + uplift_to_admin(user) + elif admin_group_name in user_groups: + uplift_to_admin(user) + elif admin_attribute_name or group_attribute_name: + if user.role.name != 'User': + user.role_id = Role.query.filter_by(name='User').first().id + history = History(msg='Demoting {0} to user'.format( + user.username), + created_by='SAML Assertion') + history.add() + user.plain_text_password = None + user.update_profile() + session['authentication_type'] = 'SAML' + login_user(user, remember=False) + return redirect(url_for('index')) + else: + return render_template('errors/SAML.html', errors=errors) + + +def create_group_to_account_mapping(): + group_to_account_mapping_string = current_app.config.get( + 'SAML_GROUP_TO_ACCOUNT_MAPPING', None) + if group_to_account_mapping_string and len( + group_to_account_mapping_string.strip()) > 0: + group_to_account_mapping = group_to_account_mapping_string.split(',') + else: + group_to_account_mapping = [] + return group_to_account_mapping + + +def handle_account(account_name): + clean_name = ''.join(c for c in account_name.lower() + if c in "abcdefghijklmnopqrstuvwxyz0123456789") + if len(clean_name) > Account.name.type.length: + logging.error( + "Account name {0} too long. Truncated.".format(clean_name)) + account = Account.query.filter_by(name=clean_name).first() + if not account: + account = Account(name=clean_name.lower(), + description='', + contact='', + mail='') + account.create_account() + history = History(msg='Account {0} created'.format(account.name), + created_by='SAML Assertion') + history.add() + return account + + +def uplift_to_admin(user): + if user.role.name != 'Administrator': + user.role_id = Role.query.filter_by(name='Administrator').first().id + history = History(msg='Promoting {0} to administrator'.format( + user.username), + created_by='SAML Assertion') + history.add() + + +@index_bp.route('/saml/sls') +def saml_logout(): + req = utils.prepare_flask_request(request) + auth = utils.init_saml_auth(req) + url = auth.process_slo() + errors = auth.get_errors() + if len(errors) == 0: + clear_session() + if url is not None: + return redirect(url) + elif current_app.config.get('SAML_LOGOUT_URL') is not None: + return redirect(current_app.config.get('SAML_LOGOUT_URL')) + else: + return redirect(url_for('login')) + else: + return render_template('errors/SAML.html', errors=errors) + + +### END SAML AUTHENTICATION ### + + +@index_bp.route('/swagger', methods=['GET']) +def swagger_spec(): + try: + spec_path = os.path.join(current_app.root_path, "swagger-spec.yaml") + spec = open(spec_path, 'r') + loaded_spec = load(spec.read(), Loader) + except Exception as e: + current_app.logger.error( + 'Cannot view swagger spec. Error: {0}'.format(e)) + current_app.logger.debug(traceback.format_exc()) + abort(500) + + resp = make_response(json.dumps(loaded_spec), 200) + resp.headers['Content-Type'] = 'application/json' + + return resp diff --git a/powerdnsadmin/routes/user.py b/powerdnsadmin/routes/user.py new file mode 100644 index 0000000..6d3d060 --- /dev/null +++ b/powerdnsadmin/routes/user.py @@ -0,0 +1,89 @@ +import qrcode as qrc +import qrcode.image.svg as qrc_svg +from io import BytesIO +from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, current_app, session, g +from flask_login import current_user, login_user, logout_user, login_required + +from .base import login_manager +from ..models.user import User +from ..models.role import Role + +user_bp = Blueprint('user', + __name__, + template_folder='templates', + url_prefix='/user') + + +@user_bp.route('/profile', methods=['GET', 'POST']) +@login_required +def profile(): + if request.method == 'GET': + return render_template('user_profile.html') + if request.method == 'POST': + if session['authentication_type'] == 'LOCAL': + firstname = request.form[ + 'firstname'] if 'firstname' in request.form else '' + lastname = request.form[ + 'lastname'] if 'lastname' in request.form else '' + email = request.form['email'] if 'email' in request.form else '' + new_password = request.form[ + 'password'] if 'password' in request.form else '' + else: + firstname = lastname = email = new_password = '' + logging.warning( + 'Authenticated externally. User {0} information will not allowed to update the profile' + .format(current_user.username)) + + if request.data: + jdata = request.json + data = jdata['data'] + if jdata['action'] == 'enable_otp': + if session['authentication_type'] in ['LOCAL', 'LDAP']: + enable_otp = data['enable_otp'] + user = User(username=current_user.username) + user.update_profile(enable_otp=enable_otp) + return make_response( + jsonify({ + 'status': + 'ok', + 'msg': + 'Change OTP Authentication successfully. Status: {0}' + .format(enable_otp) + }), 200) + else: + return make_response( + jsonify({ + 'status': + 'error', + 'msg': + 'User {0} is externally. You are not allowed to update the OTP' + .format(current_user.username) + }), 400) + + user = User(username=current_user.username, + plain_text_password=new_password, + firstname=firstname, + lastname=lastname, + email=email, + reload_info=False) + user.update_profile() + + return render_template('user_profile.html') + + +@user_bp.route('/qrcode') +@login_required +def qrcode(): + if not current_user: + return redirect(url_for('index')) + + img = qrc.make(current_user.get_totp_uri(), + image_factory=qrc_svg.SvgPathImage) + stream = BytesIO() + img.save(stream) + return stream.getvalue(), 200, { + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } diff --git a/powerdnsadmin/services/__init__.py b/powerdnsadmin/services/__init__.py new file mode 100644 index 0000000..203b4ed --- /dev/null +++ b/powerdnsadmin/services/__init__.py @@ -0,0 +1,4 @@ +from .base import authlib_oauth_client + +def init_app(app): + authlib_oauth_client.init_app(app) \ No newline at end of file diff --git a/powerdnsadmin/services/base.py b/powerdnsadmin/services/base.py new file mode 100644 index 0000000..2fffdbc --- /dev/null +++ b/powerdnsadmin/services/base.py @@ -0,0 +1,3 @@ +from authlib.flask.client import OAuth + +authlib_oauth_client = OAuth() \ No newline at end of file diff --git a/powerdnsadmin/services/github.py b/powerdnsadmin/services/github.py new file mode 100644 index 0000000..fcfd172 --- /dev/null +++ b/powerdnsadmin/services/github.py @@ -0,0 +1,42 @@ +from flask import request, session, redirect, url_for, current_app + +from .base import authlib_oauth_client +from ..models.setting import Setting + + +def github_oauth(): + if not Setting().get('github_oauth_enabled'): + return None + + def fetch_github_token(): + return session.get('github_token') + + def update_token(token): + session['google_token'] = token + return token + + github = authlib_oauth_client.register( + 'github', + client_id=Setting().get('github_oauth_key'), + client_secret=Setting().get('github_oauth_secret'), + request_token_params={'scope': Setting().get('github_oauth_scope')}, + api_base_url=Setting().get('github_oauth_api_url'), + request_token_url=None, + access_token_url=Setting().get('github_oauth_token_url'), + authorize_url=Setting().get('github_oauth_authorize_url'), + client_kwargs={'scope': Setting().get('github_oauth_scope')}, + fetch_token=fetch_github_token, + update_token=update_token) + + @current_app.route('/github/authorized') + def github_authorized(): + session['github_oauthredir'] = url_for('.github_authorized', + _external=True) + token = github.authorize_access_token() + if token is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error'], request.args['error_description']) + session['github_token'] = (token) + return redirect(url_for('index.login')) + + return github diff --git a/powerdnsadmin/services/google.py b/powerdnsadmin/services/google.py new file mode 100644 index 0000000..68775a2 --- /dev/null +++ b/powerdnsadmin/services/google.py @@ -0,0 +1,44 @@ +from flask import request, session, redirect, url_for, current_app + +from .base import authlib_oauth_client +from ..models.setting import Setting + + +def google_oauth(): + if not Setting().get('google_oauth_enabled'): + return None + + def fetch_google_token(): + return session.get('google_token') + + def update_token(token): + session['google_token'] = token + return token + + google = authlib_oauth_client.register( + 'google', + client_id=Setting().get('google_oauth_client_id'), + client_secret=Setting().get('google_oauth_client_secret'), + api_base_url=Setting().get('google_base_url'), + request_token_url=None, + access_token_url=Setting().get('google_token_url'), + authorize_url=Setting().get('google_authorize_url'), + client_kwargs={'scope': Setting().get('google_oauth_scope')}, + fetch_token=fetch_google_token, + update_token=update_token) + + @current_app.route('/google/authorized') + def google_authorized(): + session['google_oauthredir'] = url_for( + '.google_authorized', _external=True) + token = google.authorize_access_token() + if token is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error_reason'], + request.args['error_description'] + ) + session['google_token'] = (token) + return redirect(url_for('index.login')) + + return google + diff --git a/powerdnsadmin/services/oidc.py b/powerdnsadmin/services/oidc.py new file mode 100644 index 0000000..b298469 --- /dev/null +++ b/powerdnsadmin/services/oidc.py @@ -0,0 +1,41 @@ +from flask import request, session, redirect, url_for, current_app + +from .base import authlib_oauth_client +from ..models.setting import Setting + + +def oidc_oauth(): + if not Setting().get('oidc_oauth_enabled'): + return None + + def fetch_oidc_token(): + return session.get('oidc_token') + + def update_token(token): + session['google_token'] = token + return token + + oidc = authlib_oauth_client.register( + 'oidc', + client_id=Setting().get('oidc_oauth_key'), + client_secret=Setting().get('oidc_oauth_secret'), + api_base_url=Setting().get('oidc_oauth_api_url'), + request_token_url=None, + access_token_url=Setting().get('oidc_oauth_token_url'), + authorize_url=Setting().get('oidc_oauth_authorize_url'), + client_kwargs={'scope': Setting().get('oidc_oauth_scope')}, + fetch_token=fetch_oidc_token, + update_token=update_token) + + @current_app.route('/oidc/authorized') + def oidc_authorized(): + session['oidc_oauthredir'] = url_for('.oidc_authorized', + _external=True) + token = oidc.authorize_access_token() + if token is None: + return 'Access denied: reason=%s error=%s' % ( + request.args['error'], request.args['error_description']) + session['oidc_token'] = (token) + return redirect(url_for('index.login')) + + return oidc \ No newline at end of file diff --git a/app/static/custom/css/custom.css b/powerdnsadmin/static/custom/css/custom.css similarity index 100% rename from app/static/custom/css/custom.css rename to powerdnsadmin/static/custom/css/custom.css diff --git a/app/static/custom/js/custom.js b/powerdnsadmin/static/custom/js/custom.js similarity index 100% rename from app/static/custom/js/custom.js rename to powerdnsadmin/static/custom/js/custom.js diff --git a/powerdnsadmin/static/favicon.ico b/powerdnsadmin/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..6eeaa2a0d393190ce748107222d9a026f992e4a7 GIT binary patch literal 894 zcmZQzU<5(|0R|u`!H~hsz#zuJz@P!dKp_SNAO?xUfG{@$0|>*weY}F;x;`B~@Mg`jC-vo5%k$%{OoKy1zkdA+(K2o3 zEDKGQQ~gcX+N!TtEj1|9e2tL(rheJva*5dKY#gZsIRxM zB;!h75Wi?9l5?>z>S-`t`=OAFp5C?(007k>qM(bnVuyja#;c*qL9N z(Q%=@`f7d2?T&^wySIHjy#MKh?rZrO7i!Bt-@f(!@WDrwB`uD&)%Eqcd3o(gVV7ri zp6ji@(BF7(+uE0x&)(R!?s#YIg_7JW1=-KHZv1@s(DSxD9<^am-%+f#!uU}z9=j>5*%b{WE2`20#tutPWS1# zz4s39d~xOMwaqJzOl{rMU%#oo=xj&xyY1WF&71o?Hs-LOkAD^4SRbV?yXz(e#efNwKXpT1J^s+yE;1`KXoe5&f?1A$tYso}un=1c3AzCL>B&D=RpqN3i1hL&lnclP!D`u*G0#bsJv!oAH) z-rc|T`^Wdkrw?7(xb)_p%`Y#WdAn`Pvz+W#0Rbo6-Ha3zp1gPgjDkZ)k6Gz!@9D0( zG^hL4?)Be4zWep#`{$>RUhUoaq@wgyfd9*oU{6tz+SXQhl3284sj;T&{L=I*bGxt4 z@4vfX%A<*$FN^a&MMa!.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:"Glyphicons Halflings";src:url("../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.eot");src:url("../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.eot#iefix") format("embedded-opentype"),url("../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2") format("woff2"),url("../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.woff") format("woff"),url("../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf") format("truetype"),url("../node_modules/bootstrap/dist/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular") format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:"Glyphicons Halflings";font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover,a.text-primary:focus{color:#286090}.text-success{color:#3c763d}a.text-success:hover,a.text-success:focus{color:#2b542c}.text-info{color:#31708f}a.text-info:hover,a.text-info:focus{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover,a.text-warning:focus{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover,a.text-danger:focus{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover,a.bg-primary:focus{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover,a.bg-success:focus{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover,a.bg-info:focus{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover,a.bg-warning:focus{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover,a.bg-danger:focus{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:"\2014 \00A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:""}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:"\00A0 \2014"}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.row-no-gutters{margin-right:0;margin-left:0}.row-no-gutters [class*="col-"]{padding-right:0;padding-left:0}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{position:static;display:table-cell;float:none}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none;-moz-appearance:none;appearance:none}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"].disabled,input[type="checkbox"].disabled,fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"]{cursor:not-allowed}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,0.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{line-height:34px}input[type="date"].input-sm,input[type="time"].input-sm,input[type="datetime-local"].input-sm,input[type="month"].input-sm,.input-group-sm input[type="date"],.input-group-sm input[type="time"],.input-group-sm input[type="datetime-local"],.input-group-sm input[type="month"]{line-height:30px}input[type="date"].input-lg,input[type="time"].input-lg,input[type="datetime-local"].input-lg,input[type="month"].input-lg,.input-group-lg input[type="date"],.input-group-lg input[type="time"],.input-group-lg input[type="datetime-local"],.input-group-lg input[type="month"]{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm textarea.form-control,.form-group-sm select[multiple].form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg textarea.form-control,.form-group-lg select[multiple].form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback,.input-group-lg+.form-control-feedback,.form-group-lg .form-control+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback,.input-group-sm+.form-control-feedback,.form-group-sm .form-control+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label ~ .form-control-feedback{top:25px}.has-feedback label.sr-only ~ .form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media(min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media(min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);opacity:.65;-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:focus,.btn-default.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-default:active:hover,.btn-default.active:hover,.open>.dropdown-toggle.btn-default:hover,.btn-default:active:focus,.btn-default.active:focus,.open>.dropdown-toggle.btn-default:focus,.btn-default:active.focus,.btn-default.active.focus,.open>.dropdown-toggle.btn-default.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;background-image:none;border-color:#204d74}.btn-primary:active:hover,.btn-primary.active:hover,.open>.dropdown-toggle.btn-primary:hover,.btn-primary:active:focus,.btn-primary.active:focus,.open>.dropdown-toggle.btn-primary:focus,.btn-primary:active.focus,.btn-primary.active.focus,.open>.dropdown-toggle.btn-primary.focus{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;background-image:none;border-color:#398439}.btn-success:active:hover,.btn-success.active:hover,.open>.dropdown-toggle.btn-success:hover,.btn-success:active:focus,.btn-success.active:focus,.open>.dropdown-toggle.btn-success:focus,.btn-success:active.focus,.btn-success.active.focus,.open>.dropdown-toggle.btn-success.focus{color:#fff;background-color:#398439;border-color:#255625}.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;background-image:none;border-color:#269abc}.btn-info:active:hover,.btn-info.active:hover,.open>.dropdown-toggle.btn-info:hover,.btn-info:active:focus,.btn-info.active:focus,.open>.dropdown-toggle.btn-info:focus,.btn-info:active.focus,.btn-info.active.focus,.open>.dropdown-toggle.btn-info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;background-image:none;border-color:#d58512}.btn-warning:active:hover,.btn-warning.active:hover,.open>.dropdown-toggle.btn-warning:hover,.btn-warning:active:focus,.btn-warning.active:focus,.open>.dropdown-toggle.btn-warning:focus,.btn-warning:active.focus,.btn-warning.active.focus,.open>.dropdown-toggle.btn-warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;background-image:none;border-color:#ac2925}.btn-danger:active:hover,.btn-danger.active:hover,.open>.dropdown-toggle.btn-danger:hover,.btn-danger:active:focus,.btn-danger.active:focus,.open>.dropdown-toggle.btn-danger:focus,.btn-danger:active.focus,.btn-danger.active.focus,.open>.dropdown-toggle.btn-danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid \9;border-right:4px solid transparent;border-left:4px solid transparent}.dropup,.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid \9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle="buttons"]>.btn input[type="radio"],[data-toggle="buttons"]>.btn-group>.btn input[type="radio"],[data-toggle="buttons"]>.btn input[type="checkbox"],[data-toggle="buttons"]>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media(min-width:768px){.navbar{border-radius:4px}}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media(max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media(min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-right:15px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-right:-15px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:8px;margin-bottom:8px}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right ~ .navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge,.btn-group-xs>.btn .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,button.list-group-item:hover,a.list-group-item:focus,button.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,button.list-group-item-success:hover,a.list-group-item-success:focus,button.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active,a.list-group-item-success.active:hover,button.list-group-item-success.active:hover,a.list-group-item-success.active:focus,button.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,button.list-group-item-info:hover,a.list-group-item-info:focus,button.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active,a.list-group-item-info.active:hover,button.list-group-item-info.active:hover,a.list-group-item-info.active:focus,button.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,button.list-group-item-warning:hover,a.list-group-item-warning:focus,button.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active,a.list-group-item-warning.active:hover,button.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus,button.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,button.list-group-item-danger:hover,a.list-group-item-danger:focus,button.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active,a.list-group-item-danger.active:hover,button.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus,button.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a,.panel-title>small,.panel-title>.small,.panel-title>small>a,.panel-title>.small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;-moz-appearance:none;appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media(min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media(min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:12px;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;line-height:1.42857143;line-break:auto;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover>.arrow{border-width:11px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,0.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.next,.carousel-inner>.item.active.right{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,0.5)),to(rgba(0,0,0,0.0001)));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,0.0001)),to(rgba(0,0,0,0.5)));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;filter:alpha(opacity=90);opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203a"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-header:before,.modal-header:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-header:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(max-width:767px){.visible-xs-block{display:block!important}}@media(max-width:767px){.visible-xs-inline{display:inline!important}}@media(max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media(min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media(min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media(min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media(min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media(min-width:1200px){.visible-lg-block{display:block!important}}@media(min-width:1200px){.visible-lg-inline{display:inline!important}}@media(min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media(max-width:767px){.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*!* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license(Font:SIL OFL 1.1,CSS:MIT License) */ @font-face{font-family:'FontAwesome';src:url('../node_modules/font-awesome/fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../node_modules/font-awesome/fonts/fontawesome-webfont.eot#iefix&v=4.7.0') format('embedded-opentype'),url('../node_modules/font-awesome/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../node_modules/font-awesome/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../node_modules/font-awesome/fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../node_modules/font-awesome/fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}/*!Ionicons,v3.0.0-alpha.3 Created by Ben Sperry for the Ionic Framework,http://ionicons.com/ https://twitter.com/benjsperry https://twitter.com/ionicframework MIT License:https://github.com/driftyco/ionicons Android-style icons originally built by Google’s Material Design Icons:https://github.com/google/material-design-icons used under CC BY http://creativecommons.org/licenses/by/4.0/ Modified icons to fit ionicon’s grid from original. */ @font-face{font-family:"Ionicons";src:url("../node_modules/ionicons/dist/fonts/ionicons.eot?v=3.0.0-alpha.3");src:url("../node_modules/ionicons/dist/fonts/ionicons.eot?v=3.0.0-alpha.3#iefix") format("embedded-opentype"),url("../node_modules/ionicons/dist/fonts/ionicons.woff2?v=3.0.0-alpha.3") format("woff2"),url("../node_modules/ionicons/dist/fonts/ionicons.woff?v=3.0.0-alpha.3") format("woff"),url("../node_modules/ionicons/dist/fonts/ionicons.ttf?v=3.0.0-alpha.3") format("truetype"),url("../node_modules/ionicons/dist/fonts/ionicons.svg?v=3.0.0-alpha.3#Ionicons") format("svg");font-weight:normal;font-style:normal}.ion,.ionicons,.ion-ios-add:before,.ion-ios-add-circle:before,.ion-ios-add-circle-outline:before,.ion-ios-add-outline:before,.ion-ios-alarm:before,.ion-ios-alarm-outline:before,.ion-ios-albums:before,.ion-ios-albums-outline:before,.ion-ios-alert:before,.ion-ios-alert-outline:before,.ion-ios-american-football:before,.ion-ios-american-football-outline:before,.ion-ios-analytics:before,.ion-ios-analytics-outline:before,.ion-ios-aperture:before,.ion-ios-aperture-outline:before,.ion-ios-apps:before,.ion-ios-apps-outline:before,.ion-ios-appstore:before,.ion-ios-appstore-outline:before,.ion-ios-archive:before,.ion-ios-archive-outline:before,.ion-ios-arrow-back:before,.ion-ios-arrow-back-outline:before,.ion-ios-arrow-down:before,.ion-ios-arrow-down-outline:before,.ion-ios-arrow-dropdown:before,.ion-ios-arrow-dropdown-circle:before,.ion-ios-arrow-dropdown-circle-outline:before,.ion-ios-arrow-dropdown-outline:before,.ion-ios-arrow-dropleft:before,.ion-ios-arrow-dropleft-circle:before,.ion-ios-arrow-dropleft-circle-outline:before,.ion-ios-arrow-dropleft-outline:before,.ion-ios-arrow-dropright:before,.ion-ios-arrow-dropright-circle:before,.ion-ios-arrow-dropright-circle-outline:before,.ion-ios-arrow-dropright-outline:before,.ion-ios-arrow-dropup:before,.ion-ios-arrow-dropup-circle:before,.ion-ios-arrow-dropup-circle-outline:before,.ion-ios-arrow-dropup-outline:before,.ion-ios-arrow-forward:before,.ion-ios-arrow-forward-outline:before,.ion-ios-arrow-round-back:before,.ion-ios-arrow-round-back-outline:before,.ion-ios-arrow-round-down:before,.ion-ios-arrow-round-down-outline:before,.ion-ios-arrow-round-forward:before,.ion-ios-arrow-round-forward-outline:before,.ion-ios-arrow-round-up:before,.ion-ios-arrow-round-up-outline:before,.ion-ios-arrow-up:before,.ion-ios-arrow-up-outline:before,.ion-ios-at:before,.ion-ios-at-outline:before,.ion-ios-attach:before,.ion-ios-attach-outline:before,.ion-ios-backspace:before,.ion-ios-backspace-outline:before,.ion-ios-barcode:before,.ion-ios-barcode-outline:before,.ion-ios-baseball:before,.ion-ios-baseball-outline:before,.ion-ios-basket:before,.ion-ios-basket-outline:before,.ion-ios-basketball:before,.ion-ios-basketball-outline:before,.ion-ios-battery-charging:before,.ion-ios-battery-charging-outline:before,.ion-ios-battery-dead:before,.ion-ios-battery-dead-outline:before,.ion-ios-battery-full:before,.ion-ios-battery-full-outline:before,.ion-ios-beaker:before,.ion-ios-beaker-outline:before,.ion-ios-beer:before,.ion-ios-beer-outline:before,.ion-ios-bicycle:before,.ion-ios-bicycle-outline:before,.ion-ios-bluetooth:before,.ion-ios-bluetooth-outline:before,.ion-ios-boat:before,.ion-ios-boat-outline:before,.ion-ios-body:before,.ion-ios-body-outline:before,.ion-ios-bonfire:before,.ion-ios-bonfire-outline:before,.ion-ios-book:before,.ion-ios-book-outline:before,.ion-ios-bookmark:before,.ion-ios-bookmark-outline:before,.ion-ios-bookmarks:before,.ion-ios-bookmarks-outline:before,.ion-ios-bowtie:before,.ion-ios-bowtie-outline:before,.ion-ios-briefcase:before,.ion-ios-briefcase-outline:before,.ion-ios-browsers:before,.ion-ios-browsers-outline:before,.ion-ios-brush:before,.ion-ios-brush-outline:before,.ion-ios-bug:before,.ion-ios-bug-outline:before,.ion-ios-build:before,.ion-ios-build-outline:before,.ion-ios-bulb:before,.ion-ios-bulb-outline:before,.ion-ios-bus:before,.ion-ios-bus-outline:before,.ion-ios-cafe:before,.ion-ios-cafe-outline:before,.ion-ios-calculator:before,.ion-ios-calculator-outline:before,.ion-ios-calendar:before,.ion-ios-calendar-outline:before,.ion-ios-call:before,.ion-ios-call-outline:before,.ion-ios-camera:before,.ion-ios-camera-outline:before,.ion-ios-car:before,.ion-ios-car-outline:before,.ion-ios-card:before,.ion-ios-card-outline:before,.ion-ios-cart:before,.ion-ios-cart-outline:before,.ion-ios-cash:before,.ion-ios-cash-outline:before,.ion-ios-chatboxes:before,.ion-ios-chatboxes-outline:before,.ion-ios-chatbubbles:before,.ion-ios-chatbubbles-outline:before,.ion-ios-checkbox:before,.ion-ios-checkbox-outline:before,.ion-ios-checkmark:before,.ion-ios-checkmark-circle:before,.ion-ios-checkmark-circle-outline:before,.ion-ios-checkmark-outline:before,.ion-ios-clipboard:before,.ion-ios-clipboard-outline:before,.ion-ios-clock:before,.ion-ios-clock-outline:before,.ion-ios-close:before,.ion-ios-close-circle:before,.ion-ios-close-circle-outline:before,.ion-ios-close-outline:before,.ion-ios-closed-captioning:before,.ion-ios-closed-captioning-outline:before,.ion-ios-cloud:before,.ion-ios-cloud-circle:before,.ion-ios-cloud-circle-outline:before,.ion-ios-cloud-done:before,.ion-ios-cloud-done-outline:before,.ion-ios-cloud-download:before,.ion-ios-cloud-download-outline:before,.ion-ios-cloud-outline:before,.ion-ios-cloud-upload:before,.ion-ios-cloud-upload-outline:before,.ion-ios-cloudy:before,.ion-ios-cloudy-night:before,.ion-ios-cloudy-night-outline:before,.ion-ios-cloudy-outline:before,.ion-ios-code:before,.ion-ios-code-download:before,.ion-ios-code-download-outline:before,.ion-ios-code-outline:before,.ion-ios-code-working:before,.ion-ios-code-working-outline:before,.ion-ios-cog:before,.ion-ios-cog-outline:before,.ion-ios-color-fill:before,.ion-ios-color-fill-outline:before,.ion-ios-color-filter:before,.ion-ios-color-filter-outline:before,.ion-ios-color-palette:before,.ion-ios-color-palette-outline:before,.ion-ios-color-wand:before,.ion-ios-color-wand-outline:before,.ion-ios-compass:before,.ion-ios-compass-outline:before,.ion-ios-construct:before,.ion-ios-construct-outline:before,.ion-ios-contact:before,.ion-ios-contact-outline:before,.ion-ios-contacts:before,.ion-ios-contacts-outline:before,.ion-ios-contract:before,.ion-ios-contract-outline:before,.ion-ios-contrast:before,.ion-ios-contrast-outline:before,.ion-ios-copy:before,.ion-ios-copy-outline:before,.ion-ios-create:before,.ion-ios-create-outline:before,.ion-ios-crop:before,.ion-ios-crop-outline:before,.ion-ios-cube:before,.ion-ios-cube-outline:before,.ion-ios-cut:before,.ion-ios-cut-outline:before,.ion-ios-desktop:before,.ion-ios-desktop-outline:before,.ion-ios-disc:before,.ion-ios-disc-outline:before,.ion-ios-document:before,.ion-ios-document-outline:before,.ion-ios-done-all:before,.ion-ios-done-all-outline:before,.ion-ios-download:before,.ion-ios-download-outline:before,.ion-ios-easel:before,.ion-ios-easel-outline:before,.ion-ios-egg:before,.ion-ios-egg-outline:before,.ion-ios-exit:before,.ion-ios-exit-outline:before,.ion-ios-expand:before,.ion-ios-expand-outline:before,.ion-ios-eye:before,.ion-ios-eye-off:before,.ion-ios-eye-off-outline:before,.ion-ios-eye-outline:before,.ion-ios-fastforward:before,.ion-ios-fastforward-outline:before,.ion-ios-female:before,.ion-ios-female-outline:before,.ion-ios-filing:before,.ion-ios-filing-outline:before,.ion-ios-film:before,.ion-ios-film-outline:before,.ion-ios-finger-print:before,.ion-ios-finger-print-outline:before,.ion-ios-flag:before,.ion-ios-flag-outline:before,.ion-ios-flame:before,.ion-ios-flame-outline:before,.ion-ios-flash:before,.ion-ios-flash-outline:before,.ion-ios-flask:before,.ion-ios-flask-outline:before,.ion-ios-flower:before,.ion-ios-flower-outline:before,.ion-ios-folder:before,.ion-ios-folder-open:before,.ion-ios-folder-open-outline:before,.ion-ios-folder-outline:before,.ion-ios-football:before,.ion-ios-football-outline:before,.ion-ios-funnel:before,.ion-ios-funnel-outline:before,.ion-ios-game-controller-a:before,.ion-ios-game-controller-a-outline:before,.ion-ios-game-controller-b:before,.ion-ios-game-controller-b-outline:before,.ion-ios-git-branch:before,.ion-ios-git-branch-outline:before,.ion-ios-git-commit:before,.ion-ios-git-commit-outline:before,.ion-ios-git-compare:before,.ion-ios-git-compare-outline:before,.ion-ios-git-merge:before,.ion-ios-git-merge-outline:before,.ion-ios-git-network:before,.ion-ios-git-network-outline:before,.ion-ios-git-pull-request:before,.ion-ios-git-pull-request-outline:before,.ion-ios-glasses:before,.ion-ios-glasses-outline:before,.ion-ios-globe:before,.ion-ios-globe-outline:before,.ion-ios-grid:before,.ion-ios-grid-outline:before,.ion-ios-hammer:before,.ion-ios-hammer-outline:before,.ion-ios-hand:before,.ion-ios-hand-outline:before,.ion-ios-happy:before,.ion-ios-happy-outline:before,.ion-ios-headset:before,.ion-ios-headset-outline:before,.ion-ios-heart:before,.ion-ios-heart-outline:before,.ion-ios-help:before,.ion-ios-help-buoy:before,.ion-ios-help-buoy-outline:before,.ion-ios-help-circle:before,.ion-ios-help-circle-outline:before,.ion-ios-help-outline:before,.ion-ios-home:before,.ion-ios-home-outline:before,.ion-ios-ice-cream:before,.ion-ios-ice-cream-outline:before,.ion-ios-image:before,.ion-ios-image-outline:before,.ion-ios-images:before,.ion-ios-images-outline:before,.ion-ios-infinite:before,.ion-ios-infinite-outline:before,.ion-ios-information:before,.ion-ios-information-circle:before,.ion-ios-information-circle-outline:before,.ion-ios-information-outline:before,.ion-ios-ionic:before,.ion-ios-ionic-outline:before,.ion-ios-ionitron:before,.ion-ios-ionitron-outline:before,.ion-ios-jet:before,.ion-ios-jet-outline:before,.ion-ios-key:before,.ion-ios-key-outline:before,.ion-ios-keypad:before,.ion-ios-keypad-outline:before,.ion-ios-laptop:before,.ion-ios-laptop-outline:before,.ion-ios-leaf:before,.ion-ios-leaf-outline:before,.ion-ios-link:before,.ion-ios-link-outline:before,.ion-ios-list:before,.ion-ios-list-box:before,.ion-ios-list-box-outline:before,.ion-ios-list-outline:before,.ion-ios-locate:before,.ion-ios-locate-outline:before,.ion-ios-lock:before,.ion-ios-lock-outline:before,.ion-ios-log-in:before,.ion-ios-log-in-outline:before,.ion-ios-log-out:before,.ion-ios-log-out-outline:before,.ion-ios-magnet:before,.ion-ios-magnet-outline:before,.ion-ios-mail:before,.ion-ios-mail-open:before,.ion-ios-mail-open-outline:before,.ion-ios-mail-outline:before,.ion-ios-male:before,.ion-ios-male-outline:before,.ion-ios-man:before,.ion-ios-man-outline:before,.ion-ios-map:before,.ion-ios-map-outline:before,.ion-ios-medal:before,.ion-ios-medal-outline:before,.ion-ios-medical:before,.ion-ios-medical-outline:before,.ion-ios-medkit:before,.ion-ios-medkit-outline:before,.ion-ios-megaphone:before,.ion-ios-megaphone-outline:before,.ion-ios-menu:before,.ion-ios-menu-outline:before,.ion-ios-mic:before,.ion-ios-mic-off:before,.ion-ios-mic-off-outline:before,.ion-ios-mic-outline:before,.ion-ios-microphone:before,.ion-ios-microphone-outline:before,.ion-ios-moon:before,.ion-ios-moon-outline:before,.ion-ios-more:before,.ion-ios-more-outline:before,.ion-ios-move:before,.ion-ios-move-outline:before,.ion-ios-musical-note:before,.ion-ios-musical-note-outline:before,.ion-ios-musical-notes:before,.ion-ios-musical-notes-outline:before,.ion-ios-navigate:before,.ion-ios-navigate-outline:before,.ion-ios-no-smoking:before,.ion-ios-no-smoking-outline:before,.ion-ios-notifications:before,.ion-ios-notifications-off:before,.ion-ios-notifications-off-outline:before,.ion-ios-notifications-outline:before,.ion-ios-nuclear:before,.ion-ios-nuclear-outline:before,.ion-ios-nutrition:before,.ion-ios-nutrition-outline:before,.ion-ios-open:before,.ion-ios-open-outline:before,.ion-ios-options:before,.ion-ios-options-outline:before,.ion-ios-outlet:before,.ion-ios-outlet-outline:before,.ion-ios-paper:before,.ion-ios-paper-outline:before,.ion-ios-paper-plane:before,.ion-ios-paper-plane-outline:before,.ion-ios-partly-sunny:before,.ion-ios-partly-sunny-outline:before,.ion-ios-pause:before,.ion-ios-pause-outline:before,.ion-ios-paw:before,.ion-ios-paw-outline:before,.ion-ios-people:before,.ion-ios-people-outline:before,.ion-ios-person:before,.ion-ios-person-add:before,.ion-ios-person-add-outline:before,.ion-ios-person-outline:before,.ion-ios-phone-landscape:before,.ion-ios-phone-landscape-outline:before,.ion-ios-phone-portrait:before,.ion-ios-phone-portrait-outline:before,.ion-ios-photos:before,.ion-ios-photos-outline:before,.ion-ios-pie:before,.ion-ios-pie-outline:before,.ion-ios-pin:before,.ion-ios-pin-outline:before,.ion-ios-pint:before,.ion-ios-pint-outline:before,.ion-ios-pizza:before,.ion-ios-pizza-outline:before,.ion-ios-plane:before,.ion-ios-plane-outline:before,.ion-ios-planet:before,.ion-ios-planet-outline:before,.ion-ios-play:before,.ion-ios-play-outline:before,.ion-ios-podium:before,.ion-ios-podium-outline:before,.ion-ios-power:before,.ion-ios-power-outline:before,.ion-ios-pricetag:before,.ion-ios-pricetag-outline:before,.ion-ios-pricetags:before,.ion-ios-pricetags-outline:before,.ion-ios-print:before,.ion-ios-print-outline:before,.ion-ios-pulse:before,.ion-ios-pulse-outline:before,.ion-ios-qr-scanner:before,.ion-ios-qr-scanner-outline:before,.ion-ios-quote:before,.ion-ios-quote-outline:before,.ion-ios-radio:before,.ion-ios-radio-button-off:before,.ion-ios-radio-button-off-outline:before,.ion-ios-radio-button-on:before,.ion-ios-radio-button-on-outline:before,.ion-ios-radio-outline:before,.ion-ios-rainy:before,.ion-ios-rainy-outline:before,.ion-ios-recording:before,.ion-ios-recording-outline:before,.ion-ios-redo:before,.ion-ios-redo-outline:before,.ion-ios-refresh:before,.ion-ios-refresh-circle:before,.ion-ios-refresh-circle-outline:before,.ion-ios-refresh-outline:before,.ion-ios-remove:before,.ion-ios-remove-circle:before,.ion-ios-remove-circle-outline:before,.ion-ios-remove-outline:before,.ion-ios-reorder:before,.ion-ios-reorder-outline:before,.ion-ios-repeat:before,.ion-ios-repeat-outline:before,.ion-ios-resize:before,.ion-ios-resize-outline:before,.ion-ios-restaurant:before,.ion-ios-restaurant-outline:before,.ion-ios-return-left:before,.ion-ios-return-left-outline:before,.ion-ios-return-right:before,.ion-ios-return-right-outline:before,.ion-ios-reverse-camera:before,.ion-ios-reverse-camera-outline:before,.ion-ios-rewind:before,.ion-ios-rewind-outline:before,.ion-ios-ribbon:before,.ion-ios-ribbon-outline:before,.ion-ios-rose:before,.ion-ios-rose-outline:before,.ion-ios-sad:before,.ion-ios-sad-outline:before,.ion-ios-school:before,.ion-ios-school-outline:before,.ion-ios-search:before,.ion-ios-search-outline:before,.ion-ios-send:before,.ion-ios-send-outline:before,.ion-ios-settings:before,.ion-ios-settings-outline:before,.ion-ios-share:before,.ion-ios-share-alt:before,.ion-ios-share-alt-outline:before,.ion-ios-share-outline:before,.ion-ios-shirt:before,.ion-ios-shirt-outline:before,.ion-ios-shuffle:before,.ion-ios-shuffle-outline:before,.ion-ios-skip-backward:before,.ion-ios-skip-backward-outline:before,.ion-ios-skip-forward:before,.ion-ios-skip-forward-outline:before,.ion-ios-snow:before,.ion-ios-snow-outline:before,.ion-ios-speedometer:before,.ion-ios-speedometer-outline:before,.ion-ios-square:before,.ion-ios-square-outline:before,.ion-ios-star:before,.ion-ios-star-half:before,.ion-ios-star-half-outline:before,.ion-ios-star-outline:before,.ion-ios-stats:before,.ion-ios-stats-outline:before,.ion-ios-stopwatch:before,.ion-ios-stopwatch-outline:before,.ion-ios-subway:before,.ion-ios-subway-outline:before,.ion-ios-sunny:before,.ion-ios-sunny-outline:before,.ion-ios-swap:before,.ion-ios-swap-outline:before,.ion-ios-switch:before,.ion-ios-switch-outline:before,.ion-ios-sync:before,.ion-ios-sync-outline:before,.ion-ios-tablet-landscape:before,.ion-ios-tablet-landscape-outline:before,.ion-ios-tablet-portrait:before,.ion-ios-tablet-portrait-outline:before,.ion-ios-tennisball:before,.ion-ios-tennisball-outline:before,.ion-ios-text:before,.ion-ios-text-outline:before,.ion-ios-thermometer:before,.ion-ios-thermometer-outline:before,.ion-ios-thumbs-down:before,.ion-ios-thumbs-down-outline:before,.ion-ios-thumbs-up:before,.ion-ios-thumbs-up-outline:before,.ion-ios-thunderstorm:before,.ion-ios-thunderstorm-outline:before,.ion-ios-time:before,.ion-ios-time-outline:before,.ion-ios-timer:before,.ion-ios-timer-outline:before,.ion-ios-train:before,.ion-ios-train-outline:before,.ion-ios-transgender:before,.ion-ios-transgender-outline:before,.ion-ios-trash:before,.ion-ios-trash-outline:before,.ion-ios-trending-down:before,.ion-ios-trending-down-outline:before,.ion-ios-trending-up:before,.ion-ios-trending-up-outline:before,.ion-ios-trophy:before,.ion-ios-trophy-outline:before,.ion-ios-umbrella:before,.ion-ios-umbrella-outline:before,.ion-ios-undo:before,.ion-ios-undo-outline:before,.ion-ios-unlock:before,.ion-ios-unlock-outline:before,.ion-ios-videocam:before,.ion-ios-videocam-outline:before,.ion-ios-volume-down:before,.ion-ios-volume-down-outline:before,.ion-ios-volume-mute:before,.ion-ios-volume-mute-outline:before,.ion-ios-volume-off:before,.ion-ios-volume-off-outline:before,.ion-ios-volume-up:before,.ion-ios-volume-up-outline:before,.ion-ios-walk:before,.ion-ios-walk-outline:before,.ion-ios-warning:before,.ion-ios-warning-outline:before,.ion-ios-watch:before,.ion-ios-watch-outline:before,.ion-ios-water:before,.ion-ios-water-outline:before,.ion-ios-wifi:before,.ion-ios-wifi-outline:before,.ion-ios-wine:before,.ion-ios-wine-outline:before,.ion-ios-woman:before,.ion-ios-woman-outline:before,.ion-logo-android:before,.ion-logo-angular:before,.ion-logo-apple:before,.ion-logo-bitcoin:before,.ion-logo-buffer:before,.ion-logo-chrome:before,.ion-logo-codepen:before,.ion-logo-css3:before,.ion-logo-designernews:before,.ion-logo-dribbble:before,.ion-logo-dropbox:before,.ion-logo-euro:before,.ion-logo-facebook:before,.ion-logo-foursquare:before,.ion-logo-freebsd-devil:before,.ion-logo-github:before,.ion-logo-google:before,.ion-logo-googleplus:before,.ion-logo-hackernews:before,.ion-logo-html5:before,.ion-logo-instagram:before,.ion-logo-javascript:before,.ion-logo-linkedin:before,.ion-logo-markdown:before,.ion-logo-nodejs:before,.ion-logo-octocat:before,.ion-logo-pinterest:before,.ion-logo-playstation:before,.ion-logo-python:before,.ion-logo-reddit:before,.ion-logo-rss:before,.ion-logo-sass:before,.ion-logo-skype:before,.ion-logo-snapchat:before,.ion-logo-steam:before,.ion-logo-tumblr:before,.ion-logo-tux:before,.ion-logo-twitch:before,.ion-logo-twitter:before,.ion-logo-usd:before,.ion-logo-vimeo:before,.ion-logo-whatsapp:before,.ion-logo-windows:before,.ion-logo-wordpress:before,.ion-logo-xbox:before,.ion-logo-yahoo:before,.ion-logo-yen:before,.ion-logo-youtube:before,.ion-md-add:before,.ion-md-add-circle:before,.ion-md-alarm:before,.ion-md-albums:before,.ion-md-alert:before,.ion-md-american-football:before,.ion-md-analytics:before,.ion-md-aperture:before,.ion-md-apps:before,.ion-md-appstore:before,.ion-md-archive:before,.ion-md-arrow-back:before,.ion-md-arrow-down:before,.ion-md-arrow-dropdown:before,.ion-md-arrow-dropdown-circle:before,.ion-md-arrow-dropleft:before,.ion-md-arrow-dropleft-circle:before,.ion-md-arrow-dropright:before,.ion-md-arrow-dropright-circle:before,.ion-md-arrow-dropup:before,.ion-md-arrow-dropup-circle:before,.ion-md-arrow-forward:before,.ion-md-arrow-round-back:before,.ion-md-arrow-round-down:before,.ion-md-arrow-round-forward:before,.ion-md-arrow-round-up:before,.ion-md-arrow-up:before,.ion-md-at:before,.ion-md-attach:before,.ion-md-backspace:before,.ion-md-barcode:before,.ion-md-baseball:before,.ion-md-basket:before,.ion-md-basketball:before,.ion-md-battery-charging:before,.ion-md-battery-dead:before,.ion-md-battery-full:before,.ion-md-beaker:before,.ion-md-beer:before,.ion-md-bicycle:before,.ion-md-bluetooth:before,.ion-md-boat:before,.ion-md-body:before,.ion-md-bonfire:before,.ion-md-book:before,.ion-md-bookmark:before,.ion-md-bookmarks:before,.ion-md-bowtie:before,.ion-md-briefcase:before,.ion-md-browsers:before,.ion-md-brush:before,.ion-md-bug:before,.ion-md-build:before,.ion-md-bulb:before,.ion-md-bus:before,.ion-md-cafe:before,.ion-md-calculator:before,.ion-md-calendar:before,.ion-md-call:before,.ion-md-camera:before,.ion-md-car:before,.ion-md-card:before,.ion-md-cart:before,.ion-md-cash:before,.ion-md-chatboxes:before,.ion-md-chatbubbles:before,.ion-md-checkbox:before,.ion-md-checkbox-outline:before,.ion-md-checkmark:before,.ion-md-checkmark-circle:before,.ion-md-checkmark-circle-outline:before,.ion-md-clipboard:before,.ion-md-clock:before,.ion-md-close:before,.ion-md-close-circle:before,.ion-md-closed-captioning:before,.ion-md-cloud:before,.ion-md-cloud-circle:before,.ion-md-cloud-done:before,.ion-md-cloud-download:before,.ion-md-cloud-outline:before,.ion-md-cloud-upload:before,.ion-md-cloudy:before,.ion-md-cloudy-night:before,.ion-md-code:before,.ion-md-code-download:before,.ion-md-code-working:before,.ion-md-cog:before,.ion-md-color-fill:before,.ion-md-color-filter:before,.ion-md-color-palette:before,.ion-md-color-wand:before,.ion-md-compass:before,.ion-md-construct:before,.ion-md-contact:before,.ion-md-contacts:before,.ion-md-contract:before,.ion-md-contrast:before,.ion-md-copy:before,.ion-md-create:before,.ion-md-crop:before,.ion-md-cube:before,.ion-md-cut:before,.ion-md-desktop:before,.ion-md-disc:before,.ion-md-document:before,.ion-md-done-all:before,.ion-md-download:before,.ion-md-easel:before,.ion-md-egg:before,.ion-md-exit:before,.ion-md-expand:before,.ion-md-eye:before,.ion-md-eye-off:before,.ion-md-fastforward:before,.ion-md-female:before,.ion-md-filing:before,.ion-md-film:before,.ion-md-finger-print:before,.ion-md-flag:before,.ion-md-flame:before,.ion-md-flash:before,.ion-md-flask:before,.ion-md-flower:before,.ion-md-folder:before,.ion-md-folder-open:before,.ion-md-football:before,.ion-md-funnel:before,.ion-md-game-controller-a:before,.ion-md-game-controller-b:before,.ion-md-git-branch:before,.ion-md-git-commit:before,.ion-md-git-compare:before,.ion-md-git-merge:before,.ion-md-git-network:before,.ion-md-git-pull-request:before,.ion-md-glasses:before,.ion-md-globe:before,.ion-md-grid:before,.ion-md-hammer:before,.ion-md-hand:before,.ion-md-happy:before,.ion-md-headset:before,.ion-md-heart:before,.ion-md-heart-outline:before,.ion-md-help:before,.ion-md-help-buoy:before,.ion-md-help-circle:before,.ion-md-home:before,.ion-md-ice-cream:before,.ion-md-image:before,.ion-md-images:before,.ion-md-infinite:before,.ion-md-information:before,.ion-md-information-circle:before,.ion-md-ionic:before,.ion-md-ionitron:before,.ion-md-jet:before,.ion-md-key:before,.ion-md-keypad:before,.ion-md-laptop:before,.ion-md-leaf:before,.ion-md-link:before,.ion-md-list:before,.ion-md-list-box:before,.ion-md-locate:before,.ion-md-lock:before,.ion-md-log-in:before,.ion-md-log-out:before,.ion-md-magnet:before,.ion-md-mail:before,.ion-md-mail-open:before,.ion-md-male:before,.ion-md-man:before,.ion-md-map:before,.ion-md-medal:before,.ion-md-medical:before,.ion-md-medkit:before,.ion-md-megaphone:before,.ion-md-menu:before,.ion-md-mic:before,.ion-md-mic-off:before,.ion-md-microphone:before,.ion-md-moon:before,.ion-md-more:before,.ion-md-move:before,.ion-md-musical-note:before,.ion-md-musical-notes:before,.ion-md-navigate:before,.ion-md-no-smoking:before,.ion-md-notifications:before,.ion-md-notifications-off:before,.ion-md-notifications-outline:before,.ion-md-nuclear:before,.ion-md-nutrition:before,.ion-md-open:before,.ion-md-options:before,.ion-md-outlet:before,.ion-md-paper:before,.ion-md-paper-plane:before,.ion-md-partly-sunny:before,.ion-md-pause:before,.ion-md-paw:before,.ion-md-people:before,.ion-md-person:before,.ion-md-person-add:before,.ion-md-phone-landscape:before,.ion-md-phone-portrait:before,.ion-md-photos:before,.ion-md-pie:before,.ion-md-pin:before,.ion-md-pint:before,.ion-md-pizza:before,.ion-md-plane:before,.ion-md-planet:before,.ion-md-play:before,.ion-md-podium:before,.ion-md-power:before,.ion-md-pricetag:before,.ion-md-pricetags:before,.ion-md-print:before,.ion-md-pulse:before,.ion-md-qr-scanner:before,.ion-md-quote:before,.ion-md-radio:before,.ion-md-radio-button-off:before,.ion-md-radio-button-on:before,.ion-md-rainy:before,.ion-md-recording:before,.ion-md-redo:before,.ion-md-refresh:before,.ion-md-refresh-circle:before,.ion-md-remove:before,.ion-md-remove-circle:before,.ion-md-reorder:before,.ion-md-repeat:before,.ion-md-resize:before,.ion-md-restaurant:before,.ion-md-return-left:before,.ion-md-return-right:before,.ion-md-reverse-camera:before,.ion-md-rewind:before,.ion-md-ribbon:before,.ion-md-rose:before,.ion-md-sad:before,.ion-md-school:before,.ion-md-search:before,.ion-md-send:before,.ion-md-settings:before,.ion-md-share:before,.ion-md-share-alt:before,.ion-md-shirt:before,.ion-md-shuffle:before,.ion-md-skip-backward:before,.ion-md-skip-forward:before,.ion-md-snow:before,.ion-md-speedometer:before,.ion-md-square:before,.ion-md-square-outline:before,.ion-md-star:before,.ion-md-star-half:before,.ion-md-star-outline:before,.ion-md-stats:before,.ion-md-stopwatch:before,.ion-md-subway:before,.ion-md-sunny:before,.ion-md-swap:before,.ion-md-switch:before,.ion-md-sync:before,.ion-md-tablet-landscape:before,.ion-md-tablet-portrait:before,.ion-md-tennisball:before,.ion-md-text:before,.ion-md-thermometer:before,.ion-md-thumbs-down:before,.ion-md-thumbs-up:before,.ion-md-thunderstorm:before,.ion-md-time:before,.ion-md-timer:before,.ion-md-train:before,.ion-md-transgender:before,.ion-md-trash:before,.ion-md-trending-down:before,.ion-md-trending-up:before,.ion-md-trophy:before,.ion-md-umbrella:before,.ion-md-undo:before,.ion-md-unlock:before,.ion-md-videocam:before,.ion-md-volume-down:before,.ion-md-volume-mute:before,.ion-md-volume-off:before,.ion-md-volume-up:before,.ion-md-walk:before,.ion-md-warning:before,.ion-md-watch:before,.ion-md-water:before,.ion-md-wifi:before,.ion-md-wine:before,.ion-md-woman:before{display:inline-block;font-family:"Ionicons";speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;text-rendering:auto;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.ion-ios-add:before{content:"\f102"}.ion-ios-add-circle:before{content:"\f101"}.ion-ios-add-circle-outline:before{content:"\f100"}.ion-ios-add-outline:before{content:"\f102"}.ion-ios-alarm:before{content:"\f3c8"}.ion-ios-alarm-outline:before{content:"\f3c7"}.ion-ios-albums:before{content:"\f3ca"}.ion-ios-albums-outline:before{content:"\f3c9"}.ion-ios-alert:before{content:"\f104"}.ion-ios-alert-outline:before{content:"\f103"}.ion-ios-american-football:before{content:"\f106"}.ion-ios-american-football-outline:before{content:"\f105"}.ion-ios-analytics:before{content:"\f3ce"}.ion-ios-analytics-outline:before{content:"\f3cd"}.ion-ios-aperture:before{content:"\f108"}.ion-ios-aperture-outline:before{content:"\f107"}.ion-ios-apps:before{content:"\f10a"}.ion-ios-apps-outline:before{content:"\f109"}.ion-ios-appstore:before{content:"\f10c"}.ion-ios-appstore-outline:before{content:"\f10b"}.ion-ios-archive:before{content:"\f10e"}.ion-ios-archive-outline:before{content:"\f10d"}.ion-ios-arrow-back:before{content:"\f3cf"}.ion-ios-arrow-back-outline:before{content:"\f3cf"}.ion-ios-arrow-down:before{content:"\f3d0"}.ion-ios-arrow-down-outline:before{content:"\f3d0"}.ion-ios-arrow-dropdown:before{content:"\f110"}.ion-ios-arrow-dropdown-circle:before{content:"\f10f"}.ion-ios-arrow-dropdown-circle-outline:before{content:"\f10f"}.ion-ios-arrow-dropdown-outline:before{content:"\f110"}.ion-ios-arrow-dropleft:before{content:"\f112"}.ion-ios-arrow-dropleft-circle:before{content:"\f111"}.ion-ios-arrow-dropleft-circle-outline:before{content:"\f111"}.ion-ios-arrow-dropleft-outline:before{content:"\f112"}.ion-ios-arrow-dropright:before{content:"\f114"}.ion-ios-arrow-dropright-circle:before{content:"\f113"}.ion-ios-arrow-dropright-circle-outline:before{content:"\f113"}.ion-ios-arrow-dropright-outline:before{content:"\f114"}.ion-ios-arrow-dropup:before{content:"\f116"}.ion-ios-arrow-dropup-circle:before{content:"\f115"}.ion-ios-arrow-dropup-circle-outline:before{content:"\f115"}.ion-ios-arrow-dropup-outline:before{content:"\f116"}.ion-ios-arrow-forward:before{content:"\f3d1"}.ion-ios-arrow-forward-outline:before{content:"\f3d1"}.ion-ios-arrow-round-back:before{content:"\f117"}.ion-ios-arrow-round-back-outline:before{content:"\f117"}.ion-ios-arrow-round-down:before{content:"\f118"}.ion-ios-arrow-round-down-outline:before{content:"\f118"}.ion-ios-arrow-round-forward:before{content:"\f119"}.ion-ios-arrow-round-forward-outline:before{content:"\f119"}.ion-ios-arrow-round-up:before{content:"\f11a"}.ion-ios-arrow-round-up-outline:before{content:"\f11a"}.ion-ios-arrow-up:before{content:"\f3d8"}.ion-ios-arrow-up-outline:before{content:"\f3d8"}.ion-ios-at:before{content:"\f3da"}.ion-ios-at-outline:before{content:"\f3d9"}.ion-ios-attach:before{content:"\f11b"}.ion-ios-attach-outline:before{content:"\f11b"}.ion-ios-backspace:before{content:"\f11d"}.ion-ios-backspace-outline:before{content:"\f11c"}.ion-ios-barcode:before{content:"\f3dc"}.ion-ios-barcode-outline:before{content:"\f3db"}.ion-ios-baseball:before{content:"\f3de"}.ion-ios-baseball-outline:before{content:"\f3dd"}.ion-ios-basket:before{content:"\f11f"}.ion-ios-basket-outline:before{content:"\f11e"}.ion-ios-basketball:before{content:"\f3e0"}.ion-ios-basketball-outline:before{content:"\f3df"}.ion-ios-battery-charging:before{content:"\f120"}.ion-ios-battery-charging-outline:before{content:"\f120"}.ion-ios-battery-dead:before{content:"\f121"}.ion-ios-battery-dead-outline:before{content:"\f121"}.ion-ios-battery-full:before{content:"\f122"}.ion-ios-battery-full-outline:before{content:"\f122"}.ion-ios-beaker:before{content:"\f124"}.ion-ios-beaker-outline:before{content:"\f123"}.ion-ios-beer:before{content:"\f126"}.ion-ios-beer-outline:before{content:"\f125"}.ion-ios-bicycle:before{content:"\f127"}.ion-ios-bicycle-outline:before{content:"\f127"}.ion-ios-bluetooth:before{content:"\f128"}.ion-ios-bluetooth-outline:before{content:"\f128"}.ion-ios-boat:before{content:"\f12a"}.ion-ios-boat-outline:before{content:"\f129"}.ion-ios-body:before{content:"\f3e4"}.ion-ios-body-outline:before{content:"\f3e3"}.ion-ios-bonfire:before{content:"\f12c"}.ion-ios-bonfire-outline:before{content:"\f12b"}.ion-ios-book:before{content:"\f3e8"}.ion-ios-book-outline:before{content:"\f3e7"}.ion-ios-bookmark:before{content:"\f12e"}.ion-ios-bookmark-outline:before{content:"\f12d"}.ion-ios-bookmarks:before{content:"\f3ea"}.ion-ios-bookmarks-outline:before{content:"\f3e9"}.ion-ios-bowtie:before{content:"\f130"}.ion-ios-bowtie-outline:before{content:"\f12f"}.ion-ios-briefcase:before{content:"\f3ee"}.ion-ios-briefcase-outline:before{content:"\f3ed"}.ion-ios-browsers:before{content:"\f3f0"}.ion-ios-browsers-outline:before{content:"\f3ef"}.ion-ios-brush:before{content:"\f132"}.ion-ios-brush-outline:before{content:"\f131"}.ion-ios-bug:before{content:"\f134"}.ion-ios-bug-outline:before{content:"\f133"}.ion-ios-build:before{content:"\f136"}.ion-ios-build-outline:before{content:"\f135"}.ion-ios-bulb:before{content:"\f138"}.ion-ios-bulb-outline:before{content:"\f137"}.ion-ios-bus:before{content:"\f13a"}.ion-ios-bus-outline:before{content:"\f139"}.ion-ios-cafe:before{content:"\f13c"}.ion-ios-cafe-outline:before{content:"\f13b"}.ion-ios-calculator:before{content:"\f3f2"}.ion-ios-calculator-outline:before{content:"\f3f1"}.ion-ios-calendar:before{content:"\f3f4"}.ion-ios-calendar-outline:before{content:"\f3f3"}.ion-ios-call:before{content:"\f13e"}.ion-ios-call-outline:before{content:"\f13d"}.ion-ios-camera:before{content:"\f3f6"}.ion-ios-camera-outline:before{content:"\f3f5"}.ion-ios-car:before{content:"\f140"}.ion-ios-car-outline:before{content:"\f13f"}.ion-ios-card:before{content:"\f142"}.ion-ios-card-outline:before{content:"\f141"}.ion-ios-cart:before{content:"\f3f8"}.ion-ios-cart-outline:before{content:"\f3f7"}.ion-ios-cash:before{content:"\f144"}.ion-ios-cash-outline:before{content:"\f143"}.ion-ios-chatboxes:before{content:"\f3fa"}.ion-ios-chatboxes-outline:before{content:"\f3f9"}.ion-ios-chatbubbles:before{content:"\f146"}.ion-ios-chatbubbles-outline:before{content:"\f145"}.ion-ios-checkbox:before{content:"\f148"}.ion-ios-checkbox-outline:before{content:"\f147"}.ion-ios-checkmark:before{content:"\f3ff"}.ion-ios-checkmark-circle:before{content:"\f14a"}.ion-ios-checkmark-circle-outline:before{content:"\f149"}.ion-ios-checkmark-outline:before{content:"\f3ff"}.ion-ios-clipboard:before{content:"\f14c"}.ion-ios-clipboard-outline:before{content:"\f14b"}.ion-ios-clock:before{content:"\f403"}.ion-ios-clock-outline:before{content:"\f402"}.ion-ios-close:before{content:"\f406"}.ion-ios-close-circle:before{content:"\f14e"}.ion-ios-close-circle-outline:before{content:"\f14d"}.ion-ios-close-outline:before{content:"\f406"}.ion-ios-closed-captioning:before{content:"\f150"}.ion-ios-closed-captioning-outline:before{content:"\f14f"}.ion-ios-cloud:before{content:"\f40c"}.ion-ios-cloud-circle:before{content:"\f152"}.ion-ios-cloud-circle-outline:before{content:"\f151"}.ion-ios-cloud-done:before{content:"\f154"}.ion-ios-cloud-done-outline:before{content:"\f153"}.ion-ios-cloud-download:before{content:"\f408"}.ion-ios-cloud-download-outline:before{content:"\f407"}.ion-ios-cloud-outline:before{content:"\f409"}.ion-ios-cloud-upload:before{content:"\f40b"}.ion-ios-cloud-upload-outline:before{content:"\f40a"}.ion-ios-cloudy:before{content:"\f410"}.ion-ios-cloudy-night:before{content:"\f40e"}.ion-ios-cloudy-night-outline:before{content:"\f40d"}.ion-ios-cloudy-outline:before{content:"\f40f"}.ion-ios-code:before{content:"\f157"}.ion-ios-code-download:before{content:"\f155"}.ion-ios-code-download-outline:before{content:"\f155"}.ion-ios-code-outline:before{content:"\f157"}.ion-ios-code-working:before{content:"\f156"}.ion-ios-code-working-outline:before{content:"\f156"}.ion-ios-cog:before{content:"\f412"}.ion-ios-cog-outline:before{content:"\f411"}.ion-ios-color-fill:before{content:"\f159"}.ion-ios-color-fill-outline:before{content:"\f158"}.ion-ios-color-filter:before{content:"\f414"}.ion-ios-color-filter-outline:before{content:"\f413"}.ion-ios-color-palette:before{content:"\f15b"}.ion-ios-color-palette-outline:before{content:"\f15a"}.ion-ios-color-wand:before{content:"\f416"}.ion-ios-color-wand-outline:before{content:"\f415"}.ion-ios-compass:before{content:"\f15d"}.ion-ios-compass-outline:before{content:"\f15c"}.ion-ios-construct:before{content:"\f15f"}.ion-ios-construct-outline:before{content:"\f15e"}.ion-ios-contact:before{content:"\f41a"}.ion-ios-contact-outline:before{content:"\f419"}.ion-ios-contacts:before{content:"\f161"}.ion-ios-contacts-outline:before{content:"\f160"}.ion-ios-contract:before{content:"\f162"}.ion-ios-contract-outline:before{content:"\f162"}.ion-ios-contrast:before{content:"\f163"}.ion-ios-contrast-outline:before{content:"\f163"}.ion-ios-copy:before{content:"\f41c"}.ion-ios-copy-outline:before{content:"\f41b"}.ion-ios-create:before{content:"\f165"}.ion-ios-create-outline:before{content:"\f164"}.ion-ios-crop:before{content:"\f41e"}.ion-ios-crop-outline:before{content:"\f166"}.ion-ios-cube:before{content:"\f168"}.ion-ios-cube-outline:before{content:"\f167"}.ion-ios-cut:before{content:"\f16a"}.ion-ios-cut-outline:before{content:"\f169"}.ion-ios-desktop:before{content:"\f16c"}.ion-ios-desktop-outline:before{content:"\f16b"}.ion-ios-disc:before{content:"\f16e"}.ion-ios-disc-outline:before{content:"\f16d"}.ion-ios-document:before{content:"\f170"}.ion-ios-document-outline:before{content:"\f16f"}.ion-ios-done-all:before{content:"\f171"}.ion-ios-done-all-outline:before{content:"\f171"}.ion-ios-download:before{content:"\f420"}.ion-ios-download-outline:before{content:"\f41f"}.ion-ios-easel:before{content:"\f173"}.ion-ios-easel-outline:before{content:"\f172"}.ion-ios-egg:before{content:"\f175"}.ion-ios-egg-outline:before{content:"\f174"}.ion-ios-exit:before{content:"\f177"}.ion-ios-exit-outline:before{content:"\f176"}.ion-ios-expand:before{content:"\f178"}.ion-ios-expand-outline:before{content:"\f178"}.ion-ios-eye:before{content:"\f425"}.ion-ios-eye-off:before{content:"\f17a"}.ion-ios-eye-off-outline:before{content:"\f179"}.ion-ios-eye-outline:before{content:"\f424"}.ion-ios-fastforward:before{content:"\f427"}.ion-ios-fastforward-outline:before{content:"\f426"}.ion-ios-female:before{content:"\f17b"}.ion-ios-female-outline:before{content:"\f17b"}.ion-ios-filing:before{content:"\f429"}.ion-ios-filing-outline:before{content:"\f428"}.ion-ios-film:before{content:"\f42b"}.ion-ios-film-outline:before{content:"\f42a"}.ion-ios-finger-print:before{content:"\f17c"}.ion-ios-finger-print-outline:before{content:"\f17c"}.ion-ios-flag:before{content:"\f42d"}.ion-ios-flag-outline:before{content:"\f42c"}.ion-ios-flame:before{content:"\f42f"}.ion-ios-flame-outline:before{content:"\f42e"}.ion-ios-flash:before{content:"\f17e"}.ion-ios-flash-outline:before{content:"\f17d"}.ion-ios-flask:before{content:"\f431"}.ion-ios-flask-outline:before{content:"\f430"}.ion-ios-flower:before{content:"\f433"}.ion-ios-flower-outline:before{content:"\f432"}.ion-ios-folder:before{content:"\f435"}.ion-ios-folder-open:before{content:"\f180"}.ion-ios-folder-open-outline:before{content:"\f17f"}.ion-ios-folder-outline:before{content:"\f434"}.ion-ios-football:before{content:"\f437"}.ion-ios-football-outline:before{content:"\f436"}.ion-ios-funnel:before{content:"\f182"}.ion-ios-funnel-outline:before{content:"\f181"}.ion-ios-game-controller-a:before{content:"\f439"}.ion-ios-game-controller-a-outline:before{content:"\f438"}.ion-ios-game-controller-b:before{content:"\f43b"}.ion-ios-game-controller-b-outline:before{content:"\f43a"}.ion-ios-git-branch:before{content:"\f183"}.ion-ios-git-branch-outline:before{content:"\f183"}.ion-ios-git-commit:before{content:"\f184"}.ion-ios-git-commit-outline:before{content:"\f184"}.ion-ios-git-compare:before{content:"\f185"}.ion-ios-git-compare-outline:before{content:"\f185"}.ion-ios-git-merge:before{content:"\f186"}.ion-ios-git-merge-outline:before{content:"\f186"}.ion-ios-git-network:before{content:"\f187"}.ion-ios-git-network-outline:before{content:"\f187"}.ion-ios-git-pull-request:before{content:"\f188"}.ion-ios-git-pull-request-outline:before{content:"\f188"}.ion-ios-glasses:before{content:"\f43f"}.ion-ios-glasses-outline:before{content:"\f43e"}.ion-ios-globe:before{content:"\f18a"}.ion-ios-globe-outline:before{content:"\f189"}.ion-ios-grid:before{content:"\f18c"}.ion-ios-grid-outline:before{content:"\f18b"}.ion-ios-hammer:before{content:"\f18e"}.ion-ios-hammer-outline:before{content:"\f18d"}.ion-ios-hand:before{content:"\f190"}.ion-ios-hand-outline:before{content:"\f18f"}.ion-ios-happy:before{content:"\f192"}.ion-ios-happy-outline:before{content:"\f191"}.ion-ios-headset:before{content:"\f194"}.ion-ios-headset-outline:before{content:"\f193"}.ion-ios-heart:before{content:"\f443"}.ion-ios-heart-outline:before{content:"\f442"}.ion-ios-help:before{content:"\f446"}.ion-ios-help-buoy:before{content:"\f196"}.ion-ios-help-buoy-outline:before{content:"\f195"}.ion-ios-help-circle:before{content:"\f198"}.ion-ios-help-circle-outline:before{content:"\f197"}.ion-ios-help-outline:before{content:"\f446"}.ion-ios-home:before{content:"\f448"}.ion-ios-home-outline:before{content:"\f447"}.ion-ios-ice-cream:before{content:"\f19a"}.ion-ios-ice-cream-outline:before{content:"\f199"}.ion-ios-image:before{content:"\f19c"}.ion-ios-image-outline:before{content:"\f19b"}.ion-ios-images:before{content:"\f19e"}.ion-ios-images-outline:before{content:"\f19d"}.ion-ios-infinite:before{content:"\f44a"}.ion-ios-infinite-outline:before{content:"\f449"}.ion-ios-information:before{content:"\f44d"}.ion-ios-information-circle:before{content:"\f1a0"}.ion-ios-information-circle-outline:before{content:"\f19f"}.ion-ios-information-outline:before{content:"\f44d"}.ion-ios-ionic:before{content:"\f1a1"}.ion-ios-ionic-outline:before{content:"\f44e"}.ion-ios-ionitron:before{content:"\f1a3"}.ion-ios-ionitron-outline:before{content:"\f1a2"}.ion-ios-jet:before{content:"\f1a5"}.ion-ios-jet-outline:before{content:"\f1a4"}.ion-ios-key:before{content:"\f1a7"}.ion-ios-key-outline:before{content:"\f1a6"}.ion-ios-keypad:before{content:"\f450"}.ion-ios-keypad-outline:before{content:"\f44f"}.ion-ios-laptop:before{content:"\f1a8"}.ion-ios-laptop-outline:before{content:"\f1a8"}.ion-ios-leaf:before{content:"\f1aa"}.ion-ios-leaf-outline:before{content:"\f1a9"}.ion-ios-link:before{content:"\f22a"}.ion-ios-link-outline:before{content:"\f1ca"}.ion-ios-list:before{content:"\f454"}.ion-ios-list-box:before{content:"\f1ac"}.ion-ios-list-box-outline:before{content:"\f1ab"}.ion-ios-list-outline:before{content:"\f454"}.ion-ios-locate:before{content:"\f1ae"}.ion-ios-locate-outline:before{content:"\f1ad"}.ion-ios-lock:before{content:"\f1b0"}.ion-ios-lock-outline:before{content:"\f1af"}.ion-ios-log-in:before{content:"\f1b1"}.ion-ios-log-in-outline:before{content:"\f1b1"}.ion-ios-log-out:before{content:"\f1b2"}.ion-ios-log-out-outline:before{content:"\f1b2"}.ion-ios-magnet:before{content:"\f1b4"}.ion-ios-magnet-outline:before{content:"\f1b3"}.ion-ios-mail:before{content:"\f1b8"}.ion-ios-mail-open:before{content:"\f1b6"}.ion-ios-mail-open-outline:before{content:"\f1b5"}.ion-ios-mail-outline:before{content:"\f1b7"}.ion-ios-male:before{content:"\f1b9"}.ion-ios-male-outline:before{content:"\f1b9"}.ion-ios-man:before{content:"\f1bb"}.ion-ios-man-outline:before{content:"\f1ba"}.ion-ios-map:before{content:"\f1bd"}.ion-ios-map-outline:before{content:"\f1bc"}.ion-ios-medal:before{content:"\f1bf"}.ion-ios-medal-outline:before{content:"\f1be"}.ion-ios-medical:before{content:"\f45c"}.ion-ios-medical-outline:before{content:"\f45b"}.ion-ios-medkit:before{content:"\f45e"}.ion-ios-medkit-outline:before{content:"\f45d"}.ion-ios-megaphone:before{content:"\f1c1"}.ion-ios-megaphone-outline:before{content:"\f1c0"}.ion-ios-menu:before{content:"\f1c3"}.ion-ios-menu-outline:before{content:"\f1c2"}.ion-ios-mic:before{content:"\f461"}.ion-ios-mic-off:before{content:"\f45f"}.ion-ios-mic-off-outline:before{content:"\f1c4"}.ion-ios-mic-outline:before{content:"\f460"}.ion-ios-microphone:before{content:"\f1c6"}.ion-ios-microphone-outline:before{content:"\f1c5"}.ion-ios-moon:before{content:"\f468"}.ion-ios-moon-outline:before{content:"\f467"}.ion-ios-more:before{content:"\f1c8"}.ion-ios-more-outline:before{content:"\f1c7"}.ion-ios-move:before{content:"\f1cb"}.ion-ios-move-outline:before{content:"\f1cb"}.ion-ios-musical-note:before{content:"\f46b"}.ion-ios-musical-note-outline:before{content:"\f1cc"}.ion-ios-musical-notes:before{content:"\f46c"}.ion-ios-musical-notes-outline:before{content:"\f1cd"}.ion-ios-navigate:before{content:"\f46e"}.ion-ios-navigate-outline:before{content:"\f46d"}.ion-ios-no-smoking:before{content:"\f1cf"}.ion-ios-no-smoking-outline:before{content:"\f1ce"}.ion-ios-notifications:before{content:"\f1d3"}.ion-ios-notifications-off:before{content:"\f1d1"}.ion-ios-notifications-off-outline:before{content:"\f1d0"}.ion-ios-notifications-outline:before{content:"\f1d2"}.ion-ios-nuclear:before{content:"\f1d5"}.ion-ios-nuclear-outline:before{content:"\f1d4"}.ion-ios-nutrition:before{content:"\f470"}.ion-ios-nutrition-outline:before{content:"\f46f"}.ion-ios-open:before{content:"\f1d7"}.ion-ios-open-outline:before{content:"\f1d6"}.ion-ios-options:before{content:"\f1d9"}.ion-ios-options-outline:before{content:"\f1d8"}.ion-ios-outlet:before{content:"\f1db"}.ion-ios-outlet-outline:before{content:"\f1da"}.ion-ios-paper:before{content:"\f472"}.ion-ios-paper-outline:before{content:"\f471"}.ion-ios-paper-plane:before{content:"\f1dd"}.ion-ios-paper-plane-outline:before{content:"\f1dc"}.ion-ios-partly-sunny:before{content:"\f1df"}.ion-ios-partly-sunny-outline:before{content:"\f1de"}.ion-ios-pause:before{content:"\f478"}.ion-ios-pause-outline:before{content:"\f477"}.ion-ios-paw:before{content:"\f47a"}.ion-ios-paw-outline:before{content:"\f479"}.ion-ios-people:before{content:"\f47c"}.ion-ios-people-outline:before{content:"\f47b"}.ion-ios-person:before{content:"\f47e"}.ion-ios-person-add:before{content:"\f1e1"}.ion-ios-person-add-outline:before{content:"\f1e0"}.ion-ios-person-outline:before{content:"\f47d"}.ion-ios-phone-landscape:before{content:"\f1e2"}.ion-ios-phone-landscape-outline:before{content:"\f1e2"}.ion-ios-phone-portrait:before{content:"\f1e3"}.ion-ios-phone-portrait-outline:before{content:"\f1e3"}.ion-ios-photos:before{content:"\f482"}.ion-ios-photos-outline:before{content:"\f481"}.ion-ios-pie:before{content:"\f484"}.ion-ios-pie-outline:before{content:"\f483"}.ion-ios-pin:before{content:"\f1e5"}.ion-ios-pin-outline:before{content:"\f1e4"}.ion-ios-pint:before{content:"\f486"}.ion-ios-pint-outline:before{content:"\f485"}.ion-ios-pizza:before{content:"\f1e7"}.ion-ios-pizza-outline:before{content:"\f1e6"}.ion-ios-plane:before{content:"\f1e9"}.ion-ios-plane-outline:before{content:"\f1e8"}.ion-ios-planet:before{content:"\f1eb"}.ion-ios-planet-outline:before{content:"\f1ea"}.ion-ios-play:before{content:"\f488"}.ion-ios-play-outline:before{content:"\f487"}.ion-ios-podium:before{content:"\f1ed"}.ion-ios-podium-outline:before{content:"\f1ec"}.ion-ios-power:before{content:"\f1ef"}.ion-ios-power-outline:before{content:"\f1ee"}.ion-ios-pricetag:before{content:"\f48d"}.ion-ios-pricetag-outline:before{content:"\f48c"}.ion-ios-pricetags:before{content:"\f48f"}.ion-ios-pricetags-outline:before{content:"\f48e"}.ion-ios-print:before{content:"\f1f1"}.ion-ios-print-outline:before{content:"\f1f0"}.ion-ios-pulse:before{content:"\f493"}.ion-ios-pulse-outline:before{content:"\f1f2"}.ion-ios-qr-scanner:before{content:"\f1f3"}.ion-ios-qr-scanner-outline:before{content:"\f1f3"}.ion-ios-quote:before{content:"\f1f5"}.ion-ios-quote-outline:before{content:"\f1f4"}.ion-ios-radio:before{content:"\f1f9"}.ion-ios-radio-button-off:before{content:"\f1f6"}.ion-ios-radio-button-off-outline:before{content:"\f1f6"}.ion-ios-radio-button-on:before{content:"\f1f7"}.ion-ios-radio-button-on-outline:before{content:"\f1f7"}.ion-ios-radio-outline:before{content:"\f1f8"}.ion-ios-rainy:before{content:"\f495"}.ion-ios-rainy-outline:before{content:"\f494"}.ion-ios-recording:before{content:"\f497"}.ion-ios-recording-outline:before{content:"\f496"}.ion-ios-redo:before{content:"\f499"}.ion-ios-redo-outline:before{content:"\f498"}.ion-ios-refresh:before{content:"\f49c"}.ion-ios-refresh-circle:before{content:"\f226"}.ion-ios-refresh-circle-outline:before{content:"\f224"}.ion-ios-refresh-outline:before{content:"\f49c"}.ion-ios-remove:before{content:"\f1fc"}.ion-ios-remove-circle:before{content:"\f1fb"}.ion-ios-remove-circle-outline:before{content:"\f1fa"}.ion-ios-remove-outline:before{content:"\f1fc"}.ion-ios-reorder:before{content:"\f1fd"}.ion-ios-reorder-outline:before{content:"\f1fd"}.ion-ios-repeat:before{content:"\f1fe"}.ion-ios-repeat-outline:before{content:"\f1fe"}.ion-ios-resize:before{content:"\f1ff"}.ion-ios-resize-outline:before{content:"\f1ff"}.ion-ios-restaurant:before{content:"\f201"}.ion-ios-restaurant-outline:before{content:"\f200"}.ion-ios-return-left:before{content:"\f202"}.ion-ios-return-left-outline:before{content:"\f202"}.ion-ios-return-right:before{content:"\f203"}.ion-ios-return-right-outline:before{content:"\f203"}.ion-ios-reverse-camera:before{content:"\f49f"}.ion-ios-reverse-camera-outline:before{content:"\f49e"}.ion-ios-rewind:before{content:"\f4a1"}.ion-ios-rewind-outline:before{content:"\f4a0"}.ion-ios-ribbon:before{content:"\f205"}.ion-ios-ribbon-outline:before{content:"\f204"}.ion-ios-rose:before{content:"\f4a3"}.ion-ios-rose-outline:before{content:"\f4a2"}.ion-ios-sad:before{content:"\f207"}.ion-ios-sad-outline:before{content:"\f206"}.ion-ios-school:before{content:"\f209"}.ion-ios-school-outline:before{content:"\f208"}.ion-ios-search:before{content:"\f4a5"}.ion-ios-search-outline:before{content:"\f20a"}.ion-ios-send:before{content:"\f20c"}.ion-ios-send-outline:before{content:"\f20b"}.ion-ios-settings:before{content:"\f4a7"}.ion-ios-settings-outline:before{content:"\f20d"}.ion-ios-share:before{content:"\f211"}.ion-ios-share-alt:before{content:"\f20f"}.ion-ios-share-alt-outline:before{content:"\f20e"}.ion-ios-share-outline:before{content:"\f210"}.ion-ios-shirt:before{content:"\f213"}.ion-ios-shirt-outline:before{content:"\f212"}.ion-ios-shuffle:before{content:"\f4a9"}.ion-ios-shuffle-outline:before{content:"\f4a9"}.ion-ios-skip-backward:before{content:"\f215"}.ion-ios-skip-backward-outline:before{content:"\f214"}.ion-ios-skip-forward:before{content:"\f217"}.ion-ios-skip-forward-outline:before{content:"\f216"}.ion-ios-snow:before{content:"\f218"}.ion-ios-snow-outline:before{content:"\f22c"}.ion-ios-speedometer:before{content:"\f4b0"}.ion-ios-speedometer-outline:before{content:"\f4af"}.ion-ios-square:before{content:"\f21a"}.ion-ios-square-outline:before{content:"\f219"}.ion-ios-star:before{content:"\f4b3"}.ion-ios-star-half:before{content:"\f4b1"}.ion-ios-star-half-outline:before{content:"\f4b1"}.ion-ios-star-outline:before{content:"\f4b2"}.ion-ios-stats:before{content:"\f21c"}.ion-ios-stats-outline:before{content:"\f21b"}.ion-ios-stopwatch:before{content:"\f4b5"}.ion-ios-stopwatch-outline:before{content:"\f4b4"}.ion-ios-subway:before{content:"\f21e"}.ion-ios-subway-outline:before{content:"\f21d"}.ion-ios-sunny:before{content:"\f4b7"}.ion-ios-sunny-outline:before{content:"\f4b6"}.ion-ios-swap:before{content:"\f21f"}.ion-ios-swap-outline:before{content:"\f21f"}.ion-ios-switch:before{content:"\f221"}.ion-ios-switch-outline:before{content:"\f220"}.ion-ios-sync:before{content:"\f222"}.ion-ios-sync-outline:before{content:"\f222"}.ion-ios-tablet-landscape:before{content:"\f223"}.ion-ios-tablet-landscape-outline:before{content:"\f223"}.ion-ios-tablet-portrait:before{content:"\f24e"}.ion-ios-tablet-portrait-outline:before{content:"\f24e"}.ion-ios-tennisball:before{content:"\f4bb"}.ion-ios-tennisball-outline:before{content:"\f4ba"}.ion-ios-text:before{content:"\f250"}.ion-ios-text-outline:before{content:"\f24f"}.ion-ios-thermometer:before{content:"\f252"}.ion-ios-thermometer-outline:before{content:"\f251"}.ion-ios-thumbs-down:before{content:"\f254"}.ion-ios-thumbs-down-outline:before{content:"\f253"}.ion-ios-thumbs-up:before{content:"\f256"}.ion-ios-thumbs-up-outline:before{content:"\f255"}.ion-ios-thunderstorm:before{content:"\f4bd"}.ion-ios-thunderstorm-outline:before{content:"\f4bc"}.ion-ios-time:before{content:"\f4bf"}.ion-ios-time-outline:before{content:"\f4be"}.ion-ios-timer:before{content:"\f4c1"}.ion-ios-timer-outline:before{content:"\f4c0"}.ion-ios-train:before{content:"\f258"}.ion-ios-train-outline:before{content:"\f257"}.ion-ios-transgender:before{content:"\f259"}.ion-ios-transgender-outline:before{content:"\f259"}.ion-ios-trash:before{content:"\f4c5"}.ion-ios-trash-outline:before{content:"\f4c4"}.ion-ios-trending-down:before{content:"\f25a"}.ion-ios-trending-down-outline:before{content:"\f25a"}.ion-ios-trending-up:before{content:"\f25b"}.ion-ios-trending-up-outline:before{content:"\f25b"}.ion-ios-trophy:before{content:"\f25d"}.ion-ios-trophy-outline:before{content:"\f25c"}.ion-ios-umbrella:before{content:"\f25f"}.ion-ios-umbrella-outline:before{content:"\f25e"}.ion-ios-undo:before{content:"\f4c7"}.ion-ios-undo-outline:before{content:"\f4c6"}.ion-ios-unlock:before{content:"\f261"}.ion-ios-unlock-outline:before{content:"\f260"}.ion-ios-videocam:before{content:"\f4cd"}.ion-ios-videocam-outline:before{content:"\f4cc"}.ion-ios-volume-down:before{content:"\f262"}.ion-ios-volume-down-outline:before{content:"\f262"}.ion-ios-volume-mute:before{content:"\f263"}.ion-ios-volume-mute-outline:before{content:"\f263"}.ion-ios-volume-off:before{content:"\f264"}.ion-ios-volume-off-outline:before{content:"\f264"}.ion-ios-volume-up:before{content:"\f265"}.ion-ios-volume-up-outline:before{content:"\f265"}.ion-ios-walk:before{content:"\f266"}.ion-ios-walk-outline:before{content:"\f266"}.ion-ios-warning:before{content:"\f268"}.ion-ios-warning-outline:before{content:"\f267"}.ion-ios-watch:before{content:"\f269"}.ion-ios-watch-outline:before{content:"\f269"}.ion-ios-water:before{content:"\f26b"}.ion-ios-water-outline:before{content:"\f26a"}.ion-ios-wifi:before{content:"\f26d"}.ion-ios-wifi-outline:before{content:"\f26c"}.ion-ios-wine:before{content:"\f26f"}.ion-ios-wine-outline:before{content:"\f26e"}.ion-ios-woman:before{content:"\f271"}.ion-ios-woman-outline:before{content:"\f270"}.ion-logo-android:before{content:"\f225"}.ion-logo-angular:before{content:"\f227"}.ion-logo-apple:before{content:"\f229"}.ion-logo-bitcoin:before{content:"\f22b"}.ion-logo-buffer:before{content:"\f22d"}.ion-logo-chrome:before{content:"\f22f"}.ion-logo-codepen:before{content:"\f230"}.ion-logo-css3:before{content:"\f231"}.ion-logo-designernews:before{content:"\f232"}.ion-logo-dribbble:before{content:"\f233"}.ion-logo-dropbox:before{content:"\f234"}.ion-logo-euro:before{content:"\f235"}.ion-logo-facebook:before{content:"\f236"}.ion-logo-foursquare:before{content:"\f237"}.ion-logo-freebsd-devil:before{content:"\f238"}.ion-logo-github:before{content:"\f239"}.ion-logo-google:before{content:"\f23a"}.ion-logo-googleplus:before{content:"\f23b"}.ion-logo-hackernews:before{content:"\f23c"}.ion-logo-html5:before{content:"\f23d"}.ion-logo-instagram:before{content:"\f23e"}.ion-logo-javascript:before{content:"\f23f"}.ion-logo-linkedin:before{content:"\f240"}.ion-logo-markdown:before{content:"\f241"}.ion-logo-nodejs:before{content:"\f242"}.ion-logo-octocat:before{content:"\f243"}.ion-logo-pinterest:before{content:"\f244"}.ion-logo-playstation:before{content:"\f245"}.ion-logo-python:before{content:"\f246"}.ion-logo-reddit:before{content:"\f247"}.ion-logo-rss:before{content:"\f248"}.ion-logo-sass:before{content:"\f249"}.ion-logo-skype:before{content:"\f24a"}.ion-logo-snapchat:before{content:"\f24b"}.ion-logo-steam:before{content:"\f24c"}.ion-logo-tumblr:before{content:"\f24d"}.ion-logo-tux:before{content:"\f2ae"}.ion-logo-twitch:before{content:"\f2af"}.ion-logo-twitter:before{content:"\f2b0"}.ion-logo-usd:before{content:"\f2b1"}.ion-logo-vimeo:before{content:"\f2c4"}.ion-logo-whatsapp:before{content:"\f2c5"}.ion-logo-windows:before{content:"\f32f"}.ion-logo-wordpress:before{content:"\f330"}.ion-logo-xbox:before{content:"\f34c"}.ion-logo-yahoo:before{content:"\f34d"}.ion-logo-yen:before{content:"\f34e"}.ion-logo-youtube:before{content:"\f34f"}.ion-md-add:before{content:"\f273"}.ion-md-add-circle:before{content:"\f272"}.ion-md-alarm:before{content:"\f274"}.ion-md-albums:before{content:"\f275"}.ion-md-alert:before{content:"\f276"}.ion-md-american-football:before{content:"\f277"}.ion-md-analytics:before{content:"\f278"}.ion-md-aperture:before{content:"\f279"}.ion-md-apps:before{content:"\f27a"}.ion-md-appstore:before{content:"\f27b"}.ion-md-archive:before{content:"\f27c"}.ion-md-arrow-back:before{content:"\f27d"}.ion-md-arrow-down:before{content:"\f27e"}.ion-md-arrow-dropdown:before{content:"\f280"}.ion-md-arrow-dropdown-circle:before{content:"\f27f"}.ion-md-arrow-dropleft:before{content:"\f282"}.ion-md-arrow-dropleft-circle:before{content:"\f281"}.ion-md-arrow-dropright:before{content:"\f284"}.ion-md-arrow-dropright-circle:before{content:"\f283"}.ion-md-arrow-dropup:before{content:"\f286"}.ion-md-arrow-dropup-circle:before{content:"\f285"}.ion-md-arrow-forward:before{content:"\f287"}.ion-md-arrow-round-back:before{content:"\f288"}.ion-md-arrow-round-down:before{content:"\f289"}.ion-md-arrow-round-forward:before{content:"\f28a"}.ion-md-arrow-round-up:before{content:"\f28b"}.ion-md-arrow-up:before{content:"\f28c"}.ion-md-at:before{content:"\f28d"}.ion-md-attach:before{content:"\f28e"}.ion-md-backspace:before{content:"\f28f"}.ion-md-barcode:before{content:"\f290"}.ion-md-baseball:before{content:"\f291"}.ion-md-basket:before{content:"\f292"}.ion-md-basketball:before{content:"\f293"}.ion-md-battery-charging:before{content:"\f294"}.ion-md-battery-dead:before{content:"\f295"}.ion-md-battery-full:before{content:"\f296"}.ion-md-beaker:before{content:"\f297"}.ion-md-beer:before{content:"\f298"}.ion-md-bicycle:before{content:"\f299"}.ion-md-bluetooth:before{content:"\f29a"}.ion-md-boat:before{content:"\f29b"}.ion-md-body:before{content:"\f29c"}.ion-md-bonfire:before{content:"\f29d"}.ion-md-book:before{content:"\f29e"}.ion-md-bookmark:before{content:"\f29f"}.ion-md-bookmarks:before{content:"\f2a0"}.ion-md-bowtie:before{content:"\f2a1"}.ion-md-briefcase:before{content:"\f2a2"}.ion-md-browsers:before{content:"\f2a3"}.ion-md-brush:before{content:"\f2a4"}.ion-md-bug:before{content:"\f2a5"}.ion-md-build:before{content:"\f2a6"}.ion-md-bulb:before{content:"\f2a7"}.ion-md-bus:before{content:"\f2a8"}.ion-md-cafe:before{content:"\f2a9"}.ion-md-calculator:before{content:"\f2aa"}.ion-md-calendar:before{content:"\f2ab"}.ion-md-call:before{content:"\f2ac"}.ion-md-camera:before{content:"\f2ad"}.ion-md-car:before{content:"\f2b2"}.ion-md-card:before{content:"\f2b3"}.ion-md-cart:before{content:"\f2b4"}.ion-md-cash:before{content:"\f2b5"}.ion-md-chatboxes:before{content:"\f2b6"}.ion-md-chatbubbles:before{content:"\f2b7"}.ion-md-checkbox:before{content:"\f2b9"}.ion-md-checkbox-outline:before{content:"\f2b8"}.ion-md-checkmark:before{content:"\f2bc"}.ion-md-checkmark-circle:before{content:"\f2bb"}.ion-md-checkmark-circle-outline:before{content:"\f2ba"}.ion-md-clipboard:before{content:"\f2bd"}.ion-md-clock:before{content:"\f2be"}.ion-md-close:before{content:"\f2c0"}.ion-md-close-circle:before{content:"\f2bf"}.ion-md-closed-captioning:before{content:"\f2c1"}.ion-md-cloud:before{content:"\f2c9"}.ion-md-cloud-circle:before{content:"\f2c2"}.ion-md-cloud-done:before{content:"\f2c3"}.ion-md-cloud-download:before{content:"\f2c6"}.ion-md-cloud-outline:before{content:"\f2c7"}.ion-md-cloud-upload:before{content:"\f2c8"}.ion-md-cloudy:before{content:"\f2cb"}.ion-md-cloudy-night:before{content:"\f2ca"}.ion-md-code:before{content:"\f2ce"}.ion-md-code-download:before{content:"\f2cc"}.ion-md-code-working:before{content:"\f2cd"}.ion-md-cog:before{content:"\f2cf"}.ion-md-color-fill:before{content:"\f2d0"}.ion-md-color-filter:before{content:"\f2d1"}.ion-md-color-palette:before{content:"\f2d2"}.ion-md-color-wand:before{content:"\f2d3"}.ion-md-compass:before{content:"\f2d4"}.ion-md-construct:before{content:"\f2d5"}.ion-md-contact:before{content:"\f2d6"}.ion-md-contacts:before{content:"\f2d7"}.ion-md-contract:before{content:"\f2d8"}.ion-md-contrast:before{content:"\f2d9"}.ion-md-copy:before{content:"\f2da"}.ion-md-create:before{content:"\f2db"}.ion-md-crop:before{content:"\f2dc"}.ion-md-cube:before{content:"\f2dd"}.ion-md-cut:before{content:"\f2de"}.ion-md-desktop:before{content:"\f2df"}.ion-md-disc:before{content:"\f2e0"}.ion-md-document:before{content:"\f2e1"}.ion-md-done-all:before{content:"\f2e2"}.ion-md-download:before{content:"\f2e3"}.ion-md-easel:before{content:"\f2e4"}.ion-md-egg:before{content:"\f2e5"}.ion-md-exit:before{content:"\f2e6"}.ion-md-expand:before{content:"\f2e7"}.ion-md-eye:before{content:"\f2e9"}.ion-md-eye-off:before{content:"\f2e8"}.ion-md-fastforward:before{content:"\f2ea"}.ion-md-female:before{content:"\f2eb"}.ion-md-filing:before{content:"\f2ec"}.ion-md-film:before{content:"\f2ed"}.ion-md-finger-print:before{content:"\f2ee"}.ion-md-flag:before{content:"\f2ef"}.ion-md-flame:before{content:"\f2f0"}.ion-md-flash:before{content:"\f2f1"}.ion-md-flask:before{content:"\f2f2"}.ion-md-flower:before{content:"\f2f3"}.ion-md-folder:before{content:"\f2f5"}.ion-md-folder-open:before{content:"\f2f4"}.ion-md-football:before{content:"\f2f6"}.ion-md-funnel:before{content:"\f2f7"}.ion-md-game-controller-a:before{content:"\f2f8"}.ion-md-game-controller-b:before{content:"\f2f9"}.ion-md-git-branch:before{content:"\f2fa"}.ion-md-git-commit:before{content:"\f2fb"}.ion-md-git-compare:before{content:"\f2fc"}.ion-md-git-merge:before{content:"\f2fd"}.ion-md-git-network:before{content:"\f2fe"}.ion-md-git-pull-request:before{content:"\f2ff"}.ion-md-glasses:before{content:"\f300"}.ion-md-globe:before{content:"\f301"}.ion-md-grid:before{content:"\f302"}.ion-md-hammer:before{content:"\f303"}.ion-md-hand:before{content:"\f304"}.ion-md-happy:before{content:"\f305"}.ion-md-headset:before{content:"\f306"}.ion-md-heart:before{content:"\f308"}.ion-md-heart-outline:before{content:"\f307"}.ion-md-help:before{content:"\f30b"}.ion-md-help-buoy:before{content:"\f309"}.ion-md-help-circle:before{content:"\f30a"}.ion-md-home:before{content:"\f30c"}.ion-md-ice-cream:before{content:"\f30d"}.ion-md-image:before{content:"\f30e"}.ion-md-images:before{content:"\f30f"}.ion-md-infinite:before{content:"\f310"}.ion-md-information:before{content:"\f312"}.ion-md-information-circle:before{content:"\f311"}.ion-md-ionic:before{content:"\f313"}.ion-md-ionitron:before{content:"\f314"}.ion-md-jet:before{content:"\f315"}.ion-md-key:before{content:"\f316"}.ion-md-keypad:before{content:"\f317"}.ion-md-laptop:before{content:"\f318"}.ion-md-leaf:before{content:"\f319"}.ion-md-link:before{content:"\f22e"}.ion-md-list:before{content:"\f31b"}.ion-md-list-box:before{content:"\f31a"}.ion-md-locate:before{content:"\f31c"}.ion-md-lock:before{content:"\f31d"}.ion-md-log-in:before{content:"\f31e"}.ion-md-log-out:before{content:"\f31f"}.ion-md-magnet:before{content:"\f320"}.ion-md-mail:before{content:"\f322"}.ion-md-mail-open:before{content:"\f321"}.ion-md-male:before{content:"\f323"}.ion-md-man:before{content:"\f324"}.ion-md-map:before{content:"\f325"}.ion-md-medal:before{content:"\f326"}.ion-md-medical:before{content:"\f327"}.ion-md-medkit:before{content:"\f328"}.ion-md-megaphone:before{content:"\f329"}.ion-md-menu:before{content:"\f32a"}.ion-md-mic:before{content:"\f32c"}.ion-md-mic-off:before{content:"\f32b"}.ion-md-microphone:before{content:"\f32d"}.ion-md-moon:before{content:"\f32e"}.ion-md-more:before{content:"\f1c9"}.ion-md-move:before{content:"\f331"}.ion-md-musical-note:before{content:"\f332"}.ion-md-musical-notes:before{content:"\f333"}.ion-md-navigate:before{content:"\f334"}.ion-md-no-smoking:before{content:"\f335"}.ion-md-notifications:before{content:"\f338"}.ion-md-notifications-off:before{content:"\f336"}.ion-md-notifications-outline:before{content:"\f337"}.ion-md-nuclear:before{content:"\f339"}.ion-md-nutrition:before{content:"\f33a"}.ion-md-open:before{content:"\f33b"}.ion-md-options:before{content:"\f33c"}.ion-md-outlet:before{content:"\f33d"}.ion-md-paper:before{content:"\f33f"}.ion-md-paper-plane:before{content:"\f33e"}.ion-md-partly-sunny:before{content:"\f340"}.ion-md-pause:before{content:"\f341"}.ion-md-paw:before{content:"\f342"}.ion-md-people:before{content:"\f343"}.ion-md-person:before{content:"\f345"}.ion-md-person-add:before{content:"\f344"}.ion-md-phone-landscape:before{content:"\f346"}.ion-md-phone-portrait:before{content:"\f347"}.ion-md-photos:before{content:"\f348"}.ion-md-pie:before{content:"\f349"}.ion-md-pin:before{content:"\f34a"}.ion-md-pint:before{content:"\f34b"}.ion-md-pizza:before{content:"\f354"}.ion-md-plane:before{content:"\f355"}.ion-md-planet:before{content:"\f356"}.ion-md-play:before{content:"\f357"}.ion-md-podium:before{content:"\f358"}.ion-md-power:before{content:"\f359"}.ion-md-pricetag:before{content:"\f35a"}.ion-md-pricetags:before{content:"\f35b"}.ion-md-print:before{content:"\f35c"}.ion-md-pulse:before{content:"\f35d"}.ion-md-qr-scanner:before{content:"\f35e"}.ion-md-quote:before{content:"\f35f"}.ion-md-radio:before{content:"\f362"}.ion-md-radio-button-off:before{content:"\f360"}.ion-md-radio-button-on:before{content:"\f361"}.ion-md-rainy:before{content:"\f363"}.ion-md-recording:before{content:"\f364"}.ion-md-redo:before{content:"\f365"}.ion-md-refresh:before{content:"\f366"}.ion-md-refresh-circle:before{content:"\f228"}.ion-md-remove:before{content:"\f368"}.ion-md-remove-circle:before{content:"\f367"}.ion-md-reorder:before{content:"\f369"}.ion-md-repeat:before{content:"\f36a"}.ion-md-resize:before{content:"\f36b"}.ion-md-restaurant:before{content:"\f36c"}.ion-md-return-left:before{content:"\f36d"}.ion-md-return-right:before{content:"\f36e"}.ion-md-reverse-camera:before{content:"\f36f"}.ion-md-rewind:before{content:"\f370"}.ion-md-ribbon:before{content:"\f371"}.ion-md-rose:before{content:"\f372"}.ion-md-sad:before{content:"\f373"}.ion-md-school:before{content:"\f374"}.ion-md-search:before{content:"\f375"}.ion-md-send:before{content:"\f376"}.ion-md-settings:before{content:"\f377"}.ion-md-share:before{content:"\f379"}.ion-md-share-alt:before{content:"\f378"}.ion-md-shirt:before{content:"\f37a"}.ion-md-shuffle:before{content:"\f37b"}.ion-md-skip-backward:before{content:"\f37c"}.ion-md-skip-forward:before{content:"\f37d"}.ion-md-snow:before{content:"\f37e"}.ion-md-speedometer:before{content:"\f37f"}.ion-md-square:before{content:"\f381"}.ion-md-square-outline:before{content:"\f380"}.ion-md-star:before{content:"\f384"}.ion-md-star-half:before{content:"\f382"}.ion-md-star-outline:before{content:"\f383"}.ion-md-stats:before{content:"\f385"}.ion-md-stopwatch:before{content:"\f386"}.ion-md-subway:before{content:"\f387"}.ion-md-sunny:before{content:"\f388"}.ion-md-swap:before{content:"\f389"}.ion-md-switch:before{content:"\f38a"}.ion-md-sync:before{content:"\f38b"}.ion-md-tablet-landscape:before{content:"\f38c"}.ion-md-tablet-portrait:before{content:"\f38d"}.ion-md-tennisball:before{content:"\f38e"}.ion-md-text:before{content:"\f38f"}.ion-md-thermometer:before{content:"\f390"}.ion-md-thumbs-down:before{content:"\f391"}.ion-md-thumbs-up:before{content:"\f392"}.ion-md-thunderstorm:before{content:"\f393"}.ion-md-time:before{content:"\f394"}.ion-md-timer:before{content:"\f395"}.ion-md-train:before{content:"\f396"}.ion-md-transgender:before{content:"\f397"}.ion-md-trash:before{content:"\f398"}.ion-md-trending-down:before{content:"\f399"}.ion-md-trending-up:before{content:"\f39a"}.ion-md-trophy:before{content:"\f39b"}.ion-md-umbrella:before{content:"\f39c"}.ion-md-undo:before{content:"\f39d"}.ion-md-unlock:before{content:"\f39e"}.ion-md-videocam:before{content:"\f39f"}.ion-md-volume-down:before{content:"\f3a0"}.ion-md-volume-mute:before{content:"\f3a1"}.ion-md-volume-off:before{content:"\f3a2"}.ion-md-volume-up:before{content:"\f3a3"}.ion-md-walk:before{content:"\f3a4"}.ion-md-warning:before{content:"\f3a5"}.ion-md-watch:before{content:"\f3a6"}.ion-md-water:before{content:"\f3a7"}.ion-md-wifi:before{content:"\f3a8"}.ion-md-wine:before{content:"\f3a9"}.ion-md-woman:before{content:"\f3aa"}.icheckbox_square-blue,.iradio_square-blue{display:inline-block;*display:inline;vertical-align:middle;margin:0;padding:0;width:22px;height:22px;background:url(../node_modules/icheck/skins/square/blue.png) no-repeat;border:none;cursor:pointer}.icheckbox_square-blue{background-position:0 0}.icheckbox_square-blue.hover{background-position:-24px 0}.icheckbox_square-blue.checked{background-position:-48px 0}.icheckbox_square-blue.disabled{background-position:-72px 0;cursor:default}.icheckbox_square-blue.checked.disabled{background-position:-96px 0}.iradio_square-blue{background-position:-120px 0}.iradio_square-blue.hover{background-position:-144px 0}.iradio_square-blue.checked{background-position:-168px 0}.iradio_square-blue.disabled{background-position:-192px 0;cursor:default}.iradio_square-blue.checked.disabled{background-position:-216px 0}@media(-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-resolution:120dpi){.icheckbox_square-blue,.iradio_square-blue{background-image:url(../node_modules/icheck/skins/square/blue@2x.png);-webkit-background-size:240px 24px;background-size:240px 24px}}/*!* AdminLTE v2.4.8 * Author:Almsaeed Studio * Website:Almsaeed Studio * License:Open source - MIT * Please visit http://opensource.org/licenses/MIT for more information */ html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background-color:#f9fafc}.content-wrapper,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer{margin-left:0}@media(max-width:767px){.content-wrapper,.main-footer{margin-left:0}}@media(min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer{margin-left:0}}@media(max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .main-footer{-webkit-transform:translate(230px,0);-ms-transform:translate(230px,0);-o-transform:translate(230px,0);transform:translate(230px,0)}}.content-wrapper{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media(max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}.fixed .wrapper{overflow:hidden}.hold-transition .content-wrapper,.hold-transition .right-side,.hold-transition .main-footer,.hold-transition .main-sidebar,.hold-transition .left-side,.hold-transition .main-header .navbar,.hold-transition .main-header .logo,.hold-transition .menu-open .fa-angle-left{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#3c8dbc}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#72afd2}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media(max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media(max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media(max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media(max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media(max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media(max-width:991px){.navbar-collapse.pull-left{float:none!important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media(max-width:767px){.main-sidebar{padding-top:100px}}@media(max-width:767px){.main-sidebar{-webkit-transform:translate(-230px,0);-ms-transform:translate(-230px,0);-o-transform:translate(-230px,0);transform:translate(-230px,0)}}@media(min-width:768px){.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px,0);-ms-transform:translate(-230px,0);-o-transform:translate(-230px,0);transform:translate(-230px,0)}}@media(max-width:767px){.sidebar-open .main-sidebar{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;-webkit-transition:transform .5s ease;-o-transition:transform .5s ease;transition:transform .5s ease}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu .menu-open>a>.fa-angle-left,.sidebar-menu .menu-open>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu .active>.treeview-menu{display:block}@media(min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px!important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0);width:50px!important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none!important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}@media(min-width:768px){.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block!important;position:absolute;width:180px;left:50px}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative!important;float:right;width:auto!important;left:180px!important;top:-22px!important;z-index:900}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}}.sidebar-expanded-on-hover .main-footer,.sidebar-expanded-on-hover .content-wrapper{margin-left:50px}.sidebar-expanded-on-hover .main-sidebar{box-shadow:3px 0 8px rgba(0,0,0,0.125)}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media(max-width:767px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media(min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.fixed .control-sidebar{position:fixed;height:100%;overflow-y:auto;padding-bottom:50px}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media(max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#b8c7ce}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#222d32}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#181f23;color:#b8c7ce}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#141a1d;border-bottom-color:#141a1d}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#222d32;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1e282c}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b8c7ce}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#fff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eee;color:#444!important;text-align:center}@media(max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff!important;color:#444!important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #ddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444!important}@media(max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff!important;color:#444!important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666}@media(max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media(max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible!important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media(max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#3c8dbc;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#3c8dbc}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media(max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#fff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#3c8dbc}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #3c8dbc}.box.box-solid.box-primary>.box-header{color:#fff;background:#3c8dbc;background-color:#3c8dbc}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media(max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de!important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#3c8dbc}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#3c8dbc;border-color:#367fa9}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#367fa9}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#3c8dbc}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#3c8dbc}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li.disabled>a{color:#777}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none!important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0!important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.direct-chat-messages{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.direct-chat-contacts{-webkit-transform:translate(101%,0);-ms-transform:translate(101%,0);-o-transform:translate(101%,0);transform:translate(101%,0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#3c8dbc}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media(min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#307095}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.treeview-menu .treeview-menu{padding-left:20px}.treeview-menu>li{margin:0}.treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.treeview-menu>li>a>.fa,.treeview-menu>li>a>.glyphicon,.treeview-menu>li>a>.ion{width:20px}.treeview-menu>li>a>.pull-right-container>.fa-angle-left,.treeview-menu>li>a>.pull-right-container>.fa-angle-down,.treeview-menu>li>a>.fa-angle-left,.treeview-menu>li>a>.fa-angle-down{width:auto}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media(max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media(max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media(max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media(max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media(max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#3c8dbc}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-search--dropdown .select2-search__field{border-color:#3c8dbc!important}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#3c8dbc}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.box .datepicker-inline,.box .datepicker-inline .datepicker-days,.box .datepicker-inline>table,.box .datepicker-inline .datepicker-days>table{width:100%}.box .datepicker-inline td:hover,.box .datepicker-inline .datepicker-days td:hover,.box .datepicker-inline>table td:hover,.box .datepicker-inline .datepicker-days>table td:hover{background-color:rgba(255,255,255,0.3)}.box .datepicker-inline td.day.old,.box .datepicker-inline .datepicker-days td.day.old,.box .datepicker-inline>table td.day.old,.box .datepicker-inline .datepicker-days>table td.day.old,.box .datepicker-inline td.day.new,.box .datepicker-inline .datepicker-days td.day.new,.box .datepicker-inline>table td.day.new,.box .datepicker-inline .datepicker-days>table td.day.new{color:#777}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff!important}.bg-gray{color:#000;background-color:#d2d6de!important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111!important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39!important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12!important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef!important}.bg-blue{background-color:#0073b7!important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#3c8dbc!important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a!important}.bg-navy{background-color:#001f3f!important}.bg-teal{background-color:#39cccc!important}.bg-olive{background-color:#3d9970!important}.bg-lime{background-color:#01ff70!important}.bg-orange{background-color:#ff851b!important}.bg-fuchsia{background-color:#f012be!important}.bg-purple{background-color:#605ca8!important}.bg-maroon{background-color:#d81b60!important}.bg-gray-active{color:#000;background-color:#b5bbc8!important}.bg-black-active{background-color:#000!important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724!important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b!important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0!important}.bg-blue-active{background-color:#005384!important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#357ca5!important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c!important}.bg-navy-active{background-color:#001a35!important}.bg-teal-active{background-color:#30bbbb!important}.bg-olive-active{background-color:#368763!important}.bg-lime-active{background-color:#00e765!important}.bg-orange-active{background-color:#ff7701!important}.bg-fuchsia-active{background-color:#db0ead!important}.bg-purple-active{background-color:#555299!important}.bg-maroon-active{background-color:#ca195a!important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39!important}.text-yellow{color:#f39c12!important}.text-aqua{color:#00c0ef!important}.text-blue{color:#0073b7!important}.text-black{color:#111!important}.text-light-blue{color:#3c8dbc!important}.text-green{color:#00a65a!important}.text-gray{color:#d2d6de!important}.text-navy{color:#001f3f!important}.text-teal{color:#39cccc!important}.text-olive{color:#3d9970!important}.text-lime{color:#01ff70!important}.text-orange{color:#ff851b!important}.text-fuchsia{color:#f012be!important}.text-purple{color:#605ca8!important}.text-maroon{color:#d81b60!important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none!important}.no-border{border:0!important}.no-padding{padding:0!important}.no-margin{margin:0!important}.no-shadow{box-shadow:none!important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0!important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px!important;width:auto!important;height:auto!important}.bg-teal-gradient{background:#39cccc!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#39cccc),color-stop(1,#7adddd))!important;background:-ms-linear-gradient(bottom,#39cccc,#7adddd)!important;background:-moz-linear-gradient(center bottom,#39cccc 0,#7adddd 100%)!important;background:-o-linear-gradient(#7adddd,#39cccc)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd',endColorstr='#39cccc',GradientType=0)!important;color:#fff}.bg-light-blue-gradient{background:#3c8dbc!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#3c8dbc),color-stop(1,#67a8ce))!important;background:-ms-linear-gradient(bottom,#3c8dbc,#67a8ce)!important;background:-moz-linear-gradient(center bottom,#3c8dbc 0,#67a8ce 100%)!important;background:-o-linear-gradient(#67a8ce,#3c8dbc)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce',endColorstr='#3c8dbc',GradientType=0)!important;color:#fff}.bg-blue-gradient{background:#0073b7!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#0073b7),color-stop(1,#0089db))!important;background:-ms-linear-gradient(bottom,#0073b7,#0089db)!important;background:-moz-linear-gradient(center bottom,#0073b7 0,#0089db 100%)!important;background:-o-linear-gradient(#0089db,#0073b7)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db',endColorstr='#0073b7',GradientType=0)!important;color:#fff}.bg-aqua-gradient{background:#00c0ef!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#00c0ef),color-stop(1,#14d1ff))!important;background:-ms-linear-gradient(bottom,#00c0ef,#14d1ff)!important;background:-moz-linear-gradient(center bottom,#00c0ef 0,#14d1ff 100%)!important;background:-o-linear-gradient(#14d1ff,#00c0ef)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff',endColorstr='#00c0ef',GradientType=0)!important;color:#fff}.bg-yellow-gradient{background:#f39c12!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#f39c12),color-stop(1,#f7bc60))!important;background:-ms-linear-gradient(bottom,#f39c12,#f7bc60)!important;background:-moz-linear-gradient(center bottom,#f39c12 0,#f7bc60 100%)!important;background:-o-linear-gradient(#f7bc60,#f39c12)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60',endColorstr='#f39c12',GradientType=0)!important;color:#fff}.bg-purple-gradient{background:#605ca8!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#605ca8),color-stop(1,#9491c4))!important;background:-ms-linear-gradient(bottom,#605ca8,#9491c4)!important;background:-moz-linear-gradient(center bottom,#605ca8 0,#9491c4 100%)!important;background:-o-linear-gradient(#9491c4,#605ca8)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4',endColorstr='#605ca8',GradientType=0)!important;color:#fff}.bg-green-gradient{background:#00a65a!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#00a65a),color-stop(1,#00ca6d))!important;background:-ms-linear-gradient(bottom,#00a65a,#00ca6d)!important;background:-moz-linear-gradient(center bottom,#00a65a 0,#00ca6d 100%)!important;background:-o-linear-gradient(#00ca6d,#00a65a)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d',endColorstr='#00a65a',GradientType=0)!important;color:#fff}.bg-red-gradient{background:#dd4b39!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#dd4b39),color-stop(1,#e47365))!important;background:-ms-linear-gradient(bottom,#dd4b39,#e47365)!important;background:-moz-linear-gradient(center bottom,#dd4b39 0,#e47365 100%)!important;background:-o-linear-gradient(#e47365,#dd4b39)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365',endColorstr='#dd4b39',GradientType=0)!important;color:#fff}.bg-black-gradient{background:#111!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#111),color-stop(1,#2b2b2b))!important;background:-ms-linear-gradient(bottom,#111,#2b2b2b)!important;background:-moz-linear-gradient(center bottom,#111 0,#2b2b2b 100%)!important;background:-o-linear-gradient(#2b2b2b,#111)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b',endColorstr='#111111',GradientType=0)!important;color:#fff}.bg-maroon-gradient{background:#d81b60!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#d81b60),color-stop(1,#e73f7c))!important;background:-ms-linear-gradient(bottom,#d81b60,#e73f7c)!important;background:-moz-linear-gradient(center bottom,#d81b60 0,#e73f7c 100%)!important;background:-o-linear-gradient(#e73f7c,#d81b60)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c',endColorstr='#d81b60',GradientType=0)!important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static!important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px!important;height:30px!important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100%!important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none!important}.content-wrapper,.right-side,.main-footer{margin-left:0!important;min-height:0!important;-webkit-transform:translate(0,0)!important;-ms-transform:translate(0,0)!important;-o-transform:translate(0,0)!important;transform:translate(0,0)!important}.fixed .content-wrapper,.fixed .right-side{padding-top:0!important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal!important}} \ No newline at end of file diff --git a/powerdnsadmin/static/generated/login.js b/powerdnsadmin/static/generated/login.js new file mode 100644 index 0000000..880a059 --- /dev/null +++ b/powerdnsadmin/static/generated/login.js @@ -0,0 +1,1603 @@ +/*! + * jQuery JavaScript Library v3.3.1 + * https://jquery.com/ + * + * Includes Sizzle.js + * https://sizzlejs.com/ + * + * Copyright JS Foundation and other contributors + * Released under the MIT license + * https://jquery.org/license + * + * Date: 2018-01-20T17:24Z + */ +(function(global,factory){"use strict";if(typeof module==="object"&&typeof module.exports==="object"){module.exports=global.document?factory(global,true):function(w){if(!w.document){throw new Error("jQuery requires a window with a document");} +return factory(w);};}else{factory(global);} +})(typeof window!=="undefined"?window:this,function(window,noGlobal){"use strict";var arr=[];var document=window.document;var getProto=Object.getPrototypeOf;var slice=arr.slice;var concat=arr.concat;var push=arr.push;var indexOf=arr.indexOf;var class2type={};var toString=class2type.toString;var hasOwn=class2type.hasOwnProperty;var fnToString=hasOwn.toString;var ObjectFunctionString=fnToString.call(Object);var support={};var isFunction=function isFunction(obj){return typeof obj==="function"&&typeof obj.nodeType!=="number";};var isWindow=function isWindow(obj){return obj!=null&&obj===obj.window;};var preservedScriptAttributes={type:true,src:true,noModule:true};function DOMEval(code,doc,node){doc=doc||document;var i,script=doc.createElement("script");script.text=code;if(node){for(i in preservedScriptAttributes){if(node[i]){script[i]=node[i];}}} +doc.head.appendChild(script).parentNode.removeChild(script);} +function toType(obj){if(obj==null){return obj+"";} +return typeof obj==="object"||typeof obj==="function"?class2type[toString.call(obj)]||"object":typeof obj;} +var +version="3.3.1",jQuery=function(selector,context){return new jQuery.fn.init(selector,context);},rtrim=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;jQuery.fn=jQuery.prototype={jquery:version,constructor:jQuery,length:0,toArray:function(){return slice.call(this);},get:function(num){if(num==null){return slice.call(this);} +return num<0?this[num+this.length]:this[num];},pushStack:function(elems){var ret=jQuery.merge(this.constructor(),elems);ret.prevObject=this;return ret;},each:function(callback){return jQuery.each(this,callback);},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},slice:function(){return this.pushStack(slice.apply(this,arguments));},first:function(){return this.eq(0);},last:function(){return this.eq(-1);},eq:function(i){var len=this.length,j=+i+(i<0?len:0);return this.pushStack(j>=0&&j0&&(length-1)in obj;} +var Sizzle=/*! + * Sizzle CSS Selector Engine v2.3.3 + * https://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-08-08 + */ +(function(window){var i,support,Expr,getText,isXML,tokenize,compile,select,outermostContext,sortInput,hasDuplicate,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+1*new Date(),preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),sortOrder=function(a,b){if(a===b){hasDuplicate=true;} +return 0;},hasOwn=({}).hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=function(list,elem){var i=0,len=list.length;for(;i+~]|"+whitespace+")"+whitespace+"*"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*?)"+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={"ID":new RegExp("^#("+identifier+")"),"CLASS":new RegExp("^\\.("+identifier+")"),"TAG":new RegExp("^("+identifier+"|[*])"),"ATTR":new RegExp("^"+attributes),"PSEUDO":new RegExp("^"+pseudos),"CHILD":new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+ +"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+ +"*(\\d+)|))"+whitespace+"*\\)|)","i"),"bool":new RegExp("^(?:"+booleans+")$","i"),"needsContext":new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ +whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rsibling=/[+~]/,runescape=new RegExp("\\\\([\\da-f]{1,6}"+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-0x10000;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+0x10000):String.fromCharCode(high>>10|0xD800,high&0x3FF|0xDC00);},rcssescape=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,fcssescape=function(ch,asCodePoint){if(asCodePoint){if(ch==="\0"){return"\uFFFD";} +return ch.slice(0,-1)+"\\"+ch.charCodeAt(ch.length-1).toString(16)+" ";} +return"\\"+ch;},unloadHandler=function(){setDocument();},disabledAncestor=addCombinator(function(elem){return elem.disabled===true&&("form"in elem||"label"in elem);},{dir:"parentNode",next:"legend"});try{push.apply((arr=slice.call(preferredDoc.childNodes)),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType;}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els));}:function(target,els){var j=target.length,i=0;while((target[j++]=els[i++])){} +target.length=j-1;}};} +function Sizzle(selector,context,results,seed){var m,i,elem,nid,match,groups,newSelector,newContext=context&&context.ownerDocument,nodeType=context?context.nodeType:9;results=results||[];if(typeof selector!=="string"||!selector||nodeType!==1&&nodeType!==9&&nodeType!==11){return results;} +if(!seed){if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context);} +context=context||document;if(documentIsHTML){if(nodeType!==11&&(match=rquickExpr.exec(selector))){if((m=match[1])){if(nodeType===9){if((elem=context.getElementById(m))){if(elem.id===m){results.push(elem);return results;}}else{return results;} +}else{if(newContext&&(elem=newContext.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results;}} +}else if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results;}else if((m=match[3])&&support.getElementsByClassName&&context.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results;}} +if(support.qsa&&!compilerCache[selector+" "]&&(!rbuggyQSA||!rbuggyQSA.test(selector))){if(nodeType!==1){newContext=context;newSelector=selector;}else if(context.nodeName.toLowerCase()!=="object"){if((nid=context.getAttribute("id"))){nid=nid.replace(rcssescape,fcssescape);}else{context.setAttribute("id",(nid=expando));} +groups=tokenize(selector);i=groups.length;while(i--){groups[i]="#"+nid+" "+toSelector(groups[i]);} +newSelector=groups.join(",");newContext=rsibling.test(selector)&&testContext(context.parentNode)||context;} +if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results;}catch(qsaError){}finally{if(nid===expando){context.removeAttribute("id");}}}}}} +return select(selector.replace(rtrim,"$1"),context,results,seed);} +function createCache(){var keys=[];function cache(key,value){if(keys.push(key+" ")>Expr.cacheLength){delete cache[keys.shift()];} +return(cache[key+" "]=value);} +return cache;} +function markFunction(fn){fn[expando]=true;return fn;} +function assert(fn){var el=document.createElement("fieldset");try{return!!fn(el);}catch(e){return false;}finally{if(el.parentNode){el.parentNode.removeChild(el);} +el=null;}} +function addHandle(attrs,handler){var arr=attrs.split("|"),i=arr.length;while(i--){Expr.attrHandle[arr[i]]=handler;}} +function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&a.sourceIndex-b.sourceIndex;if(diff){return diff;} +if(cur){while((cur=cur.nextSibling)){if(cur===b){return-1;}}} +return a?1:-1;} +function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type;};} +function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type;};} +function createDisabledPseudo(disabled){return function(elem){if("form"in elem){if(elem.parentNode&&elem.disabled===false){if("label"in elem){if("label"in elem.parentNode){return elem.parentNode.disabled===disabled;}else{return elem.disabled===disabled;}} +return elem.isDisabled===disabled||elem.isDisabled!==!disabled&&disabledAncestor(elem)===disabled;} +return elem.disabled===disabled;}else if("label"in elem){return elem.disabled===disabled;} +return false;};} +function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[(j=matchIndexes[i])]){seed[j]=!(matches[j]=seed[j]);}}});});} +function testContext(context){return context&&typeof context.getElementsByTagName!=="undefined"&&context;} +support=Sizzle.support={};isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false;};setDocument=Sizzle.setDocument=function(node){var hasCompare,subWindow,doc=node?node.ownerDocument||node:preferredDoc;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document;} +document=doc;docElem=document.documentElement;documentIsHTML=!isXML(document);if(preferredDoc!==document&&(subWindow=document.defaultView)&&subWindow.top!==subWindow){if(subWindow.addEventListener){subWindow.addEventListener("unload",unloadHandler,false);}else if(subWindow.attachEvent){subWindow.attachEvent("onunload",unloadHandler);}} +support.attributes=assert(function(el){el.className="i";return!el.getAttribute("className");});support.getElementsByTagName=assert(function(el){el.appendChild(document.createComment(""));return!el.getElementsByTagName("*").length;});support.getElementsByClassName=rnative.test(document.getElementsByClassName);support.getById=assert(function(el){docElem.appendChild(el).id=expando;return!document.getElementsByName||!document.getElementsByName(expando).length;});if(support.getById){Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId;};};Expr.find["ID"]=function(id,context){if(typeof context.getElementById!=="undefined"&&documentIsHTML){var elem=context.getElementById(id);return elem?[elem]:[];}};}else{Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!=="undefined"&&elem.getAttributeNode("id");return node&&node.value===attrId;};};Expr.find["ID"]=function(id,context){if(typeof context.getElementById!=="undefined"&&documentIsHTML){var node,i,elems,elem=context.getElementById(id);if(elem){node=elem.getAttributeNode("id");if(node&&node.value===id){return[elem];} +elems=context.getElementsByName(id);i=0;while((elem=elems[i++])){node=elem.getAttributeNode("id");if(node&&node.value===id){return[elem];}}} +return[];}};} +Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!=="undefined"){return context.getElementsByTagName(tag);}else if(support.qsa){return context.querySelectorAll(tag);}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while((elem=results[i++])){if(elem.nodeType===1){tmp.push(elem);}} +return tmp;} +return results;};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(typeof context.getElementsByClassName!=="undefined"&&documentIsHTML){return context.getElementsByClassName(className);}};rbuggyMatches=[];rbuggyQSA=[];if((support.qsa=rnative.test(document.querySelectorAll))){assert(function(el){docElem.appendChild(el).innerHTML=""+ +"";if(el.querySelectorAll("[msallowcapture^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")");} +if(!el.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")");} +if(!el.querySelectorAll("[id~="+expando+"-]").length){rbuggyQSA.push("~=");} +if(!el.querySelectorAll(":checked").length){rbuggyQSA.push(":checked");} +if(!el.querySelectorAll("a#"+expando+"+*").length){rbuggyQSA.push(".#.+[+~]");}});assert(function(el){el.innerHTML=""+ +"";var input=document.createElement("input");input.setAttribute("type","hidden");el.appendChild(input).setAttribute("name","D");if(el.querySelectorAll("[name=d]").length){rbuggyQSA.push("name"+whitespace+"*[*^$|!~]?=");} +if(el.querySelectorAll(":enabled").length!==2){rbuggyQSA.push(":enabled",":disabled");} +docElem.appendChild(el).disabled=true;if(el.querySelectorAll(":disabled").length!==2){rbuggyQSA.push(":enabled",":disabled");} +el.querySelectorAll("*,:x");rbuggyQSA.push(",.*:");});} +if((support.matchesSelector=rnative.test((matches=docElem.matches||docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)))){assert(function(el){support.disconnectedMatch=matches.call(el,"*");matches.call(el,"[s!='']:x");rbuggyMatches.push("!=",pseudos);});} +rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));hasCompare=rnative.test(docElem.compareDocumentPosition);contains=hasCompare||rnative.test(docElem.contains)?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16));}:function(a,b){if(b){while((b=b.parentNode)){if(b===a){return true;}}} +return false;};sortOrder=hasCompare?function(a,b){if(a===b){hasDuplicate=true;return 0;} +var compare=!a.compareDocumentPosition-!b.compareDocumentPosition;if(compare){return compare;} +compare=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1;if(compare&1||(!support.sortDetached&&b.compareDocumentPosition(a)===compare)){if(a===document||a.ownerDocument===preferredDoc&&contains(preferredDoc,a)){return-1;} +if(b===document||b.ownerDocument===preferredDoc&&contains(preferredDoc,b)){return 1;} +return sortInput?(indexOf(sortInput,a)-indexOf(sortInput,b)):0;} +return compare&4?-1:1;}:function(a,b){if(a===b){hasDuplicate=true;return 0;} +var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(!aup||!bup){return a===document?-1:b===document?1:aup?-1:bup?1:sortInput?(indexOf(sortInput,a)-indexOf(sortInput,b)):0;}else if(aup===bup){return siblingCheck(a,b);} +cur=a;while((cur=cur.parentNode)){ap.unshift(cur);} +cur=b;while((cur=cur.parentNode)){bp.unshift(cur);} +while(ap[i]===bp[i]){i++;} +return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0;};return document;};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements);};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem);} +expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&!compilerCache[expr+" "]&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret;}}catch(e){}} +return Sizzle(expr,document,null,[elem]).length>0;};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context);} +return contains(context,elem);};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem);} +var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val!==undefined?val:support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null;};Sizzle.escape=function(sel){return(sel+"").replace(rcssescape,fcssescape);};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg);};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while((elem=results[i++])){if(elem===results[i]){j=duplicates.push(i);}} +while(j--){results.splice(duplicates[j],1);}} +sortInput=null;return results;};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){while((node=elem[i++])){ret+=getText(node);}}else if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent;}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem);}}}else if(nodeType===3||nodeType===4){return elem.nodeValue;} +return ret;};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{"ATTR":function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[3]||match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" ";} +return match.slice(0,4);},"CHILD":function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0]);} +match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+((match[7]+match[8])||match[3]==="odd");}else if(match[3]){Sizzle.error(match[0]);} +return match;},"PSEUDO":function(match){var excess,unquoted=!match[6]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null;} +if(match[3]){match[2]=match[4]||match[5]||"";}else if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess);} +return match.slice(0,3);}},filter:{"TAG":function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true;}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName;};},"CLASS":function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!=="undefined"&&elem.getAttribute("class")||"");});},"ATTR":function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!=";} +if(!operator){return true;} +result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result.replace(rwhitespace," ")+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false;};},"CHILD":function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode;}:function(elem,context,xml){var cache,uniqueCache,outerCache,node,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType,diff=false;if(parent){if(simple){while(dir){node=elem;while((node=node[dir])){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false;}} +start=dir=type==="only"&&!start&&"nextSibling";} +return true;} +start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){node=parent;outerCache=node[expando]||(node[expando]={});uniqueCache=outerCache[node.uniqueID]||(outerCache[node.uniqueID]={});cache=uniqueCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=nodeIndex&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while((node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop())){if(node.nodeType===1&&++diff&&node===elem){uniqueCache[type]=[dirruns,nodeIndex,diff];break;}}}else{if(useCache){node=elem;outerCache=node[expando]||(node[expando]={});uniqueCache=outerCache[node.uniqueID]||(outerCache[node.uniqueID]={});cache=uniqueCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=nodeIndex;} +if(diff===false){while((node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop())){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){outerCache=node[expando]||(node[expando]={});uniqueCache=outerCache[node.uniqueID]||(outerCache[node.uniqueID]={});uniqueCache[type]=[dirruns,diff];} +if(node===elem){break;}}}}} +diff-=last;return diff===first||(diff%first===0&&diff/first>=0);}};},"PSEUDO":function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument);} +if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i]);}}):function(elem){return fn(elem,0,args);};} +return fn;}},pseudos:{"not":markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if((elem=unmatched[i])){seed[i]=!(matches[i]=elem);}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);input[0]=null;return!results.pop();};}),"has":markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0;};}),"contains":markFunction(function(text){text=text.replace(runescape,funescape);return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1;};}),"lang":markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang);} +lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if((elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang"))){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0;}}while((elem=elem.parentNode)&&elem.nodeType===1);return false;};}),"target":function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id;},"root":function(elem){return elem===docElem;},"focus":function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex);},"enabled":createDisabledPseudo(false),"disabled":createDisabledPseudo(true),"checked":function(elem){var nodeName=elem.nodeName.toLowerCase();return(nodeName==="input"&&!!elem.checked)||(nodeName==="option"&&!!elem.selected);},"selected":function(elem){if(elem.parentNode){elem.parentNode.selectedIndex;} +return elem.selected===true;},"empty":function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeType<6){return false;}} +return true;},"parent":function(elem){return!Expr.pseudos["empty"](elem);},"header":function(elem){return rheader.test(elem.nodeName);},"input":function(elem){return rinputs.test(elem.nodeName);},"button":function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button";},"text":function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()==="text");},"first":createPositionalPseudo(function(){return[0];}),"last":createPositionalPseudo(function(matchIndexes,length){return[length-1];}),"eq":createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument];}),"even":createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i=0;){matchIndexes.push(i);} +return matchIndexes;}),"gt":createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false;}} +return true;}:matchers[0];} +function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i-1){seed[temp]=!(results[temp]=elem);}}} +}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml);}else{push.apply(results,matcherOut);}}});} +function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext;},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf(checkContext,elem)>-1;},implicitRelative,true),matchers=[function(elem,context,xml){var ret=(!leadingRelative&&(xml||context!==outermostContext))||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml));checkContext=null;return ret;}];for(;i1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,outermost){var elem,j,matcher,matchedCount=0,i="0",unmatched=seed&&[],setMatched=[],contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",outermost),dirrunsUnique=(dirruns+=contextBackup==null?1:Math.random()||0.1),len=elems.length;if(outermost){outermostContext=context===document||context||outermost;} +for(;i!==len&&(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;if(!context&&elem.ownerDocument!==document){setDocument(elem);xml=!documentIsHTML;} +while((matcher=elementMatchers[j++])){if(matcher(elem,context||document,xml)){results.push(elem);break;}} +if(outermost){dirruns=dirrunsUnique;}} +if(bySet){if((elem=!matcher&&elem)){matchedCount--;} +if(seed){unmatched.push(elem);}}} +matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while((matcher=setMatchers[j++])){matcher(unmatched,setMatched,context,xml);} +if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results);}}} +setMatched=condense(setMatched);} +push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&(matchedCount+setMatchers.length)>1){Sizzle.uniqueSort(results);}} +if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup;} +return unmatched;};return bySet?markFunction(superMatcher):superMatcher;} +compile=Sizzle.compile=function(selector,match ){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!match){match=tokenize(selector);} +i=match.length;while(i--){cached=matcherFromTokens(match[i]);if(cached[expando]){setMatchers.push(cached);}else{elementMatchers.push(cached);}} +cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers));cached.selector=selector;} +return cached;};select=Sizzle.select=function(selector,context,results,seed){var i,tokens,token,type,find,compiled=typeof selector==="function"&&selector,match=!seed&&tokenize((selector=compiled.selector||selector));results=results||[];if(match.length===1){tokens=match[0]=match[0].slice(0);if(tokens.length>2&&(token=tokens[0]).type==="ID"&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results;}else if(compiled){context=context.parentNode;} +selector=selector.slice(tokens.shift().value.length);} +i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[(type=token.type)]){break;} +if((find=Expr.find[type])){if((seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&testContext(context.parentNode)||context))){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results;} +break;}}}} +(compiled||compile(selector,match))(seed,context,!documentIsHTML,results,!context||rsibling.test(selector)&&testContext(context.parentNode)||context);return results;};support.sortStable=expando.split("").sort(sortOrder).join("")===expando;support.detectDuplicates=!!hasDuplicate;setDocument();support.sortDetached=assert(function(el){return el.compareDocumentPosition(document.createElement("fieldset"))&1;});if(!assert(function(el){el.innerHTML="";return el.firstChild.getAttribute("href")==="#";})){addHandle("type|href|height|width",function(elem,name,isXML){if(!isXML){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2);}});} +if(!support.attributes||!assert(function(el){el.innerHTML="";el.firstChild.setAttribute("value","");return el.firstChild.getAttribute("value")==="";})){addHandle("value",function(elem,name,isXML){if(!isXML&&elem.nodeName.toLowerCase()==="input"){return elem.defaultValue;}});} +if(!assert(function(el){return el.getAttribute("disabled")==null;})){addHandle(booleans,function(elem,name,isXML){var val;if(!isXML){return elem[name]===true?name.toLowerCase():(val=elem.getAttributeNode(name))&&val.specified?val.value:null;}});} +return Sizzle;})(window);jQuery.find=Sizzle;jQuery.expr=Sizzle.selectors;jQuery.expr[":"]=jQuery.expr.pseudos;jQuery.uniqueSort=jQuery.unique=Sizzle.uniqueSort;jQuery.text=Sizzle.getText;jQuery.isXMLDoc=Sizzle.isXML;jQuery.contains=Sizzle.contains;jQuery.escapeSelector=Sizzle.escape;var dir=function(elem,dir,until){var matched=[],truncate=until!==undefined;while((elem=elem[dir])&&elem.nodeType!==9){if(elem.nodeType===1){if(truncate&&jQuery(elem).is(until)){break;} +matched.push(elem);}} +return matched;};var siblings=function(n,elem){var matched=[];for(;n;n=n.nextSibling){if(n.nodeType===1&&n!==elem){matched.push(n);}} +return matched;};var rneedsContext=jQuery.expr.match.needsContext;function nodeName(elem,name){return elem.nodeName&&elem.nodeName.toLowerCase()===name.toLowerCase();};var rsingleTag=(/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i);function winnow(elements,qualifier,not){if(isFunction(qualifier)){return jQuery.grep(elements,function(elem,i){return!!qualifier.call(elem,i,elem)!==not;});} +if(qualifier.nodeType){return jQuery.grep(elements,function(elem){return(elem===qualifier)!==not;});} +if(typeof qualifier!=="string"){return jQuery.grep(elements,function(elem){return(indexOf.call(qualifier,elem)>-1)!==not;});} +return jQuery.filter(qualifier,elements,not);} +jQuery.filter=function(expr,elems,not){var elem=elems[0];if(not){expr=":not("+expr+")";} +if(elems.length===1&&elem.nodeType===1){return jQuery.find.matchesSelector(elem,expr)?[elem]:[];} +return jQuery.find.matches(expr,jQuery.grep(elems,function(elem){return elem.nodeType===1;}));};jQuery.fn.extend({find:function(selector){var i,ret,len=this.length,self=this;if(typeof selector!=="string"){return this.pushStack(jQuery(selector).filter(function(){for(i=0;i1?jQuery.uniqueSort(ret):ret;},filter:function(selector){return this.pushStack(winnow(this,selector||[],false));},not:function(selector){return this.pushStack(winnow(this,selector||[],true));},is:function(selector){return!!winnow(this,typeof selector==="string"&&rneedsContext.test(selector)?jQuery(selector):selector||[],false).length;}});var rootjQuery,rquickExpr=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,init=jQuery.fn.init=function(selector,context,root){var match,elem;if(!selector){return this;} +root=root||rootjQuery;if(typeof selector==="string"){if(selector[0]==="<"&&selector[selector.length-1]===">"&&selector.length>=3){match=[null,selector,null];}else{match=rquickExpr.exec(selector);} +if(match&&(match[1]||!context)){if(match[1]){context=context instanceof jQuery?context[0]:context;jQuery.merge(this,jQuery.parseHTML(match[1],context&&context.nodeType?context.ownerDocument||context:document,true));if(rsingleTag.test(match[1])&&jQuery.isPlainObject(context)){for(match in context){if(isFunction(this[match])){this[match](context[match]);}else{this.attr(match,context[match]);}}} +return this;}else{elem=document.getElementById(match[2]);if(elem){this[0]=elem;this.length=1;} +return this;} +}else if(!context||context.jquery){return(context||root).find(selector);}else{return this.constructor(context).find(selector);} +}else if(selector.nodeType){this[0]=selector;this.length=1;return this;}else if(isFunction(selector)){return root.ready!==undefined?root.ready(selector):selector(jQuery);} +return jQuery.makeArray(selector,this);};init.prototype=jQuery.fn;rootjQuery=jQuery(document);var rparentsprev=/^(?:parents|prev(?:Until|All))/,guaranteedUnique={children:true,contents:true,next:true,prev:true};jQuery.fn.extend({has:function(target){var targets=jQuery(target,this),l=targets.length;return this.filter(function(){var i=0;for(;i-1:cur.nodeType===1&&jQuery.find.matchesSelector(cur,selectors))){matched.push(cur);break;}}}} +return this.pushStack(matched.length>1?jQuery.uniqueSort(matched):matched);},index:function(elem){if(!elem){return(this[0]&&this[0].parentNode)?this.first().prevAll().length:-1;} +if(typeof elem==="string"){return indexOf.call(jQuery(elem),this[0]);} +return indexOf.call(this,elem.jquery?elem[0]:elem);},add:function(selector,context){return this.pushStack(jQuery.uniqueSort(jQuery.merge(this.get(),jQuery(selector,context))));},addBack:function(selector){return this.add(selector==null?this.prevObject:this.prevObject.filter(selector));}});function sibling(cur,dir){while((cur=cur[dir])&&cur.nodeType!==1){} +return cur;} +jQuery.each({parent:function(elem){var parent=elem.parentNode;return parent&&parent.nodeType!==11?parent:null;},parents:function(elem){return dir(elem,"parentNode");},parentsUntil:function(elem,i,until){return dir(elem,"parentNode",until);},next:function(elem){return sibling(elem,"nextSibling");},prev:function(elem){return sibling(elem,"previousSibling");},nextAll:function(elem){return dir(elem,"nextSibling");},prevAll:function(elem){return dir(elem,"previousSibling");},nextUntil:function(elem,i,until){return dir(elem,"nextSibling",until);},prevUntil:function(elem,i,until){return dir(elem,"previousSibling",until);},siblings:function(elem){return siblings((elem.parentNode||{}).firstChild,elem);},children:function(elem){return siblings(elem.firstChild);},contents:function(elem){if(nodeName(elem,"iframe")){return elem.contentDocument;} +if(nodeName(elem,"template")){elem=elem.content||elem;} +return jQuery.merge([],elem.childNodes);}},function(name,fn){jQuery.fn[name]=function(until,selector){var matched=jQuery.map(this,fn,until);if(name.slice(-5)!=="Until"){selector=until;} +if(selector&&typeof selector==="string"){matched=jQuery.filter(selector,matched);} +if(this.length>1){if(!guaranteedUnique[name]){jQuery.uniqueSort(matched);} +if(rparentsprev.test(name)){matched.reverse();}} +return this.pushStack(matched);};});var rnothtmlwhite=(/[^\x20\t\r\n\f]+/g);function createOptions(options){var object={};jQuery.each(options.match(rnothtmlwhite)||[],function(_,flag){object[flag]=true;});return object;} +jQuery.Callbacks=function(options){options=typeof options==="string"?createOptions(options):jQuery.extend({},options);var +firing,memory,fired,locked,list=[],queue=[],firingIndex=-1,fire=function(){locked=locked||options.once;fired=firing=true;for(;queue.length;firingIndex=-1){memory=queue.shift();while(++firingIndex-1){list.splice(index,1);if(index<=firingIndex){firingIndex--;}}});return this;},has:function(fn){return fn?jQuery.inArray(fn,list)>-1:list.length>0;},empty:function(){if(list){list=[];} +return this;},disable:function(){locked=queue=[];list=memory="";return this;},disabled:function(){return!list;},lock:function(){locked=queue=[];if(!memory&&!firing){list=memory="";} +return this;},locked:function(){return!!locked;},fireWith:function(context,args){if(!locked){args=args||[];args=[context,args.slice?args.slice():args];queue.push(args);if(!firing){fire();}} +return this;},fire:function(){self.fireWith(this,arguments);return this;},fired:function(){return!!fired;}};return self;};function Identity(v){return v;} +function Thrower(ex){throw ex;} +function adoptValue(value,resolve,reject,noValue){var method;try{if(value&&isFunction((method=value.promise))){method.call(value).done(resolve).fail(reject);}else if(value&&isFunction((method=value.then))){method.call(value,resolve,reject);}else{resolve.apply(undefined,[value].slice(noValue));} +}catch(value){reject.apply(undefined,[value]);}} +jQuery.extend({Deferred:function(func){var tuples=[["notify","progress",jQuery.Callbacks("memory"),jQuery.Callbacks("memory"),2],["resolve","done",jQuery.Callbacks("once memory"),jQuery.Callbacks("once memory"),0,"resolved"],["reject","fail",jQuery.Callbacks("once memory"),jQuery.Callbacks("once memory"),1,"rejected"]],state="pending",promise={state:function(){return state;},always:function(){deferred.done(arguments).fail(arguments);return this;},"catch":function(fn){return promise.then(null,fn);},pipe:function(){var fns=arguments;return jQuery.Deferred(function(newDefer){jQuery.each(tuples,function(i,tuple){var fn=isFunction(fns[tuple[4]])&&fns[tuple[4]];deferred[tuple[1]](function(){var returned=fn&&fn.apply(this,arguments);if(returned&&isFunction(returned.promise)){returned.promise().progress(newDefer.notify).done(newDefer.resolve).fail(newDefer.reject);}else{newDefer[tuple[0]+"With"](this,fn?[returned]:arguments);}});});fns=null;}).promise();},then:function(onFulfilled,onRejected,onProgress){var maxDepth=0;function resolve(depth,deferred,handler,special){return function(){var that=this,args=arguments,mightThrow=function(){var returned,then;if(depth=maxDepth){if(handler!==Thrower){that=undefined;args=[e];} +deferred.rejectWith(that,args);}}};if(depth){process();}else{if(jQuery.Deferred.getStackHook){process.stackTrace=jQuery.Deferred.getStackHook();} +window.setTimeout(process);}};} +return jQuery.Deferred(function(newDefer){tuples[0][3].add(resolve(0,newDefer,isFunction(onProgress)?onProgress:Identity,newDefer.notifyWith));tuples[1][3].add(resolve(0,newDefer,isFunction(onFulfilled)?onFulfilled:Identity));tuples[2][3].add(resolve(0,newDefer,isFunction(onRejected)?onRejected:Thrower));}).promise();},promise:function(obj){return obj!=null?jQuery.extend(obj,promise):promise;}},deferred={};jQuery.each(tuples,function(i,tuple){var list=tuple[2],stateString=tuple[5];promise[tuple[1]]=list.add;if(stateString){list.add(function(){state=stateString;},tuples[3-i][2].disable,tuples[3-i][3].disable,tuples[0][2].lock,tuples[0][3].lock);} +list.add(tuple[3].fire);deferred[tuple[0]]=function(){deferred[tuple[0]+"With"](this===deferred?undefined:this,arguments);return this;};deferred[tuple[0]+"With"]=list.fireWith;});promise.promise(deferred);if(func){func.call(deferred,deferred);} +return deferred;},when:function(singleValue){var +remaining=arguments.length,i=remaining,resolveContexts=Array(i),resolveValues=slice.call(arguments),master=jQuery.Deferred(),updateFunc=function(i){return function(value){resolveContexts[i]=this;resolveValues[i]=arguments.length>1?slice.call(arguments):value;if(!(--remaining)){master.resolveWith(resolveContexts,resolveValues);}};};if(remaining<=1){adoptValue(singleValue,master.done(updateFunc(i)).resolve,master.reject,!remaining);if(master.state()==="pending"||isFunction(resolveValues[i]&&resolveValues[i].then)){return master.then();}} +while(i--){adoptValue(resolveValues[i],updateFunc(i),master.reject);} +return master.promise();}});var rerrorNames=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;jQuery.Deferred.exceptionHook=function(error,stack){if(window.console&&window.console.warn&&error&&rerrorNames.test(error.name)){window.console.warn("jQuery.Deferred exception: "+error.message,error.stack,stack);}};jQuery.readyException=function(error){window.setTimeout(function(){throw error;});};var readyList=jQuery.Deferred();jQuery.fn.ready=function(fn){readyList.then(fn) +.catch(function(error){jQuery.readyException(error);});return this;};jQuery.extend({isReady:false,readyWait:1,ready:function(wait){if(wait===true?--jQuery.readyWait:jQuery.isReady){return;} +jQuery.isReady=true;if(wait!==true&&--jQuery.readyWait>0){return;} +readyList.resolveWith(document,[jQuery]);}});jQuery.ready.then=readyList.then;function completed(){document.removeEventListener("DOMContentLoaded",completed);window.removeEventListener("load",completed);jQuery.ready();} +if(document.readyState==="complete"||(document.readyState!=="loading"&&!document.documentElement.doScroll)){window.setTimeout(jQuery.ready);}else{document.addEventListener("DOMContentLoaded",completed);window.addEventListener("load",completed);} +var access=function(elems,fn,key,value,chainable,emptyGet,raw){var i=0,len=elems.length,bulk=key==null;if(toType(key)==="object"){chainable=true;for(i in key){access(elems,fn,i,key[i],true,emptyGet,raw);} +}else if(value!==undefined){chainable=true;if(!isFunction(value)){raw=true;} +if(bulk){if(raw){fn.call(elems,value);fn=null;}else{bulk=fn;fn=function(elem,key,value){return bulk.call(jQuery(elem),value);};}} +if(fn){for(;i1,null,true);},removeData:function(key){return this.each(function(){dataUser.remove(this,key);});}});jQuery.extend({queue:function(elem,type,data){var queue;if(elem){type=(type||"fx")+"queue";queue=dataPriv.get(elem,type);if(data){if(!queue||Array.isArray(data)){queue=dataPriv.access(elem,type,jQuery.makeArray(data));}else{queue.push(data);}} +return queue||[];}},dequeue:function(elem,type){type=type||"fx";var queue=jQuery.queue(elem,type),startLength=queue.length,fn=queue.shift(),hooks=jQuery._queueHooks(elem,type),next=function(){jQuery.dequeue(elem,type);};if(fn==="inprogress"){fn=queue.shift();startLength--;} +if(fn){if(type==="fx"){queue.unshift("inprogress");} +delete hooks.stop;fn.call(elem,next,hooks);} +if(!startLength&&hooks){hooks.empty.fire();}},_queueHooks:function(elem,type){var key=type+"queueHooks";return dataPriv.get(elem,key)||dataPriv.access(elem,key,{empty:jQuery.Callbacks("once memory").add(function(){dataPriv.remove(elem,[type+"queue",key]);})});}});jQuery.fn.extend({queue:function(type,data){var setter=2;if(typeof type!=="string"){data=type;type="fx";setter--;} +if(arguments.length\x20\t\r\n\f]+)/i);var rscriptType=(/^$|^module$|\/(?:java|ecma)script/i);var wrapMap={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};wrapMap.optgroup=wrapMap.option;wrapMap.tbody=wrapMap.tfoot=wrapMap.colgroup=wrapMap.caption=wrapMap.thead;wrapMap.th=wrapMap.td;function getAll(context,tag){var ret;if(typeof context.getElementsByTagName!=="undefined"){ret=context.getElementsByTagName(tag||"*");}else if(typeof context.querySelectorAll!=="undefined"){ret=context.querySelectorAll(tag||"*");}else{ret=[];} +if(tag===undefined||tag&&nodeName(context,tag)){return jQuery.merge([context],ret);} +return ret;} +function setGlobalEval(elems,refElements){var i=0,l=elems.length;for(;i-1){if(ignored){ignored.push(elem);} +continue;} +contains=jQuery.contains(elem.ownerDocument,elem);tmp=getAll(fragment.appendChild(elem),"script");if(contains){setGlobalEval(tmp);} +if(scripts){j=0;while((elem=tmp[j++])){if(rscriptType.test(elem.type||"")){scripts.push(elem);}}}} +return fragment;} +(function(){var fragment=document.createDocumentFragment(),div=fragment.appendChild(document.createElement("div")),input=document.createElement("input");input.setAttribute("type","radio");input.setAttribute("checked","checked");input.setAttribute("name","t");div.appendChild(input);support.checkClone=div.cloneNode(true).cloneNode(true).lastChild.checked;div.innerHTML="";support.noCloneChecked=!!div.cloneNode(true).lastChild.defaultValue;})();var documentElement=document.documentElement;var +rkeyEvent=/^key/,rmouseEvent=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,rtypenamespace=/^([^.]*)(?:\.(.+)|)/;function returnTrue(){return true;} +function returnFalse(){return false;} +function safeActiveElement(){try{return document.activeElement;}catch(err){}} +function on(elem,types,selector,data,fn,one){var origFn,type;if(typeof types==="object"){if(typeof selector!=="string"){data=data||selector;selector=undefined;} +for(type in types){on(elem,type,selector,data,types[type],one);} +return elem;} +if(data==null&&fn==null){fn=selector;data=selector=undefined;}else if(fn==null){if(typeof selector==="string"){fn=data;data=undefined;}else{fn=data;data=selector;selector=undefined;}} +if(fn===false){fn=returnFalse;}else if(!fn){return elem;} +if(one===1){origFn=fn;fn=function(event){jQuery().off(event);return origFn.apply(this,arguments);};fn.guid=origFn.guid||(origFn.guid=jQuery.guid++);} +return elem.each(function(){jQuery.event.add(this,types,fn,data,selector);});} +jQuery.event={global:{},add:function(elem,types,handler,data,selector){var handleObjIn,eventHandle,tmp,events,t,handleObj,special,handlers,type,namespaces,origType,elemData=dataPriv.get(elem);if(!elemData){return;} +if(handler.handler){handleObjIn=handler;handler=handleObjIn.handler;selector=handleObjIn.selector;} +if(selector){jQuery.find.matchesSelector(documentElement,selector);} +if(!handler.guid){handler.guid=jQuery.guid++;} +if(!(events=elemData.events)){events=elemData.events={};} +if(!(eventHandle=elemData.handle)){eventHandle=elemData.handle=function(e){return typeof jQuery!=="undefined"&&jQuery.event.triggered!==e.type?jQuery.event.dispatch.apply(elem,arguments):undefined;};} +types=(types||"").match(rnothtmlwhite)||[""];t=types.length;while(t--){tmp=rtypenamespace.exec(types[t])||[];type=origType=tmp[1];namespaces=(tmp[2]||"").split(".").sort();if(!type){continue;} +special=jQuery.event.special[type]||{};type=(selector?special.delegateType:special.bindType)||type;special=jQuery.event.special[type]||{};handleObj=jQuery.extend({type:type,origType:origType,data:data,handler:handler,guid:handler.guid,selector:selector,needsContext:selector&&jQuery.expr.match.needsContext.test(selector),namespace:namespaces.join(".")},handleObjIn);if(!(handlers=events[type])){handlers=events[type]=[];handlers.delegateCount=0;if(!special.setup||special.setup.call(elem,data,namespaces,eventHandle)===false){if(elem.addEventListener){elem.addEventListener(type,eventHandle);}}} +if(special.add){special.add.call(elem,handleObj);if(!handleObj.handler.guid){handleObj.handler.guid=handler.guid;}} +if(selector){handlers.splice(handlers.delegateCount++,0,handleObj);}else{handlers.push(handleObj);} +jQuery.event.global[type]=true;}},remove:function(elem,types,handler,selector,mappedTypes){var j,origCount,tmp,events,t,handleObj,special,handlers,type,namespaces,origType,elemData=dataPriv.hasData(elem)&&dataPriv.get(elem);if(!elemData||!(events=elemData.events)){return;} +types=(types||"").match(rnothtmlwhite)||[""];t=types.length;while(t--){tmp=rtypenamespace.exec(types[t])||[];type=origType=tmp[1];namespaces=(tmp[2]||"").split(".").sort();if(!type){for(type in events){jQuery.event.remove(elem,type+types[t],handler,selector,true);} +continue;} +special=jQuery.event.special[type]||{};type=(selector?special.delegateType:special.bindType)||type;handlers=events[type]||[];tmp=tmp[2]&&new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)");origCount=j=handlers.length;while(j--){handleObj=handlers[j];if((mappedTypes||origType===handleObj.origType)&&(!handler||handler.guid===handleObj.guid)&&(!tmp||tmp.test(handleObj.namespace))&&(!selector||selector===handleObj.selector||selector==="**"&&handleObj.selector)){handlers.splice(j,1);if(handleObj.selector){handlers.delegateCount--;} +if(special.remove){special.remove.call(elem,handleObj);}}} +if(origCount&&!handlers.length){if(!special.teardown||special.teardown.call(elem,namespaces,elemData.handle)===false){jQuery.removeEvent(elem,type,elemData.handle);} +delete events[type];}} +if(jQuery.isEmptyObject(events)){dataPriv.remove(elem,"handle events");}},dispatch:function(nativeEvent){var event=jQuery.event.fix(nativeEvent);var i,j,ret,matched,handleObj,handlerQueue,args=new Array(arguments.length),handlers=(dataPriv.get(this,"events")||{})[event.type]||[],special=jQuery.event.special[event.type]||{};args[0]=event;for(i=1;i=1)){for(;cur!==this;cur=cur.parentNode||this){if(cur.nodeType===1&&!(event.type==="click"&&cur.disabled===true)){matchedHandlers=[];matchedSelectors={};for(i=0;i-1:jQuery.find(sel,this,null,[cur]).length;} +if(matchedSelectors[sel]){matchedHandlers.push(handleObj);}} +if(matchedHandlers.length){handlerQueue.push({elem:cur,handlers:matchedHandlers});}}}} +cur=this;if(delegateCount\x20\t\r\n\f]*)[^>]*)\/>/gi,rnoInnerhtml=/\s*$/g;function manipulationTarget(elem,content){if(nodeName(elem,"table")&&nodeName(content.nodeType!==11?content:content.firstChild,"tr")){return jQuery(elem).children("tbody")[0]||elem;} +return elem;} +function disableScript(elem){elem.type=(elem.getAttribute("type")!==null)+"/"+elem.type;return elem;} +function restoreScript(elem){if((elem.type||"").slice(0,5)==="true/"){elem.type=elem.type.slice(5);}else{elem.removeAttribute("type");} +return elem;} +function cloneCopyEvent(src,dest){var i,l,type,pdataOld,pdataCur,udataOld,udataCur,events;if(dest.nodeType!==1){return;} +if(dataPriv.hasData(src)){pdataOld=dataPriv.access(src);pdataCur=dataPriv.set(dest,pdataOld);events=pdataOld.events;if(events){delete pdataCur.handle;pdataCur.events={};for(type in events){for(i=0,l=events[type].length;i1&&typeof value==="string"&&!support.checkClone&&rchecked.test(value))){return collection.each(function(index){var self=collection.eq(index);if(valueIsFunction){args[0]=value.call(this,index,self.html());} +domManip(self,args,callback,ignored);});} +if(l){fragment=buildFragment(args,collection[0].ownerDocument,false,collection,ignored);first=fragment.firstChild;if(fragment.childNodes.length===1){fragment=first;} +if(first||ignored){scripts=jQuery.map(getAll(fragment,"script"),disableScript);hasScripts=scripts.length;for(;i");},clone:function(elem,dataAndEvents,deepDataAndEvents){var i,l,srcElements,destElements,clone=elem.cloneNode(true),inPage=jQuery.contains(elem.ownerDocument,elem);if(!support.noCloneChecked&&(elem.nodeType===1||elem.nodeType===11)&&!jQuery.isXMLDoc(elem)){destElements=getAll(clone);srcElements=getAll(elem);for(i=0,l=srcElements.length;i0){setGlobalEval(destElements,!inPage&&getAll(elem,"script"));} +return clone;},cleanData:function(elems){var data,elem,type,special=jQuery.event.special,i=0;for(;(elem=elems[i])!==undefined;i++){if(acceptData(elem)){if((data=elem[dataPriv.expando])){if(data.events){for(type in data.events){if(special[type]){jQuery.event.remove(elem,type);}else{jQuery.removeEvent(elem,type,data.handle);}}} +elem[dataPriv.expando]=undefined;} +if(elem[dataUser.expando]){elem[dataUser.expando]=undefined;}}}}});jQuery.fn.extend({detach:function(selector){return remove(this,selector,true);},remove:function(selector){return remove(this,selector);},text:function(value){return access(this,function(value){return value===undefined?jQuery.text(this):this.empty().each(function(){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){this.textContent=value;}});},null,value,arguments.length);},append:function(){return domManip(this,arguments,function(elem){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var target=manipulationTarget(this,elem);target.appendChild(elem);}});},prepend:function(){return domManip(this,arguments,function(elem){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var target=manipulationTarget(this,elem);target.insertBefore(elem,target.firstChild);}});},before:function(){return domManip(this,arguments,function(elem){if(this.parentNode){this.parentNode.insertBefore(elem,this);}});},after:function(){return domManip(this,arguments,function(elem){if(this.parentNode){this.parentNode.insertBefore(elem,this.nextSibling);}});},empty:function(){var elem,i=0;for(;(elem=this[i])!=null;i++){if(elem.nodeType===1){jQuery.cleanData(getAll(elem,false));elem.textContent="";}} +return this;},clone:function(dataAndEvents,deepDataAndEvents){dataAndEvents=dataAndEvents==null?false:dataAndEvents;deepDataAndEvents=deepDataAndEvents==null?dataAndEvents:deepDataAndEvents;return this.map(function(){return jQuery.clone(this,dataAndEvents,deepDataAndEvents);});},html:function(value){return access(this,function(value){var elem=this[0]||{},i=0,l=this.length;if(value===undefined&&elem.nodeType===1){return elem.innerHTML;} +if(typeof value==="string"&&!rnoInnerhtml.test(value)&&!wrapMap[(rtagName.exec(value)||["",""])[1].toLowerCase()]){value=jQuery.htmlPrefilter(value);try{for(;i=0){delta+=Math.max(0,Math.ceil(elem["offset"+dimension[0].toUpperCase()+dimension.slice(1)]- +computedVal- +delta- +extra- +0.5));} +return delta;} +function getWidthOrHeight(elem,dimension,extra){var styles=getStyles(elem),val=curCSS(elem,dimension,styles),isBorderBox=jQuery.css(elem,"boxSizing",false,styles)==="border-box",valueIsBorderBox=isBorderBox;if(rnumnonpx.test(val)){if(!extra){return val;} +val="auto";} +valueIsBorderBox=valueIsBorderBox&&(support.boxSizingReliable()||val===elem.style[dimension]);if(val==="auto"||!parseFloat(val)&&jQuery.css(elem,"display",false,styles)==="inline"){val=elem["offset"+dimension[0].toUpperCase()+dimension.slice(1)];valueIsBorderBox=true;} +val=parseFloat(val)||0;return(val+ +boxModelAdjustment(elem,dimension,extra||(isBorderBox?"border":"content"),valueIsBorderBox,styles,val))+"px";} +jQuery.extend({cssHooks:{opacity:{get:function(elem,computed){if(computed){var ret=curCSS(elem,"opacity");return ret===""?"1":ret;}}}},cssNumber:{"animationIterationCount":true,"columnCount":true,"fillOpacity":true,"flexGrow":true,"flexShrink":true,"fontWeight":true,"lineHeight":true,"opacity":true,"order":true,"orphans":true,"widows":true,"zIndex":true,"zoom":true},cssProps:{},style:function(elem,name,value,extra){if(!elem||elem.nodeType===3||elem.nodeType===8||!elem.style){return;} +var ret,type,hooks,origName=camelCase(name),isCustomProp=rcustomProp.test(name),style=elem.style;if(!isCustomProp){name=finalPropName(origName);} +hooks=jQuery.cssHooks[name]||jQuery.cssHooks[origName];if(value!==undefined){type=typeof value;if(type==="string"&&(ret=rcssNum.exec(value))&&ret[1]){value=adjustCSS(elem,name,ret);type="number";} +if(value==null||value!==value){return;} +if(type==="number"){value+=ret&&ret[3]||(jQuery.cssNumber[origName]?"":"px");} +if(!support.clearCloneStyle&&value===""&&name.indexOf("background")===0){style[name]="inherit";} +if(!hooks||!("set"in hooks)||(value=hooks.set(elem,value,extra))!==undefined){if(isCustomProp){style.setProperty(name,value);}else{style[name]=value;}}}else{if(hooks&&"get"in hooks&&(ret=hooks.get(elem,false,extra))!==undefined){return ret;} +return style[name];}},css:function(elem,name,extra,styles){var val,num,hooks,origName=camelCase(name),isCustomProp=rcustomProp.test(name);if(!isCustomProp){name=finalPropName(origName);} +hooks=jQuery.cssHooks[name]||jQuery.cssHooks[origName];if(hooks&&"get"in hooks){val=hooks.get(elem,true,extra);} +if(val===undefined){val=curCSS(elem,name,styles);} +if(val==="normal"&&name in cssNormalTransform){val=cssNormalTransform[name];} +if(extra===""||extra){num=parseFloat(val);return extra===true||isFinite(num)?num||0:val;} +return val;}});jQuery.each(["height","width"],function(i,dimension){jQuery.cssHooks[dimension]={get:function(elem,computed,extra){if(computed){return rdisplayswap.test(jQuery.css(elem,"display"))&&(!elem.getClientRects().length||!elem.getBoundingClientRect().width)?swap(elem,cssShow,function(){return getWidthOrHeight(elem,dimension,extra);}):getWidthOrHeight(elem,dimension,extra);}},set:function(elem,value,extra){var matches,styles=getStyles(elem),isBorderBox=jQuery.css(elem,"boxSizing",false,styles)==="border-box",subtract=extra&&boxModelAdjustment(elem,dimension,extra,isBorderBox,styles);if(isBorderBox&&support.scrollboxSize()===styles.position){subtract-=Math.ceil(elem["offset"+dimension[0].toUpperCase()+dimension.slice(1)]- +parseFloat(styles[dimension])- +boxModelAdjustment(elem,dimension,"border",false,styles)- +0.5);} +if(subtract&&(matches=rcssNum.exec(value))&&(matches[3]||"px")!=="px"){elem.style[dimension]=value;value=jQuery.css(elem,dimension);} +return setPositiveNumber(elem,value,subtract);}};});jQuery.cssHooks.marginLeft=addGetHookIf(support.reliableMarginLeft,function(elem,computed){if(computed){return(parseFloat(curCSS(elem,"marginLeft"))||elem.getBoundingClientRect().left- +swap(elem,{marginLeft:0},function(){return elem.getBoundingClientRect().left;}))+"px";}});jQuery.each({margin:"",padding:"",border:"Width"},function(prefix,suffix){jQuery.cssHooks[prefix+suffix]={expand:function(value){var i=0,expanded={},parts=typeof value==="string"?value.split(" "):[value];for(;i<4;i++){expanded[prefix+cssExpand[i]+suffix]=parts[i]||parts[i-2]||parts[0];} +return expanded;}};if(prefix!=="margin"){jQuery.cssHooks[prefix+suffix].set=setPositiveNumber;}});jQuery.fn.extend({css:function(name,value){return access(this,function(elem,name,value){var styles,len,map={},i=0;if(Array.isArray(name)){styles=getStyles(elem);len=name.length;for(;i1);}});function Tween(elem,options,prop,end,easing){return new Tween.prototype.init(elem,options,prop,end,easing);} +jQuery.Tween=Tween;Tween.prototype={constructor:Tween,init:function(elem,options,prop,end,easing,unit){this.elem=elem;this.prop=prop;this.easing=easing||jQuery.easing._default;this.options=options;this.start=this.now=this.cur();this.end=end;this.unit=unit||(jQuery.cssNumber[prop]?"":"px");},cur:function(){var hooks=Tween.propHooks[this.prop];return hooks&&hooks.get?hooks.get(this):Tween.propHooks._default.get(this);},run:function(percent){var eased,hooks=Tween.propHooks[this.prop];if(this.options.duration){this.pos=eased=jQuery.easing[this.easing](percent,this.options.duration*percent,0,1,this.options.duration);}else{this.pos=eased=percent;} +this.now=(this.end-this.start)*eased+this.start;if(this.options.step){this.options.step.call(this.elem,this.now,this);} +if(hooks&&hooks.set){hooks.set(this);}else{Tween.propHooks._default.set(this);} +return this;}};Tween.prototype.init.prototype=Tween.prototype;Tween.propHooks={_default:{get:function(tween){var result;if(tween.elem.nodeType!==1||tween.elem[tween.prop]!=null&&tween.elem.style[tween.prop]==null){return tween.elem[tween.prop];} +result=jQuery.css(tween.elem,tween.prop,"");return!result||result==="auto"?0:result;},set:function(tween){if(jQuery.fx.step[tween.prop]){jQuery.fx.step[tween.prop](tween);}else if(tween.elem.nodeType===1&&(tween.elem.style[jQuery.cssProps[tween.prop]]!=null||jQuery.cssHooks[tween.prop])){jQuery.style(tween.elem,tween.prop,tween.now+tween.unit);}else{tween.elem[tween.prop]=tween.now;}}}};Tween.propHooks.scrollTop=Tween.propHooks.scrollLeft={set:function(tween){if(tween.elem.nodeType&&tween.elem.parentNode){tween.elem[tween.prop]=tween.now;}}};jQuery.easing={linear:function(p){return p;},swing:function(p){return 0.5-Math.cos(p*Math.PI)/2;},_default:"swing"};jQuery.fx=Tween.prototype.init;jQuery.fx.step={};var +fxNow,inProgress,rfxtypes=/^(?:toggle|show|hide)$/,rrun=/queueHooks$/;function schedule(){if(inProgress){if(document.hidden===false&&window.requestAnimationFrame){window.requestAnimationFrame(schedule);}else{window.setTimeout(schedule,jQuery.fx.interval);} +jQuery.fx.tick();}} +function createFxNow(){window.setTimeout(function(){fxNow=undefined;});return(fxNow=Date.now());} +function genFx(type,includeWidth){var which,i=0,attrs={height:type};includeWidth=includeWidth?1:0;for(;i<4;i+=2-includeWidth){which=cssExpand[i];attrs["margin"+which]=attrs["padding"+which]=type;} +if(includeWidth){attrs.opacity=attrs.width=type;} +return attrs;} +function createTween(value,prop,animation){var tween,collection=(Animation.tweeners[prop]||[]).concat(Animation.tweeners["*"]),index=0,length=collection.length;for(;index1);},removeAttr:function(name){return this.each(function(){jQuery.removeAttr(this,name);});}});jQuery.extend({attr:function(elem,name,value){var ret,hooks,nType=elem.nodeType;if(nType===3||nType===8||nType===2){return;} +if(typeof elem.getAttribute==="undefined"){return jQuery.prop(elem,name,value);} +if(nType!==1||!jQuery.isXMLDoc(elem)){hooks=jQuery.attrHooks[name.toLowerCase()]||(jQuery.expr.match.bool.test(name)?boolHook:undefined);} +if(value!==undefined){if(value===null){jQuery.removeAttr(elem,name);return;} +if(hooks&&"set"in hooks&&(ret=hooks.set(elem,value,name))!==undefined){return ret;} +elem.setAttribute(name,value+"");return value;} +if(hooks&&"get"in hooks&&(ret=hooks.get(elem,name))!==null){return ret;} +ret=jQuery.find.attr(elem,name);return ret==null?undefined:ret;},attrHooks:{type:{set:function(elem,value){if(!support.radioValue&&value==="radio"&&nodeName(elem,"input")){var val=elem.value;elem.setAttribute("type",value);if(val){elem.value=val;} +return value;}}}},removeAttr:function(elem,value){var name,i=0,attrNames=value&&value.match(rnothtmlwhite);if(attrNames&&elem.nodeType===1){while((name=attrNames[i++])){elem.removeAttribute(name);}}}});boolHook={set:function(elem,value,name){if(value===false){jQuery.removeAttr(elem,name);}else{elem.setAttribute(name,name);} +return name;}};jQuery.each(jQuery.expr.match.bool.source.match(/\w+/g),function(i,name){var getter=attrHandle[name]||jQuery.find.attr;attrHandle[name]=function(elem,name,isXML){var ret,handle,lowercaseName=name.toLowerCase();if(!isXML){handle=attrHandle[lowercaseName];attrHandle[lowercaseName]=ret;ret=getter(elem,name,isXML)!=null?lowercaseName:null;attrHandle[lowercaseName]=handle;} +return ret;};});var rfocusable=/^(?:input|select|textarea|button)$/i,rclickable=/^(?:a|area)$/i;jQuery.fn.extend({prop:function(name,value){return access(this,jQuery.prop,name,value,arguments.length>1);},removeProp:function(name){return this.each(function(){delete this[jQuery.propFix[name]||name];});}});jQuery.extend({prop:function(elem,name,value){var ret,hooks,nType=elem.nodeType;if(nType===3||nType===8||nType===2){return;} +if(nType!==1||!jQuery.isXMLDoc(elem)){name=jQuery.propFix[name]||name;hooks=jQuery.propHooks[name];} +if(value!==undefined){if(hooks&&"set"in hooks&&(ret=hooks.set(elem,value,name))!==undefined){return ret;} +return(elem[name]=value);} +if(hooks&&"get"in hooks&&(ret=hooks.get(elem,name))!==null){return ret;} +return elem[name];},propHooks:{tabIndex:{get:function(elem){var tabindex=jQuery.find.attr(elem,"tabindex");if(tabindex){return parseInt(tabindex,10);} +if(rfocusable.test(elem.nodeName)||rclickable.test(elem.nodeName)&&elem.href){return 0;} +return-1;}}},propFix:{"for":"htmlFor","class":"className"}});if(!support.optSelected){jQuery.propHooks.selected={get:function(elem){var parent=elem.parentNode;if(parent&&parent.parentNode){parent.parentNode.selectedIndex;} +return null;},set:function(elem){var parent=elem.parentNode;if(parent){parent.selectedIndex;if(parent.parentNode){parent.parentNode.selectedIndex;}}}};} +jQuery.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){jQuery.propFix[this.toLowerCase()]=this;});function stripAndCollapse(value){var tokens=value.match(rnothtmlwhite)||[];return tokens.join(" ");} +function getClass(elem){return elem.getAttribute&&elem.getAttribute("class")||"";} +function classesToArray(value){if(Array.isArray(value)){return value;} +if(typeof value==="string"){return value.match(rnothtmlwhite)||[];} +return[];} +jQuery.fn.extend({addClass:function(value){var classes,elem,cur,curValue,clazz,j,finalValue,i=0;if(isFunction(value)){return this.each(function(j){jQuery(this).addClass(value.call(this,j,getClass(this)));});} +classes=classesToArray(value);if(classes.length){while((elem=this[i++])){curValue=getClass(elem);cur=elem.nodeType===1&&(" "+stripAndCollapse(curValue)+" ");if(cur){j=0;while((clazz=classes[j++])){if(cur.indexOf(" "+clazz+" ")<0){cur+=clazz+" ";}} +finalValue=stripAndCollapse(cur);if(curValue!==finalValue){elem.setAttribute("class",finalValue);}}}} +return this;},removeClass:function(value){var classes,elem,cur,curValue,clazz,j,finalValue,i=0;if(isFunction(value)){return this.each(function(j){jQuery(this).removeClass(value.call(this,j,getClass(this)));});} +if(!arguments.length){return this.attr("class","");} +classes=classesToArray(value);if(classes.length){while((elem=this[i++])){curValue=getClass(elem);cur=elem.nodeType===1&&(" "+stripAndCollapse(curValue)+" ");if(cur){j=0;while((clazz=classes[j++])){while(cur.indexOf(" "+clazz+" ")>-1){cur=cur.replace(" "+clazz+" "," ");}} +finalValue=stripAndCollapse(cur);if(curValue!==finalValue){elem.setAttribute("class",finalValue);}}}} +return this;},toggleClass:function(value,stateVal){var type=typeof value,isValidValue=type==="string"||Array.isArray(value);if(typeof stateVal==="boolean"&&isValidValue){return stateVal?this.addClass(value):this.removeClass(value);} +if(isFunction(value)){return this.each(function(i){jQuery(this).toggleClass(value.call(this,i,getClass(this),stateVal),stateVal);});} +return this.each(function(){var className,i,self,classNames;if(isValidValue){i=0;self=jQuery(this);classNames=classesToArray(value);while((className=classNames[i++])){if(self.hasClass(className)){self.removeClass(className);}else{self.addClass(className);}} +}else if(value===undefined||type==="boolean"){className=getClass(this);if(className){dataPriv.set(this,"__className__",className);} +if(this.setAttribute){this.setAttribute("class",className||value===false?"":dataPriv.get(this,"__className__")||"");}}});},hasClass:function(selector){var className,elem,i=0;className=" "+selector+" ";while((elem=this[i++])){if(elem.nodeType===1&&(" "+stripAndCollapse(getClass(elem))+" ").indexOf(className)>-1){return true;}} +return false;}});var rreturn=/\r/g;jQuery.fn.extend({val:function(value){var hooks,ret,valueIsFunction,elem=this[0];if(!arguments.length){if(elem){hooks=jQuery.valHooks[elem.type]||jQuery.valHooks[elem.nodeName.toLowerCase()];if(hooks&&"get"in hooks&&(ret=hooks.get(elem,"value"))!==undefined){return ret;} +ret=elem.value;if(typeof ret==="string"){return ret.replace(rreturn,"");} +return ret==null?"":ret;} +return;} +valueIsFunction=isFunction(value);return this.each(function(i){var val;if(this.nodeType!==1){return;} +if(valueIsFunction){val=value.call(this,i,jQuery(this).val());}else{val=value;} +if(val==null){val="";}else if(typeof val==="number"){val+="";}else if(Array.isArray(val)){val=jQuery.map(val,function(value){return value==null?"":value+"";});} +hooks=jQuery.valHooks[this.type]||jQuery.valHooks[this.nodeName.toLowerCase()];if(!hooks||!("set"in hooks)||hooks.set(this,val,"value")===undefined){this.value=val;}});}});jQuery.extend({valHooks:{option:{get:function(elem){var val=jQuery.find.attr(elem,"value");return val!=null?val:stripAndCollapse(jQuery.text(elem));}},select:{get:function(elem){var value,option,i,options=elem.options,index=elem.selectedIndex,one=elem.type==="select-one",values=one?null:[],max=one?index+1:options.length;if(index<0){i=max;}else{i=one?index:0;} +for(;i-1){optionSet=true;} +} +if(!optionSet){elem.selectedIndex=-1;} +return values;}}}});jQuery.each(["radio","checkbox"],function(){jQuery.valHooks[this]={set:function(elem,value){if(Array.isArray(value)){return(elem.checked=jQuery.inArray(jQuery(elem).val(),value)>-1);}}};if(!support.checkOn){jQuery.valHooks[this].get=function(elem){return elem.getAttribute("value")===null?"on":elem.value;};}});support.focusin="onfocusin"in window;var rfocusMorph=/^(?:focusinfocus|focusoutblur)$/,stopPropagationCallback=function(e){e.stopPropagation();};jQuery.extend(jQuery.event,{trigger:function(event,data,elem,onlyHandlers){var i,cur,tmp,bubbleType,ontype,handle,special,lastElement,eventPath=[elem||document],type=hasOwn.call(event,"type")?event.type:event,namespaces=hasOwn.call(event,"namespace")?event.namespace.split("."):[];cur=lastElement=tmp=elem=elem||document;if(elem.nodeType===3||elem.nodeType===8){return;} +if(rfocusMorph.test(type+jQuery.event.triggered)){return;} +if(type.indexOf(".")>-1){namespaces=type.split(".");type=namespaces.shift();namespaces.sort();} +ontype=type.indexOf(":")<0&&"on"+type;event=event[jQuery.expando]?event:new jQuery.Event(type,typeof event==="object"&&event);event.isTrigger=onlyHandlers?2:3;event.namespace=namespaces.join(".");event.rnamespace=event.namespace?new RegExp("(^|\\.)"+namespaces.join("\\.(?:.*\\.|)")+"(\\.|$)"):null;event.result=undefined;if(!event.target){event.target=elem;} +data=data==null?[event]:jQuery.makeArray(data,[event]);special=jQuery.event.special[type]||{};if(!onlyHandlers&&special.trigger&&special.trigger.apply(elem,data)===false){return;} +if(!onlyHandlers&&!special.noBubble&&!isWindow(elem)){bubbleType=special.delegateType||type;if(!rfocusMorph.test(bubbleType+type)){cur=cur.parentNode;} +for(;cur;cur=cur.parentNode){eventPath.push(cur);tmp=cur;} +if(tmp===(elem.ownerDocument||document)){eventPath.push(tmp.defaultView||tmp.parentWindow||window);}} +i=0;while((cur=eventPath[i++])&&!event.isPropagationStopped()){lastElement=cur;event.type=i>1?bubbleType:special.bindType||type;handle=(dataPriv.get(cur,"events")||{})[event.type]&&dataPriv.get(cur,"handle");if(handle){handle.apply(cur,data);} +handle=ontype&&cur[ontype];if(handle&&handle.apply&&acceptData(cur)){event.result=handle.apply(cur,data);if(event.result===false){event.preventDefault();}}} +event.type=type;if(!onlyHandlers&&!event.isDefaultPrevented()){if((!special._default||special._default.apply(eventPath.pop(),data)===false)&&acceptData(elem)){if(ontype&&isFunction(elem[type])&&!isWindow(elem)){tmp=elem[ontype];if(tmp){elem[ontype]=null;} +jQuery.event.triggered=type;if(event.isPropagationStopped()){lastElement.addEventListener(type,stopPropagationCallback);} +elem[type]();if(event.isPropagationStopped()){lastElement.removeEventListener(type,stopPropagationCallback);} +jQuery.event.triggered=undefined;if(tmp){elem[ontype]=tmp;}}}} +return event.result;},simulate:function(type,elem,event){var e=jQuery.extend(new jQuery.Event(),event,{type:type,isSimulated:true});jQuery.event.trigger(e,null,elem);}});jQuery.fn.extend({trigger:function(type,data){return this.each(function(){jQuery.event.trigger(type,data,this);});},triggerHandler:function(type,data){var elem=this[0];if(elem){return jQuery.event.trigger(type,data,elem,true);}}});if(!support.focusin){jQuery.each({focus:"focusin",blur:"focusout"},function(orig,fix){var handler=function(event){jQuery.event.simulate(fix,event.target,jQuery.event.fix(event));};jQuery.event.special[fix]={setup:function(){var doc=this.ownerDocument||this,attaches=dataPriv.access(doc,fix);if(!attaches){doc.addEventListener(orig,handler,true);} +dataPriv.access(doc,fix,(attaches||0)+1);},teardown:function(){var doc=this.ownerDocument||this,attaches=dataPriv.access(doc,fix)-1;if(!attaches){doc.removeEventListener(orig,handler,true);dataPriv.remove(doc,fix);}else{dataPriv.access(doc,fix,attaches);}}};});} +var location=window.location;var nonce=Date.now();var rquery=(/\?/);jQuery.parseXML=function(data){var xml;if(!data||typeof data!=="string"){return null;} +try{xml=(new window.DOMParser()).parseFromString(data,"text/xml");}catch(e){xml=undefined;} +if(!xml||xml.getElementsByTagName("parsererror").length){jQuery.error("Invalid XML: "+data);} +return xml;};var +rbracket=/\[\]$/,rCRLF=/\r?\n/g,rsubmitterTypes=/^(?:submit|button|image|reset|file)$/i,rsubmittable=/^(?:input|select|textarea|keygen)/i;function buildParams(prefix,obj,traditional,add){var name;if(Array.isArray(obj)){jQuery.each(obj,function(i,v){if(traditional||rbracket.test(prefix)){add(prefix,v);}else{buildParams(prefix+"["+(typeof v==="object"&&v!=null?i:"")+"]",v,traditional,add);}});}else if(!traditional&&toType(obj)==="object"){for(name in obj){buildParams(prefix+"["+name+"]",obj[name],traditional,add);}}else{add(prefix,obj);}} +jQuery.param=function(a,traditional){var prefix,s=[],add=function(key,valueOrFunction){var value=isFunction(valueOrFunction)?valueOrFunction():valueOrFunction;s[s.length]=encodeURIComponent(key)+"="+ +encodeURIComponent(value==null?"":value);};if(Array.isArray(a)||(a.jquery&&!jQuery.isPlainObject(a))){jQuery.each(a,function(){add(this.name,this.value);});}else{for(prefix in a){buildParams(prefix,a[prefix],traditional,add);}} +return s.join("&");};jQuery.fn.extend({serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){var elements=jQuery.prop(this,"elements");return elements?jQuery.makeArray(elements):this;}).filter(function(){var type=this.type;return this.name&&!jQuery(this).is(":disabled")&&rsubmittable.test(this.nodeName)&&!rsubmitterTypes.test(type)&&(this.checked||!rcheckableType.test(type));}).map(function(i,elem){var val=jQuery(this).val();if(val==null){return null;} +if(Array.isArray(val)){return jQuery.map(val,function(val){return{name:elem.name,value:val.replace(rCRLF,"\r\n")};});} +return{name:elem.name,value:val.replace(rCRLF,"\r\n")};}).get();}});var +r20=/%20/g,rhash=/#.*$/,rantiCache=/([?&])_=[^&]*/,rheaders=/^(.*?):[ \t]*([^\r\n]*)$/mg,rlocalProtocol=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,rnoContent=/^(?:GET|HEAD)$/,rprotocol=/^\/\//,prefilters={},transports={},allTypes="*/".concat("*"),originAnchor=document.createElement("a");originAnchor.href=location.href;function addToPrefiltersOrTransports(structure){return function(dataTypeExpression,func){if(typeof dataTypeExpression!=="string"){func=dataTypeExpression;dataTypeExpression="*";} +var dataType,i=0,dataTypes=dataTypeExpression.toLowerCase().match(rnothtmlwhite)||[];if(isFunction(func)){while((dataType=dataTypes[i++])){if(dataType[0]==="+"){dataType=dataType.slice(1)||"*";(structure[dataType]=structure[dataType]||[]).unshift(func);}else{(structure[dataType]=structure[dataType]||[]).push(func);}}}};} +function inspectPrefiltersOrTransports(structure,options,originalOptions,jqXHR){var inspected={},seekingTransport=(structure===transports);function inspect(dataType){var selected;inspected[dataType]=true;jQuery.each(structure[dataType]||[],function(_,prefilterOrFactory){var dataTypeOrTransport=prefilterOrFactory(options,originalOptions,jqXHR);if(typeof dataTypeOrTransport==="string"&&!seekingTransport&&!inspected[dataTypeOrTransport]){options.dataTypes.unshift(dataTypeOrTransport);inspect(dataTypeOrTransport);return false;}else if(seekingTransport){return!(selected=dataTypeOrTransport);}});return selected;} +return inspect(options.dataTypes[0])||!inspected["*"]&&inspect("*");} +function ajaxExtend(target,src){var key,deep,flatOptions=jQuery.ajaxSettings.flatOptions||{};for(key in src){if(src[key]!==undefined){(flatOptions[key]?target:(deep||(deep={})))[key]=src[key];}} +if(deep){jQuery.extend(true,target,deep);} +return target;} +function ajaxHandleResponses(s,jqXHR,responses){var ct,type,finalDataType,firstDataType,contents=s.contents,dataTypes=s.dataTypes;while(dataTypes[0]==="*"){dataTypes.shift();if(ct===undefined){ct=s.mimeType||jqXHR.getResponseHeader("Content-Type");}} +if(ct){for(type in contents){if(contents[type]&&contents[type].test(ct)){dataTypes.unshift(type);break;}}} +if(dataTypes[0]in responses){finalDataType=dataTypes[0];}else{for(type in responses){if(!dataTypes[0]||s.converters[type+" "+dataTypes[0]]){finalDataType=type;break;} +if(!firstDataType){firstDataType=type;}} +finalDataType=finalDataType||firstDataType;} +if(finalDataType){if(finalDataType!==dataTypes[0]){dataTypes.unshift(finalDataType);} +return responses[finalDataType];}} +function ajaxConvert(s,response,jqXHR,isSuccess){var conv2,current,conv,tmp,prev,converters={},dataTypes=s.dataTypes.slice();if(dataTypes[1]){for(conv in s.converters){converters[conv.toLowerCase()]=s.converters[conv];}} +current=dataTypes.shift();while(current){if(s.responseFields[current]){jqXHR[s.responseFields[current]]=response;} +if(!prev&&isSuccess&&s.dataFilter){response=s.dataFilter(response,s.dataType);} +prev=current;current=dataTypes.shift();if(current){if(current==="*"){current=prev;}else if(prev!=="*"&&prev!==current){conv=converters[prev+" "+current]||converters["* "+current];if(!conv){for(conv2 in converters){tmp=conv2.split(" ");if(tmp[1]===current){conv=converters[prev+" "+tmp[0]]||converters["* "+tmp[0]];if(conv){if(conv===true){conv=converters[conv2];}else if(converters[conv2]!==true){current=tmp[0];dataTypes.unshift(tmp[1]);} +break;}}}} +if(conv!==true){if(conv&&s.throws){response=conv(response);}else{try{response=conv(response);}catch(e){return{state:"parsererror",error:conv?e:"No conversion from "+prev+" to "+current};}}}}}} +return{state:"success",data:response};} +jQuery.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:location.href,type:"GET",isLocal:rlocalProtocol.test(location.protocol),global:true,processData:true,async:true,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":allTypes,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":true,"text json":JSON.parse,"text xml":jQuery.parseXML},flatOptions:{url:true,context:true}},ajaxSetup:function(target,settings){return settings?ajaxExtend(ajaxExtend(target,jQuery.ajaxSettings),settings):ajaxExtend(jQuery.ajaxSettings,target);},ajaxPrefilter:addToPrefiltersOrTransports(prefilters),ajaxTransport:addToPrefiltersOrTransports(transports),ajax:function(url,options){if(typeof url==="object"){options=url;url=undefined;} +options=options||{};var transport,cacheURL,responseHeadersString,responseHeaders,timeoutTimer,urlAnchor,completed,fireGlobals,i,uncached,s=jQuery.ajaxSetup({},options),callbackContext=s.context||s,globalEventContext=s.context&&(callbackContext.nodeType||callbackContext.jquery)?jQuery(callbackContext):jQuery.event,deferred=jQuery.Deferred(),completeDeferred=jQuery.Callbacks("once memory"),statusCode=s.statusCode||{},requestHeaders={},requestHeadersNames={},strAbort="canceled",jqXHR={readyState:0,getResponseHeader:function(key){var match;if(completed){if(!responseHeaders){responseHeaders={};while((match=rheaders.exec(responseHeadersString))){responseHeaders[match[1].toLowerCase()]=match[2];}} +match=responseHeaders[key.toLowerCase()];} +return match==null?null:match;},getAllResponseHeaders:function(){return completed?responseHeadersString:null;},setRequestHeader:function(name,value){if(completed==null){name=requestHeadersNames[name.toLowerCase()]=requestHeadersNames[name.toLowerCase()]||name;requestHeaders[name]=value;} +return this;},overrideMimeType:function(type){if(completed==null){s.mimeType=type;} +return this;},statusCode:function(map){var code;if(map){if(completed){jqXHR.always(map[jqXHR.status]);}else{for(code in map){statusCode[code]=[statusCode[code],map[code]];}}} +return this;},abort:function(statusText){var finalText=statusText||strAbort;if(transport){transport.abort(finalText);} +done(0,finalText);return this;}};deferred.promise(jqXHR);s.url=((url||s.url||location.href)+"").replace(rprotocol,location.protocol+"//");s.type=options.method||options.type||s.method||s.type;s.dataTypes=(s.dataType||"*").toLowerCase().match(rnothtmlwhite)||[""];if(s.crossDomain==null){urlAnchor=document.createElement("a");try{urlAnchor.href=s.url;urlAnchor.href=urlAnchor.href;s.crossDomain=originAnchor.protocol+"//"+originAnchor.host!==urlAnchor.protocol+"//"+urlAnchor.host;}catch(e){s.crossDomain=true;}} +if(s.data&&s.processData&&typeof s.data!=="string"){s.data=jQuery.param(s.data,s.traditional);} +inspectPrefiltersOrTransports(prefilters,s,options,jqXHR);if(completed){return jqXHR;} +fireGlobals=jQuery.event&&s.global;if(fireGlobals&&jQuery.active++===0){jQuery.event.trigger("ajaxStart");} +s.type=s.type.toUpperCase();s.hasContent=!rnoContent.test(s.type);cacheURL=s.url.replace(rhash,"");if(!s.hasContent){uncached=s.url.slice(cacheURL.length);if(s.data&&(s.processData||typeof s.data==="string")){cacheURL+=(rquery.test(cacheURL)?"&":"?")+s.data;delete s.data;} +if(s.cache===false){cacheURL=cacheURL.replace(rantiCache,"$1");uncached=(rquery.test(cacheURL)?"&":"?")+"_="+(nonce++)+uncached;} +s.url=cacheURL+uncached;}else if(s.data&&s.processData&&(s.contentType||"").indexOf("application/x-www-form-urlencoded")===0){s.data=s.data.replace(r20,"+");} +if(s.ifModified){if(jQuery.lastModified[cacheURL]){jqXHR.setRequestHeader("If-Modified-Since",jQuery.lastModified[cacheURL]);} +if(jQuery.etag[cacheURL]){jqXHR.setRequestHeader("If-None-Match",jQuery.etag[cacheURL]);}} +if(s.data&&s.hasContent&&s.contentType!==false||options.contentType){jqXHR.setRequestHeader("Content-Type",s.contentType);} +jqXHR.setRequestHeader("Accept",s.dataTypes[0]&&s.accepts[s.dataTypes[0]]?s.accepts[s.dataTypes[0]]+ +(s.dataTypes[0]!=="*"?", "+allTypes+"; q=0.01":""):s.accepts["*"]);for(i in s.headers){jqXHR.setRequestHeader(i,s.headers[i]);} +if(s.beforeSend&&(s.beforeSend.call(callbackContext,jqXHR,s)===false||completed)){return jqXHR.abort();} +strAbort="abort";completeDeferred.add(s.complete);jqXHR.done(s.success);jqXHR.fail(s.error);transport=inspectPrefiltersOrTransports(transports,s,options,jqXHR);if(!transport){done(-1,"No Transport");}else{jqXHR.readyState=1;if(fireGlobals){globalEventContext.trigger("ajaxSend",[jqXHR,s]);} +if(completed){return jqXHR;} +if(s.async&&s.timeout>0){timeoutTimer=window.setTimeout(function(){jqXHR.abort("timeout");},s.timeout);} +try{completed=false;transport.send(requestHeaders,done);}catch(e){if(completed){throw e;} +done(-1,e);}} +function done(status,nativeStatusText,responses,headers){var isSuccess,success,error,response,modified,statusText=nativeStatusText;if(completed){return;} +completed=true;if(timeoutTimer){window.clearTimeout(timeoutTimer);} +transport=undefined;responseHeadersString=headers||"";jqXHR.readyState=status>0?4:0;isSuccess=status>=200&&status<300||status===304;if(responses){response=ajaxHandleResponses(s,jqXHR,responses);} +response=ajaxConvert(s,response,jqXHR,isSuccess);if(isSuccess){if(s.ifModified){modified=jqXHR.getResponseHeader("Last-Modified");if(modified){jQuery.lastModified[cacheURL]=modified;} +modified=jqXHR.getResponseHeader("etag");if(modified){jQuery.etag[cacheURL]=modified;}} +if(status===204||s.type==="HEAD"){statusText="nocontent";}else if(status===304){statusText="notmodified";}else{statusText=response.state;success=response.data;error=response.error;isSuccess=!error;}}else{error=statusText;if(status||!statusText){statusText="error";if(status<0){status=0;}}} +jqXHR.status=status;jqXHR.statusText=(nativeStatusText||statusText)+"";if(isSuccess){deferred.resolveWith(callbackContext,[success,statusText,jqXHR]);}else{deferred.rejectWith(callbackContext,[jqXHR,statusText,error]);} +jqXHR.statusCode(statusCode);statusCode=undefined;if(fireGlobals){globalEventContext.trigger(isSuccess?"ajaxSuccess":"ajaxError",[jqXHR,s,isSuccess?success:error]);} +completeDeferred.fireWith(callbackContext,[jqXHR,statusText]);if(fireGlobals){globalEventContext.trigger("ajaxComplete",[jqXHR,s]);if(!(--jQuery.active)){jQuery.event.trigger("ajaxStop");}}} +return jqXHR;},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},getScript:function(url,callback){return jQuery.get(url,undefined,callback,"script");}});jQuery.each(["get","post"],function(i,method){jQuery[method]=function(url,data,callback,type){if(isFunction(data)){type=type||callback;callback=data;data=undefined;} +return jQuery.ajax(jQuery.extend({url:url,type:method,dataType:type,data:data,success:callback},jQuery.isPlainObject(url)&&url));};});jQuery._evalUrl=function(url){return jQuery.ajax({url:url,type:"GET",dataType:"script",cache:true,async:false,global:false,"throws":true});};jQuery.fn.extend({wrapAll:function(html){var wrap;if(this[0]){if(isFunction(html)){html=html.call(this[0]);} +wrap=jQuery(html,this[0].ownerDocument).eq(0).clone(true);if(this[0].parentNode){wrap.insertBefore(this[0]);} +wrap.map(function(){var elem=this;while(elem.firstElementChild){elem=elem.firstElementChild;} +return elem;}).append(this);} +return this;},wrapInner:function(html){if(isFunction(html)){return this.each(function(i){jQuery(this).wrapInner(html.call(this,i));});} +return this.each(function(){var self=jQuery(this),contents=self.contents();if(contents.length){contents.wrapAll(html);}else{self.append(html);}});},wrap:function(html){var htmlIsFunction=isFunction(html);return this.each(function(i){jQuery(this).wrapAll(htmlIsFunction?html.call(this,i):html);});},unwrap:function(selector){this.parent(selector).not("body").each(function(){jQuery(this).replaceWith(this.childNodes);});return this;}});jQuery.expr.pseudos.hidden=function(elem){return!jQuery.expr.pseudos.visible(elem);};jQuery.expr.pseudos.visible=function(elem){return!!(elem.offsetWidth||elem.offsetHeight||elem.getClientRects().length);};jQuery.ajaxSettings.xhr=function(){try{return new window.XMLHttpRequest();}catch(e){}};var xhrSuccessStatus={0:200,1223:204},xhrSupported=jQuery.ajaxSettings.xhr();support.cors=!!xhrSupported&&("withCredentials"in xhrSupported);support.ajax=xhrSupported=!!xhrSupported;jQuery.ajaxTransport(function(options){var callback,errorCallback;if(support.cors||xhrSupported&&!options.crossDomain){return{send:function(headers,complete){var i,xhr=options.xhr();xhr.open(options.type,options.url,options.async,options.username,options.password);if(options.xhrFields){for(i in options.xhrFields){xhr[i]=options.xhrFields[i];}} +if(options.mimeType&&xhr.overrideMimeType){xhr.overrideMimeType(options.mimeType);} +if(!options.crossDomain&&!headers["X-Requested-With"]){headers["X-Requested-With"]="XMLHttpRequest";} +for(i in headers){xhr.setRequestHeader(i,headers[i]);} +callback=function(type){return function(){if(callback){callback=errorCallback=xhr.onload=xhr.onerror=xhr.onabort=xhr.ontimeout=xhr.onreadystatechange=null;if(type==="abort"){xhr.abort();}else if(type==="error"){if(typeof xhr.status!=="number"){complete(0,"error");}else{complete(xhr.status,xhr.statusText);}}else{complete(xhrSuccessStatus[xhr.status]||xhr.status,xhr.statusText,(xhr.responseType||"text")!=="text"||typeof xhr.responseText!=="string"?{binary:xhr.response}:{text:xhr.responseText},xhr.getAllResponseHeaders());}}};};xhr.onload=callback();errorCallback=xhr.onerror=xhr.ontimeout=callback("error");if(xhr.onabort!==undefined){xhr.onabort=errorCallback;}else{xhr.onreadystatechange=function(){if(xhr.readyState===4){window.setTimeout(function(){if(callback){errorCallback();}});}};} +callback=callback("abort");try{xhr.send(options.hasContent&&options.data||null);}catch(e){if(callback){throw e;}}},abort:function(){if(callback){callback();}}};}});jQuery.ajaxPrefilter(function(s){if(s.crossDomain){s.contents.script=false;}});jQuery.ajaxSetup({accepts:{script:"text/javascript, application/javascript, "+ +"application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(text){jQuery.globalEval(text);return text;}}});jQuery.ajaxPrefilter("script",function(s){if(s.cache===undefined){s.cache=false;} +if(s.crossDomain){s.type="GET";}});jQuery.ajaxTransport("script",function(s){if(s.crossDomain){var script,callback;return{send:function(_,complete){script=jQuery(" +{% endblock %} diff --git a/powerdnsadmin/templates/admin_edit_user.html b/powerdnsadmin/templates/admin_edit_user.html new file mode 100644 index 0000000..34f4272 --- /dev/null +++ b/powerdnsadmin/templates/admin_edit_user.html @@ -0,0 +1,166 @@ +{% extends "base.html" %} +{% set active_page = "admin_users" %} +{% block title %}Edit User - {{ SITE_NAME }}{% endblock %} + +{% block dashboard_stat %} + +
+

+ User + {% if create %}New user{% else %}{{ user.username }}{% endif %} +

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

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

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

Error!

+ {{ error }} +
+ {{ error }} + {% endif %} +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+ + + {% if blank_password %} + The password cannot be blank. + {% endif %} +
+
+ +
+
+ {% if not create %} +
+
+

Two Factor Authentication

+
+
+

If two factor authentication was configured and is causing problems due to a lost device or + technical issue, it can be disabled here.

+

The user will need to reconfigure two factor authentication, to re-enable it.

+

Beware: This could compromise security!

+
+ +
+ {% endif %} +
+
+
+
+

Help with {% if create %}creating a new{% else%}updating a{% endif %} user +

+
+
+

Fill in all the fields to the in the form to the left.

+ {% if create %} +

Newly created users do not have access to any domains. You will need to grant + access to the user once it is created via the domain management buttons on the dashboard.

+ {% else %} +

Password can be left empty to keep the current password.

+

Username cannot be changed.

+ {% endif %} +
+
+
+
+
+{% endblock %} +{% block extrascripts %} + +{% endblock %} +{% block modals %} + +{% endblock %} diff --git a/app/templates/admin_history.html b/powerdnsadmin/templates/admin_history.html similarity index 76% rename from app/templates/admin_history.html rename to powerdnsadmin/templates/admin_history.html index d5d6e43..b8229d3 100644 --- a/app/templates/admin_history.html +++ b/powerdnsadmin/templates/admin_history.html @@ -6,11 +6,10 @@

- History Recent PowerDNS-Admin events + History Recent events

@@ -23,7 +22,9 @@

History Management

-
@@ -44,7 +45,8 @@ {{ history.msg }} {{ history.created_on }} - @@ -65,24 +67,24 @@ {% endblock %} {% block modals %} @@ -111,8 +118,7 @@
- + @@ -49,22 +48,30 @@ {{ user.lastname }} {{ user.email }} - {% for role in roles %} - + {% endfor %} - - - @@ -86,39 +93,50 @@ {% endblock %} @@ -146,8 +164,7 @@