Projects Update
* Conversion script to new configuration layout. This is to prevent from secrets being loaded into memory aside from when they are needed. * Updated code throughout the project to account for the new methods. * New classes and code for new configuration layout * New Javascript for backend to control all the dialogues * Moved modals to separate files * Smaller database model update * Removed redundant code * Added database upgrade path via the dashboard * Admin password is now properly hashed * You can now export all locations manually in event of an error
This commit is contained in:
parent
194ffc754b
commit
ad8f68cdd8
7
.gitignore
vendored
7
.gitignore
vendored
@ -4,8 +4,11 @@ venv
|
|||||||
db/*.db
|
db/*.db
|
||||||
db/*.db-shm
|
db/*.db-shm
|
||||||
db/*.db-wal
|
db/*.db-wal
|
||||||
output
|
|
||||||
/output/
|
|
||||||
*.old
|
*.old
|
||||||
|
*.server
|
||||||
/backup/
|
/backup/
|
||||||
labertasche.yaml
|
labertasche.yaml
|
||||||
|
/.secret
|
||||||
|
/credentials.yaml
|
||||||
|
*.bak
|
||||||
|
smileys.yaml
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
// post-internal-server-error
|
// post-internal-server-error
|
||||||
// post-success
|
// post-success
|
||||||
// post-before-fetch
|
// post-before-fetch
|
||||||
|
// post-project-not-found
|
||||||
function labertasche_callback(state)
|
function labertasche_callback(state)
|
||||||
{
|
{
|
||||||
if (state === "post-before-fetch"){
|
if (state === "post-before-fetch"){
|
||||||
|
@ -15,8 +15,8 @@ from sqlalchemy import exc
|
|||||||
from labertasche.database import labertasche_db as db
|
from labertasche.database import labertasche_db as db
|
||||||
from labertasche.helper import is_valid_json, default_timestamp, check_gravatar, export_location
|
from labertasche.helper import is_valid_json, default_timestamp, check_gravatar, export_location
|
||||||
from labertasche.mail import mail
|
from labertasche.mail import mail
|
||||||
from labertasche.models import TComments, TLocation, TEmail
|
from labertasche.models import TComments, TLocation, TEmail, TProjects
|
||||||
from labertasche.settings import Settings
|
from labertasche.settings import Smileys
|
||||||
from secrets import compare_digest
|
from secrets import compare_digest
|
||||||
|
|
||||||
|
|
||||||
@ -25,19 +25,17 @@ bp_comments = Blueprint("bp_comments", __name__, url_prefix='/comments')
|
|||||||
|
|
||||||
|
|
||||||
# Route for adding new comments
|
# Route for adding new comments
|
||||||
@bp_comments.route("/new", methods=['POST'])
|
@bp_comments.route("/<name>/new", methods=['POST'])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def check_and_insert_new_comment():
|
def check_and_insert_new_comment(name):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
settings = Settings()
|
smileys = Smileys()
|
||||||
smileys = settings.smileys
|
|
||||||
addons = settings.addons
|
|
||||||
sender = mail()
|
sender = mail()
|
||||||
|
|
||||||
# Check length of content and abort if too long or too short
|
# Check length of content and abort if too long or too short
|
||||||
if request.content_length > 2048:
|
if request.content_length > 2048:
|
||||||
return make_response(jsonify(status="post-max-length"), 400)
|
return make_response(jsonify(status="post-max-length"), 400)
|
||||||
if request.content_length <= 0:
|
if request.content_length == 0:
|
||||||
return make_response(jsonify(status="post-min-length"), 400)
|
return make_response(jsonify(status="post-min-length"), 400)
|
||||||
|
|
||||||
# get json from request
|
# get json from request
|
||||||
@ -59,9 +57,14 @@ def check_and_insert_new_comment():
|
|||||||
content = re.sub(tags, '', new_comment['content']).strip()
|
content = re.sub(tags, '', new_comment['content']).strip()
|
||||||
content = re.sub(special, '', content).strip()
|
content = re.sub(special, '', content).strip()
|
||||||
|
|
||||||
# Convert smileys
|
# Get project
|
||||||
if addons['smileys']:
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
for key, value in smileys.items():
|
if not project:
|
||||||
|
return make_response(jsonify(status="post-project-not-found"), 400)
|
||||||
|
|
||||||
|
# Convert smileys if enabled
|
||||||
|
if project.addon_smileys:
|
||||||
|
for key, value in smileys.smileys.items():
|
||||||
content = content.replace(key, value)
|
content = content.replace(key, value)
|
||||||
|
|
||||||
# Validate replied_to field is integer
|
# Validate replied_to field is integer
|
||||||
@ -119,7 +122,8 @@ def check_and_insert_new_comment():
|
|||||||
else:
|
else:
|
||||||
# Insert new location
|
# Insert new location
|
||||||
loc_table = {
|
loc_table = {
|
||||||
'location': new_comment['location']
|
'location': new_comment['location'],
|
||||||
|
'project_id': project.id_project
|
||||||
}
|
}
|
||||||
new_loc = TLocation(**loc_table)
|
new_loc = TLocation(**loc_table)
|
||||||
db.session.add(new_loc)
|
db.session.add(new_loc)
|
||||||
@ -133,11 +137,15 @@ def check_and_insert_new_comment():
|
|||||||
# insert comment
|
# insert comment
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
|
if project.sendotp:
|
||||||
new_comment.update({"is_published": False})
|
new_comment.update({"is_published": False})
|
||||||
|
else:
|
||||||
|
new_comment.update({"is_published": True})
|
||||||
new_comment.update({"created_on": default_timestamp()})
|
new_comment.update({"created_on": default_timestamp()})
|
||||||
new_comment.update({"is_spam": is_spam})
|
new_comment.update({"is_spam": is_spam})
|
||||||
new_comment.update({"spam_score": has_score})
|
new_comment.update({"spam_score": has_score})
|
||||||
new_comment.update({"gravatar": check_gravatar(new_comment['email'])})
|
new_comment.update({"gravatar": check_gravatar(new_comment['email'], project.name)})
|
||||||
|
new_comment.update({"project_id": project.id_project})
|
||||||
t_comment = TComments(**new_comment)
|
t_comment = TComments(**new_comment)
|
||||||
db.session.add(t_comment)
|
db.session.add(t_comment)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -145,7 +153,8 @@ def check_and_insert_new_comment():
|
|||||||
db.session.refresh(t_comment)
|
db.session.refresh(t_comment)
|
||||||
|
|
||||||
# Send confirmation link and store returned value
|
# Send confirmation link and store returned value
|
||||||
hashes = sender.send_confirmation_link(new_comment['email'])
|
if project.sendotp:
|
||||||
|
hashes = sender.send_confirmation_link(new_comment['email'], project.name)
|
||||||
setattr(t_comment, "confirmation", hashes[0])
|
setattr(t_comment, "confirmation", hashes[0])
|
||||||
setattr(t_comment, "deletion", hashes[1])
|
setattr(t_comment, "deletion", hashes[1])
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@ -163,11 +172,12 @@ def check_and_insert_new_comment():
|
|||||||
|
|
||||||
|
|
||||||
# Route for confirming comments
|
# Route for confirming comments
|
||||||
@bp_comments.route("/confirm/<email_hash>", methods=['GET'])
|
@bp_comments.route("/confirm/<name>/<email_hash>", methods=['GET'])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def check_confirmation_link(email_hash):
|
def check_confirmation_link(name, email_hash):
|
||||||
settings = Settings()
|
|
||||||
comment = db.session.query(TComments).filter(TComments.confirmation == email_hash).first()
|
comment = db.session.query(TComments).filter(TComments.confirmation == email_hash).first()
|
||||||
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
|
|
||||||
if comment:
|
if comment:
|
||||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||||
if compare_digest(comment.confirmation, email_hash):
|
if compare_digest(comment.confirmation, email_hash):
|
||||||
@ -175,27 +185,27 @@ def check_confirmation_link(email_hash):
|
|||||||
if not comment.is_spam:
|
if not comment.is_spam:
|
||||||
setattr(comment, "is_published", True)
|
setattr(comment, "is_published", True)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
url = f"{settings.system['blog_url']}{location.location}#comment_{comment.comments_id}"
|
url = f"{project.blogurl}{location.location}#comment_{comment.comments_id}"
|
||||||
export_location(location.id_location)
|
export_location(location.id_location)
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
|
|
||||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
return redirect(f"{project.blogurl}?cnf=true")
|
||||||
|
|
||||||
|
|
||||||
# Route for deleting comments
|
# Route for deleting comments
|
||||||
@bp_comments.route("/delete/<email_hash>", methods=['GET'])
|
@bp_comments.route("/delete/<name>/<email_hash>", methods=['GET'])
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def check_deletion_link(email_hash):
|
def check_deletion_link(name, email_hash):
|
||||||
settings = Settings()
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
query = db.session.query(TComments).filter(TComments.deletion == email_hash)
|
comment = db.session.query(TComments).filter(TComments.deletion == email_hash).first()
|
||||||
comment = query.first()
|
|
||||||
if comment:
|
if comment:
|
||||||
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
location = db.session.query(TLocation).filter(TLocation.id_location == comment.location_id).first()
|
||||||
if compare_digest(comment.deletion, email_hash):
|
if compare_digest(comment.deletion, email_hash):
|
||||||
query.delete()
|
comment.delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
url = f"{settings.system['blog_url']}?deleted=true"
|
url = f"{project.blogurl}?deleted=true"
|
||||||
export_location(location.id_location)
|
export_location(location.id_location)
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
|
|
||||||
return redirect(f"{settings.system['blog_url']}?cnf=true")
|
return redirect(f"{project.blogurl}?cnf=true")
|
||||||
|
@ -7,12 +7,12 @@
|
|||||||
# * _license : This project is under MIT License
|
# * _license : This project is under MIT License
|
||||||
# *********************************************************************************/
|
# *********************************************************************************/
|
||||||
from . import bp_jsconnector
|
from . import bp_jsconnector
|
||||||
from flask import request, redirect
|
from flask import request, redirect, make_response, jsonify
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
from flask_cors import cross_origin
|
from flask_cors import cross_origin
|
||||||
from labertasche.database import labertasche_db as db
|
from labertasche.database import labertasche_db as db
|
||||||
from labertasche.helper import export_location
|
from labertasche.helper import export_location, get_id_from_project_name
|
||||||
from labertasche.models import TComments, TEmail
|
from labertasche.models import TComments, TEmail, TLocation
|
||||||
|
|
||||||
# This file contains the routes for the manage comments menu point.
|
# This file contains the routes for the manage comments menu point.
|
||||||
# They are called via GET
|
# They are called via GET
|
||||||
@ -112,3 +112,19 @@ def api_comment_block_mail(comment_id):
|
|||||||
return redirect(request.referrer)
|
return redirect(request.referrer)
|
||||||
|
|
||||||
|
|
||||||
|
@cross_origin()
|
||||||
|
@bp_jsconnector.route('/comment-export-all/<name>', methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
def api_export_all_by_project(name):
|
||||||
|
proj_id = get_id_from_project_name(name)
|
||||||
|
if proj_id == -1:
|
||||||
|
return make_response(jsonify(status='not-found'), 400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
locations = db.session.query(TLocation).all()
|
||||||
|
for each in locations:
|
||||||
|
export_location(each.id_location)
|
||||||
|
except Exception as e:
|
||||||
|
return make_response(jsonify(status='sql-error', msg=str(e)), 400)
|
||||||
|
|
||||||
|
return make_response(jsonify(status='ok'), 200)
|
||||||
|
@ -13,9 +13,57 @@ from flask_cors import cross_origin
|
|||||||
from labertasche.database import labertasche_db as db
|
from labertasche.database import labertasche_db as db
|
||||||
from labertasche.helper import get_id_from_project_name
|
from labertasche.helper import get_id_from_project_name
|
||||||
from labertasche.models import TProjects, TComments, TEmail, TLocation
|
from labertasche.models import TProjects, TComments, TEmail, TLocation
|
||||||
|
from validators import url as validate_url
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def validate_project(project):
|
||||||
|
"""
|
||||||
|
Validates important bits of a project database entry
|
||||||
|
|
||||||
|
:param project: The json from the request, containing the data for a project.
|
||||||
|
:return: A response with the error or None if the project is valid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Validate length
|
||||||
|
if not len(project['name']) and \
|
||||||
|
not len(project['blogurl']) and \
|
||||||
|
not len(project['output']):
|
||||||
|
return make_response(jsonify(status='too-short'), 400)
|
||||||
|
|
||||||
|
# Validate project name
|
||||||
|
if not re.match('^\\w+$', project['name']):
|
||||||
|
print(project['name'])
|
||||||
|
return make_response(jsonify(status='invalid-project-name'), 400)
|
||||||
|
|
||||||
|
# Check if project name already exists
|
||||||
|
name_check = db.session.query(TProjects.name).filter(TProjects.name == project['name']).first()
|
||||||
|
if name_check:
|
||||||
|
return make_response(jsonify(status='project-exists'), 400)
|
||||||
|
|
||||||
|
# Validate url
|
||||||
|
url_exists = db.session.query(TProjects.blogurl).filter(TProjects.blogurl == project['blogurl']).first()
|
||||||
|
if not validate_url(project['blogurl']) or url_exists:
|
||||||
|
return make_response(jsonify(status='invalid-blog-url'), 400)
|
||||||
|
|
||||||
|
# Validate output path
|
||||||
|
output = Path(project['output']).absolute()
|
||||||
|
# The second check is needed, since javascript is passing an empty string instead of
|
||||||
|
# null. For some reason, this makes SQLAlchemy accept the data and commit it to the db
|
||||||
|
# without exception. This check prevents this issue from happening.
|
||||||
|
if not output.exists() or len(project['output'].strip()) == 0:
|
||||||
|
return make_response(jsonify(status='invalid-path-output'), 400)
|
||||||
|
|
||||||
|
# Validate cache path, if caching is enabled
|
||||||
|
if project['gravatar_cache']:
|
||||||
|
cache = Path(project['gravatar_cache_dir']).absolute()
|
||||||
|
if not cache.exists() or len(project['gravatar_cache_dir'].strip()) == 0:
|
||||||
|
return make_response(jsonify(status='invalid-path-cache'), 400)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
@bp_jsconnector.route("/project/new", methods=['POST'])
|
@bp_jsconnector.route("/project/new", methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@ -25,58 +73,64 @@ def api_create_project():
|
|||||||
|
|
||||||
:return: A string with an error code and 'ok' as string on success.
|
:return: A string with an error code and 'ok' as string on success.
|
||||||
"""
|
"""
|
||||||
# TODO: Project name exists?
|
response = validate_project(request.json)
|
||||||
name = request.json['name']
|
if response is not None:
|
||||||
|
return response
|
||||||
|
|
||||||
if not len(name):
|
try:
|
||||||
return make_response(jsonify(status='too-short'), 400)
|
db.session.add(TProjects(**request.json))
|
||||||
if not re.match('^\\w+$', name):
|
|
||||||
return make_response(jsonify(status='invalid-name'), 400)
|
|
||||||
|
|
||||||
proj = TProjects(name=name)
|
|
||||||
db.session.add(proj)
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(str(e))
|
||||||
|
db.session.rollback()
|
||||||
|
return make_response(jsonify(status='exception', msg=str(e)), 500)
|
||||||
|
|
||||||
return make_response(jsonify(status='ok'), 200)
|
return make_response(jsonify(status='ok'), 200)
|
||||||
|
|
||||||
|
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
@bp_jsconnector.route('project/edit/<name>', methods=['POST'])
|
@bp_jsconnector.route('/project/edit/<name>', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def api_edit_project_name(name: str):
|
def api_edit_project_name(name: str):
|
||||||
"""
|
"""
|
||||||
Renames the project.
|
Renames the project.
|
||||||
:param name:
|
:param name: The previous name of the project to edit, must exist
|
||||||
:return: A string with an error code and 'ok' as string on success.
|
:return: A string with an error code and 'ok' as string on success.
|
||||||
"""
|
"""
|
||||||
# TODO: Project name exists?
|
response = validate_project(request.json)
|
||||||
new_name = request.json['name']
|
if response is not None:
|
||||||
|
return response
|
||||||
|
|
||||||
if not len(new_name):
|
try:
|
||||||
return make_response(jsonify(status='too-short'), 400)
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
if not re.match('^\\w+$', new_name):
|
setattr(project, "id_project", project.id_project)
|
||||||
return make_response(jsonify(status='invalid-name'), 400)
|
setattr(project, "name", request.json['name'])
|
||||||
|
setattr(project, "blogurl", request.json['blogurl'].strip())
|
||||||
proj_id = get_id_from_project_name(name)
|
setattr(project, "output", request.json['output'].strip())
|
||||||
project = db.session.query(TProjects).filter(TProjects.id_project == proj_id)
|
setattr(project, "sendotp", request.json['sendotp'])
|
||||||
setattr(project, 'name', new_name)
|
setattr(project, "gravatar_cache", request.json['gravatar_cache'])
|
||||||
db.session.upate(project)
|
setattr(project, "gravatar_cache_dir", request.json['gravatar_cache_dir'])
|
||||||
|
setattr(project, "gravatar_size", request.json['gravatar_size'])
|
||||||
|
setattr(project, "addon_smileys", request.json['addon_smileys'])
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(str(e))
|
||||||
|
return make_response(jsonify(status='exception', msg=str(e)), 500)
|
||||||
|
|
||||||
return make_response(jsonify(status='ok'), 200)
|
return make_response(jsonify(status='ok'), 200)
|
||||||
|
|
||||||
|
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
@bp_jsconnector.route('project/delete/<project>', methods=['GET'])
|
@bp_jsconnector.route('/project/delete/<name>', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def api_delete_project(project: str):
|
def api_delete_project(name: str):
|
||||||
"""
|
"""
|
||||||
Deletes a project from the database and all associated data
|
Deletes a project from the database and all associated data
|
||||||
|
|
||||||
:param project: The name of the project
|
:param name: The name of the project
|
||||||
:return: A string with an error code and 'ok' as string on success.
|
:return: A string with an error code and 'ok' as string on success.
|
||||||
"""
|
"""
|
||||||
proj_id = get_id_from_project_name(project)
|
proj_id = get_id_from_project_name(name)
|
||||||
if proj_id == -1:
|
if proj_id == -1:
|
||||||
return make_response(jsonify(status='not-found'), 400)
|
return make_response(jsonify(status='not-found'), 400)
|
||||||
|
|
||||||
@ -92,3 +146,34 @@ def api_delete_project(project: str):
|
|||||||
return make_response(jsonify(status='exception'), 400)
|
return make_response(jsonify(status='exception'), 400)
|
||||||
|
|
||||||
return make_response(jsonify(status='ok'), 200)
|
return make_response(jsonify(status='ok'), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@cross_origin()
|
||||||
|
@bp_jsconnector.route('/project/exists/<name>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def api_project_exists(name: str):
|
||||||
|
proj_id = get_id_from_project_name(name)
|
||||||
|
if proj_id == -1:
|
||||||
|
return make_response(jsonify(status='not-found'), 200)
|
||||||
|
return make_response(jsonify(status='ok'), 200)
|
||||||
|
|
||||||
|
|
||||||
|
@cross_origin()
|
||||||
|
@bp_jsconnector.route('/project/get/<name>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def api_project_get_data(name: str):
|
||||||
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
|
if project:
|
||||||
|
return make_response(jsonify(status='ok',
|
||||||
|
id_project=project.id_project,
|
||||||
|
name=project.name,
|
||||||
|
blogurl=project.blogurl,
|
||||||
|
output=project.output,
|
||||||
|
sendotp=project.sendotp,
|
||||||
|
gravatar_cache=project.gravatar_cache,
|
||||||
|
gravatar_cache_dir=project.gravatar_cache_dir,
|
||||||
|
gravatar_size=project.gravatar_size,
|
||||||
|
addon_smileys=project.addon_smileys), 200)
|
||||||
|
else:
|
||||||
|
print('400')
|
||||||
|
return make_response(jsonify(status='not-found'), 400)
|
||||||
|
@ -8,7 +8,9 @@
|
|||||||
# *********************************************************************************/
|
# *********************************************************************************/
|
||||||
from flask import Blueprint, render_template, request, redirect, url_for
|
from flask import Blueprint, render_template, request, redirect, url_for
|
||||||
from flask_cors import cross_origin
|
from flask_cors import cross_origin
|
||||||
from labertasche.helper import check_auth, User
|
from labertasche.helper import User
|
||||||
|
from labertasche.settings import Credentials
|
||||||
|
from secrets import compare_digest
|
||||||
from flask_login import login_user, current_user, logout_user
|
from flask_login import login_user, current_user, logout_user
|
||||||
|
|
||||||
# Blueprint
|
# Blueprint
|
||||||
@ -30,7 +32,9 @@ def login():
|
|||||||
username = request.form['username']
|
username = request.form['username']
|
||||||
password = request.form['password']
|
password = request.form['password']
|
||||||
|
|
||||||
if check_auth(username, password):
|
credentials = Credentials()
|
||||||
|
if compare_digest(username.encode('utf8'), credentials.username.encode('utf8')) and \
|
||||||
|
credentials.compare_password(password):
|
||||||
login_user(User(0), remember=True)
|
login_user(User(0), remember=True)
|
||||||
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
return redirect(url_for('bp_dashboard.dashboard_project_list'))
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ from flask import render_template, jsonify, make_response
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from labertasche.database import labertasche_db as db
|
from labertasche.database import labertasche_db as db
|
||||||
from labertasche.models import TProjects, TComments, TLocation, TEmail, TVersion
|
from labertasche.models import TProjects, TComments, TLocation, TEmail, TVersion
|
||||||
from labertasche.helper import Settings
|
from labertasche.settings import LegacySettings
|
||||||
from json import dump, load
|
from json import dump, load
|
||||||
from shutil import copy, make_archive
|
from shutil import copy, make_archive
|
||||||
from re import search
|
from re import search
|
||||||
@ -128,7 +128,7 @@ def upgrade_db_to_v2_export():
|
|||||||
|
|
||||||
# Copy database
|
# Copy database
|
||||||
try:
|
try:
|
||||||
settings = Settings()
|
settings = LegacySettings(True)
|
||||||
db_uri = settings.system['database_uri']
|
db_uri = settings.system['database_uri']
|
||||||
if compare_digest(db_uri[0:6], "sqlite"):
|
if compare_digest(db_uri[0:6], "sqlite"):
|
||||||
m = search("([/]{3})(.*)", db_uri)
|
m = search("([/]{3})(.*)", db_uri)
|
||||||
@ -163,7 +163,7 @@ def upgrade_db_to_v2_recreate():
|
|||||||
@login_required
|
@login_required
|
||||||
def upgrade_db_to_v2_import():
|
def upgrade_db_to_v2_import():
|
||||||
path = get_backup_folder()
|
path = get_backup_folder()
|
||||||
settings = Settings()
|
settings = LegacySettings(True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# load location
|
# load location
|
||||||
|
@ -8,21 +8,19 @@
|
|||||||
# *********************************************************************************/
|
# *********************************************************************************/
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
from labertasche.models import TLocation, TComments, TProjects
|
|
||||||
from labertasche.settings import Settings
|
|
||||||
from labertasche.database import labertasche_db as db
|
|
||||||
from functools import wraps
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from flask import request
|
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin
|
||||||
from secrets import compare_digest
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sys import stderr
|
from sys import stderr
|
||||||
from re import match as re_match
|
from re import match as re_match
|
||||||
import requests
|
from labertasche.models import TLocation, TComments, TProjects
|
||||||
|
from labertasche.database import labertasche_db as db
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin):
|
class User(UserMixin):
|
||||||
|
"""
|
||||||
|
Class for flask-login, which represents a user
|
||||||
|
"""
|
||||||
def __init__(self, user_id):
|
def __init__(self, user_id):
|
||||||
self.id = user_id
|
self.id = user_id
|
||||||
|
|
||||||
@ -37,7 +35,7 @@ def is_valid_json(j):
|
|||||||
try:
|
try:
|
||||||
json.dumps(j)
|
json.dumps(j)
|
||||||
return True
|
return True
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError:
|
||||||
print("not valid json")
|
print("not valid json")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -77,60 +75,30 @@ def alchemy_query_to_dict(obj):
|
|||||||
|
|
||||||
# Come on, it's a mail hash, don't complain
|
# Come on, it's a mail hash, don't complain
|
||||||
# noinspection InsecureHash
|
# noinspection InsecureHash
|
||||||
def check_gravatar(email: str):
|
def check_gravatar(email: str, name: str):
|
||||||
"""
|
"""
|
||||||
Builds the gravatar email hash, which uses md5.
|
Builds the gravatar email hash, which uses md5.
|
||||||
You may use ?size=128 for example to dictate size in the final template.
|
You may use ?size=128 for example to dictate size in the final template.
|
||||||
:param email: the email to use for the hash
|
:param email: the email to use for the hash
|
||||||
|
:param name: The project name
|
||||||
:return: the gravatar url of the image
|
:return: the gravatar url of the image
|
||||||
"""
|
"""
|
||||||
settings = Settings()
|
from requests import get
|
||||||
options = settings.gravatar
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
gravatar_hash = md5(email.strip().lower().encode("utf8")).hexdigest()
|
gravatar_hash = md5(email.strip().lower().encode("utf8")).hexdigest()
|
||||||
if options['cache']:
|
|
||||||
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={options['size']}"
|
if project.gravatar_cache:
|
||||||
response = requests.get(url)
|
url = f"https://www.gravatar.com/avatar/{gravatar_hash}?s={project.gravatar_size}"
|
||||||
|
response = get(url)
|
||||||
if response.ok:
|
if response.ok:
|
||||||
outfile = Path(f"{options['static_dir']}/{gravatar_hash}.jpg")
|
outfile = Path(f"{project.gravatar_cache_dir}/{gravatar_hash}.jpg")
|
||||||
with outfile.open('wb') as fp:
|
with outfile.open('wb') as fp:
|
||||||
response.raw.decode_content = True
|
response.raw.decode_content = True
|
||||||
for chunk in response:
|
for chunk in response:
|
||||||
fp.write(chunk)
|
fp.write(chunk)
|
||||||
|
|
||||||
return gravatar_hash
|
return gravatar_hash
|
||||||
|
|
||||||
|
|
||||||
def check_auth(username: str, password: str):
|
|
||||||
"""
|
|
||||||
Compares username and password from the settings file in a safe way.
|
|
||||||
Direct string comparison is vulnerable to timing attacks
|
|
||||||
https://sqreen.github.io/DevelopersSecurityBestPractices/timing-attack/python
|
|
||||||
:param username: username entered by the user
|
|
||||||
:param password: password entered by the user
|
|
||||||
:return: True if equal, False if not
|
|
||||||
"""
|
|
||||||
settings = Settings()
|
|
||||||
if compare_digest(username, settings.dashboard['username']) and \
|
|
||||||
compare_digest(password, settings.dashboard['password']):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def basic_login_required(f):
|
|
||||||
"""
|
|
||||||
Decorator for basic auth
|
|
||||||
"""
|
|
||||||
@wraps(f)
|
|
||||||
def wrapped_view(**kwargs):
|
|
||||||
auth = request.authorization
|
|
||||||
if not (auth and check_auth(auth.username, auth.password)):
|
|
||||||
return ('Unauthorized', 401, {
|
|
||||||
'WWW-Authenticate': 'Basic realm="Login Required"'
|
|
||||||
})
|
|
||||||
return f(**kwargs)
|
|
||||||
return wrapped_view
|
|
||||||
|
|
||||||
|
|
||||||
def export_location(location_id: int) -> bool:
|
def export_location(location_id: int) -> bool:
|
||||||
"""
|
"""
|
||||||
Exports the comments for the location after the comment was accepted
|
Exports the comments for the location after the comment was accepted
|
||||||
@ -141,12 +109,28 @@ def export_location(location_id: int) -> bool:
|
|||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
# Query
|
# Query
|
||||||
loc_query = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
location = db.session.query(TLocation).filter(TLocation.id_location == location_id).first()
|
||||||
|
|
||||||
if loc_query:
|
if location:
|
||||||
comments = db.session.query(TComments).filter(TComments.is_spam != True) \
|
comments = db.session.query(TComments).filter(TComments.is_spam != True) \
|
||||||
.filter(TComments.is_published == True) \
|
.filter(TComments.is_published == True) \
|
||||||
.filter(TComments.location_id == loc_query.id_location)
|
.filter(TComments.location_id == location.id_location) \
|
||||||
|
.filter(TProjects.id_project == location.project_id) \
|
||||||
|
.all()
|
||||||
|
project = db.session.query(TProjects).filter(TProjects.id_project == location.project_id).first()
|
||||||
|
|
||||||
|
# Removes the last slash
|
||||||
|
path_loc = re_match(".*(?=/)", location.location)[0]
|
||||||
|
|
||||||
|
# Construct export path
|
||||||
|
jsonfile = Path(f"{project.output}/{path_loc}.json").absolute()
|
||||||
|
folder = jsonfile.parents[0]
|
||||||
|
|
||||||
|
# If there are no comments, do not export and remove empty file.
|
||||||
|
# The database is the point of trust.
|
||||||
|
if len(comments) == 0:
|
||||||
|
jsonfile.unlink(missing_ok=True)
|
||||||
|
return True
|
||||||
|
|
||||||
bundle = {
|
bundle = {
|
||||||
"comments": [],
|
"comments": [],
|
||||||
@ -158,20 +142,14 @@ def export_location(location_id: int) -> bool:
|
|||||||
continue
|
continue
|
||||||
bundle['comments'].append(alchemy_query_to_dict(comment))
|
bundle['comments'].append(alchemy_query_to_dict(comment))
|
||||||
|
|
||||||
path_loc = re_match(".*(?=/)", loc_query.location)[0]
|
# Create folder if not exists and write file
|
||||||
|
|
||||||
system = Settings().system
|
|
||||||
out = Path(f"{system['output']}/{path_loc}.json")
|
|
||||||
out = out.absolute()
|
|
||||||
folder = out.parents[0]
|
|
||||||
folder.mkdir(parents=True, exist_ok=True)
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
with out.open('w') as fp:
|
with jsonfile.open('w') as fp:
|
||||||
json.dump(bundle, fp)
|
json.dump(bundle, fp)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# mail(f"export_comments has thrown an error: {str(e)}")
|
|
||||||
print(e, file=stderr)
|
print(e, file=stderr)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -13,10 +13,11 @@ from pathlib import Path
|
|||||||
from platform import system
|
from platform import system
|
||||||
from smtplib import SMTP_SSL, SMTPHeloError, SMTPAuthenticationError, SMTPException
|
from smtplib import SMTP_SSL, SMTPHeloError, SMTPAuthenticationError, SMTPException
|
||||||
from ssl import create_default_context
|
from ssl import create_default_context
|
||||||
from labertasche.settings import Settings
|
|
||||||
from validate_email import validate_email_or_fail
|
from validate_email import validate_email_or_fail
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
|
from labertasche.models import TProjects
|
||||||
|
from labertasche.database import labertasche_db as db
|
||||||
|
from labertasche.settings import Settings
|
||||||
|
|
||||||
class mail:
|
class mail:
|
||||||
|
|
||||||
@ -61,24 +62,30 @@ class mail:
|
|||||||
except SMTPException as e:
|
except SMTPException as e:
|
||||||
print(f"SMTPException: {e}")
|
print(f"SMTPException: {e}")
|
||||||
|
|
||||||
def send_confirmation_link(self, email):
|
def send_confirmation_link(self, email: str, name: str) -> tuple:
|
||||||
"""
|
"""
|
||||||
Send confirmation link after entering a comment
|
Send confirmation link after entering a comment
|
||||||
:param email: The address to send the mail to
|
:param email: The address to send the mail to
|
||||||
|
:param name: The name of the project
|
||||||
:return: A tuple with the confirmation token and the deletion token, in this order
|
:return: A tuple with the confirmation token and the deletion token, in this order
|
||||||
"""
|
"""
|
||||||
|
project = db.session.query(TProjects).filter(TProjects.name == name).first()
|
||||||
|
if not project:
|
||||||
|
return None, None
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
confirm_digest = token_urlsafe(48)
|
confirm_digest = token_urlsafe(48)
|
||||||
delete_digest = token_urlsafe(48)
|
delete_digest = token_urlsafe(48)
|
||||||
|
|
||||||
confirm_url = f"{settings.system['web_url']}/comments/confirm/{confirm_digest}"
|
confirm_url = f"{settings.weburl}/comments/confirm/{confirm_digest}"
|
||||||
delete_url = f"{settings.system['web_url']}/comments/delete/{delete_digest}"
|
delete_url = f"{settings.weburl}/comments/delete/{delete_digest}"
|
||||||
|
|
||||||
txt_what = f"Hey there. You have made a comment on {settings.system['blog_url']}. Please confirm it by " \
|
txt_what = f"Hey there. You have made a comment on {project.blogurl}. Please confirm it by " \
|
||||||
f"copying this link into your browser:\n{confirm_url}\nIf you want to delete your comment for,"\
|
f"copying this link into your browser:\n{confirm_url}\nIf you want to delete your comment for,"\
|
||||||
f"whatever reason, please use this link:\n{delete_url}"
|
f"whatever reason, please use this link:\n{delete_url}"
|
||||||
|
|
||||||
html_what = f"Hey there. You have made a comment on {settings.system['blog_url']}.<br>Please confirm it by " \
|
html_what = f"Hey there. You have made a comment on {project.blogurl}.<br>Please confirm it by " \
|
||||||
f"clicking on this <a href='{confirm_url}'>link</a>.<br>"\
|
f"clicking on this <a href='{confirm_url}'>link</a>.<br>"\
|
||||||
f"In case you want to delete your comment later, please click <a href='{delete_url}'>here</a>."\
|
f"In case you want to delete your comment later, please click <a href='{delete_url}'>here</a>."\
|
||||||
f"<br><br>If you think this is in error or someone made this comment in your name, please "\
|
f"<br><br>If you think this is in error or someone made this comment in your name, please "\
|
||||||
|
@ -19,8 +19,8 @@ class TProjects(db.Model):
|
|||||||
|
|
||||||
# data
|
# data
|
||||||
name = db.Column(db.Text, nullable=False, unique=True)
|
name = db.Column(db.Text, nullable=False, unique=True)
|
||||||
blogurl = db.Column(db.Text, nullable=False)
|
blogurl = db.Column(db.Text, nullable=False, unique=False)
|
||||||
output = db.Column(db.Text, nullable=False)
|
output = db.Column(db.Text, nullable=False, unique=True)
|
||||||
sendotp = db.Column(db.Boolean, nullable=False)
|
sendotp = db.Column(db.Boolean, nullable=False)
|
||||||
|
|
||||||
gravatar_cache = db.Column(db.Boolean, nullable=False)
|
gravatar_cache = db.Column(db.Boolean, nullable=False)
|
||||||
|
@ -9,12 +9,29 @@
|
|||||||
import yaml
|
import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from platform import system
|
from platform import system
|
||||||
|
from shutil import copy
|
||||||
|
from hashlib import pbkdf2_hmac
|
||||||
|
from secrets import compare_digest
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password, secret=None):
|
||||||
|
"""
|
||||||
|
Hashes the administrator password
|
||||||
|
:param password: The password to hash
|
||||||
|
:param secret: The site secret
|
||||||
|
:return: The hashed value as a hexadecimal string
|
||||||
|
"""
|
||||||
|
if not secret:
|
||||||
|
secret = Secret()
|
||||||
|
h = pbkdf2_hmac('sha512',
|
||||||
|
password=password.encode('utf8'),
|
||||||
|
salt=secret.key.encode('utf8'),
|
||||||
|
iterations=250000)
|
||||||
|
return h.hex()
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
class Settings:
|
||||||
"""
|
|
||||||
Automatically loads the settings from /etc/ on Linux and same directory on other OS
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
file = Path("labertasche.yaml")
|
file = Path("labertasche.yaml")
|
||||||
if system().lower() == "linux":
|
if system().lower() == "linux":
|
||||||
@ -23,9 +40,133 @@ class Settings:
|
|||||||
with file.open('r') as fp:
|
with file.open('r') as fp:
|
||||||
conf = yaml.safe_load(fp)
|
conf = yaml.safe_load(fp)
|
||||||
|
|
||||||
|
self.weburl = conf['system']['weburl']
|
||||||
|
self.cookie_domain = conf['system']['cookie_domain']
|
||||||
|
self.database_uri = conf['system']['database_uri']
|
||||||
|
self.debug = conf['system']['debug']
|
||||||
|
self.cookie_secure = conf['system']['cookie_secure']
|
||||||
|
|
||||||
|
|
||||||
|
class Secret:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
file = Path(".secret")
|
||||||
|
if system().lower() == "linux":
|
||||||
|
file = Path("/etc/labertasche/.secret")
|
||||||
|
|
||||||
|
with file.open('r') as fp:
|
||||||
|
self.key = fp.readline()
|
||||||
|
|
||||||
|
|
||||||
|
class Smileys:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
file = Path("smileys.yaml")
|
||||||
|
if system().lower() == "linux":
|
||||||
|
file = Path("/etc/labertasche/smileys.yaml")
|
||||||
|
|
||||||
|
with file.open('r') as fp:
|
||||||
|
conf = yaml.safe_load(fp)
|
||||||
|
|
||||||
|
self.smileys = conf['smileys']
|
||||||
|
|
||||||
|
|
||||||
|
class Credentials:
|
||||||
|
def __init__(self):
|
||||||
|
file = Path("credentials.yaml")
|
||||||
|
if system().lower() == "linux":
|
||||||
|
file = Path("/etc/labertasche/credentials.yaml")
|
||||||
|
|
||||||
|
with file.open('r') as fp:
|
||||||
|
conf = yaml.safe_load(fp)
|
||||||
|
|
||||||
|
self.username = conf['credentials']['username']
|
||||||
|
self.password = conf['credentials']['password']
|
||||||
|
|
||||||
|
def compare_password(self, userinput):
|
||||||
|
"""
|
||||||
|
Compares 2 passwords with one another
|
||||||
|
|
||||||
|
:param userinput: The input on the login page
|
||||||
|
:return: True if the passwords match, otherwise False
|
||||||
|
"""
|
||||||
|
secret = Secret()
|
||||||
|
hashed = pbkdf2_hmac('sha512',
|
||||||
|
password=userinput.encode('utf8'),
|
||||||
|
salt=secret.key.encode('utf8'),
|
||||||
|
iterations=250000)
|
||||||
|
return compare_digest(self.password, hashed.hex())
|
||||||
|
|
||||||
|
|
||||||
|
# deprecated, leave as is
|
||||||
|
class LegacySettings:
|
||||||
|
"""
|
||||||
|
Automatically loads the settings from /etc/ on Linux and same directory on other OS
|
||||||
|
"""
|
||||||
|
def __init__(self, use_backup: bool = False):
|
||||||
|
file = Path("labertasche.yaml")
|
||||||
|
if system().lower() == "linux":
|
||||||
|
file = Path("/etc/labertasche/labertasche.yaml")
|
||||||
|
|
||||||
|
# Use backup when conversion is done, this is used in db_v2.py
|
||||||
|
if use_backup:
|
||||||
|
file = file.with_suffix('.bak')
|
||||||
|
|
||||||
|
with file.open('r') as fp:
|
||||||
|
conf = yaml.safe_load(fp)
|
||||||
|
|
||||||
self.system = conf['system']
|
self.system = conf['system']
|
||||||
|
self.smileys = conf['smileys']
|
||||||
self.dashboard = conf['dashboard']
|
self.dashboard = conf['dashboard']
|
||||||
self.gravatar = conf['gravatar']
|
self.gravatar = conf['gravatar']
|
||||||
self.addons = conf['addons']
|
self.addons = conf['addons']
|
||||||
self.smileys = conf['smileys']
|
|
||||||
|
|
||||||
|
def convert_to_v2(self):
|
||||||
|
old = Path("labertasche.yaml")
|
||||||
|
if system().lower() == "linux":
|
||||||
|
old = Path("/etc/labertasche/labertasche.yaml")
|
||||||
|
|
||||||
|
systemvars = {
|
||||||
|
'system': {
|
||||||
|
'weburl': self.system['web_url'],
|
||||||
|
'cookie_domain': self.system['cookie_domain'],
|
||||||
|
'database_uri': self.system['database_uri'],
|
||||||
|
'debug': self.system['debug'],
|
||||||
|
'cookie_secure': self.system['cookie_secure']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
'credentials': {
|
||||||
|
'username': self.dashboard['username'],
|
||||||
|
'password': hash_password(self.dashboard['password'], self.system['secret'])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
smileys = {
|
||||||
|
'smileys': self.smileys
|
||||||
|
}
|
||||||
|
|
||||||
|
# backup old config
|
||||||
|
copy(old, old.with_suffix('.bak'))
|
||||||
|
|
||||||
|
# Write new config files
|
||||||
|
p_sys = Path('labertasche.yaml')
|
||||||
|
p_credentials = Path('credentials.yaml')
|
||||||
|
p_smileys = Path('smileys.yaml')
|
||||||
|
p_secret = Path('.secret')
|
||||||
|
|
||||||
|
if system().lower() == 'linux':
|
||||||
|
p_sys = '/etc/labertasche/' / p_sys
|
||||||
|
p_credentials = '/etc/labertasche/' / p_credentials
|
||||||
|
p_smileys = '/etc/labertasche/' / p_smileys
|
||||||
|
p_secret = '/etc/labertasche/' / p_secret
|
||||||
|
|
||||||
|
with p_sys.open('w') as fp:
|
||||||
|
yaml.dump(systemvars, fp)
|
||||||
|
with p_credentials.open('w') as fp:
|
||||||
|
yaml.dump(credentials, fp)
|
||||||
|
with p_smileys.open('w') as fp:
|
||||||
|
yaml.dump(smileys, fp)
|
||||||
|
with p_secret.open('w') as fp:
|
||||||
|
fp.write(self.system['secret'])
|
||||||
|
37
server.py
37
server.py
@ -6,13 +6,13 @@
|
|||||||
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
# * _repo : https://git.tuxstash.de/gothseidank/labertasche
|
||||||
# * _license : This project is under MIT License
|
# * _license : This project is under MIT License
|
||||||
# *********************************************************************************/
|
# *********************************************************************************/
|
||||||
import logging
|
# noinspection PyProtectedMember
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
from logging import getLogger, ERROR as LOGGING_ERROR
|
||||||
from flask import Flask, redirect, url_for
|
from flask import Flask, redirect, url_for
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from sqlalchemy import event, inspect
|
from sqlalchemy import event, inspect
|
||||||
# noinspection PyProtectedMember
|
from labertasche.settings import Settings, Secret
|
||||||
from sqlalchemy.engine import Engine
|
|
||||||
from labertasche.settings import Settings
|
|
||||||
from labertasche.database import labertasche_db
|
from labertasche.database import labertasche_db
|
||||||
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector, bp_dbupgrades
|
from labertasche.blueprints import bp_comments, bp_login, bp_dashboard, bp_jsconnector, bp_dbupgrades
|
||||||
from labertasche.helper import User
|
from labertasche.helper import User
|
||||||
@ -21,28 +21,29 @@ from datetime import timedelta
|
|||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
secret = Secret()
|
||||||
|
|
||||||
# Flask App
|
# Flask App
|
||||||
laberflask = Flask(__name__)
|
laberflask = Flask(__name__)
|
||||||
laberflask.config.update(dict(
|
laberflask.config.update(dict(
|
||||||
SESSION_COOKIE_DOMAIN=settings.system['cookie_domain'],
|
SESSION_COOKIE_DOMAIN=settings.cookie_domain,
|
||||||
SESSION_COOKIE_SECURE=settings.system['cookie_secure'],
|
SESSION_COOKIE_SECURE=settings.cookie_secure,
|
||||||
REMEMBER_COOKIE_SECURE=settings.system['cookie_secure'],
|
REMEMBER_COOKIE_SECURE=settings.cookie_secure,
|
||||||
REMEMBER_COOKIE_DURATION=timedelta(days=7),
|
REMEMBER_COOKIE_DURATION=timedelta(days=7),
|
||||||
REMEMBER_COOKIE_HTTPONLY=True,
|
REMEMBER_COOKIE_HTTPONLY=True,
|
||||||
REMEMBER_COOKIE_REFRESH_EACH_REQUEST=True,
|
REMEMBER_COOKIE_REFRESH_EACH_REQUEST=True,
|
||||||
DEBUG=settings.system['debug'],
|
DEBUG=settings.debug,
|
||||||
SECRET_KEY=settings.system['secret'],
|
SECRET_KEY=secret.key,
|
||||||
TEMPLATES_AUTO_RELOAD=settings.system['debug'],
|
TEMPLATES_AUTO_RELOAD=settings.debug,
|
||||||
SQLALCHEMY_DATABASE_URI=settings.system['database_uri'],
|
SQLALCHEMY_DATABASE_URI=settings.database_uri,
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
SQLALCHEMY_TRACK_MODIFICATIONS=False
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Mark secret for deletion
|
||||||
|
del secret
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
CORS(laberflask, resources={r"/comments": {"origins": settings.system['blog_url']},
|
cors = CORS(laberflask)
|
||||||
r"/api": {"origins": settings.system['web_url']},
|
|
||||||
r"/dashboard": {"origins": settings.system['web_url']},
|
|
||||||
})
|
|
||||||
|
|
||||||
# Import blueprints
|
# Import blueprints
|
||||||
laberflask.register_blueprint(bp_comments)
|
laberflask.register_blueprint(bp_comments)
|
||||||
@ -52,8 +53,8 @@ laberflask.register_blueprint(bp_jsconnector)
|
|||||||
laberflask.register_blueprint(bp_dbupgrades)
|
laberflask.register_blueprint(bp_dbupgrades)
|
||||||
|
|
||||||
# Disable Werkzeug's verbosity during development
|
# Disable Werkzeug's verbosity during development
|
||||||
log = logging.getLogger('werkzeug')
|
log = getLogger('werkzeug')
|
||||||
log.setLevel(logging.ERROR)
|
log.setLevel(LOGGING_ERROR)
|
||||||
|
|
||||||
# Set up login manager
|
# Set up login manager
|
||||||
loginmgr = LoginManager(laberflask)
|
loginmgr = LoginManager(laberflask)
|
||||||
@ -87,7 +88,7 @@ def login_invalid():
|
|||||||
# noinspection PyUnusedLocal
|
# noinspection PyUnusedLocal
|
||||||
@event.listens_for(Engine, "connect")
|
@event.listens_for(Engine, "connect")
|
||||||
def set_sqlite_pragma(dbapi_connection, connection_record):
|
def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||||
if settings.system["database_uri"][0:6] == 'sqlite':
|
if settings.database_uri[0:6] == 'sqlite':
|
||||||
cursor = dbapi_connection.cursor()
|
cursor = dbapi_connection.cursor()
|
||||||
cursor.execute("PRAGMA journal_mode=WAL;")
|
cursor.execute("PRAGMA journal_mode=WAL;")
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
@ -5,6 +5,48 @@
|
|||||||
// # * _license : This project is under MIT License
|
// # * _license : This project is under MIT License
|
||||||
// # *********************************************************************************/
|
// # *********************************************************************************/
|
||||||
|
|
||||||
|
async function get(partial, callback) {
|
||||||
|
await fetch(window.location.protocol + "//" + window.location.host + partial,
|
||||||
|
{
|
||||||
|
mode: "cors",
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': window.location.host,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "GET"
|
||||||
|
})
|
||||||
|
.then(async function (response) {
|
||||||
|
let result = await response.json();
|
||||||
|
callback(result);
|
||||||
|
})
|
||||||
|
.catch(function (exc) {
|
||||||
|
console.log(exc);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(partial, stringified_json, callback) {
|
||||||
|
await fetch(window.location.protocol + "//" + window.location.host + partial,
|
||||||
|
{
|
||||||
|
mode: "cors",
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': window.location.host,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
body: stringified_json
|
||||||
|
})
|
||||||
|
.then(async function (response) {
|
||||||
|
let result = await response.json();
|
||||||
|
callback(result);
|
||||||
|
})
|
||||||
|
.catch(function (exc) {
|
||||||
|
console.log(exc);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Called when search for mail addresses in manage mail
|
// Called when search for mail addresses in manage mail
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
@ -24,74 +66,15 @@ function dashboard_mailsearch(search_txt)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Called when a new project is created,
|
// Deletes a project from the db
|
||||||
// posts it to the server
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
function new_project_save() {
|
async function project_delete()
|
||||||
let modal_ok = document.getElementById('modal-ok');
|
|
||||||
let modal_cancel = document.getElementById('modal-cancel');
|
|
||||||
let short_help_short = document.getElementById('new-project-too-short');
|
|
||||||
let short_help_invalid = document.getElementById('new-project-invalid-name');
|
|
||||||
let name = document.getElementById('project-name').value
|
|
||||||
|
|
||||||
short_help_short.classList.add('is-hidden');
|
|
||||||
short_help_invalid.classList.add('is-hidden');
|
|
||||||
|
|
||||||
// Validate input
|
|
||||||
if (name.length === 0) {
|
|
||||||
short_help_short.classList.remove('is-hidden');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (/^\w+$/.test(name) === false){
|
|
||||||
short_help_invalid.classList.remove('is-hidden');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
modal_ok.classList.add('is-loading');
|
|
||||||
modal_cancel.classList.add('is-hidden');
|
|
||||||
fetch(window.location.protocol + "//" + window.location.host + '/api/project/new',
|
|
||||||
{
|
|
||||||
mode: "cors",
|
|
||||||
headers: {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
// use real location
|
|
||||||
body: JSON.stringify({
|
|
||||||
"name": name
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.then(async function (response) {
|
|
||||||
let result = await response.json();
|
|
||||||
result = result['status'];
|
|
||||||
modal_ok.classList.remove('is-loading');
|
|
||||||
modal_cancel.classList.remove('is-hidden');
|
|
||||||
if (result === "ok"){
|
|
||||||
hide_modal('modal-new-project');
|
|
||||||
window.location.reload(true);
|
|
||||||
}
|
|
||||||
if (result === "too-short"){
|
|
||||||
short_help_short.classList.remove('is-hidden');
|
|
||||||
}
|
|
||||||
if (result === "invalid-name"){
|
|
||||||
short_help_invalid.classList.remove('is-hidden');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (exc) {
|
|
||||||
console.log(exc);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function project_delete()
|
|
||||||
{
|
{
|
||||||
let modal = document.getElementById('modal-project-delete');
|
let modal = document.getElementById('modal-project-delete');
|
||||||
let modal_ok = document.getElementById('modal-delete-ok');
|
let modal_ok = document.getElementById('modal-delete-ok');
|
||||||
let modal_cancel = document.getElementById('modal-delete-cancel');
|
let modal_cancel = document.getElementById('modal-delete-cancel');
|
||||||
|
|
||||||
const project = modal.dataset.project;
|
const project = modal.dataset.name;
|
||||||
console.log("Project: " + project);
|
|
||||||
if (project === null || project.length === 0){
|
if (project === null || project.length === 0){
|
||||||
console.log("Couldn't find a valid dataset");
|
console.log("Couldn't find a valid dataset");
|
||||||
return;
|
return;
|
||||||
@ -99,58 +82,253 @@ function project_delete()
|
|||||||
|
|
||||||
modal_ok.classList.add('is-loading');
|
modal_ok.classList.add('is-loading');
|
||||||
modal_cancel.classList.add('is-hidden');
|
modal_cancel.classList.add('is-hidden');
|
||||||
fetch(window.location.protocol + "//" + window.location.host + '/api/project/delete/' + project,
|
|
||||||
{
|
await get('/api/project/delete/' + project, function(result){
|
||||||
mode: "cors",
|
|
||||||
headers: {
|
|
||||||
'Access-Control-Allow-Origin': '*',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
method: "GET"
|
|
||||||
})
|
|
||||||
.then(async function (response) {
|
|
||||||
let result = await response.json();
|
|
||||||
result = result['status'];
|
|
||||||
modal_ok.classList.remove('is-loading');
|
modal_ok.classList.remove('is-loading');
|
||||||
modal_cancel.classList.remove('is-hidden');
|
modal_cancel.classList.remove('is-hidden');
|
||||||
if (result === "ok") {
|
if (result === "ok") {
|
||||||
hide_modal('modal-project-delete');
|
console.log(result);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
modal.classList.remove('is-active');
|
||||||
window.location.reload(true);
|
window.location.reload(true);
|
||||||
}
|
}
|
||||||
console.log(result);
|
|
||||||
})
|
|
||||||
.catch(function (exc) {
|
|
||||||
console.log(exc);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Hides any modal
|
// Hides any modal
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
function hide_modal(id_name)
|
function hide_modal(id_name, redirect=null)
|
||||||
{
|
{
|
||||||
let el = document.getElementById(id_name);
|
let el = document.getElementById(id_name);
|
||||||
el.classList.remove("is-active");
|
el.classList.remove("is-active");
|
||||||
|
|
||||||
|
if(redirect != null){
|
||||||
|
window.location = redirect;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Shows any modal
|
// Shows any modal and attach project name
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
function show_modal(id_name)
|
function show_modal(id_name, proj_name)
|
||||||
{
|
{
|
||||||
let el = document.getElementById(id_name);
|
let el = document.getElementById(id_name);
|
||||||
el.classList.add("is-active");
|
el.classList.add("is-active");
|
||||||
|
el.setAttribute('data-name', proj_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
// Shows any modal
|
// Shows edit/new modal
|
||||||
// ------------------------------------------------------
|
// ------------------------------------------------------
|
||||||
function show_modal_with_project(id_name, proj_name)
|
async function show_modal_with_project(id_name, proj_name)
|
||||||
{
|
{
|
||||||
let el = document.getElementById(id_name);
|
// Get title element
|
||||||
el.classList.add("is-active");
|
let title = document.getElementById('modal-title');
|
||||||
el.setAttribute('data-project', proj_name)
|
|
||||||
|
// Get Dialog
|
||||||
|
let modal = document.getElementById(id_name);
|
||||||
|
|
||||||
|
if (proj_name){
|
||||||
|
// Get Data
|
||||||
|
await get('/api/project/get/' + proj_name,
|
||||||
|
function(r){
|
||||||
|
document.getElementById('edit-project-blog-url').value = r['blogurl'];
|
||||||
|
document.getElementById('edit-project-output').value = r['output'];
|
||||||
|
document.getElementById('edit-project-gravatar-cache').checked = r['gravatar_cache'];
|
||||||
|
document.getElementById('edit-project-gravatar-cache-dir').value = r['gravatar_cache_dir'];
|
||||||
|
document.getElementById('edit-project-gravatar-size').value = r['gravatar_size'];
|
||||||
|
document.getElementById('edit-project-send-otp').checked = r['sendotp'];
|
||||||
|
document.getElementById('edit-project-addons-smileys').checked = r['addon_smileys'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set project name
|
||||||
|
let proj_el = document.getElementById('edit-project-name');
|
||||||
|
proj_el.value = proj_name
|
||||||
|
|
||||||
|
// Set project name
|
||||||
|
title.innerText = "Edit project '" + proj_name + "'";
|
||||||
|
|
||||||
|
// Make active
|
||||||
|
modal.classList.add("is-active");
|
||||||
|
|
||||||
|
// Edit mode
|
||||||
|
modal.setAttribute('data-mode', 'edit');
|
||||||
|
modal.setAttribute('data-name', proj_name);
|
||||||
|
}
|
||||||
|
if (proj_name == null){
|
||||||
|
// Set project name
|
||||||
|
title.innerText = "New Project";
|
||||||
|
|
||||||
|
// Reset fields, needed when user pressed cancel on edit modal
|
||||||
|
document.getElementById('edit-project-name').value = "";
|
||||||
|
document.getElementById('edit-project-blog-url').value = "";
|
||||||
|
document.getElementById('edit-project-output').value = "";
|
||||||
|
document.getElementById('edit-project-gravatar-cache').checked = true;
|
||||||
|
document.getElementById('edit-project-gravatar-cache-dir').value = "";
|
||||||
|
document.getElementById('edit-project-gravatar-size').value = 256;
|
||||||
|
document.getElementById('edit-project-send-otp').checked = true;
|
||||||
|
document.getElementById('edit-project-addons-smileys').checked = true;
|
||||||
|
|
||||||
|
// Edit mode
|
||||||
|
modal.setAttribute('data-mode', 'new');
|
||||||
|
|
||||||
|
// Make active
|
||||||
|
modal.classList.add("is-active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save_project_settings(id_name)
|
||||||
|
{
|
||||||
|
// Spin the tea cups
|
||||||
|
let btn = document.getElementById('modal-save-ok');
|
||||||
|
btn.classList.add('is-loading');
|
||||||
|
|
||||||
|
// Get modal
|
||||||
|
let modal = document.getElementById(id_name);
|
||||||
|
|
||||||
|
// Get field data
|
||||||
|
let json_data = {
|
||||||
|
"name": document.getElementById('edit-project-name').value,
|
||||||
|
"blogurl": document.getElementById('edit-project-blog-url').value,
|
||||||
|
"output": document.getElementById('edit-project-output').value,
|
||||||
|
"gravatar_cache": document.getElementById('edit-project-gravatar-cache').checked,
|
||||||
|
"gravatar_cache_dir": document.getElementById('edit-project-gravatar-cache-dir').value,
|
||||||
|
"gravatar_size": document.getElementById('edit-project-gravatar-size').value,
|
||||||
|
"sendotp": document.getElementById('edit-project-send-otp').checked,
|
||||||
|
"addon_smileys": document.getElementById('edit-project-addons-smileys').checked
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modal.dataset.mode === "edit"){
|
||||||
|
let old_name = modal.dataset.name;
|
||||||
|
|
||||||
|
await post('/api/project/edit/' + old_name, JSON.stringify(json_data), function(result){
|
||||||
|
let error = document.getElementById('modal-edit-error-messages')
|
||||||
|
error.innerText = ''
|
||||||
|
|
||||||
|
if (result['status'] === 'too-short'){
|
||||||
|
error.innerText = "A required field has been left empty!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-project-name') {
|
||||||
|
error.innerText = "The project name is not valid. Please only use alphanumeric characters!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'project-exists') {
|
||||||
|
error.innerText = "A project with this name already exists!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-blog-url') {
|
||||||
|
error.innerText = "The blog-url is invalid!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-path-output') {
|
||||||
|
error.innerText = "This output path does not exist!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-path-cache') {
|
||||||
|
error.innerText = "The cache path does not exist!"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'exception') {
|
||||||
|
error.innerText = "There was an unexpected exception. Please report this to contact@tuxstash.de:"
|
||||||
|
error.innerText += result['msg']
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
window.location.reload(true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (modal.dataset.mode === 'new'){
|
||||||
|
await post('/api/project/new', JSON.stringify(json_data), function(result){
|
||||||
|
let error = document.getElementById('modal-edit-error-messages')
|
||||||
|
error.innerText = '';
|
||||||
|
|
||||||
|
console.log(result['status']);
|
||||||
|
if (result['status'] === 'too-short'){
|
||||||
|
error.innerText = "A required field has been left empty!";
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-project-name') {
|
||||||
|
error.innerText = "The project name is not valid. Please only use alphanumeric characters!";
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'project-exists') {
|
||||||
|
error.innerText = "A project with this name already exists!";
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-blog-url') {
|
||||||
|
error.innerText = "The blog-url is invalid!";
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-path-output') {
|
||||||
|
error.innerText = "This output path does not exist!";
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'invalid-path-cache') {
|
||||||
|
error.innerText = "The cache path does not exist!";
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result['status'] === 'exception') {
|
||||||
|
error.innerText = "There was an unexpected exception. Please report this to contact@tuxstash.de:";
|
||||||
|
error.innerText += result['msg'];
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset button
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
window.location.reload(true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Disables inputs when gravatar caching is disabled.
|
||||||
|
// ------------------------------------------------------
|
||||||
|
function toggle_gravatar_settings(chkbx)
|
||||||
|
{
|
||||||
|
let cache = document.getElementById('edit-project-gravatar-cache-dir');
|
||||||
|
let size = document.getElementById('edit-project-gravatar-size');
|
||||||
|
|
||||||
|
if(!chkbx.checked){
|
||||||
|
cache.setAttribute('disabled', '');
|
||||||
|
size.setAttribute('disabled', '');
|
||||||
|
cache.value = "disabled";
|
||||||
|
size.value = "disabled";
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
cache.removeAttribute('disabled');
|
||||||
|
size.removeAttribute('disabled');
|
||||||
|
cache.value = "";
|
||||||
|
size.value = "256";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// Exports all comments
|
||||||
|
// ------------------------------------------------------
|
||||||
|
async function export_all_comments(btn)
|
||||||
|
{
|
||||||
|
btn.classList.add('is-loading');
|
||||||
|
let proj_name = document.getElementById('modal-comments-export').dataset.name;
|
||||||
|
|
||||||
|
await get('/api/comment-export-all/' + proj_name, function(result){
|
||||||
|
if (result['status'] === 'ok'){
|
||||||
|
hide_modal('modal-comments-export');
|
||||||
|
}
|
||||||
|
if (result['status'] === 'not-found'){
|
||||||
|
// Redirect to error
|
||||||
|
hide_modal('modal-comments-export', '?error=404');
|
||||||
|
}
|
||||||
|
btn.classList.remove('is-loading');
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,6 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||||
<meta name="description" content="labertasche comment system dashboard">
|
<meta name="description" content="labertasche comment system dashboard">
|
||||||
|
|
||||||
<!-- Preload -->
|
|
||||||
<link rel="preload" href="/static/css/open-sans-v18-latin-regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" media="all">
|
|
||||||
<link rel="preload" href="/static/css/materialdesignicons-webfont.woff2" as="font" type="font/woff2" crossorigin="anonymous" media="all">
|
|
||||||
<link rel="preload" href="/static/css/labertasche.css" as="style" crossorigin="anonymous" media="screen">
|
|
||||||
<link rel="preload" href="/static/css/materialdesignicons.min.css" as="style" crossorigin="anonymous" media="screen">
|
|
||||||
<link rel="preload" href="/static/css/Chart.min.css" as="style" media="screen">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/static/css/labertasche.css" media="screen">
|
<link rel="stylesheet" href="/static/css/labertasche.css" media="screen">
|
||||||
<link rel="stylesheet" href="/static/css/materialdesignicons.min.css" media="screen">
|
<link rel="stylesheet" href="/static/css/materialdesignicons.min.css" media="screen">
|
||||||
<link rel="stylesheet" href="/static/css/Chart.min.css" media="screen">
|
<link rel="stylesheet" href="/static/css/Chart.min.css" media="screen">
|
||||||
|
@ -4,15 +4,9 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
|
||||||
<meta name="description" content="labertasche comment system dashboard">
|
<meta name="description" content="labertasche comment system dashboard">
|
||||||
<link rel="preconnect" href="https://cdn.materialdesignicons.com/">
|
|
||||||
|
|
||||||
<!-- Preload -->
|
|
||||||
<link rel="preload" href="/static/css/open-sans-v18-latin-regular.woff2" as="font" type="font/woff2" crossorigin="anonymous" media="all">
|
|
||||||
<link rel="preload" href="https://cdn.materialdesignicons.com/5.4.55/css/materialdesignicons.min.css" as="style">
|
|
||||||
<link rel="preload" as="style" href="/static/css/labertasche.css">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/5.4.55/css/materialdesignicons.min.css">
|
|
||||||
<link rel="stylesheet" href="/static/css/labertasche.css" media="screen">
|
<link rel="stylesheet" href="/static/css/labertasche.css" media="screen">
|
||||||
|
<link rel="stylesheet" href="/static/css/materialdesignicons.min.css" media="screen">
|
||||||
|
<link rel="stylesheet" href="/static/css/Chart.min.css" media="screen">
|
||||||
|
|
||||||
<title>labertasche Dashboard</title>
|
<title>labertasche Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
|
22
templates/modals/comments-export-all.html
Normal file
22
templates/modals/comments-export-all.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<div class="modal" id="modal-comments-export">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head has-background-warning">
|
||||||
|
<p class="modal-card-title has-text-black is-uppercase">Export all comments</p>
|
||||||
|
<button onclick="hide_modal('modal-comments-export')" class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body has-text-white bg-deepmatte">
|
||||||
|
<p class="block">This will export all comments of this project to all locations.
|
||||||
|
Usually this is not needed, but can be helpful, if you have imported backups or similar.</p>
|
||||||
|
<p class="block has-text-danger">Do you wish to proceed?</p>
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot bg-deepmatte">
|
||||||
|
<button id="modal-ok" onclick="export_all_comments(this);" class="button is-success">
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
<button id="modal-cancel" onclick="hide_modal('modal-comments-export')" class="button is-info">
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
21
templates/modals/project-delete.html
Normal file
21
templates/modals/project-delete.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<div class="modal" id="modal-project-delete">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head has-background-warning">
|
||||||
|
<p class="modal-card-title has-text-black">WARNING!</p>
|
||||||
|
<button onclick="hide_modal('modal-project-delete')" class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body has-text-black">
|
||||||
|
You are about to delete a project. All associated data will be unrecoverably lost!
|
||||||
|
Please perform a manual sql dump if you would like to retain that data.
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<button id="modal-delete-ok" onclick="project_delete()" class="button is-danger">
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
<button id="modal-delete-cancel" onclick="hide_modal('modal-project-delete')" class="button is-success">
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
109
templates/modals/project_edit.html
Normal file
109
templates/modals/project_edit.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<div class="modal" id="modal-project-edit">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head has-background-warning">
|
||||||
|
<p id="modal-title" class="modal-card-title has-text-black">Edit Project</p>
|
||||||
|
<button onclick="hide_modal('modal-project-edit')" class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body has-text-black bg-deepmatte">
|
||||||
|
<form>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control has-icons-left"
|
||||||
|
data-tippy-content="Please select an unique name for your project.">
|
||||||
|
<input class="input" id="edit-project-name"
|
||||||
|
name="edit-project-name" type="text">
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label class="label help has-text-white" for="edit-project-name">
|
||||||
|
Project Name
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control has-icons-left"
|
||||||
|
data-tippy-content="URL of your hugo site, e.g. https://example.com">
|
||||||
|
<input class="input" id="edit-project-blog-url" name="edit-project-blog-url" type="text">
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label class="label help has-text-white" for="edit-project-blog-url">
|
||||||
|
URL of your Hugo site for this project
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control has-icons-left"
|
||||||
|
data-tippy-content="The path to the data directory of your Hugo installation.">
|
||||||
|
<input class="input" id="edit-project-output" name="edit-project-output" type="text">
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label class="label help has-text-white" for="edit-project-output">Output Directory</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox help has-text-white" for="edit-project-gravatar-cache">
|
||||||
|
<input id="edit-project-gravatar-cache" class="checkbox" type="checkbox"
|
||||||
|
name="edit-project-gravatar-cache" checked onclick="toggle_gravatar_settings(this);">
|
||||||
|
Cache Gravatar images?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control has-icons-left"
|
||||||
|
data-tippy-content="The directory where to save the gravatar images. Should be a full path.">
|
||||||
|
<input class="input" id="edit-project-gravatar-cache-dir" name="edit-project-gravatar-cache-dir" type="text">
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label class="label help has-text-white" for="edit-project-gravatar-cache-dir">
|
||||||
|
Gravatar Cache Directory
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control has-icons-left"
|
||||||
|
data-tippy-content="The numeric size of the images to download. Must be a power of 2.">
|
||||||
|
<input class="input" id="edit-project-gravatar-size" name="edit-project-gravatar-size" type="text" value="256">
|
||||||
|
<span class="icon is-small is-left">
|
||||||
|
<i class="mdi mdi-24px mdi-help-rhombus-outline"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label class="label help has-text-white" for="edit-project-gravatar-size">
|
||||||
|
Gravatar image size
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox help has-text-white" for="edit-project-send-otp">
|
||||||
|
<input id="edit-project-send-otp" class="checkbox" type="checkbox"
|
||||||
|
name="edit-project-send-otp" checked>
|
||||||
|
Send OTP to publish?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="control">
|
||||||
|
<label class="checkbox help has-text-white" for="edit-project-addons-smileys">
|
||||||
|
<input id="edit-project-addons-smileys" class="checkbox" type="checkbox"
|
||||||
|
name="edit-project-addons-smileys" checked>
|
||||||
|
Enable Smiley Addon?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="label has-text-danger" id="modal-edit-error-messages">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot bg-deepmatte">
|
||||||
|
<button id="modal-save-ok" onclick="save_project_settings('modal-project-edit');" class="button is-success">
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
<button onclick="hide_modal('modal-project-edit')" class="button is-info">
|
||||||
|
CANCEL
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
24
templates/modals/project_not_found.html
Normal file
24
templates/modals/project_not_found.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<div class="modal" id="modal-project-not-found">
|
||||||
|
<div class="modal-background"></div>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head has-background-danger">
|
||||||
|
<p class="modal-card-title has-text-white">ERROR</p>
|
||||||
|
<button onclick="hide_modal('modal-project-not-found')" class="delete" aria-label="close"></button>
|
||||||
|
</header>
|
||||||
|
<section class="modal-card-body has-text-black">
|
||||||
|
The specified project was not found! Did you delete it? Try refreshing the page.<br>
|
||||||
|
If you believe this to be a bug, please report it
|
||||||
|
<a class="has-text-info"
|
||||||
|
href="https://github.com/domeniko-gentner/labertasche/issues"
|
||||||
|
target="_blank" rel="nofollow noopener norefferer">
|
||||||
|
here
|
||||||
|
</a>.
|
||||||
|
</section>
|
||||||
|
<footer class="modal-card-foot">
|
||||||
|
<button id="modal-ok" onclick="hide_modal('modal-project-not-found', {{ url_for('bp_dashboard.dashboard_project_list') }})"
|
||||||
|
class="button is-success">
|
||||||
|
OK
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -36,10 +36,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
|
<!-- TODO: onclick -->
|
||||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||||
onclick="show_modal('modal-new-project');"
|
onclick="show_modal_with_project('modal-project-edit', null)"
|
||||||
data-tippy-content="Create a new project"
|
data-tippy-content="Create a new project" href="#"
|
||||||
href="#">NEW</a>
|
>NEW</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -80,22 +81,24 @@
|
|||||||
<div class="card-footer-item has-background-danger-dark">
|
<div class="card-footer-item has-background-danger-dark">
|
||||||
<a class="has-text-weight-bold has-text-white is-uppercase"
|
<a class="has-text-weight-bold has-text-white is-uppercase"
|
||||||
data-tippy-content="Delete the project and all of its content"
|
data-tippy-content="Delete the project and all of its content"
|
||||||
onclick="show_modal_with_project('modal-project-delete', '{{ each['name'] }}');">DELETE</a>
|
onclick="show_modal('modal-project-delete', '{{ each['name'] }}');">DELETE</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||||
|
onclick="show_modal_with_project('modal-project-edit', '{{ each['name'] }}');"
|
||||||
data-tippy-content="Edit the name of the project and it's properties"
|
data-tippy-content="Edit the name of the project and it's properties"
|
||||||
href="#">EDIT</a>
|
href="#">EDIT</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||||
|
onclick="show_modal('modal-comments-export', '{{ each['name'] }}')"
|
||||||
data-tippy-content="Export all comments to Hugo.<br>This is normally not needed."
|
data-tippy-content="Export all comments to Hugo.<br>This is normally not needed."
|
||||||
href="#">EXPORT</a>
|
href="#">EXPORT</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer-item">
|
<div class="card-footer-item">
|
||||||
<a class="has-text-weight-bold has-text-black is-uppercase"
|
<a class="has-text-weight-bold has-text-black is-uppercase"
|
||||||
data-tippy-content="Manage this project"
|
data-tippy-content="Manage this project"
|
||||||
href="{{ url_for('bp_dashboard.dashboard_project_stats', project=each['name']) }}">VIEW</a>
|
href="{{ url_for('bp_dashboard.dashboard_project_stats', project=each['name']) }}">manage</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -103,142 +106,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal" id="modal-new-project">
|
{% include "modals/project_not_found.html" %}
|
||||||
<div class="modal-background"></div>
|
{% include "modals/project-delete.html" %}
|
||||||
<div class="modal-card">
|
{% include "modals/project_edit.html" %}
|
||||||
<header class="modal-card-head">
|
{% include "modals/comments-export-all.html"%}
|
||||||
<p class="modal-card-title">New Project</p>
|
|
||||||
<button onclick="hide_modal('modal-new-project')" class="delete" aria-label="close"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body">
|
|
||||||
<label for="project-name" class="has-text-black">PROJECT NAME
|
|
||||||
<input class="input is-success"
|
|
||||||
type="text"
|
|
||||||
name="project-name"
|
|
||||||
id="project-name"
|
|
||||||
placeholder="Type a name without special characters.."
|
|
||||||
>
|
|
||||||
</label>
|
|
||||||
<p id="new-project-too-short" class="is-hidden help has-text-danger">Input too short. Needs at least 1 character!</p>
|
|
||||||
<p id="new-project-invalid-name" class="is-hidden help has-text-danger">Input is invalid. Please use only a-z and 0-9.</p>
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button id="modal-ok" onclick="new_project_save()" class="button is-success">Save</button>
|
|
||||||
<button id="modal-cancel" onclick="hide_modal('modal-new-project')" class="button is-danger">Cancel</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal" id="modal-project-not-found">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head has-background-danger">
|
|
||||||
<p class="modal-card-title has-text-white">ERROR</p>
|
|
||||||
<button onclick="hide_modal('modal-project-not-found')" class="delete" aria-label="close"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body has-text-black">
|
|
||||||
The specified project was not found! Did you delete it? Try refreshing the page.<br>
|
|
||||||
If you believe this to be a bug, please report it
|
|
||||||
<a class="has-text-info"
|
|
||||||
href="https://github.com/domeniko-gentner/labertasche/issues"
|
|
||||||
target="_blank" rel="nofollow noopener norefferer">
|
|
||||||
here
|
|
||||||
</a>.
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button id="modal-ok" onclick="hide_modal('modal-project-not-found')" class="button is-success">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal" id="modal-project-delete">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head has-background-warning">
|
|
||||||
<p class="modal-card-title has-text-black">WARNING!</p>
|
|
||||||
<button onclick="hide_modal('modal-project-delete')" class="delete" aria-label="close"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body has-text-black">
|
|
||||||
You are about to delete a project. All associated data will be unrecoverably lost!
|
|
||||||
Please perform a manual sql dump if you would like to retain that data.
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot">
|
|
||||||
<button id="modal-delete-ok" onclick="project_delete()" class="button is-danger">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
<button id="modal-delete-cancel" onclick="hide_modal('modal-project-delete')" class="button is-success">
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal is-active" id="modal-project-edit">
|
|
||||||
<div class="modal-background"></div>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head has-background-warning">
|
|
||||||
<p class="modal-card-title has-text-black">Edit Project</p>
|
|
||||||
<button onclick="hide_modal('modal-project-edit')" class="delete" aria-label="close"></button>
|
|
||||||
</header>
|
|
||||||
<section class="modal-card-body has-text-black bg-deepmatte">
|
|
||||||
<form>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" id="edit-project-name" name="edit-project-name" type="text">
|
|
||||||
</div>
|
|
||||||
<label class="label help has-text-white" for="edit-project-name">
|
|
||||||
Project Name
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" id="edit-project-web-url" name="edit-project-web-url" type="text">
|
|
||||||
</div>
|
|
||||||
<label class="label help has-text-white" for="edit-project-web-url">
|
|
||||||
URI of the comment system
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" id="edit-project-blog-url" name="edit-project-blog-url" type="text">
|
|
||||||
</div>
|
|
||||||
<label class="label help has-text-white" for="edit-project-blog-url">
|
|
||||||
URL of your Hugo site for this project
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<input class="input" id="edit-project-output" name="edit-project-output" type="text">
|
|
||||||
</div>
|
|
||||||
<label class="label help has-text-white" for="edit-project-output">Output Directory</label>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<div class="control">
|
|
||||||
<label class="checkbox help has-text-white" for="edit-project-send-otp">
|
|
||||||
<input id="edit-project-send-otp" class="checkbox" type="checkbox"
|
|
||||||
name="edit-project-send-otp" checked>
|
|
||||||
Send OTP to publish?
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
<footer class="modal-card-foot bg-deepmatte">
|
|
||||||
<button id="modal-delete-ok" onclick="project_delete()" class="button is-danger">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
<button id="modal-delete-cancel" onclick="hide_modal('modal-project-edit')" class="button is-success">
|
|
||||||
CANCEL
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block javascript %}
|
{% block javascript %}
|
||||||
tippy('[data-tippy-content]', {
|
tippy('[data-tippy-content]', {
|
||||||
allowHTML: true,
|
allowHTML: true,
|
||||||
delay: 500
|
delay: 500
|
||||||
});
|
});
|
||||||
|
|
||||||
weburl = document.getElementById('edit-project-web-url')
|
|
||||||
weburl.value = window.location.host;
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user