diff --git a/.gitignore b/.gitignore index 177f8e8..57b2dde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /venv __pycache__ *.db +*.config +*.pid diff --git a/forum/change_admin_pw.sh b/forum/change_admin_pw.sh new file mode 100644 index 0000000..5de0098 --- /dev/null +++ b/forum/change_admin_pw.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +SQLITE=sqlite3 +PYTHON=python3 + +set -eu + +if [[ -z "$DB" ]]; then + echo set DB env to the Sqlite database. + exit 1 +fi + +read -p 'Admin username: ' username +read -sp 'Admin password: ' password + + +password=$($PYTHON tool.py password "$password") +time=$($PYTHON -c 'import time; print(time.time_ns())') + +$SQLITE "$DB" " + UPDATE users + SET password = '$password', + role = 2, + join_time = $time + WHERE + user = lower('$username') + " + diff --git a/forum/db/config.py b/forum/db/config.py new file mode 100644 index 0000000..ec6534a --- /dev/null +++ b/forum/db/config.py @@ -0,0 +1,54 @@ +import json +import shutil + + +class Config: + def __init__(self, path): + self.path = path + self.config_values = [ + "version", + "server_name", + "server_description", + "secret_key", + "captcha_key", + "registration_enabled", + "login_required", + "threads_per_page", + "user_css", + ] + self._update_class(self._read_values()) + + def get_config(self): + current = self._read_values() + self._update_class(current) + return self + + def set_config(self, **kwargs): + for key in kwargs: + if key not in self.config_values: + raise ValueError(f"Unknown config key {key}!") + + current = self._read_values() + current.update(kwargs) + self._update_class(current) + self._write_values(current) + + def set_config_secrets(self, secret_key, captcha_key): + current = self._read_values() + current["secret_key"] = secret_key + current["captcha_key"] = captcha_key + self._update_class(current) + self._write_values(current) + + def _read_values(self): + with open(self.path, "rt") as fp: + return json.load(fp) + + def _write_values(self, values): + shutil.copy(self.path, self.path + ".bkp") + with open(self.path, "wt") as fp: + json.dump(values, fp, indent=2, sort_keys=True) + + def _update_class(self, values): + for item in values: + setattr(self, item, values[item]) diff --git a/forum/db/sqlite.py b/forum/db/sqlite.py index 9115374..91d4f66 100644 --- a/forum/db/sqlite.py +++ b/forum/db/sqlite.py @@ -1,4 +1,5 @@ import sqlite3 +from db.config import Config class DB: @@ -6,16 +7,16 @@ class DB: self.conn = conn pass - def get_config(self): - return ( - self._db() - .execute( - """ - select version, name, description, secret_key, captcha_key, registration_enabled, login_required from config - """ - ) - .fetchone() - ) + # ~ def get_config(self): + # ~ return ( + # ~ self._db() + # ~ .execute( + # ~ """ + # ~ select version, name, description, secret_key, captcha_key, registration_enabled, login_required from config + # ~ """ + # ~ ) + # ~ .fetchone() + # ~ ) def get_forums(self): return self._db().execute( @@ -48,7 +49,7 @@ class DB: ) def get_thread_forum(self, thread_id): - """ Returns forum_id of a thread """ + """Returns forum_id of a thread""" return ( self._db() .execute( @@ -104,7 +105,16 @@ class DB: def get_thread(self, thread): db = self._db() - title, text, author, author_id, create_time, modify_time, hidden, forum_id = db.execute( + ( + title, + text, + author, + author_id, + create_time, + modify_time, + hidden, + forum_id, + ) = db.execute( """ select title, text, name, author_id, create_time, modify_time, hidden, forum_id from threads, users @@ -139,7 +149,7 @@ class DB: modify_time, comments, hidden, - forum_id + forum_id, ) def get_thread_title(self, thread_id): @@ -525,14 +535,16 @@ class DB: Add a user if registrations are enabled. """ try: + config = Config(os.getenv("CONF")) + if not config.registration_enable: + return None + db = self._db() c = db.cursor() c.execute( """ insert into users(name, password, join_time) - select lower(?), ?, ? - from config - where registration_enabled = 1 + values lower(?), ?, ? """, (username, password, time), ) @@ -616,25 +628,25 @@ class DB: ) db.commit() - def set_config( - self, server_name, server_description, registration_enabled, login_required - ): - return self.change_one( - """ - update config - set name = ?, description = ?, registration_enabled = ?, login_required = ? - """, - (server_name, server_description, registration_enabled, login_required), - ) + # ~ def set_config( + # ~ self, server_name, server_description, registration_enabled, login_required + # ~ ): + # ~ return self.change_one( + # ~ """ + # ~ update config + # ~ set name = ?, description = ?, registration_enabled = ?, login_required = ? + # ~ """, + # ~ (server_name, server_description, registration_enabled, login_required), + # ~ ) - def set_config_secrets(self, secret_key, captcha_key): - return self.change_one( - """ - update config - set secret_key = ?, captcha_key = ? - """, - (secret_key, captcha_key), - ) + # ~ def set_config_secrets(self, secret_key, captcha_key): + # ~ return self.change_one( + # ~ """ + # ~ update config + # ~ set secret_key = ?, captcha_key = ? + # ~ """, + # ~ (secret_key, captcha_key), + # ~ ) def set_user_ban(self, user_id, until): return self.change_one( diff --git a/forum/docker-entrypoint.sh b/forum/docker-entrypoint.sh index 67e8a8a..1dd7c87 100755 --- a/forum/docker-entrypoint.sh +++ b/forum/docker-entrypoint.sh @@ -3,6 +3,7 @@ PYTHON=python3 SQLITE=sqlite3 export DB="/app/forum.db" +export CONF="/app/forum.config" export SERVER=gunicorn export PID="forum.pid" export WORKERS=4 @@ -15,36 +16,6 @@ fi set -eu . /opt/venv/bin/activate -if [[ -e "$DB" ]]; then - $SQLITE -header "$DB" "SELECT version,name,description,registration_enabled,login_required FROM config" - echo Database already exists -else - password=$($PYTHON tool.py password "$ADMINP") - time=$($PYTHON -c 'import time; print(time.time_ns())') - version=$($PYTHON tool.py version) - - $SQLITE "$DB" -init schema.txt "insert into config ( - version, - name, - description, - secret_key, - captcha_key, - registration_enabled, - login_required -) -values ( - '$version', - 'Forum', - '', - '$(head -c 30 /dev/urandom | base64)', - '$(head -c 30 /dev/urandom | base64)', - 0, - 1 - )" - $SQLITE "$DB" " - insert into users (name, password, role, join_time) - values (lower('$ADMINU'), '$password', 2, $time) - " -fi +sh ./init_forum.sh exec "$SERVER" -w $WORKERS 'main:app' --pid="$PID" -b 0.0.0.0:5000 diff --git a/forum/init_forum.sh b/forum/init_forum.sh new file mode 100755 index 0000000..7fabf9e --- /dev/null +++ b/forum/init_forum.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +SQLITE=sqlite3 +PYTHON=python3 + +set -e + +if [ -z "$DB" ]; then + echo set DB env to point the Sqlite database. + exit 1 +fi + +if [ -z "$CONF" ]; then + echo set CONF env to point the config file. + exit 1 +fi + +if [ -e "$CONF" ]; then + echo Config exists +else + version=$($PYTHON tool.py version) + cat < "$CONF" +{ + "version": "$version", + "server_name": "Forum", + "server_description": "", + "secret_key": "$(head -c 30 /dev/urandom | base64)", + "captcha_key": "$(head -c 30 /dev/urandom | base64)", + "registration_enabled": false, + "login_required": true, + "threads_per_page": 50, + "user_css": "" +} +EOF + echo "Config '$CONF' created" >&2 + +fi + +if [ -e "$DB" ]; then + echo Database already exists +else + password=$($PYTHON tool.py password "$ADMINP") + time=$($PYTHON -c 'import time; print(time.time_ns())') + + $SQLITE "$DB" -init schema.txt " + insert into users (name, password, role, join_time) + values (lower('$ADMINU'), '$password', 2, $time) + " + echo "Database '$DB' created" >&2 +fi + diff --git a/forum/init_sqlite.sh b/forum/init_sqlite.sh deleted file mode 100755 index 14009ea..0000000 --- a/forum/init_sqlite.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -SQLITE=sqlite3 -PYTHON=python3 - -set -e - -make -. ./venv/bin/activate - -if [ $# -le 0 ] -then - echo "Usage: $0 [--no-admin]" >&2 - exit 1 -fi - -if [ -e "$1" ] -then - echo "Database '$1' already exists" >&2 - exit 1 -fi - -if [ "$2" != --no-admin ] -then - read -p 'Admin username: ' username - read -sp 'Admin password: ' password -fi - -password=$($PYTHON tool.py password "$password") -time=$($PYTHON -c 'import time; print(time.time_ns())') - -$SQLITE "$1" -init schema.txt "insert into config ( - version, - name, - description, - secret_key, - captcha_key, - registration_enabled, - login_required -) -values ( - 'agreper-v0.1.1', - 'Agreper', - '', - '$(head -c 30 /dev/urandom | base64)', - '$(head -c 30 /dev/urandom | base64)', - 0, - 0 -)" -if [ "$2" != --no-admin ] -then - $SQLITE "$1" " - insert into users (name, password, role, join_time) - values (lower('$username'), '$password', 2, $time) - " -fi - -echo "Database '$1' created" >&2 diff --git a/forum/main.py b/forum/main.py index d19a341..c9dabf6 100644 --- a/forum/main.py +++ b/forum/main.py @@ -1,9 +1,8 @@ from version import VERSION -# TODO put in config table -THREADS_PER_PAGE = 50 from flask import Flask, render_template, session, request, redirect, url_for, flash, g from db.sqlite import DB +from db.config import Config import os, sys, subprocess import passlib.hash, secrets import time @@ -13,33 +12,33 @@ import captcha, password, minimd app = Flask(__name__) db = DB(os.getenv("DB")) - +config = Config(os.getenv("CONF")) # This defaults to None, which allows CSRF attacks in FireFox # and older versions of Chrome. # 'Lax' is sufficient to prevent malicious POST requests. app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +app.config["SECRET_KEY"] = config.secret_key - -class Config: - pass - - -config = Config() -( - config.version, - config.server_name, - config.server_description, - app.config["SECRET_KEY"], - config.captcha_key, - config.registration_enabled, - config.login_required -) = db.get_config() -config.user_css = os.path.exists(os.path.join(app.static_folder, 'user.css')) +# ~ class Config: +# ~ pass +# ~ config = Config() +# ~ ( +# ~ config.version, +# ~ config.server_name, +# ~ config.server_description, +# ~ app.config["SECRET_KEY"], +# ~ config.captcha_key, +# ~ config.registration_enabled, +# ~ config.login_required +# ~ ) = db.get_config() +# ~ app.config['user_css'] = os.path.exists(os.path.join(app.static_folder, 'user.css')) +# ~ config.threads_per_page = 50 if config.version != VERSION: print(f"Incompatible version {config.version} (expected {VERSION})") sys.exit(1) + class Role: USER = 0 MODERATOR = 1 @@ -50,11 +49,10 @@ class Role: def before_request(): if config.login_required: user_id = session.get("user_id", -1) - if user_id == -1 and request.endpoint not in ("login","static"): + if user_id == -1 and request.endpoint not in ("login", "static"): return redirect(url_for("login")) - @app.after_request def after_request(response): # This forbids other sites from embedding this site in an iframe, @@ -80,10 +78,10 @@ def forum(forum_id): title, description = db.get_forum(forum_id) offset = int(request.args.get("p", 0)) user_id = session.get("user_id", -1) - threads = [*db.get_threads(forum_id, offset, THREADS_PER_PAGE + 1, user_id)] - if len(threads) == THREADS_PER_PAGE + 1: + threads = [*db.get_threads(forum_id, offset, config.threads_per_page + 1, user_id)] + if len(threads) == config.threads_per_page + 1: threads.pop() - next_page = offset + THREADS_PER_PAGE + next_page = offset + config.threads_per_page else: next_page = None return render_template( @@ -95,7 +93,7 @@ def forum(forum_id): description=description, threads=threads, next_page=next_page, - prev_page=max(offset - THREADS_PER_PAGE, 0) if offset > 0 else None, + prev_page=max(offset - config.threads_per_page, 0) if offset > 0 else None, ) @@ -111,7 +109,7 @@ def thread(thread_id): modify_time, comments, hidden, - forum_id + forum_id, ) = db.get_thread(thread_id) forum_title, _ = db.get_forum(forum_id) @@ -155,7 +153,7 @@ def comment(comment_id): parent_id=parent_id, thread_id=thread_id, forum_id=forum_id, - forum_title=forum_title + forum_title=forum_title, ) @@ -532,11 +530,14 @@ def admin_edit_config(): return user try: - db.set_config( - request.form["server_name"], - trim_text(request.form["server_description"]), - "registration_enabled" in request.form, - "login_required" in request.form, + # db.set_config( + config.set_config( + server_name=request.form["server_name"], + server_description=trim_text(request.form["server_description"]), + registration_enabled="registration_enabled" in request.form, + login_required="login_required" in request.form, + threads_per_page=int(request.form["threads_per_page"]), + user_css=request.form["user_css"], ) flash("Updated config. Refresh the page to see the changes.", "success") restart() @@ -554,7 +555,8 @@ def admin_new_secrets(): secret_key = secrets.token_urlsafe(30) captcha_key = secrets.token_urlsafe(30) try: - db.set_config_secrets(secret_key, captcha_key) + # ~ db.set_config_secrets(secret_key, captcha_key) + config.set_config_secrets(secret_key, captcha_key) flash("Changed secrets. You will be logged out.", "success") restart() except Exception as e: @@ -776,7 +778,7 @@ def create_comment_tree(comments, user): # Sort each comment based on create time def sort_time(l): - l.sort(key=lambda c: c.modify_time, reverse=True) + l.sort(key=lambda c: c.modify_time, reverse=False) for c in l: sort_time(c.children) diff --git a/forum/minimd.py b/forum/minimd.py index c81addd..0a06d5c 100755 --- a/forum/minimd.py +++ b/forum/minimd.py @@ -14,8 +14,9 @@ RE_PLAINURL = re.compile( r"(?P
^|\s|\n)(?Phttps?://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]))(?P\s|\n|$)"
 )
 
