From 2f3f8b61555d70013de1cd16ad1029a30f1a036c Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:30:03 +1300 Subject: [PATCH] account approval process --- app/admin/routes.py | 80 +++++++++++++++---- app/auth/email.py | 30 ------- app/auth/forms.py | 1 + app/auth/routes.py | 65 ++++++++++----- app/cli.py | 2 +- app/email.py | 30 ++++++- app/models.py | 19 ++++- app/static/structure.css | 2 +- app/static/structure.scss | 2 +- app/templates/admin/_nav.html | 5 +- .../admin/approve_registrations.html | 49 ++++++++++++ app/templates/admin/users.html | 7 +- app/templates/auth/check_email.html | 15 ++++ app/templates/auth/please_wait.html | 15 ++++ app/templates/email/welcome.html | 2 +- app/templates/user/edit_profile.html | 4 +- app/user/forms.py | 8 +- app/user/routes.py | 4 +- app/utils.py | 14 ++++ ...b833e34c75_user_registration_processing.py | 47 +++++++++++ 20 files changed, 319 insertions(+), 82 deletions(-) delete mode 100644 app/auth/email.py create mode 100644 app/templates/admin/approve_registrations.html create mode 100644 app/templates/auth/check_email.html create mode 100644 app/templates/auth/please_wait.html create mode 100644 migrations/versions/3fb833e34c75_user_registration_processing.py diff --git a/app/admin/routes.py b/app/admin/routes.py index 9e70f422..0e2ca625 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from time import sleep -from flask import request, flash, json, url_for, current_app, redirect +from flask import request, flash, json, url_for, current_app, redirect, g from flask_login import login_required, current_user from flask_babel import _ from sqlalchemy import text, desc @@ -15,9 +15,9 @@ from app.admin.forms import FederationForm, SiteMiscForm, SiteProfileForm, EditC from app.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community from app.community.util import save_icon_file, save_banner_file from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \ - User, Instance, File, Report, Topic + User, Instance, File, Report, Topic, UserRegistration from app.utils import render_template, permission_required, set_setting, get_setting, gibberish, markdown_to_html, \ - moderating_communities, joined_communities + moderating_communities, joined_communities, finalize_user_setup from app.admin import bp @@ -26,7 +26,8 @@ from app.admin import bp @permission_required('change instance settings') def admin_home(): return render_template('admin/home.html', title=_('Admin'), moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id())) + joined_communities=joined_communities(current_user.get_id()), + site=g.site) @bp.route('/site', methods=['GET', 'POST']) @@ -54,7 +55,8 @@ def admin_site(): form.legal_information.data = site.legal_information return render_template('admin/site.html', title=_('Site profile'), form=form, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -95,7 +97,8 @@ def admin_misc(): form.log_activitypub_json.data = site.log_activitypub_json return render_template('admin/misc.html', title=_('Misc settings'), form=form, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -135,7 +138,8 @@ def admin_federation(): return render_template('admin/federation.html', title=_('Federation settings'), form=form, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -155,7 +159,8 @@ def admin_activities(): prev_url = url_for('admin.admin_activities', page=activities.prev_num) if activities.has_prev and page != 1 else None return render_template('admin/activities.html', title=_('ActivityPub Log'), next_url=next_url, prev_url=prev_url, - activities=activities) + activities=activities, + site=g.site) @bp.route('/activity_json/') @@ -164,7 +169,9 @@ def admin_activities(): def activity_json(activity_id): activity = ActivityPubLog.query.get_or_404(activity_id) return render_template('admin/activity_json.html', title=_('Activity JSON'), - activity_json_data=json.loads(activity.activity_json), activity=activity, current_app=current_app) + activity_json_data=json.loads(activity.activity_json), activity=activity, + current_app=current_app, + site=g.site) @bp.route('/activity_json//replay') @@ -198,7 +205,8 @@ def admin_communities(): return render_template('admin/communities.html', title=_('Communities'), next_url=next_url, prev_url=prev_url, communities=communities, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id())) + joined_communities=joined_communities(current_user.get_id()), + site=g.site) def topics_for_form(): @@ -280,7 +288,8 @@ def admin_community_edit(community_id): form.default_layout.data = community.default_layout return render_template('admin/edit_community.html', title=_('Edit community'), form=form, community=community, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -332,7 +341,8 @@ def admin_topics(): topics = Topic.query.order_by(Topic.name).all() return render_template('admin/topics.html', title=_('Topics'), topics=topics, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -356,7 +366,8 @@ def admin_topic_add(): return render_template('admin/edit_topic.html', title=_('Add topic'), form=form, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @bp.route('/topic//edit', methods=['GET', 'POST']) @@ -381,7 +392,8 @@ def admin_topic_edit(topic_id): form.machine_name.data = topic.machine_name return render_template('admin/edit_topic.html', title=_('Edit topic'), form=form, topic=topic, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -425,10 +437,42 @@ def admin_users(): return render_template('admin/users.html', title=_('Users'), next_url=next_url, prev_url=prev_url, users=users, local_remote=local_remote, search=search, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) +@bp.route('/approve_registrations', methods=['GET']) +@login_required +@permission_required('approve registrations') +def admin_approve_registrations(): + registrations = UserRegistration.query.filter_by(status=0).order_by(UserRegistration.created_at).all() + recently_approved = UserRegistration.query.filter_by(status=1).order_by(desc(UserRegistration.approved_at)).limit(30) + return render_template('admin/approve_registrations.html', + registrations=registrations, + recently_approved=recently_approved, + site=g.site) + + +@bp.route('/approve_registrations//approve', methods=['GET']) +@login_required +@permission_required('approve registrations') +def admin_approve_registrations_approve(user_id): + user = User.query.get_or_404(user_id) + registration = UserRegistration.query.filter_by(status=0, user_id=user_id).first() + if registration: + registration.status = 1 + registration.approved_at = utcnow() + registration.approved_by = current_user.id + db.session.commit() + if user.verified: + finalize_user_setup(user, True) + + flash(_('Registration approved.')) + + return redirect(url_for('admin.admin_approve_registrations')) + + @bp.route('/user//edit', methods=['GET', 'POST']) @login_required @permission_required('administer all users') @@ -493,7 +537,8 @@ def admin_user_edit(user_id): return render_template('admin/edit_user.html', title=_('Edit user'), form=form, user=user, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) @@ -540,5 +585,6 @@ def admin_reports(): return render_template('admin/reports.html', title=_('Reports'), next_url=next_url, prev_url=prev_url, reports=reports, local_remote=local_remote, search=search, moderating_communities=moderating_communities(current_user.get_id()), - joined_communities=joined_communities(current_user.get_id()) + joined_communities=joined_communities(current_user.get_id()), + site=g.site ) \ No newline at end of file diff --git a/app/auth/email.py b/app/auth/email.py deleted file mode 100644 index ab718db8..00000000 --- a/app/auth/email.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import render_template, current_app -from flask_babel import _ -from app.email import send_email - - -def send_password_reset_email(user): - token = user.get_reset_password_token() - send_email(_('[PieFed] Reset Your Password'), - sender='PieFed ', - recipients=[user.email], - text_body=render_template('email/reset_password.txt', - user=user, token=token), - html_body=render_template('email/reset_password.html', - user=user, token=token)) - - -def send_welcome_email(user): - send_email(_('Welcome to PieFed'), - sender='PieFed ', - recipients=[user.email], - text_body=render_template('email/welcome.txt', user=user), - html_body=render_template('email/welcome.html', user=user)) - - -def send_verification_email(user): - send_email(_('Please verify your email address'), - sender='PieFed ', - recipients=[user.email], - text_body=render_template('email/verification.txt', user=user), - html_body=render_template('email/verification.html', user=user)) \ No newline at end of file diff --git a/app/auth/forms.py b/app/auth/forms.py index 7bd1899a..eeddf077 100644 --- a/app/auth/forms.py +++ b/app/auth/forms.py @@ -20,6 +20,7 @@ class RegistrationForm(FlaskForm): password2 = PasswordField( _l('Repeat password'), validators=[DataRequired(), EqualTo('password')]) + question = StringField(_('Why would you like to join this site?'), validators=[DataRequired(), Length(min=1, max=512)]) recaptcha = RecaptchaField() submit = SubmitField(_l('Register')) diff --git a/app/auth/routes.py b/app/auth/routes.py index 6a704c98..62591803 100644 --- a/app/auth/routes.py +++ b/app/auth/routes.py @@ -3,14 +3,16 @@ from flask import redirect, url_for, flash, request, make_response, session, Mar from werkzeug.urls import url_parse from flask_login import login_user, logout_user, current_user from flask_babel import _ +from wtforms import Label + from app import db, cache from app.auth import bp from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm from app.auth.util import random_token, normalize_utf -from app.models import User, utcnow, IpBan -from app.auth.email import send_password_reset_email, send_welcome_email, send_verification_email -from app.activitypub.signature import RsaKeys -from app.utils import render_template, ip_address, user_ip_banned, user_cookie_banned, banned_ip_addresses +from app.email import send_verification_email, send_password_reset_email +from app.models import User, utcnow, IpBan, UserRegistration +from app.utils import render_template, ip_address, user_ip_banned, user_cookie_banned, banned_ip_addresses, \ + finalize_user_setup @bp.route('/login', methods=['GET', 'POST']) @@ -51,6 +53,8 @@ def login(): # Set a cookie so we have another way to track banned people response.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30)) return response + if user.waiting_for_approval(): + return redirect(url_for('auth.please_wait')) login_user(user, remember=True) current_user.last_seen = utcnow() current_user.ip_address = ip_address() @@ -84,6 +88,8 @@ def register(): if current_user.is_authenticated: return redirect(url_for('main.index')) form = RegistrationForm() + if g.site.registration_mode != 'RequireApplication': + form.question.validators = () if form.validate_on_submit(): if form.email.data == '': # ignore any registration where the email field is filled out. spam prevention if form.real_email.data.lower().startswith('postmaster@') or form.real_email.data.lower().startswith('abuse@') or \ @@ -104,20 +110,37 @@ def register(): user.set_password(form.password.data) db.session.add(user) db.session.commit() - login_user(user, remember=True) - send_welcome_email(user) send_verification_email(user) - if current_app.config['MODE'] == 'development': current_app.logger.info('Verify account:' + url_for('auth.verify_email', token=user.verification_token, _external=True)) - - flash(_('Great, you are now a registered user!')) + if g.site.registration_mode == 'RequireApplication': + application = UserRegistration(user_id=user.id, answer=form.question.data) + db.session.add(application) + db.session.commit() + return redirect(url_for('auth.please_wait')) + else: + return redirect(url_for('auth.check_email')) resp = make_response(redirect(url_for('topic.choose_topics'))) if user_ip_banned(): resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30)) return resp - return render_template('auth/register.html', title=_('Register'), form=form, site=g.site) + else: + if g.site.registration_mode == 'RequireApplication' and g.site.application_question != '': + form.question.label = Label('question', g.site.application_question) + if g.site.registration_mode != 'RequireApplication': + del form.question + return render_template('auth/register.html', title=_('Register'), form=form, site=g.site) + + +@bp.route('/please_wait', methods=['GET']) +def please_wait(): + return render_template('auth/please_wait.html', title=_('Account under review'), site=g.site) + + +@bp.route('/check_email', methods=['GET']) +def check_email(): + return render_template('auth/check_email.html', title=_('Check your email'), site=g.site) @bp.route('/reset_password_request', methods=['GET', 'POST']) @@ -168,21 +191,21 @@ def verify_email(token): if user.verified: # guard against users double-clicking the link in the email return redirect(url_for('main.index')) user.verified = True - user.last_seen = utcnow() - private_key, public_key = RsaKeys.generate_keypair() - user.private_key = private_key - user.public_key = public_key - user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" - user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" - user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox" db.session.commit() - flash(_('Thank you for verifying your email address.')) + if not user.waiting_for_approval(): + finalize_user_setup(user) + else: + flash(_('Thank you for verifying your email address.')) else: flash(_('Email address validation failed.'), 'error') - if len(user.communities()) == 0: - return redirect(url_for('topic.choose_topics')) + if user.waiting_for_approval(): + return redirect(url_for('auth.please_wait')) else: - return redirect(url_for('main.index')) + login_user(user, remember=True) + if len(user.communities()) == 0: + return redirect(url_for('topic.choose_topics')) + else: + return redirect(url_for('main.index')) @bp.route('/validation_required') diff --git a/app/cli.py b/app/cli.py index 257d4724..dba51034 100644 --- a/app/cli.py +++ b/app/cli.py @@ -9,8 +9,8 @@ import click import os from app.activitypub.signature import RsaKeys -from app.auth.email import send_verification_email from app.auth.util import random_token +from app.email import send_verification_email from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \ utcnow, Site, Instance from app.utils import file_get_contents, retrieve_block_list diff --git a/app/email.py b/app/email.py index d43bb935..59884139 100644 --- a/app/email.py +++ b/app/email.py @@ -1,6 +1,6 @@ from flask import current_app, render_template, escape from app import db, celery -from flask_babel import _, lazy_gettext as _l # todo: set the locale based on account_id so that _() works +from flask_babel import _, lazy_gettext as _l # todo: set the locale based on account_id so that _() works import boto3 from botocore.exceptions import ClientError from typing import List @@ -9,6 +9,34 @@ AWS_REGION = "ap-southeast-2" CHARSET = "UTF-8" +def send_password_reset_email(user): + token = user.get_reset_password_token() + send_email(_('[PieFed] Reset Your Password'), + sender='PieFed ', + recipients=[user.email], + text_body=render_template('email/reset_password.txt', + user=user, token=token), + html_body=render_template('email/reset_password.html', + user=user, token=token)) + + +def send_verification_email(user): + send_email(_('Please verify your email address'), + sender='PieFed ', + recipients=[user.email], + text_body=render_template('email/verification.txt', user=user), + html_body=render_template('email/verification.html', user=user)) + + +def send_welcome_email(user, application_required): + subject = _('Your application has been approved - welcome to PieFed') if application_required else _('Welcome to PieFed') + send_email(subject, + sender='PieFed ', + recipients=[user.email], + text_body=render_template('email/welcome.txt', user=user, application_required=application_required), + html_body=render_template('email/welcome.html', user=user, application_required=application_required)) + + @celery.task def send_async_email(subject, sender, recipients, text_body, html_body, reply_to): if type(recipients) == str: diff --git a/app/models.py b/app/models.py index 838b90e2..a336c25a 100644 --- a/app/models.py +++ b/app/models.py @@ -447,6 +447,10 @@ class User(UserMixin, db.Model): def is_local(self): return self.ap_id is None or self.ap_profile_id.startswith('https://' + current_app.config['SERVER_NAME']) + def waiting_for_approval(self): + application = UserRegistration.query.filter_by(user_id=self.id, status=0).first() + return application is not None + @cache.memoize(timeout=30) def is_admin(self): for role in self.roles: @@ -581,6 +585,8 @@ class User(UserMixin, db.Model): file.delete_from_disk() self.avatar_id = None db.session.delete(file) + if self.waiting_for_approval(): + db.session.query(UserRegistration).filter(UserRegistration.user_id == self.id).delete() def purge_content(self): files = File.query.join(Post).filter(Post.user_id == self.id).all() @@ -861,6 +867,17 @@ class UserFollowRequest(db.Model): follow_id = db.Column(db.Integer, db.ForeignKey('user.id')) +class UserRegistration(db.Model): + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) + answer = db.Column(db.String(512)) + status = db.Column(db.Integer, default=0, index=True) # 0 = unapproved, 1 = approved + created_at = db.Column(db.DateTime, default=utcnow) + approved_at = db.Column(db.DateTime) + approved_by = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', foreign_keys=[user_id], lazy='joined') + + class PostVote(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=True) @@ -980,7 +997,7 @@ class Site(db.Model): enable_nsfl = db.Column(db.Boolean, default=False) community_creation_admin_only = db.Column(db.Boolean, default=False) reports_email_admins = db.Column(db.Boolean, default=True) - registration_mode = db.Column(db.String(20), default='Closed') + registration_mode = db.Column(db.String(20), default='Closed') # possible values: Open, RequireApplication, Closed application_question = db.Column(db.Text, default='') allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list allowlist = db.Column(db.Text, default='') diff --git a/app/static/structure.css b/app/static/structure.css index bc67fda1..3648d938 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -598,7 +598,7 @@ fieldset legend { } .post_list .post_teaser .utilities_row a { display: inline-block; - width: 44px; + min-width: 44px; } .post_list .post_teaser .utilities_row .preview_image, .post_list .post_teaser .utilities_row .post_options { text-align: center; diff --git a/app/static/structure.scss b/app/static/structure.scss index ce1a8545..f1c3f7da 100644 --- a/app/static/structure.scss +++ b/app/static/structure.scss @@ -221,7 +221,7 @@ nav, etc which are used site-wide */ .utilities_row { a { display: inline-block; - width: 44px; + min-width: 44px; } .preview_image, .post_options { text-align: center; diff --git a/app/templates/admin/_nav.html b/app/templates/admin/_nav.html index eb18c5a2..0df1acfe 100644 --- a/app/templates/admin/_nav.html +++ b/app/templates/admin/_nav.html @@ -4,7 +4,10 @@ {{ _('Misc settings') }} | {{ _('Communities') }} | {{ _('Topics') }} | - {{ _('Users') }} | + {{ _('Users') }} | + {% if site.registration_mode == 'RequireApplication' %} + {{ _('Registration applications') }} | + {% endif %} {{ _('Moderation') }} | {{ _('Federation') }} | {{ _('Activities') }} diff --git a/app/templates/admin/approve_registrations.html b/app/templates/admin/approve_registrations.html new file mode 100644 index 00000000..0b86f2eb --- /dev/null +++ b/app/templates/admin/approve_registrations.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+
+ {% include 'admin/_nav.html' %} +
+
+ +
+
+ {% if registrations %} +

{{ _('When registering, people are asked "%(question)s".', question=site.application_question) }}

+
+ +
+ + + + + + + + + + + {% for registration in registrations %} + + + + + + + + + + {% endfor %} +
NameEmailEmail verifedAnswerAppliedIPActions
+ {{ registration.user.display_name() }}{{ registration.user.email }}{{ ''|safe if registration.user.verified else ''|safe }}{{ registration.answer }}{{ moment(registration.created_at).fromNow() }}{{ registration.user.ip_address if registration.user.ip_address }} {{ _('Approve') }} + {{ _('View') }} | + {{ _('Delete') }} +
+ {% else %} +

{{ _('No one is waiting to be approved.') }}

+ {% endif %} +
+
+{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/users.html b/app/templates/admin/users.html index 627d8221..f2a52d34 100644 --- a/app/templates/admin/users.html +++ b/app/templates/admin/users.html @@ -33,7 +33,12 @@ {{ user.display_name() }} {{ 'Local' if user.is_local() else 'Remote' }} - {{ user.last_seen }} + {% if request.args.get('local_remote', '') == 'local' %} + {{ moment(user.last_seen).fromNow() }} + {% else %} + {{ user.last_seen }} + {% endif %} + {{ user.attitude * 100 if user.attitude != 1 }} {{ 'R ' + str(user.reputation) if user.reputation }} {{ 'Banned'|safe if user.banned }} diff --git a/app/templates/auth/check_email.html b/app/templates/auth/check_email.html new file mode 100644 index 00000000..a8ab001a --- /dev/null +++ b/app/templates/auth/check_email.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block app_content %} +
+ +
+ +{% endblock %} diff --git a/app/templates/auth/please_wait.html b/app/templates/auth/please_wait.html new file mode 100644 index 00000000..04e96144 --- /dev/null +++ b/app/templates/auth/please_wait.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block app_content %} +
+ +
+ +{% endblock %} diff --git a/app/templates/email/welcome.html b/app/templates/email/welcome.html index 990ba21a..43382828 100644 --- a/app/templates/email/welcome.html +++ b/app/templates/email/welcome.html @@ -3,7 +3,7 @@

I'm Rimu, the founder of PieFed, and I'm super grateful for your sign-up.

As PieFed is still in an early development phase, your thoughts and ideas mean the world to me. You might stumble upon a few hiccups or notice some missing bits here and there — that's totally expected. Just hit reply to this email and - let me know what's up. Screenshots, if you've got 'em, will make a world of difference!

+ let me know what's up. Screenshots, if you've got 'em, will be very helpful!

If you are technically-minded, reporting issues on the Codeberg issue tracker would be awesome. And hey, if Python's your jam and you're keen to pitch in, check out the project site or diff --git a/app/templates/user/edit_profile.html b/app/templates/user/edit_profile.html index ba8640b8..34c04570 100644 --- a/app/templates/user/edit_profile.html +++ b/app/templates/user/edit_profile.html @@ -12,14 +12,14 @@

{{ _('Edit profile of %(name)s', name=user.user_name) }}

-
+ {{ form.csrf_token() }} {{ render_field(form.title) }} {{ render_field(form.email) }} {{ render_field(form.password_field) }}
{{ render_field(form.about) }} - {{ render_field(form.matrix_user_id) }} + {{ render_field(form.matrixuserid) }} e.g. @something:matrix.org. Include leading @ and use : before server {{ render_field(form.profile_file) }} Provide a square image that looks good when small. diff --git a/app/user/forms.py b/app/user/forms.py index 147ff8d1..a5e08d37 100644 --- a/app/user/forms.py +++ b/app/user/forms.py @@ -13,9 +13,9 @@ class ProfileForm(FlaskForm): title = StringField(_l('Display name'), validators=[Optional(), Length(max=255)]) email = EmailField(_l('Email address'), validators=[Email(), DataRequired(), Length(min=5, max=255)]) password_field = PasswordField(_l('Set new password'), validators=[Optional(), Length(min=1, max=50)], - render_kw={"autocomplete": 'Off'}) + render_kw={"autocomplete": 'new-password'}) about = TextAreaField(_l('Bio'), validators=[Optional(), Length(min=3, max=5000)]) - matrix_user_id = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)], render_kw={'autocomplete': 'off'}) + matrixuserid = StringField(_l('Matrix User ID'), validators=[Optional(), Length(max=255)], render_kw={'autocomplete': 'off'}) profile_file = FileField(_('Avatar image')) banner_file = FileField(_('Top banner image')) bot = BooleanField(_l('This profile is a bot')) @@ -25,6 +25,10 @@ class ProfileForm(FlaskForm): if current_user.another_account_using_email(field.data): raise ValidationError(_l('That email address is already in use by another account')) + def validate_matrix_user_id(self, matrix_user_id): + if not matrix_user_id.data.strip().startswith('@'): + raise ValidationError(_('Matrix user ids start with @')) + class SettingsForm(FlaskForm): newsletter = BooleanField(_l('Subscribe to email newsletter')) diff --git a/app/user/routes.py b/app/user/routes.py index 54cce194..45587948 100644 --- a/app/user/routes.py +++ b/app/user/routes.py @@ -96,7 +96,7 @@ def edit_profile(actor): current_user.set_password(form.password_field.data) current_user.about = form.about.data current_user.about_html = markdown_to_html(form.about.data) - current_user.matrix_user_id = form.matrix_user_id.data + current_user.matrix_user_id = form.matrixuserid.data current_user.bot = form.bot.data profile_file = request.files['profile_file'] if profile_file and profile_file.filename != '': @@ -135,7 +135,7 @@ def edit_profile(actor): form.title.data = current_user.title form.email.data = current_user.email form.about.data = current_user.about - form.matrix_user_id.data = current_user.matrix_user_id + form.matrixuserid.data = current_user.matrix_user_id form.password_field.data = '' return render_template('user/edit_profile.html', title=_('Edit profile'), form=form, user=current_user, diff --git a/app/utils.py b/app/utils.py index e4fbcddf..eb008b15 100644 --- a/app/utils.py +++ b/app/utils.py @@ -22,6 +22,7 @@ from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput from app import db, cache import re +from app.email import send_welcome_email from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \ Site, Post, PostReply, utcnow, Filter, CommunityMember @@ -562,6 +563,19 @@ def joined_communities(user_id): filter(CommunityMember.user_id == user_id).order_by(Community.title).all() +def finalize_user_setup(user, application_required=False): + from app.activitypub.signature import RsaKeys + user.verified = True + user.last_seen = utcnow() + private_key, public_key = RsaKeys.generate_keypair() + user.private_key = private_key + user.public_key = public_key + user.ap_profile_id = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" + user.ap_public_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}" + user.ap_inbox_url = f"https://{current_app.config['SERVER_NAME']}/u/{user.user_name}/inbox" + db.session.commit() + send_welcome_email(user, application_required) + # All the following post/comment ranking math is explained at https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9 epoch = datetime(1970, 1, 1) diff --git a/migrations/versions/3fb833e34c75_user_registration_processing.py b/migrations/versions/3fb833e34c75_user_registration_processing.py new file mode 100644 index 00000000..1fc49dc9 --- /dev/null +++ b/migrations/versions/3fb833e34c75_user_registration_processing.py @@ -0,0 +1,47 @@ +"""user registration processing + +Revision ID: 3fb833e34c75 +Revises: 52e8d73b69ba +Create Date: 2024-02-02 11:56:44.895871 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3fb833e34c75' +down_revision = '52e8d73b69ba' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_registration', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('answer', sa.String(length=512), nullable=True), + sa.Column('status', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('approved_at', sa.DateTime(), nullable=True), + sa.Column('approved_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['approved_by'], ['user.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user_registration', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_user_registration_status'), ['status'], unique=False) + batch_op.create_index(batch_op.f('ix_user_registration_user_id'), ['user_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('user_registration', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_registration_user_id')) + batch_op.drop_index(batch_op.f('ix_user_registration_status')) + + op.drop_table('user_registration') + # ### end Alembic commands ###