add settings routes
This commit is contained in:
17
app/auth.py
17
app/auth.py
@@ -66,6 +66,13 @@ def revoke_session(user_id):
|
|||||||
sess.delete()
|
sess.delete()
|
||||||
session.clear()
|
session.clear()
|
||||||
|
|
||||||
|
def revoke_all_sessions(user_id):
|
||||||
|
if not is_logged_in():
|
||||||
|
return
|
||||||
|
|
||||||
|
Sessions.revoke_all(user_id)
|
||||||
|
session.clear()
|
||||||
|
|
||||||
def parse_username(username: str) -> Tuple[str, str]:
|
def parse_username(username: str) -> Tuple[str, str]:
|
||||||
"""first is the unmodified name/display name, second is username"""
|
"""first is the unmodified name/display name, second is username"""
|
||||||
if len(username) < 3:
|
if len(username) < 3:
|
||||||
@@ -77,6 +84,15 @@ def parse_username(username: str) -> Tuple[str, str]:
|
|||||||
invalid_regex = r'[^a-zA-Z0-9_-]'
|
invalid_regex = r'[^a-zA-Z0-9_-]'
|
||||||
return re.sub(invalid_regex, '_', username.lower())[:24], username
|
return re.sub(invalid_regex, '_', username.lower())[:24], username
|
||||||
|
|
||||||
|
def parse_display_name(display_name: str) -> str:
|
||||||
|
if len(display_name) == 0:
|
||||||
|
return display_name
|
||||||
|
invalid_regex = r'[@<>&]'
|
||||||
|
res = re.sub(invalid_regex, '_', display_name)[:50]
|
||||||
|
while len(res) < 3:
|
||||||
|
res += '_'
|
||||||
|
return res
|
||||||
|
|
||||||
def is_password_valid(password: str) -> bool:
|
def is_password_valid(password: str) -> bool:
|
||||||
return re.match(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}$', password) is not None
|
return re.match(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,255}$', password) is not None
|
||||||
|
|
||||||
@@ -130,4 +146,3 @@ def csrf_verified(view_func):
|
|||||||
|
|
||||||
return view_func(*args, **kwargs)
|
return view_func(*args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,24 @@ class Users(Model):
|
|||||||
res = db.fetch_one(q, self.id)
|
res = db.fetch_one(q, self.id)
|
||||||
return res["c"] or 0
|
return res["c"] or 0
|
||||||
|
|
||||||
|
def set_signature(self, content:str, language: str = 'babycode'):
|
||||||
|
if not content:
|
||||||
|
self.update({
|
||||||
|
'signature_original_markup': '',
|
||||||
|
'signature_rendered': '',
|
||||||
|
'signature_format_version': None,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
|
||||||
|
from .lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||||
|
from .constants import SIG_BANNED_TAGS
|
||||||
|
signature_rendered = babycode_to_html(content, SIG_BANNED_TAGS).result
|
||||||
|
self.update({
|
||||||
|
'signature_original_markup': content,
|
||||||
|
'signature_rendered': signature_rendered,
|
||||||
|
'signature_format_version': BABYCODE_VERSION,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
class Topics(Model):
|
class Topics(Model):
|
||||||
table = 'topics'
|
table = 'topics'
|
||||||
@@ -396,6 +414,12 @@ class PostHistory(Model):
|
|||||||
class Sessions(Model):
|
class Sessions(Model):
|
||||||
table = 'sessions'
|
table = 'sessions'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def revoke_all(cls, user_id: int):
|
||||||
|
qb = db.QueryBuilder(cls.table).where({'user_id': user_id})
|
||||||
|
sql, params = qb.build_delete()
|
||||||
|
db.execute(sql, *params)
|
||||||
|
|
||||||
class Avatars(Model):
|
class Avatars(Model):
|
||||||
table = 'avatars'
|
table = 'avatars'
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,60 @@
|
|||||||
from flask import Blueprint, redirect, url_for, render_template, request, session, abort
|
from flask import (
|
||||||
|
Blueprint, redirect, url_for,
|
||||||
|
render_template, request, session,
|
||||||
|
abort, flash, current_app
|
||||||
|
)
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import time
|
from secrets import compare_digest as compare_timesafe
|
||||||
|
from wand.image import Image
|
||||||
|
from wand.exceptions import WandException
|
||||||
from ..auth import (
|
from ..auth import (
|
||||||
digest, verify, create_session,
|
digest, verify, create_session,
|
||||||
is_logged_in, parse_username, is_password_valid,
|
is_logged_in, parse_username, is_password_valid,
|
||||||
login_required, revoke_session, get_active_user
|
login_required, revoke_session, get_active_user,
|
||||||
|
parse_display_name, revoke_all_sessions
|
||||||
)
|
)
|
||||||
from ..models import Users, Posts, Reactions, Threads
|
from ..models import Users, Posts, Reactions, Threads, Avatars
|
||||||
from ..constants import PermissionLevel
|
from ..constants import PermissionLevel, InfoboxKind
|
||||||
from secrets import compare_digest as compare_timesafe
|
from ..util import get_form_checkbox
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
AVATAR_MAX_SIZE = 1000 * 1000 # 1MB
|
||||||
|
|
||||||
bp = Blueprint('users', __name__, url_prefix='/users/')
|
bp = Blueprint('users', __name__, url_prefix='/users/')
|
||||||
|
|
||||||
|
def validate_and_create_avatar(input_image, filename):
|
||||||
|
try:
|
||||||
|
with Image(blob=input_image) as img:
|
||||||
|
if hasattr(img, 'sequence') and len(img.sequence) > 1:
|
||||||
|
img = Image(image=img.sequence[0])
|
||||||
|
img.strip()
|
||||||
|
img.gravity = 'center'
|
||||||
|
|
||||||
|
width, height = img.width, img.height
|
||||||
|
min_dim = min(width, height)
|
||||||
|
if min_dim > 256:
|
||||||
|
ratio = 256.0 / min_dim
|
||||||
|
new_width = int(width * ratio)
|
||||||
|
new_height = int(height * ratio)
|
||||||
|
img.resize(new_width, new_height)
|
||||||
|
|
||||||
|
width, height = img.width, img.height
|
||||||
|
crop_size = min(width, height)
|
||||||
|
x_offset = (width - crop_size) // 2
|
||||||
|
y_offset = (height - crop_size) // 2
|
||||||
|
img.crop(left=x_offset, top=y_offset,
|
||||||
|
width=crop_size, height=crop_size)
|
||||||
|
|
||||||
|
img.resize(256, 256)
|
||||||
|
img.format = 'webp'
|
||||||
|
img.compression_quality = 85
|
||||||
|
img.save(filename=filename)
|
||||||
|
return True
|
||||||
|
except WandException:
|
||||||
|
return False
|
||||||
|
|
||||||
def redirect_if_logged_in(destination='topics.all_topics'):
|
def redirect_if_logged_in(destination='topics.all_topics'):
|
||||||
def decorator(view_func):
|
def decorator(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
@@ -36,6 +77,15 @@ def redirect_to_own(view_func):
|
|||||||
return view_func(username, *args, **kwargs)
|
return view_func(username, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
def user_required(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(username, *args, **kwargs):
|
||||||
|
user = get_active_user()
|
||||||
|
if user.is_guest():
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return view_func(username, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
@bp.get('/log-in/')
|
@bp.get('/log-in/')
|
||||||
@redirect_if_logged_in()
|
@redirect_if_logged_in()
|
||||||
@@ -106,7 +156,7 @@ def sign_up_post():
|
|||||||
|
|
||||||
if username_pair[0] != username_pair[1]:
|
if username_pair[0] != username_pair[1]:
|
||||||
user.update({
|
user.update({
|
||||||
'display_name': username_pair[1]
|
'display_name': parse_display_name(username_pair[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
session['remember'] = request.form.get('remember') == 'on'
|
session['remember'] = request.form.get('remember') == 'on'
|
||||||
@@ -182,7 +232,135 @@ def comments(username):
|
|||||||
@login_required
|
@login_required
|
||||||
@redirect_to_own
|
@redirect_to_own
|
||||||
def settings(username):
|
def settings(username):
|
||||||
username = username.lower()
|
user = get_active_user()
|
||||||
|
sort_by = session.get('sort_by', 'activity')
|
||||||
|
return render_template(
|
||||||
|
'users/settings.html', user=user,
|
||||||
|
sort_by=sort_by
|
||||||
|
)
|
||||||
|
|
||||||
|
@bp.post('/<username>/settings/set-avatar')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def set_avatar(username):
|
||||||
|
user = get_active_user()
|
||||||
|
return_to = redirect(url_for('.settings', username=user.username))
|
||||||
|
if 'avatar' not in request.files:
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
avi_file = request.files['avatar']
|
||||||
|
|
||||||
|
if avi_file.filename == '':
|
||||||
|
abort(400)
|
||||||
|
|
||||||
|
avi_file.seek(0, os.SEEK_END)
|
||||||
|
file_size = avi_file.tell()
|
||||||
|
avi_file.seek(0, os.SEEK_SET)
|
||||||
|
|
||||||
|
if file_size > AVATAR_MAX_SIZE:
|
||||||
|
flash('Your avatar must be 1MB or less.', InfoboxKind.ERROR)
|
||||||
|
return return_to
|
||||||
|
|
||||||
|
avi_bytes = avi_file.read()
|
||||||
|
now = int(time.time())
|
||||||
|
filename = f'u{user.id}d{now}.webp'
|
||||||
|
output_path = os.path.join(current_app.config['AVATAR_UPLOAD_PATH'], filename)
|
||||||
|
proxied_filename = f'/static/avatars/{filename}'
|
||||||
|
|
||||||
|
res = validate_and_create_avatar(avi_bytes, output_path)
|
||||||
|
if res:
|
||||||
|
flash('Avatar updated.', InfoboxKind.INFO)
|
||||||
|
avatar = Avatars.create({
|
||||||
|
'file_path': proxied_filename,
|
||||||
|
'uploaded_at': now,
|
||||||
|
})
|
||||||
|
old_avatar = Avatars.find({'id': user.avatar_id})
|
||||||
|
user.update({'avatar_id': avatar.id})
|
||||||
|
if int(old_avatar.id) != 1:
|
||||||
|
filename = os.path.join(current_app.config['AVATAR_UPLOAD_PATH'], os.path.basename(old_avatar.file_path))
|
||||||
|
os.remove(filename)
|
||||||
|
old_avatar.delete()
|
||||||
|
return return_to
|
||||||
|
else:
|
||||||
|
flash('Something went wrong.;Please try again.', InfoboxKind.ERROR)
|
||||||
|
|
||||||
|
return return_to
|
||||||
|
|
||||||
|
@bp.post('/<username>/settings/clear-avatar')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def clear_avatar(username):
|
||||||
|
user = get_active_user()
|
||||||
|
if user.is_default_avatar():
|
||||||
|
return redirect(url_for('.settings', username=username))
|
||||||
|
|
||||||
|
old_avatar = Avatars.find({'id': user.avatar_id})
|
||||||
|
user.update({'avatar_id': 1})
|
||||||
|
filename = os.path.join(current_app.config['AVATAR_UPLOAD_PATH'], os.path.basename(old_avatar.file_path))
|
||||||
|
os.remove(filename)
|
||||||
|
old_avatar.delete()
|
||||||
|
return redirect(url_for('.settings', username=username))
|
||||||
|
|
||||||
|
@bp.post('/<username>/settings/change-password')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
|
def change_password(username):
|
||||||
|
user = get_active_user()
|
||||||
|
current_password = request.form.get('current_password', '')
|
||||||
|
new_password = request.form.get('new_password', '')
|
||||||
|
new_password2 = request.form.get('new_password2', '')
|
||||||
|
|
||||||
|
new_hash = digest(new_password)
|
||||||
|
|
||||||
|
old_correct = verify(user.password_hash, current_password)
|
||||||
|
new_match = compare_timesafe(new_password, new_password2)
|
||||||
|
|
||||||
|
if (old_correct and new_match) == False:
|
||||||
|
flash('The current password is incorrect or the new passwords do not match.;Please try again.', InfoboxKind.ERROR)
|
||||||
|
return redirect(url_for('.settings', username=username))
|
||||||
|
|
||||||
|
user.update({
|
||||||
|
'password_hash': new_hash
|
||||||
|
})
|
||||||
|
|
||||||
|
revoke_all_sessions(user.id)
|
||||||
|
|
||||||
|
return redirect(url_for('.log_in'))
|
||||||
|
|
||||||
|
@bp.post('/<username>/settings/set-personalization')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def set_personalization(username):
|
||||||
|
user = get_active_user()
|
||||||
|
session['sort_by'] = request.form.get('sort_by', 'activity')
|
||||||
|
session['dont_subscribe_by_default'] = not get_form_checkbox('subscribe_by_default')
|
||||||
|
|
||||||
|
user.update({
|
||||||
|
'status': request.form.get('status', '')[:100],
|
||||||
|
'display_name': parse_display_name(request.form.get('display_name', ''))
|
||||||
|
})
|
||||||
|
|
||||||
|
flash('Personalization settings updated.', InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('.settings', username=username))
|
||||||
|
|
||||||
|
@bp.post('/<username>/settings/set-sig')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def set_sig(username):
|
||||||
|
user = get_active_user()
|
||||||
|
user.set_signature(request.form.get('babycode_content', ''))
|
||||||
|
flash('Signature updated.', InfoboxKind.INFO)
|
||||||
|
return redirect(url_for('.settings', username=username))
|
||||||
|
|
||||||
|
@bp.post('/<username>/settings/set-bio')
|
||||||
|
@login_required
|
||||||
|
@user_required
|
||||||
|
@redirect_to_own
|
||||||
|
def set_bio(username):
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
@bp.get('/<username>/inbox/')
|
@bp.get('/<username>/inbox/')
|
||||||
@@ -200,4 +378,3 @@ def inbox(username):
|
|||||||
def bookmarks(username):
|
def bookmarks(username):
|
||||||
username = username.lower()
|
username = username.lower()
|
||||||
return 'stub'
|
return 'stub'
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
<div class="tab-container">
|
<div class="tab-container">
|
||||||
<div class="tab-bar" role="tablist">
|
<div class="tab-bar" role="tablist">
|
||||||
{%- for tab_label in labels -%}
|
{%- for tab_label in labels -%}
|
||||||
<button type="button" class="tab-button" role="tab" aria-selected="{{'true' if loop.index0==0 else 'false'}}" id="{{prefix+'-'+(tab_label | lower)+'-tab'}}" aria-controls="{{prefix+'-'+(tab_label | lower)+'-content'}}">{{tab_label}}</button>
|
<button type="button" class="tab-button" role="tab" aria-selected="{{'true' if loop.index0==0 else 'false'}}" id="{{prefix+'-'+(tab_label | lower)+'-tab'}}" aria-controls="{{prefix+'-'+(tab_label | lower)+'-content'}}" disabled>{{tab_label}}</button>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</div>
|
</div>
|
||||||
{%- for tab_label in labels -%}
|
{%- for tab_label in labels -%}
|
||||||
@@ -87,24 +87,37 @@
|
|||||||
placeholder='Post content',
|
placeholder='Post content',
|
||||||
prefill='',
|
prefill='',
|
||||||
required=true,
|
required=true,
|
||||||
id='babycode-content'
|
id='babycode-content',
|
||||||
|
banned_tags=[]
|
||||||
) -%}
|
) -%}
|
||||||
{%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview']) -%}
|
{%- call(idx) tabs(prefix='babycode', labels=['Write', 'Preview']) -%}
|
||||||
{%- if idx == 0 -%}
|
{%- if idx == 0 -%}
|
||||||
<span class="babycode-editor-controls">
|
<span class="babycode-editor-controls">
|
||||||
<span class="button-row">
|
<span class="button-row">
|
||||||
<button type="button" class="minimal"><b>B</b></button>
|
<button type="button" class="minimal" disabled><b>B</b></button>
|
||||||
<button type="button" class="minimal"><i>i</i></button>
|
<button type="button" class="minimal" disabled><i>i</i></button>
|
||||||
<button type="button" class="minimal"><s>S</s></button>
|
<button type="button" class="minimal" disabled><s>S</s></button>
|
||||||
<button type="button" class="minimal"><u>U</u></button>
|
<button type="button" class="minimal" disabled><u>U</u></button>
|
||||||
<button type="button" class="minimal"><code>://</code></button>
|
<button type="button" class="minimal" disabled><code>://</code></button>
|
||||||
<button type="button" class="minimal"><code></></code></button>
|
<button type="button" class="minimal" disabled><code></></code></button>
|
||||||
<button type="button" class="minimal">1.</button>
|
<button type="button" class="minimal" disabled>1.</button>
|
||||||
<button type="button" class="minimal">•</button>
|
<button type="button" class="minimal" disabled>•</button>
|
||||||
<button type="button" class="minimal"><img src="/static/emoji/angry.png" class="emoji"></button>
|
<button type="button" class="minimal" disabled><img src="/static/emoji/angry.png" class="emoji"></button>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="flex-last">{# stub: char count #}</span>
|
||||||
</span>
|
</span>
|
||||||
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}}>{{ prefill }}</textarea>
|
<input type="hidden" name="babycode_banned_tags" id="{{id}}-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
|
||||||
|
<textarea name="babycode_content" id="{{id}}" class="babycode-editor" placeholder="{{placeholder}}" {{'required' if required else ''}} autocomplete="off" maxlength="5000">{{ prefill }}</textarea>
|
||||||
|
{%- if banned_tags -%}
|
||||||
|
<div>
|
||||||
|
<span>Forbidden tags:</span>
|
||||||
|
<ul class="horizontal">
|
||||||
|
{%- for tag in banned_tags -%}
|
||||||
|
<li><code class="inline-code">{{tag}}</code></li>
|
||||||
|
{%- endfor -%}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{%- endif -%}
|
||||||
<a href="##">babycode help</a>
|
<a href="##">babycode help</a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endcall -%}
|
{%- endcall -%}
|
||||||
@@ -132,7 +145,7 @@
|
|||||||
<div class="usercard-inner">
|
<div class="usercard-inner">
|
||||||
{{avatar(post.avatar_path)}}
|
{{avatar(post.avatar_path)}}
|
||||||
<div class="usercard-rest">
|
<div class="usercard-rest">
|
||||||
<a href="{{url_for('users.user_page', username=post.username)}}">{{post.display_name if post.display_name else post.username}}</a>
|
<a href="{{url_for('users.user_page', username=post.username)}}" class="usercard-username">{{post.display_name if post.display_name else post.username}}</a>
|
||||||
<abbr title="mention">@{{post.username}}</abbr>
|
<abbr title="mention">@{{post.username}}</abbr>
|
||||||
<i>{{post.status}}</i>
|
<i>{{post.status}}</i>
|
||||||
{%- set badges=post.badges_json | fromjson -%}
|
{%- set badges=post.badges_json | fromjson -%}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<h2 class="info">Reply to "{{thread.title}}"</h2>
|
<h2 class="info">Reply to "{{thread.title}}"</h2>
|
||||||
{{- babycode_editor_component() -}}
|
{{- babycode_editor_component() -}}
|
||||||
<span>
|
<span>
|
||||||
<input type="checkbox" checked name="subscribe" id="subscribe">
|
<input type="checkbox" {{'' if session['dont_subscribe_by_default'] else 'checked'}} name="subscribe" id="subscribe" autocomplete=off>
|
||||||
<label for="subscribe">Subscribe to thread</label>
|
<label for="subscribe">Subscribe to thread</label>
|
||||||
</span>
|
</span>
|
||||||
<span><input type="submit" value="Post reply"></span>
|
<span><input type="submit" value="Post reply"></span>
|
||||||
|
|||||||
80
app/templates/users/settings.html
Normal file
80
app/templates/users/settings.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
{%- from 'common/macros.html' import babycode_editor_component -%}
|
||||||
|
{%- from 'common/macros.html' import subheader, avatar -%}
|
||||||
|
{%- extends 'base.html' -%}
|
||||||
|
{%- block title -%}settings{%- endblock -%}
|
||||||
|
{%- block content -%}
|
||||||
|
{%- set sub -%}
|
||||||
|
{%- if user.is_guest() -%}You are a guest. Your customization options are limited until a moderator confirms your account.{%- endif -%}
|
||||||
|
{%- endset -%}
|
||||||
|
{{- subheader('User settings', sub) -}}
|
||||||
|
{%- if not user.is_guest() -%}
|
||||||
|
<fieldset class="plank">
|
||||||
|
<legend>Avatar</legend>
|
||||||
|
<form method="POST" class="avatar-form" action="{{url_for('users.set_avatar', username=user.username)}}" enctype="multipart/form-data">
|
||||||
|
{{- avatar(user.get_avatar_url()) -}}
|
||||||
|
<span class="avatar-form-controls">
|
||||||
|
<label for="avatar" class="linkbutton alt">Upload…</label>
|
||||||
|
<span class="avatar-form-size-label">1MB max. Will be cropped to square.</span>
|
||||||
|
<input type="file" style="display: none;" id="avatar" name="avatar" accept="image/*">
|
||||||
|
<input type="submit" value="Save">
|
||||||
|
<input type="submit" class="warn" value="Clear" formaction="{{url_for('users.clear_avatar', username=user.username)}}">
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
<fieldset class="plank">
|
||||||
|
<legend>Change password</legend>
|
||||||
|
<p>After you change your password, you will be logged out of all sessions and will need to log in again.</p>
|
||||||
|
<form class="full-width" method="POST" action="{{ url_for('users.change_password', username=user.username) }}">
|
||||||
|
<label for="current-password">Current password</label>
|
||||||
|
<input type="password" name="current_password" id="current-password" autocomplete="current-password" required>
|
||||||
|
<label for="new-password">New password</label>
|
||||||
|
<input type="password" name="new_password" id="new-password" autocomplete="new-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" required>
|
||||||
|
<label for="new-password2">Confirm new password</label>
|
||||||
|
<input type="password" name="new_password2" id="new-password2" autocomplete="new-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" required>
|
||||||
|
<input type="submit" value="Change password" class="warn">
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
{%- if not user.is_guest() -%}
|
||||||
|
<fieldset class="plank">
|
||||||
|
<legend>Personalization</legend>
|
||||||
|
<form class="full-width" method="POST" action="{{ url_for('users.set_personalization', username=user.username)}}">
|
||||||
|
<label for="sort-by">Sort threads by:</label>
|
||||||
|
<select name="sort_by" id="sort-by" autocomplete=off>
|
||||||
|
<option value="activity" {{ 'selected' if sort_by == 'activity' else '' }}>Activity</option>
|
||||||
|
<option value="thread" {{ 'selected' if sort_by == 'thread' else '' }}>Newest</option>
|
||||||
|
</select>
|
||||||
|
<label for="display-name">Display name</label>
|
||||||
|
<input type="text" name="display_name" id="display-name" value="{{user.display_name}}" placeholder="Same as username" pattern="(?:[\w!#$%^*\(\)\-_=+\[\]\{\}\|;:,.?\s]{3,50})?" title="Optional. 3-50 characters, no @, no <>, no &." maxlength="50" autocomplete=off>
|
||||||
|
<label for="status">Status</label>
|
||||||
|
<input type="text" name="status" id="status" maxlength="100" value="{{user.status}}" placeholder="Will be shown under your username on posts. Max. 100 characters." autocomplete="off">
|
||||||
|
<span>
|
||||||
|
<input type="checkbox" id="subscribe-by-default" name="subscribe_by_default" {{'' if session['dont_subscribe_by_default'] else 'checked'}} autocomplete="off">
|
||||||
|
<label for="subscribe-by-default">Automatically subscribe to thread when responding</label>
|
||||||
|
</span>
|
||||||
|
<input type="submit" value="Save">
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="plank">
|
||||||
|
<legend>Signature</legend>
|
||||||
|
<form method="POST" class="full-width" action="{{url_for('users.set_sig', username=user.username)}}">
|
||||||
|
<p>The signature will appear under each of your posts.</p>
|
||||||
|
{{babycode_editor_component(id='signature-content', placeholder='Signature content', prefill=user.signature_original_markup, required=false, banned_tags=['@mention'])}}
|
||||||
|
<input type="submit" value="Save signature">
|
||||||
|
</form>
|
||||||
|
</fieldset>
|
||||||
|
{#<fieldset class="plank">
|
||||||
|
<legend>About me/Bio</legend>
|
||||||
|
<form method="POST" class="full-width">
|
||||||
|
<span>Your bio will appear on your profile.</span>
|
||||||
|
{{babycode_editor_component(id='bio-content', placeholder='Bio content', prefill=user.signature_original_markup, required=false, banned_tags=['@mention'])}}
|
||||||
|
<input type="submit" value="Save bio">
|
||||||
|
</form>
|
||||||
|
</fieldset>#}
|
||||||
|
<fieldset class="plank">
|
||||||
|
<legend>Badges</legend>
|
||||||
|
<div>Loading badges…</div>
|
||||||
|
<div>If badges fail to load, make sure JS is enabled.</div>
|
||||||
|
</fieldset>
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endblock -%}
|
||||||
@@ -14,9 +14,9 @@ Please read the rules etc. stub
|
|||||||
<label for="username">Username</label>
|
<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>
|
<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>
|
<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>
|
<input type="password" id="password" 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>
|
||||||
<label for="password2">Confirm password</label>
|
<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>
|
<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>
|
<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">
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ body {
|
|||||||
margin: var(--big-padding) var(--wrapper-side-margin);
|
margin: var(--big-padding) var(--wrapper-side-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
button, .linkbutton, input[type="submit"] {
|
button, .linkbutton, input[type="submit"], input[type="file"]::file-selector-button {
|
||||||
--main-color: var(--button-color-primary);
|
--main-color: var(--button-color-primary);
|
||||||
--font-color: var(--font-color-main);
|
--font-color: var(--font-color-main);
|
||||||
--border-color: hsl(from var(--main-color) h calc(s * 1.3) 25);
|
--border-color: hsl(from var(--main-color) h calc(s * 1.3) 25);
|
||||||
@@ -189,7 +189,6 @@ button, .linkbutton, input[type="submit"] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.babycode-editor {
|
.babycode-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
@@ -466,19 +465,6 @@ footer {
|
|||||||
gap: var(--base-padding);
|
gap: var(--base-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-grid {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--base-padding);
|
|
||||||
--grid-item-base-width: 600px;
|
|
||||||
--grid-item-max-width: calc((100% - var(--grid-item-base-width)) / 2);
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(max(var(--grid-item-base-width), var(--grid-item-max-width)), 1fr));
|
|
||||||
|
|
||||||
&> * {
|
|
||||||
height: fit-content;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thread-actions {
|
.thread-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -524,6 +510,10 @@ footer {
|
|||||||
padding: var(--base-padding);
|
padding: var(--base-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usercard-username {
|
||||||
|
word-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -676,6 +666,18 @@ details.separated {
|
|||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-form {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--huge-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-form-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* babycode tags */
|
/* babycode tags */
|
||||||
.inline-code {
|
.inline-code {
|
||||||
background-color: var(--code-bg-color);
|
background-color: var(--code-bg-color);
|
||||||
@@ -863,10 +865,6 @@ a.mention {
|
|||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-grid {
|
|
||||||
--grid-item-base-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-fill-flex {
|
.mobile-fill-flex {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -908,4 +906,22 @@ a.mention {
|
|||||||
max-width: min(75vw, 400px);
|
max-width: min(75vw, 400px);
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-form {
|
||||||
|
flex-direction: column;
|
||||||
|
.avatar {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.avatar-form-controls {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--base-padding);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-form-size-label {
|
||||||
|
order: 999;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user