diff --git a/README.md b/README.md index 44a25f5..f85575b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * проект открыт для редактирования и доработки; * создан в 2020 году в рамках образовательной программы "Яндекс.Практикум"; -* Python 3.8 +* Python 3.8. ### Использованные ресурсы ### @@ -14,6 +14,5 @@ ### Задачи развития ### -1. С помощью cookies приложение должно запоминать последний набор пользователя и восстанавливать его при возвращении. -2. Предусмотреть авторизацию или иной механизм для редактирования хранящихся данных. -3. Теневая проверка ссылок и удаление некорректных и неработающих (без участия пользователя, в тени). +1. Создать механизм редактирования сведений в базе данных. +2. Теневая проверка ссылок и удаление некорректных и неработающих (без участия пользователя, в тени). diff --git a/app/__init__.py b/app/__init__.py index f727b42..6f65057 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,10 +1,12 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- from flask import Flask -from config import Config -from flask_sslify import SSLify -from flask_sqlalchemy import SQLAlchemy as SQLA +from flask_login import LoginManager from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy as SQLA +from flask_sslify import SSLify + +from config import Config # *** временно выключено *** # import logging @@ -21,6 +23,10 @@ db_lib = SQLA(application) migrate = Migrate(application, db_lib) +# Login +login_manager = LoginManager(application) +login_manager.login_view = 'login' + from app import routes diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..cb1c106 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,11 @@ +""" +Система авторизации пользователей. +""" + +from app import login_manager +from app.models import User + + +@login_manager.user_loader +def load_user(id): + return User.query.get(int(id)) diff --git a/app/dbpanel.py b/app/dbpanel.py index 41207f2..02c47f5 100644 --- a/app/dbpanel.py +++ b/app/dbpanel.py @@ -5,10 +5,11 @@ # https://www.tutorialspoint.com/sqlalchemy/sqlalchemy_orm_filter_operators.htm -from app import db_lib -from app.models import Content, Types, Category from sqlalchemy.exc import SQLAlchemyError + +from app import db_lib from app.exc import * +from app.models import Category, Content, Types def add_to_db(*, create_types: bool = True, create_category: bool = True, **kwargs): diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..9a0adf8 --- /dev/null +++ b/app/forms.py @@ -0,0 +1,43 @@ +""" +Формы приложения. +""" + +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField, validators +from wtforms.validators import DataRequired, EqualTo, ValidationError +from app.models import User + + +class LoginForm(FlaskForm): + """ + Форма авторизации. + """ + nickname = StringField('Никнейм', validators=[DataRequired()]) + password = PasswordField('Пароль', validators=[DataRequired()]) + remember_me = BooleanField('Запомнить меня') + submit = SubmitField('Войти') + + def validate_nickname(self, nickname): + user = User.query.filter_by(nickname=nickname.data).first() + if user and not user.active: + raise ValidationError('Учётная запись неактивна.') + + +class RegForm(FlaskForm): + """ + Форма регистрации пользователя. + """ + nickname = StringField('Никнейм', validators=[DataRequired()]) + password1 = PasswordField('Пароль', [ + validators.DataRequired(), + validators.EqualTo('password2', message='Пароли должны совпадать!') + ]) + password2 = PasswordField('Пароль повторно', [ + validators.DataRequired() + ]) + submit = SubmitField('Зарегистрировать') + + def validate_nickname(self, nickname): + user = User.query.filter_by(nickname=nickname.data).first() + if user is not None: + raise ValidationError('Ник занят. Выберите другой, пожалуйста.') diff --git a/app/generator.py b/app/generator.py index 6ecf254..840157d 100644 --- a/app/generator.py +++ b/app/generator.py @@ -3,8 +3,10 @@ Принимает через функцию start_module запрос пользователя (user_req). Возвращает подготовленный для публикации текст. """ +from __future__ import annotations import re + from app.dbpanel import DBWork, get_content @@ -40,7 +42,7 @@ def get_key_from_request(user_req: str) -> dict: point_in_phrase = len(user_phrase) - 1 # Кроличья нора: вывести всё сразу. - if user_phrase[0] in ['всё', 'все', 'all'] or not user_phrase: + if not user_phrase or user_phrase[0] in ['всё', 'все', 'all']: return key_dict dbw = DBWork() diff --git a/app/models.py b/app/models.py index f9adf71..fd350df 100644 --- a/app/models.py +++ b/app/models.py @@ -2,9 +2,52 @@ Конфигурационные модели для SQL. """ -from app import db_lib from datetime import datetime + from sqlalchemy.orm import relationship +from werkzeug.security import check_password_hash, generate_password_hash +from flask_login import UserMixin +from app import login_manager + +from app import db_lib + + +class User(UserMixin, db_lib.Model): + """ + Таблица User - информация о зарегистрированных пользователях. + """ + + __tablename__ = 'User' + + id = db_lib.Column(db_lib.Integer, primary_key=True) + social_id = db_lib.Column(db_lib.String(64), nullable=True, unique=True) + nickname = db_lib.Column(db_lib.String(64), nullable=False, unique=True) + password_hash = db_lib.Column(db_lib.String(128)) + isadmin = db_lib.Column(db_lib.Boolean, default=False) + active = db_lib.Column(db_lib.Boolean, default=True) + + def set_password(self, password: str) -> None: + """ + Создание хеш-слепка пароля. + """ + self.password_hash = generate_password_hash(password) + + def check_password(self, password: str) -> bool: + """ + Проверка соответствия хэш-слепка пароля введённому паролю. + """ + return check_password_hash(self.password_hash, password) + + def __repr__(self): + return f'' + + def __str__(self): + return self.nickname + + +@login_manager.user_loader +def load_user(id): + return User.query.get(int(id)) class Category(db_lib.Model): diff --git a/app/routes.py b/app/routes.py index 065b19b..cee1739 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,12 +1,19 @@ #!/usr/bin/env python3 -from flask import render_template, redirect, request +from flask import flash, redirect, render_template, request, url_for +from flask_login import current_user, login_required, login_user +from werkzeug.urls import url_parse + from app import application -from app.generator import start_module from app.dbpanel import DBWork +from app.forms import LoginForm, RegForm +from app.generator import start_module +from app.models import User -active_output: str = '' +from app import db_lib +# Дополнительные настройки +application.add_template_global(DBWork().get_list_category, 'list_category') @application.route('/', methods=['GET', 'POST']) @application.route('/index', methods=['GET', 'POST']) @@ -14,37 +21,107 @@ def index(): """ Вывод главной страницы проекта. """ - global active_output dbw = DBWork() - output: str = '' - tag_links: list = list() + form_login = LoginForm() + data_render = dict( + title='online', + output='', + category_links=dbw.get_list_category, # chat_string + tag_links=list(), # tag_string + exoutput='', + form_login=form_login + ) - # Обработчик запроса пользователя GET - if request.method == 'GET': + chatstring = request.args.get(key="chatstring", default="") + tagstring = request.args.get(key="tagstring", default="") + question_to_base = f'{chatstring} {tagstring}' + data_render['output'] = start_module(question_to_base) - if request.args.get(key='chatstring'): - output = start_module(request.args['chatstring']) - active_output = request.args['chatstring'] + data_render['exoutput'] = request.args.get(key='chatstring', default='') - elif request.args.get(key='tagstring'): - output = start_module(f"{active_output} {request.args['tagstring']}") - else: - output = start_module('all') - active_output = '' + tag_links = dbw.get_choices_types(category=data_render['exoutput']) + data_render['tag_links'] = tag_links if len(tag_links) > 1 else list() - tag_links = dbw.get_choices_types(category=active_output) - if len(tag_links) < 2: - tag_links = list() + if request.method == 'POST': + if form_login.validate_on_submit() and form_login.submit.data: + user = User.query.filter_by(nickname=form_login.nickname.data).first() + if user is None or not user.check_password(form_login.password.data): + flash('Неверные данные.', category='error') + return render_template('index.html', **data_render) + + login_user(user, remember=True) + + if request.method == 'GET': + pass - return render_template('index.html', - title='online', - output=output, - exoutput=active_output, - category_links=dbw.get_list_category, - tag_links=tag_links) + return render_template('index.html', **data_render) @application.route('/db') -def createbase(): - pass +@login_required +def db_view(): + """ + Для авторизованных пользователей работа с записями в базе данных. + + :return: HttpResponse, форма. + """ + + if not current_user.isadmin: + return f'Техническая страница недоступна для вашей учётной записи. На главную.' + + return 'В стадии разработки.' + + +@application.route('/login', methods=['GET', 'POST']) +@application.template_global() +def login(): + """ + Форма регистрации и авторизации пользователя. + + :return: HttpResponse, форма. + """ + # https://habr.com/ru/post/346346/ + + if current_user.is_authenticated: + return redirect(url_for('index')) + + form_login = LoginForm(prefix='login') + + if form_login.validate_on_submit(): + user = User.query.filter_by(nickname=form_login.nickname.data).first() + if user is None or not user.check_password(form_login.password.data): + flash('Неверные данные.', category='error') + return redirect(url_for('login')) + login_user(user, remember=True) + + next_page = request.args.get('next') + if not next_page or url_parse(next_page).netloc != '': + next_page = url_for('index') + return redirect(next_page) + + return render_template('auth/login.html', title='Авторизация', + form=form_login) + +@application.route('/singin', methods=['GET', 'POST']) +@application.template_global() +def singin(): + + if current_user.is_authenticated: + return redirect(url_for('index')) + + form_reg = RegForm() + + if form_reg.validate_on_submit(): + user = User( + nickname=form_reg.nickname.data + ) + user.set_password(form_reg.password1.data) + db_lib.session.add(user) + db_lib.session.commit() + + login_user(user, remember=True) + + return redirect(url_for('index')) + return render_template('auth/registration.html', title='Регистрация', + form_reg=form_reg) diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000..cb82206 --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block content %} + +

