diff --git a/app/models.py b/app/models.py index 7d222d3..a96f0c7 100644 --- a/app/models.py +++ b/app/models.py @@ -281,3 +281,7 @@ class Reactions(Model): """ return db.query(q, post_id, reaction_text) + + +class PasswordResetLinks(Model): + table = "password_reset_links" diff --git a/app/routes/mod.py b/app/routes/mod.py index e4277d1..02dd46d 100644 --- a/app/routes/mod.py +++ b/app/routes/mod.py @@ -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/") +@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)) diff --git a/app/routes/users.py b/app/routes/users.py index 54b668b..7011580 100644 --- a/app/routes/users.py +++ b/app/routes/users.py @@ -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/') +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/') +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)) diff --git a/app/schema.py b/app/schema.py index ae1f1bf..c40cbdf 100644 --- a/app/schema.py +++ b/app/schema.py @@ -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)", diff --git a/app/templates/mod/user-list.html b/app/templates/mod/user-list.html index 3cd660b..7b17a75 100644 --- a/app/templates/mod/user-list.html +++ b/app/templates/mod/user-list.html @@ -37,6 +37,9 @@ Username Permission Signed up on + {% if active_user.is_admin() %} + Create password reset link + {% endif %} {% for user in not_guests %} @@ -50,6 +53,13 @@ {{ timestamp(user.created_at) }} + {% if active_user.is_admin() %} + +
+ +
+ + {% endif %} {% endfor %} diff --git a/app/templates/users/reset_link_login.html b/app/templates/users/reset_link_login.html new file mode 100644 index 0000000..7fe8801 --- /dev/null +++ b/app/templates/users/reset_link_login.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% block title %}Reset password{% endblock %} +{% block content %} + +{% endblock %}