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 datetime import datetime, timedelta
from time import sleep 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_login import login_required, current_user
from flask_babel import _ from flask_babel import _
from sqlalchemy import text, desc 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.admin.util import unsubscribe_from_everything_then_delete, unsubscribe_from_community
from app.community.util import save_icon_file, save_banner_file from app.community.util import save_icon_file, save_banner_file
from app.models import AllowedInstances, BannedInstances, ActivityPubLog, utcnow, Site, Community, CommunityMember, \ 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, \ 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 from app.admin import bp
@ -26,7 +26,8 @@ from app.admin import bp
@permission_required('change instance settings') @permission_required('change instance settings')
def admin_home(): def admin_home():
return render_template('admin/home.html', title=_('Admin'), moderating_communities=moderating_communities(current_user.get_id()), 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']) @bp.route('/site', methods=['GET', 'POST'])
@ -54,7 +55,8 @@ def admin_site():
form.legal_information.data = site.legal_information form.legal_information.data = site.legal_information
return render_template('admin/site.html', title=_('Site profile'), form=form, return render_template('admin/site.html', title=_('Site profile'), form=form,
moderating_communities=moderating_communities(current_user.get_id()), 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 form.log_activitypub_json.data = site.log_activitypub_json
return render_template('admin/misc.html', title=_('Misc settings'), form=form, return render_template('admin/misc.html', title=_('Misc settings'), form=form,
moderating_communities=moderating_communities(current_user.get_id()), 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, return render_template('admin/federation.html', title=_('Federation settings'), form=form,
moderating_communities=moderating_communities(current_user.get_id()), 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 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, 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>') @bp.route('/activity_json/<int:activity_id>')
@ -164,7 +169,9 @@ def admin_activities():
def activity_json(activity_id): def activity_json(activity_id):
activity = ActivityPubLog.query.get_or_404(activity_id) activity = ActivityPubLog.query.get_or_404(activity_id)
return render_template('admin/activity_json.html', title=_('Activity JSON'), 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') @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, 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()), 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(): def topics_for_form():
@ -280,7 +288,8 @@ def admin_community_edit(community_id):
form.default_layout.data = community.default_layout form.default_layout.data = community.default_layout
return render_template('admin/edit_community.html', title=_('Edit community'), form=form, community=community, return render_template('admin/edit_community.html', title=_('Edit community'), form=form, community=community,
moderating_communities=moderating_communities(current_user.get_id()), 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() topics = Topic.query.order_by(Topic.name).all()
return render_template('admin/topics.html', title=_('Topics'), topics=topics, return render_template('admin/topics.html', title=_('Topics'), topics=topics,
moderating_communities=moderating_communities(current_user.get_id()), 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, return render_template('admin/edit_topic.html', title=_('Add topic'), form=form,
moderating_communities=moderating_communities(current_user.get_id()), 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']) @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 form.machine_name.data = topic.machine_name
return render_template('admin/edit_topic.html', title=_('Edit topic'), form=form, topic=topic, return render_template('admin/edit_topic.html', title=_('Edit topic'), form=form, topic=topic,
moderating_communities=moderating_communities(current_user.get_id()), 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, return render_template('admin/users.html', title=_('Users'), next_url=next_url, prev_url=prev_url, users=users,
local_remote=local_remote, search=search, local_remote=local_remote, search=search,
moderating_communities=moderating_communities(current_user.get_id()), 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']) @bp.route('/user/<int:user_id>/edit', methods=['GET', 'POST'])
@login_required @login_required
@permission_required('administer all users') @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, return render_template('admin/edit_user.html', title=_('Edit user'), form=form, user=user,
moderating_communities=moderating_communities(current_user.get_id()), 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, return render_template('admin/reports.html', title=_('Reports'), next_url=next_url, prev_url=prev_url, reports=reports,
local_remote=local_remote, search=search, local_remote=local_remote, search=search,
moderating_communities=moderating_communities(current_user.get_id()), 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( password2 = PasswordField(
_l('Repeat password'), validators=[DataRequired(), _l('Repeat password'), validators=[DataRequired(),
EqualTo('password')]) EqualTo('password')])
question = StringField(_('Why would you like to join this site?'), validators=[DataRequired(), Length(min=1, max=512)])
recaptcha = RecaptchaField() recaptcha = RecaptchaField()
submit = SubmitField(_l('Register')) 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 werkzeug.urls import url_parse
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from flask_babel import _ from flask_babel import _
from wtforms import Label
from app import db, cache from app import db, cache
from app.auth import bp from app.auth import bp
from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordRequestForm, ResetPasswordForm
from app.auth.util import random_token, normalize_utf from app.auth.util import random_token, normalize_utf
from app.models import User, utcnow, IpBan from app.email import send_verification_email, send_password_reset_email
from app.auth.email import send_password_reset_email, send_welcome_email, send_verification_email from app.models import User, utcnow, IpBan, UserRegistration
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.utils import render_template, ip_address, user_ip_banned, user_cookie_banned, banned_ip_addresses finalize_user_setup
@bp.route('/login', methods=['GET', 'POST']) @bp.route('/login', methods=['GET', 'POST'])
@ -51,6 +53,8 @@ def login():
# Set a cookie so we have another way to track banned people # 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)) response.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
return response return response
if user.waiting_for_approval():
return redirect(url_for('auth.please_wait'))
login_user(user, remember=True) login_user(user, remember=True)
current_user.last_seen = utcnow() current_user.last_seen = utcnow()
current_user.ip_address = ip_address() current_user.ip_address = ip_address()
@ -84,6 +88,8 @@ def register():
if current_user.is_authenticated: if current_user.is_authenticated:
return redirect(url_for('main.index')) return redirect(url_for('main.index'))
form = RegistrationForm() form = RegistrationForm()
if g.site.registration_mode != 'RequireApplication':
form.question.validators = ()
if form.validate_on_submit(): if form.validate_on_submit():
if form.email.data == '': # ignore any registration where the email field is filled out. spam prevention 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 \ 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) user.set_password(form.password.data)
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
login_user(user, remember=True)
send_welcome_email(user)
send_verification_email(user) send_verification_email(user)
if current_app.config['MODE'] == 'development': if current_app.config['MODE'] == 'development':
current_app.logger.info('Verify account:' + url_for('auth.verify_email', token=user.verification_token, _external=True)) current_app.logger.info('Verify account:' + url_for('auth.verify_email', token=user.verification_token, _external=True))
if g.site.registration_mode == 'RequireApplication':
flash(_('Great, you are now a registered user!')) 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'))) resp = make_response(redirect(url_for('topic.choose_topics')))
if user_ip_banned(): if user_ip_banned():
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30)) resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
return resp 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']) @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 if user.verified: # guard against users double-clicking the link in the email
return redirect(url_for('main.index')) return redirect(url_for('main.index'))
user.verified = True 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() 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: else:
flash(_('Email address validation failed.'), 'error') flash(_('Email address validation failed.'), 'error')
if len(user.communities()) == 0: if user.waiting_for_approval():
return redirect(url_for('topic.choose_topics')) return redirect(url_for('auth.please_wait'))
else: 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') @bp.route('/validation_required')

View file

@ -9,8 +9,8 @@ import click
import os import os
from app.activitypub.signature import RsaKeys from app.activitypub.signature import RsaKeys
from app.auth.email import send_verification_email
from app.auth.util import random_token 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, \ from app.models import Settings, BannedInstances, Interest, Role, User, RolePermission, Domain, ActivityPubLog, \
utcnow, Site, Instance utcnow, Site, Instance
from app.utils import file_get_contents, retrieve_block_list from app.utils import file_get_contents, retrieve_block_list

View file

@ -1,6 +1,6 @@
from flask import current_app, render_template, escape from flask import current_app, render_template, escape
from app import db, celery 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 import boto3
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from typing import List from typing import List
@ -9,6 +9,34 @@ AWS_REGION = "ap-southeast-2"
CHARSET = "UTF-8" 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 @celery.task
def send_async_email(subject, sender, recipients, text_body, html_body, reply_to): def send_async_email(subject, sender, recipients, text_body, html_body, reply_to):
if type(recipients) == str: if type(recipients) == str:

View file

@ -447,6 +447,10 @@ class User(UserMixin, db.Model):
def is_local(self): def is_local(self):
return self.ap_id is None or self.ap_profile_id.startswith('https://' + current_app.config['SERVER_NAME']) 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) @cache.memoize(timeout=30)
def is_admin(self): def is_admin(self):
for role in self.roles: for role in self.roles:
@ -581,6 +585,8 @@ class User(UserMixin, db.Model):
file.delete_from_disk() file.delete_from_disk()
self.avatar_id = None self.avatar_id = None
db.session.delete(file) db.session.delete(file)
if self.waiting_for_approval():
db.session.query(UserRegistration).filter(UserRegistration.user_id == self.id).delete()
def purge_content(self): def purge_content(self):
files = File.query.join(Post).filter(Post.user_id == self.id).all() 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')) 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): class PostVote(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), index=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) enable_nsfl = db.Column(db.Boolean, default=False)
community_creation_admin_only = 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) 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='') application_question = db.Column(db.Text, default='')
allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list allow_or_block_list = db.Column(db.Integer, default=2) # 1 = allow list, 2 = block list
allowlist = db.Column(db.Text, default='') allowlist = db.Column(db.Text, default='')

View file

@ -598,7 +598,7 @@ fieldset legend {
} }
.post_list .post_teaser .utilities_row a { .post_list .post_teaser .utilities_row a {
display: inline-block; display: inline-block;
width: 44px; min-width: 44px;
} }
.post_list .post_teaser .utilities_row .preview_image, .post_list .post_teaser .utilities_row .post_options { .post_list .post_teaser .utilities_row .preview_image, .post_list .post_teaser .utilities_row .post_options {
text-align: center; text-align: center;

View file

@ -221,7 +221,7 @@ nav, etc which are used site-wide */
.utilities_row { .utilities_row {
a { a {
display: inline-block; display: inline-block;
width: 44px; min-width: 44px;
} }
.preview_image, .post_options { .preview_image, .post_options {
text-align: center; 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_misc') }}">{{ _('Misc settings') }}</a> |
<a href="{{ url_for('admin.admin_communities') }}">{{ _('Communities') }}</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_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_reports') }}">{{ _('Moderation') }}</a> |
<a href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a> | <a href="{{ url_for('admin.admin_federation') }}">{{ _('Federation') }}</a> |
<a href="{{ url_for('admin.admin_activities') }}">{{ _('Activities') }}</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" /> <td><img src="{{ user.avatar_thumbnail() }}" class="community_icon rounded-circle" loading="lazy" />
{{ user.display_name() }}</td> {{ user.display_name() }}</td>
<td>{{ 'Local' if user.is_local() else 'Remote' }}</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>{{ user.attitude * 100 if user.attitude != 1 }}</td>
<td>{{ 'R ' + str(user.reputation) if user.reputation }}</td> <td>{{ 'R ' + str(user.reputation) if user.reputation }}</td>
<td>{{ '<span class="red">Banned</span>'|safe if user.banned }} </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>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 <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 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 <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 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 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> </ol>
</nav> </nav>
<h1 class="mt-2">{{ _('Edit profile of %(name)s', name=user.user_name) }}</h1> <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() }} {{ form.csrf_token() }}
{{ render_field(form.title) }} {{ render_field(form.title) }}
{{ render_field(form.email) }} {{ render_field(form.email) }}
{{ render_field(form.password_field) }} {{ render_field(form.password_field) }}
<hr /> <hr />
{{ render_field(form.about) }} {{ 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> <small class="field_hint">e.g. @something:matrix.org. Include leading @ and use : before server</small>
{{ render_field(form.profile_file) }} {{ render_field(form.profile_file) }}
<small class="field_hint">Provide a square image that looks good when small.</small> <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)]) title = StringField(_l('Display name'), validators=[Optional(), Length(max=255)])
email = EmailField(_l('Email address'), validators=[Email(), DataRequired(), Length(min=5, 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)], 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)]) 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')) profile_file = FileField(_('Avatar image'))
banner_file = FileField(_('Top banner image')) banner_file = FileField(_('Top banner image'))
bot = BooleanField(_l('This profile is a bot')) bot = BooleanField(_l('This profile is a bot'))
@ -25,6 +25,10 @@ class ProfileForm(FlaskForm):
if current_user.another_account_using_email(field.data): if current_user.another_account_using_email(field.data):
raise ValidationError(_l('That email address is already in use by another account')) 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): class SettingsForm(FlaskForm):
newsletter = BooleanField(_l('Subscribe to email newsletter')) 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.set_password(form.password_field.data)
current_user.about = form.about.data current_user.about = form.about.data
current_user.about_html = markdown_to_html(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 current_user.bot = form.bot.data
profile_file = request.files['profile_file'] profile_file = request.files['profile_file']
if profile_file and profile_file.filename != '': if profile_file and profile_file.filename != '':
@ -135,7 +135,7 @@ def edit_profile(actor):
form.title.data = current_user.title form.title.data = current_user.title
form.email.data = current_user.email form.email.data = current_user.email
form.about.data = current_user.about 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 = '' form.password_field.data = ''
return render_template('user/edit_profile.html', title=_('Edit profile'), form=form, user=current_user, 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 from app import db, cache
import re import re
from app.email import send_welcome_email
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \ from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
Site, Post, PostReply, utcnow, Filter, CommunityMember 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() 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 # 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) 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 ###