diff --git a/db/sqlite.py b/db/sqlite.py index dad5562..c13fe36 100644 --- a/db/sqlite.py +++ b/db/sqlite.py @@ -425,11 +425,21 @@ def register_user(self, username, password, time): ) if c.rowcount > 0: db.commit() - return True - return False + # TODO find a way to get the (autoincremented) user ID without looking + # up by name. + # ROWID is *probably* not always consistent (race conditions). + # Ideally we get the ID immediately on insert. + return c.execute(''' + select user_id + from users + where name = lower(?) + ''', + (username,) + ).fetchone() + return None except sqlite3.IntegrityError: # User already exists, probably - return False + return None def add_user(self, username, password, time): ''' diff --git a/main.py b/main.py index 3cd16ad..4a6ec54 100644 --- a/main.py +++ b/main.py @@ -234,34 +234,39 @@ def delete_thread(thread_id): # TODO return 403, maybe? return redirect(url_for('index')) -@app.route('/thread//comment/', methods = ['POST']) -def add_comment(thread_id): +def _add_comment_check_user(): user_id = session.get('user_id') - if user_id is None: - return redirect(url_for('login')) + if user_id is not None: + return user_id + if not config.registration_enabled: + flash('Registrations are not enabled. Please log in to comment', 'error') + if register_user(True): + return session['user_id'] - text = trim_text(request.form['text']) - if text == '': - flash('Text may not be empty', 'error') - elif db.add_comment_to_thread(thread_id, user_id, text, time.time_ns()): - flash('Added comment', 'success') - else: - flash('Failed to add comment', 'error') +@app.route('/thread//comment/', methods = ['POST']) +def add_comment(thread_id): + user_id = _add_comment_check_user() + if user_id is not None: + text = trim_text(request.form['text']) + if text == '': + flash('Text may not be empty', 'error') + elif db.add_comment_to_thread(thread_id, user_id, text, time.time_ns()): + flash('Added comment', 'success') + else: + flash('Failed to add comment', 'error') return redirect(url_for('thread', thread_id = thread_id)) @app.route('/comment//comment/', methods = ['POST']) def add_comment_parent(comment_id): - user_id = session.get('user_id') - if user_id is None: - return redirect(url_for('login')) - - text = trim_text(request.form['text']) - if text == '': - flash('Text may not be empty', 'error') - elif db.add_comment_to_comment(comment_id, user_id, text, time.time_ns()): - flash('Added comment', 'success') - else: - flash('Failed to add comment', 'error') + user_id = _add_comment_check_user() + if user_id is not None: + text = trim_text(request.form['text']) + if text == '': + flash('Text may not be empty', 'error') + elif db.add_comment_to_comment(comment_id, user_id, text, time.time_ns()): + flash('Added comment', 'success') + else: + flash('Failed to add comment', 'error') return redirect(url_for('comment', comment_id = comment_id)) @app.route('/comment//confirm_delete/') @@ -358,23 +363,7 @@ def edit_comment(comment_id): def register(): if request.method == 'POST': username, passwd = request.form['username'], request.form['password'] - if any(c in username for c in string.whitespace): - # This error is more ergonomic in case someone tries to play tricks again :) - flash('Username may not contain whitespace', 'error') - elif len(username) < 3: - flash('Username must be at least 3 characters long', 'error') - elif len(passwd) < 8: - flash('Password must be at least 8 characters long', 'error') - elif not captcha.verify( - config.captcha_key, - request.form['captcha'], - request.form['answer'], - ): - flash('CAPTCHA answer is incorrect', 'error') - elif not db.register_user(username, password.hash(passwd), time.time_ns()): - flash('Failed to create account (username may already be taken)', 'error') - else: - flash('Account has been created. You can login now.', 'success') + if register_user(False): return redirect(url_for('index')) capt, answer = captcha.generate(config.captcha_key) @@ -715,6 +704,35 @@ def get_user(): return User(id, name, role, banned_until) return None +def register_user(show_password): + username, passwd = request.form['username'], request.form['password'] + if any(c in username for c in string.whitespace): + # This error is more ergonomic in case someone tries to play tricks again :) + flash('Username may not contain whitespace', 'error') + elif len(username) < 3: + flash('Username must be at least 3 characters long', 'error') + elif len(passwd) < 8: + flash('Password must be at least 8 characters long', 'error') + elif not captcha.verify( + config.captcha_key, + request.form['captcha'], + request.form['answer'], + ): + flash('CAPTCHA answer is incorrect', 'error') + else: + uid = db.register_user(username, password.hash(passwd), time.time_ns()) + if uid is None: + flash('Failed to create account (username may already be taken)', 'error') + else: + s = 'Account has been created.' + if show_password: + s += f' Your password is {passwd} (hover to reveal).' + flash(s, 'success') + uid, = uid + session['user_id'] = uid + return True + return False + @app.context_processor def utility_processor(): @@ -764,11 +782,26 @@ def format_until(t): def format_time(t): return datetime.utcfromtimestamp(t / 10 ** 9).replace(microsecond=0) + def rand_password(): + ''' + Generate a random password. + + The current implementation returns 12 random lower- and uppercase alphabet characters. + This gives up to `log((26 * 2) ** 12) / log(2) = ~68` bits of entropy, which should be + enough for the foreseeable future. + ''' + return ''.join(string.ascii_letters[secrets.randbelow(52)] for _ in range(12)) + + def gen_captcha(): + return captcha.generate(config.captcha_key) + return { 'format_since': format_since, 'format_time': format_time, 'format_until': format_until, 'minimd': minimd.html, + 'rand_password': rand_password, + 'gen_captcha': gen_captcha, } diff --git a/static/theme.css b/static/theme.css index fd8628f..426effe 100644 --- a/static/theme.css +++ b/static/theme.css @@ -55,17 +55,21 @@ th, td { } textarea { - width: 50em; + width: min(100%, 500px); height: 15em; font-size: 1em; } -input[type=text] { - width: 50em; +input[type=text], input[type=password] { + width: min(100%, 20em); font-family: monospace; font-size: 1em; } +td > input[type=text], td > input[type=password] { + width: min(100%, 500px); +} + .logo { margin: 0; padding: 5px; @@ -74,9 +78,13 @@ input[type=text] { font-weight: bold; } +form.form { + width: 90%; +} + table.form { border-collapse: unset; - width: auto; + width: 100%; } table.form > * > tr > td, th { @@ -103,14 +111,6 @@ table.form > * > tr > td, th { padding: 8px; } -.login { - width: 50%; -} - -.login input[type=text], .login input[type=password] { - width: 90%; -} - /* Abuse checkboxes to collapse comments */ .collapse { appearance: none; @@ -129,3 +129,12 @@ table.form > * > tr > td, th { .small { font-size: 85%; } + +.spoiler { + background-color: black; + color: black; +} +.spoiler:hover { + opacity: 1; + color: white; +} diff --git a/templates/base.html b/templates/base.html index ca786d6..df21e1a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -34,7 +34,11 @@

