diff --git a/dyxless/__init__.py b/dyxless/__init__.py index 09bb763..b99ac1f 100644 --- a/dyxless/__init__.py +++ b/dyxless/__init__.py @@ -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) diff --git a/dyxless/auth.py b/dyxless/auth.py index 3f9cccd..d8cf609 100644 --- a/dyxless/auth.py +++ b/dyxless/auth.py @@ -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/") +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() diff --git a/dyxless/config.json_template b/dyxless/config.json_template index 32f7a4f..375bcb7 100644 --- a/dyxless/config.json_template +++ b/dyxless/config.json_template @@ -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" diff --git a/dyxless/mails.py b/dyxless/mails.py index fc9bdbc..69f92dd 100644 --- a/dyxless/mails.py +++ b/dyxless/mails.py @@ -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) diff --git a/dyxless/models.py b/dyxless/models.py index abf4188..3a3232c 100644 --- a/dyxless/models.py +++ b/dyxless/models.py @@ -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 diff --git a/dyxless/templates/base.html b/dyxless/templates/base.html index 2931f86..6767d83 100644 --- a/dyxless/templates/base.html +++ b/dyxless/templates/base.html @@ -19,19 +19,19 @@