Merge pull request #13 from Demindiro/register-on-comment
This commit is contained in:
16
db/sqlite.py
16
db/sqlite.py
@@ -425,11 +425,21 @@ class DB:
|
|||||||
)
|
)
|
||||||
if c.rowcount > 0:
|
if c.rowcount > 0:
|
||||||
db.commit()
|
db.commit()
|
||||||
return True
|
# TODO find a way to get the (autoincremented) user ID without looking
|
||||||
return False
|
# 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:
|
except sqlite3.IntegrityError:
|
||||||
# User already exists, probably
|
# User already exists, probably
|
||||||
return False
|
return None
|
||||||
|
|
||||||
def add_user(self, username, password, time):
|
def add_user(self, username, password, time):
|
||||||
'''
|
'''
|
||||||
|
|||||||
83
main.py
83
main.py
@@ -234,12 +234,19 @@ def delete_thread(thread_id):
|
|||||||
# TODO return 403, maybe?
|
# TODO return 403, maybe?
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
def _add_comment_check_user():
|
||||||
|
user_id = session.get('user_id')
|
||||||
|
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']
|
||||||
|
|
||||||
@app.route('/thread/<int:thread_id>/comment/', methods = ['POST'])
|
@app.route('/thread/<int:thread_id>/comment/', methods = ['POST'])
|
||||||
def add_comment(thread_id):
|
def add_comment(thread_id):
|
||||||
user_id = session.get('user_id')
|
user_id = _add_comment_check_user()
|
||||||
if user_id is None:
|
if user_id is not None:
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
text = trim_text(request.form['text'])
|
text = trim_text(request.form['text'])
|
||||||
if text == '':
|
if text == '':
|
||||||
flash('Text may not be empty', 'error')
|
flash('Text may not be empty', 'error')
|
||||||
@@ -251,10 +258,8 @@ def add_comment(thread_id):
|
|||||||
|
|
||||||
@app.route('/comment/<int:comment_id>/comment/', methods = ['POST'])
|
@app.route('/comment/<int:comment_id>/comment/', methods = ['POST'])
|
||||||
def add_comment_parent(comment_id):
|
def add_comment_parent(comment_id):
|
||||||
user_id = session.get('user_id')
|
user_id = _add_comment_check_user()
|
||||||
if user_id is None:
|
if user_id is not None:
|
||||||
return redirect(url_for('login'))
|
|
||||||
|
|
||||||
text = trim_text(request.form['text'])
|
text = trim_text(request.form['text'])
|
||||||
if text == '':
|
if text == '':
|
||||||
flash('Text may not be empty', 'error')
|
flash('Text may not be empty', 'error')
|
||||||
@@ -358,23 +363,7 @@ def edit_comment(comment_id):
|
|||||||
def register():
|
def register():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
username, passwd = request.form['username'], request.form['password']
|
username, passwd = request.form['username'], request.form['password']
|
||||||
if any(c in username for c in string.whitespace):
|
if register_user(False):
|
||||||
# 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')
|
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
capt, answer = captcha.generate(config.captcha_key)
|
capt, answer = captcha.generate(config.captcha_key)
|
||||||
@@ -715,6 +704,35 @@ def get_user():
|
|||||||
return User(id, name, role, banned_until)
|
return User(id, name, role, banned_until)
|
||||||
return None
|
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
|
@app.context_processor
|
||||||
def utility_processor():
|
def utility_processor():
|
||||||
@@ -764,11 +782,26 @@ def utility_processor():
|
|||||||
def format_time(t):
|
def format_time(t):
|
||||||
return datetime.utcfromtimestamp(t / 10 ** 9).replace(microsecond=0)
|
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 {
|
return {
|
||||||
'format_since': format_since,
|
'format_since': format_since,
|
||||||
'format_time': format_time,
|
'format_time': format_time,
|
||||||
'format_until': format_until,
|
'format_until': format_until,
|
||||||
'minimd': minimd.html,
|
'minimd': minimd.html,
|
||||||
|
'rand_password': rand_password,
|
||||||
|
'gen_captcha': gen_captcha,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,17 +55,21 @@ th, td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 50em;
|
width: min(100%, 500px);
|
||||||
height: 15em;
|
height: 15em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=text] {
|
input[type=text], input[type=password] {
|
||||||
width: 50em;
|
width: min(100%, 20em);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td > input[type=text], td > input[type=password] {
|
||||||
|
width: min(100%, 500px);
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@@ -74,9 +78,13 @@ input[type=text] {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.form {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
table.form {
|
table.form {
|
||||||
border-collapse: unset;
|
border-collapse: unset;
|
||||||
width: auto;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.form > * > tr > td, th {
|
table.form > * > tr > td, th {
|
||||||
@@ -103,14 +111,6 @@ table.form > * > tr > td, th {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login input[type=text], .login input[type=password] {
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Abuse checkboxes to collapse comments */
|
/* Abuse checkboxes to collapse comments */
|
||||||
.collapse {
|
.collapse {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
@@ -129,3 +129,12 @@ table.form > * > tr > td, th {
|
|||||||
.small {
|
.small {
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.spoiler {
|
||||||
|
background-color: black;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.spoiler:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,7 +34,11 @@
|
|||||||
<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>
|
{#-
|
||||||
|
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 -%}
|
{%- endfor -%}
|
||||||
{%- block content %}{% endblock -%}
|
{%- block content %}{% endblock -%}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -57,10 +57,26 @@
|
|||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
{%- macro reply() -%}
|
{%- 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/">
|
<form method="post" action="comment/">
|
||||||
<p><textarea name="text"></textarea></p>
|
<p><textarea name=text></textarea></p>
|
||||||
<p><input type="submit" value="Post comment"></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>
|
</form>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ cd $base/..
|
|||||||
|
|
||||||
export DB=$db
|
export DB=$db
|
||||||
export SERVER=dev
|
export SERVER=dev
|
||||||
$FLASK --app main --debug run
|
$FLASK --app main --debug run $TEST_FLASK_ARGS
|
||||||
|
|||||||
@@ -29,3 +29,5 @@ insert into comments (author_id, thread_id, create_time, modify_time, text)
|
|||||||
values (2, 1, 0, 0, "Hi!");
|
values (2, 1, 0, 0, "Hi!");
|
||||||
insert into comments (author_id, thread_id, create_time, modify_time, text, parent_id)
|
insert into comments (author_id, thread_id, create_time, modify_time, text, parent_id)
|
||||||
values (3, 1, 0, 0, "Greetings.", 1);
|
values (3, 1, 0, 0, "Greetings.", 1);
|
||||||
|
|
||||||
|
update config set registration_enabled = 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user