Добавлена двухфакторная регистрация через email

This commit is contained in:
2021-07-14 16:55:06 +03:00
parent 729012ecca
commit 4cad60b47f
12 changed files with 188 additions and 65 deletions

View File

@@ -5,48 +5,45 @@ from flask_mail import Mail
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager from flask_login import LoginManager
app = Flask("__name__", template_folder="dyxless/templates")
db = SQLAlchemy() db = SQLAlchemy()
mail = Mail() mail = Mail()
with open("dyxless/config.json") as config_file: with open("dyxless/config.json") as config_file:
config_data = json.load(config_file) config_data = json.load(config_file)
main_settings = config_data["main_settings"]
app.config.update(main_settings)
def create_app(): db_settings = config_data["db_settings"]
app = Flask("__name__", template_folder="dyxless/templates") app.config.update(db_settings)
main_settings = config_data["main_settings"] mail_settings = config_data["mail_settings"]
app.config.update(main_settings) app.config.update(mail_settings)
db_settings = config_data["db_settings"] db.init_app(app)
app.config.update(db_settings) mail.init_app(app)
mail_settings = config_data["mail_settings"] login_manager = LoginManager()
app.config.update(mail_settings) login_manager.login_view = "auth.login"
login_manager.init_app(app)
db.init_app(app) from .models import User
mail.init_app(app)
login_manager = LoginManager()
login_manager.login_view = "auth.login"
login_manager.init_app(app)
from .models import User @login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
from .auth import auth as auth_blueprint from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint) app.register_blueprint(auth_blueprint)
from .main import main as main_blueprint from .main import main as main_blueprint
app.register_blueprint(main_blueprint) app.register_blueprint(main_blueprint)
from .mails import mails as mails_blueprint from .mails import mails as mails_blueprint
app.register_blueprint(mails_blueprint) app.register_blueprint(mails_blueprint)
return app

View File

@@ -1,9 +1,21 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import current_user, login_user, logout_user from flask import (
Blueprint,
render_template,
redirect,
url_for,
request,
flash,
current_app,
)
from werkzeug.security import check_password_hash
from flask_login import current_user, login_user, logout_user
from itsdangerous import URLSafeTimedSerializer
from .models import User
from . import db from . import db
from .models import User
from .mails import send_async_email
auth = Blueprint("auth", __name__) auth = Blueprint("auth", __name__)
@@ -25,10 +37,20 @@ def login():
user = User.query.filter_by(email=email).first() user = User.query.filter_by(email=email).first()
if not user or not check_password_hash(user.password, password): if not user or not check_password_hash(user.password, password):
flash("Please check your login details and try again.") flash("Пожалуйста, проверьте введенные данные и попробуйте снова")
return redirect(url_for("auth.login"))
elif not user.is_confirmed:
flash(
"Аккаунт еще не активирован. Пожалуйста, проверьте вашу почту"
)
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
login_user(user, remember=remember) login_user(user, remember=remember)
user.last_login = datetime.datetime.now()
db.session.commit()
return redirect(url_for("main.profile")) return redirect(url_for("main.profile"))
@@ -43,27 +65,82 @@ def signup():
elif request.method == "POST": elif request.method == "POST":
email = request.form.get("email") email = request.form.get("email")
name = request.form.get("name") username = request.form.get("username")
password = request.form.get("password") password = request.form.get("password")
user = User.query.filter_by(email=email).first() user = User.query.filter_by(email=email).first()
if user: if user:
flash("Email address already exists") flash("Указанная почта уже используется")
return redirect(url_for("auth.signup"))
user = User.query.filter_by(email=username).first()
if user:
flash("Указанное имя уже используется")
return redirect(url_for("auth.signup")) return redirect(url_for("auth.signup"))
new_user = User( new_user = User(
email=email, email=email,
name=name, password=password,
password=generate_password_hash(password, method="sha256"), username=username,
) )
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
token = generate_confirmation_token(new_user.email)
confirm_url = url_for(
"auth.confirm_email", token=token, _external=True
)
send_async_email(
subject="Подтверждение регистрации",
recipients=[new_user.email],
html=render_template(
"mail/confirmation_mail.html", confirm_url=confirm_url
),
)
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
def generate_confirmation_token(email):
serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
return serializer.dumps(
email, salt=current_app.config["SECURITY_PASSWORD_SALT"]
)
def confirm_token(token, expiration=3600):
serializer = URLSafeTimedSerializer(current_app.config["SECRET_KEY"])
try:
email = serializer.loads(
token,
salt=current_app.config["SECURITY_PASSWORD_SALT"],
max_age=expiration,
)
except:
return False
return email
@auth.route("/confirm/<token>")
def confirm_email(token):
try:
email = confirm_token(token)
except:
flash("Ссылка подтверждения невалидна или устарела", "danger")
user = User.query.filter_by(email=email).first_or_404()
if user.is_confirmed:
flash("Аккаунт уже подтвержден", "success")
else:
user.is_confirmed = True
db.session.commit()
flash("Ваш аккаунт подвтержден!", "success")
return redirect(url_for("auth.login"))
@auth.route("/logout") @auth.route("/logout")
def logout(): def logout():
logout_user() logout_user()

