account approval process

This commit is contained in:
rimu 2024-02-02 15:30:03 +13:00
parent da9e77cbd7
commit 2f3f8b6155
20 changed files with 319 additions and 82 deletions

View file

@ -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/<int:activity_id>')
@ -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/<int:activity_id>/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/<int:topic_id>/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/<int:user_id>/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/<int:user_id>/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
)

View file

@ -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 <rimu@chorebuster.net>',
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 <rimu@chorebuster.net>',
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 <rimu@chorebuster.net>',
recipients=[user.email],
text_body=render_template('email/verification.txt', user=user),
html_body=render_template('email/verification.html', user=user))

View file

@ -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'))

View file

@ -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,22 +110,39 @@ 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
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'])
def reset_password_request():
if current_user.is_authenticated:
@ -168,17 +191,17 @@ 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()
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 user.waiting_for_approval():
return redirect(url_for('auth.please_wait'))
else:
login_user(user, remember=True)
if len(user.communities()) == 0:
return redirect(url_for('topic.choose_topics'))
else:

View file

@ -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

View file

@ -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 <rimu@chorebuster.net>',
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 <rimu@chorebuster.net>',
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 <rimu@chorebuster.net>',
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:

View file

@ -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='')

View file

@ -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;

View file

@ -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;

View file

@ -4,7 +4,10 @@
<a href="{{ url_for('admin.admin_misc') }}">{{ _('Misc settings') }}</a> |
<a href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</a> |
<a href="{{ url_for('admin.admin_topics') }}">{{ _('Topics') }}</a> |
<a href="{{ url_for('admin.admin_users') }}">{{ _('Users') }}</a> |
<a href="{{ url_for('admin.admin_users', local_remote='local') }}">{{ _('Users') }}</a> |
{% if site.registration_mode == 'RequireApplication' %}
<a href="{{ url_for('admin.admin_approve_registrations') }}">{{ _('Registration applications') }}</a> |
{% endif %}
<a href="{{ url_for('admin.admin_reports') }}">{{ _('Moderation') }}</a> |
<a href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a> |
<a href="{{ url_for('admin.admin_activities') }}">{{ _('Activities') }}</a>

View file

@ -0,0 +1,49 @@
{% extends "base.html" %}
{% from 'bootstrap/form.html' import render_form %}
{% block app_content %}
<div class="row">
<div class="col">
{% include 'admin/_nav.html' %}
</div>
</div>
<div class="row">
<div class="col">
{% if registrations %}
<p>{{ _('When registering, people are asked "%(question)s".', question=site.application_question) }} </p>
<form method="get">
<input type="search" name="search" value="{{ search }}">
</form>
<table class="table table-striped">
<tr>
<th>Name</th>
<th>Email</th>
<th>Email verifed</th>
<th>Answer</th>
<th>Applied</th>
<th>IP</th>
<th>Actions</th>
</tr>
{% for registration in registrations %}
<tr>
<td><img src="{{ registration.user.avatar_thumbnail() }}" class="community_icon rounded-circle" loading="lazy" />
{{ registration.user.display_name() }}</td>
<td><a href="mailto:{{ registration.user.email }}">{{ registration.user.email }}</a></td>
<td>{{ '<span class="green">&check;</span>'|safe if registration.user.verified else '<span class="red">&cross;</span>'|safe }}</td>
<td>{{ registration.answer }}</td>
<td>{{ moment(registration.created_at).fromNow() }}</td>
<td>{{ registration.user.ip_address if registration.user.ip_address }} </td>
<td><a href="{{ url_for('admin.admin_approve_registrations_approve', user_id=registration.user.id) }}" class="btn btn-sm btn-primary">{{ _('Approve') }}</a>
<a href="/u/{{ registration.user.link() }}">{{ _('View') }}</a> |
<a href="{{ url_for('admin.admin_user_delete', user_id=registration.user.id) }}" class="confirm_first">{{ _('Delete') }}</a>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>{{ _('No one is waiting to be approved.') }}</p>
{% endif %}
</div>
</div>
{% endblock %}

View file

@ -33,7 +33,12 @@
<td><img src="{{ user.avatar_thumbnail() }}" class="community_icon rounded-circle" loading="lazy" />
{{ user.display_name() }}</td>
<td>{{ 'Local' if user.is_local() else 'Remote' }}</td>
<td>{{ user.last_seen }}</td>
<td>{% if request.args.get('local_remote', '') == 'local' %}
{{ moment(user.last_seen).fromNow() }}
{% else %}
{{ user.last_seen }}
{% endif %}
</td>
<td>{{ user.attitude * 100 if user.attitude != 1 }}</td>
<td>{{ 'R ' + str(user.reputation) if user.reputation }}</td>
<td>{{ '<span class="red">Banned</span>'|safe if user.banned }} </td>

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block app_content %}
<div class="row">
<div class="col col-login mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title text-center">{{ _('Check your email') }}</div>
<p>{{ _('We sent you an email containing a link that you need to click to enable your account.') }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block app_content %}
<div class="row">
<div class="col col-login mx-auto">
<div class="card mt-5">
<div class="card-body p-6">
<div class="card-title text-center">{{ _('Thanks for registering') }}</div>
<p>{{ _('We are reviewing your application and will email you once it has been accepted.') }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -3,7 +3,7 @@
<p>I'm Rimu, the founder of PieFed, and I'm super grateful for your sign-up.</p>
<p>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!</p>
let me know what's up. Screenshots, if you've got 'em, will be very helpful!</p>
<p>If you are technically-minded, reporting issues on the <a href="https://codeberg.org/rimu/pyfedi/issues">Codeberg
issue tracker</a> would be awesome. And hey, if Python's
your jam and you're keen to pitch in, <a href="https://join.piefed.social/">check out the project site</a> or

View file

@ -12,14 +12,14 @@
</ol>
</nav>
<h1 class="mt-2">{{ _('Edit profile of %(name)s', name=user.user_name) }}</h1>
<form method='post' enctype="multipart/form-data" role="form">
<form method='post' enctype="multipart/form-data" role="form" autocomplete="off">
{{ form.csrf_token() }}
{{ render_field(form.title) }}
{{ render_field(form.email) }}
{{ render_field(form.password_field) }}
<hr />
{{ render_field(form.about) }}
{{ render_field(form.matrix_user_id) }}
{{ render_field(form.matrixuserid) }}
<small class="field_hint">e.g. @something:matrix.org. Include leading @ and use : before server</small>
{{ render_field(form.profile_file) }}
<small class="field_hint">Provide a square image that looks good when small.</small>

View file

@ -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'))

View file

@ -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,

View file

@ -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)

View file

@ -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 ###