Add configuration table

This commit is contained in:
David Hoppenbrouwers
2022-10-09 12:27:59 +02:00
parent e3af03bbac
commit f73f2b405c
10 changed files with 227 additions and 56 deletions

View File

@@ -5,6 +5,12 @@ class DB:
self.conn = conn self.conn = conn
pass pass
def get_config(self):
return self._db().execute('''
select version, name, description, secret_key, captcha_key, registration_enabled from config
'''
).fetchone()
def get_forums(self): def get_forums(self):
return self._db().execute(''' return self._db().execute('''
select f.forum_id, name, description, thread_id, title, update_time select f.forum_id, name, description, thread_id, title, update_time
@@ -329,6 +335,7 @@ class DB:
c.execute(''' c.execute('''
insert into users(name, password, join_time) insert into users(name, password, join_time)
values (lower(?), ?, ?) values (lower(?), ?, ?)
where (select registration_enabled from config)
''', ''',
(username, password, time) (username, password, time)
) )
@@ -375,6 +382,22 @@ class DB:
) )
db.commit() db.commit()
def set_config(self, server_name, server_description, registration_enabled):
return self.change_one('''
update config
set name = ?, description = ?, registration_enabled = ?
''',
(server_name, server_description, registration_enabled)
)
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 change_one(self, query, values): def change_one(self, query, values):
db = self._db() db = self._db()
c = db.cursor() c = db.cursor()
@@ -385,7 +408,11 @@ class DB:
return False return False
def query(self, q): def query(self, q):
return self._db().execute(q) db = self._db()
c = db.cursor()
rows = c.execute(q)
db.commit()
return rows, c.rowcount
def _db(self): def _db(self):
return sqlite3.connect(self.conn) return sqlite3.connect(self.conn)

28
init_sqlite.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
SQLITE=sqlite3
set -e
if [ $# != 1 ]
then
echo "Usage: $0 <file>" >&2
exit 1
fi
$SQLITE $1 -init schema.txt "insert into config (
version,
name,
description,
secret_key,
captcha_key,
registration_enabled
)
values (
'agrepy-v0.1',
'Agrepy',
'',
'$(head -c 30 /dev/urandom | base64)',
'$(head -c 30 /dev/urandom | base64)',
0
);"

150
main.py
View File

