Добавлена двухфакторная регистрация через 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_login import LoginManager
app = Flask("__name__", template_folder="dyxless/templates")
db = SQLAlchemy()
mail = Mail()
with open("dyxless/config.json") as config_file:
config_data = json.load(config_file)
main_settings = config_data["main_settings"]
app.config.update(main_settings)
def create_app():
app = Flask("__name__", template_folder="dyxless/templates")
db_settings = config_data["db_settings"]
app.config.update(db_settings)
main_settings = config_data["main_settings"]
app.config.update(main_settings)
mail_settings = config_data["mail_settings"]
app.config.update(mail_settings)
db_settings = config_data["db_settings"]
app.config.update(db_settings)
db.init_app(app)
mail.init_app(app)
mail_settings = config_data["mail_settings"]
app.config.update(mail_settings)
login_manager = LoginManager()
login_manager.login_view = "auth.login"
login_manager.init_app(app)
db.init_app(app)
mail.init_app(app)
from .models import User
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)
return app
app.register_blueprint(mails_blueprint)

View File

@@ -1,9 +1,21 @@
from flask import Blueprint, render_template, redirect, url_for, request, flash
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import current_user, login_user, logout_user
import datetime
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 .models import User
from .mails import send_async_email
auth = Blueprint("auth", __name__)
@@ -25,10 +37,20 @@ def login():
user = User.query.filter_by(email=email).first()
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"))
login_user(user, remember=remember)
user.last_login = datetime.datetime.now()
db.session.commit()
return redirect(url_for("main.profile"))
@@ -43,27 +65,82 @@ def signup():
elif request.method == "POST":
email = request.form.get("email")
name = request.form.get("name")
username = request.form.get("username")
password = request.form.get("password")
user = User.query.filter_by(email=email).first()
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"))
new_user = User(
email=email,
name=name,
password=generate_password_hash(password, method="sha256"),
password=password,
username=username,
)
db.session.add(new_user)
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"))
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")
def logout():
logout_user()

View File

@@ -1,7 +1,9 @@
{
"main_settings": {
"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": {
"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 . import mail
from . import app, mail
from .decorators import async_work
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.body = text_body
msg.html = html_body
msg.body = body
msg.html = html
return msg
@async_work
def send_async_email(subject, sender, recipients, text_body, html_body):
msg = prepare_msg(subject, sender, recipients, text_body, html_body)
mail.send(msg)
def send_async_email(
subject,
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):
msg = prepare_msg(subject, sender, recipients, text_body, html_body)
def send_mail(
subject,
recipients,
body=None,
html=None,
sender=app.config["APP_EMAIL"],
):
msg = prepare_msg(subject, recipients, body, html, sender)
mail.send(msg)

View File

@@ -1,10 +1,35 @@
import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash
from . import db
class User(UserMixin, db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(100), unique=True)
password = db.Column(db.String(100))
name = db.Column(db.String(1000))
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
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 class="navbar-end">
<a href="{{ url_for('main.index') }}" class="navbar-item">
Home
Домой
</a>
{% if not current_user.is_authenticated %}
<a href="{{ url_for('auth.login') }}" class="navbar-item">
Login
Вход
</a>
<a href="{{ url_for('auth.signup') }}" class="navbar-item">
Sign Up
Регистрация
</a>
{% endif %}
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.logout') }}" class="navbar-item">
Logout
Выйти
</a>
{% endif %}
{% if current_user.is_authenticated %}

View File

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

View File

@@ -2,7 +2,7 @@
{% block content %}
<div class="column is-4 is-offset-4">
<h3 class="title">Login</h3>
<h3 class="title">Вход</h3>
<div class="box">
{% with messages = get_flashed_messages() %}
{% if messages %}
@@ -14,22 +14,22 @@
<form method="POST" action="/login">
<div class="field">
<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 class="field">
<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 class="field">
<label class="checkbox">
<input type="checkbox">
Remember me
Запомнить меня
</label>
</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>
</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 %}
<h1 class="title">
Welcome, {{ current_user.name }}!
Добро пожаловать, {{ current_user.username }}!
</h1>
{% endblock %}

View File

@@ -2,12 +2,12 @@
{% block content %}
<div class="column is-4 is-offset-4">
<h3 class="title">Sign Up</h3>
<h3 class="title">Регистрация</h3>
<div class="box">
{% with messages = get_flashed_messages() %}
{% if messages %}
<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>
{% endif %}
{% endwith %}
@@ -20,17 +20,17 @@
<div class="field">
<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 class="field">
<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>
<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>
</div>
</div>

View File

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