add user signup flow

This commit is contained in:
2026-04-17 10:45:37 +03:00
parent 84e69187ff
commit 9d8404b774
4 changed files with 83 additions and 4 deletions

View File

@@ -4,6 +4,7 @@ from argon2 import PasswordHasher
from functools import wraps
import secrets
import time
import re
ph = PasswordHasher()
@@ -44,6 +45,15 @@ def create_session(user_id, temporary=False):
'expires_at': int(time.time()) + (expires_days * 24 * 60 * 60),
})
def parse_username(username: str) -> Tuple[str, str]:
if len(username) < 3:
raise ValueError
invalid_regex = r'[^a-zA-Z0-9_-]'
return username, re.sub(invalid_regex, '_', username.lower())[:20]
def is_password_valid(password: str) -> bool:
return re.match(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}$', password) is not None
# annotations
def login_required(view_func):
@wraps(view_func)

View File

@@ -1,8 +1,10 @@
from flask import Blueprint, redirect, url_for, render_template, request, session
from functools import wraps
import time
from ..auth import digest, verify, create_session, is_logged_in
from ..auth import digest, verify, create_session, is_logged_in, parse_username, is_password_valid
from ..models import Users
from ..constants import PermissionLevel
bp = Blueprint('users', __name__, url_prefix='/users/')
@@ -39,10 +41,53 @@ def log_in_post():
session.permanent = True
return redirect(request.form.get('return_to', default=url_for('topics.all_topics')))
@bp.get('/sign-up')
@bp.get('/sign-up/')
@redirect_if_logged_in()
def sign_up():
return 'stub'
return render_template('users/sign_up.html')
@bp.post('/sign-up/')
@redirect_if_logged_in()
def sign_up_post():
generic_error_page = redirect(url_for('.sign_up', error='The username or password you entered is invalid.'))
user_exists_error_page = redirect(url_for('.sign_up', error='This username is already taken. Please pick another.'))
passwords_error_page = redirect(url_for('.sign_up', error='The passwords do not match.'))
username = request.form.get('username', default='')
if not username:
return generic_error_page
if request.form.get('password', default=None) is None:
return generic_error_page
if len(request.form.getlist('password')) != 2:
return passwords_error_page
username_pair = parse_username(username)
potential_user = Users.find({'username': username})
if potential_user:
return user_exists_error_page
if request.form.getlist('password')[0] != request.form.getlist('password')[1]:
return passwords_error_page
password_hash = digest(request.form.get('password'))
user = Users.create({
'username': username_pair[0],
'password_hash': password_hash,
'permission': PermissionLevel.GUEST.value,
'created_at': int(time.time()),
})
if username_pair[0] != username_pair[1]:
user.update({
'display_name': username_pair[1]
})
session['remember'] = request.form.get('remember') == 'on'
sess = create_session(user.id, not session['remember'])
session['pyrom_session_key'] = sess.key
if session['remember']:
session.permanent = True
return redirect(url_for('topics.all_topics'))
@bp.get('/<username>')
def user_page(username):

View File

@@ -13,7 +13,7 @@
{%- endif %}
</ul>
{%- endwith -%}
{%- else -%}
{%- elif request.path != url_for('users.sign_up') and request.path != url_for('users.log_in') -%}
<form class="horizontal wrap" method="POST" action="{{url_for('users.log_in_post')}}">
<input type="hidden" name="return_to" value="{{request.path}}">
<input type="text" placeholder="Username" name="username" autocomplete="username" required>

View File

@@ -0,0 +1,24 @@
{% from 'common/macros.html' import subheader %}
{%- extends 'base.html' -%}
{%- block title -%}sign up{%- endblock -%}
{%- block content -%}
{%- set welcome -%}
Please read the rules etc. stub
{%- endset -%}
{{ subheader('Sign up', welcome)}}
{%- if request.args.get('error') -%}
<div class="infobox plank critical">
{{request.args.get('error')}}
</div>
{%- endif -%}
<form class="plank primary-bg full-width" method="POST">
<label for="username">Username</label>
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_\-]{3,20}" title="3-20 characters. Only upper and lowercase letters, digits, hyphens, and underscores" autocomplete="username" required>
<label for="password">Create password</label>
<input type="password" id="password" name="password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" autocomplete="new-password" required>
<label for="password2">Confirm password</label>
<input type="password" id="password2" name="password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" autocomplete="new-password" required>
<span><input type="checkbox" name="remember" id="remember"> <label for="remember">Remember me</label></span>
<input type="submit" value="Sign up">
</form>
{%- endblock -%}