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
__pycache__
*.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
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(

View File

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

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
# 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)

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|$)"
)
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)

View File

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

View File

@@ -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,

View File

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

View File

@@ -12,7 +12,7 @@
</tr>
{%- for id, name, join_date, role, banned_until in users -%}
<tr>
<td>{{ id }}</td>
<td><a href="{{ url_for('user_info', user_id = id) }}">{{ id }}</a></td>
<td>{{ name }}</td>
<td>{{ format_time(join_date) }}</td>
<td>
@@ -61,7 +61,7 @@
</tr>
{% for id, name, description, _, _, _ in forums %}
<tr>
<td>{{ id }}</td>
<td><a href="{{ url_for('forum', forum_id = id) }}">{{ id }}</a></td>
<td>
<form method=post action="forum/{{ id }}/edit/name/">
<input type=text name=name value="{{ name }}"</input>
@@ -114,6 +114,14 @@
<td>Login required</td>
<td><input name=login_required type=checkbox {{ 'checked' if config.login_required else '' }}></td>
</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>
<input type=submit value=Update>
</form>
@@ -129,8 +137,6 @@
</p>
<!-- -->
<h2 class=admin_h2>Query</h2>
<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">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel=stylesheet href="{{ url_for('static', filename='theme.css') }}">
{%- if config.server_name -%}
<link rel=stylesheet href="{{ url_for('static', filename='user.css') }}">
{%- if config.user_css -%}
<link rel=stylesheet href="{{ url_for('static', filename=config.user_css) }}">
{%- endif -%}
</head>
<body>
@@ -50,5 +50,6 @@
<p class="flash {{ category }}">{{ msg | safe }}</p>
{%- endfor -%}
{%- block content %}{% endblock -%}
<a id="end"></a>
</main>
</body>

View File

@@ -3,7 +3,7 @@
{% block content %}
<form method="post" class=login>
<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>
</table>
<input type="submit" value="Login">

View File

@@ -3,7 +3,10 @@
{%- from 'moderator.html' import moderate_thread with context %}
{%- 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() -%}
<p>{{ moderate_thread(thread_id, hidden) }}</p>
{%- endif -%}
@@ -15,4 +18,7 @@
{%- for c in comments %}
{{- render_comment(c, thread_id) }}
{%- endfor %}
<p>
<span> &raquo; </span><a href="#top">Page top</a>
</p>
{%- endblock %}

View File

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

View File

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