moved config to a json, which makes adding more variables easier, but perhaps otherwise adds complexity

This commit is contained in:
Ville Rantanen
2023-07-28 13:08:54 +03:00
parent 80af9c321c
commit f1c453d3d4
18 changed files with 258 additions and 182 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
/venv /venv
__pycache__ __pycache__
*.db *.db
*.config
*.pid

28
forum/change_admin_pw.sh Normal file
View File

@@ -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')
"

54
forum/db/config.py Normal file
View File

@@ -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])

View File

@@ -1,4 +1,5 @@
import sqlite3 import sqlite3
from db.config import Config
class DB: class DB:
@@ -6,16 +7,16 @@ class DB:
self.conn = conn self.conn = conn
pass pass
def get_config(self): # ~ def get_config(self):
return ( # ~ return (
self._db() # ~ self._db()
.execute( # ~ .execute(
""" # ~ """
select version, name, description, secret_key, captcha_key, registration_enabled, login_required from config # ~ select version, name, description, secret_key, captcha_key, registration_enabled, login_required from config
""" # ~ """
) # ~ )
.fetchone() # ~ .fetchone()
) # ~ )
def get_forums(self): def get_forums(self):
return self._db().execute( return self._db().execute(
@@ -48,7 +49,7 @@ class DB:
) )
def get_thread_forum(self, thread_id): def get_thread_forum(self, thread_id):
""" Returns forum_id of a thread """ """Returns forum_id of a thread"""
return ( return (
self._db() self._db()
.execute( .execute(
@@ -104,7 +105,16 @@ class DB:
def get_thread(self, thread): def get_thread(self, thread):
db = self._db() 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 select title, text, name, author_id, create_time, modify_time, hidden, forum_id
from threads, users from threads, users
@@ -139,7 +149,7 @@ class DB:
modify_time, modify_time,
comments, comments,
hidden, hidden,
forum_id forum_id,
) )
def get_thread_title(self, thread_id): def get_thread_title(self, thread_id):
@@ -525,14 +535,16 @@ class DB:
Add a user if registrations are enabled. Add a user if registrations are enabled.
""" """
try: try:
config = Config(os.getenv("CONF"))
if not config.registration_enable:
return None
db = self._db() db = self._db()
c = db.cursor() c = db.cursor()
c.execute( c.execute(
""" """
insert into users(name, password, join_time) insert into users(name, password, join_time)
select lower(?), ?, ? values lower(?), ?, ?
from config
where registration_enabled = 1
""", """,
(username, password, time), (username, password, time),
) )
@@ -616,25 +628,25 @@ class DB:
) )
db.commit() db.commit()
def set_config( # ~ def set_config(
self, server_name, server_description, registration_enabled, login_required # ~ self, server_name, server_description, registration_enabled, login_required
): # ~ ):
return self.change_one( # ~ return self.change_one(
""" # ~ """
update config # ~ update config
set name = ?, description = ?, registration_enabled = ?, login_required = ? # ~ set name = ?, description = ?, registration_enabled = ?, login_required = ?
""", # ~ """,
(server_name, server_description, registration_enabled, login_required), # ~ (server_name, server_description, registration_enabled, login_required),
) # ~ )
def set_config_secrets(self, secret_key, captcha_key): # ~ def set_config_secrets(self, secret_key, captcha_key):
return self.change_one( # ~ return self.change_one(
""" # ~ """
update config # ~ update config
set secret_key = ?, captcha_key = ? # ~ set secret_key = ?, captcha_key = ?
""", # ~ """,
(secret_key, captcha_key), # ~ (secret_key, captcha_key),
) # ~ )
def set_user_ban(self, user_id, until): def set_user_ban(self, user_id, until):
return self.change_one( return self.change_one(

View File

@@ -3,6 +3,7 @@ PYTHON=python3
SQLITE=sqlite3 SQLITE=sqlite3
export DB="/app/forum.db" export DB="/app/forum.db"
export CONF="/app/forum.config"
export SERVER=gunicorn export SERVER=gunicorn
export PID="forum.pid" export PID="forum.pid"
export WORKERS=4 export WORKERS=4
@@ -15,36 +16,6 @@ fi
set -eu set -eu
. /opt/venv/bin/activate . /opt/venv/bin/activate
if [[ -e "$DB" ]]; then sh ./init_forum.sh
$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
exec "$SERVER" -w $WORKERS 'main:app' --pid="$PID" -b 0.0.0.0:5000 exec "$SERVER" -w $WORKERS 'main:app' --pid="$PID" -b 0.0.0.0:5000

51
forum/init_forum.sh Executable file
View File

@@ -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 <<EOF > "$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

View File

@@ -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 <file> [--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

View File

@@ -1,9 +1,8 @@
from version import VERSION 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 flask import Flask, render_template, session, request, redirect, url_for, flash, g
from db.sqlite import DB from db.sqlite import DB
from db.config import Config
import os, sys, subprocess import os, sys, subprocess
import passlib.hash, secrets import passlib.hash, secrets
import time import time
@@ -13,33 +12,33 @@ import captcha, password, minimd
app = Flask(__name__) app = Flask(__name__)
db = DB(os.getenv("DB")) db = DB(os.getenv("DB"))
config = Config(os.getenv("CONF"))
# This defaults to None, which allows CSRF attacks in FireFox # This defaults to None, which allows CSRF attacks in FireFox
# and older versions of Chrome. # and older versions of Chrome.
# 'Lax' is sufficient to prevent malicious POST requests. # 'Lax' is sufficient to prevent malicious POST requests.
app.config["SESSION_COOKIE_SAMESITE"] = "Lax" app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
app.config["SECRET_KEY"] = config.secret_key
# ~ class Config:
class Config: # ~ pass
pass # ~ config = Config()
# ~ (
# ~ config.version,
config = Config() # ~ config.server_name,
( # ~ config.server_description,
config.version, # ~ app.config["SECRET_KEY"],
config.server_name, # ~ config.captcha_key,
config.server_description, # ~ config.registration_enabled,
app.config["SECRET_KEY"], # ~ config.login_required
config.captcha_key, # ~ ) = db.get_config()
config.registration_enabled, # ~ app.config['user_css'] = os.path.exists(os.path.join(app.static_folder, 'user.css'))
config.login_required # ~ config.threads_per_page = 50
) = db.get_config()
config.user_css = os.path.exists(os.path.join(app.static_folder, 'user.css'))
if config.version != VERSION: if config.version != VERSION:
print(f"Incompatible version {config.version} (expected {VERSION})") print(f"Incompatible version {config.version} (expected {VERSION})")
sys.exit(1) sys.exit(1)
class Role: class Role:
USER = 0 USER = 0
MODERATOR = 1 MODERATOR = 1
@@ -50,11 +49,10 @@ class Role:
def before_request(): def before_request():
if config.login_required: if config.login_required:
user_id = session.get("user_id", -1) 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")) return redirect(url_for("login"))
@app.after_request @app.after_request
def after_request(response): def after_request(response):
# This forbids other sites from embedding this site in an iframe, # 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) title, description = db.get_forum(forum_id)
offset = int(request.args.get("p", 0)) offset = int(request.args.get("p", 0))
user_id = session.get("user_id", -1) user_id = session.get("user_id", -1)
threads = [*db.get_threads(forum_id, offset, THREADS_PER_PAGE + 1, user_id)] threads = [*db.get_threads(forum_id, offset, config.threads_per_page + 1, user_id)]
if len(threads) == THREADS_PER_PAGE + 1: if len(threads) == config.threads_per_page + 1:
threads.pop() threads.pop()
next_page = offset + THREADS_PER_PAGE next_page = offset + config.threads_per_page
else: else:
next_page = None next_page = None
return render_template( return render_template(
@@ -95,7 +93,7 @@ def forum(forum_id):
description=description, description=description,
threads=threads, threads=threads,
next_page=next_page, 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, modify_time,
comments, comments,
hidden, hidden,
forum_id forum_id,
) = db.get_thread(thread_id) ) = db.get_thread(thread_id)
forum_title, _ = db.get_forum(forum_id) forum_title, _ = db.get_forum(forum_id)
@@ -155,7 +153,7 @@ def comment(comment_id):
parent_id=parent_id, parent_id=parent_id,
thread_id=thread_id, thread_id=thread_id,
forum_id=forum_id, forum_id=forum_id,
forum_title=forum_title forum_title=forum_title,
) )
@@ -532,11 +530,14 @@ def admin_edit_config():
return user return user
try: try:
db.set_config( # db.set_config(
request.form["server_name"], config.set_config(
trim_text(request.form["server_description"]), server_name=request.form["server_name"],
"registration_enabled" in request.form, server_description=trim_text(request.form["server_description"]),
"login_required" in request.form, 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") flash("Updated config. Refresh the page to see the changes.", "success")
restart() restart()
@@ -554,7 +555,8 @@ def admin_new_secrets():
secret_key = secrets.token_urlsafe(30) secret_key = secrets.token_urlsafe(30)
captcha_key = secrets.token_urlsafe(30) captcha_key = secrets.token_urlsafe(30)
try: 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") flash("Changed secrets. You will be logged out.", "success")
restart() restart()
except Exception as e: except Exception as e:
@@ -776,7 +778,7 @@ def create_comment_tree(comments, user):
# Sort each comment based on create time # Sort each comment based on create time
def sort_time(l): 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: for c in l:
sort_time(c.children) sort_time(c.children)

View File

@@ -14,8 +14,9 @@ RE_PLAINURL = re.compile(
r"(?P<pre>^|\s|\n)(?P<url>https?://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]))(?P<post>\s|\n|$)" r"(?P<pre>^|\s|\n)(?P<url>https?://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-]))(?P<post>\s|\n|$)"
) )
def html(text): def html(text):
text = RE_PLAINURL.sub(r'\g<pre>[\g<url>](\g<url>)\g<post>', text) text = RE_PLAINURL.sub(r"\g<pre>[\g<url>](\g<url>)\g<post>", text)
return markdown2.markdown(text) return markdown2.markdown(text)

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
SERVER="$1"
if [ -z "$SERVER" ] if [ -z "$SERVER" ]
then then
echo "SERVER is not set" >&2 echo "SERVER is not set" >&2
exit 1 exit 1
fi fi
echo Restarting service >&2
case "$SERVER" in case "$SERVER" in
dev) dev)
touch main.py touch main.py

