Passed
Push — master ( 23587f...1016f0 )
by Jan
04:38 queued 10s
created

UserSettingsController   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 134
c 1
b 0
f 0
dl 0
loc 257
rs 9.68
wmc 34

5 Methods

Rating   Name   Duplication   Size   Complexity  
D userSettings() 0 141 19
B removeU2FToken() 0 43 7
A __construct() 0 3 1
A resetTrustedDevices() 0 25 4
A showBackupCodes() 0 18 3
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
20
 */
21
22
namespace App\Controller;
23
24
25
use App\Entity\UserSystem\U2FKey;
26
use App\Entity\UserSystem\User;
27
use App\Form\TFAGoogleSettingsType;
28
use App\Form\UserSettingsType;
29
use App\Services\TFA\BackupCodeManager;
30
use Doctrine\ORM\EntityManagerInterface;
31
use Exception;
32
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
33
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
34
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
35
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
36
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
37
use Symfony\Component\Form\Extension\Core\Type\TextType;
38
use Symfony\Component\HttpFoundation\Request;
39
use Symfony\Component\Routing\Annotation\Route;
40
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
41
use Symfony\Component\Security\Core\Validator\Constraints\UserPassword;
42
use Symfony\Component\Validator\Constraints\Length;
43
44
/**
45
 * @Route("/user")
46
 * @package App\Controller
47
 */
48
class UserSettingsController extends AbstractController
49
{
50
    protected $demo_mode;
51
52
    public function __construct(bool $demo_mode)
53
    {
54
        $this->demo_mode = $demo_mode;
55
    }
56
57
    /**
58
     * @Route("/2fa_backup_codes", name="show_backup_codes")
59
     */
60
    public function showBackupCodes()
61
    {
62
        $user = $this->getUser();
63
64
        //When user change its settings, he should be logged  in fully.
65
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
66
67
        if (!$user instanceof User) {
68
            return new \RuntimeException('This controller only works only for Part-DB User objects!');
69
        }
70
71
        if (empty($user->getBackupCodes())) {
72
            $this->addFlash('error', 'tfa_backup.no_codes_enabled');
73
            throw new Exception('You do not have any backup codes enabled, therefore you can not view them!');
74
        }
75
76
        return $this->render('Users/backup_codes.html.twig', [
77
            'user' => $user
78
        ]);
79
    }
80
81
    /**
82
     * @Route("/u2f_delete", name="u2f_delete", methods={"DELETE"})
83
     *
84
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
85
     */
86
    public function removeU2FToken(Request $request, EntityManagerInterface $entityManager, BackupCodeManager $backupCodeManager)
87
    {
88
        if($this->demo_mode) {
89
            throw new \RuntimeException('You can not do 2FA things in demo mode');
90
        }
91
92
        $user = $this->getUser();
93
94
        //When user change its settings, he should be logged  in fully.
95
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
96
97
        if (!$user instanceof User) {
98
            throw new \RuntimeException('This controller only works only for Part-DB User objects!');
99
        }
100
101
102
        if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
103
            if($request->request->has('key_id')) {
104
                $key_id = $request->request->get('key_id');
105
                $key_repo = $entityManager->getRepository(U2FKey::class);
106
                /** @var U2FKey|null $u2f */
107
                $u2f = $key_repo->find($key_id);
108
                if($u2f === null) {
109
                    $this->addFlash('danger','tfa_u2f.u2f_delete.not_existing');
110
                    throw new \RuntimeException('Key not existing!');
111
                }
112
113
                //User can only delete its own U2F keys
114
                if ($u2f->getUser() !== $user) {
115
                    $this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied');
116
                    throw new \RuntimeException('You can only delete your own U2F keys!');
117
                }
118
119
                $backupCodeManager->disableBackupCodesIfUnused($user);
120
                $entityManager->remove($u2f);
121
                $entityManager->flush();
122
                $this->addFlash('success', 'tfa.u2f.u2f_delete.success');
123
            }
124
        } else {
125
            $this->addFlash('error','csfr_invalid');
126
        }
127
128
        return $this->redirectToRoute('user_settings');
129
    }
130
131
    /**
132
     * @Route("/invalidate_trustedDevices", name="tfa_trustedDevices_invalidate", methods={"DELETE"})
133
     */
134
    public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager)
135
    {
136
        if($this->demo_mode) {
137
            throw new \RuntimeException('You can not do 2FA things in demo mode');
138
        }
139
140
        $user = $this->getUser();
141
142
        //When user change its settings, he should be logged  in fully.
143
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
144
145
        if (!$user instanceof User) {
146
            return new \RuntimeException('This controller only works only for Part-DB User objects!');
147
        }
148
149
150
        if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) {
151
            $user->invalidateTrustedDeviceTokens();
152
            $entityManager->flush();
153
            $this->addFlash('success', 'tfa_trustedDevice.invalidate.success');
154
        } else {
155
            $this->addFlash('error','csfr_invalid');
156
        }
157
158
        return $this->redirectToRoute('user_settings');
159
    }
160
161
    /**
162
     * @Route("/settings", name="user_settings")
163
     */
164
    public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager)
