mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-28 14:05:41 +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
|
||||
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_ENABLED = False
|
||||
# SAML_DEBUG = True
|
||||
@ -47,20 +57,20 @@ SAML_ENABLED = False
|
||||
# Following parameter defines RequestedAttributes section in SAML metadata
|
||||
# since certain iDPs require explicit attribute request. If not provided section
|
||||
# will not be available in metadata.
|
||||
#
|
||||
#
|
||||
# Possible attributes:
|
||||
# name (mandatory), nameFormat, isRequired, friendlyName
|
||||
#
|
||||
#
|
||||
# NOTE: This parameter requires to be entered in valid JSON format as displayed below
|
||||
# and multiple attributes can given
|
||||
#
|
||||
#
|
||||
# Following example:
|
||||
#
|
||||
#
|
||||
# SAML_SP_REQUESTED_ATTRIBUTES = '[ \
|
||||
# {"name": "urn:oid:0.9.2342.19200300.100.1.3", "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", "isRequired": true, "friendlyName": "email"}, \
|
||||
# {"name": "mail", "isRequired": false, "friendlyName": "test-field"} \
|
||||
# ]'
|
||||
#
|
||||
# ]'
|
||||
#
|
||||
# produces following metadata section:
|
||||
# <md:AttributeConsumingService index="1">
|
||||
# <md:RequestedAttribute Name="urn:oid:0.9.2342.19200300.100.1.3" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="email" isRequired="true"/>
|
||||
@ -108,13 +118,13 @@ SAML_ENABLED = False
|
||||
# SAML_SP_CONTACT_MAIL = '<contact mail>'
|
||||
|
||||
# Configures the path to certificate file and it's respective private key file
|
||||
# This pair is used for signing metadata, encrypting tokens and all other signing/encryption
|
||||
# This pair is used for signing metadata, encrypting tokens and all other signing/encryption
|
||||
# tasks during communication between iDP and SP
|
||||
# NOTE: if this two parameters aren't explicitly provided, self-signed certificate-key pair
|
||||
# NOTE: if this two parameters aren't explicitly provided, self-signed certificate-key pair
|
||||
# will be generated in "PowerDNS-Admin" root directory
|
||||
# ###########################################################################################
|
||||
# CAUTION: For production use, usage of self-signed certificates it's highly discouraged.
|
||||
# Use certificates from trusted CA instead
|
||||
# Use certificates from trusted CA instead
|
||||
# ###########################################################################################
|
||||
# SAML_CERT_FILE = '/etc/pki/powerdns-admin/cert.crt'
|
||||
# SAML_CERT_KEY = '/etc/pki/powerdns-admin/key.pem'
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Defaults for Docker image
|
||||
BIND_ADDRESS='0.0.0.0'
|
||||
PORT=80
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 80
|
||||
|
||||
legal_envvars = (
|
||||
'SECRET_KEY',
|
||||
@ -10,6 +10,14 @@ legal_envvars = (
|
||||
'SALT',
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||
'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_DEBUG',
|
||||
'SAML_PATH',
|
||||
@ -37,14 +45,14 @@ legal_envvars = (
|
||||
'SAML_LOGOUT_URL',
|
||||
)
|
||||
|
||||
legal_envvars_int = (
|
||||
'PORT',
|
||||
'SAML_METADATA_CACHE_LIFETIME',
|
||||
)
|
||||
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
|
||||
|
||||
legal_envvars_bool = (
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||
'HSTS_ENABLED',
|
||||
'MAIL_DEBUG',
|
||||
'MAIL_USE_TLS',
|
||||
'MAIL_USE_SSL',
|
||||
'SAML_ENABLED',
|
||||
'SAML_DEBUG',
|
||||
'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
|
||||
from flask import Flask
|
||||
from flask_seasurf import SeaSurf
|
||||
from flask_mail import Mail
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
from .lib import utils
|
||||
@ -64,6 +65,9 @@ def create_app(config=None):
|
||||
from flask_sslify import SSLify
|
||||
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
|
||||
|
||||
# SMTP
|
||||
app.mail = Mail(app)
|
||||
|
||||
# Load app's components
|
||||
assets.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_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 DomainAccessForbidden
|
||||
|
||||
@ -121,6 +121,12 @@ def api_basic_auth(f):
|
||||
plain_text_password=password)
|
||||
|
||||
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 = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
||||
auth = user.is_validate(method=auth_method,
|
||||
|
@ -25,6 +25,7 @@ class Setting(db.Model):
|
||||
'allow_user_create_domain': False,
|
||||
'bg_domain_updates': False,
|
||||
'site_name': 'PowerDNS-Admin',
|
||||
'site_url': 'http://localhost:9191',
|
||||
'session_timeout': 10,
|
||||
'warn_session_timeout': True,
|
||||
'pdns_api_url': '',
|
||||
@ -33,6 +34,7 @@ class Setting(db.Model):
|
||||
'pdns_version': '4.1.1',
|
||||
'local_db_enabled': True,
|
||||
'signup_enabled': True,
|
||||
'verify_user_email': False,
|
||||
'ldap_enabled': False,
|
||||
'ldap_type': 'ldap',
|
||||
'ldap_uri': '',
|
||||
|
@ -26,6 +26,7 @@ class User(db.Model):
|
||||
lastname = db.Column(db.String(64))
|
||||
email = db.Column(db.String(128))
|
||||
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'))
|
||||
|
||||
def __init__(self,
|
||||
@ -38,6 +39,7 @@ class User(db.Model):
|
||||
role_id=None,
|
||||
email=None,
|
||||
otp_secret=None,
|
||||
confirmed=False,
|
||||
reload_info=True):
|
||||
self.id = id
|
||||
self.username = username
|
||||
@ -48,6 +50,7 @@ class User(db.Model):
|
||||
self.role_id = role_id
|
||||
self.email = email
|
||||
self.otp_secret = otp_secret
|
||||
self.confirmed = confirmed
|
||||
|
||||
if reload_info:
|
||||
user_info = self.get_user_info_by_id(
|
||||
@ -61,6 +64,7 @@ class User(db.Model):
|
||||
self.email = user_info.email
|
||||
self.role_id = user_info.role_id
|
||||
self.otp_secret = user_info.otp_secret
|
||||
self.confirmed = user_info.confirmed
|
||||
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
@ -504,11 +508,25 @@ class User(db.Model):
|
||||
|
||||
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 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:
|
||||
user.otp_secret = ""
|
||||
|
||||
@ -524,6 +542,13 @@ class User(db.Model):
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def update_confirmed(self, confirmed):
|
||||
"""
|
||||
Update user email confirmation status
|
||||
"""
|
||||
self.confirmed = confirmed
|
||||
db.session.commit()
|
||||
|
||||
def get_domains(self):
|
||||
"""
|
||||
Get list of domains which the user is granted to have
|
||||
|
@ -501,7 +501,7 @@ def setting_basic():
|
||||
'pretty_ipv6_ptr', 'dnssec_admins_only',
|
||||
'allow_user_create_domain', 'bg_domain_updates', 'site_name',
|
||||
'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)
|
||||
|
@ -29,6 +29,8 @@ from ..services.github import github_oauth
|
||||
from ..services.azure import azure_oauth
|
||||
from ..services.oidc import oidc_oauth
|
||||
from ..services.saml import SAML
|
||||
from ..services.token import confirm_token
|
||||
from ..services.email import send_account_verification
|
||||
|
||||
google = None
|
||||
github = None
|
||||
@ -280,6 +282,12 @@ def login():
|
||||
plain_text_password=password)
|
||||
|
||||
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,
|
||||
src_ip=request.remote_addr)
|
||||
if auth == False:
|
||||
@ -411,6 +419,8 @@ def register():
|
||||
try:
|
||||
result = user.create_local_user()
|
||||
if result and result['status']:
|
||||
if Setting().get('verify_user_email'):
|
||||
send_account_verification(email)
|
||||
return redirect(url_for('index.login'))
|
||||
else:
|
||||
return render_template('register.html',
|
||||
@ -421,6 +431,50 @@ def register():
|
||||
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'])
|
||||
def dyndns_checkip():
|
||||
# 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 %}
|
||||
{% assets "js_main" -%}
|
||||
<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>
|
||||
// close the session warning popup when receive
|
||||
// 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') %}
|
||||
<br>
|
||||
<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 %}
|
||||
</div>
|
||||
<!-- /.login-box-body -->
|
||||
|
@ -48,8 +48,7 @@
|
||||
</div>
|
||||
<div class="form-group has-feedback">
|
||||
<input type="email" class="form-control" placeholder="Email" name="email"
|
||||
data-error="Please input your valid email address"
|
||||
pattern="^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$" required>
|
||||
data-error="Please input your valid email address" required>
|
||||
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||
<span class="help-block with-errors"></span>
|
||||
</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
|
||||
PyYAML==5.1.1
|
||||
Flask-SSLify==0.1.5
|
||||
Flask-Mail==0.9.1
|
Loading…
Reference in New Issue
Block a user