Passed
Push — main ( 0d6f99...16c963 )
by Jochen
08:08
created

byceps.blueprints.common.authentication.password.views   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Test Coverage

Coverage 47.57%

Importance

Changes 0
Metric Value
eloc 135
dl 0
loc 241
ccs 49
cts 103
cp 0.4757
rs 10
c 0
b 0
f 0
wmc 28

11 Functions

Rating   Name   Duplication   Size   Complexity  
A reset_form() 0 11 2
A _get_sender() 0 7 2
A update_form() 0 9 2
A _is_verification_token_valid() 0 3 1
A _redirect_to_login_form() 0 5 2
A reset() 0 16 2
A _verify_reset_token() 0 20 3
C request_reset() 0 67 8
A _get_current_user_or_404() 0 6 2
A update() 0 17 2
A request_reset_form() 0 7 2
1
"""
2
byceps.blueprints.common.authentication.password.views
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2006-2021 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from typing import Optional
10
11 1
from flask import abort, g, request
12 1
from flask_babel import gettext
13
14 1
from .....services.authentication.password import (
15
    reset_service as password_reset_service,
16
    service as password_service,
17
)
18 1
from .....services.email import service as email_service
19 1
from .....services.email.transfer.models import Sender
20 1
from .....services.site import service as site_service
21 1
from .....services.user import service as user_service
22 1
from .....services.verification_token import (
23
    service as verification_token_service,
24
)
25 1
from .....services.verification_token.transfer.models import (
26
    Token as VerificationToken,
27
)
28 1
from .....util.framework.blueprint import create_blueprint
29 1
from .....util.framework.flash import flash_error, flash_success
30 1
from .....util.framework.templating import templated
31 1
from .....util.views import redirect_to
32
33 1
from .forms import RequestResetForm, ResetForm, UpdateForm
34
35
36 1
blueprint = create_blueprint('authentication_password', __name__)
37
38
39
# -------------------------------------------------------------------- #
40
# password update
41
42
43 1
@blueprint.get('/update')
44 1
@templated
45 1
def update_form(erroneous_form=None):
46
    """Show a form to update the current user's password."""
47 1
    _get_current_user_or_404()
48
49 1
    form = erroneous_form if erroneous_form else UpdateForm()
50
51 1
    return {'form': form}
52
53
54 1
@blueprint.post('/')
55
def update():
56
    """Update the current user's password."""
57 1
    user = _get_current_user_or_404()
58
59 1
    form = UpdateForm(request.form)
60
61 1
    if not form.validate():
62
        return update_form(form)
63
64 1
    password = form.new_password.data
65
66 1
    password_service.update_password_hash(user.id, password, user.id)
67
68 1
    flash_success(gettext('Password has been updated. Please log in again.'))
69
70 1
    return _redirect_to_login_form()
71
72
73
# -------------------------------------------------------------------- #
74
# password reset
75
76
77 1
@blueprint.get('/reset/request')
78 1
@templated
79 1
def request_reset_form(erroneous_form=None):
80
    """Show a form to request a password reset."""
81
    form = erroneous_form if erroneous_form else RequestResetForm()
82
83
    return {'form': form}
84
85
86 1
@blueprint.post('/reset/request')
87
def request_reset():
88
    """Request a password reset."""
89
    form = RequestResetForm(request.form)
90
    if not form.validate():
91
        return request_reset_form(form)
92
93
    screen_name = form.screen_name.data.strip()
94
    user = user_service.find_user_by_screen_name(
95
        screen_name, case_insensitive=True
96
    )
97
98
    if (user is None) or user.deleted:
99
        flash_error(
100
            gettext(
101
                'User name "%(screen_name)s" is unknown.',
102
                screen_name=screen_name,
103
            )
104
        )
105
        return request_reset_form(form)
106
107
    email_address = user_service.get_email_address_data(user.id)
108
109
    if email_address.address is None:
110
        flash_error(
111
            gettext(
112
                'No email address is set for user "%(screen_name)s".',
113
                screen_name=screen_name,
114
            )
115
        )
116
        return request_reset_form(form)
117
118
    if not email_address.verified:
119
        flash_error(
120
            gettext(
121
                'The email address for user "%(screen_name)s" has not been verified.',
122
                screen_name=screen_name,
123
            )
124
        )
125
        if g.app_mode.is_admin():
126
            return request_reset_form(form)
127
        else:
128
            return redirect_to('user_email_address.request_confirmation_email')
129
130
    if user.suspended:
131
        flash_error(
132
            gettext(
133
                'User "%(screen_name)s" has been suspended.',
134
                screen_name=screen_name,
135
            )
136
        )
137
        return request_reset_form(form)
138
139
    sender = _get_sender()
140
141
    password_reset_service.prepare_password_reset(
142
        user, email_address.address, request.url_root, sender=sender
143
    )
144
145
    flash_success(
146
        gettext(
147
            'A link to set a new password for user "%(screen_name)s" '
148
            'has been sent to the corresponding email address.',
149
            screen_name=user.screen_name,
150
        )
151
    )
152
    return request_reset_form()
153
154
155 1
def _get_sender() -> Optional[Sender]:
156
    if not g.app_mode.is_site():
157
        return None
158
159
    site = site_service.get_site(g.site_id)
160
    email_config = email_service.get_config(site.brand_id)
161
    return email_config.sender
162
163
164 1
@blueprint.get('/reset/token/<token>')
165 1
@templated
166 1
def reset_form(token, erroneous_form=None):
167
    """Show a form to reset the current user's password."""
168
    _verify_reset_token(token)
169
170
    form = erroneous_form if erroneous_form else ResetForm()
171
172
    return {
173
        'form': form,
174
        'token': token,
175
    }
176
177
178 1
@blueprint.post('/reset/token/<token>')
179
def reset(token):
180
    """Reset the current user's password."""
181
    verification_token = _verify_reset_token(token)
182
183
    form = ResetForm(request.form)
184
    if not form.validate():
185
        return reset_form(token, form)
186
187
    password = form.new_password.data
188
189
    password_reset_service.reset_password(verification_token, password)
190
191
    flash_success(gettext('Password has been updated.'))
192
193
    return _redirect_to_login_form()
194
195
196 1
def _verify_reset_token(token: str) -> VerificationToken:
197
    verification_token = (
198
        verification_token_service.find_for_password_reset_by_token(token)
199
    )
200
201
    if not _is_verification_token_valid(verification_token):
202
        flash_error(
203
            gettext(
204
                'Invalid token. A token expires after %(hours)s hours.',
205
                hours=24,
206
            )
207
        )
208
        abort(404)
209
210
    user = user_service.find_active_user(verification_token.user_id)
211
    if user is None:
212
        flash_error(gettext('No valid token specified.'))
213
        abort(404)
214
215
    return verification_token
216
217
218 1
def _is_verification_token_valid(token: Optional[VerificationToken]) -> bool:
219
    return (token is not None) and not verification_token_service.is_expired(
220
        token
221
    )
222
223
224
# -------------------------------------------------------------------- #
225
# helpers
226
227
228 1
def _get_current_user_or_404():
229 1
    user = g.user
230 1
    if not user.authenticated:
231 1
        abort(404)
232
233 1
    return user
234
235
236 1
def _redirect_to_login_form():
237 1
    if g.app_mode.is_admin():
238
        return redirect_to('authentication_login_admin.login_form')
239
    else:
240
        return redirect_to('authentication_login.login_form')
241