View File

@@ -1,7 +1,9 @@
{ {
"main_settings": { "main_settings": {
"SECRET_KEY": "some-very-secret-key", "SECRET_KEY": "some-very-secret-key",
"SQLALCHEMY_TRACK_MODIFICATIONS": false "SECURITY_PASSWORD_SALT": "second-some-very-secret-key",
"SQLALCHEMY_TRACK_MODIFICATIONS": false,
"APP_EMAIL": "appsender@gmail.com
}, },
"db_settings": { "db_settings": {
"SQLALCHEMY_DATABASE_URI": "sqlite:///db.sqlite" "SQLALCHEMY_DATABASE_URI": "sqlite:///db.sqlite"

View File

@@ -1,25 +1,38 @@
from flask import Blueprint from flask import Blueprint, render_template
from flask_mail import Message from flask_mail import Message
from . import mail from . import app, mail
from .decorators import async_work from .decorators import async_work
mails = Blueprint("mails", __name__) mails = Blueprint("mails", __name__)
def prepare_msg(subject, sender, recipients, text_body, html_body): def prepare_msg(subject, recipients, body, html, sender):
msg = Message(subject, sender=sender, recipients=recipients) msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body msg.body = body
msg.html = html_body msg.html = html
return msg return msg
@async_work @async_work
def send_async_email(subject, sender, recipients, text_body, html_body): def send_async_email(
msg = prepare_msg(subject, sender, recipients, text_body, html_body) subject,
mail.send(msg) recipients,
body=None,
html=None,
sender=app.config["APP_EMAIL"],
):
msg = prepare_msg(subject, recipients, body, html, sender)
with app.app_context():
mail.send(msg)
def send_mail(subject, sender, recipients, text_body, html_body): def send_mail(
msg = prepare_msg(subject, sender, recipients, text_body, html_body) subject,
recipients,
body=None,
html=None,
sender=app.config["APP_EMAIL"],
):
msg = prepare_msg(subject, recipients, body, html, sender)
mail.send(msg) mail.send(msg)

View File

@@ -1,10 +1,35 @@
import datetime
from flask_login import UserMixin from flask_login import UserMixin
from werkzeug.security import generate_password_hash
from . import db from . import db
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), unique=True) email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String(100)) password = db.Column(db.String, nullable=False)
name = db.Column(db.String(1000)) username = db.Column(db.String, unique=True, nullable=False)
registered_on = db.Column(db.DateTime, nullable=False)
last_login = db.Column(db.DateTime, nullable=True)
is_confirmed = db.Column(db.Boolean, nullable=False, default=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
def __init__(
self,
email,
password,
username,
is_confirmed=False,
is_admin=False,
):
self.email = email
self.password = generate_password_hash(password, method="sha256")
self.username = username
self.registered_on = datetime.datetime.now()
self.is_confirmed = is_confirmed
self.is_admin = is_admin

View File

@@ -19,19 +19,19 @@
<div id="navbarMenuHeroA" class="navbar-menu"> <div id="navbarMenuHeroA" class="navbar-menu">
<div class="navbar-end"> <div class="navbar-end">
<a href="{{ url_for('main.index') }}" class="navbar-item"> <a href="{{ url_for('main.index') }}" class="navbar-item">
Home Домой
</a> </a>
{% if not current_user.is_authenticated %} {% if not current_user.is_authenticated %}
<a href="{{ url_for('auth.login') }}" class="navbar-item"> <a href="{{ url_for('auth.login') }}" class="navbar-item">
Login Вход
</a> </a>
<a href="{{ url_for('auth.signup') }}" class="navbar-item"> <a href="{{ url_for('auth.signup') }}" class="navbar-item">
Sign Up Регистрация
</a> </a>
{% endif %} {% endif %}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}
<a href="{{ url_for('auth.logout') }}" class="navbar-item"> <a href="{{ url_for('auth.logout') }}" class="navbar-item">
Logout Выйти
</a> </a>
{% endif %} {% endif %}
{% if current_user.is_authenticated %} {% if current_user.is_authenticated %}

View File

@@ -2,9 +2,9 @@
{% block content %} {% block content %}
<h1 class="title"> <h1 class="title">
Flask Login Example Dyxless
</h1> </h1>
<h2 class="subtitle"> <h2 class="subtitle">
Easy authentication and authorization in Flask. Заготовка web-приложения
</h2> </h2>
{% endblock %} {% endblock %}

View File

@@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="column is-4 is-offset-4"> <div class="column is-4 is-offset-4">
<h3 class="title">Login</h3> <h3 class="title">Вход</h3>
<div class="box"> <div class="box">
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
@@ -14,22 +14,22 @@
<form method="POST" action="/login"> <form method="POST" action="/login">
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input is-large" type="email" name="email" placeholder="Your Email" autofocus=""> <input class="input is-large" type="email" name="email" placeholder="Email" autofocus="">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input is-large" type="password" name="password" placeholder="Your Password"> <input class="input is-large" type="password" name="password" placeholder="Пароль">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="checkbox"> <label class="checkbox">
<input type="checkbox"> <input type="checkbox">
Remember me Запомнить меня
</label> </label>
</div> </div>
<button class="button is-block is-info is-large is-fullwidth">Login</button> <button class="button is-block is-info is-large is-fullwidth">Войти</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,3 @@
<p>Перейдите по ссылке для подтверждения регистрации</p>
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
<br>

View File

@@ -2,6 +2,6 @@
{% block content %} {% block content %}
<h1 class="title"> <h1 class="title">
Welcome, {{ current_user.name }}! Добро пожаловать, {{ current_user.username }}!
</h1> </h1>
{% endblock %} {% endblock %}

View File

@@ -2,12 +2,12 @@
{% block content %} {% block content %}
<div class="column is-4 is-offset-4"> <div class="column is-4 is-offset-4">
<h3 class="title">Sign Up</h3> <h3 class="title">Регистрация</h3>
<div class="box"> <div class="box">
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages() %}
{% if messages %} {% if messages %}
<div class="notification is-danger"> <div class="notification is-danger">
{{ messages[0] }}. Go to <a href="{{ url_for('auth.login') }}">login page</a>. {{ messages[0] }}<a href="{{ url_for('auth.login') }}">Перейти к странице входа</a>.
</div> </div>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
@@ -20,17 +20,17 @@
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input is-large" type="text" name="name" placeholder="Name" autofocus=""> <input class="input is-large" type="text" name="username" placeholder="Имя пользователя" autofocus="">
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<div class="control"> <div class="control">
<input class="input is-large" type="password" name="password" placeholder="Password"> <input class="input is-large" type="password" name="password" placeholder="Пароль">
</div> </div>
</div> </div>
<button class="button is-block is-info is-large is-fullwidth">Sign Up</button> <button class="button is-block is-info is-large is-fullwidth">Зарегистрироваться</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,10 @@
@echo off @echo off
if not exist "db.sqlite" (
python init_db.py
)
set FLASK_APP=dyxless set FLASK_APP=dyxless
set FLASK_ENV=development set FLASK_ENV=development