165
    {
166
        /**
167
         * @var User
168
         */
169
        $user = $this->getUser();
170
171
        $page_need_reload = false;
172
173
        //When user change its settings, he should be logged  in fully.
174
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
175
176
        if (!$user instanceof User) {
177
            throw new \RuntimeException('This controller only works only for Part-DB User objects!');
178
        }
179
180
        /***************************
181
         * User settings form
182
         ***************************/
183
184
        $form = $this->createForm(UserSettingsType::class, $user);
185
186
        $form->handleRequest($request);
187
188
        if ($form->isSubmitted() && $form->isValid() && !$this->demo_mode) {
189
            //Check if user theme setting has changed
190
            if ($user->getTheme() !== $em->getUnitOfWork()->getOriginalEntityData($user)['theme']) {
191
                $page_need_reload = true;
192
            }
193
194
            $em->flush();
195
            $this->addFlash('success', 'user.settings.saved_flash');
196
        }
197
198
        /*****************************
199
         * Password change form
200
         ****************************/
201
202
        $pw_form = $this->createFormBuilder()
203
            //Username field for autocomplete
204
            ->add('username', TextType::class, [
205
                'data' => $user->getName(),
206
                'attr' => ['autocomplete' => 'username'],
207
                'disabled' => true,
208
                'row_attr' => ['class' => 'd-none']
209
            ])
210
            ->add('old_password', PasswordType::class, [
211
                'label' => 'user.settings.pw_old.label',
212
                'disabled' => $this->demo_mode,
213
                'attr' => ['autocomplete' => 'current-password'],
214
                'constraints' => [new UserPassword()], ]) //This constraint checks, if the current user pw was inputted.
215
            ->add('new_password', RepeatedType::class, [
216
                'disabled' => $this->demo_mode,
217
                'type' => PasswordType::class,
218
                'first_options' => ['label' => 'user.settings.pw_new.label'],
219
                'second_options' => ['label' => 'user.settings.pw_confirm.label'],
220
                'invalid_message' => 'password_must_match',
221
                'options' => [
222
                    'attr' => ['autocomplete' => 'new-password']
223
                ],
224
                'constraints' => [new Length([
225
                                                 'min' => 6,
226
                                                 'max' => 128,
227
                                             ])],
228
            ])
229
            ->add('submit', SubmitType::class, ['label' => 'save'])
230
            ->getForm();
231
232
        $pw_form->handleRequest($request);
233
234
        //Check if password if everything was correct, then save it to User and DB
235
        if ($pw_form->isSubmitted() && $pw_form->isValid() && !$this->demo_mode) {
236
            $password = $passwordEncoder->encodePassword($user, $pw_form['new_password']->getData());
237
            $user->setPassword($password);
238
239
            //After the change reset the password change needed setting
240
            $user->setNeedPwChange(false);
241
242
            $em->persist($user);
243
            $em->flush();
244
            $this->addFlash('success', 'user.settings.pw_changed_flash');
245
        }
246
247
        //Handle 2FA things
248
        $google_form = $this->createForm(TFAGoogleSettingsType::class, $user);
249
        $google_enabled = $user->isGoogleAuthenticatorEnabled();
250
        if (!$form->isSubmitted() && !$google_enabled) {
251
            $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());
252
            $google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret());
253
        }
254
        $google_form->handleRequest($request);
255
256
        if($google_form->isSubmitted() && $google_form->isValid() && !$this->demo_mode) {
257
            if (!$google_enabled) {
258
                //Save 2FA settings (save secrets)
259
                $user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData());
260
                $backupCodeManager->enableBackupCodes($user);
261
                $em->flush();
262
                $this->addFlash('success', 'user.settings.2fa.google.activated');
263
                return $this->redirectToRoute('user_settings');
264
            } elseif ($google_enabled) {
0 ignored issues
show
introduced by
The condition $google_enabled is always true.
Loading history...
265
                //Remove secret to disable google authenticator
266
                $user->setGoogleAuthenticatorSecret(null);
267
                $backupCodeManager->disableBackupCodesIfUnused($user);
268
                $em->flush();
269
                $this->addFlash('success', 'user.settings.2fa.google.disabled');
270
                return $this->redirectToRoute('user_settings');
271
            }
272
        }
273
274
        $backup_form = $this->get('form.factory')->createNamedBuilder('backup_codes')->add('reset_codes', SubmitType::class,[
275
            'label' => 'tfa_backup.regenerate_codes',
276
            'attr' => ['class' => 'btn-danger'],
277
            'disabled' => empty($user->getBackupCodes())
278
        ])->getForm();
279
280
        $backup_form->handleRequest($request);
281
        if ($backup_form->isSubmitted() && $backup_form->isValid() && !$this->demo_mode) {
282
            $backupCodeManager->regenerateBackupCodes($user);
283
            $em->flush();
284
            $this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated');
285
        }
286
287
288
        /******************************
289
         * Output both forms
290
         *****************************/
291
292
        return $this->render('Users/user_settings.html.twig', [
293
            'user' => $user,
294
            'settings_form' => $form->createView(),
295
            'pw_form' => $pw_form->createView(),
296
            'page_need_reload' => $page_need_reload,
297
298
            'google_form' => $google_form->createView(),
299
            'backup_form' => $backup_form->createView(),
300
            'tfa_google' => [
301
                'enabled' => $google_enabled,
302
                'qrContent' => $googleAuthenticator->getQRContent($user),
303
                'secret' => $user->getGoogleAuthenticatorSecret(),
304
                'username' => $user->getGoogleAuthenticatorUsername()
305
            ]
306
        ]);
307
    }
308
}