start work on invite keys
This commit is contained in:
@@ -139,6 +139,11 @@ def bind_default_badges(path):
|
||||
'uploaded_at': int(os.path.getmtime(real_path)),
|
||||
})
|
||||
|
||||
def clear_stale_invites():
|
||||
from .db import db
|
||||
from .util import time_now
|
||||
db.execute('DELETE FROM "invite_keys" WHERE expires_at < ?', time_now())
|
||||
|
||||
def clear_stale_sessions():
|
||||
from .db import db
|
||||
with db.transaction():
|
||||
@@ -234,6 +239,7 @@ def create_app():
|
||||
|
||||
clear_stale_sessions()
|
||||
clear_api_limits()
|
||||
clear_stale_invites()
|
||||
|
||||
reparse_babycode()
|
||||
|
||||
|
||||
@@ -43,7 +43,8 @@ MIGRATIONS = [
|
||||
add_signature_format,
|
||||
create_default_bookmark_collections,
|
||||
add_display_name,
|
||||
'ALTER TABLE "post_history" ADD COLUMN "content_rss" STRING DEFAULT NULL'
|
||||
'ALTER TABLE "post_history" ADD COLUMN "content_rss" STRING DEFAULT NULL',
|
||||
'ALTER TABLE "invite_keys" ADD COLUMN "expires_at" INTEGER NOT NULL DEFAULT 0',
|
||||
]
|
||||
|
||||
def run_migrations():
|
||||
|
||||
@@ -4,7 +4,7 @@ from flask import (
|
||||
abort, flash, current_app
|
||||
)
|
||||
from functools import wraps
|
||||
from secrets import compare_digest as compare_timesafe
|
||||
from secrets import compare_digest as compare_timesafe, token_urlsafe
|
||||
from wand.image import Image
|
||||
from wand.color import Color
|
||||
from wand.exceptions import WandException
|
||||
@@ -14,9 +14,9 @@ from ..auth import (
|
||||
login_required, revoke_session, get_active_user,
|
||||
parse_display_name, revoke_all_sessions, csrf_verified
|
||||
)
|
||||
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections
|
||||
from ..models import Users, Posts, Reactions, Threads, Avatars, PostHistory, Mentions, BookmarkCollections, InviteKeys
|
||||
from ..constants import PermissionLevel, InfoboxKind
|
||||
from ..util import get_form_checkbox
|
||||
from ..util import get_form_checkbox, time_now
|
||||
from ..lib.babycode import babycode_to_html
|
||||
from ..db import db
|
||||
import math
|
||||
@@ -169,15 +169,34 @@ def log_out():
|
||||
@bp.get('/sign-up/')
|
||||
@redirect_if_logged_in()
|
||||
def sign_up():
|
||||
return render_template('users/sign_up.html')
|
||||
key = request.args.get('key', '')
|
||||
if not key and current_app.config['DISABLE_SIGNUP']:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
elif key and current_app.config['DISABLE_SIGNUP']:
|
||||
invite = InviteKeys.find({'key': key})
|
||||
if not invite:
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
inviter = Users.find({'id': invite.created_by})
|
||||
return render_template('users/sign_up.html', invite=invite, inviter=inviter)
|
||||
|
||||
@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.'))
|
||||
invalid_username_error_page = redirect(url_for('.sign_up', error='This username cannot be used. Please pick another.'))
|
||||
passwords_error_page = redirect(url_for('.sign_up', error='The passwords do not match.'))
|
||||
args_sans_error = dict(request.args)
|
||||
args_sans_error.pop('error', '')
|
||||
generic_error_page = redirect(url_for('.sign_up', error='The username or password you entered is invalid.', **args_sans_error))
|
||||
invalid_username_error_page = redirect(url_for('.sign_up', error='This username cannot be used. Please pick another.', **args_sans_error))
|
||||
passwords_error_page = redirect(url_for('.sign_up', error='The passwords do not match.', **args_sans_error))
|
||||
username = request.form.get('username', default='')
|
||||
if current_app.config['DISABLE_SIGNUP']:
|
||||
key = request.form.get('key', '')
|
||||
if not key:
|
||||
return generic_error_page
|
||||
invite = InviteKeys.find({'key': key})
|
||||
if not invite:
|
||||
return generic_error_page
|
||||
if invite.expires_at < time_now():
|
||||
return generic_error_page
|
||||
if not username:
|
||||
return generic_error_page
|
||||
if request.form.get('password') is None:
|
||||
@@ -197,12 +216,18 @@ def sign_up_post():
|
||||
|
||||
password_hash = digest(request.form.get('password'))
|
||||
|
||||
user = Users.create({
|
||||
user_data = {
|
||||
'username': username_pair[0],
|
||||
'password_hash': password_hash,
|
||||
'permission': PermissionLevel.GUEST.value,
|
||||
'created_at': int(time.time()),
|
||||
})
|
||||
}
|
||||
if invite:
|
||||
user_data['invited_by'] = invite.created_by
|
||||
user_data['permission'] = PermissionLevel.USER.value
|
||||
invite.delete()
|
||||
|
||||
user = Users.create(user_data)
|
||||
|
||||
BookmarkCollections.create_default(user.id)
|
||||
|
||||
@@ -217,6 +242,7 @@ def sign_up_post():
|
||||
if session['remember']:
|
||||
session.permanent = True
|
||||
|
||||
flash(f'Welcome to {current_app.config['SITE_NAME']}!', InfoboxKind.INFO)
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
@bp.get('/<username>/')
|
||||
@@ -286,9 +312,11 @@ def comments(username):
|
||||
def settings(username):
|
||||
user = get_active_user()
|
||||
sort_by = session.get('sort_by', 'activity')
|
||||
invites = InviteKeys.findall({'created_by': user.id})
|
||||
return render_template(
|
||||
'users/settings.html', user=user,
|
||||
sort_by=sort_by
|
||||
sort_by=sort_by,
|
||||
invites=invites,
|
||||
)
|
||||
|
||||
@bp.post('/<username>/settings/set-avatar')
|
||||
@@ -539,3 +567,40 @@ def delete_confirm_post(username):
|
||||
user.delete()
|
||||
|
||||
return redirect(url_for('topics.all_topics'))
|
||||
|
||||
@bp.post('/<username>/invite-keys/create/')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
@csrf_verified
|
||||
def create_invite_key(username):
|
||||
user = get_active_user()
|
||||
if not user.can_invite():
|
||||
abort(404)
|
||||
|
||||
key = token_urlsafe(16)
|
||||
expires_at = time_now() + 48 * 60 * 60
|
||||
|
||||
invite = InviteKeys.create({
|
||||
'created_by': user.id,
|
||||
'expires_at': expires_at,
|
||||
'key': key,
|
||||
})
|
||||
|
||||
return redirect(url_for('.settings', username=username, _anchor='invite'))
|
||||
|
||||
@bp.post('/<username>/invite-keys/revoke/')
|
||||
@login_required
|
||||
@redirect_to_own
|
||||
@csrf_verified
|
||||
def revoke_invite_key(username):
|
||||
user = get_active_user()
|
||||
if not user.can_invite():
|
||||
abort(404)
|
||||
|
||||
key = request.form.get('key', '')
|
||||
invite = InviteKeys.find({'created_by': user.id, 'key': key})
|
||||
if not invite:
|
||||
abort(404)
|
||||
|
||||
invite.delete()
|
||||
return redirect(url_for('.settings', username=username, _anchor='invite'))
|
||||
|
||||
@@ -187,6 +187,8 @@ SCHEMA = [
|
||||
|
||||
'CREATE INDEX IF NOT EXISTS idx_badge_upload_user ON badge_uploads(user_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_badge_user ON badges(user_id)',
|
||||
|
||||
'CREATE INDEX IF NOT EXISTS idx_invite_key_user ON invite_keys(created_by, key)'
|
||||
]
|
||||
|
||||
def create():
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
<input type="password" placeholder="Password" name="password" autocomplete="current-password" required>
|
||||
<span><input type="checkbox" name="remember" id="remember"> <label for="remember">Remember me</label></span>
|
||||
<input type="submit" value="Log in">
|
||||
{%- if not config.DISABLE_SIGNUP -%}
|
||||
<a href="{{url_for('users.sign_up')}}" class="linkbutton alt">Sign up</a>
|
||||
{%- endif -%}
|
||||
</form>
|
||||
{%- endif -%}
|
||||
</nav>
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
{%- if thread_count > 0 -%}
|
||||
<details class="inner" data-id="{{collection.id}}" data-r="restoreThreadDetails setThreadDetails" data-s="setThreadDetails">
|
||||
<summary class="plank no-shadow even">Threads</summary>
|
||||
<table class="three-cols">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="plank even no-shadow contrast-bg">Title</th>
|
||||
<th class="plank even no-shadow contrast-bg">Memo</th>
|
||||
<th class="plank even no-shadow contrast-bg">Manage</th>
|
||||
<th class="plank even no-shadow contrast-bg" style="--w:65%">Title</th>
|
||||
<th class="plank even no-shadow contrast-bg" style="--w:25%">Memo</th>
|
||||
<th class="plank even no-shadow contrast-bg" style="--w:10%">Manage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{%- from 'common/macros.html' import babycode_editor_component -%}
|
||||
{%- from 'common/macros.html' import subheader, avatar -%}
|
||||
{%- from 'common/macros.html' import subheader, avatar, timestamp -%}
|
||||
{%- extends 'base.html' -%}
|
||||
{%- block title -%}settings{%- endblock -%}
|
||||
{%- block content -%}
|
||||
@@ -76,6 +76,45 @@
|
||||
<div>Loading badges…</div>
|
||||
<div>If badges fail to load, make sure JS is enabled.</div>
|
||||
</fieldset>
|
||||
{%- if user.can_invite() -%}
|
||||
<fieldset class="plank" id="invite">
|
||||
<legend>Invite keys</legend>
|
||||
<p>To manage growth, {{ config.SITE_NAME }} disallows direct sign ups. Instead, users already with an account may invite people they know. You can create invite links here.</p>
|
||||
<p>Invite links are valid for 48 hours. Once an invite link is used to sign up, it can no longer be used.</p>
|
||||
<form method="POST" action="{{url_for('users.create_invite_key', username=user.username)}}">
|
||||
{{ csrf_input() | safe }}
|
||||
<input type="submit" value="Create new invite">
|
||||
</form>
|
||||
{%- if invites -%}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="plank even no-shadow contrast-bg" style="--w: 50%;">Link</th>
|
||||
<th class="plank even no-shadow contrast-bg" style="--w: 30%;">Expires</th>
|
||||
<th class="plank even no-shadow contrast-bg" style="--w: 20%;">Revoke</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for invite in invites -%}
|
||||
<tr>
|
||||
<td class="plank even no-shadow minimal"><a href="{{url_for('users.sign_up', key=invite.key)}}">Copy this</a></td>
|
||||
<td class="plank even no-shadow minimal">{{timestamp(invite.expires_at)}}</td>
|
||||
<td class="plank even no-shadow minimal center">
|
||||
<form method="POST" action="{{url_for('users.revoke_invite_key', username=user.username)}}">
|
||||
{{ csrf_input() | safe }}
|
||||
<input type="hidden" name="key" value="{{invite.key}}">
|
||||
<input type="submit" class="warn" value="Revoke">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- else -%}
|
||||
<p>You do not have any invites pending activation.</p>
|
||||
{%- endif -%}
|
||||
</fieldset>
|
||||
{%- endif -%}
|
||||
{%- endif -%}
|
||||
<fieldset class="plank">
|
||||
<legend>Disown & Delete account</legend>
|
||||
|
||||
@@ -4,13 +4,22 @@
|
||||
{%- block title -%}sign up{%- endblock -%}
|
||||
{%- block content -%}
|
||||
{%- set welcome -%}
|
||||
Please read the rules etc. stub
|
||||
<p>Please read the rules etc. stub</p>
|
||||
{%- if not inviter -%}
|
||||
<p>After you sign up, a moderator will need to confirm your account before you will be allowed to post.
|
||||
{%- else -%}
|
||||
You have been invited by <a href="{{url_for('users.user_page', username=inviter.username)}}">{{inviter.get_readable_name()}}</a> to join {{config.SITE_NAME}}. Create an identity below.
|
||||
{%- endif -%}
|
||||
</p>
|
||||
{%- endset -%}
|
||||
{{ subheader('Sign up', welcome)}}
|
||||
{%- if request.args.get('error') -%}
|
||||
{{infobox(request.args.error, InfoboxKind.ERROR)}}
|
||||
{%- endif -%}
|
||||
<form class="plank primary-bg full-width" method="POST">
|
||||
{%- if invite -%}
|
||||
<input type="hidden" name="key" value="{{invite.key}}">
|
||||
{%- endif -%}
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" pattern="[a-zA-Z0-9_\-]{3,24}" title="3-24 characters. Only upper and lowercase letters, digits, hyphens, and underscores" autocomplete="username" required>
|
||||
<label for="password">Create password</label>
|
||||
@@ -18,6 +27,6 @@ Please read the rules etc. stub
|
||||
<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 at least: 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">
|
||||
<input type="submit" value="Sign up" class="alt">
|
||||
</form>
|
||||
{%- endblock -%}
|
||||
|
||||
@@ -731,21 +731,13 @@ details.inner {
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
|
||||
&.three-cols > thead > tr > th {
|
||||
&:nth-child(1) {
|
||||
width: 70%;
|
||||
th {
|
||||
width: var(--w, 50%);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 25%;
|
||||
td.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* babycode tags */
|
||||
|
||||
Reference in New Issue
Block a user