diff --git a/flaschengeist/controller/userController.py b/flaschengeist/controller/userController.py index b359d92..1781755 100644 --- a/flaschengeist/controller/userController.py +++ b/flaschengeist/controller/userController.py @@ -1,22 +1,19 @@ -from flask import current_app, url_for -from sqlalchemy.orm.exc import NoResultFound -from werkzeug.exceptions import NotFound, BadRequest +import secrets +from flask import current_app +from datetime import datetime, timedelta, timezone +from werkzeug.exceptions import NotFound, BadRequest, Forbidden -from flaschengeist.utils.hook import Hook -from flaschengeist.models.user import User, Role -from flaschengeist.database import db from flaschengeist import logger +from flaschengeist.database import db +from flaschengeist.utils.hook import Hook +from flaschengeist.models.user import User, Role, _PasswordReset +from flaschengeist.controller import messageController, sessionController def login_user(username, password): logger.info("login user {{ {} }}".format(username)) - mail = username.split("@") - mail = len(mail) == 2 and len(mail[0]) > 0 and len(mail[1]) > 0 - query = User.userid == username - if mail: - query |= User.mail == username - user = User.query.filter(query).one_or_none() + user = find_user(username) if not user: logger.debug("User not found in Database.") user = User(userid=username) @@ -27,6 +24,49 @@ def login_user(username, password): return None +def request_reset(user: User): + logger.debug(f"New password reset request for {user.userid}") + reset = _PasswordReset.query.get(user._id) + if not reset: + reset = _PasswordReset(_user_id=user._id) + db.session.add(reset) + + expires = datetime.now(tz=timezone.utc) + if not reset.expires or reset.expires < expires: + expires = expires + timedelta(hours=12) + reset.expires = expires + reset.token = secrets.token_urlsafe(16) + + subject = "Flaschengeist - Passwort zurücksetzten" + domain = "flaschengeist.local" + text = f"""Hallo {user.display_name}, +Jemand hat das Zurücksetzen des Passworts für dein Flaschengeist Benutzerkonto angefordert. + +Benutzername: {user.userid} + +Falls das nicht beabsichtigt war, ignoriere diese E-Mail einfach. Es wird dann nichts passieren. + +Um dein Passwort zurückzusetzen, besuche folgende Adresse, der Link ist 12 Stunden gültig: + + + """ + db.session.commit() + messageController.send_message(messageController.Message(user, text, subject)) + + +def reset_password(token: str, password: str): + reset = _PasswordReset.query.filter(_PasswordReset.token == token).one_or_none() + logger.debug(f"Token is {'valid' if reset else 'invalid'}") + if not reset or reset.expires < datetime.now(tz=timezone.utc): + raise Forbidden + + modify_user(reset.user, None, password) + sessionController.delete_sessions(reset.user) + + db.session.delete(reset) + db.session.commit() + + @Hook def update_user(user): current_app.config["FG_AUTH_BACKEND"].update_user(user) @@ -60,6 +100,10 @@ def modify_user(user, password, new_password=None): """ current_app.config["FG_AUTH_BACKEND"].modify_user(user, password, new_password) + if new_password: + # TODO: Password changed mail + logger.error(f"Password changed for user {user.userid}") + def get_users(): return User.query.all() @@ -76,6 +120,16 @@ def get_user(uid): return user +def find_user(uid_mail): + mail = uid_mail.split("@") + mail = len(mail) == 2 and len(mail[0]) > 0 and len(mail[1]) > 0 + + query = User.userid == uid_mail + if mail: + query |= User.mail == uid_mail + return User.query.filter(query).one_or_none() + + def delete(user): current_app.config["FG_AUTH_BACKEND"].delete_user(user) db.session.delete(user) diff --git a/flaschengeist/models/user.py b/flaschengeist/models/user.py index 98a2f4a..a25fd33 100644 --- a/flaschengeist/models/user.py +++ b/flaschengeist/models/user.py @@ -1,11 +1,10 @@ -from datetime import date -from typing import Optional - from flask import url_for +from typing import Optional +from datetime import date, datetime from sqlalchemy.orm.collections import attribute_mapped_collection -from . import ModelSerializeMixin -from flaschengeist.database import db +from ..database import db +from . import ModelSerializeMixin, UtcDateTime association_table = db.Table( "user_x_role", @@ -103,6 +102,15 @@ class _UserAttribute(db.Model, ModelSerializeMixin): value: any = db.Column(db.PickleType(protocol=4)) +class _PasswordReset(db.Model): + """Table containing password reset requests""" + __tablename__ = "password_reset" + _user_id: User = db.Column("user", db.Integer, db.ForeignKey("user.id"), primary_key=True) + user: User = db.relationship("User", foreign_keys=[_user_id]) + token: str = db.Column(db.String(30)) + expires: datetime = db.Column(UtcDateTime) + + class _Avatar: """Wrapper class for avatar binaries"""