views.py 11.1 KB
Newer Older
1
#   views.py — accounts views
2
#
3
4
#   This file is part of debexpo -
#   https://salsa.debian.org/mentors.debian.net-team/debexpo
5
#
Jonny Lamb's avatar
Jonny Lamb committed
6
#   Copyright © 2008 Jonny Lamb <jonny@debian.org>
7
#   Copyright © 2010 Jan Dittberner <jandd@debian.org>
8
#   Copyright © 2019 Baptiste Beauplat <lyknode@cilg.org>
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#
#   Permission is hereby granted, free of charge, to any person
#   obtaining a copy of this software and associated documentation
#   files (the "Software"), to deal in the Software without
#   restriction, including without limitation the rights to use,
#   copy, modify, merge, publish, distribute, sublicense, and/or sell
#   copies of the Software, and to permit persons to whom the
#   Software is furnished to do so, subject to the following
#   conditions:
#
#   The above copyright notice and this permission notice shall be
#   included in all copies or substantial portions of the Software.
#
#   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
#   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
#   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
#   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
#   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
#   OTHER DEALINGS IN THE SOFTWARE.

31
import logging
Baptiste Beauplat's avatar
Baptiste Beauplat committed
32
from datetime import datetime
33

34
from django.conf import settings
35
36
37
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
38
from django.contrib.auth.views import PasswordResetConfirmView
39
from django.contrib.auth.tokens import default_token_generator
40
41
from django.http import HttpResponseRedirect
from django.urls import reverse, reverse_lazy
42
from django.utils.translation import gettext as _
43
from django.shortcuts import render
44
from django.utils.decorators import method_decorator
45
46
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
47
48
from django.views.decorators.cache import never_cache
from django.views.decorators.debug import sensitive_post_parameters
49

50
51
from .forms import RegistrationForm, AccountForm, ProfileForm, GPGForm, \
    EmailChangeForm
52
from .models import Profile, User, UserStatus
53
from debexpo.keyring.models import Key
54

55
from debexpo.tools.email import Email
56
from debexpo.tools.token import email_change_token_generator
57

58
log = logging.getLogger(__name__)
59
60
INTERNAL_EMAIL_URL_TOKEN = 'change-email'
INTERNAL_EMAIL_SESSION_TOKEN = '_change_email_token'
61

62

63
def _send_activate_email(request, uid, token, recipient, new_email=False):
64
65
    """
    Sends an activation email to the potential new user.
66

67
68
69
70
71
72
73
    ``key``
        Activation key that's already stored in the database.

    ``recipient``
        Email address to send to.
    """
    log.debug('Sending activation email')
74
75
76
77
78
79
80
81
82
83
84
85
86
87

    if new_email:
        email = Email('email-change.html')
        activate_url = request.scheme + '://' + request.site.domain + \
            reverse('email_change_confirm', kwargs={
                'uidb64': uid, 'token': token, 'email': recipient,
            })
    else:
        email = Email('email-password-creation.html')
        activate_url = request.scheme + '://' + request.site.domain + \
            reverse('password_reset_confirm', kwargs={
                'uidb64': uid, 'token': token
            })

88
    email.send(_('Next step: Confirm your email address'), [recipient],
89
               activate_url=activate_url, settings=settings)
90

91
92

def _register_submit(request, info):
93
94
95
    """
    Handles the form submission for a maintainer account registration.
    """
96
97
    log.info('Creating new user {} <{}> as {}'.format(
        info.get('name'), info.get('email'),
98
        UserStatus(int(info.get('account_type'))).label
99
    ))
100

101
    # Debexpo use the email field as the username
102
    user = User.objects.create_user(info.get('email'), info.get('name'))
103
    user.save()
104
105
    profile = Profile(user=user, status=info.get('account_type'))
    profile.save()
106

107
108
    uid = urlsafe_base64_encode(force_bytes(user.pk))
    token = default_token_generator.make_token(user)
109

110
    _send_activate_email(request, uid, token, user.email)
111

112
    log.debug('New user saved')
113
114
115
    return render(request, 'activate.html', {
        'settings': settings
    })
116
117


118
119
120
121
122
123
124
125
126
127
128
def _request_email_change(request, email):
    uid = urlsafe_base64_encode(force_bytes(request.user.pk))
    token = email_change_token_generator.make_token(request.user, email)

    _send_activate_email(request, uid, token, email, new_email=True)

    return render(request, 'change-email.html', {
        'settings': settings
    })


129
def _update_account(request, info):
130
    request.user.name = info.get('name')
131
132
133
    request.user.save()


134
def _update_key(request, gpg_form):
135
136
137
    data = gpg_form.key
    key = gpg_form.save(commit=False)

138
    key.user = request.user
139
140
141
142
    key.size = data.size
    key.fingerprint = data.fingerprint
    key.algorithm = data.algorithm

143
    key.save()
144
    key.update_subkeys()
145
146
147
148
149
150
151
152
153
154
155
156
157
158


def _format_fingerprint(fingerprint):
    prettify = ''
    for index in range(0, len(fingerprint)):
        prettify += fingerprint[index]
        if not (index + 1) % 4:
            prettify += '&nbsp;'
        if not (index + 1) % 20:
            prettify += '&nbsp;'

    return prettify


159
160
161
162
163
164
165
def register(request):
    """
    Provides the form for a maintainer account registration.
    """
    # Has the form been submitted?
    if request.method == 'POST':
        log.debug('Maintainer form submitted')