+
 def html(text):
-    text = RE_PLAINURL.sub(r'\g
[\g](\g)\g', text)
+    text = RE_PLAINURL.sub(r"\g
[\g](\g)\g", text)
     return markdown2.markdown(text)
 
 
diff --git a/forum/restart.sh b/forum/restart.sh
index a1997c7..80cb82f 100755
--- a/forum/restart.sh
+++ b/forum/restart.sh
@@ -1,13 +1,13 @@
 #!/usr/bin/env bash
 
 set -e
-SERVER="$1"
 if [ -z "$SERVER" ]
 then
 	echo "SERVER is not set" >&2
 	exit 1
 fi
 
+echo Restarting service >&2
 case "$SERVER" in
 	dev)
 		touch main.py
diff --git a/forum/schema.txt b/forum/schema.txt
index 9c9fada..c9894b9 100644
--- a/forum/schema.txt
+++ b/forum/schema.txt
@@ -1,12 +1,12 @@
-create table config (
-	version                text     not null,
-	name                   text     not null,
-	description            text     not null,
-	secret_key             text     not null,
-	captcha_key            text     not null,
-	registration_enabled   boolean  not null,
-	login_required         boolean  not null
-);
+--create table config (
+--	version                text     not null,
+--	name                   text     not null,
+--	description            text     not null,
+--	secret_key             text     not null,
+--	captcha_key            text     not null,
+--	registration_enabled   boolean  not null,
+--	login_required         boolean  not null
+--);
 
 create table users (
 	user_id    integer       unique  not null  primary key  autoincrement,
diff --git a/forum/templates/admin/base.html b/forum/templates/admin/base.html
index 030f9a5..97560be 100644
--- a/forum/templates/admin/base.html
+++ b/forum/templates/admin/base.html
@@ -4,16 +4,17 @@
 {{ title }}
 
 
+
 
-{%- if config.server_name -%}
-   
+{%- if config.user_css -%}
+   
 {%- endif -%}
 
 
 

{{ title }}

-Admin panel | -Home page + « Forum Home + | Admin panel

{%- for category, msg in get_flashed_messages(True) -%}

{{ msg }}

diff --git a/forum/templates/admin/index.html b/forum/templates/admin/index.html index 3bcecef..b369cb7 100644 --- a/forum/templates/admin/index.html +++ b/forum/templates/admin/index.html @@ -12,7 +12,7 @@ {%- for id, name, join_date, role, banned_until in users -%} -{{ id }} +{{ id }} {{ name }} {{ format_time(join_date) }} @@ -61,7 +61,7 @@ {% for id, name, description, _, _, _ in forums %} -{{ id }} +{{ id }}
@@ -114,6 +114,14 @@ Login required + +Number of threads per page + + + +User defined CSS file in static/ folder + +
@@ -129,8 +137,6 @@

- -

Query

⚠ Only use queries if you know what you're doing ⚠

diff --git a/forum/templates/base.html b/forum/templates/base.html index 82267c9..3d1ad91 100644 --- a/forum/templates/base.html +++ b/forum/templates/base.html @@ -6,8 +6,8 @@ - {%- if config.server_name -%} - + {%- if config.user_css -%} + {%- endif -%} @@ -50,5 +50,6 @@

{{ msg | safe }}

{%- endfor -%} {%- block content %}{% endblock -%} + diff --git a/forum/templates/login.html b/forum/templates/login.html index 5903f67..998cc61 100644 --- a/forum/templates/login.html +++ b/forum/templates/login.html @@ -3,7 +3,7 @@ {% block content %}