add a way for mods to create a password reset link for users

This commit is contained in:
2025-08-10 19:00:47 +03:00
parent cf2d605077
commit 4c2877403d
6 changed files with 114 additions and 3 deletions

View File

@ -281,3 +281,7 @@ class Reactions(Model):
"""
return db.query(q, post_id, reaction_text)
class PasswordResetLinks(Model):
table = "password_reset_links"

View File

@ -1,9 +1,12 @@
from flask import (
Blueprint, render_template, request, redirect, url_for
)
from .users import login_required, mod_only, get_active_user
from ..models import Users
from .users import login_required, mod_only, get_active_user, admin_only
from ..models import Users, PasswordResetLinks
from ..db import db, DB
import secrets
import time
bp = Blueprint("mod", __name__, url_prefix = "/mod/")
@bp.get("/sort-topics")
@ -31,3 +34,18 @@ def sort_topics_post():
def user_list():
users = Users.select()
return render_template("mod/user-list.html", users = users)
@bp.post("/reset-pass/<user_id>")
@login_required
@mod_only("topics.all_topics")
def create_reset_pass(user_id):
now = int(time.time())
key = secrets.token_urlsafe(20)
reset_link = PasswordResetLinks.create({
'user_id': int(user_id),
'expires_at': now + 24 * 60 * 60,
'key': key,
})
return redirect(url_for('users.reset_link_login', key=key))

View File

@ -4,7 +4,7 @@ from flask import (
from functools import wraps
from ..db import db
from ..lib.babycode import babycode_to_html
from ..models import Users, Sessions, Subscriptions, Avatars
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks
from ..constants import InfoboxKind, PermissionLevel
from ..auth import digest, verify
from wand.image import Image
@ -516,3 +516,60 @@ def inbox(username):
})
return render_template("users/inbox.html", new_posts = new_posts, total_unreads_count = total_unreads_count, all_subscriptions = all_subscriptions)
@bp.get('/reset-link/<key>')
def reset_link_login(key):
reset_link = PasswordResetLinks.find({
'key': key
})
if not reset_link:
return redirect(url_for('topics.all_topics'))
if int(time.time()) > int(reset_link.expires_at):
reset_link.delete()
return redirect(url_for('topics.all_topics'))
target_user = Users.find({
'id': reset_link.user_id
})
return render_template('users/reset_link_login.html', username = target_user.username)
@bp.post('/reset-link/<key>')
def reset_link_login_form(key):
reset_link = PasswordResetLinks.find({
'key': key
})
if not reset_link:
return redirect('topics.all_topics')
if int(time.time()) > int(reset_link.expires_at):
reset_link.delete()
return redirect('topics.all_topics')
password = request.form.get('password')
password2 = request.form.get('password2')
if not validate_password(password):
flash("Invalid password.", InfoboxKind.ERROR)
return redirect(url_for('.reset_link_login', key=key))
if password != password2:
flash("Passwords do not match.", InfoboxKind.ERROR)
return redirect(url_for('.reset_link_login', key=key))
target_user = Users.find({
'id': reset_link.user_id
})
reset_link.delete()
hashed = digest(password)
target_user.update({'password_hash': hashed})
session_obj = create_session(target_user.id)
session['pyrom_session_key'] = session_obj.key
flash("Logged in!", InfoboxKind.INFO)
return redirect(url_for('.page', username=target_user.username))

View File

@ -83,6 +83,13 @@ SCHEMA = [
"reaction_text" TEXT NOT NULL DEFAULT ''
)""",
"""CREATE TABLE IF NOT EXISTS "password_reset_links" (
"id" INTEGER NOT NULL PRIMARY KEY,
"user_id" REFERENCES users(id) ON DELETE CASCADE,
"expires_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
"key" TEXT NOT NULL UNIQUE
)""",
# INDEXES
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",

View File

@ -37,6 +37,9 @@
<th>Username</th>
<th class="small">Permission</th>
<th class="small">Signed up on</th>
{% if active_user.is_admin() %}
<th class="small">Create password reset link</th>
{% endif %}
</thead>
{% for user in not_guests %}
<tr>
@ -50,6 +53,13 @@
<td>
{{ timestamp(user.created_at) }}
</td>
{% if active_user.is_admin() %}
<td>
<form method="post" action="{{url_for('mod.create_reset_pass', user_id=user.id)}}">
<input type="submit" class="warn" value="Create password reset link">
</form>
</td>
{% endif %}
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% block title %}Reset password{% endblock %}
{% block content %}
<div class="darkbg login-container">
<h1>Reset password for {{username}}</h1>
<p>Send this link to {{username}} to allow them to reset their password.</p>
<form method="post">
<label for="password">New password</label><br>
<input type="password" id="password" name="password" autocomplete="new-password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required><br>
<label for="password2">Confirm password</label><br>
<input type="password" id="password2" name="password2" autocomplete="new-password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required><br>
<input type="submit" value="Reset password">
</form>
</div>
{% endblock %}