Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
33.33% covered (danger)
33.33%
25 / 75
20.00% covered (danger)
20.00%
1 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
IdDocsController
33.33% covered (danger)
33.33%
25 / 75
20.00% covered (danger)
20.00%
1 / 5
125.96
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
 addFiles
65.22% covered (warning)
65.22%
15 / 23
0.00% covered (danger)
0.00%
0 / 1
6.05
 delete
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 listUnauthenticatedSignerDocuments
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 listToApproval
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
3.94
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Controller;
10
11use Exception;
12use OCA\Libresign\AppInfo\Application;
13use OCA\Libresign\Middleware\Attribute\RequireSignRequestUuid;
14use OCA\Libresign\Service\IdDocsService;
15use OCA\Libresign\Service\SignFileService;
16use OCA\Libresign\Service\Validation\IdentityDocumentValidator;
17use OCP\AppFramework\Http;
18use OCP\AppFramework\Http\Attribute\AnonRateLimit;
19use OCP\AppFramework\Http\Attribute\ApiRoute;
20use OCP\AppFramework\Http\Attribute\NoAdminRequired;
21use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
22use OCP\AppFramework\Http\Attribute\PublicPage;
23use OCP\AppFramework\Http\DataResponse;
24use OCP\IL10N;
25use OCP\IRequest;
26use OCP\IUserSession;
27use Psr\Log\LoggerInterface;
28
29/**
30 * @psalm-import-type LibresignFile from \OCA\Libresign\ResponseDefinitions
31 * @psalm-import-type LibresignIdDocs from \OCA\Libresign\ResponseDefinitions
32 * @psalm-import-type LibresignIdDocsApprovalListResponse from \OCA\Libresign\ResponseDefinitions
33 * @psalm-import-type LibresignIdDocsListResponse from \OCA\Libresign\ResponseDefinitions
34 * @psalm-import-type LibresignIdDocsUploadErrorResponse from \OCA\Libresign\ResponseDefinitions
35 * @psalm-import-type LibresignMessageResponse from \OCA\Libresign\ResponseDefinitions
36 * @psalm-import-type LibresignMessagesResponse from \OCA\Libresign\ResponseDefinitions
37 */
38class IdDocsController extends AEnvironmentAwareController implements ISignatureUuid {
39    use LibresignTrait;
40    public function __construct(
41        IRequest $request,
42        protected SignFileService $signFileService,
43        protected IL10N $l10n,
44        protected IdDocsService $idDocsService,
45        protected IUserSession $userSession,
46        protected IdentityDocumentValidator $identityDocumentValidator,
47        protected LoggerInterface $logger,
48    ) {
49        parent::__construct(Application::APP_ID, $request);
50    }
51
52    /**
53     * Add identification documents to user profile
54     *
55     * @param LibresignIdDocs[] $files The list of files to add to profile
56     * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignIdDocsUploadErrorResponse, array{}>
57     *
58     * 200: Certificate saved with success
59     * 401: No file provided or other problem with provided file
60     */
61    #[PublicPage]
62    #[AnonRateLimit(limit: 30, period: 60)]
63    #[NoAdminRequired]
64    #[NoCSRFRequired]
65    #[RequireSignRequestUuid(skipIfAuthenticated: true)]
66    #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/id-docs', requirements: ['apiVersion' => '(v1)'])]
67    public function addFiles(array $files): DataResponse {
68        try {
69            if ($user = $this->userSession->getUser()) {
70                $this->idDocsService->addIdDocs($files, $user);
71            } elseif ($signRequest = $this->getSignRequestEntity()) {
72                $this->idDocsService->addFilesToDocumentFolder($files, $signRequest);
73            } else {
74                throw new Exception('Invalid data');
75            }
76            return new DataResponse();
77        } catch (\Exception $exception) {
78            $exceptionData = json_decode($exception->getMessage());
79            if (isset($exceptionData->file)) {
80                $message = [
81                    'file' => $exceptionData->file,
82                    'type' => $exceptionData->type,
83                    'message' => $exceptionData->message
84                ];
85            } else {
86                $message = [
87                    'file' => null,
88                    'type' => null,
89                    'message' => $exception->getMessage()
90                ];
91            }
92            return new DataResponse(
93                $message,
94                Http::STATUS_UNAUTHORIZED
95            );
96        }
97    }
98
99    /**
100     * Delete file from account
101     *
102     * @param int $nodeId the nodeId of file to be delete
103     * @param string|null $uuid Sign request UUID for unauthenticated access
104     *
105     * @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, LibresignMessagesResponse, array{}>
106     *
107     * 200: File deleted with success
108     * 401: Failure to delete file from account
109     */
110    #[PublicPage]
111    #[AnonRateLimit(limit: 30, period: 60)]
112    #[NoAdminRequired]
113    #[NoCSRFRequired]
114    #[RequireSignRequestUuid(skipIfAuthenticated: true)]
115    #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/id-docs/{nodeId}', requirements: ['apiVersion' => '(v1)'])]
116    public function delete(int $nodeId, ?string $uuid = null): DataResponse {
117        try {
118            if ($user = $this->userSession->getUser()) {
119                $this->idDocsService->deleteIdDoc($nodeId, $user);
120            } elseif ($signRequest = $this->getSignRequestEntity()) {
121                $this->idDocsService->deleteIdDocBySignRequest($nodeId, $signRequest);
122            } else {
123                throw new Exception('Invalid data');
124            }
125            return new DataResponse();
126        } catch (\Exception $exception) {
127            return new DataResponse(
128                [
129                    'messages' => [
130                        $exception->getMessage(),
131                    ],
132                ],
133                Http::STATUS_UNAUTHORIZED,
134            );
135        }
136    }
137
138    /**
139     * List files of unauthenticated account
140     *
141     * @param string|null $userId User ID to filter by
142     * @param int|null $signRequestId Sign request ID to filter by
143     * @param int|null $page the number of page to return
144     * @param int|null $length Total of elements to return
145     * @return DataResponse<Http::STATUS_OK, LibresignIdDocsListResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignMessageResponse, array{}>
146     *
147     * 200: Certificate saved with success
148     * 404: No file provided or other problem with provided file
149     */
150    #[PublicPage]
151    #[AnonRateLimit(limit: 30, period: 60)]
152    #[NoCSRFRequired]
153    #[RequireSignRequestUuid(skipIfAuthenticated: true)]
154    #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/id-docs', requirements: ['apiVersion' => '(v1)'])]
155    public function listUnauthenticatedSignerDocuments(
156        ?string $userId = null,
157        ?int $signRequestId = null,
158        ?int $page = null,
159        ?int $length = null,
160    ): DataResponse {
161        try {
162            if ($user = $this->userSession->getUser()) {
163                $userId = $user->getUID();
164            } elseif ($signRequest = $this->getSignRequestEntity()) {
165                $signRequestId = $signRequest->getId();
166            } elseif (!$userId && !$signRequestId) {
167                throw new Exception('Invalid data');
168            }
169
170            $filter = array_filter([
171                'userId' => $userId,
172                'signRequestId' => $signRequestId,
173            ], static fn ($var) => $var !== null);
174
175            $return = $this->idDocsService->list($filter, $page, $length);
176            return new DataResponse($return, Http::STATUS_OK);
177        } catch (\Throwable $th) {
178            return new DataResponse(
179                [
180                    'message' => $th->getMessage()
181                ],
182                Http::STATUS_NOT_FOUND
183            );
184        }
185    }
186
187    /**
188     * List files that need to be approved
189     *
190     * @param string|null $userId User ID to filter by
191     * @param int|null $signRequestId Sign request ID to filter by
192     * @param int|null $page the number of page to return
193     * @param int|null $length Total of elements to return
194     * @param string|null $sortBy Sort field (e.g., 'owner', 'file_type', 'status')
195     * @param string|null $sortOrder Sort order (ASC or DESC)
196     * @return DataResponse<Http::STATUS_OK, LibresignIdDocsApprovalListResponse, array{}>|DataResponse<Http::STATUS_NOT_FOUND, LibresignMessageResponse, array{}>
197     *
198     * 200: OK
199     * 404: Account not found
200     */
201    #[NoAdminRequired]
202    #[NoCSRFRequired]
203    #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/id-docs/approval/list', requirements: ['apiVersion' => '(v1)'])]
204    public function listToApproval(
205        ?string $userId = null,
206        ?int $signRequestId = null,
207        ?int $page = null,
208        ?int $length = null,
209        ?string $sortBy = null,
210        ?string $sortOrder = null,
211    ): DataResponse {
212        try {
213            $this->identityDocumentValidator->userCanApproveValidationDocuments($this->userSession->getUser());
214            $filter = array_filter([
215                'userId' => $userId,
216                'signRequestId' => $signRequestId,
217            ], static fn ($var) => $var !== null);
218            $sort = [];
219            if ($sortBy !== null) {
220                $sort[$sortBy] = $sortOrder ?? 'DESC';
221            }
222            $return = $this->idDocsService->list($filter, $page, $length, $sort);
223            return new DataResponse($return, Http::STATUS_OK);
224        } catch (\Throwable $th) {
225            return new DataResponse(
226                [
227                    'message' => $th->getMessage()
228                ],
229                Http::STATUS_NOT_FOUND
230            );
231        }
232    }
233}