diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..33629eb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +# Игнорировать байткод +*.pyc +*.pio +**/__pycache__/ + +# Каталог с данными +**/data/ diff --git a/.gitignore b/.gitignore index e31ef77..a236708 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ **/data/ /config **/logs/ + +.env* +alembic.ini diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..058378b --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,70 @@ +from __future__ import with_statement +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = None + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/1_users.py b/alembic/versions/1_users.py new file mode 100644 index 0000000..4dda0df --- /dev/null +++ b/alembic/versions/1_users.py @@ -0,0 +1,33 @@ +"""Пользователи + +Revision ID: 1d212513b25f +Revises: +Create Date: 2018-02-25 14:09:43.942507 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '1d212513b25f' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # Пользователи + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False, unique=True), + sa.Column('password', sa.String(), nullable=True), + sa.Column('disabled', sa.Boolean(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + + +def downgrade(): + op.drop_table("user") diff --git a/alembic/versions/2_tag.py b/alembic/versions/2_tag.py new file mode 100644 index 0000000..9cd2034 --- /dev/null +++ b/alembic/versions/2_tag.py @@ -0,0 +1,29 @@ +"""Tag + +Revision ID: 61ede83f5cd4 +Revises: 1d212513b25f +Create Date: 2018-02-25 14:10:08.770210 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '61ede83f5cd4' +down_revision = '1d212513b25f' +branch_labels = None +depends_on = None + + +def upgrade(): + # Объекты доступа: процедура и содержащий процедуру модуль + op.create_table('tag', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False, unique=True), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table("tag") diff --git a/alembic/versions/3_page.py b/alembic/versions/3_page.py new file mode 100644 index 0000000..07204a4 --- /dev/null +++ b/alembic/versions/3_page.py @@ -0,0 +1,46 @@ +"""empty message + +Revision ID: da6bc5b8d253 +Revises: 61ede83f5cd4 +Create Date: 2020-08-19 04:51:11.868138 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'da6bc5b8d253' +down_revision = '61ede83f5cd4' +branch_labels = None +depends_on = None + + +def upgrade(): + # Статьи + op.create_table('page', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('title', sa.String(), nullable=True), + sa.Column('body', sa.String(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('updated', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + # Теги к заметкам + op.create_table('tagpage', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('page_id', sa.Integer(), nullable=True), + sa.Column('tag_id', sa.Integer(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['page_id'], ['page.id'], ), + sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table("tagpage") + op.drop_table("page") diff --git a/alembic/versions/4_note.py b/alembic/versions/4_note.py new file mode 100644 index 0000000..eae442e --- /dev/null +++ b/alembic/versions/4_note.py @@ -0,0 +1,45 @@ +"""empty message + +Revision ID: df6187181c66 +Revises: da6bc5b8d253 +Create Date: 2020-08-19 04:51:24.137270 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'df6187181c66' +down_revision = 'da6bc5b8d253' +branch_labels = None +depends_on = None + + +def upgrade(): + # Заметка + op.create_table('note', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('title', sa.String(), nullable=True), + sa.Column('body', sa.String(), nullable=True), + sa.Column('created', sa.DateTime(), nullable=True), + sa.Column('updated', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + # Теги к заметкам + op.create_table('tagnote', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('note_id', sa.Integer(), nullable=True), + sa.Column('tag_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['note_id'], ['note.id'], ), + sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + +def downgrade(): + op.drop_table("tagnote") + op.drop_table("note") diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..37be1ac --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.8 + +RUN groupadd user +RUN useradd -m -d /home/user -s /bin/bash -g user -m user + +WORKDIR /home/user +COPY deploy/requirements.txt /tmp/ +RUN pip install -r /tmp/requirements.txt + +USER user:user + +RUN mkdir -p ~/data/files +RUN touch ~/data/myapp.log + +COPY --chown=user:user . ./ + +# EXPOSE 8000 + +CMD ["gunicorn", "-w", "4", "--bind=0.0.0.0", "myapp:app"] diff --git a/deploy/nginx/app.conf b/deploy/nginx/app.conf new file mode 100644 index 0000000..409f12e --- /dev/null +++ b/deploy/nginx/app.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name _; + access_log /var/log/nginx/myapp-access.log; + error_log /var/log/nginx/myapp-error.log; + location / { + include proxy_params; + proxy_pass http://app:8000/; + } +} diff --git a/deploy/nginx/myapp.conf b/deploy/nginx/myapp.conf deleted file mode 100644 index 4bcbf0e..0000000 --- a/deploy/nginx/myapp.conf +++ /dev/null @@ -1,10 +0,0 @@ -server { - listen 80; - server_name myapp www.myapp; - access_log /var/log/nginx/myapp-access.log mainproxy; - error_log /var/log/nginx/myapp-error.log info; - location / { - include uwsgi_params; - uwsgi_pass unix:///run/uwsgi/myapp.sock; - } -} diff --git a/deploy/nginx/proxy_params b/deploy/nginx/proxy_params new file mode 100644 index 0000000..7fc3792 --- /dev/null +++ b/deploy/nginx/proxy_params @@ -0,0 +1,3 @@ +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/deploy/requirements.txt b/deploy/requirements.txt index 2d3ceb5..8976d1e 100644 --- a/deploy/requirements.txt +++ b/deploy/requirements.txt @@ -1,6 +1,8 @@ -wtforms -flask -sqlalchemy -sqlalchemy-utils -alembic -celery +Flask==1.1.2 +Flask-JSONRPC==1.1.0 +alembic==1.4.2 +SQLAlchemy==1.3.17 +SQLAlchemy-Utils==0.36.6 +flake8==3.8.3 +gunicorn==20.0.4 +psycopg2-binary==2.8.5 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fd4f0c4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.8' + +services: + app: + build: + context: ./ + dockerfile: ./deploy/Dockerfile + volumes: + - data:/home/app/data + - config:/home/app/config + links: + - db + depends_on: + - db + restart: + always + command: ./entrypoint.sh + + db: + image: postgres:latest + restart: always + volumes: + - ./data/db:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/10-init.sql + env_file: + - .env.db + + nginx: + image: nginx:latest + restart: always + links: + - app + ports: + - "8000:80" + volumes: + - ./logs:/var/log/nginx + - ./deploy/nginx/proxy_params:/etc/nginx/proxy_params + - ./deploy/nginx/app.conf:/etc/nginx/conf.d/app.conf + +volumes: + db: + data: + config: diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..e27e7b4 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +alembic upgrade head +gunicorn -w 4 --bind=0.0.0.0 myapp:app diff --git a/myapp/models/note.py b/myapp/models/note.py index 12e3013..5b2c1f4 100644 --- a/myapp/models/note.py +++ b/myapp/models/note.py @@ -50,7 +50,6 @@ class TagNote(Base): id = Column(Integer, primary_key=True) note_id = Column(Integer, ForeignKey('note.id')) tag_id = Column(Integer, ForeignKey('tag.id')) - created = Column(DateTime) # Дата создания # Связи note = relationship( @@ -69,7 +68,6 @@ class TagNote(Base): assert type(tag).__name__ == 'Tag', 'Не передан объект Tag' self.note_id = note.id self.tag_id = tag.id - self.created = datetime.datetime.now() def __repr__(self): return "" % (self.id) diff --git a/myapp/ns_api/login.py b/myapp/ns_api/login.py index fd1b2d9..db72454 100644 --- a/myapp/ns_api/login.py +++ b/myapp/ns_api/login.py @@ -2,6 +2,8 @@ __author__ = 'RemiZOffAlex' __email__ = 'remizoffalex@mail.ru' __url__ = 'https://remizoffalex.ru/' +import string + from flask import session from . import jsonrpc @@ -25,3 +27,34 @@ def login(username: str, password: str) -> bool: session['logged_in'] = True session['user_id'] = user.id return True + + +@jsonrpc.method('login.register') +def login_register(username: str, password: str) -> bool: + """Регистрация + """ + if len(username) < 4 or len(username) > 25: + raise ValueError('Длина логина от 4 до 25 символов') + if len(password) < 4 or len(password) > 150: + raise ValueError('Длина пароля от 4 до 150 символов') + for char in username: + if char not in string.ascii_letters + string.digits: + raise ValueError + + user = models.db_session.query( + models.User + ).filter( + models.User.name == username + ).first() + if user: + raise ValueError('Пользователь с таким логином уже существует') + + newuser = models.User(username) + newuser.password = lib.get_hash_password( + password, + app.config['SECRET_KEY'] + ) + models.db_session.add(newuser) + models.db_session.commit() + + return True diff --git a/myapp/ns_login/templates/login.html b/myapp/ns_login/templates/login.html index a550d9c..e7a1496 100644 --- a/myapp/ns_login/templates/login.html +++ b/myapp/ns_login/templates/login.html @@ -28,6 +28,7 @@
diff --git a/myapp/ns_login/templates/register.html b/myapp/ns_login/templates/register.html new file mode 100644 index 0000000..cf80258 --- /dev/null +++ b/myapp/ns_login/templates/register.html @@ -0,0 +1,90 @@ +{% extends "skeleton.html" %} +{% block content %} + +
+
+ +