{{ title }}

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

{{ msg }}

+ {#- + FIXME ensure all flash() messages are free of XSS vectors. + In particular, check places where we flash error messages. + -#} +

{{ msg | safe }}

{%- endfor -%} {%- block content %}{% endblock -%}
diff --git a/templates/comment.html b/templates/comment.html index d4b4024..f5c089f 100644 --- a/templates/comment.html +++ b/templates/comment.html @@ -57,10 +57,26 @@ {%- endmacro -%} {%- macro reply() -%} -{%- if user is not none and not user.is_banned() -%} +{%- if user is none -%} +{%- if config.registration_enabled -%}
-

-

+

+{#- + Using the password generator for usernames should be sufficient to ensure it is unique. + If not, it means the password generator is broken and *must* be fixed. +-#} + + +{% set q, a = gen_captcha() %} +

Captcha: {{ q }}

+ +

(I already have an account)

+
+{%- endif -%} +{%- elif not user.is_banned() -%} +
+

+

{%- endif -%} {%- endmacro -%} diff --git a/test/all.sh b/test/all.sh index 691bcb7..8a83a11 100755 --- a/test/all.sh +++ b/test/all.sh @@ -21,4 +21,4 @@ cd $base/.. export DB=$db export SERVER=dev -$FLASK --app main --debug run +$FLASK --app main --debug run $TEST_FLASK_ARGS diff --git a/test/init_db.txt b/test/init_db.txt index d46a34c..cc01c5f 100644 --- a/test/init_db.txt +++ b/test/init_db.txt @@ -29,3 +29,5 @@ insert into comments (author_id, thread_id, create_time, modify_time, text) values (2, 1, 0, 0, "Hi!"); insert into comments (author_id, thread_id, create_time, modify_time, text, parent_id) values (3, 1, 0, 0, "Greetings.", 1); + +update config set registration_enabled = 1;