Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-register user on comment #13

Merged
merged 4 commits into from
Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;