@@ -1,20 +1,24 @@
VERSION = 'agrepy-v0.1'
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
import os import os, sys, subprocess
import passlib.hash import passlib.hash, secrets
import time import time
from datetime import datetime from datetime import datetime
import captcha import captcha
app = Flask(__name__) app = Flask(__name__)
db = DB(os.getenv('DB')) db = DB(os.getenv('DB'))
NAME = 'Agrepy'
# TODO config file class Config:
app.config['SECRET_KEY'] = 'totally random' pass
captcha_key = 'piss off bots' config = Config()
app.jinja_env.trim_blocks = True config.version, config.server_name, config.server_description, app.config['SECRET_KEY'], config.captcha_key, config.registration_enabled = db.get_config()
app.jinja_env.lstrip_blocks = True
if config.version != VERSION:
print(f'Incompatible version {config.version} (expected {VERSION})')
sys.exit(1)
class Role: class Role:
USER = 0 USER = 0
@@ -25,7 +29,9 @@ class Role:
def index(): def index():
return render_template( return render_template(
'index.html', 'index.html',
title = NAME, title = config.server_name,
description = config.server_description,
config = config,
user = get_user(), user = get_user(),
forums = db.get_forums() forums = db.get_forums()
) )
@@ -38,6 +44,7 @@ def forum(forum_id):
'forum.html', 'forum.html',
title = title, title = title,
user = get_user(), user = get_user(),
config = config,
forum_id = forum_id, forum_id = forum_id,
description = description, description = description,
threads = threads, threads = threads,
@@ -51,6 +58,7 @@ def thread(thread_id):
return render_template( return render_template(
'thread.html', 'thread.html',
title = title, title = title,
config = config,
user = get_user(), user = get_user(),
text = text, text = text,
author = author, author = author,
@@ -71,6 +79,7 @@ def comment(comment_id):
return render_template( return render_template(
'comments.html', 'comments.html',
title = title, title = title,
config = config,
user = get_user(), user = get_user(),
reply_comment = reply_comment, reply_comment = reply_comment,
comments = comments, comments = comments,
@@ -95,6 +104,7 @@ def login():
return render_template( return render_template(
'login.html', 'login.html',
title = 'Login', title = 'Login',
config = config,
user = get_user() user = get_user()
) )
@@ -110,7 +120,7 @@ def user_edit():
return redirect(url_for('login')) return redirect(url_for('login'))
if request.method == 'POST': if request.method == 'POST':
about = request.form['about'].replace('\r', '') about = trim_text(request.form['about'])
db.set_user_private_info(user.id, about) db.set_user_private_info(user.id, about)
flash('Updated profile', 'success') flash('Updated profile', 'success')
else: else:
@@ -119,6 +129,7 @@ def user_edit():
return render_template( return render_template(
'user_edit.html', 'user_edit.html',
title = 'Edit profile', title = 'Edit profile',
config = config,
user = user, user = user,
about = about about = about
) )
@@ -129,6 +140,7 @@ def user_info(user_id):
return render_template( return render_template(
'user_info.html', 'user_info.html',
title = 'Profile', title = 'Profile',
config = config,
user = get_user(), user = get_user(),
name = name, name = name,
about = about about = about
@@ -141,13 +153,14 @@ def new_thread(forum_id):
return redirect(url_for('login')) return redirect(url_for('login'))
if request.method == 'POST': if request.method == 'POST':
id, = db.add_thread(user_id, forum_id, request.form['title'], request.form['text'].replace('\r', ''), time.time_ns()) id, = db.add_thread(user_id, forum_id, request.form['title'], trim_text(request.form['text']), time.time_ns())
flash('Created thread', 'success') flash('Created thread', 'success')
return redirect(url_for('thread', thread_id = id)) return redirect(url_for('thread', thread_id = id))
return render_template( return render_template(
'new_thread.html', 'new_thread.html',
title = 'Create new thread', title = 'Create new thread',
config = config,
user = get_user(), user = get_user(),
) )
@@ -157,6 +170,7 @@ def confirm_delete_thread(thread_id):
return render_template( return render_template(
'confirm_delete_thread.html', 'confirm_delete_thread.html',
title = 'Delete thread', title = 'Delete thread',
config = config,
user = get_user(), user = get_user(),
thread_title = title, thread_title = title,
) )
@@ -180,7 +194,7 @@ def add_comment(thread_id):
if user_id is None: if user_id is None:
return redirect(url_for('login')) return redirect(url_for('login'))
if db.add_comment_to_thread(thread_id, user_id, request.form['text'].replace('\r', ''), time.time_ns()): if db.add_comment_to_thread(thread_id, user_id, trim_text(request.form['text']), time.time_ns()):
flash('Added comment', 'success') flash('Added comment', 'success')
else: else:
flash('Failed to add comment', 'error') flash('Failed to add comment', 'error')
@@ -192,7 +206,7 @@ def add_comment_parent(comment_id):
if user_id is None: if user_id is None:
return redirect(url_for('login')) return redirect(url_for('login'))
if db.add_comment_to_comment(comment_id, user_id, request.form['text'].replace('\r', ''), time.time_ns()): if db.add_comment_to_comment(comment_id, user_id, trim_text(request.form['text']), time.time_ns()):
flash('Added comment', 'success') flash('Added comment', 'success')
else: else:
flash('Failed to add comment', 'error') flash('Failed to add comment', 'error')
@@ -204,6 +218,7 @@ def confirm_delete_comment(comment_id):
return render_template( return render_template(
'confirm_delete_comment.html', 'confirm_delete_comment.html',
title = 'Delete comment', title = 'Delete comment',
config = config,
user = get_user(), user = get_user(),
thread_title = title, thread_title = title,
text = text, text = text,
@@ -233,7 +248,7 @@ def edit_thread(thread_id):
thread_id, thread_id,
user_id, user_id,
request.form['title'], request.form['title'],
request.form['text'].replace('\r', ''), trim_text(request.form['text']),
time.time_ns(), time.time_ns(),
): ):
flash('Thread has been edited', 'success') flash('Thread has been edited', 'success')
@@ -246,6 +261,7 @@ def edit_thread(thread_id):
return render_template( return render_template(
'edit_thread.html', 'edit_thread.html',
title = 'Edit thread', title = 'Edit thread',
config = config,
user = get_user(), user = get_user(),
thread_title = title, thread_title = title,
text = text, text = text,
@@ -261,7 +277,7 @@ def edit_comment(comment_id):
if db.modify_comment( if db.modify_comment(
comment_id, comment_id,
user_id, user_id,
request.form['text'].replace('\r', ''), trim_text(request.form['text']),
time.time_ns(), time.time_ns(),
): ):
flash('Comment has been edited', 'success') flash('Comment has been edited', 'success')
@@ -274,6 +290,7 @@ def edit_comment(comment_id):
return render_template( return render_template(
'edit_comment.html', 'edit_comment.html',
title = 'Edit comment', title = 'Edit comment',
config = config,
user = get_user(), user = get_user(),
thread_title = title, thread_title = title,
text = text, text = text,
@@ -288,7 +305,7 @@ def register():
elif len(password) < 8: elif len(password) < 8:
flash('Password must be at least 8 characters long', 'error') flash('Password must be at least 8 characters long', 'error')
elif not captcha.verify( elif not captcha.verify(
captcha_key, config.captcha_key,
request.form['captcha'], request.form['captcha'],
request.form['answer'], request.form['answer'],
): ):
@@ -299,10 +316,11 @@ def register():
flash('Account has been created. You can login now.', 'success') flash('Account has been created. You can login now.', 'success')
return redirect(url_for('index')) return redirect(url_for('index'))
capt, answer = captcha.generate(captcha_key) capt, answer = captcha.generate(config.captcha_key)
return render_template( return render_template(
'register.html', 'register.html',
title = 'Register', title = 'Register',
config = config,
user = get_user(), user = get_user(),
captcha = capt, captcha = capt,
answer = answer, answer = answer,
@@ -310,43 +328,47 @@ def register():
@app.route('/admin/') @app.route('/admin/')
def admin(): def admin():
user = get_user() chk, user = _admin_check()
if user is None: if not chk:
return redirect(url_for('login')) return user
if not user.is_admin():
return '<h1>Forbidden</h1>', 403
return render_template( return render_template(
'admin/index.html', 'admin/index.html',
title = 'Admin panel', title = 'Admin panel',
config = config,
forums = db.get_forums(), forums = db.get_forums(),
users = db.get_users(), users = db.get_users(),
) )
@app.route('/admin/query/', methods = ['GET', 'POST']) @app.route('/admin/query/', methods = ['GET', 'POST'])
def admin_query(): def admin_query():
user = get_user() chk, user = _admin_check()
if user is None: if not chk:
return redirect(url_for('login')) return user
if not user.is_admin():
return '<h1>Forbidden</h1>', 403
try: try:
rows = db.query(request.form['q']) if request.method == 'POST' else [] rows, rowcount = db.query(request.form['q']) if request.method == 'POST' else []
if rowcount > 0:
flash(f'{rowcount} rows changed', 'success')
except Exception as e: except Exception as e:
flash(e, 'error') flash(e, 'error')
rows = [] rows = []
return render_template( return render_template(
'admin/query.html', 'admin/query.html',
title = 'Query', title = 'Query',
config = config,
rows = rows, rows = rows,
) )
@app.route('/admin/forum/<int:forum_id>/edit/<string:what>/', methods = ['POST']) @app.route('/admin/forum/<int:forum_id>/edit/<string:what>/', methods = ['POST'])
def admin_edit_forum(forum_id, what): def admin_edit_forum(forum_id, what):
chk, user = _admin_check()
if not chk:
return user
try: try:
if what == 'description': if what == 'description':
res = db.set_forum_description(forum_id, request.form['description'].replace('\r', '')) res = db.set_forum_description(forum_id, trim_text(request.form['description']))
elif what == 'name': elif what == 'name':
res = db.set_forum_name(forum_id, request.form['name']) res = db.set_forum_name(forum_id, request.form['name'])
else: else:
@@ -362,13 +384,60 @@ def admin_edit_forum(forum_id, what):
@app.route('/admin/forum/new/', methods = ['POST']) @app.route('/admin/forum/new/', methods = ['POST'])
def admin_new_forum(): def admin_new_forum():
chk, user = _admin_check()
if not chk:
return user
try: try:
db.add_forum(request.form['name'], request.form['description'].replace('\r', '')) db.add_forum(request.form['name'], trim_text(request.form['description']))
flash('Added forum', 'success') flash('Added forum', 'success')
except Exception as e: except Exception as e:
flash(str(e), 'error') flash(str(e), 'error')
return redirect(url_for('admin')) return redirect(url_for('admin'))
@app.route('/admin/config/edit/', methods = ['POST'])
def admin_edit_config():
print('what')
chk, user = _admin_check()
if not chk:
return user
try:
db.set_config(
request.form['server_name'],
trim_text(request.form['server_description']),
'registration_enabled' in request.form,
)
flash('Updated config. Refresh the page to see the changes.', 'success')
restart()
except Exception as e:
flash(str(e), 'error')
return redirect(url_for('admin'))
@app.route('/admin/config/new_secrets/', methods = ['POST'])
def admin_new_secrets():
chk, user = _admin_check()
if not chk:
return user
secret_key = secrets.token_urlsafe(30)
captcha_key = secrets.token_urlsafe(30)
try:
db.set_config_secrets(secret_key, captcha_key)
flash('Changed secrets. You will be logged out.', 'success')
restart()
except Exception as e:
flash(str(e), 'error')
return redirect(url_for('admin'))
def _admin_check():
user = get_user()
if user is None:
return False, redirect(url_for('login'))
if not user.is_admin():
return False, ('<h1>Forbidden</h1>', 403)
return True, user
class Comment: class Comment:
def __init__(self, id, author_id, author, text, create_time, modify_time, parent_id): def __init__(self, id, author_id, author, text, create_time, modify_time, parent_id):
@@ -494,3 +563,24 @@ def hash_password(password):
def verify_password(password, hash): def verify_password(password, hash):
return passlib.hash.argon2.verify(password, hash) return passlib.hash.argon2.verify(password, hash)
def restart():
'''
Shut down *all* workers and spawn new ones.
This is necessary on e.g. a configuration change.
Since restarting workers depends is platform-dependent this task is delegated to an external
program.
'''
r = subprocess.call(['./restart.sh'])
if r == 0:
flash('Restart script exited successfully', 'success')
else:
flash(f'Restart script exited with error (code {r})', 'error')
def trim_text(s):
'''
Because browsers LOVE \\r, trailing whitespace etc.
'''
return s.strip().replace('\r', '')

4
restart.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# This script is intended for dev environments only.
touch main.py

View File

@@ -1,3 +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
);
create table users ( create table users (
user_id integer unique not null primary key autoincrement, user_id integer unique not null primary key autoincrement,
name varchar(32) unique not null, name varchar(32) unique not null,

View File

@@ -7,22 +7,28 @@
<input type=submit value=Submit> <input type=submit value=Submit>
</form> </form>
<h2>Configuration</h2> <h2>Configuration</h2>
<form action=config/edit/ method=post>
<table> <table>
<tr> <tr>
<td>Name</td> <td>Server name</td>
<td> <td><input type=text name=server_name value="{{ config.server_name }}"></td>
<input type=text value="Agrepy">
<input type=submit value=Set>
</td>
</tr> </tr>
<tr> <tr>
<td>Registrations enabled</td> <td>Server description</td>
<td> <td><textarea name=server_description>{{ config.server_description }}</textarea></td>
<input type=checkbox checked> </tr>
<input type=submit value=Set> <tr>
</td> <td>Registration enabled</td>
<td><input name=registration_enabled type=checkbox {{ 'checked' if config.registration_enabled else '' }}></td>
</tr> </tr>
</table> </table>
<input type=submit value=Update>
</form>
<p>
<form action=config/new_secrets/ method=post>
<input type=submit value="Generate new secrets">
</form>
</p>
<h2>Forums</h2> <h2>Forums</h2>
<table> <table>
<tr> <tr>

View File

@@ -9,25 +9,27 @@
<nav> <nav>
<a class=logo href="{{ url_for('index') }}">A</a> <a class=logo href="{{ url_for('index') }}">A</a>
<div style="margin:auto"></div> <div style="margin:auto"></div>
{% if user is not none %} {%- if user is not none -%}
<a href="{{ url_for('user_edit') }}">{{ user.name }}</a> <a href="{{ url_for('user_edit') }}">{{ user.name }}</a>
<span> | </span> <span> | </span>
{% if user.is_admin() %} {%- if user.is_admin() -%}
<a href="{{ url_for('admin') }}">Admin panel</a> <a href="{{ url_for('admin') }}">Admin panel</a>
<span> | </span> <span> | </span>
{% endif %} {%- endif -%}
<a href="{{ url_for('logout') }}">Logout</a> <a href="{{ url_for('logout') }}">Logout</a>
{% else %} {%- else -%}
{%- if config.registration_enabled -%}
<a href="{{ url_for('register') }}">Register</a> <a href="{{ url_for('register') }}">Register</a>
<span> | </span> <span> | </span>
{%- endif -%}
<a href="{{ url_for('login') }}">Login</a> <a href="{{ url_for('login') }}">Login</a>
{% endif %} {%- endif -%}
</nav> </nav>
<main> <main>
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
{% 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>
{% endfor %} {%- endfor -%}
{% block content %}{% endblock %} {%- block content %}{% endblock -%}
</main> </main>
</body> </body>

View File

@@ -1,6 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {%- block content %}
<p>{{ minimd(description) | safe }}</p>
<table> <table>
<tr> <tr>
<th>Forum</th> <th>Forum</th>
@@ -23,4 +24,4 @@
</tr> </tr>
{%- endfor -%} {%- endfor -%}
</table> </table>
{% endblock %} {%- endblock %}

View File

@@ -1,6 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{%- block content %} {%- block content %}
{%- if config.registration_enabled -%}
<form method="post" class=login> <form method="post" class=login>
<table> <table>
<tr><td>Username</td><td><input type="text" name="username" minlength=3></td></tr> <tr><td>Username</td><td><input type="text" name="username" minlength=3></td></tr>
@@ -10,4 +11,7 @@
<input name="answer" value="{{ answer }}" hidden> <input name="answer" value="{{ answer }}" hidden>
<input type="submit" value="Register"> <input type="submit" value="Register">
</form> </form>
{% endblock %} {%- else -%}
<p>Registrations are disabled.</p>
{%- endif %}
{%- endblock %}

View File

@@ -15,7 +15,7 @@ db=$tmp/forum.db
. $base/../venv/bin/activate . $base/../venv/bin/activate
# initialize db # initialize db
$SQLITE $db < $base/../schema.txt $base/../init_sqlite.sh $db
$SQLITE $db < $base/init_db.txt $SQLITE $db < $base/init_db.txt
cd $base/.. cd $base/..