mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
account approval process
This commit is contained in:
parent
da9e77cbd7
commit
2f3f8b6155
20 changed files with 319 additions and 82 deletions
|
@ -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
|
||||
)
|
|
@ -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))
|
|
@ -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'))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
28
app/email.py
28
app/email.py
|
@ -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:
|
||||
|
|
|
@ -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='')
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
49
app/templates/admin/approve_registrations.html
Normal file
49
app/templates/admin/approve_registrations.html
Normal 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">✓</span>'|safe if registration.user.verified else '<span class="red">✗</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 %}
|
|
@ -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>
|
||||
|
|
15
app/templates/auth/check_email.html
Normal file
15
app/templates/auth/check_email.html
Normal 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 %}
|
15
app/templates/auth/please_wait.html
Normal file
15
app/templates/auth/please_wait.html
Normal 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 %}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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,
|
||||
|
|
14
app/utils.py
14
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)
|
||||
|
|
|
@ -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 ###
|
Loading…
Reference in a new issue