def generate_token(self, username=None, regenerate=True): """ Generate and store a new registration token for this user Tokens are not re-generated if they exist already :param username: Username to generate for: if left empty, it will be inferred from self.data :param regenerate: Force regenerating even if token exists :return str: The token """ if self.data.get("register_token", None) and not regenerate: return self.data["register_token"] if not username: username = self.data["name"] register_token = hashlib.sha256() register_token.update(os.urandom(128)) register_token = register_token.hexdigest() db.update("users", data={ "register_token": register_token, "timestamp_token": int(time.time()) }, where={"name": username}) return register_token
def clear_token(self): """ Reset password rest token Clears the token and token timestamp. This allows requesting a new one even if the old one had not expired yet. :return: """ db.update("users", data={"register_token": "", "timestamp_token": 0}, where={"name": self.get_id()})
def set_value(self, key, value): """ Set persistently stored user property :param key: Name of item to store :param value: Value :return: """ self.userdata[key] = value db.update("users", where={"name": self.get_id()}, data={"userdata": json.dumps(self.userdata)})
def set_password(self, password): """ Set user password :param password: Password to set """ if self.is_anonymous: raise Exception("Cannot set password for anonymous user") salt = bcrypt.gensalt() password_hash = bcrypt.hashpw(password.encode("ascii"), salt) db.update("users", where={"name": self.data["name"]}, data={"password": password_hash.decode("utf-8")})
def __init__(self, data, authenticated=False): """ Instantiate user object Also sets the properties required by Flask-Login. :param data: User data :param authenticated: Whether the user should be marked as authenticated """ self.data = data if self.data["name"] != "anonymous": self.is_anonymous = False self.is_active = True self.name = self.data["name"] self.is_authenticated = authenticated self.userdata = json.loads(self.data.get("userdata", "{}")) if not self.is_anonymous and self.is_authenticated: db.update("users", where={"name": self.data["name"]}, data={"timestamp_seen": int(time.time())})
def add_user(): """ Create a new user Sends the user an e-mail with a link through which they can set their password. :return: Either an html page with a message, or a JSON response, depending on whether ?format == html """ if not current_user.is_authenticated or not current_user.is_admin(): return error(403, message="This page is off-limits to you.") response = {"success": False} email = request.form.get("email", request.args.get("email", "")).strip() fmt = request.form.get("format", request.args.get("format", "")).strip() force = request.form.get("force", request.args.get("force", None)) if not email or not re.match(r"[^@]+\@.*?\.[a-zA-Z]+", email): response = { **response, **{ "message": "Please provide a valid e-mail address." } } else: username = email try: db.insert("users", data={ "name": username, "timestamp_token": int(time.time()) }) user = User.get_by_name(username) if user is None: response = { **response, **{ "message": "User was created but could not be instantiated properly." } } else: try: user.email_token(new=True) response["success"] = True response = { **response, **{ "message": "An e-mail containing a link through which the registration can be completed has been sent to %s." % username } } except RuntimeError as e: response = { **response, **{ "message": "User was created but the registration e-mail could not be sent to them (%s)." % e } } except psycopg2.IntegrityError: db.rollback() if not force: response = { **response, **{ "message": 'Error: User %s already exists. If you want to re-create the user and re-send the registration e-mail, use [this link](/admin/add-user?email=%s&force=1&format=%s).' % (username, username, fmt) } } else: # if a user does not use their token in time, maybe you want to # be a benevolent admin and give them another change, without # having them go through the whole signup again user = User.get_by_name(username) db.update("users", data={"timestamp_token": int(time.time())}, where={"name": username}) try: user.email_token(new=True) response["success"] = True response = { **response, **{ "message": "A new registration e-mail has been sent to %s." % username } } except RuntimeError as e: response = { **response, **{ "message": "Token was reset registration e-mail could not be sent to them (%s)." % e } } if fmt == "html": return render_template( "error.html", message=response["message"], title=("New account created" if response["success"] else "Error")) else: return jsonify(response)
def email_token(self, new=False): """ Send user an e-mail with a password reset link Generates a token that the user can use to reset their password. The token is valid for 72 hours. Note that this requires a mail server to be configured, or a `RuntimeError` will be raised. If a server is configured but the mail still fails to send, it will also raise a `RuntimeError`. Note that in these cases a token is still created and valid (the user just gets no notification, but an admin could forward the correct link). If the user is a 'special' user, a `ValueError` is raised. :param bool new: Is this the first time setting a password for this account? """ if not config.MAILHOST: raise RuntimeError( "No e-mail server configured. 4CAT cannot send any e-mails.") if self.is_special(): raise ValueError( "Cannot send password reset e-mails for a special user.") username = self.get_id() # generate a password reset token register_token = hashlib.sha256() register_token.update(os.urandom(128)) register_token = register_token.hexdigest() db.update("users", data={ "register_token": register_token, "timestamp_token": int(time.time()) }, where={"name": username}) # prepare welcome e-mail sender = "*****@*****.**" message = MIMEMultipart("alternative") message["Subject"] = "Account created" message["From"] = sender message["To"] = username # the actual e-mail... url_base = config.FlaskConfig.SERVER_NAME url = "https://%s/reset-password/?token=%s" % (url_base, register_token) # we use slightly different e-mails depending on whether this is the first time setting a password if new: mail = """ <p>Hello %s,</p> <p>A 4CAT account has been created for you. You can now log in to 4CAT at <a href="http://%s">%s</a>.</p> <p>Note that before you log in, you will need to set a password. You can do so via the following link:</p> <p><a href="%s">%s</a></p> <p>Please complete your registration within 72 hours as the link above will become invalid after this time.</p> """ % (username, url_base, url_base, url, url) else: mail = """ <p>Hello %s,</p> <p>Someone has requested a password reset for your 4CAT account. If that someone is you, great! If not, feel free to ignore this e-mail.</p> <p>You can change your password via the following link:</p> <p><a href="%s">%s</a></p> <p>Please do this within 72 hours as the link above will become invalid after this time.</p> """ % (username, url, url) # provide a plain-text alternative as well html_parser = html2text.HTML2Text() message.attach(MIMEText(html_parser.handle(mail), "plain")) message.attach(MIMEText(mail, "html")) # try to send it try: with smtplib.SMTP(config.MAILHOST) as smtp: smtp.sendmail("*****@*****.**", [username], message.as_string()) except (smtplib.SMTPException, ConnectionRefusedError) as e: raise RuntimeError("Could not send password reset e-mail: %s" % e)