Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
37.10% covered (danger)
37.10%
69 / 186
30.00% covered (danger)
30.00%
3 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
AccountController
37.10% covered (danger)
37.10%
69 / 186
30.00% covered (danger)
30.00%
3 / 10
194.25
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createToSign
46.34% covered (danger)
46.34%
19 / 41
0.00% covered (danger)
0.00%
0 / 1
2.62
 signatureGenerate
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
3
 me
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 updateSettings
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
20
 deletePfx
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 uploadPfx
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 updatePfxPassword
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 readPfxData
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 setConfig
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Controller;
10
11use InvalidArgumentException;
12use OC\Authentication\Login\Chain;
13use OC\Authentication\Login\LoginData;
14use OCA\Libresign\AppInfo\Application;
15use OCA\Libresign\Exception\LibresignException;
16use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
17use OCA\Libresign\Helper\JSActions;
18use OCA\Libresign\Service\AccountService;
19use OCA\Libresign\Service\SessionService;
20use OCA\Libresign\Service\SignerElementsService;
21use OCA\Libresign\Service\SignFileService;
22use OCP\Accounts\IAccountManager;
23use OCP\AppFramework\Http;
24use OCP\AppFramework\Http\Attribute\ApiRoute;
25use OCP\AppFramework\Http\Attribute\CORS;
26use OCP\AppFramework\Http\Attribute\NoAdminRequired;
27use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
28use OCP\AppFramework\Http\Attribute\PublicPage;
29use OCP\AppFramework\Http\Attribute\UseSession;
30use OCP\AppFramework\Http\DataResponse;
31use OCP\Config\IUserConfig;
32use OCP\IL10N;
33use OCP\IRequest;
34use OCP\IURLGenerator;
35use OCP\IUserSession;
36use Psr\Log\LoggerInterface;
37
38/**
39 * @psalm-import-type LibresignCertificatePfxData from \OCA\Libresign\ResponseDefinitions
40 * @psalm-import-type LibresignAccountMeResponse from \OCA\Libresign\ResponseDefinitions
41 * @psalm-import-type LibresignAccountSettingsUpdateResponse from \OCA\Libresign\ResponseDefinitions
42 * @psalm-import-type LibresignActionMessageResponse from \OCA\Libresign\ResponseDefinitions
43 * @psalm-import-type LibresignConfigValueResponse from \OCA\Libresign\ResponseDefinitions
44 * @psalm-import-type LibresignCreateToSignResponse from \OCA\Libresign\ResponseDefinitions
45 * @psalm-import-type LibresignFile from \OCA\Libresign\ResponseDefinitions
46 * @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
47 * @psalm-import-type LibresignPagination from \OCA\Libresign\ResponseDefinitions
48 */
49class AccountController extends AEnvironmentAwareController implements ISignatureUuid {
50    use LibresignTrait;
51    public function __construct(
52        IRequest $request,
53        protected IL10N $l10n,
54        private IAccountManager $accountManager,
55        private AccountService $accountService,
56        protected SignFileService $signFileService,
57        private SignerElementsService $signerElementsService,
58        private Pkcs12Handler $pkcs12Handler,
59        private Chain $loginChain,
60        private IURLGenerator $urlGenerator,
61        private LoggerInterface $logger,
62        protected IUserSession $userSession,
63        protected SessionService $sessionService,
64        private IUserConfig $userConfig,
65    ) {
66        parent::__construct(Application::APP_ID, $request);
67    }
68
69    /**
70     * Create account to sign a document
71     *
72     * @param string $uuid Sign request uuid to allow account creation
73     * @param string $email email to the new account
74     * @param string $password the password to then new account
75     * @param string|null $signPassword The password to create certificate
76     * @return DataResponse<Http::STATUS_OK, LibresignCreateToSignResponse, array{}>|DataResponse<Http::STATUS_UNPROCESSABLE_ENTITY, LibresignActionMessageResponse, array{}>
77     *
78     * 200: OK
79     * 422: Validation page not accessible if unauthenticated
80     */
81    #[NoAdminRequired]
82    #[CORS]
83    #[NoCSRFRequired]
84    #[PublicPage]
85    #[UseSession]
86    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/account/create/{uuid}', requirements: ['apiVersion' => '(v1)'])]
87    public function createToSign(string $uuid, string $email, string $password, ?string $signPassword): DataResponse {
88        try {
89            $data = [
90                'uuid' => $uuid,
91                'user' => [
92                    'identify' => [
93                        'email' => $email,
94                    ]
95                ],
96                'password' => $password,
97                'signPassword' => $signPassword
98            ];
99            $this->accountService->validateCreateToSign($data);
100
101            $fileToSign = $this->accountService->getFileByUuid($uuid);
102            $signRequest = $this->accountService->getSignRequestByUuid($uuid);
103
104            $this->accountService->createToSign($uuid, $email, $password, $signPassword);
105            $data = [
106                // TRANSLATORS Success message shown after a guest creates an account and is redirected to sign a pending document.
107                'message' => $this->l10n->t('Success'),
108                'action' => JSActions::ACTION_SIGN,
109                'pdf' => [
110                    'url' => $this->urlGenerator->linkToRoute('libresign.page.getPdfFile', ['uuid' => $uuid])
111                ],
112                'filename' => $fileToSign['fileData']->getName(),
113                'description' => $signRequest->getDescription()
114            ];
115
116            $loginData = new LoginData(
117                $this->request,
118                trim($email),
119                $password
120            );
121            $this->loginChain->process($loginData);
122        } catch (\Throwable $th) {
123            return new DataResponse(
124                [
125                    'message' => $th->getMessage(),
126                    'action' => JSActions::ACTION_DO_NOTHING
127                ],
128                Http::STATUS_UNPROCESSABLE_ENTITY
129            );
130        }
131        return new DataResponse(
132            $data,
133            Http::STATUS_OK
134        );
135    }
136
137    /**
138     * Create PFX file using self-signed certificate
139     *
140     * @param string $signPassword The password that will be used to encrypt the certificate file
141     *
142     * @return DataResponse<Http::STATUS_OK, array{}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessageResponse, array{}>
143     *
144     * 200: Settings saved
145     * 401: Failure to create PFX file
146     */
147    #[NoAdminRequired]
148    #[NoCSRFRequired]
149    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/account/signature', requirements: ['apiVersion' => '(v1)'])]
150    public function signatureGenerate(
151        string $signPassword,
152    ): DataResponse {
153        try {
154            $identify = $this->userSession->getUser()->getEMailAddress();
155            if (!$identify) {
156                $identify = $this->userSession->getUser()->getUID()
157                    . '@'
158                    . $this->request->getServerHost();
159            }
160            $data = [
161                'user' => [
162                    'host' => $identify,
163                    'uid' => 'account:' . $this->userSession->getUser()->getUID(),
164                    'name' => $this->userSession->getUser()->getDisplayName(),
165                ],
166                'signPassword' => $signPassword,
167                'userId' => $this->userSession->getUser()->getUID()
168            ];
169            $this->accountService->validateCertificateData($data);
170            $certificate = $this->pkcs12Handler->generateCertificate(
171                $data['user'],
172                $data['signPassword'],
173                $this->userSession->getUser()->getDisplayName()
174            );
175            $this->pkcs12Handler->savePfx($this->userSession->getUser()->getUID(), $certificate);
176
177            return new DataResponse([], Http::STATUS_OK);
178        } catch (\Exception $exception) {
179            $this->logger->error($exception->getMessage());
180            return new DataResponse(
181                [
182                    'message' => $exception->getMessage()
183                ],
184                Http::STATUS_UNAUTHORIZED
185            );
186        }
187    }
188
189    /**
190     * Who am I
191     *
192     * Validates API access data and returns the authenticated user's data.
193     *
194     * @return DataResponse<Http::STATUS_OK, LibresignAccountMeResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignMessageResponse, array{}>
195     *
196     * 200: OK
197     * 404: Invalid user or password
198     */
199    #[NoAdminRequired]
200    #[CORS]
201    #[NoCSRFRequired]
202    #[PublicPage]
203    #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/account/me', requirements: ['apiVersion' => '(v1)'])]
204    public function me(): DataResponse {
205        $user = $this->userSession->getUser();
206        if (!$user) {
207            return new DataResponse(
208                [
209                    // TRANSLATORS Error shown when the user credentials are invalid or the account does not exist.
210                    'message' => $this->l10n->t('Invalid user or password')
211                ],
212                Http::STATUS_NOT_FOUND
213            );
214        }
215        return new DataResponse(
216            [
217                'account' => [
218                    'uid' => $user->getUID(),
219                    'emailAddress' => $user->getEMailAddress() ?? '',
220                    'displayName' => $user->getDisplayName()
221                ],
222                'settings' => $this->accountService->getSettings($this->userSession->getUser())
223            ],
224            Http::STATUS_OK
225        );
226    }
227
228    /**
229     * Update the account phone number
230     *
231     * @param string|null $phone the phone number to be defined. If null will remove the phone number
232     *
233     * @return DataResponse<Http::STATUS_OK, LibresignAccountSettingsUpdateResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignMessageResponse, array{}>
234     *
235     * 200: Settings saved
236     * 404: Invalid data to update phone number
237     */
238    #[NoAdminRequired]
239    #[NoCSRFRequired]
240    #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/account/settings', requirements: ['apiVersion' => '(v1)'])]
241    public function updateSettings(?string $phone = null): DataResponse {
242        try {
243            $user = $this->userSession->getUser();
244            $userAccount = $this->accountManager->getAccount($user);
245            $updatable = [
246                IAccountManager::PROPERTY_PHONE => ['value' => $phone],
247            ];
248            foreach ($updatable as $property => $data) {
249                $property = $userAccount->getProperty($property);
250                if ($data['value'] !== null) {
251                    $property->setValue($data['value']);
252                }
253            }
254            $this->accountManager->updateAccount($userAccount);
255        } catch (\Throwable $th) {
256            return new DataResponse(
257                [
258                    'message' => $th->getMessage(),
259                ],
260                Http::STATUS_NOT_FOUND
261            );
262        }
263        return new DataResponse(
264            [
265                'data' => [
266                    'userId' => $user->getUID(),
267                    'phone' => $userAccount->getProperty(IAccountManager::PROPERTY_PHONE)->getValue(),
268                    // TRANSLATORS Success message shown after the signed-in user saves account settings such as phone number.
269                    'message' => $this->l10n->t('Settings saved'),
270                ],
271            ],
272            Http::STATUS_OK
273        );
274    }
275
276    /**
277     * Delete PFX file
278     *
279     * @return DataResponse<Http::STATUS_ACCEPTED, LibresignMessageResponse, array{}>
280     *
281     * 202: Certificate deleted with success
282     */
283    #[NoAdminRequired]
284    #[NoCSRFRequired]
285    #[ApiRoute(verb: 'delete', url: '/api/{apiVersion}/account/pfx', requirements: ['apiVersion' => '(v1)'])]
286    public function deletePfx(): DataResponse {
287        $this->accountService->deletePfx($this->userSession->getUser());
288        return new DataResponse(
289            [
290                // TRANSLATORS Feedback to user after delete the certificate file that is used to sign documents with success
291                'message' => $this->l10n->t('Certificate file deleted with success.')
292            ],
293            Http::STATUS_ACCEPTED
294        );
295    }
296
297    /**
298     * Upload PFX file
299     *
300     * @return DataResponse<Http::STATUS_ACCEPTED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignMessageResponse, array{}>
301     *
302     * 202: Certificate saved with success
303     * 400: No file provided or other problem with provided file
304     */
305    #[NoAdminRequired]
306    #[NoCSRFRequired]
307    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/account/pfx', requirements: ['apiVersion' => '(v1)'])]
308    public function uploadPfx(): DataResponse {
309        $file = $this->request->getUploadedFile('file');
310        try {
311            if (empty($file)) {
312                // TRANSLATORS Error shown when uploading a certificate used to digitally sign documents and no file was sent.
313                throw new LibresignException($this->l10n->t('No certificate file provided'));
314            }
315            $this->accountService->uploadPfx($file, $this->userSession->getUser());
316        } catch (InvalidArgumentException|LibresignException $e) {
317            return new DataResponse(
318                [
319                    'message' => $e->getMessage()
320                ],
321                Http::STATUS_BAD_REQUEST
322            );
323        }
324        return new DataResponse(
325            [
326                // TRANSLATORS Feedback to user after upload the certificate file that is used to sign documents with success
327                'message' => $this->l10n->t('Certificate file saved with success.')
328            ],
329            Http::STATUS_ACCEPTED
330        );
331    }
332
333    /**
334     * Update PFX file
335     *
336     * Used to change the password of PFX file
337     *
338     * @param string $current Current password
339     * @param string $new New password
340     *
341     * @return DataResponse<Http::STATUS_ACCEPTED, LibresignMessageResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignMessageResponse, array{}>
342     *
343     * 202: Certificate saved with success
344     * 400: No file provided or other problem with provided file
345     */
346    #[NoAdminRequired]
347    #[NoCSRFRequired]
348    #[ApiRoute(verb: 'PATCH', url: '/api/{apiVersion}/account/pfx', requirements: ['apiVersion' => '(v1)'])]
349    public function updatePfxPassword($current, $new): DataResponse {
350        try {
351            $this->accountService->updatePfxPassword($this->userSession->getUser(), $current, $new);
352        } catch (LibresignException $e) {
353            return new DataResponse(
354                [
355                    'message' => $e->getMessage()
356                ],
357                Http::STATUS_BAD_REQUEST
358            );
359        }
360        return new DataResponse(
361            [
362                // TRANSLATORS Feedback to user after change the certificate file that is used to sign documents with success
363                'message' => $this->l10n->t('New password to sign documents has been created')
364            ],
365            Http::STATUS_ACCEPTED
366        );
367    }
368
369    /**
370     * Read content of PFX file
371     *
372     * @param string $password password of PFX file to decrypt the file and return his content
373     *
374     * @return DataResponse<Http::STATUS_ACCEPTED, LibresignCertificatePfxData, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignMessageResponse, array{}>
375     *
376     * 202: Certificate saved with success
377     * 400: No file provided or other problem with provided file
378     */
379    #[NoAdminRequired]
380    #[NoCSRFRequired]
381    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/account/pfx/read', requirements: ['apiVersion' => '(v1)'])]
382    public function readPfxData(string $password): DataResponse {
383        try {
384            $data = $this->accountService->readPfxData($this->userSession->getUser(), $password);
385        } catch (LibresignException $e) {
386            return new DataResponse(
387                [
388                    'message' => $e->getMessage()
389                ],
390                Http::STATUS_BAD_REQUEST
391            );
392        }
393        return new DataResponse(
394            $data,
395            Http::STATUS_ACCEPTED
396        );
397    }
398
399    /**
400     * Set user config value
401     *
402     * @param string $key Config key
403     * @param mixed $value Config value
404     * @return DataResponse<Http::STATUS_OK, LibresignConfigValueResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignMessageResponse, array{}>
405     *
406     * 200: Config updated
407     * 400: Error updating config
408     */
409    #[NoAdminRequired]
410    #[NoCSRFRequired]
411    #[ApiRoute(verb: 'PUT', url: '/api/{apiVersion}/account/config/{key}', requirements: ['apiVersion' => '(v1)'])]
412    public function setConfig(string $key): DataResponse {
413        try {
414            $user = $this->userSession->getUser();
415            if (!$user) {
416                throw new \Exception('User not authenticated');
417            }
418            $data = $this->request->getParams();
419            $value = $data['value'] ?? null;
420
421            if (is_bool($value)) {
422                $value = $value ? '1' : '0';
423            } elseif (is_array($value)) {
424                $value = json_encode($value);
425            }
426
427            $this->userConfig->setValueString($user->getUID(), Application::APP_ID, $key, $value);
428
429            return new DataResponse([
430                'key' => $key,
431                'value' => $value,
432            ], Http::STATUS_OK);
433        } catch (\Throwable $e) {
434            return new DataResponse([
435                'message' => $e->getMessage(),
436            ], Http::STATUS_BAD_REQUEST);
437        }
438    }
439}