Регистрация

+
+ +
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+ +
Зарегистрироваться
+Вход + +
+
+ +{% endblock %} + +{% block script %} + +{% endblock %} diff --git a/myapp/ns_login/views.py b/myapp/ns_login/views.py index 47b7954..c6fb0d0 100644 --- a/myapp/ns_login/views.py +++ b/myapp/ns_login/views.py @@ -23,3 +23,12 @@ def logout(): session.pop('logged_in', None) session.pop('user_id', None) return redirect("/", code=302) + + +@app.route('/register') +def register(): + """Регистрация нового пользователя + """ + pagedata = {} + body = render_template('register.html', pagedata=pagedata) + return body diff --git a/myapp/ns_page/views_guest.py b/myapp/ns_page/views_guest.py index a1faa67..bcda278 100644 --- a/myapp/ns_page/views_guest.py +++ b/myapp/ns_page/views_guest.py @@ -8,7 +8,8 @@ from .. import app, models def pages(page): - """Список статей""" + """Список статей + """ pagedata = {'title': 'Статьи - ' + app.config['TITLE']} pagedata['pagination'] = { diff --git a/myapp/ns_page/views_user.py b/myapp/ns_page/views_user.py index ff24edf..80b2e50 100644 --- a/myapp/ns_page/views_user.py +++ b/myapp/ns_page/views_user.py @@ -8,8 +8,7 @@ from .. import app, models def pages(page): - """ - Список статей + """Список статей """ pagedata = {'title': 'Статьи - ' + app.config['TITLE']} diff --git a/myapp/templates/navbar.html b/myapp/templates/navbar.html index 4aaae9e..4cde13c 100644 --- a/myapp/templates/navbar.html +++ b/myapp/templates/navbar.html @@ -1,13 +1,13 @@
diff --git a/myapp/templates/user/navbar.html b/myapp/templates/user/navbar.html index 2fdd040..c2d7594 100644 --- a/myapp/templates/user/navbar.html +++ b/myapp/templates/user/navbar.html @@ -1,16 +1,16 @@