diff --git a/.gitignore b/.gitignore index b44effb..ffba436 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ db/*.db-wal output /output/ *.old +/backup/ diff --git a/labertasche.yaml b/labertasche.yaml index d2d7959..a1d947f 100644 --- a/labertasche.yaml +++ b/labertasche.yaml @@ -12,23 +12,10 @@ system: database_uri: "sqlite:///db/labertasche.db" # Database URI. See documentation. Default is sqlite. secret: "6Gxvb52bIJCm2vfDsmWKzShKp1omrzVG" # CHANGE ME! THIS IS IMPORTANT! output: "./__implementation_example/data/" # Base path for the output json - debug: false # Leave this as is, this is for development. + debug: true # Leave this as is, this is for development. send_otp_to_publish: true # Disables confirmation w/ OTP via mail cookie_secure: false -projects: - - default: - web_url: "http://dev.localhost:1314/" - blog_url: "http://dev.localhost:1313/" - output: "./__implementation_example/data/" - send_otp_to_publish: true - - - example.com: - web_url: "http://comments.example.com/" - blog_url: "http://blog.example.com/" - output: "./example/data/" - send_otp_to_publish: true - gravatar: cache: true # Enable caching of gravatar images static_dir: "./__implementation_example/static/images/gravatar/" # Where to store cached images, must exist! diff --git a/labertasche/blueprints/__init__.py b/labertasche/blueprints/__init__.py index 4d25aed..e9acfac 100644 --- a/labertasche/blueprints/__init__.py +++ b/labertasche/blueprints/__init__.py @@ -10,3 +10,4 @@ from .bp_comments import bp_comments from .bp_login import bp_login from .bp_dashboard import bp_dashboard from .bp_jsconnector import bp_jsconnector +from .bp_upgrades import bp_dbupgrades diff --git a/labertasche/blueprints/bp_upgrades/__init__.py b/labertasche/blueprints/bp_upgrades/__init__.py new file mode 100644 index 0000000..afceb8c --- /dev/null +++ b/labertasche/blueprints/bp_upgrades/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from flask import Blueprint + +# Blueprint +bp_dbupgrades = Blueprint("bp_dbupgrades", __name__, url_prefix='/upgrade') + +from .db_v2 import upgrade_db_to_v2 diff --git a/labertasche/blueprints/bp_upgrades/db_v2.py b/labertasche/blueprints/bp_upgrades/db_v2.py new file mode 100644 index 0000000..cae930d --- /dev/null +++ b/labertasche/blueprints/bp_upgrades/db_v2.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from . import bp_dbupgrades +from flask_cors import cross_origin +from flask_login import login_required +from flask import render_template, jsonify, make_response +from pathlib import Path +from labertasche.database import labertasche_db as db +from labertasche.models import TProjects, TComments, TLocation, TEmail, TVersion +from labertasche.helper import Settings +from json import dump, load +from shutil import copy, make_archive +from re import search +from secrets import compare_digest +from datetime import datetime + + +def get_backup_folder() -> Path: + path = Path('.').absolute() / "backup" / "v1" + return path + + +@cross_origin() +@bp_dbupgrades.route('/db_v2/') +@login_required +def upgrade_db_to_v2(): + # TODO: Check if db has already been upgraded + status = False + try: + version = db.session.query(TVersion).first() + if version: + status = True + except Exception as e: + print(e.__class__) + pass + + return render_template("db-upgrades.html", title="DB upgrade V1 to V2", + prev_version=1, new_version=2, status=status) + + +@cross_origin() +@bp_dbupgrades.route('/db_v2/backup/', methods=['GET']) +@login_required +def upgrade_db_to_v2_backup(): + path = get_backup_folder() + # Create path for backup + try: + if not path.exists(): + path.mkdir(mode=755, exist_ok=True, parents=True) + except OSError as e: + return make_response(jsonify(status='exception', msg=str(e)), 400) + + return make_response(jsonify(status="ok"), 200) + + +@cross_origin() +@bp_dbupgrades.route('/db_v2/export/') +@login_required +def upgrade_db_to_v2_export(): + path = get_backup_folder() + + # make sure nothing is pending + db.session.commit() + + # Export tables + t_locations = db.session.query(TLocation.id_location, TLocation.location).all() + t_emails = db.session.query(TEmail.id_email, TEmail.email, TEmail.is_allowed, TEmail.is_blocked).all() + t_comments = db.session.query(TComments.comments_id, TComments.location_id, TComments.email, + TComments.content, TComments.created_on, TComments.is_published, + TComments.is_spam, TComments.spam_score, TComments.replied_to, + TComments.confirmation, TComments.deletion, TComments.gravatar).all() + + locations = [] + for loc in t_locations: + locations.append({ + "id_location": loc.id_location, + "location": loc.location + }) + + emails = [] + for mail in t_emails: + emails.append({ + "id_email": mail.id_email, + "email": mail.email, + "is_allowed": mail.is_allowed, + "is_blocked": mail.is_blocked + }) + + comments = [] + for comment in t_comments: + comments.append({ + "comments_id": comment.comments_id, + "location_id": comment.location_id, + "email": comment.email, + "content": comment.content, + "created_on": f"{comment.created_on.__str__()}", + "is_published": comment.is_published, + "is_spam": comment.is_spam, + "spam_score": comment.spam_score, + "replied_to": comment.replied_to, + "confirmation": comment.confirmation, + "deletion": comment.deletion, + "gravatar": comment.gravatar + }) + + # Output jsons + try: + p_export_location = path / "locations.json" + with p_export_location.open('w') as fp: + dump(locations, fp, indent=4, sort_keys=True) + + p_export_mail = path / "emails.json" + with p_export_mail.open('w') as fp: + dump(emails, fp, indent=4, sort_keys=True) + + p_export_comments = path / "comments.json" + with p_export_comments.open('w') as fp: + dump(comments, fp, indent=4, sort_keys=True) + + except Exception as e: + return make_response(jsonify(status='exception-write-json', msg=str(e)), 400) + + # Copy database + try: + settings = Settings() + db_uri = settings.system['database_uri'] + if compare_digest(db_uri[0:6], "sqlite"): + m = search("([/]{3})(.*)", db_uri) + new_db = get_backup_folder() / "labertasche.db" + old_db = Path(m.group(2)).absolute() + copy(old_db, new_db) + except Exception as e: + return make_response(jsonify(status='exception-copy-db', msg=str(e)), 400) + + make_archive(path, "zip", path) + + return make_response(jsonify(status='ok'), 200) + + +@cross_origin() +@bp_dbupgrades.route('/db_v2/recreate/') +@login_required +def upgrade_db_to_v2_recreate(): + try: + db.drop_all() + db.session.flush() + db.session.commit() + db.create_all() + except Exception as e: + return make_response(jsonify(status='exception', msg=str(e)), 400) + + return make_response(jsonify(status='ok'), 200) + + +@cross_origin() +@bp_dbupgrades.route('/db_v2/import/') +@login_required +def upgrade_db_to_v2_import(): + path = get_backup_folder() + settings = Settings() + + try: + # load location + p_loc = (path / 'locations.json').absolute() + with p_loc.open('r') as fp: + locations = load(fp) + + # load mails + m_loc = (path / 'emails.json').absolute() + with m_loc.open('r') as fp: + mails = load(fp) + + # load comments + c_loc = (path / 'comments.json').absolute() + with c_loc.open('r') as fp: + comments = load(fp) + + except FileNotFoundError as e: + return make_response(jsonify(status='exception-filenotfound', msg=str(e)), 400) + + # Create project + default_project = { + "id_project": 1, + "name": "default", + "weburl": settings.system['web_url'], + "blogurl": settings.system['blog_url'], + "output": settings.system['output'], + "sendotp": settings.system['send_otp_to_publish'], + "gravatar_cache": settings.gravatar['cache'], + "gravatar_cache_dir": settings.gravatar['static_dir'], + "gravatar_size": settings.gravatar['size'], + "addon_smileys": settings.addons['smileys'] + } + + # Create db version, so we can track it in the future + version = { + "id_version": 1, + "version": 2 + } + + try: + # Add to db + db.session.add(TVersion(**version)) + db.session.add(TProjects(**default_project)) + + # walk json and readd to database with project set to project 1 + for each in mails: + each.update({'project_id': 1}) + db.session.add(TEmail(**each)) + + for each in locations: + each.update({'project_id': 1}) + db.session.add(TLocation(**each)) + + for each in comments: + each.update({'project_id': 1}) + dt = datetime.fromisoformat(each['created_on']) + each.update({'created_on': dt}) + db.session.add(TComments(**each)) + + # Commit + db.session.commit() + db.session.flush() + except Exception as e: + return make_response(jsonify(status='exception-database', msg=str(e)), 400) + + return make_response(jsonify(status='ok'), 200) diff --git a/labertasche/database/__init__.py b/labertasche/database/__init__.py index c257476..3f447f5 100644 --- a/labertasche/database/__init__.py +++ b/labertasche/database/__init__.py @@ -13,7 +13,7 @@ from sqlalchemy import MetaData convention = { "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", + "ck": "ck_%(table_name)s_%(column_0_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s" } diff --git a/labertasche/models/__init__.py b/labertasche/models/__init__.py index 7e9bc2c..dbf0ee2 100644 --- a/labertasche/models/__init__.py +++ b/labertasche/models/__init__.py @@ -10,3 +10,4 @@ from .t_comments import TComments from .t_location import TLocation from .t_emails import TEmail from .t_projects import TProjects +from .t_version import TVersion diff --git a/labertasche/models/t_version.py b/labertasche/models/t_version.py new file mode 100644 index 0000000..c54dfd6 --- /dev/null +++ b/labertasche/models/t_version.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# /********************************************************************************** +# * _author : Domeniko Gentner +# * _mail : code@tuxstash.de +# * _repo : https://git.tuxstash.de/gothseidank/labertasche +# * _license : This project is under MIT License +# *********************************************************************************/ +from labertasche.database import labertasche_db as db + + +class TVersion(db.Model): + # table name + __tablename__ = "t_version" + __table_args__ = {'useexisting': True} + + # primary key + id_version = db.Column(db.Integer, primary_key=True, autoincrement=True) + + # data + version = db.Column(db.Integer) diff --git a/labertasche/settings/__init__.py b/labertasche/settings/__init__.py index 3f7e22f..6fba975 100644 --- a/labertasche/settings/__init__.py +++ b/labertasche/settings/__init__.py @@ -28,4 +28,4 @@ class Settings: self.gravatar = conf['gravatar'] self.addons = conf['addons'] self.smileys = conf['smileys'] - self.projects = conf['projects'] + diff --git a/server.py b/server.py index b37b929..8707b10 100644 --- a/server.py +++ b/server.py @@ -9,17 +9,15 @@ import logging from flask import Flask, redirect, url_for from flask_cors import CORS -from sqlalchemy import event +from sqlalchemy import event, inspect # noinspection PyProtectedMember from sqlalchemy.engine import Engine from labertasche.settings import Settings from labertasche.database import labertasche_db -from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector -from labertasche.models import TProjects +from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector, bp_dbupgrades from labertasche.helper import User from flask_login import LoginManager -from flask_migrate import Migrate - +from datetime import timedelta # Load settings settings = Settings() @@ -30,48 +28,48 @@ laberflask.config.update(dict( SESSION_COOKIE_DOMAIN=settings.system['cookie_domain'], SESSION_COOKIE_SECURE=settings.system['cookie_secure'], REMEMBER_COOKIE_SECURE=settings.system['cookie_secure'], + REMEMBER_COOKIE_DURATION=timedelta(days=7), + REMEMBER_COOKIE_HTTPONLY=True, + REMEMBER_COOKIE_REFRESH_EACH_REQUEST=True, DEBUG=settings.system['debug'], SECRET_KEY=settings.system['secret'], - TEMPLATES_AUTO_RELOAD=True, + TEMPLATES_AUTO_RELOAD=settings.system['debug'], SQLALCHEMY_DATABASE_URI=settings.system['database_uri'], SQLALCHEMY_TRACK_MODIFICATIONS=False )) -# Flask migrate -migrate = Migrate(laberflask, labertasche_db, render_as_batch=True) - -# Initialize ORM -labertasche_db.init_app(laberflask) -with laberflask.app_context(): - labertasche_db.create_all() - project = labertasche_db.session.query(TProjects).filter(TProjects.id_project == 1).first() - if not project: - default_project = { - "id_project": 1, - "name": "default" - } - labertasche_db.session.add(TProjects(**default_project)) - labertasche_db.session.commit() - # CORS -CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']}}) +CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']}, + r"/api": {"origins": settings.system['web_url']}, + r"/dashboard": {"origins": settings.system['web_url']}, + }) # Import blueprints laberflask.register_blueprint(bp_comments) laberflask.register_blueprint(bp_dashboard) laberflask.register_blueprint(bp_login) laberflask.register_blueprint(bp_jsconnector) +laberflask.register_blueprint(bp_dbupgrades) # Disable Werkzeug's verbosity during development log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) - # Set up login manager loginmgr = LoginManager(laberflask) loginmgr.login_view = 'bp_admin_login.login' +# Initialize ORM +labertasche_db.init_app(laberflask) +with laberflask.app_context(): + table_names = inspect(labertasche_db.get_engine()).get_table_names() + is_empty = table_names == [] + # Only create tables if the db is empty, so we can a controlled upgrade. + if is_empty: + labertasche_db.create_all() + +# There is only one user @loginmgr.user_loader def user_loader(user_id): if user_id != "0": @@ -79,11 +77,13 @@ def user_loader(user_id): return User(user_id) +# User not authorized @loginmgr.unauthorized_handler def login_invalid(): return redirect(url_for('bp_login.show_login')) +# Enable write-ahead-log for sqlite databases @event.listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): if settings.system["database_uri"][0:6] == 'sqlite': diff --git a/templates/base.html b/templates/base.html index 5157629..725e907 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,15 +4,16 @@ - - - + + + + - + labertasche Dashboard @@ -49,21 +50,22 @@ {% block main %} {% endblock %} - - - - - + + + + {% block javascript_libs %} {% endblock %} - }); - + diff --git a/templates/db-upgrades.html b/templates/db-upgrades.html new file mode 100644 index 0000000..3121ed4 --- /dev/null +++ b/templates/db-upgrades.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block main %} +
+

{{ title }}

+ + {% if not status %} +

+ The latest update has brought some changes to the database and your current db is incompatible. + This will upgrade the database to work with the recent update. + The wizard will create a backup, so don't worry! You will find the + backup in the labertasche root directory under /backup/v{{ prev_version }}.zip. +
+ Please do not reload this page during the process! +

+
+
+ +
+
+
+ {% else %} +

+ This update has already run. Please return to the + dashboard. +

+ {% endif %} +
+{% endblock %} +{% block javascript_libs %} + +{% endblock %} diff --git a/templates/project-list.html b/templates/project-list.html index 1b9256d..48acb86 100644 --- a/templates/project-list.html +++ b/templates/project-list.html @@ -80,7 +80,7 @@ + + {% endblock %} {% block javascript %} tippy('[data-tippy-content]', { allowHTML: true, delay: 500 }); + + weburl = document.getElementById('edit-project-web-url') + weburl.value = window.location.host; {% endblock %}