From 408f51eeec51206b6572d2496ff8a208c060f630 Mon Sep 17 00:00:00 2001 From: Vincent Auger Date: Thu, 22 Aug 2024 13:15:00 -0300 Subject: [PATCH] Feat: confidentiality labels (#727) * refactor: add sensitivity label to User and Media models * chore: pint format * feat: add sensitivity label to profile and manuscript --- README.md | 27 +++++++++ app/Enums/SensitivityLabel.php | 32 +++++++++++ app/Events/ItemShared.php | 4 +- app/Events/ManagementReviewStepCreated.php | 4 +- .../ManuscriptManagementReviewComplete.php | 4 +- app/Events/ManuscriptRecordToReviewEvent.php | 4 +- .../ManuscriptRecordWithdrawnByAuthor.php | 4 +- .../ManuscriptRecordWithheldByManagement.php | 4 +- .../Auth/InvitedUserController.php | 1 - .../Auth/RegisteredUserController.php | 6 +- .../Auth/Traits/AuthorizedDomainTrait.php | 11 ++-- .../ManagementReviewStepController.php | 10 ++-- .../ManuscriptAuthorController.php | 2 +- .../ManuscriptRecordController.php | 2 +- .../Controllers/Orcid/FullFlowController.php | 19 ++++--- .../PublicationAuthorController.php | 2 +- .../Orcid/OrcidMemberAPIConnector.php | 3 +- .../Orcid/Requests/GetPersonalInfo.php | 3 +- .../Orcid/Requests/GetPersonalInfoRequest.php | 3 +- app/Http/Requests/Auth/LoginRequest.php | 7 +-- .../Resources/AuthenticatedUserResource.php | 2 + app/Http/Resources/AuthorResource.php | 2 + .../ManagementReviewStepResource.php | 2 + app/Http/Resources/MediaResource.php | 2 + app/Http/Resources/UserResource.php | 2 + app/Models/Invitation.php | 1 - app/Models/ManuscriptAuthor.php | 1 - app/Models/ManuscriptRecord.php | 22 ++++++-- app/Models/Publication.php | 2 +- app/Models/Region.php | 4 +- app/Models/Shareable.php | 2 +- app/Models/User.php | 2 +- app/Queries/ExpertiseListQuery.php | 4 +- app/Queries/JournalListQuery.php | 2 +- app/Queries/OrganizationListQuery.php | 4 +- app/Rules/Doi.php | 3 +- app/Rules/UserNotAManuscriptAuthor.php | 6 +- bootstrap/app.php | 1 - config/app.php | 1 - config/backup.php | 2 +- config/osp.php | 3 +- .../factories/ManuscriptRecordFactory.php | 2 +- ...2024_06_03_162357_create_health_tables.php | 8 +-- ...08_07_191215_create_activity_log_table.php | 4 +- ...add_event_column_to_activity_log_table.php | 4 +- ...atch_uuid_column_to_activity_log_table.php | 4 +- database/seeders/DatabaseSeeder.php | 1 - database/seeders/FunctionalAreaSeeder.php | 3 +- database/seeders/LocalTestDataSeeder.php | 4 ++ .../src/components/SensitivityLabelChip.vue | 43 +++++++++++++++ resources/src/locales/en.json | 9 ++- resources/src/locales/fr.json | 9 ++- resources/src/models/Author/Author.ts | 7 ++- .../components/ManageAuthorProfileCard.vue | 22 +++++--- .../ManagementReviewStep.ts | 35 ++++++------ .../ManagementReviewStepDecisionSpan.vue | 12 ++-- .../ManagementReviewStepTimelineEntry.vue | 55 +++++++++---------- .../views/ManagementReviewStepsView.vue | 42 +++++++------- .../ManuscriptFileManagementCard.vue | 19 ++++--- resources/src/models/Resource.ts | 3 + .../src/models/User/AuthenticatedUser.ts | 3 +- resources/src/models/User/User.ts | 3 +- .../User/components/ManageUserProfileCard.vue | 21 ++++--- routes/web.php | 2 - tests/Feature/AdminAreasTest.php | 6 +- tests/Feature/Auth/RegistrationTest.php | 6 +- tests/Feature/Models/ManuscriptRecordTest.php | 2 +- tests/Feature/UserInvitationTest.php | 2 +- 68 files changed, 337 insertions(+), 216 deletions(-) create mode 100644 app/Enums/SensitivityLabel.php create mode 100644 resources/src/components/SensitivityLabelChip.vue diff --git a/README.md b/README.md index a002aae5..52fda4ab 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,30 @@ The commit message should be structured as follows: 'test' ] ``` + +## Running Tests Locally + +### Backend Tests + +```sh +php artisan test +``` + +### Frontend Tests + +We use Cypress for the front-end E2E tests. It must run with the +`env.ci` environment. + +Before starting the test, start the dev server. For the frontend, +you can either use `pnpnm dev` or `pnmp build`. In most cases, `dev` +is better as changes to the code can instantently be tested again. + +```sh +pnpm dev +php artisan server --env=ci +``` + +Once the local server is up and running, you can launch Cypress +with `pnpm cy:open` or just run the tests with `pnpm cy:run` + +If using WSL, you will need to follow this [guide](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) first. diff --git a/app/Enums/SensitivityLabel.php b/app/Enums/SensitivityLabel.php new file mode 100644 index 00000000..62d23f0b --- /dev/null +++ b/app/Enums/SensitivityLabel.php @@ -0,0 +1,32 @@ +contains($locale) ?: $locale = null; + if ($locale == null) { + $locale = App::getLocale(); + } + + return match ($locale) { + 'en' => match ($this->value) { + 'Unclassified' => 'Unclassified', + 'Protected A' => 'Protected A', + }, + 'fr' => match ($this->value) { + 'Unclassified' => 'Non classifié', + 'Protected A' => 'Protégé A', + }, + default => $this->value, + }; + } +} diff --git a/app/Events/ItemShared.php b/app/Events/ItemShared.php index 2600e1ef..5b990f96 100644 --- a/app/Events/ItemShared.php +++ b/app/Events/ItemShared.php @@ -15,9 +15,7 @@ class ItemShared /** * Create a new event instance. */ - public function __construct(public Shareable $shareableItem) - { - } + public function __construct(public Shareable $shareableItem) {} /** * Get the channels the event should broadcast on. diff --git a/app/Events/ManagementReviewStepCreated.php b/app/Events/ManagementReviewStepCreated.php index 59bf80ab..c51272b1 100644 --- a/app/Events/ManagementReviewStepCreated.php +++ b/app/Events/ManagementReviewStepCreated.php @@ -17,9 +17,7 @@ class ManagementReviewStepCreated * * @return void */ - public function __construct(public ManagementReviewStep $managementReviewStep) - { - } + public function __construct(public ManagementReviewStep $managementReviewStep) {} /** * Get the channels the event should broadcast on. diff --git a/app/Events/ManuscriptManagementReviewComplete.php b/app/Events/ManuscriptManagementReviewComplete.php index 883a8b00..5b8ed02f 100644 --- a/app/Events/ManuscriptManagementReviewComplete.php +++ b/app/Events/ManuscriptManagementReviewComplete.php @@ -17,9 +17,7 @@ class ManuscriptManagementReviewComplete * * @return void */ - public function __construct(public ManuscriptRecord $manuscriptRecord) - { - } + public function __construct(public ManuscriptRecord $manuscriptRecord) {} /** * Get the channels the event should broadcast on. diff --git a/app/Events/ManuscriptRecordToReviewEvent.php b/app/Events/ManuscriptRecordToReviewEvent.php index 37551d1f..3e460514 100644 --- a/app/Events/ManuscriptRecordToReviewEvent.php +++ b/app/Events/ManuscriptRecordToReviewEvent.php @@ -18,9 +18,7 @@ class ManuscriptRecordToReviewEvent * * @return void */ - public function __construct(public ManuscriptRecord $manuscriptRecord, public User $divisionManagerUser) - { - } + public function __construct(public ManuscriptRecord $manuscriptRecord, public User $divisionManagerUser) {} /** * Get the channels the event should broadcast on. diff --git a/app/Events/ManuscriptRecordWithdrawnByAuthor.php b/app/Events/ManuscriptRecordWithdrawnByAuthor.php index a017984f..05c049a0 100644 --- a/app/Events/ManuscriptRecordWithdrawnByAuthor.php +++ b/app/Events/ManuscriptRecordWithdrawnByAuthor.php @@ -17,9 +17,7 @@ class ManuscriptRecordWithdrawnByAuthor * * @return void */ - public function __construct(public ManuscriptRecord $manuscriptRecord) - { - } + public function __construct(public ManuscriptRecord $manuscriptRecord) {} /** * Get the channels the event should broadcast on. diff --git a/app/Events/ManuscriptRecordWithheldByManagement.php b/app/Events/ManuscriptRecordWithheldByManagement.php index 187c1dda..285a3e66 100644 --- a/app/Events/ManuscriptRecordWithheldByManagement.php +++ b/app/Events/ManuscriptRecordWithheldByManagement.php @@ -17,9 +17,7 @@ class ManuscriptRecordWithheldByManagement * * @return void */ - public function __construct(public ManuscriptRecord $manuscriptRecord) - { - } + public function __construct(public ManuscriptRecord $manuscriptRecord) {} /** * Get the channels the event should broadcast on. diff --git a/app/Http/Controllers/Auth/InvitedUserController.php b/app/Http/Controllers/Auth/InvitedUserController.php index 61e6a01c..d03ca4f8 100644 --- a/app/Http/Controllers/Auth/InvitedUserController.php +++ b/app/Http/Controllers/Auth/InvitedUserController.php @@ -16,7 +16,6 @@ class InvitedUserController extends Controller { - use AuthorizedDomainTrait; public function invite(Request $request): UserResource diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index c7ca8c27..bb76c149 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -15,8 +15,8 @@ class RegisteredUserController extends Controller { - use LocaleTrait; use AuthorizedDomainTrait; + use LocaleTrait; /** * Handle an incoming registration request. @@ -47,8 +47,8 @@ public function store(Request $request): JsonResponse // User exits and does not have an invitation or is active (has registered) if ($user->active) { throw ValidationException::withMessages( - ['account' => __('Problem with registration, please contact support') - ]); + ['account' => __('Problem with registration, please contact support'), + ]); } // User exists but is not active (has not registered), so update the user diff --git a/app/Http/Controllers/Auth/Traits/AuthorizedDomainTrait.php b/app/Http/Controllers/Auth/Traits/AuthorizedDomainTrait.php index 8a99370f..c826c892 100644 --- a/app/Http/Controllers/Auth/Traits/AuthorizedDomainTrait.php +++ b/app/Http/Controllers/Auth/Traits/AuthorizedDomainTrait.php @@ -1,16 +1,13 @@ isEmailDomainAllowed($email)) { throw ValidationException::withMessages([ - 'email' => __('Email domain not allowed') + 'email' => __('Email domain not allowed'), ]); } } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/ManagementReviewStepController.php b/app/Http/Controllers/ManagementReviewStepController.php index f8a208a1..88eeea50 100644 --- a/app/Http/Controllers/ManagementReviewStepController.php +++ b/app/Http/Controllers/ManagementReviewStepController.php @@ -68,7 +68,7 @@ public function approve(Request $request, ManuscriptRecord $manuscriptRecord, Ma 'comments' => 'required|string', ])->validate(); - $nextReviewStep = new ManagementReviewStep(); + $nextReviewStep = new ManagementReviewStep; $nextReviewStep->user_id = $validated['next_user_id']; $nextReviewStep->status = ManagementReviewStepStatus::PENDING; $nextReviewStep->decision = ManagementReviewStepDecision::NONE; @@ -124,7 +124,7 @@ public function withhold(Request $request, ManuscriptRecord $manuscriptRecord, M // if the next user is not set, then the review is complete and the manuscript review is complete. if (isset($validated['next_user_id'])) { - $nextReviewStep = new ManagementReviewStep(); + $nextReviewStep = new ManagementReviewStep; $nextReviewStep->user_id = $validated['next_user_id']; $nextReviewStep->status = ManagementReviewStepStatus::PENDING; $nextReviewStep->decision = ManagementReviewStepDecision::NONE; @@ -172,7 +172,7 @@ public function reassign(Request $request, ManuscriptRecord $manuscriptRecord, M 'comments' => 'required|string', ])->validate(); - $nextReviewStep = new ManagementReviewStep(); + $nextReviewStep = new ManagementReviewStep; $nextReviewStep->user_id = $validated['next_user_id']; $nextReviewStep->status = ManagementReviewStepStatus::PENDING; $nextReviewStep->decision = ManagementReviewStepDecision::NONE; @@ -207,7 +207,7 @@ public function flag(Request $request, ManuscriptRecord $manuscriptRecord, Manag 'comments' => 'required|string', ])->validate(); - $nextReviewStep = new ManagementReviewStep(); + $nextReviewStep = new ManagementReviewStep; $nextReviewStep->user_id = $manuscriptRecord->user_id; $nextReviewStep->status = ManagementReviewStepStatus::ON_HOLD; $nextReviewStep->decision = ManagementReviewStepDecision::NONE; @@ -244,7 +244,7 @@ public function flaggedResponse(Request $request, ManuscriptRecord $manuscriptRe 'comments' => 'required|string', ])->validate(); - $nextReviewStep = new ManagementReviewStep(); + $nextReviewStep = new ManagementReviewStep; $nextReviewStep->user_id = $previousStep->user_id; $nextReviewStep->status = ManagementReviewStepStatus::PENDING; $nextReviewStep->decision = ManagementReviewStepDecision::NONE; diff --git a/app/Http/Controllers/ManuscriptAuthorController.php b/app/Http/Controllers/ManuscriptAuthorController.php index 47516e6c..2a1f231a 100644 --- a/app/Http/Controllers/ManuscriptAuthorController.php +++ b/app/Http/Controllers/ManuscriptAuthorController.php @@ -42,7 +42,7 @@ public function store(Request $request, ManuscriptRecord $manuscriptRecord): Jso $author = Author::find($validated['author_id']); - $manuscriptAuthor = new ManuscriptAuthor(); + $manuscriptAuthor = new ManuscriptAuthor; $manuscriptAuthor->manuscript_record_id = $manuscriptRecord->id; $manuscriptAuthor->author_id = $validated['author_id']; $manuscriptAuthor->is_corresponding_author = $validated['is_corresponding_author'] ?? false; diff --git a/app/Http/Controllers/ManuscriptRecordController.php b/app/Http/Controllers/ManuscriptRecordController.php index 529c89cd..12192df3 100644 --- a/app/Http/Controllers/ManuscriptRecordController.php +++ b/app/Http/Controllers/ManuscriptRecordController.php @@ -111,7 +111,7 @@ public function submitForReview(Request $request, ManuscriptRecord $manuscriptRe $reviewUser = User::findOrFail($validated['reviewer_user_id']); // create the first management review step for this record - $reviewStep = new ManagementReviewStep(); + $reviewStep = new ManagementReviewStep; $reviewStep->manuscript_record_id = $manuscriptRecord->id; $reviewStep->decision_expected_by = now()->addBusinessDays(config('osp.management_review.decision_expected_business_days')); $reviewStep->user_id = $reviewUser->id; diff --git a/app/Http/Controllers/Orcid/FullFlowController.php b/app/Http/Controllers/Orcid/FullFlowController.php index d8fa9015..0e9d4555 100644 --- a/app/Http/Controllers/Orcid/FullFlowController.php +++ b/app/Http/Controllers/Orcid/FullFlowController.php @@ -25,7 +25,7 @@ class FullFlowController public function callback(Request $request): RedirectResponse { - Log::debug("Callback from ORCID"); + Log::debug('Callback from ORCID'); Log::debug($request->all()); $frontendUrl = config('app.frontend_url'); @@ -37,7 +37,8 @@ public function callback(Request $request): RedirectResponse if (! Cache::has($validated['key'])) { Log::error('Invalid key for ORCID callback'); - $url = $frontendUrl . '#/auth/orcid-callback?status=invalid-key'; + $url = $frontendUrl.'#/auth/orcid-callback?status=invalid-key'; + return redirect($url); } @@ -47,7 +48,8 @@ public function callback(Request $request): RedirectResponse if (! $user) { Log::error('Invalid user for ORCID callback'); - $url = $frontendUrl . '#/auth/orcid-callback?status=invalid-user'; + $url = $frontendUrl.'#/auth/orcid-callback?status=invalid-user'; + return redirect($url); } @@ -70,7 +72,8 @@ public function callback(Request $request): RedirectResponse if ($response->failed()) { Log::error('Failed to get access token from ORCID'); Log::debug($response->body()); - return redirect($frontendUrl . '#/auth/orcid-callback?status=failed'); + + return redirect($frontendUrl.'#/auth/orcid-callback?status=failed'); } Log::info('Access token received from ORCID'); @@ -80,12 +83,11 @@ public function callback(Request $request): RedirectResponse $scope = $response->json('scope'); $orcid = $response->json('orcid'); - CauserResolver::setCauser($user); $author = $user->author; - $author->orcid = 'https://' . $baseUrl . '/' . $orcid; + $author->orcid = 'https://'.$baseUrl.'/'.$orcid; $author->orcid_access_token = $accessToken; $author->orcid_token_scope = $scope; $author->orcid_refresh_token = $refreshToken; @@ -93,7 +95,7 @@ public function callback(Request $request): RedirectResponse $author->orcid_expires_at = now()->addSeconds($expiresIn); $author->save(); - return redirect($frontendUrl . '#/auth/orcid-callback?status=success'); + return redirect($frontendUrl.'#/auth/orcid-callback?status=success'); } /** @@ -118,7 +120,7 @@ public function redirect(Request $request): RedirectResponse Cache::add($key, Auth::id(), now()->addMinutes(15)); // add key to redirect URI - $redirectURI .= '?key=' . $key; + $redirectURI .= '?key='.$key; // encode URI - if it's not encoded, ORCID will returns to base URL $encoded = urlencode($redirectURI); @@ -131,6 +133,7 @@ public function redirect(Request $request): RedirectResponse Log::info('Redirecting to ORCID for authorization'); Log::debug($link); + // redirect to ORCID return redirect($link); } diff --git a/app/Http/Controllers/PublicationAuthorController.php b/app/Http/Controllers/PublicationAuthorController.php index fe791f3b..faeb51c6 100644 --- a/app/Http/Controllers/PublicationAuthorController.php +++ b/app/Http/Controllers/PublicationAuthorController.php @@ -43,7 +43,7 @@ public function store(Request $request, Publication $publication): JsonResource $author = Author::find($validated['author_id']); - $publicationAuthor = new PublicationAuthor(); + $publicationAuthor = new PublicationAuthor; $publicationAuthor->publication_id = $publication->id; $publicationAuthor->author_id = $validated['author_id']; $publicationAuthor->is_corresponding_author = $validated['is_corresponding_author'] ?? false; diff --git a/app/Http/Integrations/Orcid/OrcidMemberAPIConnector.php b/app/Http/Integrations/Orcid/OrcidMemberAPIConnector.php index b0b14115..346c441b 100644 --- a/app/Http/Integrations/Orcid/OrcidMemberAPIConnector.php +++ b/app/Http/Integrations/Orcid/OrcidMemberAPIConnector.php @@ -18,8 +18,7 @@ class OrcidMemberAPIConnector extends Connector public function __construct( protected readonly string $bearerToken, protected readonly string $orcid - ) { - } + ) {} /** * The Base URL of the Member API this will use the sandbox if diff --git a/app/Http/Integrations/Orcid/Requests/GetPersonalInfo.php b/app/Http/Integrations/Orcid/Requests/GetPersonalInfo.php index 9d810880..35422ac4 100644 --- a/app/Http/Integrations/Orcid/Requests/GetPersonalInfo.php +++ b/app/Http/Integrations/Orcid/Requests/GetPersonalInfo.php @@ -15,8 +15,7 @@ class GetPersonalInfo extends Request public function __construct( protected PersonalInfoEndpoints $endpoint - ) { - } + ) {} /** * The endpoint for the request diff --git a/app/Http/Integrations/Orcid/Requests/GetPersonalInfoRequest.php b/app/Http/Integrations/Orcid/Requests/GetPersonalInfoRequest.php index a46fc9d3..62b99e95 100644 --- a/app/Http/Integrations/Orcid/Requests/GetPersonalInfoRequest.php +++ b/app/Http/Integrations/Orcid/Requests/GetPersonalInfoRequest.php @@ -25,8 +25,7 @@ class GetPersonalInfoRequest extends Request */ public function __construct( protected PersonalInfoEndpoints $endpoint = PersonalInfoEndpoints::PERSON - ) { - } + ) {} /** * The endpoint for the request diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php index 89d645ae..79be871e 100644 --- a/app/Http/Requests/Auth/LoginRequest.php +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -9,8 +9,6 @@ use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; -use Spatie\Activitylog\Contracts\Activity; -use Spatie\Activitylog\Models\Activity as ModelsActivity; class LoginRequest extends FormRequest { @@ -104,10 +102,9 @@ public function ensureIsNotRateLimited() */ public function throttleKey() { - return Str::lower($this->input('email')) . '|' . $this->ip(); + return Str::lower($this->input('email')).'|'.$this->ip(); } - /** * Log lockout but rate limit to one entry per 2 minutes * as we don't want to log all failed attempts in the @@ -118,7 +115,7 @@ public function logLockout() RateLimiter::attempt( $this->throttleKey().'|lockout', 1, - function() { + function () { activity()->withProperties([ 'ip' => $this->ip(), 'email' => $this->email, diff --git a/app/Http/Resources/AuthenticatedUserResource.php b/app/Http/Resources/AuthenticatedUserResource.php index 2eba6604..0a246072 100644 --- a/app/Http/Resources/AuthenticatedUserResource.php +++ b/app/Http/Resources/AuthenticatedUserResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use App\Enums\SensitivityLabel; use Illuminate\Http\Resources\Json\JsonResource; /** @@ -23,6 +24,7 @@ public function toArray($request) 'last_name' => $this->last_name, 'email' => $this->email, 'locale' => $this->locale, + 'sensitivity_label' => SensitivityLabel::ProtectedA, 'new_password_required' => $this->new_password_required, 'author' => AuthorResource::make($this->author), 'roles' => $this->getRoleNames(), diff --git a/app/Http/Resources/AuthorResource.php b/app/Http/Resources/AuthorResource.php index 8c6d52da..a242d655 100644 --- a/app/Http/Resources/AuthorResource.php +++ b/app/Http/Resources/AuthorResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use App\Enums\SensitivityLabel; use Auth; use Illuminate\Http\Resources\Json\JsonResource; @@ -28,6 +29,7 @@ public function toArray($request): array 'email' => $this->email, 'user_id' => $this->user_id, 'organization_id' => $this->organization_id, + 'sensitivity_label' => SensitivityLabel::ProtectedA, 'organization' => OrganizationResource::make($this->whenLoaded('organization')), 'expertises' => ExpertiseResource::collection($this->whenLoaded('expertises')), ], diff --git a/app/Http/Resources/ManagementReviewStepResource.php b/app/Http/Resources/ManagementReviewStepResource.php index 09db7a9a..909abd5e 100644 --- a/app/Http/Resources/ManagementReviewStepResource.php +++ b/app/Http/Resources/ManagementReviewStepResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use App\Enums\SensitivityLabel; use Auth; use Illuminate\Http\Resources\Json\JsonResource; @@ -28,6 +29,7 @@ public function toArray($request) 'completed_at' => $this->completed_at, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, + 'sensitivity_label' => SensitivityLabel::ProtectedA, // relationships 'manuscript_record' => ManuscriptRecordSummaryResource::make($this->whenLoaded('manuscriptRecord')), 'previous_step' => ManagementReviewStepResource::make($this->whenLoaded('previousStep')), diff --git a/app/Http/Resources/MediaResource.php b/app/Http/Resources/MediaResource.php index ba38d2e0..3387fb96 100644 --- a/app/Http/Resources/MediaResource.php +++ b/app/Http/Resources/MediaResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use App\Enums\SensitivityLabel; use Illuminate\Http\Resources\Json\JsonResource; class MediaResource extends JsonResource @@ -22,6 +23,7 @@ public function toArray($request) 'collection_name' => $this->collection_name, 'mime_type' => $this->mime_type, 'locked' => $this->getCustomProperty('locked', false), + 'sensitivity_label' => $this->getCustomProperty('sensitivity_label', SensitivityLabel::Unclassified), ]; } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 8cc6f17a..1b7e17d3 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources; +use App\Enums\SensitivityLabel; use Illuminate\Http\Resources\Json\JsonResource; class UserResource extends JsonResource @@ -21,6 +22,7 @@ public function toArray($request) 'last_name' => $this->last_name, 'email' => $this->email, 'locale' => $this->locale, + 'sensitivity_label' => SensitivityLabel::ProtectedA, 'author' => AuthorResource::make($this->whenLoaded('author')), ], 'can' => [ diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 69739fd9..a80b017e 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -29,7 +29,6 @@ class Invitation extends Model 'invitation_token', ]; - // logging options public function getActivitylogOptions(): LogOptions { diff --git a/app/Models/ManuscriptAuthor.php b/app/Models/ManuscriptAuthor.php index 5caae2e7..ab25f8d3 100644 --- a/app/Models/ManuscriptAuthor.php +++ b/app/Models/ManuscriptAuthor.php @@ -13,7 +13,6 @@ class ManuscriptAuthor extends Model use HasFactory; use LogsActivity; - public $guarded = [ 'id', 'created_at', diff --git a/app/Models/ManuscriptRecord.php b/app/Models/ManuscriptRecord.php index da02ed73..446bce40 100644 --- a/app/Models/ManuscriptRecord.php +++ b/app/Models/ManuscriptRecord.php @@ -5,6 +5,7 @@ use App\Contracts\Fundable; use App\Enums\ManuscriptRecordStatus; use App\Enums\ManuscriptRecordType; +use App\Enums\SensitivityLabel; use App\Models\Concerns\HasUlid; use App\Traits\FundableTrait; use Exception; @@ -31,8 +32,8 @@ class ManuscriptRecord extends Model implements Fundable, HasMedia use HasFactory; use HasUlid; use InteractsWithMedia; - use SoftDeletes; use LogsActivity; + use SoftDeletes; public $guarded = [ 'id', @@ -162,11 +163,20 @@ public function getManuscriptFiles() } /** - * Add a manuscript file to the manuscript record. + * Add a manuscript file to the manuscript record. Since manuscripts are + * unpublished and should not be shared, we set the sensitivity label + * to Protected A by default. */ - public function addManuscriptFile($file) + public function addManuscriptFile($file, $preserveOriginal = false) { - return $this->addMedia($file)->withCustomProperties(['locked' => false])->toMediaCollection('manuscript'); + return $this->addMedia($file) + ->withCustomProperties([ + 'locked' => false, + 'sensitivity_label' => SensitivityLabel::ProtectedA, + ]) + ->preservingOriginal($preserveOriginal) + ->toMediaCollection('manuscript'); + } public function getManuscriptFile($uuid) @@ -180,7 +190,7 @@ public function getManuscriptFile($uuid) public function deleteManuscriptFile($uuid, $force = false) { $media = $this->getMedia('manuscript')->where('uuid', $uuid)->first(); - if (!$media) { + if (! $media) { throw new FileNotFoundException('File not found.'); } if ($force) { @@ -234,7 +244,7 @@ public function validateIsFilled(bool $noExceptions = false): bool } }); - if (!$noExceptions) { + if (! $noExceptions) { $validator->validate(); } diff --git a/app/Models/Publication.php b/app/Models/Publication.php index 5c039de6..4706d29d 100644 --- a/app/Models/Publication.php +++ b/app/Models/Publication.php @@ -20,8 +20,8 @@ class Publication extends Model implements Fundable, HasMedia use FundableTrait; use HasFactory; use InteractsWithMedia; - use SoftDeletes; use LogsActivity; + use SoftDeletes; public $guarded = [ 'id', diff --git a/app/Models/Region.php b/app/Models/Region.php index fae838fe..9168516b 100644 --- a/app/Models/Region.php +++ b/app/Models/Region.php @@ -4,6 +4,4 @@ use Illuminate\Database\Eloquent\Model; -class Region extends Model -{ -} +class Region extends Model {} diff --git a/app/Models/Shareable.php b/app/Models/Shareable.php index 6eea8c56..5e3b149b 100644 --- a/app/Models/Shareable.php +++ b/app/Models/Shareable.php @@ -28,7 +28,7 @@ class Shareable extends Model 'updated_at', ]; - //logging options + //logging options public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() diff --git a/app/Models/User.php b/app/Models/User.php index 9bc5ab01..bf32e325 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -20,11 +20,11 @@ class User extends Authenticatable implements HasLocalePreference, MustVerifyEmail { use AuthenticationLoggable; + use CausesActivity; use HasApiTokens; use HasFactory; use HasRoles; use Notifiable; - use CausesActivity; /** * The attributes that are mass assignable. diff --git a/app/Queries/ExpertiseListQuery.php b/app/Queries/ExpertiseListQuery.php index 5e2c44a3..cfac636a 100644 --- a/app/Queries/ExpertiseListQuery.php +++ b/app/Queries/ExpertiseListQuery.php @@ -19,8 +19,8 @@ public function __construct() ->allowedSorts([ 'name_en', 'name_fr', - AllowedSort::custom('name-en-length', new StringLengthSort(), 'name_en'), - AllowedSort::custom('name-fr-length', new StringLengthSort(), 'name_fr'), + AllowedSort::custom('name-en-length', new StringLengthSort, 'name_en'), + AllowedSort::custom('name-fr-length', new StringLengthSort, 'name_fr'), ]) ->allowedFilters(([ AllowedFilter::partial('name_en'), diff --git a/app/Queries/JournalListQuery.php b/app/Queries/JournalListQuery.php index 737c8618..fea1c74e 100644 --- a/app/Queries/JournalListQuery.php +++ b/app/Queries/JournalListQuery.php @@ -20,7 +20,7 @@ public function __construct() 'title_en', 'title_fr', 'publisher', - AllowedSort::custom('title-length', new StringLengthSort(), 'title_en'), + AllowedSort::custom('title-length', new StringLengthSort, 'title_en'), ]) ->allowedFilters([ AllowedFilter::exact('id'), diff --git a/app/Queries/OrganizationListQuery.php b/app/Queries/OrganizationListQuery.php index ca2d8e77..74f3bdc8 100644 --- a/app/Queries/OrganizationListQuery.php +++ b/app/Queries/OrganizationListQuery.php @@ -20,8 +20,8 @@ public function __construct() 'name_en', 'name_fr', 'country_code', - AllowedSort::custom('name-fr-length', new StringLengthSort(), 'name_fr'), - AllowedSort::custom('name-en-length', new StringLengthSort(), 'name_en'), + AllowedSort::custom('name-fr-length', new StringLengthSort, 'name_fr'), + AllowedSort::custom('name-en-length', new StringLengthSort, 'name_en'), ]) ->allowedFilters([ AllowedFilter::exact('id'), diff --git a/app/Rules/Doi.php b/app/Rules/Doi.php index cbc21537..b7563831 100644 --- a/app/Rules/Doi.php +++ b/app/Rules/Doi.php @@ -9,12 +9,11 @@ class Doi implements ValidationRule { /** * Check if the given value is a valid DOI. - * */ public function validate(string $attribute, mixed $value, Closure $fail): void { // check the value against the DOI regex - if (!preg_match('/^10\.\d{4,9}\/[-._;()\/:A-Z0-9]+$/i', $value)) { + if (! preg_match('/^10\.\d{4,9}\/[-._;()\/:A-Z0-9]+$/i', $value)) { $fail('The :attribute is not a valid DOI.'); } } diff --git a/app/Rules/UserNotAManuscriptAuthor.php b/app/Rules/UserNotAManuscriptAuthor.php index 06475592..8c264e45 100644 --- a/app/Rules/UserNotAManuscriptAuthor.php +++ b/app/Rules/UserNotAManuscriptAuthor.php @@ -15,16 +15,12 @@ */ class UserNotAManuscriptAuthor implements ValidationRule { - /** * Create a new rule instance. * * @return void */ - public function __construct(public ManuscriptRecord $manuscriptRecord) - { - } - + public function __construct(public ManuscriptRecord $manuscriptRecord) {} public function validate(string $attribute, mixed $value, Closure $fail): void { diff --git a/bootstrap/app.php b/bootstrap/app.php index f46e3916..037e17df 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -41,7 +41,6 @@ App\Exceptions\Handler::class ); - /* |-------------------------------------------------------------------------- | Return The Application diff --git a/config/app.php b/config/app.php index 4b402acd..79dad41a 100644 --- a/config/app.php +++ b/config/app.php @@ -226,5 +226,4 @@ 'enalbed' => env('HEALTH_CHECK_ENABLED', true), ], - ]; diff --git a/config/backup.php b/config/backup.php index 3a12dba2..2ab043ed 100644 --- a/config/backup.php +++ b/config/backup.php @@ -147,7 +147,7 @@ /* * The filename prefix used for the backup zip file. */ - 'filename_prefix' => env('APP_NAME', '') . '_', + 'filename_prefix' => env('APP_NAME', '').'_', /* * The disk names on which the backups will be stored. diff --git a/config/osp.php b/config/osp.php index fa775ff8..96437aeb 100644 --- a/config/osp.php +++ b/config/osp.php @@ -5,8 +5,7 @@ /* * The allowed registration email domains for new users. */ - 'allowed_registration_email_domains' => - explode(',', strtolower(env('ALLOWED_REGISTRATION_EMAIL_DOMAINS', 'dfo-mpo.gc.ca'))), + 'allowed_registration_email_domains' => explode(',', strtolower(env('ALLOWED_REGISTRATION_EMAIL_DOMAINS', 'dfo-mpo.gc.ca'))), /* The default pagination for API requests. This is used by the diff --git a/database/factories/ManuscriptRecordFactory.php b/database/factories/ManuscriptRecordFactory.php index dc54af72..69c75f56 100644 --- a/database/factories/ManuscriptRecordFactory.php +++ b/database/factories/ManuscriptRecordFactory.php @@ -50,7 +50,7 @@ public function filled() $manuscript->manuscriptAuthors()->save(ManuscriptAuthor::factory()->make(['is_corresponding_author' => true])); // create a corresponding author $manuscript->manuscriptAuthors()->saveMany(ManuscriptAuthor::factory()->count(3)->make()); // create 3 other authors $manuscript->fundingSources()->saveMany(FundingSource::factory()->count(3)->make()); // create 3 funding sources - $manuscript->addMedia(getcwd() . '/database/factories/files/BieberFever.pdf')->preservingOriginal()->toMediaCollection('manuscript'); // add a manuscript file + $manuscript->addManuscriptFile(getcwd().'/database/factories/files/BieberFever.pdf', true); // add a manuscript file }); } diff --git a/database/migrations/2024_06_03_162357_create_health_tables.php b/database/migrations/2024_06_03_162357_create_health_tables.php index fed7adf6..a064ed8a 100644 --- a/database/migrations/2024_06_03_162357_create_health_tables.php +++ b/database/migrations/2024_06_03_162357_create_health_tables.php @@ -10,9 +10,9 @@ { public function up() { - $connection = (new HealthCheckResultHistoryItem())->getConnectionName(); + $connection = (new HealthCheckResultHistoryItem)->getConnectionName(); $tableName = EloquentHealthResultStore::getHistoryItemInstance()->getTable(); - + Schema::connection($connection)->create($tableName, function (Blueprint $table) { $table->id(); @@ -27,8 +27,8 @@ public function up() $table->timestamps(); }); - - Schema::connection($connection)->table($tableName, function(Blueprint $table) { + + Schema::connection($connection)->table($tableName, function (Blueprint $table) { $table->index('created_at'); $table->index('batch'); }); diff --git a/database/migrations/2024_08_07_191215_create_activity_log_table.php b/database/migrations/2024_08_07_191215_create_activity_log_table.php index 7c05bc89..b788f65e 100644 --- a/database/migrations/2024_08_07_191215_create_activity_log_table.php +++ b/database/migrations/2024_08_07_191215_create_activity_log_table.php @@ -1,8 +1,8 @@ 'Fisheries Science', 'name_fr' => 'Sciences halieutiques'], @@ -33,7 +32,7 @@ public function run(): void ['name_en' => 'Oceans and Climate Change Science', 'name_fr' => 'Science des océans & changements climatiques'], ['name_en' => 'Hydrographic Services, Data and Science', 'name_fr' => 'Services hydrographiques, données et science'], ['name_en' => 'Aquatic invasive species', 'name_fr' => 'Espèces aquatiques envahissantes'], - ['name_en' => 'Species at Risk', 'name_fr' => 'Espèces en péril'] + ['name_en' => 'Species at Risk', 'name_fr' => 'Espèces en péril'], ]; foreach ($functionalAreas as $functionalArea) { diff --git a/database/seeders/LocalTestDataSeeder.php b/database/seeders/LocalTestDataSeeder.php index 6b1e040d..b7b54a9d 100644 --- a/database/seeders/LocalTestDataSeeder.php +++ b/database/seeders/LocalTestDataSeeder.php @@ -16,6 +16,8 @@ class LocalTestDataSeeder extends Seeder */ public function run(): void { + activity()->disableLogging(); + $this->call([ JournalsTableSeeder::class, ExpertisesTableSeeder::class, @@ -128,5 +130,7 @@ public function run(): void 'email' => 'admin@test.local', ]); $adminUser->assignRole('admin'); + + activity()->enableLogging(); } } diff --git a/resources/src/components/SensitivityLabelChip.vue b/resources/src/components/SensitivityLabelChip.vue new file mode 100644 index 00000000..64bdca7d --- /dev/null +++ b/resources/src/components/SensitivityLabelChip.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/resources/src/locales/en.json b/resources/src/locales/en.json index 4bec0b5c..0ff894b8 100644 --- a/resources/src/locales/en.json +++ b/resources/src/locales/en.json @@ -256,7 +256,11 @@ "your-first-name": "Your first name", "your-last-name": "Your last name", "your-password": "Your password", - "functional-area": "Functional Area" + "functional-area": "Functional Area", + "protected-a": "Protected A", + "unclassified": "Unclassified", + "unclassified-tooltip": "This information is unclassified", + "protected-a-tooltip": "This information is Protected A" }, "create-author-dialog": { "title": "Create a new author" @@ -637,7 +641,8 @@ "save-comments": "Save Comments", "submit-decision": "Submit Decision", "your-response": "Your Response : {0}", - "your-review": "Your Review : {0}" + "your-review": "Your Review : {0}", + "comments-required": "A comment is required." }, "review-step-view": { "all-my-reviews": "All My Reviews", diff --git a/resources/src/locales/fr.json b/resources/src/locales/fr.json index 1d475844..5ec1d080 100644 --- a/resources/src/locales/fr.json +++ b/resources/src/locales/fr.json @@ -256,7 +256,11 @@ "your-first-name": "Ton prénom", "your-last-name": "Votre nom de famille", "your-password": "Votre mot de passe", - "functional-area": "Domaine fonctionnel" + "functional-area": "Domaine fonctionnel", + "unclassified": "Non classé", + "protected-a": "Protégé A", + "unclassified-tooltip": "Cette information n'est pas classifiée", + "protected-a-tooltip": "Ces informations sont protégées A" }, "create-author-dialog": { "title": "Créer un nouvel auteur" @@ -637,7 +641,8 @@ "save-comments": "Enregistrer les commentaires", "submit-decision": "Soumettre la décision", "your-response": "Votre réponse : {0}", - "your-review": "Votre révision : {0}" + "your-review": "Votre révision : {0}", + "comments-required": "Un commentaire s'impose." }, "review-step-view": { "all-my-reviews": "Toutes mes revues", diff --git a/resources/src/models/Author/Author.ts b/resources/src/models/Author/Author.ts index 98235b03..6fa5afbf 100644 --- a/resources/src/models/Author/Author.ts +++ b/resources/src/models/Author/Author.ts @@ -1,5 +1,5 @@ import type { OrganizationResource } from '../Organization/Organization' -import type { Resource, ResourceList } from '../Resource' +import type { Resource, ResourceList, SensitivityLabel } from '../Resource' import type { ExpertiseResource, ExpertiseResourceList, @@ -16,6 +16,7 @@ export interface Author { email: string user_id: number | null organization_id: number + sentivity_label: SensitivityLabel // relationships organization?: OrganizationResource expertises?: ExpertiseResource[] @@ -56,8 +57,8 @@ export class AuthorService { */ public static async update(author: Author) { const response = await http.put( - `api/authors/${author.id}`, - author, + `api/authors/${author.id}`, + author, ) return response.data } diff --git a/resources/src/models/Author/components/ManageAuthorProfileCard.vue b/resources/src/models/Author/components/ManageAuthorProfileCard.vue index 85686fc8..c340bb8e 100644 --- a/resources/src/models/Author/components/ManageAuthorProfileCard.vue +++ b/resources/src/models/Author/components/ManageAuthorProfileCard.vue @@ -6,6 +6,7 @@ import { AuthorService } from '../Author' import ContentCard from '@/components/ContentCard.vue' import OrganizationSelect from '@/models/Organization/components/OrganizationSelect.vue' import OrcidInput from '@/components/OrcidInput.vue' +import SensitivityLabelChip from '@/components/SensitivityLabelChip.vue' const props = defineProps<{ authorId: number @@ -61,7 +62,10 @@ async function save() {