Baptiste Beauplat's avatar
Baptiste Beauplat committed
166
167
168
        form = RegistrationForm(None, request.POST,
                                elapsed=request.session.get('timestamp', None),
                                ip=request.META['REMOTE_ADDR'])
169

170
        if form.is_valid():
171
            return _register_submit(request, form.cleaned_data)
172
    else:
173
        form = RegistrationForm(None)
Baptiste Beauplat's avatar
Baptiste Beauplat committed
174
        request.session['timestamp'] = str(datetime.now())
175

176
177
178
179
180
    log.debug('Maintainer form requested')
    return render(request, 'register.html', {
        'settings': settings,
        'form': form
    })
181
182
183
184
185


@login_required
def profile(request):
    account_initial = {
186
        'name': request.user.name,
187
188
189
190
        'email': request.user.email
    }
    account_form = AccountForm(None, initial=account_initial)
    password_form = PasswordChangeForm(user=request.user)
191
    profile_form = ProfileForm(request.user, instance=request.user.profile)
192
193
    gpg_fingerprint = None
    try:
194
        gpg_form = GPGForm(request.user, instance=request.user.key)
195
196
        gpg_fingerprint = _format_fingerprint(request.user.key.fingerprint)
    except Key.DoesNotExist:
197
        gpg_form = GPGForm(request.user)
198
199
200
201
202
203
204
205
206
207

    if request.method == 'POST':
        if 'commit_account' in request.POST:
            account_form = AccountForm(request.user, request.POST)

            if account_form.is_valid():
                log.debug('Updating user account for '
                          '{}'.format(request.user.email))
                _update_account(request, account_form.cleaned_data)

208
209
210
211
212
                email = account_form.cleaned_data.get('email')

                if request.user.email != email:
                    return _request_email_change(request, email)

213
214
215
216
217
218
219
220
221
222
        if 'commit_password' in request.POST:
            password_form = PasswordChangeForm(user=request.user,
                                               data=request.POST)

            if password_form.is_valid():
                log.debug('Changing password for account '
                          '{}'.format(request.user.email))
                password_form.save()
                update_session_auth_hash(request, password_form.user)

Baptiste Beauplat's avatar
Baptiste Beauplat committed
223
        if 'commit_profile' in request.POST:
224
225
            profile_form = ProfileForm(request.user, request.POST,
                                       instance=request.user.profile)
Baptiste Beauplat's avatar
Baptiste Beauplat committed
226
227
228
229
230
231

            if profile_form.is_valid():
                profile = profile_form.save(commit=False)
                profile.user = request.user
                profile.save()

232
233
        if 'commit_gpg' in request.POST:
            try:
234
235
                gpg_form = GPGForm(request.user, request.POST,
                                   instance=request.user.key)
236
            except Key.DoesNotExist:
237
                gpg_form = GPGForm(request.user, request.POST)
238
239
240
241
242
243
244
245
246
247
248
249
250
251

            if gpg_form.is_valid():
                _update_key(request, gpg_form)
                gpg_fingerprint = _format_fingerprint(
                    request.user.key.fingerprint
                )

        if 'delete_gpg' in request.POST:
            try:
                key = request.user.key
            except Key.DoesNotExist:
                pass
            else:
                key.delete()
252
                gpg_form = GPGForm(request.user)
253
254
                gpg_fingerprint = None

255
256
257
258
    return render(request, 'profile.html', {
        'settings': settings,
        'account_form': account_form,
        'password_form': password_form,
259
260
261
        'profile_form': profile_form,
        'gpg_form': gpg_form,
        'gpg_fingerprint': gpg_fingerprint,
262
    })
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280


class EmailChangeConfirmView(PasswordResetConfirmView):
    form_class = EmailChangeForm
    title = _('Change your email')
    token_generator = email_change_token_generator
    success_url = reverse_lazy('email_change_complete')

    def get_initial(self):
        return {'email': self.email}

    @method_decorator(sensitive_post_parameters())
    @method_decorator(never_cache)
    @method_decorator(login_required)
    def dispatch(self, *args, **kwargs):
        assert 'uidb64' in kwargs and 'token' in kwargs and 'email' in kwargs

        self.validlink = False
281
        self.email = None
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
        self.user = self.get_user(kwargs['uidb64'])

        if self.user is not None:
            token = kwargs['token']
            self.email = kwargs['email']

            if token == INTERNAL_EMAIL_URL_TOKEN:
                session_token = \
                    self.request.session.get(INTERNAL_EMAIL_SESSION_TOKEN)

                if self.token_generator.check_token(self.user, session_token,
                                                    self.email):
                    self.validlink = True
                    return super(PasswordResetConfirmView, self) \
                        .dispatch(*args, **kwargs)
            else:
                if self.token_generator.check_token(self.user, token,
                                                    self.email):
                    self.request.session[INTERNAL_EMAIL_SESSION_TOKEN] = token
                    redirect_url = self.request.path.replace(
                        token,
                        INTERNAL_EMAIL_URL_TOKEN
                    )

                    return HttpResponseRedirect(redirect_url)

        return self.render_to_response(self.get_context_data())

    def form_valid(self, form):
        if self.validlink:
            self.user.email = self.email
            self.user.full_clean()
            self.user.save()

        del self.request.session[INTERNAL_EMAIL_SESSION_TOKEN]

        return super(PasswordResetConfirmView, self).form_valid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context.update({
            'email': self.email
        })

        return context