Skip to content

Commit

Permalink
Merge pull request #13 from Demindiro/register-on-comment
Browse files Browse the repository at this point in the history
  • Loading branch information
Demindiro authored Oct 20, 2022
2 parents 23c9b35 + 736ef17 commit deb0b15
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 59 deletions.
16 changes: 13 additions & 3 deletions db/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
'''
Expand Down
111 changes: 72 additions & 39 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,34 +234,39 @@ def delete_thread(thread_id):
# TODO return 403, maybe?
return redirect(url_for('index'))

@app.route('/thread/<int:thread_id>/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/<int:thread_id>/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/<int:comment_id>/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/<int:comment_id>/confirm_delete/')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 <code class=spoiler>{passwd}</code> (hover to reveal).'
flash(s, 'success')
uid, = uid
session['user_id'] = uid
return True
return False


@app.context_processor
def utility_processor():
Expand Down Expand Up @@ -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,
}


Expand Down
33 changes: 21 additions & 12 deletions static/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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;
}
6 changes: 5 additions & 1 deletion templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@
<main>
<h1>{{ title }}</h1>
{%- for category, msg in get_flashed_messages(True) -%}
<p class="flash {{ category }}">{{ msg }}</p>
{#-
FIXME ensure all flash() messages are free of XSS vectors.
In particular, check places where we flash error messages.
-#}
<p class="flash {{ category }}">{{ msg | safe }}</p>
{%- endfor -%}
{%- block content %}{% endblock -%}
</main>
Expand Down
22 changes: 19 additions & 3 deletions templates/comment.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 -%}
<form method="post" action="comment/">
<p><textarea name="text"></textarea></p>
<p><input type="submit" value="Post comment"></p>
<p><textarea name=text></textarea></p>
{#-
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.
-#}
<input type=text name=username value="{{ rand_password() }}" hidden>
<input type=password name=password value="{{ rand_password() }}" hidden>
{% set q, a = gen_captcha() %}
<p>Captcha: {{ q }} <input type=text name=captcha></p>
<input type=text name=answer value="{{ a }}" hidden>
<p><input type=submit value="Register & post comment"> (<a href="{{ url_for('login') }}">I already have an account</a>)</p>
</form>
{%- endif -%}
{%- elif not user.is_banned() -%}
<form method="post" action="comment/">
<p><textarea name="text"></textarea></p>
<p><input type="submit" value="Post comment"></p>
</form>
{%- endif -%}
{%- endmacro -%}
2 changes: 1 addition & 1 deletion test/all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions test/init_db.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit deb0b15

Please sign in to comment.