mirror of
https://github.com/Llloooggg/Dyxless.git
synced 2026-03-06 02:36:24 +03:00
Добавлена двухфакторная регистрация через email
This commit is contained in:
@@ -5,16 +5,13 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
def create_app():
|
|
||||||
app = Flask("__name__", template_folder="dyxless/templates")
|
|
||||||
|
|
||||||
main_settings = config_data["main_settings"]
|
main_settings = config_data["main_settings"]
|
||||||
app.config.update(main_settings)
|
app.config.update(main_settings)
|
||||||
|
|
||||||
@@ -33,10 +30,12 @@ def create_app():
|
|||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(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)
|
||||||
@@ -48,5 +47,3 @@ def create_app():
|
|||||||
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
|
|
||||||
|
|||||||
@@ -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,24 +65,79 @@ 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"))
|
||||||
|
|
||||||
|
|
||||||
|
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"))
|
return redirect(url_for("auth.login"))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
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)
|
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
3
dyxless/templates/mail/confirmation_mail.html
Normal file
3
dyxless/templates/mail/confirmation_mail.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<p>Перейдите по ссылке для подтверждения регистрации</p>
|
||||||
|
<p><a href="{{ confirm_url }}">{{ confirm_url }}</a></p>
|
||||||
|
<br>
|
||||||
@@ -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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user