Портал в закулисье

+ + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} + +
+ {{ form.hidden_tag() }} + +

+ {{ form.nickname.label }}*
+ {{ form.nickname(size=32) }}
+ {% for error in form.nickname.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}*
+ {{ form.password(size=32) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.submit() }} +

+
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/auth/registration.html b/app/templates/auth/registration.html new file mode 100644 index 0000000..22f08a6 --- /dev/null +++ b/app/templates/auth/registration.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} +{% block content %} + +

Регистрация

+ + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} + +
+ {{ form_reg.hidden_tag() }} + +

+ {{ form_reg.nickname.label }}
+ {{ form_reg.nickname(size=32) }}
+ {% for error in form_reg.nickname.errors %} + [{{ error }}] + {% endfor %} +

+ +

+ {{ form_reg.password1.label }}
+ {{ form_reg.password1(size=32) }}
+ {% for error in form_reg.password1.errors %} + {{ error }} + {% endfor %} +

+

+ {{ form_reg.password2.label }}
+ {{ form_reg.password2(size=32) }}
+ {% for error in form_reg.password2.errors %} + {{ error }} + {% endfor %} +

+

{{ form_reg.submit() }}

+ +
+ +{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 74d32d5..c1371ae 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -38,6 +38,11 @@ + + + diff --git a/app/templates/footer.html b/app/templates/footer.html new file mode 100644 index 0000000..abbf531 --- /dev/null +++ b/app/templates/footer.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index ee523f0..a86220d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -7,7 +7,7 @@ {{ temp }} {% if tag_links %} {% for tag_link in tag_links %} - + {% if tag_link.lower() in request.url %} {% else %} diff --git a/app/templates/navbar.html b/app/templates/navbar.html index 9e2584a..3ef7f41 100644 --- a/app/templates/navbar.html +++ b/app/templates/navbar.html @@ -5,6 +5,14 @@ - \ No newline at end of file +
+ {% if form_login %} + {% if current_user.is_authenticated %} + {{ current_user.nickname }} + {% else %} +
+ {{ form_login.hidden_tag() }} + +
+ + {% endif %} + {% endif %} +
+ diff --git a/requirements.txt b/requirements.txt index 794df46..0bad928 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,18 @@ alembic==1.4.2 astroid==2.3.3 +bcrypt==3.1.7 +cffi==1.14.0 click==7.1.1 colorama==0.4.3 DAWG-Python==0.7.2 +decorator==4.4.2 dnspython==1.16.0 docopt==0.6.2 dominate==2.5.1 entrypoints==0.3 Flask==1.1.2 +Flask-Login==0.5.0 +Flask-Migrate==2.5.3 Flask-SQLAlchemy==2.4.1 Flask-SSLify==0.1.5 Flask-WTF==0.14.3 @@ -24,6 +29,7 @@ mysql-connector-python==8.0.19 pbr==5.4.5 protobuf==3.6.1 pycodestyle==2.5.0 +pycparser==2.20 pyflakes==2.1.1 pylint==2.4.4 pymorphy2==0.8 @@ -35,8 +41,11 @@ PyYAML==5.3.1 six==1.14.0 smmap==3.0.2 SQLAlchemy==1.3.16 +sqlalchemy-migrate==0.13.0 sqlalchemy-mixins==1.2.1 +sqlparse==0.3.1 stevedore==1.32.0 +Tempita==0.5.2 visitor==0.1.3 Werkzeug==1.0.1 wrapt==1.11.2