View File

@@ -1,12 +1,12 @@
create table config ( --create table config (
version text not null, -- version text not null,
name text not null, -- name text not null,
description text not null, -- description text not null,
secret_key text not null, -- secret_key text not null,
captcha_key text not null, -- captcha_key text not null,
registration_enabled boolean not null, -- registration_enabled boolean not null,
login_required boolean not null -- login_required boolean not null
); --);
create table users ( create table users (
user_id integer unique not null primary key autoincrement, user_id integer unique not null primary key autoincrement,

View File

@@ -4,16 +4,17 @@
<title>{{ title }}</title> <title>{{ title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta content="utf-8" http-equiv="encoding"> <meta content="utf-8" http-equiv="encoding">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel=stylesheet href="{{ url_for('static', filename='theme.css') }}"> <link rel=stylesheet href="{{ url_for('static', filename='theme.css') }}">
{%- if config.server_name -%} {%- if config.user_css -%}
<link rel=stylesheet href="{{ url_for('static', filename='user.css') }}"> <link rel=stylesheet href="{{ url_for('static', filename=config.user_css) }}">
{%- endif -%} {%- endif -%}
</head> </head>
<body> <body>
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
<p> <p>
<a href="{{ url_for('admin') }}">Admin panel</a><span> | </span> <span> &laquo; </span><a href="{{ url_for('index') }}">Forum Home</a>
<a href="{{ url_for('index') }}">Home page</a> <span> | </span><a href="{{ url_for('admin') }}">Admin panel</a>
</p> </p>
{%- for category, msg in get_flashed_messages(True) -%} {%- for category, msg in get_flashed_messages(True) -%}
<p class="flash {{ category }}">{{ msg }}</p> <p class="flash {{ category }}">{{ msg }}</p>

View File

@@ -12,7 +12,7 @@
</tr> </tr>
{%- for id, name, join_date, role, banned_until in users -%} {%- for id, name, join_date, role, banned_until in users -%}
<tr> <tr>
<td>{{ id }}</td> <td><a href="{{ url_for('user_info', user_id = id) }}">{{ id }}</a></td>
<td>{{ name }}</td> <td>{{ name }}</td>
<td>{{ format_time(join_date) }}</td> <td>{{ format_time(join_date) }}</td>
<td> <td>
@@ -61,7 +61,7 @@
</tr> </tr>
{% for id, name, description, _, _, _ in forums %} {% for id, name, description, _, _, _ in forums %}
<tr> <tr>
<td>{{ id }}</td> <td><a href="{{ url_for('forum', forum_id = id) }}">{{ id }}</a></td>
<td> <td>
<form method=post action="forum/{{ id }}/edit/name/"> <form method=post action="forum/{{ id }}/edit/name/">
<input type=text name=name value="{{ name }}"</input> <input type=text name=name value="{{ name }}"</input>
@@ -114,6 +114,14 @@
<td>Login required</td> <td>Login required</td>
<td><input name=login_required type=checkbox {{ 'checked' if config.login_required else '' }}></td> <td><input name=login_required type=checkbox {{ 'checked' if config.login_required else '' }}></td>
</tr> </tr>
<tr>
<td>Number of threads per page</td>
<td><input type=number name=threads_per_page value="{{ config.threads_per_page }}"></td>
</tr>
<tr>
<td>User defined CSS file in static/ folder</td>
<td><input type=text name=user_css value="{{ config.user_css }}"></td>
</tr>
</table> </table>
<input type=submit value=Update> <input type=submit value=Update>
</form> </form>
@@ -129,8 +137,6 @@
</p> </p>
<!-- --> <!-- -->
<h2 class=admin_h2>Query</h2> <h2 class=admin_h2>Query</h2>
<p>&#9888; Only use queries if you know what you're doing &#9888;</p> <p>&#9888; Only use queries if you know what you're doing &#9888;</p>

View File

@@ -6,8 +6,8 @@
<meta content="utf-8" http-equiv="encoding"> <meta content="utf-8" http-equiv="encoding">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel=stylesheet href="{{ url_for('static', filename='theme.css') }}"> <link rel=stylesheet href="{{ url_for('static', filename='theme.css') }}">
{%- if config.server_name -%} {%- if config.user_css -%}
<link rel=stylesheet href="{{ url_for('static', filename='user.css') }}"> <link rel=stylesheet href="{{ url_for('static', filename=config.user_css) }}">
{%- endif -%} {%- endif -%}
</head> </head>
<body> <body>
@@ -50,5 +50,6 @@
<p class="flash {{ category }}">{{ msg | safe }}</p> <p class="flash {{ category }}">{{ msg | safe }}</p>
{%- endfor -%} {%- endfor -%}
{%- block content %}{% endblock -%} {%- block content %}{% endblock -%}
<a id="end"></a>
</main> </main>
</body> </body>

View File

@@ -3,7 +3,7 @@
{% block content %} {% block content %}
<form method="post" class=login> <form method="post" class=login>
<table> <table>
<tr><td>Username</td><td><input type="text" name="username" required></td></tr> <tr><td>Username</td><td><input type="text" name="username" required autofocus></td></tr>
<tr><td>Password</td><td><input type="password" name="password" required></td></tr> <tr><td>Password</td><td><input type="password" name="password" required></td></tr>
</table> </table>
<input type="submit" value="Login"> <input type="submit" value="Login">

View File

@@ -3,7 +3,10 @@
{%- from 'moderator.html' import moderate_thread with context %} {%- from 'moderator.html' import moderate_thread with context %}
{%- block content %} {%- block content %}
<p><span> &laquo; </span><a href="{{ url_for('forum', forum_id = forum_id) }}">{{ forum_title }}</a></p> <p>
<span> &laquo; </span><a href="{{ url_for('forum', forum_id = forum_id) }}">{{ forum_title }}</a>
<span> &raquo; </span><a href="#end">Page end</a>
</p>
{%- if user is not none and user.is_moderator() -%} {%- if user is not none and user.is_moderator() -%}
<p>{{ moderate_thread(thread_id, hidden) }}</p> <p>{{ moderate_thread(thread_id, hidden) }}</p>
{%- endif -%} {%- endif -%}
@@ -15,4 +18,7 @@
{%- for c in comments %} {%- for c in comments %}
{{- render_comment(c, thread_id) }} {{- render_comment(c, thread_id) }}
{%- endfor %} {%- endfor %}
<p>
<span> &raquo; </span><a href="#top">Page top</a>
</p>
{%- endblock %} {%- endblock %}

View File

@@ -25,6 +25,7 @@ if cmd == "password":
print(password.hash(pwd)) print(password.hash(pwd))
elif cmd == "version": elif cmd == "version":
from version import VERSION from version import VERSION
print(VERSION) print(VERSION)
else: else:
print("unknown command ", cmd) print("unknown command ", cmd)

View File

@@ -1,3 +1 @@
VERSION = "agreper-v0.1.1q1" VERSION = "agreper-v0.1.1q1"