File: change_username.py

package info (click to toggle)
flask-security 5.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,448 kB
  • sloc: python: 23,247; javascript: 204; makefile: 138
file content (139 lines) | stat: -rw-r--r-- 4,194 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""
flask_security.change_username
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Flask-Security Change Username module

:copyright: (c) 2025-2025 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.

Allow user to change their username.
This is really just for when username is used as an authenticating
identity (and therefore has to be unique).

The basic feature allows an authenticated user to change their username
to any available username. Normalization and validation take place
using username_util.
This doesn't offer any defense against username enumeration by another user or
preventing a user from constantly changing their username. Applications
could use the UsernameUtil.check_username() method to implement this.

Think about: username is normally considered 'public' however as an identity
should we follow a change protocol more like email - require a confirmation from
the registered email.

"""

from __future__ import annotations

import typing as t

from flask import after_this_request, request
from flask import current_app
from flask_login import current_user
from wtforms import Field, SubmitField

from .decorators import auth_required
from .forms import (
    Form,
    build_form_from_request,
    get_form_field_label,
)
from .proxies import _security, _datastore
from .quart_compat import get_quart_status
from .signals import username_changed
from .utils import (
    base_render_json,
    config_value as cv,
    do_flash,
    get_message,
    get_url,
    send_mail,
    view_commit,
)

if t.TYPE_CHECKING:  # pragma: no cover
    from flask.typing import ResponseValue

if get_quart_status():  # pragma: no cover
    from quart import redirect
else:
    from flask import redirect


class ChangeUsernameForm(Form):
    """Change Username Form.
    There is a single element - 'username'
    that will be validated by calling :meth:`.UsernameUtil.validate`
    then verified to be unique in the DB.

    username field value injected at init_app time with build_username_field()
    """

    username: t.ClassVar[Field]
    submit = SubmitField(label=get_form_field_label("submit"))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)


@auth_required(
    lambda: cv("API_ENABLED_METHODS"),
    within=lambda: cv("FRESHNESS"),
    grace=lambda: cv("FRESHNESS_GRACE_PERIOD"),
)
def change_username() -> ResponseValue:
    """Start Change Username for an existing authenticated user"""
    payload: dict[str, t.Any]

    form: ChangeUsernameForm = t.cast(
        ChangeUsernameForm, build_form_from_request("change_username_form")
    )

    if form.validate_on_submit():
        # simple - just change username
        form.user = current_user
        after_this_request(view_commit)
        update_username(form.user, form.username.data)
        if _security._want_json(request):
            return base_render_json(form)

        do_flash(*get_message("USERNAME_CHANGE"))
        return redirect(
            get_url(cv("POST_CHANGE_USERNAME_VIEW")) or get_url(cv("POST_LOGIN_VIEW"))
        )

    if _security._want_json(request):
        form.user = current_user
        payload = dict(current_username=current_user.username)
        return base_render_json(form, additional=payload)

    return _security.render_template(
        cv("CHANGE_USERNAME_TEMPLATE"),
        change_username_form=form,
        current_username=current_user.username,
        **_security._run_ctx_processor("change_username"),
    )


def update_username(user, new_username):
    old_username = user.username
    user.username = new_username
    _datastore.put(user)
    _send_username_changed_notice(user)
    username_changed.send(
        current_app._get_current_object(),
        _async_wrapper=current_app.ensure_sync,
        user=user,
        old_username=old_username,
    )


def _send_username_changed_notice(user):
    """Sends the username changed notice email for the specified user.

    :param user: The user to send the notice to
    """
    if cv("SEND_USERNAME_CHANGE_EMAIL"):
        subject = cv("EMAIL_SUBJECT_USERNAME_CHANGE_NOTICE")
        send_mail(subject, user.email, "change_username_notice", user=user)