mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-30 15:05:39 +00:00
Merge pull request #622 from ngoduykhanh/email_verification
Add user email verification
This commit is contained in:
commit
3bf6e6e9f1
@ -20,6 +20,16 @@ SQLALCHEMY_TRACK_MODIFICATIONS = True
|
|||||||
### DATABASE - SQLite
|
### DATABASE - SQLite
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||||
|
|
||||||
|
### SMTP config
|
||||||
|
# MAIL_SERVER = 'localhost'
|
||||||
|
# MAIL_PORT = 25
|
||||||
|
# MAIL_DEBUG = False
|
||||||
|
# MAIL_USE_TLS = False
|
||||||
|
# MAIL_USE_SSL = False
|
||||||
|
# MAIL_USERNAME = None
|
||||||
|
# MAIL_PASSWORD = None
|
||||||
|
# MAIL_DEFAULT_SENDER = ('PowerDNS-Admin', 'noreply@domain.ltd')
|
||||||
|
|
||||||
# SAML Authnetication
|
# SAML Authnetication
|
||||||
SAML_ENABLED = False
|
SAML_ENABLED = False
|
||||||
# SAML_DEBUG = True
|
# SAML_DEBUG = True
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Defaults for Docker image
|
# Defaults for Docker image
|
||||||
BIND_ADDRESS='0.0.0.0'
|
BIND_ADDRESS = '0.0.0.0'
|
||||||
PORT=80
|
PORT = 80
|
||||||
|
|
||||||
legal_envvars = (
|
legal_envvars = (
|
||||||
'SECRET_KEY',
|
'SECRET_KEY',
|
||||||
@ -10,6 +10,14 @@ legal_envvars = (
|
|||||||
'SALT',
|
'SALT',
|
||||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||||
'SQLALCHEMY_DATABASE_URI',
|
'SQLALCHEMY_DATABASE_URI',
|
||||||
|
'MAIL_SERVER',
|
||||||
|
'MAIL_PORT',
|
||||||
|
'MAIL_DEBUG',
|
||||||
|
'MAIL_USE_TLS',
|
||||||
|
'MAIL_USE_SSL',
|
||||||
|
'MAIL_USERNAME',
|
||||||
|
'MAIL_PASSWORD',
|
||||||
|
'MAIL_DEFAULT_SENDER',
|
||||||
'SAML_ENABLED',
|
'SAML_ENABLED',
|
||||||
'SAML_DEBUG',
|
'SAML_DEBUG',
|
||||||
'SAML_PATH',
|
'SAML_PATH',
|
||||||
@ -37,14 +45,14 @@ legal_envvars = (
|
|||||||
'SAML_LOGOUT_URL',
|
'SAML_LOGOUT_URL',
|
||||||
)
|
)
|
||||||
|
|
||||||
legal_envvars_int = (
|
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
|
||||||
'PORT',
|
|
||||||
'SAML_METADATA_CACHE_LIFETIME',
|
|
||||||
)
|
|
||||||
|
|
||||||
legal_envvars_bool = (
|
legal_envvars_bool = (
|
||||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||||
'HSTS_ENABLED',
|
'HSTS_ENABLED',
|
||||||
|
'MAIL_DEBUG',
|
||||||
|
'MAIL_USE_TLS',
|
||||||
|
'MAIL_USE_SSL',
|
||||||
'SAML_ENABLED',
|
'SAML_ENABLED',
|
||||||
'SAML_DEBUG',
|
'SAML_DEBUG',
|
||||||
'SAML_SIGN_REQUEST',
|
'SAML_SIGN_REQUEST',
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
"""Add user.confirmed column
|
||||||
|
|
||||||
|
Revision ID: 3f76448bb6de
|
||||||
|
Revises: b0fea72a3f20
|
||||||
|
Create Date: 2019-12-21 17:11:36.564632
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '3f76448bb6de'
|
||||||
|
down_revision = 'b0fea72a3f20'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
with op.batch_alter_table('user') as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column('confirmed', sa.Boolean(), nullable=False,
|
||||||
|
default=False))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
with op.batch_alter_table('user') as batch_op:
|
||||||
|
batch_op.drop_column('confirmed')
|
@ -2,6 +2,7 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_seasurf import SeaSurf
|
from flask_seasurf import SeaSurf
|
||||||
|
from flask_mail import Mail
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
from .lib import utils
|
from .lib import utils
|
||||||
@ -64,6 +65,9 @@ def create_app(config=None):
|
|||||||
from flask_sslify import SSLify
|
from flask_sslify import SSLify
|
||||||
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
|
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
|
||||||
|
|
||||||
|
# SMTP
|
||||||
|
app.mail = Mail(app)
|
||||||
|
|
||||||
# Load app's components
|
# Load app's components
|
||||||
assets.init_app(app)
|
assets.init_app(app)
|
||||||
models.init_app(app)
|
models.init_app(app)
|
||||||
|
@ -4,7 +4,7 @@ from functools import wraps
|
|||||||
from flask import g, request, abort, current_app, render_template
|
from flask import g, request, abort, current_app, render_template
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from .models import User, ApiKey, Setting, Domain
|
from .models import User, ApiKey, Setting, Domain, Setting
|
||||||
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
|
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
|
||||||
from .lib.errors import DomainAccessForbidden
|
from .lib.errors import DomainAccessForbidden
|
||||||
|
|
||||||
@ -121,6 +121,12 @@ def api_basic_auth(f):
|
|||||||
plain_text_password=password)
|
plain_text_password=password)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if Setting().get('verify_user_email') and user.email and not user.confirmed:
|
||||||
|
current_app.logger.warning(
|
||||||
|
'Basic authentication failed for user {} because of unverified email address'
|
||||||
|
.format(username))
|
||||||
|
abort(401)
|
||||||
|
|
||||||
auth_method = request.args.get('auth_method', 'LOCAL')
|
auth_method = request.args.get('auth_method', 'LOCAL')
|
||||||
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
||||||
auth = user.is_validate(method=auth_method,
|
auth = user.is_validate(method=auth_method,
|
||||||
|
@ -25,6 +25,7 @@ class Setting(db.Model):
|
|||||||
'allow_user_create_domain': False,
|
'allow_user_create_domain': False,
|
||||||
'bg_domain_updates': False,
|
'bg_domain_updates': False,
|
||||||
'site_name': 'PowerDNS-Admin',
|
'site_name': 'PowerDNS-Admin',
|
||||||
|
'site_url': 'http://localhost:9191',
|
||||||
'session_timeout': 10,
|
'session_timeout': 10,
|
||||||
'warn_session_timeout': True,
|
'warn_session_timeout': True,
|
||||||
'pdns_api_url': '',
|
'pdns_api_url': '',
|
||||||
@ -33,6 +34,7 @@ class Setting(db.Model):
|
|||||||
'pdns_version': '4.1.1',
|
'pdns_version': '4.1.1',
|
||||||
'local_db_enabled': True,
|
'local_db_enabled': True,
|
||||||
'signup_enabled': True,
|
'signup_enabled': True,
|
||||||
|
'verify_user_email': False,
|
||||||
'ldap_enabled': False,
|
'ldap_enabled': False,
|
||||||
'ldap_type': 'ldap',
|
'ldap_type': 'ldap',
|
||||||
'ldap_uri': '',
|
'ldap_uri': '',
|
||||||
|
@ -26,6 +26,7 @@ class User(db.Model):
|
|||||||
lastname = db.Column(db.String(64))
|
lastname = db.Column(db.String(64))
|
||||||
email = db.Column(db.String(128))
|
email = db.Column(db.String(128))
|
||||||
otp_secret = db.Column(db.String(16))
|
otp_secret = db.Column(db.String(16))
|
||||||
|
confirmed = db.Column(db.SmallInteger, nullable=False, default=0)
|
||||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
@ -38,6 +39,7 @@ class User(db.Model):
|
|||||||
role_id=None,
|
role_id=None,
|
||||||
email=None,
|
email=None,
|
||||||
otp_secret=None,
|
otp_secret=None,
|
||||||
|
confirmed=False,
|
||||||
reload_info=True):
|
reload_info=True):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.username = username
|
self.username = username
|
||||||
@ -48,6 +50,7 @@ class User(db.Model):
|
|||||||
self.role_id = role_id
|
self.role_id = role_id
|
||||||
self.email = email
|
self.email = email
|
||||||
self.otp_secret = otp_secret
|
self.otp_secret = otp_secret
|
||||||
|
self.confirmed = confirmed
|
||||||
|
|
||||||
if reload_info:
|
if reload_info:
|
||||||
user_info = self.get_user_info_by_id(
|
user_info = self.get_user_info_by_id(
|
||||||
@ -61,6 +64,7 @@ class User(db.Model):
|
|||||||
self.email = user_info.email
|
self.email = user_info.email
|
||||||
self.role_id = user_info.role_id
|
self.role_id = user_info.role_id
|
||||||
self.otp_secret = user_info.otp_secret
|
self.otp_secret = user_info.otp_secret
|
||||||
|
self.confirmed = user_info.confirmed
|
||||||
|
|
||||||
def is_authenticated(self):
|
def is_authenticated(self):
|
||||||
return True
|
return True
|
||||||
@ -504,11 +508,25 @@ class User(db.Model):
|
|||||||
|
|
||||||
user.firstname = self.firstname if self.firstname else user.firstname
|
user.firstname = self.firstname if self.firstname else user.firstname
|
||||||
user.lastname = self.lastname if self.lastname else user.lastname
|
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(
|
user.password = self.get_hashed_password(
|
||||||
self.plain_text_password).decode(
|
self.plain_text_password).decode(
|
||||||
"utf-8") if self.plain_text_password else user.password
|
"utf-8") if self.plain_text_password else user.password
|
||||||
|
|
||||||
|
if self.email:
|
||||||
|
# Can not update to a new email that
|
||||||
|
# already been used.
|
||||||
|
existing_email = User.query.filter(
|
||||||
|
User.email == self.email,
|
||||||
|
User.username != self.username).first()
|
||||||
|
if existing_email:
|
||||||
|
return False
|
||||||
|
# If need to verify new email,
|
||||||
|
# update the "confirmed" status.
|
||||||
|
if user.email != self.email:
|
||||||
|
user.email = self.email
|
||||||
|
if Setting().get('verify_user_email'):
|
||||||
|
user.confirmed = 0
|
||||||
|
|
||||||
if enable_otp is not None:
|
if enable_otp is not None:
|
||||||
user.otp_secret = ""
|
user.otp_secret = ""
|
||||||
|
|
||||||
@ -524,6 +542,13 @@ class User(db.Model):
|
|||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update_confirmed(self, confirmed):
|
||||||
|
"""
|
||||||
|
Update user email confirmation status
|
||||||
|
"""
|
||||||
|
self.confirmed = confirmed
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
def get_domains(self):
|
def get_domains(self):
|
||||||
"""
|
"""
|
||||||
Get list of domains which the user is granted to have
|
Get list of domains which the user is granted to have
|
||||||
|
@ -501,7 +501,7 @@ def setting_basic():
|
|||||||
'pretty_ipv6_ptr', 'dnssec_admins_only',
|
'pretty_ipv6_ptr', 'dnssec_admins_only',
|
||||||
'allow_user_create_domain', 'bg_domain_updates', 'site_name',
|
'allow_user_create_domain', 'bg_domain_updates', 'site_name',
|
||||||
'session_timeout', 'warn_session_timeout', 'ttl_options',
|
'session_timeout', 'warn_session_timeout', 'ttl_options',
|
||||||
'pdns_api_timeout'
|
'pdns_api_timeout', 'verify_user_email'
|
||||||
]
|
]
|
||||||
|
|
||||||
return render_template('admin_setting_basic.html', settings=settings)
|
return render_template('admin_setting_basic.html', settings=settings)
|
||||||
|
@ -29,6 +29,8 @@ from ..services.github import github_oauth
|
|||||||
from ..services.azure import azure_oauth
|
from ..services.azure import azure_oauth
|
||||||
from ..services.oidc import oidc_oauth
|
from ..services.oidc import oidc_oauth
|
||||||
from ..services.saml import SAML
|
from ..services.saml import SAML
|
||||||
|
from ..services.token import confirm_token
|
||||||
|
from ..services.email import send_account_verification
|
||||||
|
|
||||||
google = None
|
google = None
|
||||||
github = None
|
github = None
|
||||||
@ -280,6 +282,12 @@ def login():
|
|||||||
plain_text_password=password)
|
plain_text_password=password)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if Setting().get('verify_user_email') and user.email and not user.confirmed:
|
||||||
|
return render_template(
|
||||||
|
'login.html',
|
||||||
|
saml_enabled=SAML_ENABLED,
|
||||||
|
error='Please confirm your email address first')
|
||||||
|
|
||||||
auth = user.is_validate(method=auth_method,
|
auth = user.is_validate(method=auth_method,
|
||||||
src_ip=request.remote_addr)
|
src_ip=request.remote_addr)
|
||||||
if auth == False:
|
if auth == False:
|
||||||
@ -411,6 +419,8 @@ def register():
|
|||||||
try:
|
try:
|
||||||
result = user.create_local_user()
|
result = user.create_local_user()
|
||||||
if result and result['status']:
|
if result and result['status']:
|
||||||
|
if Setting().get('verify_user_email'):
|
||||||
|
send_account_verification(email)
|
||||||
return redirect(url_for('index.login'))
|
return redirect(url_for('index.login'))
|
||||||
else:
|
else:
|
||||||
return render_template('register.html',
|
return render_template('register.html',
|
||||||
@ -421,6 +431,50 @@ def register():
|
|||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
@index_bp.route('/confirm/<token>', methods=['GET'])
|
||||||
|
def confirm_email(token):
|
||||||
|
email = confirm_token(token)
|
||||||
|
if not email:
|
||||||
|
# Cannot confirm email
|
||||||
|
return render_template('email_confirmation.html', status=0)
|
||||||
|
|
||||||
|
user = User.query.filter_by(email=email).first_or_404()
|
||||||
|
if user.confirmed:
|
||||||
|
# Already confirmed
|
||||||
|
current_app.logger.info(
|
||||||
|
"User email {} already confirmed".format(email))
|
||||||
|
return render_template('email_confirmation.html', status=2)
|
||||||
|
else:
|
||||||
|
# Confirm email is valid
|
||||||
|
user.update_confirmed(confirmed=1)
|
||||||
|
current_app.logger.info(
|
||||||
|
"User email {} confirmed successfully".format(email))
|
||||||
|
return render_template('email_confirmation.html', status=1)
|
||||||
|
|
||||||
|
|
||||||
|
@index_bp.route('/resend-confirmation-email', methods=['GET', 'POST'])
|
||||||
|
def resend_confirmation_email():
|
||||||
|
if current_user.is_authenticated:
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('resend_confirmation_email.html')
|
||||||
|
elif request.method == 'POST':
|
||||||
|
email = request.form.get('email')
|
||||||
|
user = User.query.filter(User.email == email).first()
|
||||||
|
if not user:
|
||||||
|
# Email not found
|
||||||
|
status = 0
|
||||||
|
elif user.confirmed:
|
||||||
|
# Email already confirmed
|
||||||
|
status = 1
|
||||||
|
else:
|
||||||
|
# Send new confirmed email
|
||||||
|
send_account_verification(user.email)
|
||||||
|
status = 2
|
||||||
|
|
||||||
|
return render_template('resend_confirmation_email.html', status=status)
|
||||||
|
|
||||||
|
|
||||||
@index_bp.route('/nic/checkip.html', methods=['GET', 'POST'])
|
@index_bp.route('/nic/checkip.html', methods=['GET', 'POST'])
|
||||||
def dyndns_checkip():
|
def dyndns_checkip():
|
||||||
# This route covers the default ddclient 'web' setting for the checkip service
|
# This route covers the default ddclient 'web' setting for the checkip service
|
||||||
|
27
powerdnsadmin/services/email.py
Normal file
27
powerdnsadmin/services/email.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import traceback
|
||||||
|
from flask_mail import Message
|
||||||
|
from flask import current_app, render_template, url_for
|
||||||
|
|
||||||
|
from .token import generate_confirmation_token
|
||||||
|
from ..models.setting import Setting
|
||||||
|
|
||||||
|
|
||||||
|
def send_account_verification(user_email):
|
||||||
|
"""
|
||||||
|
Send welcome message for the new registration
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
token = generate_confirmation_token(user_email)
|
||||||
|
verification_link = url_for('index.confirm_email', token=token, _external=True)
|
||||||
|
|
||||||
|
subject = "Welcome to {}".format(Setting().get('site_name'))
|
||||||
|
msg = Message(subject=subject)
|
||||||
|
msg.recipients = [user_email]
|
||||||
|
msg.body = "Please access the following link verify your email address. {}".format(
|
||||||
|
verification_link)
|
||||||
|
msg.html = render_template('emails/account_verification.html',
|
||||||
|
verification_link=verification_link)
|
||||||
|
current_app.mail.send(msg)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error("Cannot send account verification email. Error: {}".format(e))
|
||||||
|
current_app.logger.debug(traceback.format_exc())
|
19
powerdnsadmin/services/token.py
Normal file
19
powerdnsadmin/services/token.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from flask import current_app
|
||||||
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
|
|
||||||
|
|
||||||
|
def generate_confirmation_token(email):
|
||||||
|
serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
|
||||||
|
return serializer.dumps(email, salt=current_app.config['SALT'])
|
||||||
|
|
||||||
|
|
||||||
|
def confirm_token(token, expiration=86400):
|
||||||
|
serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
|
||||||
|
try:
|
||||||
|
email = serializer.loads(token,
|
||||||
|
salt=current_app.config['SALT'],
|
||||||
|
max_age=expiration)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.debug(e)
|
||||||
|
return False
|
||||||
|
return email
|
@ -177,7 +177,7 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% assets "js_main" -%}
|
{% assets "js_main" -%}
|
||||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
{% if SETTING.get('warn_session_timeout') %}
|
{% if SETTING.get('warn_session_timeout') and current_user.is_authenticated %}
|
||||||
<script>
|
<script>
|
||||||
// close the session warning popup when receive
|
// close the session warning popup when receive
|
||||||
// a boradcast message
|
// a boradcast message
|
||||||
|
40
powerdnsadmin/templates/email_confirmation.html
Normal file
40
powerdnsadmin/templates/email_confirmation.html
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}<title>Email verification - {{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_stat %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-content">
|
||||||
|
{% if status == 1 %}
|
||||||
|
<h3>
|
||||||
|
<i class="fa fa-thumbs-o-up text-success"></i> Email verification successful!
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
You have confirmed your account. <a href="{{ url_for('index.login') }}">Click here</a> to login.
|
||||||
|
</p>
|
||||||
|
{% elif status == 2 %}
|
||||||
|
<h3>
|
||||||
|
<i class="fa fa-hand-stop-o text-info"></i> Already verified!
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
You have confirmed your account already. <a href="{{ url_for('index.login') }}">Click here</a> to login.
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<h3>
|
||||||
|
<i class="fa fa-warning text-yellow"></i> Email verification failed!
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
The confirmation link is invalid or has expired. <a href="{{ url_for('index.resend_confirmation_email') }}">Click here</a> if you want to resend a new link.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<!-- /.error-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.error-page -->
|
||||||
|
</section>
|
||||||
|
<!-- /.content -->
|
||||||
|
{% endblock %}
|
220
powerdnsadmin/templates/emails/account_verification.html
Normal file
220
powerdnsadmin/templates/emails/account_verification.html
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>{{ SITE_NAME }} user verification email</title>
|
||||||
|
<style>
|
||||||
|
@media only screen and (max-width: 620px) {
|
||||||
|
table[class=body] h1 {
|
||||||
|
font-size: 28px !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] p,
|
||||||
|
table[class=body] ul,
|
||||||
|
table[class=body] ol,
|
||||||
|
table[class=body] td,
|
||||||
|
table[class=body] span,
|
||||||
|
table[class=body] a {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .wrapper,
|
||||||
|
table[class=body] .article {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .content {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .container {
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .main {
|
||||||
|
border-left-width: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
border-right-width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn table {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .btn a {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table[class=body] .img-responsive {
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all {
|
||||||
|
.ExternalClass {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExternalClass,
|
||||||
|
.ExternalClass p,
|
||||||
|
.ExternalClass span,
|
||||||
|
.ExternalClass font,
|
||||||
|
.ExternalClass td,
|
||||||
|
.ExternalClass div {
|
||||||
|
line-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apple-link a {
|
||||||
|
color: inherit !important;
|
||||||
|
font-family: inherit !important;
|
||||||
|
font-size: inherit !important;
|
||||||
|
font-weight: inherit !important;
|
||||||
|
line-height: inherit !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#MessageViewBody a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary table td:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary a:hover {
|
||||||
|
background-color: #34495e !important;
|
||||||
|
border-color: #34495e !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class=""
|
||||||
|
style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||||
|
<span class="preheader"
|
||||||
|
style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">This
|
||||||
|
is preheader text. Some clients will show this text as a preview.</span>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background-color: #f6f6f6; width: 100%;"
|
||||||
|
width="100%" bgcolor="#f6f6f6">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td>
|
||||||
|
<td class="container"
|
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; max-width: 580px; padding: 10px; width: 580px; margin: 0 auto;"
|
||||||
|
width="580" valign="top">
|
||||||
|
<div class="content"
|
||||||
|
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||||
|
|
||||||
|
<!-- START CENTERED WHITE CONTAINER -->
|
||||||
|
<table role="presentation" class="main"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; background: #ffffff; border-radius: 3px; width: 100%;"
|
||||||
|
width="100%">
|
||||||
|
|
||||||
|
<!-- START MAIN CONTENT AREA -->
|
||||||
|
<tr>
|
||||||
|
<td class="wrapper"
|
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;"
|
||||||
|
valign="top">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
|
||||||
|
width="100%">
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"
|
||||||
|
valign="top">
|
||||||
|
<p
|
||||||
|
style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
|
||||||
|
Hello!</p>
|
||||||
|
<p
|
||||||
|
style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
|
||||||
|
Please click the button below to verify your email address.</p>
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
|
||||||
|
class="btn btn-primary"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; box-sizing: border-box; width: 100%;"
|
||||||
|
width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left"
|
||||||
|
style="font-family: sans-serif; font-size: 14px; vertical-align: top; padding-bottom: 15px;"
|
||||||
|
valign="top">
|
||||||
|
<table role="presentation" border="0" cellpadding="0"
|
||||||
|
cellspacing="0"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top; border-radius: 5px; text-align: center; background-color: #3498db;"
|
||||||
|
valign="top" align="center"
|
||||||
|
bgcolor="#3498db"> <a
|
||||||
|
href="{{ verification_link }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
style="border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; background-color: #3498db; border-color: #3498db; color: #ffffff;">Verify
|
||||||
|
Email Address</a> </td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p
|
||||||
|
style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
|
||||||
|
Your confirmation link will be expired in 24 hours. If you did not create an account, no further action is required.</p>
|
||||||
|
<p
|
||||||
|
style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px;">
|
||||||
|
Cheers!</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- END MAIN CONTENT AREA -->
|
||||||
|
</table>
|
||||||
|
<!-- END CENTERED WHITE CONTAINER -->
|
||||||
|
|
||||||
|
<!-- START FOOTER -->
|
||||||
|
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%;">
|
||||||
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
|
||||||
|
style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;"
|
||||||
|
width="100%">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block"
|
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;"
|
||||||
|
valign="top" align="center">
|
||||||
|
<span class="apple-link"
|
||||||
|
style="color: #999999; font-size: 12px; text-align: center;">This email was sent
|
||||||
|
from <a href="{{ SETTING.get('site_url') }}"
|
||||||
|
target="_blank" rel="noopener noreferrer">{{ SITE_NAME }}</a></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="content-block powered-by"
|
||||||
|
style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; color: #999999; font-size: 12px; text-align: center;"
|
||||||
|
valign="top" align="center">
|
||||||
|
Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin"
|
||||||
|
style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">PowerDNS-Admin</a>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- END FOOTER -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;" valign="top"> </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -120,6 +120,10 @@
|
|||||||
{% if SETTING.get('signup_enabled') %}
|
{% if SETTING.get('signup_enabled') %}
|
||||||
<br>
|
<br>
|
||||||
<a href="{{ url_for('index.register') }}" class="text-center">Create an account </a>
|
<a href="{{ url_for('index.register') }}" class="text-center">Create an account </a>
|
||||||
|
{% if SETTING.get('verify_user_email') %}
|
||||||
|
<br/>
|
||||||
|
<a href="{{ url_for('index.resend_confirmation_email') }}" class="text-center">Resend confirmation email</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<!-- /.login-box-body -->
|
<!-- /.login-box-body -->
|
||||||
|
@ -48,8 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group has-feedback">
|
<div class="form-group has-feedback">
|
||||||
<input type="email" class="form-control" placeholder="Email" name="email"
|
<input type="email" class="form-control" placeholder="Email" name="email"
|
||||||
data-error="Please input your valid email address"
|
data-error="Please input your valid email address" required>
|
||||||
pattern="^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$" required>
|
|
||||||
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
|
44
powerdnsadmin/templates/resend_confirmation_email.html
Normal file
44
powerdnsadmin/templates/resend_confirmation_email.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}<title>Resend confirmation email - {{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_stat %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="error-page">
|
||||||
|
<div class="error-content">
|
||||||
|
<h3>
|
||||||
|
<i class="fa fa-hand-o-right text-info"></i> Resend a confirmation email
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Enter your email address to get new account confirmation link.
|
||||||
|
</p>
|
||||||
|
<form class="search-form" method="post">
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="text" name="email" class="form-control" placeholder="Email address" data-error="Please input your email" required>
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button type="submit" name="submit" class="btn btn-success btn-flat"><i class="fa fa-mail-reply"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.input-group -->
|
||||||
|
<p>
|
||||||
|
{% if status == 0 %}
|
||||||
|
<font color="red">Email not found!</font>
|
||||||
|
{% elif status == 1 %}
|
||||||
|
<font color="red">Email already confirmed!</font>
|
||||||
|
{% elif status == 2 %}
|
||||||
|
<font color="green">Confirmation email sent!</font>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- /.error-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.error-page -->
|
||||||
|
</section>
|
||||||
|
<!-- /.content -->
|
||||||
|
{% endblock %}
|
@ -26,3 +26,4 @@ pytest==5.0.1
|
|||||||
pytimeparse==1.1.8
|
pytimeparse==1.1.8
|
||||||
PyYAML==5.1.1
|
PyYAML==5.1.1
|
||||||
Flask-SSLify==0.1.5
|
Flask-SSLify==0.1.5
|
||||||
|
Flask-Mail==0.9.1
|
Loading…
Reference in New Issue
Block a user