From df65cda20ca8ec750118487348d635269875d892 Mon Sep 17 00:00:00 2001 From: Martin Folkerts Date: Tue, 10 Oct 2023 21:11:29 +0200 Subject: [PATCH] wip --- Laravel_app/.env.example | 2 +- .../app/Actions/Fortify/CreateNewUser.php | 35 + .../Fortify/PasswordValidationRules.php | 18 + .../app/Actions/Fortify/ResetUserPassword.php | 29 + .../Actions/Fortify/UpdateUserPassword.php | 32 + .../Fortify/UpdateUserProfileInformation.php | 56 ++ .../app/Actions/Jetstream/AddTeamMember.php | 81 +++ .../app/Actions/Jetstream/CreateTeam.php | 37 + .../app/Actions/Jetstream/DeleteTeam.php | 17 + .../app/Actions/Jetstream/DeleteUser.php | 19 + .../Actions/Jetstream/InviteTeamMember.php | 88 +++ .../Actions/Jetstream/RemoveTeamMember.php | 51 ++ .../app/Actions/Jetstream/UpdateTeamName.php | 30 + Laravel_app/app/Models/Membership.php | 15 + Laravel_app/app/Models/Team.php | 44 ++ Laravel_app/app/Models/TeamInvitation.php | 28 + Laravel_app/app/Models/User.php | 20 +- Laravel_app/app/Policies/TeamPolicy.php | 76 ++ .../app/Providers/FortifyServiceProvider.php | 46 ++ .../Providers/JetstreamServiceProvider.php | 43 ++ .../app/Providers/RouteServiceProvider.php | 2 +- Laravel_app/app/View/Components/AppLayout.php | 17 + .../app/View/Components/GuestLayout.php | 17 + Laravel_app/composer.json | 4 +- Laravel_app/composer.lock | 649 +++++++++++++++++- Laravel_app/config/app.php | 2 + Laravel_app/config/fortify.php | 160 +++++ Laravel_app/config/jetstream.php | 81 +++ Laravel_app/config/sanctum.php | 22 +- Laravel_app/config/session.php | 2 +- .../database/factories/UserFactory.php | 40 +- .../2014_10_12_000000_create_users_table.php | 2 + ...000_create_password_reset_tokens_table.php | 28 - ..._add_two_factor_columns_to_users_table.php | 46 ++ ...23_10_10_185728_create_sessions_table.php} | 15 +- .../database/seeders/DatabaseSeeder.php | 10 +- Laravel_app/package-lock.json | 348 +++++++++- Laravel_app/package.json | 5 + Laravel_app/postcss.config.js | 6 + Laravel_app/resources/css/app.css | 7 + Laravel_app/resources/markdown/policy.md | 3 + Laravel_app/resources/markdown/terms.md | 3 + .../views/api/api-token-manager.blade.php | 169 +++++ .../resources/views/api/index.blade.php | 13 + .../views/auth/confirm-password.blade.php | 28 + .../views/auth/forgot-password.blade.php | 34 + .../resources/views/auth/login.blade.php | 48 ++ .../resources/views/auth/register.blade.php | 60 ++ .../views/auth/reset-password.blade.php | 36 + .../views/auth/two-factor-challenge.blade.php | 58 ++ .../views/auth/verify-email.blade.php | 45 ++ .../views/components/action-message.blade.php | 10 + .../views/components/action-section.blade.php | 12 + .../components/application-logo.blade.php | 5 + .../components/application-mark.blade.php | 4 + .../authentication-card-logo.blade.php | 6 + .../components/authentication-card.blade.php | 9 + .../views/components/banner.blade.php | 44 ++ .../views/components/button.blade.php | 3 + .../views/components/checkbox.blade.php | 1 + .../components/confirmation-modal.blade.php | 27 + .../components/confirms-password.blade.php | 46 ++ .../views/components/danger-button.blade.php | 3 + .../views/components/dialog-modal.blade.php | 17 + .../views/components/dropdown-link.blade.php | 1 + .../views/components/dropdown.blade.php | 47 ++ .../views/components/form-section.blade.php | 24 + .../views/components/input-error.blade.php | 5 + .../views/components/input.blade.php | 3 + .../views/components/label.blade.php | 5 + .../views/components/modal.blade.php | 43 ++ .../views/components/nav-link.blade.php | 11 + .../components/responsive-nav-link.blade.php | 11 + .../components/secondary-button.blade.php | 3 + .../views/components/section-border.blade.php | 5 + .../views/components/section-title.blade.php | 13 + .../components/switchable-team.blade.php | 21 + .../components/validation-errors.blade.php | 11 + .../views/components/welcome.blade.php | 64 ++ .../resources/views/dashboard.blade.php | 15 + .../views/emails/team-invitation.blade.php | 23 + .../resources/views/layouts/app.blade.php | 45 ++ .../resources/views/layouts/guest.blade.php | 27 + .../resources/views/navigation-menu.blade.php | 219 ++++++ Laravel_app/resources/views/policy.blade.php | 13 + .../views/profile/delete-user-form.blade.php | 53 ++ ...gout-other-browser-sessions-form.blade.php | 98 +++ .../resources/views/profile/show.blade.php | 45 ++ .../two-factor-authentication-form.blade.php | 124 ++++ .../profile/update-password-form.blade.php | 39 ++ .../update-profile-information-form.blade.php | 95 +++ .../views/teams/create-team-form.blade.php | 36 + .../resources/views/teams/create.blade.php | 13 + .../views/teams/delete-team-form.blade.php | 42 ++ .../resources/views/teams/show.blade.php | 23 + .../views/teams/team-member-manager.blade.php | 260 +++++++ .../teams/update-team-name-form.blade.php | 50 ++ Laravel_app/resources/views/terms.blade.php | 13 + Laravel_app/resources/views/welcome.blade.php | 2 +- Laravel_app/routes/web.php | 10 + Laravel_app/tailwind.config.js | 23 + .../tests/Feature/ApiTokenPermissionsTest.php | 47 ++ .../tests/Feature/AuthenticationTest.php | 45 ++ .../tests/Feature/BrowserSessionsTest.php | 24 + .../tests/Feature/CreateApiTokenTest.php | 41 ++ Laravel_app/tests/Feature/CreateTeamTest.php | 26 + .../tests/Feature/DeleteAccountTest.php | 50 ++ .../tests/Feature/DeleteApiTokenTest.php | 39 ++ Laravel_app/tests/Feature/DeleteTeamTest.php | 45 ++ .../tests/Feature/EmailVerificationTest.php | 79 +++ .../tests/Feature/InviteTeamMemberTest.php | 67 ++ Laravel_app/tests/Feature/LeaveTeamTest.php | 41 ++ .../Feature/PasswordConfirmationTest.php | 44 ++ .../tests/Feature/PasswordResetTest.php | 102 +++ .../tests/Feature/ProfileInformationTest.php | 36 + .../tests/Feature/RegistrationTest.php | 60 ++ .../tests/Feature/RemoveTeamMemberTest.php | 45 ++ .../TwoFactorAuthenticationSettingsTest.php | 82 +++ .../tests/Feature/UpdatePasswordTest.php | 62 ++ .../Feature/UpdateTeamMemberRoleTest.php | 53 ++ .../tests/Feature/UpdateTeamNameTest.php | 26 + Laravel_app/vite.config.js | 12 +- 122 files changed, 5387 insertions(+), 88 deletions(-) create mode 100644 Laravel_app/app/Actions/Fortify/CreateNewUser.php create mode 100644 Laravel_app/app/Actions/Fortify/PasswordValidationRules.php create mode 100644 Laravel_app/app/Actions/Fortify/ResetUserPassword.php create mode 100644 Laravel_app/app/Actions/Fortify/UpdateUserPassword.php create mode 100644 Laravel_app/app/Actions/Fortify/UpdateUserProfileInformation.php create mode 100644 Laravel_app/app/Actions/Jetstream/AddTeamMember.php create mode 100644 Laravel_app/app/Actions/Jetstream/CreateTeam.php create mode 100644 Laravel_app/app/Actions/Jetstream/DeleteTeam.php create mode 100644 Laravel_app/app/Actions/Jetstream/DeleteUser.php create mode 100644 Laravel_app/app/Actions/Jetstream/InviteTeamMember.php create mode 100644 Laravel_app/app/Actions/Jetstream/RemoveTeamMember.php create mode 100644 Laravel_app/app/Actions/Jetstream/UpdateTeamName.php create mode 100644 Laravel_app/app/Models/Membership.php create mode 100644 Laravel_app/app/Models/Team.php create mode 100644 Laravel_app/app/Models/TeamInvitation.php create mode 100644 Laravel_app/app/Policies/TeamPolicy.php create mode 100644 Laravel_app/app/Providers/FortifyServiceProvider.php create mode 100644 Laravel_app/app/Providers/JetstreamServiceProvider.php create mode 100644 Laravel_app/app/View/Components/AppLayout.php create mode 100644 Laravel_app/app/View/Components/GuestLayout.php create mode 100644 Laravel_app/config/fortify.php create mode 100644 Laravel_app/config/jetstream.php delete mode 100644 Laravel_app/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php create mode 100644 Laravel_app/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php rename Laravel_app/database/migrations/{2019_08_19_000000_create_failed_jobs_table.php => 2023_10_10_185728_create_sessions_table.php} (51%) create mode 100644 Laravel_app/postcss.config.js create mode 100644 Laravel_app/resources/markdown/policy.md create mode 100644 Laravel_app/resources/markdown/terms.md create mode 100644 Laravel_app/resources/views/api/api-token-manager.blade.php create mode 100644 Laravel_app/resources/views/api/index.blade.php create mode 100644 Laravel_app/resources/views/auth/confirm-password.blade.php create mode 100644 Laravel_app/resources/views/auth/forgot-password.blade.php create mode 100644 Laravel_app/resources/views/auth/login.blade.php create mode 100644 Laravel_app/resources/views/auth/register.blade.php create mode 100644 Laravel_app/resources/views/auth/reset-password.blade.php create mode 100644 Laravel_app/resources/views/auth/two-factor-challenge.blade.php create mode 100644 Laravel_app/resources/views/auth/verify-email.blade.php create mode 100644 Laravel_app/resources/views/components/action-message.blade.php create mode 100644 Laravel_app/resources/views/components/action-section.blade.php create mode 100644 Laravel_app/resources/views/components/application-logo.blade.php create mode 100644 Laravel_app/resources/views/components/application-mark.blade.php create mode 100644 Laravel_app/resources/views/components/authentication-card-logo.blade.php create mode 100644 Laravel_app/resources/views/components/authentication-card.blade.php create mode 100644 Laravel_app/resources/views/components/banner.blade.php create mode 100644 Laravel_app/resources/views/components/button.blade.php create mode 100644 Laravel_app/resources/views/components/checkbox.blade.php create mode 100644 Laravel_app/resources/views/components/confirmation-modal.blade.php create mode 100644 Laravel_app/resources/views/components/confirms-password.blade.php create mode 100644 Laravel_app/resources/views/components/danger-button.blade.php create mode 100644 Laravel_app/resources/views/components/dialog-modal.blade.php create mode 100644 Laravel_app/resources/views/components/dropdown-link.blade.php create mode 100644 Laravel_app/resources/views/components/dropdown.blade.php create mode 100644 Laravel_app/resources/views/components/form-section.blade.php create mode 100644 Laravel_app/resources/views/components/input-error.blade.php create mode 100644 Laravel_app/resources/views/components/input.blade.php create mode 100644 Laravel_app/resources/views/components/label.blade.php create mode 100644 Laravel_app/resources/views/components/modal.blade.php create mode 100644 Laravel_app/resources/views/components/nav-link.blade.php create mode 100644 Laravel_app/resources/views/components/responsive-nav-link.blade.php create mode 100644 Laravel_app/resources/views/components/secondary-button.blade.php create mode 100644 Laravel_app/resources/views/components/section-border.blade.php create mode 100644 Laravel_app/resources/views/components/section-title.blade.php create mode 100644 Laravel_app/resources/views/components/switchable-team.blade.php create mode 100644 Laravel_app/resources/views/components/validation-errors.blade.php create mode 100644 Laravel_app/resources/views/components/welcome.blade.php create mode 100644 Laravel_app/resources/views/dashboard.blade.php create mode 100644 Laravel_app/resources/views/emails/team-invitation.blade.php create mode 100644 Laravel_app/resources/views/layouts/app.blade.php create mode 100644 Laravel_app/resources/views/layouts/guest.blade.php create mode 100644 Laravel_app/resources/views/navigation-menu.blade.php create mode 100644 Laravel_app/resources/views/policy.blade.php create mode 100644 Laravel_app/resources/views/profile/delete-user-form.blade.php create mode 100644 Laravel_app/resources/views/profile/logout-other-browser-sessions-form.blade.php create mode 100644 Laravel_app/resources/views/profile/show.blade.php create mode 100644 Laravel_app/resources/views/profile/two-factor-authentication-form.blade.php create mode 100644 Laravel_app/resources/views/profile/update-password-form.blade.php create mode 100644 Laravel_app/resources/views/profile/update-profile-information-form.blade.php create mode 100644 Laravel_app/resources/views/teams/create-team-form.blade.php create mode 100644 Laravel_app/resources/views/teams/create.blade.php create mode 100644 Laravel_app/resources/views/teams/delete-team-form.blade.php create mode 100644 Laravel_app/resources/views/teams/show.blade.php create mode 100644 Laravel_app/resources/views/teams/team-member-manager.blade.php create mode 100644 Laravel_app/resources/views/teams/update-team-name-form.blade.php create mode 100644 Laravel_app/resources/views/terms.blade.php create mode 100644 Laravel_app/tailwind.config.js create mode 100644 Laravel_app/tests/Feature/ApiTokenPermissionsTest.php create mode 100644 Laravel_app/tests/Feature/AuthenticationTest.php create mode 100644 Laravel_app/tests/Feature/BrowserSessionsTest.php create mode 100644 Laravel_app/tests/Feature/CreateApiTokenTest.php create mode 100644 Laravel_app/tests/Feature/CreateTeamTest.php create mode 100644 Laravel_app/tests/Feature/DeleteAccountTest.php create mode 100644 Laravel_app/tests/Feature/DeleteApiTokenTest.php create mode 100644 Laravel_app/tests/Feature/DeleteTeamTest.php create mode 100644 Laravel_app/tests/Feature/EmailVerificationTest.php create mode 100644 Laravel_app/tests/Feature/InviteTeamMemberTest.php create mode 100644 Laravel_app/tests/Feature/LeaveTeamTest.php create mode 100644 Laravel_app/tests/Feature/PasswordConfirmationTest.php create mode 100644 Laravel_app/tests/Feature/PasswordResetTest.php create mode 100644 Laravel_app/tests/Feature/ProfileInformationTest.php create mode 100644 Laravel_app/tests/Feature/RegistrationTest.php create mode 100644 Laravel_app/tests/Feature/RemoveTeamMemberTest.php create mode 100644 Laravel_app/tests/Feature/TwoFactorAuthenticationSettingsTest.php create mode 100644 Laravel_app/tests/Feature/UpdatePasswordTest.php create mode 100644 Laravel_app/tests/Feature/UpdateTeamMemberRoleTest.php create mode 100644 Laravel_app/tests/Feature/UpdateTeamNameTest.php diff --git a/Laravel_app/.env.example b/Laravel_app/.env.example index ea0665b..2d7f02f 100644 --- a/Laravel_app/.env.example +++ b/Laravel_app/.env.example @@ -19,7 +19,7 @@ BROADCAST_DRIVER=log CACHE_DRIVER=file FILESYSTEM_DISK=local QUEUE_CONNECTION=sync -SESSION_DRIVER=file +SESSION_DRIVER=database SESSION_LIFETIME=120 MEMCACHED_HOST=127.0.0.1 diff --git a/Laravel_app/app/Actions/Fortify/CreateNewUser.php b/Laravel_app/app/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..566e51d --- /dev/null +++ b/Laravel_app/app/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,35 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => $this->passwordRules(), + 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['accepted', 'required'] : '', + ])->validate(); + + return User::create([ + 'name' => $input['name'], + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + ]); + } +} diff --git a/Laravel_app/app/Actions/Fortify/PasswordValidationRules.php b/Laravel_app/app/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 0000000..92fcc75 --- /dev/null +++ b/Laravel_app/app/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,18 @@ + + */ + protected function passwordRules(): array + { + return ['required', 'string', new Password, 'confirmed']; + } +} diff --git a/Laravel_app/app/Actions/Fortify/ResetUserPassword.php b/Laravel_app/app/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..7a57c50 --- /dev/null +++ b/Laravel_app/app/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/Laravel_app/app/Actions/Fortify/UpdateUserPassword.php b/Laravel_app/app/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 0000000..7005639 --- /dev/null +++ b/Laravel_app/app/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,32 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => $this->passwordRules(), + ], [ + 'current_password.current_password' => __('The provided password does not match your current password.'), + ])->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/Laravel_app/app/Actions/Fortify/UpdateUserProfileInformation.php b/Laravel_app/app/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..47e8e62 --- /dev/null +++ b/Laravel_app/app/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,56 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)], + 'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'], + ])->validateWithBag('updateProfileInformation'); + + if (isset($input['photo'])) { + $user->updateProfilePhoto($input['photo']); + } + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/Laravel_app/app/Actions/Jetstream/AddTeamMember.php b/Laravel_app/app/Actions/Jetstream/AddTeamMember.php new file mode 100644 index 0000000..cf3ae4b --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/AddTeamMember.php @@ -0,0 +1,81 @@ +authorize('addTeamMember', $team); + + $this->validate($team, $email, $role); + + $newTeamMember = Jetstream::findUserByEmailOrFail($email); + + AddingTeamMember::dispatch($team, $newTeamMember); + + $team->users()->attach( + $newTeamMember, ['role' => $role] + ); + + TeamMemberAdded::dispatch($team, $newTeamMember); + } + + /** + * Validate the add member operation. + */ + protected function validate(Team $team, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules(), [ + 'email.exists' => __('We were unable to find a registered user with this email address.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($team, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for adding a team member. + * + * @return array + */ + protected function rules(): array + { + return array_filter([ + 'email' => ['required', 'email', 'exists:users'], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + { + return function ($validator) use ($team, $email) { + $validator->errors()->addIf( + $team->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/Laravel_app/app/Actions/Jetstream/CreateTeam.php b/Laravel_app/app/Actions/Jetstream/CreateTeam.php new file mode 100644 index 0000000..7ace5d9 --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/CreateTeam.php @@ -0,0 +1,37 @@ + $input + */ + public function create(User $user, array $input): Team + { + Gate::forUser($user)->authorize('create', Jetstream::newTeamModel()); + + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + ])->validateWithBag('createTeam'); + + AddingTeam::dispatch($user); + + $user->switchTeam($team = $user->ownedTeams()->create([ + 'name' => $input['name'], + 'personal_team' => false, + ])); + + return $team; + } +} diff --git a/Laravel_app/app/Actions/Jetstream/DeleteTeam.php b/Laravel_app/app/Actions/Jetstream/DeleteTeam.php new file mode 100644 index 0000000..680dc36 --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/DeleteTeam.php @@ -0,0 +1,17 @@ +purge(); + } +} diff --git a/Laravel_app/app/Actions/Jetstream/DeleteUser.php b/Laravel_app/app/Actions/Jetstream/DeleteUser.php new file mode 100644 index 0000000..083159e --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/DeleteUser.php @@ -0,0 +1,19 @@ +deleteProfilePhoto(); + $user->tokens->each->delete(); + $user->delete(); + } +} diff --git a/Laravel_app/app/Actions/Jetstream/InviteTeamMember.php b/Laravel_app/app/Actions/Jetstream/InviteTeamMember.php new file mode 100644 index 0000000..8394796 --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/InviteTeamMember.php @@ -0,0 +1,88 @@ +authorize('addTeamMember', $team); + + $this->validate($team, $email, $role); + + InvitingTeamMember::dispatch($team, $email, $role); + + $invitation = $team->teamInvitations()->create([ + 'email' => $email, + 'role' => $role, + ]); + + Mail::to($email)->send(new TeamInvitation($invitation)); + } + + /** + * Validate the invite member operation. + */ + protected function validate(Team $team, string $email, ?string $role): void + { + Validator::make([ + 'email' => $email, + 'role' => $role, + ], $this->rules($team), [ + 'email.unique' => __('This user has already been invited to the team.'), + ])->after( + $this->ensureUserIsNotAlreadyOnTeam($team, $email) + )->validateWithBag('addTeamMember'); + } + + /** + * Get the validation rules for inviting a team member. + * + * @return array + */ + protected function rules(Team $team): array + { + return array_filter([ + 'email' => [ + 'required', 'email', + Rule::unique('team_invitations')->where(function (Builder $query) use ($team) { + $query->where('team_id', $team->id); + }), + ], + 'role' => Jetstream::hasRoles() + ? ['required', 'string', new Role] + : null, + ]); + } + + /** + * Ensure that the user is not already on the team. + */ + protected function ensureUserIsNotAlreadyOnTeam(Team $team, string $email): Closure + { + return function ($validator) use ($team, $email) { + $validator->errors()->addIf( + $team->hasUserWithEmail($email), + 'email', + __('This user already belongs to the team.') + ); + }; + } +} diff --git a/Laravel_app/app/Actions/Jetstream/RemoveTeamMember.php b/Laravel_app/app/Actions/Jetstream/RemoveTeamMember.php new file mode 100644 index 0000000..ddf755e --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/RemoveTeamMember.php @@ -0,0 +1,51 @@ +authorize($user, $team, $teamMember); + + $this->ensureUserDoesNotOwnTeam($teamMember, $team); + + $team->removeUser($teamMember); + + TeamMemberRemoved::dispatch($team, $teamMember); + } + + /** + * Authorize that the user can remove the team member. + */ + protected function authorize(User $user, Team $team, User $teamMember): void + { + if (! Gate::forUser($user)->check('removeTeamMember', $team) && + $user->id !== $teamMember->id) { + throw new AuthorizationException; + } + } + + /** + * Ensure that the currently authenticated user does not own the team. + */ + protected function ensureUserDoesNotOwnTeam(User $teamMember, Team $team): void + { + if ($teamMember->id === $team->owner->id) { + throw ValidationException::withMessages([ + 'team' => [__('You may not leave a team that you created.')], + ])->errorBag('removeTeamMember'); + } + } +} diff --git a/Laravel_app/app/Actions/Jetstream/UpdateTeamName.php b/Laravel_app/app/Actions/Jetstream/UpdateTeamName.php new file mode 100644 index 0000000..b4e28d9 --- /dev/null +++ b/Laravel_app/app/Actions/Jetstream/UpdateTeamName.php @@ -0,0 +1,30 @@ + $input + */ + public function update(User $user, Team $team, array $input): void + { + Gate::forUser($user)->authorize('update', $team); + + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + ])->validateWithBag('updateTeamName'); + + $team->forceFill([ + 'name' => $input['name'], + ])->save(); + } +} diff --git a/Laravel_app/app/Models/Membership.php b/Laravel_app/app/Models/Membership.php new file mode 100644 index 0000000..f4ca843 --- /dev/null +++ b/Laravel_app/app/Models/Membership.php @@ -0,0 +1,15 @@ + + */ + protected $casts = [ + 'personal_team' => 'boolean', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'personal_team', + ]; + + /** + * The event map for the model. + * + * @var array + */ + protected $dispatchesEvents = [ + 'created' => TeamCreated::class, + 'updated' => TeamUpdated::class, + 'deleted' => TeamDeleted::class, + ]; +} diff --git a/Laravel_app/app/Models/TeamInvitation.php b/Laravel_app/app/Models/TeamInvitation.php new file mode 100644 index 0000000..a1d432e --- /dev/null +++ b/Laravel_app/app/Models/TeamInvitation.php @@ -0,0 +1,28 @@ + + */ + protected $fillable = [ + 'email', + 'role', + ]; + + /** + * Get the team that the invitation belongs to. + */ + public function team(): BelongsTo + { + return $this->belongsTo(Jetstream::teamModel()); + } +} diff --git a/Laravel_app/app/Models/User.php b/Laravel_app/app/Models/User.php index 4d7f70f..7f186c3 100644 --- a/Laravel_app/app/Models/User.php +++ b/Laravel_app/app/Models/User.php @@ -6,11 +6,17 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Laravel\Fortify\TwoFactorAuthenticatable; +use Laravel\Jetstream\HasProfilePhoto; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { - use HasApiTokens, HasFactory, Notifiable; + use HasApiTokens; + use HasFactory; + use HasProfilePhoto; + use Notifiable; + use TwoFactorAuthenticatable; /** * The attributes that are mass assignable. @@ -31,6 +37,8 @@ class User extends Authenticatable protected $hidden = [ 'password', 'remember_token', + 'two_factor_recovery_codes', + 'two_factor_secret', ]; /** @@ -40,6 +48,14 @@ class User extends Authenticatable */ protected $casts = [ 'email_verified_at' => 'datetime', - 'password' => 'hashed', + ]; + + /** + * The accessors to append to the model's array form. + * + * @var array + */ + protected $appends = [ + 'profile_photo_url', ]; } diff --git a/Laravel_app/app/Policies/TeamPolicy.php b/Laravel_app/app/Policies/TeamPolicy.php new file mode 100644 index 0000000..0daf27f --- /dev/null +++ b/Laravel_app/app/Policies/TeamPolicy.php @@ -0,0 +1,76 @@ +belongsToTeam($team); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Team $team): bool + { + return $user->ownsTeam($team); + } + + /** + * Determine whether the user can add team members. + */ + public function addTeamMember(User $user, Team $team): bool + { + return $user->ownsTeam($team); + } + + /** + * Determine whether the user can update team member permissions. + */ + public function updateTeamMember(User $user, Team $team): bool + { + return $user->ownsTeam($team); + } + + /** + * Determine whether the user can remove team members. + */ + public function removeTeamMember(User $user, Team $team): bool + { + return $user->ownsTeam($team); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Team $team): bool + { + return $user->ownsTeam($team); + } +} diff --git a/Laravel_app/app/Providers/FortifyServiceProvider.php b/Laravel_app/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..2d741e3 --- /dev/null +++ b/Laravel_app/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,46 @@ +input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + } +} diff --git a/Laravel_app/app/Providers/JetstreamServiceProvider.php b/Laravel_app/app/Providers/JetstreamServiceProvider.php new file mode 100644 index 0000000..9139849 --- /dev/null +++ b/Laravel_app/app/Providers/JetstreamServiceProvider.php @@ -0,0 +1,43 @@ +configurePermissions(); + + Jetstream::deleteUsersUsing(DeleteUser::class); + } + + /** + * Configure the permissions that are available within the application. + */ + protected function configurePermissions(): void + { + Jetstream::defaultApiTokenPermissions(['read']); + + Jetstream::permissions([ + 'create', + 'read', + 'update', + 'delete', + ]); + } +} diff --git a/Laravel_app/app/Providers/RouteServiceProvider.php b/Laravel_app/app/Providers/RouteServiceProvider.php index 1cf5f15..025e874 100644 --- a/Laravel_app/app/Providers/RouteServiceProvider.php +++ b/Laravel_app/app/Providers/RouteServiceProvider.php @@ -17,7 +17,7 @@ class RouteServiceProvider extends ServiceProvider * * @var string */ - public const HOME = '/home'; + public const HOME = '/dashboard'; /** * Define your route model bindings, pattern filters, and other route configuration. diff --git a/Laravel_app/app/View/Components/AppLayout.php b/Laravel_app/app/View/Components/AppLayout.php new file mode 100644 index 0000000..de0d46f --- /dev/null +++ b/Laravel_app/app/View/Components/AppLayout.php @@ -0,0 +1,17 @@ +=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 | ^8 | ^9", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" + }, + "time": "2023-08-25T16:18:39+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -971,17 +1075,216 @@ "time": "2023-08-27T10:19:19+00:00" }, { - "name": "laravel/framework", - "version": "v10.27.0", + "name": "jaybizzle/crawler-detect", + "version": "v1.2.116", "source": { "type": "git", - "url": "https://github.com/laravel/framework.git", - "reference": "616f81bd6dd8aa2e26a9fc21d9c95e98bd30803b" + "url": "https://github.com/JayBizzle/Crawler-Detect.git", + "reference": "97e9fe30219e60092e107651abb379a38b342921" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/616f81bd6dd8aa2e26a9fc21d9c95e98bd30803b", - "reference": "616f81bd6dd8aa2e26a9fc21d9c95e98bd30803b", + "url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/97e9fe30219e60092e107651abb379a38b342921", + "reference": "97e9fe30219e60092e107651abb379a38b342921", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jaybizzle\\CrawlerDetect\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Beech", + "email": "m@rkbee.ch", + "role": "Developer" + } + ], + "description": "CrawlerDetect is a PHP class for detecting bots/crawlers/spiders via the user agent", + "homepage": "https://github.com/JayBizzle/Crawler-Detect/", + "keywords": [ + "crawler", + "crawler detect", + "crawler detector", + "crawlerdetect", + "php crawler detect" + ], + "support": { + "issues": "https://github.com/JayBizzle/Crawler-Detect/issues", + "source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.116" + }, + "time": "2023-07-21T15:49:49+00:00" + }, + { + "name": "jenssegers/agent", + "version": "v2.6.4", + "source": { + "type": "git", + "url": "https://github.com/jenssegers/agent.git", + "reference": "daa11c43729510b3700bc34d414664966b03bffe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jenssegers/agent/zipball/daa11c43729510b3700bc34d414664966b03bffe", + "reference": "daa11c43729510b3700bc34d414664966b03bffe", + "shasum": "" + }, + "require": { + "jaybizzle/crawler-detect": "^1.2", + "mobiledetect/mobiledetectlib": "^2.7.6", + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5.0|^6.0|^7.0" + }, + "suggest": { + "illuminate/support": "Required for laravel service providers" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + }, + "laravel": { + "providers": [ + "Jenssegers\\Agent\\AgentServiceProvider" + ], + "aliases": { + "Agent": "Jenssegers\\Agent\\Facades\\Agent" + } + } + }, + "autoload": { + "psr-4": { + "Jenssegers\\Agent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jens Segers", + "homepage": "https://jenssegers.com" + } + ], + "description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect", + "homepage": "https://github.com/jenssegers/agent", + "keywords": [ + "Agent", + "browser", + "desktop", + "laravel", + "mobile", + "platform", + "user agent", + "useragent" + ], + "support": { + "issues": "https://github.com/jenssegers/agent/issues", + "source": "https://github.com/jenssegers/agent/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://github.com/jenssegers", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/jenssegers/agent", + "type": "tidelift" + } + ], + "time": "2020-06-13T08:05:20+00:00" + }, + { + "name": "laravel/fortify", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "5af43d5cc10b70da20ddebdbe62e0dadd69c18e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/5af43d5cc10b70da20ddebdbe62e0dadd69c18e3", + "reference": "5af43d5cc10b70da20ddebdbe62e0dadd69c18e3", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "ext-json": "*", + "illuminate/support": "^8.82|^9.0|^10.0", + "php": "^7.3|^8.0", + "pragmarx/google2fa": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^6.0|^7.0|^8.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2023-09-12T11:19:24+00:00" + }, + { + "name": "laravel/framework", + "version": "v10.28.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "09137f50f715c1efc649788a26092dcb1ec4ab6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/09137f50f715c1efc649788a26092dcb1ec4ab6e", + "reference": "09137f50f715c1efc649788a26092dcb1ec4ab6e", "shasum": "" }, "require": { @@ -1168,7 +1471,76 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2023-10-09T15:15:28+00:00" + "time": "2023-10-10T13:01:37+00:00" + }, + { + "name": "laravel/jetstream", + "version": "v4.0.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/jetstream.git", + "reference": "722f641393af7a41563a777607369dba648abc0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/jetstream/zipball/722f641393af7a41563a777607369dba648abc0e", + "reference": "722f641393af7a41563a777607369dba648abc0e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^10.17", + "illuminate/support": "^10.17", + "jenssegers/agent": "^2.6", + "laravel/fortify": "^1.15", + "php": "^8.1.0" + }, + "require-dev": { + "inertiajs/inertia-laravel": "^0.6.5", + "laravel/sanctum": "^3.0", + "livewire/livewire": "^3.0", + "mockery/mockery": "^1.0", + "orchestra/testbench": "^8.11", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Jetstream\\JetstreamServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Jetstream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Tailwind scaffolding for the Laravel framework.", + "keywords": [ + "auth", + "laravel", + "tailwind" + ], + "support": { + "issues": "https://github.com/laravel/jetstream/issues", + "source": "https://github.com/laravel/jetstream" + }, + "time": "2023-10-02T00:37:02+00:00" }, { "name": "laravel/prompts", @@ -1816,6 +2188,136 @@ ], "time": "2023-08-05T12:09:49+00:00" }, + { + "name": "livewire/livewire", + "version": "v3.0.7", + "source": { + "type": "git", + "url": "https://github.com/livewire/livewire.git", + "reference": "a2fc40f6121362581e72d5da5d820a6af00f62b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/livewire/livewire/zipball/a2fc40f6121362581e72d5da5d820a6af00f62b0", + "reference": "a2fc40f6121362581e72d5da5d820a6af00f62b0", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0", + "illuminate/support": "^10.0", + "illuminate/validation": "^10.0", + "league/mime-type-detection": "^1.9", + "php": "^8.1", + "symfony/http-kernel": "^6.2" + }, + "require-dev": { + "calebporzio/sushi": "^2.1", + "laravel/framework": "^10.0", + "laravel/prompts": "^0.1.6", + "mockery/mockery": "^1.3.1", + "orchestra/testbench": "^8.0", + "orchestra/testbench-dusk": "^8.0", + "phpunit/phpunit": "^9.0", + "psy/psysh": "@stable" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Livewire\\LivewireServiceProvider" + ], + "aliases": { + "Livewire": "Livewire\\Livewire" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Livewire\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "A front-end framework for Laravel.", + "support": { + "issues": "https://github.com/livewire/livewire/issues", + "source": "https://github.com/livewire/livewire/tree/v3.0.7" + }, + "funding": [ + { + "url": "https://github.com/livewire", + "type": "github" + } + ], + "time": "2023-10-09T13:59:17+00:00" + }, + { + "name": "mobiledetect/mobiledetectlib", + "version": "2.8.41", + "source": { + "type": "git", + "url": "https://github.com/serbanghita/Mobile-Detect.git", + "reference": "fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1", + "reference": "fc9cccd4d3706d5a7537b562b59cc18f9e4c0cb1", + "shasum": "" + }, + "require": { + "php": ">=5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.35||~5.7" + }, + "type": "library", + "autoload": { + "psr-0": { + "Detection": "namespaced/" + }, + "classmap": [ + "Mobile_Detect.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Serban Ghita", + "email": "serbanghita@gmail.com", + "homepage": "http://mobiledetect.net", + "role": "Developer" + } + ], + "description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.", + "homepage": "https://github.com/serbanghita/Mobile-Detect", + "keywords": [ + "detect mobile devices", + "mobile", + "mobile detect", + "mobile detector", + "php mobile detect" + ], + "support": { + "issues": "https://github.com/serbanghita/Mobile-Detect/issues", + "source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.41" + }, + "time": "2022-11-08T18:31:26+00:00" + }, { "name": "monolog/monolog", "version": "3.4.0", @@ -2313,6 +2815,73 @@ ], "time": "2023-02-08T01:06:31+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.1", @@ -2388,6 +2957,58 @@ ], "time": "2023-02-25T19:38:58+00:00" }, + { + "name": "pragmarx/google2fa", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.18", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1" + }, + "time": "2022-06-13T21:57:56+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -5819,16 +6440,16 @@ }, { "name": "laravel/pint", - "version": "v1.13.2", + "version": "v1.13.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "bbb13460d7f8c5c0cd9a58109beedd79cd7331ff" + "reference": "93b2d0d49719bc6e444ba21cd4dbbccec935413d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/bbb13460d7f8c5c0cd9a58109beedd79cd7331ff", - "reference": "bbb13460d7f8c5c0cd9a58109beedd79cd7331ff", + "url": "https://api.github.com/repos/laravel/pint/zipball/93b2d0d49719bc6e444ba21cd4dbbccec935413d", + "reference": "93b2d0d49719bc6e444ba21cd4dbbccec935413d", "shasum": "" }, "require": { @@ -5839,7 +6460,7 @@ "php": "^8.1.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.26.1", + "friendsofphp/php-cs-fixer": "^3.34.1", "illuminate/view": "^10.23.1", "laravel-zero/framework": "^10.1.2", "mockery/mockery": "^1.6.6", @@ -5881,7 +6502,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2023-09-19T15:55:02+00:00" + "time": "2023-10-10T15:39:09+00:00" }, { "name": "laravel/sail", diff --git a/Laravel_app/config/app.php b/Laravel_app/config/app.php index 4c231b4..cde8fb5 100644 --- a/Laravel_app/config/app.php +++ b/Laravel_app/config/app.php @@ -168,6 +168,8 @@ return [ // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\RouteServiceProvider::class, + App\Providers\FortifyServiceProvider::class, + App\Providers\JetstreamServiceProvider::class, ])->toArray(), /* diff --git a/Laravel_app/config/fortify.php b/Laravel_app/config/fortify.php new file mode 100644 index 0000000..0fe22c8 --- /dev/null +++ b/Laravel_app/config/fortify.php @@ -0,0 +1,160 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => RouteServiceProvider::HOME, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => true, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + // Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + ], + +]; diff --git a/Laravel_app/config/jetstream.php b/Laravel_app/config/jetstream.php new file mode 100644 index 0000000..d5e5f11 --- /dev/null +++ b/Laravel_app/config/jetstream.php @@ -0,0 +1,81 @@ + 'livewire', + + /* + |-------------------------------------------------------------------------- + | Jetstream Route Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Jetstream will assign to the routes + | that it registers with the application. When necessary, you may modify + | these middleware; however, this default value is usually sufficient. + | + */ + + 'middleware' => ['web'], + + 'auth_session' => AuthenticateSession::class, + + /* + |-------------------------------------------------------------------------- + | Jetstream Guard + |-------------------------------------------------------------------------- + | + | Here you may specify the authentication guard Jetstream will use while + | authenticating users. This value should correspond with one of your + | guards that is already present in your "auth" configuration file. + | + */ + + 'guard' => 'sanctum', + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of Jetstream's features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + // Features::termsAndPrivacyPolicy(), + // Features::profilePhotos(), + // Features::api(), + // Features::teams(['invitations' => true]), + Features::accountDeletion(), + ], + + /* + |-------------------------------------------------------------------------- + | Profile Photo Disk + |-------------------------------------------------------------------------- + | + | This configuration value determines the default disk that will be used + | when storing profile photos for your application's users. Typically + | this will be the "public" disk but you may adjust this if needed. + | + */ + + 'profile_photo_disk' => 'public', + +]; diff --git a/Laravel_app/config/sanctum.php b/Laravel_app/config/sanctum.php index 529cfdc..e739e96 100644 --- a/Laravel_app/config/sanctum.php +++ b/Laravel_app/config/sanctum.php @@ -41,13 +41,28 @@ return [ |-------------------------------------------------------------------------- | | This value controls the number of minutes until an issued token will be - | considered expired. If this value is null, personal access tokens do - | not expire. This won't tweak the lifetime of first-party sessions. + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. | */ 'expiration' => null, + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of various + | security scanning initiaives maintained by open source platforms + | that alert developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + /* |-------------------------------------------------------------------------- | Sanctum Middleware @@ -60,8 +75,9 @@ return [ */ 'middleware' => [ - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, + 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, + 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, ], ]; diff --git a/Laravel_app/config/session.php b/Laravel_app/config/session.php index 8fed97c..cbcaf0b 100644 --- a/Laravel_app/config/session.php +++ b/Laravel_app/config/session.php @@ -18,7 +18,7 @@ return [ | */ - 'driver' => env('SESSION_DRIVER', 'file'), + 'driver' => env('SESSION_DRIVER', 'database'), /* |-------------------------------------------------------------------------- diff --git a/Laravel_app/database/factories/UserFactory.php b/Laravel_app/database/factories/UserFactory.php index a6ecc0a..46cb47d 100644 --- a/Laravel_app/database/factories/UserFactory.php +++ b/Laravel_app/database/factories/UserFactory.php @@ -2,8 +2,11 @@ namespace Database\Factories; +use App\Models\Team; +use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; +use Laravel\Jetstream\Features; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User> @@ -18,11 +21,15 @@ class UserFactory extends Factory public function definition(): array { return [ - 'name' => fake()->name(), - 'email' => fake()->unique()->safeEmail(), + 'name' => $this->faker->name(), + 'email' => $this->faker->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password + 'two_factor_secret' => null, + 'two_factor_recovery_codes' => null, 'remember_token' => Str::random(10), + 'profile_photo_path' => null, + 'current_team_id' => null, ]; } @@ -31,8 +38,31 @@ class UserFactory extends Factory */ public function unverified(): static { - return $this->state(fn (array $attributes) => [ - 'email_verified_at' => null, - ]); + return $this->state(function (array $attributes) { + return [ + 'email_verified_at' => null, + ]; + }); + } + + /** + * Indicate that the user should have a personal team. + */ + public function withPersonalTeam(callable $callback = null): static + { + if (! Features::hasTeamFeatures()) { + return $this->state([]); + } + + return $this->has( + Team::factory() + ->state(fn (array $attributes, User $user) => [ + 'name' => $user->name.'\'s Team', + 'user_id' => $user->id, + 'personal_team' => true, + ]) + ->when(is_callable($callback), $callback), + 'ownedTeams' + ); } } diff --git a/Laravel_app/database/migrations/2014_10_12_000000_create_users_table.php b/Laravel_app/database/migrations/2014_10_12_000000_create_users_table.php index 444fafb..f56e5c6 100644 --- a/Laravel_app/database/migrations/2014_10_12_000000_create_users_table.php +++ b/Laravel_app/database/migrations/2014_10_12_000000_create_users_table.php @@ -18,6 +18,8 @@ return new class extends Migration $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); + $table->foreignId('current_team_id')->nullable(); + $table->string('profile_photo_path', 2048)->nullable(); $table->timestamps(); }); } diff --git a/Laravel_app/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php b/Laravel_app/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php deleted file mode 100644 index 81a7229..0000000 --- a/Laravel_app/database/migrations/2014_10_12_100000_create_password_reset_tokens_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('password_reset_tokens'); - } -}; diff --git a/Laravel_app/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php b/Laravel_app/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..b490e24 --- /dev/null +++ b/Laravel_app/database/migrations/2014_10_12_200000_add_two_factor_columns_to_users_table.php @@ -0,0 +1,46 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + if (Fortify::confirmsTwoFactorAuthentication()) { + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(array_merge([ + 'two_factor_secret', + 'two_factor_recovery_codes', + ], Fortify::confirmsTwoFactorAuthentication() ? [ + 'two_factor_confirmed_at', + ] : [])); + }); + } +}; diff --git a/Laravel_app/database/migrations/2019_08_19_000000_create_failed_jobs_table.php b/Laravel_app/database/migrations/2023_10_10_185728_create_sessions_table.php similarity index 51% rename from Laravel_app/database/migrations/2019_08_19_000000_create_failed_jobs_table.php rename to Laravel_app/database/migrations/2023_10_10_185728_create_sessions_table.php index 249da81..f60625b 100644 --- a/Laravel_app/database/migrations/2019_08_19_000000_create_failed_jobs_table.php +++ b/Laravel_app/database/migrations/2023_10_10_185728_create_sessions_table.php @@ -11,14 +11,13 @@ return new class extends Migration */ public function up(): void { - Schema::create('failed_jobs', function (Blueprint $table) { - $table->id(); - $table->string('uuid')->unique(); - $table->text('connection'); - $table->text('queue'); + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); $table->longText('payload'); - $table->longText('exception'); - $table->timestamp('failed_at')->useCurrent(); + $table->integer('last_activity')->index(); }); } @@ -27,6 +26,6 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('failed_jobs'); + Schema::dropIfExists('sessions'); } }; diff --git a/Laravel_app/database/seeders/DatabaseSeeder.php b/Laravel_app/database/seeders/DatabaseSeeder.php index a9f4519..460e8c4 100644 --- a/Laravel_app/database/seeders/DatabaseSeeder.php +++ b/Laravel_app/database/seeders/DatabaseSeeder.php @@ -14,9 +14,11 @@ class DatabaseSeeder extends Seeder { // \App\Models\User::factory(10)->create(); - // \App\Models\User::factory()->create([ - // 'name' => 'Test User', - // 'email' => 'test@example.com', - // ]); + \App\Models\User::factory()->create([ + 'name' => 'Martin Folkerts', + 'email' => 'martin@sobit.nl', + 'password' => '$2y$10$hZZaAaiv1KXmCqq5vZ6PEeRWzvwGbaHKcfqeEMlTn.y22EEPVtofi', + + ]); } } diff --git a/Laravel_app/package-lock.json b/Laravel_app/package-lock.json index 13a0438..86f5059 100644 --- a/Laravel_app/package-lock.json +++ b/Laravel_app/package-lock.json @@ -8,8 +8,13 @@ "tailwindcss": "^3.3.3" }, "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/typography": "^0.5.0", + "autoprefixer": "^10.4.7", "axios": "^1.1.2", "laravel-vite-plugin": "^0.8.0", + "postcss": "^8.4.14", + "tailwindcss": "^3.1.0", "vite": "^4.0.0" } }, @@ -17,6 +22,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "engines": { "node": ">=10" }, @@ -380,6 +386,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -393,6 +400,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -401,6 +409,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, "engines": { "node": ">=6.0.0" } @@ -408,12 +417,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -423,6 +434,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -435,6 +447,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "engines": { "node": ">= 8" } @@ -443,6 +456,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -451,15 +465,57 @@ "node": ">= 8" } }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.6.tgz", + "integrity": "sha512-Fw+2BJ0tmAwK/w01tEFL5TiaJBX1NLT1/YbWgvm7ws3Qcn11kiXxzNTEQDMs5V3mQemhB56l3u0i9dwdzSQldA==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -471,7 +527,8 @@ "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true }, "node_modules/asynckit": { "version": "0.4.0", @@ -479,6 +536,43 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/autoprefixer": { + "version": "10.4.16", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.16.tgz", + "integrity": "sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.21.10", + "caniuse-lite": "^1.0.30001538", + "fraction.js": "^4.3.6", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/axios": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", @@ -493,12 +587,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, "engines": { "node": ">=8" } @@ -507,6 +603,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -516,6 +613,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -523,18 +621,72 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "engines": { "node": ">= 6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001547", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001547.tgz", + "integrity": "sha512-W7CrtIModMAxobGhz8iXmDfuJiiKg1WADMO/9x7/CLNin5cpSbuBjooyoIUVB5eyCc36QuTVlkVa1iB2S5+/eA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, "funding": [ { "type": "individual", @@ -561,6 +713,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -584,6 +737,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "engines": { "node": ">= 6" } @@ -591,12 +745,14 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "bin": { "cssesc": "bin/cssesc" }, @@ -616,12 +772,20 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.549", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.549.tgz", + "integrity": "sha512-gpXfJslSi4hYDkA0mTLEpYKRv9siAgSUgZ+UWyk+J5Cttpd1ThCVwdclzIwQSclz3hYn049+M2fgrP1WpvF8xg==", + "dev": true }, "node_modules/esbuild": { "version": "0.18.20", @@ -660,10 +824,20 @@ "@esbuild/win32-x64": "0.18.20" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -679,6 +853,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -690,6 +865,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -698,6 +874,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -739,15 +916,30 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.6.tgz", + "integrity": "sha512-n2aZ9tNfYDwaHhvFTkhFErqOMIb8uyzSQ+vGJBjZyanAKZVbGUQ1sngfk9FdkBw7G26O7AgNjLcecLffD1c7eg==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -761,6 +953,7 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -780,6 +973,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -791,6 +985,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, "engines": { "node": ">= 0.4.0" } @@ -799,6 +994,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -807,12 +1003,14 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -824,6 +1022,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "dev": true, "dependencies": { "has": "^1.0.3" }, @@ -835,6 +1034,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -843,6 +1043,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -854,6 +1055,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -862,6 +1064,7 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "dev": true, "bin": { "jiti": "bin/jiti.js" } @@ -886,6 +1089,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, "engines": { "node": ">=10" } @@ -893,12 +1097,32 @@ "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "engines": { "node": ">= 8" } @@ -907,6 +1131,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -936,10 +1161,20 @@ "node": ">= 0.6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -951,6 +1186,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -961,6 +1197,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, "funding": [ { "type": "github", @@ -974,10 +1211,26 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -986,6 +1239,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -994,6 +1248,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "engines": { "node": ">= 6" } @@ -1002,6 +1257,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "dependencies": { "wrappy": "1" } @@ -1010,6 +1266,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1017,17 +1274,20 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -1039,6 +1299,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1047,6 +1308,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, "engines": { "node": ">= 6" } @@ -1055,6 +1317,7 @@ "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1082,6 +1345,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -1098,6 +1362,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -1116,6 +1381,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.1.tgz", "integrity": "sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==", + "dev": true, "dependencies": { "lilconfig": "^2.0.5", "yaml": "^2.1.1" @@ -1144,6 +1410,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -1162,6 +1429,7 @@ "version": "6.0.13", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -1173,7 +1441,8 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true }, "node_modules/proxy-from-env": { "version": "1.1.0", @@ -1185,6 +1454,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -1204,6 +1474,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "dependencies": { "pify": "^2.3.0" } @@ -1212,6 +1483,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -1223,6 +1495,7 @@ "version": "1.22.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -1239,6 +1512,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -1264,6 +1538,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -1286,6 +1561,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1294,6 +1570,7 @@ "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", "integrity": "sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -1315,6 +1592,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -1326,6 +1604,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", "integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", + "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -1362,6 +1641,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -1370,6 +1650,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -1381,6 +1662,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -1391,12 +1673,44 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true }, "node_modules/vite": { "version": "4.4.11", @@ -1469,12 +1783,14 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, "node_modules/yaml": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "dev": true, "engines": { "node": ">= 14" } diff --git a/Laravel_app/package.json b/Laravel_app/package.json index f9f46d0..69666fd 100644 --- a/Laravel_app/package.json +++ b/Laravel_app/package.json @@ -6,8 +6,13 @@ "build": "vite build" }, "devDependencies": { + "@tailwindcss/forms": "^0.5.2", + "@tailwindcss/typography": "^0.5.0", + "autoprefixer": "^10.4.7", "axios": "^1.1.2", "laravel-vite-plugin": "^0.8.0", + "postcss": "^8.4.14", + "tailwindcss": "^3.1.0", "vite": "^4.0.0" }, "dependencies": { diff --git a/Laravel_app/postcss.config.js b/Laravel_app/postcss.config.js new file mode 100644 index 0000000..49c0612 --- /dev/null +++ b/Laravel_app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/Laravel_app/resources/css/app.css b/Laravel_app/resources/css/app.css index e69de29..0de2120 100644 --- a/Laravel_app/resources/css/app.css +++ b/Laravel_app/resources/css/app.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +[x-cloak] { + display: none; +} diff --git a/Laravel_app/resources/markdown/policy.md b/Laravel_app/resources/markdown/policy.md new file mode 100644 index 0000000..ff7dbea --- /dev/null +++ b/Laravel_app/resources/markdown/policy.md @@ -0,0 +1,3 @@ +# Privacy Policy + +Edit this file to define the privacy policy for your application. diff --git a/Laravel_app/resources/markdown/terms.md b/Laravel_app/resources/markdown/terms.md new file mode 100644 index 0000000..bf2ace7 --- /dev/null +++ b/Laravel_app/resources/markdown/terms.md @@ -0,0 +1,3 @@ +# Terms of Service + +Edit this file to define the terms of service for your application. diff --git a/Laravel_app/resources/views/api/api-token-manager.blade.php b/Laravel_app/resources/views/api/api-token-manager.blade.php new file mode 100644 index 0000000..e166c2a --- /dev/null +++ b/Laravel_app/resources/views/api/api-token-manager.blade.php @@ -0,0 +1,169 @@ +
+ + + + {{ __('Create API Token') }} + + + + {{ __('API tokens allow third-party services to authenticate with our application on your behalf.') }} + + + + +
+ + + +
+ + + @if (Laravel\Jetstream\Jetstream::hasPermissions()) +
+ + +
+ @foreach (Laravel\Jetstream\Jetstream::$permissions as $permission) + + @endforeach +
+
+ @endif +
+ + + + {{ __('Created.') }} + + + + {{ __('Create') }} + + +
+ + @if ($this->user->tokens->isNotEmpty()) + + + +
+ + + {{ __('Manage API Tokens') }} + + + + {{ __('You may delete any of your existing tokens if they are no longer needed.') }} + + + + +
+ @foreach ($this->user->tokens->sortBy('name') as $token) +
+
+ {{ $token->name }} +
+ +
+ @if ($token->last_used_at) +
+ {{ __('Last used') }} {{ $token->last_used_at->diffForHumans() }} +
+ @endif + + @if (Laravel\Jetstream\Jetstream::hasPermissions()) + + @endif + + +
+
+ @endforeach +
+
+
+
+ @endif + + + + + {{ __('API Token') }} + + + +
+ {{ __('Please copy your new API token. For your security, it won\'t be shown again.') }} +
+ + +
+ + + + {{ __('Close') }} + + +
+ + + + + {{ __('API Token Permissions') }} + + + +
+ @foreach (Laravel\Jetstream\Jetstream::$permissions as $permission) + + @endforeach +
+
+ + + + {{ __('Cancel') }} + + + + {{ __('Save') }} + + +
+ + + + + {{ __('Delete API Token') }} + + + + {{ __('Are you sure you would like to delete this API token?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Delete') }} + + + +
diff --git a/Laravel_app/resources/views/api/index.blade.php b/Laravel_app/resources/views/api/index.blade.php new file mode 100644 index 0000000..d15b6a3 --- /dev/null +++ b/Laravel_app/resources/views/api/index.blade.php @@ -0,0 +1,13 @@ + + +

+ {{ __('API Tokens') }} +

+
+ +
+
+ @livewire('api.api-token-manager') +
+
+
diff --git a/Laravel_app/resources/views/auth/confirm-password.blade.php b/Laravel_app/resources/views/auth/confirm-password.blade.php new file mode 100644 index 0000000..ccc4b28 --- /dev/null +++ b/Laravel_app/resources/views/auth/confirm-password.blade.php @@ -0,0 +1,28 @@ + + + + + + +
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} +
+ + + +
+ @csrf + +
+ + +
+ +
+ + {{ __('Confirm') }} + +
+
+
+
diff --git a/Laravel_app/resources/views/auth/forgot-password.blade.php b/Laravel_app/resources/views/auth/forgot-password.blade.php new file mode 100644 index 0000000..0097654 --- /dev/null +++ b/Laravel_app/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,34 @@ + + + + + + +
+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} +
+ + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + + + +
+ @csrf + +
+ + +
+ +
+ + {{ __('Email Password Reset Link') }} + +
+
+
+
diff --git a/Laravel_app/resources/views/auth/login.blade.php b/Laravel_app/resources/views/auth/login.blade.php new file mode 100644 index 0000000..0c5902d --- /dev/null +++ b/Laravel_app/resources/views/auth/login.blade.php @@ -0,0 +1,48 @@ + + + + + + + + + @if (session('status')) +
+ {{ session('status') }} +
+ @endif + +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ @if (Route::has('password.request')) + + {{ __('Forgot your password?') }} + + @endif + + + {{ __('Log in') }} + +
+
+
+
diff --git a/Laravel_app/resources/views/auth/register.blade.php b/Laravel_app/resources/views/auth/register.blade.php new file mode 100644 index 0000000..8a5f240 --- /dev/null +++ b/Laravel_app/resources/views/auth/register.blade.php @@ -0,0 +1,60 @@ + + + + + + + + +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + @if (Laravel\Jetstream\Jetstream::hasTermsAndPrivacyPolicyFeature()) +
+ +
+ + +
+ {!! __('I agree to the :terms_of_service and :privacy_policy', [ + 'terms_of_service' => ''.__('Terms of Service').'', + 'privacy_policy' => ''.__('Privacy Policy').'', + ]) !!} +
+
+
+
+ @endif + +
+ + {{ __('Already registered?') }} + + + + {{ __('Register') }} + +
+
+
+
diff --git a/Laravel_app/resources/views/auth/reset-password.blade.php b/Laravel_app/resources/views/auth/reset-password.blade.php new file mode 100644 index 0000000..5991e3e --- /dev/null +++ b/Laravel_app/resources/views/auth/reset-password.blade.php @@ -0,0 +1,36 @@ + + + + + + + + +
+ @csrf + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + {{ __('Reset Password') }} + +
+
+
+
diff --git a/Laravel_app/resources/views/auth/two-factor-challenge.blade.php b/Laravel_app/resources/views/auth/two-factor-challenge.blade.php new file mode 100644 index 0000000..eec9264 --- /dev/null +++ b/Laravel_app/resources/views/auth/two-factor-challenge.blade.php @@ -0,0 +1,58 @@ + + + + + + +
+
+ {{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }} +
+ +
+ {{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }} +
+ + + +
+ @csrf + +
+ + +
+ +
+ + +
+ +
+ + + + + + {{ __('Log in') }} + +
+
+
+
+
diff --git a/Laravel_app/resources/views/auth/verify-email.blade.php b/Laravel_app/resources/views/auth/verify-email.blade.php new file mode 100644 index 0000000..71a7998 --- /dev/null +++ b/Laravel_app/resources/views/auth/verify-email.blade.php @@ -0,0 +1,45 @@ + + + + + + +
+ {{ __('Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} +
+ + @if (session('status') == 'verification-link-sent') +
+ {{ __('A new verification link has been sent to the email address you provided in your profile settings.') }} +
+ @endif + +
+
+ @csrf + +
+ + {{ __('Resend Verification Email') }} + +
+
+ +
+ + {{ __('Edit Profile') }} + +
+ @csrf + + +
+
+
+
+
diff --git a/Laravel_app/resources/views/components/action-message.blade.php b/Laravel_app/resources/views/components/action-message.blade.php new file mode 100644 index 0000000..a8a95f9 --- /dev/null +++ b/Laravel_app/resources/views/components/action-message.blade.php @@ -0,0 +1,10 @@ +@props(['on']) + +
merge(['class' => 'text-sm text-gray-600']) }}> + {{ $slot->isEmpty() ? 'Saved.' : $slot }} +
diff --git a/Laravel_app/resources/views/components/action-section.blade.php b/Laravel_app/resources/views/components/action-section.blade.php new file mode 100644 index 0000000..93e2434 --- /dev/null +++ b/Laravel_app/resources/views/components/action-section.blade.php @@ -0,0 +1,12 @@ +
merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}> + + {{ $title }} + {{ $description }} + + +
+
+ {{ $content }} +
+
+
diff --git a/Laravel_app/resources/views/components/application-logo.blade.php b/Laravel_app/resources/views/components/application-logo.blade.php new file mode 100644 index 0000000..b9725e6 --- /dev/null +++ b/Laravel_app/resources/views/components/application-logo.blade.php @@ -0,0 +1,5 @@ + + + + + diff --git a/Laravel_app/resources/views/components/application-mark.blade.php b/Laravel_app/resources/views/components/application-mark.blade.php new file mode 100644 index 0000000..182054e --- /dev/null +++ b/Laravel_app/resources/views/components/application-mark.blade.php @@ -0,0 +1,4 @@ + + + + diff --git a/Laravel_app/resources/views/components/authentication-card-logo.blade.php b/Laravel_app/resources/views/components/authentication-card-logo.blade.php new file mode 100644 index 0000000..0b59654 --- /dev/null +++ b/Laravel_app/resources/views/components/authentication-card-logo.blade.php @@ -0,0 +1,6 @@ + + + + + + diff --git a/Laravel_app/resources/views/components/authentication-card.blade.php b/Laravel_app/resources/views/components/authentication-card.blade.php new file mode 100644 index 0000000..71235cf --- /dev/null +++ b/Laravel_app/resources/views/components/authentication-card.blade.php @@ -0,0 +1,9 @@ +
+
+ {{ $logo }} +
+ +
+ {{ $slot }} +
+
diff --git a/Laravel_app/resources/views/components/banner.blade.php b/Laravel_app/resources/views/components/banner.blade.php new file mode 100644 index 0000000..cb779b2 --- /dev/null +++ b/Laravel_app/resources/views/components/banner.blade.php @@ -0,0 +1,44 @@ +@props(['style' => session('flash.bannerStyle', 'success'), 'message' => session('flash.banner')]) + +
+
+
+
+ + + + + + + + + + + + +

+
+ +
+ +
+
+
+
diff --git a/Laravel_app/resources/views/components/button.blade.php b/Laravel_app/resources/views/components/button.blade.php new file mode 100644 index 0000000..d71f0b6 --- /dev/null +++ b/Laravel_app/resources/views/components/button.blade.php @@ -0,0 +1,3 @@ + diff --git a/Laravel_app/resources/views/components/checkbox.blade.php b/Laravel_app/resources/views/components/checkbox.blade.php new file mode 100644 index 0000000..ae71a8a --- /dev/null +++ b/Laravel_app/resources/views/components/checkbox.blade.php @@ -0,0 +1 @@ +merge(['class' => 'rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500']) !!}> diff --git a/Laravel_app/resources/views/components/confirmation-modal.blade.php b/Laravel_app/resources/views/components/confirmation-modal.blade.php new file mode 100644 index 0000000..1bcacc9 --- /dev/null +++ b/Laravel_app/resources/views/components/confirmation-modal.blade.php @@ -0,0 +1,27 @@ +@props(['id' => null, 'maxWidth' => null]) + + +
+
+
+ + + +
+ +
+

+ {{ $title }} +

+ +
+ {{ $content }} +
+
+
+
+ +
+ {{ $footer }} +
+
diff --git a/Laravel_app/resources/views/components/confirms-password.blade.php b/Laravel_app/resources/views/components/confirms-password.blade.php new file mode 100644 index 0000000..5c1c717 --- /dev/null +++ b/Laravel_app/resources/views/components/confirms-password.blade.php @@ -0,0 +1,46 @@ +@props(['title' => __('Confirm Password'), 'content' => __('For your security, please confirm your password to continue.'), 'button' => __('Confirm')]) + +@php + $confirmableId = md5($attributes->wire('then')); +@endphp + +wire('then') }} + x-data + x-ref="span" + x-on:click="$wire.startConfirmingPassword('{{ $confirmableId }}')" + x-on:password-confirmed.window="setTimeout(() => $event.detail.id === '{{ $confirmableId }}' && $refs.span.dispatchEvent(new CustomEvent('then', { bubbles: false })), 250);" +> + {{ $slot }} + + +@once + + + {{ $title }} + + + + {{ $content }} + +
+ + + +
+
+ + + + {{ __('Cancel') }} + + + + {{ $button }} + + +
+@endonce diff --git a/Laravel_app/resources/views/components/danger-button.blade.php b/Laravel_app/resources/views/components/danger-button.blade.php new file mode 100644 index 0000000..55831ff --- /dev/null +++ b/Laravel_app/resources/views/components/danger-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/Laravel_app/resources/views/components/dialog-modal.blade.php b/Laravel_app/resources/views/components/dialog-modal.blade.php new file mode 100644 index 0000000..3e75aa5 --- /dev/null +++ b/Laravel_app/resources/views/components/dialog-modal.blade.php @@ -0,0 +1,17 @@ +@props(['id' => null, 'maxWidth' => null]) + + +
+
+ {{ $title }} +
+ +
+ {{ $content }} +
+
+ +
+ {{ $footer }} +
+
diff --git a/Laravel_app/resources/views/components/dropdown-link.blade.php b/Laravel_app/resources/views/components/dropdown-link.blade.php new file mode 100644 index 0000000..9912ac1 --- /dev/null +++ b/Laravel_app/resources/views/components/dropdown-link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'block w-full px-4 py-2 text-left text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }} diff --git a/Laravel_app/resources/views/components/dropdown.blade.php b/Laravel_app/resources/views/components/dropdown.blade.php new file mode 100644 index 0000000..5fd9f91 --- /dev/null +++ b/Laravel_app/resources/views/components/dropdown.blade.php @@ -0,0 +1,47 @@ +@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white', 'dropdownClasses' => '']) + +@php +switch ($align) { + case 'left': + $alignmentClasses = 'origin-top-left left-0'; + break; + case 'top': + $alignmentClasses = 'origin-top'; + break; + case 'none': + case 'false': + $alignmentClasses = ''; + break; + case 'right': + default: + $alignmentClasses = 'origin-top-right right-0'; + break; +} + +switch ($width) { + case '48': + $width = 'w-48'; + break; +} +@endphp + +
+
+ {{ $trigger }} +
+ + +
diff --git a/Laravel_app/resources/views/components/form-section.blade.php b/Laravel_app/resources/views/components/form-section.blade.php new file mode 100644 index 0000000..6329625 --- /dev/null +++ b/Laravel_app/resources/views/components/form-section.blade.php @@ -0,0 +1,24 @@ +@props(['submit']) + +
merge(['class' => 'md:grid md:grid-cols-3 md:gap-6']) }}> + + {{ $title }} + {{ $description }} + + +
+
+
+
+ {{ $form }} +
+
+ + @if (isset($actions)) +
+ {{ $actions }} +
+ @endif +
+
+
diff --git a/Laravel_app/resources/views/components/input-error.blade.php b/Laravel_app/resources/views/components/input-error.blade.php new file mode 100644 index 0000000..b5ad968 --- /dev/null +++ b/Laravel_app/resources/views/components/input-error.blade.php @@ -0,0 +1,5 @@ +@props(['for']) + +@error($for) +

merge(['class' => 'text-sm text-red-600']) }}>{{ $message }}

+@enderror diff --git a/Laravel_app/resources/views/components/input.blade.php b/Laravel_app/resources/views/components/input.blade.php new file mode 100644 index 0000000..1df7f0d --- /dev/null +++ b/Laravel_app/resources/views/components/input.blade.php @@ -0,0 +1,3 @@ +@props(['disabled' => false]) + +merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}> diff --git a/Laravel_app/resources/views/components/label.blade.php b/Laravel_app/resources/views/components/label.blade.php new file mode 100644 index 0000000..1cc65e2 --- /dev/null +++ b/Laravel_app/resources/views/components/label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/Laravel_app/resources/views/components/modal.blade.php b/Laravel_app/resources/views/components/modal.blade.php new file mode 100644 index 0000000..b1c1e68 --- /dev/null +++ b/Laravel_app/resources/views/components/modal.blade.php @@ -0,0 +1,43 @@ +@props(['id', 'maxWidth']) + +@php +$id = $id ?? md5($attributes->wire('model')); + +$maxWidth = [ + 'sm' => 'sm:max-w-sm', + 'md' => 'sm:max-w-md', + 'lg' => 'sm:max-w-lg', + 'xl' => 'sm:max-w-xl', + '2xl' => 'sm:max-w-2xl', +][$maxWidth ?? '2xl']; +@endphp + + diff --git a/Laravel_app/resources/views/components/nav-link.blade.php b/Laravel_app/resources/views/components/nav-link.blade.php new file mode 100644 index 0000000..5c101a2 --- /dev/null +++ b/Laravel_app/resources/views/components/nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out' + : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/Laravel_app/resources/views/components/responsive-nav-link.blade.php b/Laravel_app/resources/views/components/responsive-nav-link.blade.php new file mode 100644 index 0000000..8fc2b65 --- /dev/null +++ b/Laravel_app/resources/views/components/responsive-nav-link.blade.php @@ -0,0 +1,11 @@ +@props(['active']) + +@php +$classes = ($active ?? false) + ? 'block w-full pl-3 pr-4 py-2 border-l-4 border-indigo-400 text-left text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out' + : 'block w-full pl-3 pr-4 py-2 border-l-4 border-transparent text-left text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out'; +@endphp + +merge(['class' => $classes]) }}> + {{ $slot }} + diff --git a/Laravel_app/resources/views/components/secondary-button.blade.php b/Laravel_app/resources/views/components/secondary-button.blade.php new file mode 100644 index 0000000..b32b69f --- /dev/null +++ b/Laravel_app/resources/views/components/secondary-button.blade.php @@ -0,0 +1,3 @@ + diff --git a/Laravel_app/resources/views/components/section-border.blade.php b/Laravel_app/resources/views/components/section-border.blade.php new file mode 100644 index 0000000..414ade6 --- /dev/null +++ b/Laravel_app/resources/views/components/section-border.blade.php @@ -0,0 +1,5 @@ + diff --git a/Laravel_app/resources/views/components/section-title.blade.php b/Laravel_app/resources/views/components/section-title.blade.php new file mode 100644 index 0000000..72e5193 --- /dev/null +++ b/Laravel_app/resources/views/components/section-title.blade.php @@ -0,0 +1,13 @@ +
+
+

{{ $title }}

+ +

+ {{ $description }} +

+
+ +
+ {{ $aside ?? '' }} +
+
diff --git a/Laravel_app/resources/views/components/switchable-team.blade.php b/Laravel_app/resources/views/components/switchable-team.blade.php new file mode 100644 index 0000000..6392a99 --- /dev/null +++ b/Laravel_app/resources/views/components/switchable-team.blade.php @@ -0,0 +1,21 @@ +@props(['team', 'component' => 'dropdown-link']) + +
+ @method('PUT') + @csrf + + + + + +
+ @if (Auth::user()->isCurrentTeam($team)) + + + + @endif + +
{{ $team->name }}
+
+
+
diff --git a/Laravel_app/resources/views/components/validation-errors.blade.php b/Laravel_app/resources/views/components/validation-errors.blade.php new file mode 100644 index 0000000..ef753f5 --- /dev/null +++ b/Laravel_app/resources/views/components/validation-errors.blade.php @@ -0,0 +1,11 @@ +@if ($errors->any()) +
+
{{ __('Whoops! Something went wrong.') }}
+ +
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+@endif diff --git a/Laravel_app/resources/views/components/welcome.blade.php b/Laravel_app/resources/views/components/welcome.blade.php new file mode 100644 index 0000000..9955ffb --- /dev/null +++ b/Laravel_app/resources/views/components/welcome.blade.php @@ -0,0 +1,64 @@ +
+ + +

+ Introducing Smart Cane: A Pioneer Project by Smart Farming Tech +

+ +

+ Smart Farming Tech unveils its cutting-edge initiative – "Smart Cane," a pioneering project that integrates the profound potential of satellite imaging with advanced agronomic advice, meticulously designed for both Estate holders and outgrowers. The project embarks on a journey where remote sensing technology becomes the eyes through which farmers and agricultural stakeholders visualize the vitality and conditions of their crops. Smart Cane not merely champions technological innovation in agriculture but also aligns itself as a solution-oriented model, offering real-time and accurate agronomic advice derived from detailed analysis of satellite images. +

+
+ +
+
+
+ + + +

+ Leveraging Remote Sensing Technology for Agronomic Excellence +

+
+ +

+ The project revolves around harnessing the intricate data obtained from satellite images and translating it into actionable, agriculturally-relevant advice, which can fundamentally revolutionize traditional farming methods. Remote sensing technology in the Smart Cane project provides a continuous, detailed, and comprehensive overview of the agricultural fields, recognizing the subtle nuances that might be imperceptible to the naked eye. This technological approach ensures that the provided agronomic advice is deeply rooted in accurate data, ensuring that solutions are not just theoretically sound but also practically applicable, ultimately steering towards enhancing crop health, productivity, and yield. +

+ +

+ + Read More + + + + +

+
+ +
+
+ + + +

+ Bridging Technological Advancements with Sustainable Agriculture +

+
+ +

+ Smart Cane extends beyond being a technological marvel by integrating sustainability and pragmatic farming solutions into its core objectives. The project comprehends the distinct needs of Estate holders and outgrowers, providing them with tailored advice that not only enhances productivity but also fosters an environmentally sustainable approach to agriculture. By offering insightful agronomic advice based on precise satellite data, Smart Cane empowers farmers with the knowledge to make informed decisions, reducing resource wastage, optimizing crop management, and substantiating a methodology that resonates with both technological advancement and sustainable farming practices. +

+ +

+ + Read more + + + + + +

+
+ + +
diff --git a/Laravel_app/resources/views/dashboard.blade.php b/Laravel_app/resources/views/dashboard.blade.php new file mode 100644 index 0000000..4db8ebf --- /dev/null +++ b/Laravel_app/resources/views/dashboard.blade.php @@ -0,0 +1,15 @@ + + +

+ {{ __('Dashboard') }} +

+
+ +
+
+
+ +
+
+
+
diff --git a/Laravel_app/resources/views/emails/team-invitation.blade.php b/Laravel_app/resources/views/emails/team-invitation.blade.php new file mode 100644 index 0000000..1701212 --- /dev/null +++ b/Laravel_app/resources/views/emails/team-invitation.blade.php @@ -0,0 +1,23 @@ +@component('mail::message') +{{ __('You have been invited to join the :team team!', ['team' => $invitation->team->name]) }} + +@if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::registration())) +{{ __('If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:') }} + +@component('mail::button', ['url' => route('register')]) +{{ __('Create Account') }} +@endcomponent + +{{ __('If you already have an account, you may accept this invitation by clicking the button below:') }} + +@else +{{ __('You may accept this invitation by clicking the button below:') }} +@endif + + +@component('mail::button', ['url' => $acceptUrl]) +{{ __('Accept Invitation') }} +@endcomponent + +{{ __('If you did not expect to receive an invitation to this team, you may discard this email.') }} +@endcomponent diff --git a/Laravel_app/resources/views/layouts/app.blade.php b/Laravel_app/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..e85a103 --- /dev/null +++ b/Laravel_app/resources/views/layouts/app.blade.php @@ -0,0 +1,45 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + @livewireStyles + + + + +
+ @livewire('navigation-menu') + + + @if (isset($header)) +
+
+ {{ $header }} +
+
+ @endif + + +
+ {{ $slot }} +
+
+ + @stack('modals') + + @livewireScripts + + diff --git a/Laravel_app/resources/views/layouts/guest.blade.php b/Laravel_app/resources/views/layouts/guest.blade.php new file mode 100644 index 0000000..ec469d4 --- /dev/null +++ b/Laravel_app/resources/views/layouts/guest.blade.php @@ -0,0 +1,27 @@ + + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + @livewireStyles + + +
+ {{ $slot }} +
+ + @livewireScripts + + diff --git a/Laravel_app/resources/views/navigation-menu.blade.php b/Laravel_app/resources/views/navigation-menu.blade.php new file mode 100644 index 0000000..b97314b --- /dev/null +++ b/Laravel_app/resources/views/navigation-menu.blade.php @@ -0,0 +1,219 @@ + diff --git a/Laravel_app/resources/views/policy.blade.php b/Laravel_app/resources/views/policy.blade.php new file mode 100644 index 0000000..4bd3326 --- /dev/null +++ b/Laravel_app/resources/views/policy.blade.php @@ -0,0 +1,13 @@ + +
+
+
+ +
+ +
+ {!! $policy !!} +
+
+
+
diff --git a/Laravel_app/resources/views/profile/delete-user-form.blade.php b/Laravel_app/resources/views/profile/delete-user-form.blade.php new file mode 100644 index 0000000..809e3ed --- /dev/null +++ b/Laravel_app/resources/views/profile/delete-user-form.blade.php @@ -0,0 +1,53 @@ + + + {{ __('Delete Account') }} + + + + {{ __('Permanently delete your account.') }} + + + +
+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }} +
+ +
+ + {{ __('Delete Account') }} + +
+ + + + + {{ __('Delete Account') }} + + + + {{ __('Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }} + +
+ + + +
+
+ + + + {{ __('Cancel') }} + + + + {{ __('Delete Account') }} + + +
+
+
diff --git a/Laravel_app/resources/views/profile/logout-other-browser-sessions-form.blade.php b/Laravel_app/resources/views/profile/logout-other-browser-sessions-form.blade.php new file mode 100644 index 0000000..bbba051 --- /dev/null +++ b/Laravel_app/resources/views/profile/logout-other-browser-sessions-form.blade.php @@ -0,0 +1,98 @@ + + + {{ __('Browser Sessions') }} + + + + {{ __('Manage and log out your active sessions on other browsers and devices.') }} + + + +
+ {{ __('If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.') }} +
+ + @if (count($this->sessions) > 0) +
+ + @foreach ($this->sessions as $session) +
+
+ @if ($session->agent->isDesktop()) + + + + @else + + + + @endif +
+ +
+
+ {{ $session->agent->platform() ? $session->agent->platform() : __('Unknown') }} - {{ $session->agent->browser() ? $session->agent->browser() : __('Unknown') }} +
+ +
+
+ {{ $session->ip_address }}, + + @if ($session->is_current_device) + {{ __('This device') }} + @else + {{ __('Last active') }} {{ $session->last_active }} + @endif +
+
+
+
+ @endforeach +
+ @endif + +
+ + {{ __('Log Out Other Browser Sessions') }} + + + + {{ __('Done.') }} + +
+ + + + + {{ __('Log Out Other Browser Sessions') }} + + + + {{ __('Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.') }} + +
+ + + +
+
+ + + + {{ __('Cancel') }} + + + + {{ __('Log Out Other Browser Sessions') }} + + +
+
+
diff --git a/Laravel_app/resources/views/profile/show.blade.php b/Laravel_app/resources/views/profile/show.blade.php new file mode 100644 index 0000000..bd22d98 --- /dev/null +++ b/Laravel_app/resources/views/profile/show.blade.php @@ -0,0 +1,45 @@ + + +

+ {{ __('Profile') }} +

+
+ +
+
+ @if (Laravel\Fortify\Features::canUpdateProfileInformation()) + @livewire('profile.update-profile-information-form') + + + @endif + + @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords())) +
+ @livewire('profile.update-password-form') +
+ + + @endif + + @if (Laravel\Fortify\Features::canManageTwoFactorAuthentication()) +
+ @livewire('profile.two-factor-authentication-form') +
+ + + @endif + +
+ @livewire('profile.logout-other-browser-sessions-form') +
+ + @if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures()) + + +
+ @livewire('profile.delete-user-form') +
+ @endif +
+
+
diff --git a/Laravel_app/resources/views/profile/two-factor-authentication-form.blade.php b/Laravel_app/resources/views/profile/two-factor-authentication-form.blade.php new file mode 100644 index 0000000..4295ee2 --- /dev/null +++ b/Laravel_app/resources/views/profile/two-factor-authentication-form.blade.php @@ -0,0 +1,124 @@ + + + {{ __('Two Factor Authentication') }} + + + + {{ __('Add additional security to your account using two factor authentication.') }} + + + +

+ @if ($this->enabled) + @if ($showingConfirmation) + {{ __('Finish enabling two factor authentication.') }} + @else + {{ __('You have enabled two factor authentication.') }} + @endif + @else + {{ __('You have not enabled two factor authentication.') }} + @endif +

+ +
+

+ {{ __('When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone\'s Google Authenticator application.') }} +

+
+ + @if ($this->enabled) + @if ($showingQrCode) +
+

+ @if ($showingConfirmation) + {{ __('To finish enabling two factor authentication, scan the following QR code using your phone\'s authenticator application or enter the setup key and provide the generated OTP code.') }} + @else + {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application or enter the setup key.') }} + @endif +

+
+ +
+ {!! $this->user->twoFactorQrCodeSvg() !!} +
+ +
+

+ {{ __('Setup Key') }}: {{ decrypt($this->user->two_factor_secret) }} +

+
+ + @if ($showingConfirmation) +
+ + + + + +
+ @endif + @endif + + @if ($showingRecoveryCodes) +
+

+ {{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }} +

+
+ +
+ @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code) +
{{ $code }}
+ @endforeach +
+ @endif + @endif + +
+ @if (! $this->enabled) + + + {{ __('Enable') }} + + + @else + @if ($showingRecoveryCodes) + + + {{ __('Regenerate Recovery Codes') }} + + + @elseif ($showingConfirmation) + + + {{ __('Confirm') }} + + + @else + + + {{ __('Show Recovery Codes') }} + + + @endif + + @if ($showingConfirmation) + + + {{ __('Cancel') }} + + + @else + + + {{ __('Disable') }} + + + @endif + + @endif +
+
+
diff --git a/Laravel_app/resources/views/profile/update-password-form.blade.php b/Laravel_app/resources/views/profile/update-password-form.blade.php new file mode 100644 index 0000000..cd055b2 --- /dev/null +++ b/Laravel_app/resources/views/profile/update-password-form.blade.php @@ -0,0 +1,39 @@ + + + {{ __('Update Password') }} + + + + {{ __('Ensure your account is using a long, random password to stay secure.') }} + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + + {{ __('Saved.') }} + + + + {{ __('Save') }} + + +
diff --git a/Laravel_app/resources/views/profile/update-profile-information-form.blade.php b/Laravel_app/resources/views/profile/update-profile-information-form.blade.php new file mode 100644 index 0000000..8c86f97 --- /dev/null +++ b/Laravel_app/resources/views/profile/update-profile-information-form.blade.php @@ -0,0 +1,95 @@ + + + {{ __('Profile Information') }} + + + + {{ __('Update your account\'s profile information and email address.') }} + + + + + @if (Laravel\Jetstream\Jetstream::managesProfilePhotos()) +
+ + + + + + +
+ {{ $this->user->name }} +
+ + + + + + {{ __('Select A New Photo') }} + + + @if ($this->user->profile_photo_path) + + {{ __('Remove Photo') }} + + @endif + + +
+ @endif + + +
+ + + +
+ + +
+ + + + + @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::emailVerification()) && ! $this->user->hasVerifiedEmail()) +

+ {{ __('Your email address is unverified.') }} + + +

+ + @if ($this->verificationLinkSent) +

+ {{ __('A new verification link has been sent to your email address.') }} +

+ @endif + @endif +
+
+ + + + {{ __('Saved.') }} + + + + {{ __('Save') }} + + +
diff --git a/Laravel_app/resources/views/teams/create-team-form.blade.php b/Laravel_app/resources/views/teams/create-team-form.blade.php new file mode 100644 index 0000000..760bd08 --- /dev/null +++ b/Laravel_app/resources/views/teams/create-team-form.blade.php @@ -0,0 +1,36 @@ + + + {{ __('Team Details') }} + + + + {{ __('Create a new team to collaborate with others on projects.') }} + + + +
+ + +
+ {{ $this->user->name }} + +
+
{{ $this->user->name }}
+
{{ $this->user->email }}
+
+
+
+ +
+ + + +
+
+ + + + {{ __('Create') }} + + +
diff --git a/Laravel_app/resources/views/teams/create.blade.php b/Laravel_app/resources/views/teams/create.blade.php new file mode 100644 index 0000000..12d006a --- /dev/null +++ b/Laravel_app/resources/views/teams/create.blade.php @@ -0,0 +1,13 @@ + + +

+ {{ __('Create Team') }} +

+
+ +
+
+ @livewire('teams.create-team-form') +
+
+
diff --git a/Laravel_app/resources/views/teams/delete-team-form.blade.php b/Laravel_app/resources/views/teams/delete-team-form.blade.php new file mode 100644 index 0000000..3bbe4e9 --- /dev/null +++ b/Laravel_app/resources/views/teams/delete-team-form.blade.php @@ -0,0 +1,42 @@ + + + {{ __('Delete Team') }} + + + + {{ __('Permanently delete this team.') }} + + + +
+ {{ __('Once a team is deleted, all of its resources and data will be permanently deleted. Before deleting this team, please download any data or information regarding this team that you wish to retain.') }} +
+ +
+ + {{ __('Delete Team') }} + +
+ + + + + {{ __('Delete Team') }} + + + + {{ __('Are you sure you want to delete this team? Once a team is deleted, all of its resources and data will be permanently deleted.') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Delete Team') }} + + + +
+
diff --git a/Laravel_app/resources/views/teams/show.blade.php b/Laravel_app/resources/views/teams/show.blade.php new file mode 100644 index 0000000..fd2c9c7 --- /dev/null +++ b/Laravel_app/resources/views/teams/show.blade.php @@ -0,0 +1,23 @@ + + +

+ {{ __('Team Settings') }} +

+
+ +
+
+ @livewire('teams.update-team-name-form', ['team' => $team]) + + @livewire('teams.team-member-manager', ['team' => $team]) + + @if (Gate::check('delete', $team) && ! $team->personal_team) + + +
+ @livewire('teams.delete-team-form', ['team' => $team]) +
+ @endif +
+
+
diff --git a/Laravel_app/resources/views/teams/team-member-manager.blade.php b/Laravel_app/resources/views/teams/team-member-manager.blade.php new file mode 100644 index 0000000..33aa5af --- /dev/null +++ b/Laravel_app/resources/views/teams/team-member-manager.blade.php @@ -0,0 +1,260 @@ +
+ @if (Gate::check('addTeamMember', $team)) + + + +
+ + + {{ __('Add Team Member') }} + + + + {{ __('Add a new team member to your team, allowing them to collaborate with you.') }} + + + +
+
+ {{ __('Please provide the email address of the person you would like to add to this team.') }} +
+
+ + +
+ + + +
+ + + @if (count($this->roles) > 0) +
+ + + +
+ @foreach ($this->roles as $index => $role) + + @endforeach +
+
+ @endif +
+ + + + {{ __('Added.') }} + + + + {{ __('Add') }} + + +
+
+ @endif + + @if ($team->teamInvitations->isNotEmpty() && Gate::check('addTeamMember', $team)) + + + +
+ + + {{ __('Pending Team Invitations') }} + + + + {{ __('These people have been invited to your team and have been sent an invitation email. They may join the team by accepting the email invitation.') }} + + + +
+ @foreach ($team->teamInvitations as $invitation) +
+
{{ $invitation->email }}
+ +
+ @if (Gate::check('removeTeamMember', $team)) + + + @endif +
+
+ @endforeach +
+
+
+
+ @endif + + @if ($team->users->isNotEmpty()) + + + +
+ + + {{ __('Team Members') }} + + + + {{ __('All of the people that are part of this team.') }} + + + + +
+ @foreach ($team->users->sortBy('name') as $user) +
+
+ {{ $user->name }} +
{{ $user->name }}
+
+ +
+ + @if (Gate::check('updateTeamMember', $team) && Laravel\Jetstream\Jetstream::hasRoles()) + + @elseif (Laravel\Jetstream\Jetstream::hasRoles()) +
+ {{ Laravel\Jetstream\Jetstream::findRole($user->membership->role)->name }} +
+ @endif + + + @if ($this->user->id === $user->id) + + + + @elseif (Gate::check('removeTeamMember', $team)) + + @endif +
+
+ @endforeach +
+
+
+
+ @endif + + + + + {{ __('Manage Role') }} + + + +
+ @foreach ($this->roles as $index => $role) + + @endforeach +
+
+ + + + {{ __('Cancel') }} + + + + {{ __('Save') }} + + +
+ + + + + {{ __('Leave Team') }} + + + + {{ __('Are you sure you would like to leave this team?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Leave') }} + + + + + + + + {{ __('Remove Team Member') }} + + + + {{ __('Are you sure you would like to remove this person from the team?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Remove') }} + + + +
diff --git a/Laravel_app/resources/views/teams/update-team-name-form.blade.php b/Laravel_app/resources/views/teams/update-team-name-form.blade.php new file mode 100644 index 0000000..e3cafbb --- /dev/null +++ b/Laravel_app/resources/views/teams/update-team-name-form.blade.php @@ -0,0 +1,50 @@ + + + {{ __('Team Name') }} + + + + {{ __('The team\'s name and owner information.') }} + + + + +
+ + +
+ {{ $team->owner->name }} + +
+
{{ $team->owner->name }}
+
{{ $team->owner->email }}
+
+
+
+ + +
+ + + + + +
+
+ + @if (Gate::check('update', $team)) + + + {{ __('Saved.') }} + + + + {{ __('Save') }} + + + @endif +
diff --git a/Laravel_app/resources/views/terms.blade.php b/Laravel_app/resources/views/terms.blade.php new file mode 100644 index 0000000..acfdbbb --- /dev/null +++ b/Laravel_app/resources/views/terms.blade.php @@ -0,0 +1,13 @@ + +
+
+
+ +
+ +
+ {!! $terms !!} +
+
+
+
diff --git a/Laravel_app/resources/views/welcome.blade.php b/Laravel_app/resources/views/welcome.blade.php index 638ec96..d23b99e 100644 --- a/Laravel_app/resources/views/welcome.blade.php +++ b/Laravel_app/resources/views/welcome.blade.php @@ -20,7 +20,7 @@ @if (Route::has('login'))
@auth - Home + Dashboard @else Log in diff --git a/Laravel_app/routes/web.php b/Laravel_app/routes/web.php index d259f33..5b2f286 100644 --- a/Laravel_app/routes/web.php +++ b/Laravel_app/routes/web.php @@ -16,3 +16,13 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { return view('welcome'); }); + +Route::middleware([ + 'auth:sanctum', + config('jetstream.auth_session'), + 'verified', +])->group(function () { + Route::get('/dashboard', function () { + return view('dashboard'); + })->name('dashboard'); +}); diff --git a/Laravel_app/tailwind.config.js b/Laravel_app/tailwind.config.js new file mode 100644 index 0000000..abfacff --- /dev/null +++ b/Laravel_app/tailwind.config.js @@ -0,0 +1,23 @@ +import defaultTheme from 'tailwindcss/defaultTheme'; +import forms from '@tailwindcss/forms'; +import typography from '@tailwindcss/typography'; + +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', + './vendor/laravel/jetstream/**/*.blade.php', + './storage/framework/views/*.php', + './resources/views/**/*.blade.php', + ], + + theme: { + extend: { + fontFamily: { + sans: ['Figtree', ...defaultTheme.fontFamily.sans], + }, + }, + }, + + plugins: [forms, typography], +}; diff --git a/Laravel_app/tests/Feature/ApiTokenPermissionsTest.php b/Laravel_app/tests/Feature/ApiTokenPermissionsTest.php new file mode 100644 index 0000000..b16b54d --- /dev/null +++ b/Laravel_app/tests/Feature/ApiTokenPermissionsTest.php @@ -0,0 +1,47 @@ +markTestSkipped('API support is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + $token = $user->tokens()->create([ + 'name' => 'Test Token', + 'token' => Str::random(40), + 'abilities' => ['create', 'read'], + ]); + + Livewire::test(ApiTokenManager::class) + ->set(['managingPermissionsFor' => $token]) + ->set(['updateApiTokenForm' => [ + 'permissions' => [ + 'delete', + 'missing-permission', + ], + ]]) + ->call('updateApiToken'); + + $this->assertTrue($user->fresh()->tokens->first()->can('delete')); + $this->assertFalse($user->fresh()->tokens->first()->can('read')); + $this->assertFalse($user->fresh()->tokens->first()->can('missing-permission')); + } +} diff --git a/Laravel_app/tests/Feature/AuthenticationTest.php b/Laravel_app/tests/Feature/AuthenticationTest.php new file mode 100644 index 0000000..7623fe9 --- /dev/null +++ b/Laravel_app/tests/Feature/AuthenticationTest.php @@ -0,0 +1,45 @@ +get('/login'); + + $response->assertStatus(200); + } + + public function test_users_can_authenticate_using_the_login_screen(): void + { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } + + public function test_users_can_not_authenticate_with_invalid_password(): void + { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); + } +} diff --git a/Laravel_app/tests/Feature/BrowserSessionsTest.php b/Laravel_app/tests/Feature/BrowserSessionsTest.php new file mode 100644 index 0000000..2fe8a24 --- /dev/null +++ b/Laravel_app/tests/Feature/BrowserSessionsTest.php @@ -0,0 +1,24 @@ +actingAs($user = User::factory()->create()); + + Livewire::test(LogoutOtherBrowserSessionsForm::class) + ->set('password', 'password') + ->call('logoutOtherBrowserSessions') + ->assertSuccessful(); + } +} diff --git a/Laravel_app/tests/Feature/CreateApiTokenTest.php b/Laravel_app/tests/Feature/CreateApiTokenTest.php new file mode 100644 index 0000000..744ed90 --- /dev/null +++ b/Laravel_app/tests/Feature/CreateApiTokenTest.php @@ -0,0 +1,41 @@ +markTestSkipped('API support is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + Livewire::test(ApiTokenManager::class) + ->set(['createApiTokenForm' => [ + 'name' => 'Test Token', + 'permissions' => [ + 'read', + 'update', + ], + ]]) + ->call('createApiToken'); + + $this->assertCount(1, $user->fresh()->tokens); + $this->assertEquals('Test Token', $user->fresh()->tokens->first()->name); + $this->assertTrue($user->fresh()->tokens->first()->can('read')); + $this->assertFalse($user->fresh()->tokens->first()->can('delete')); + } +} diff --git a/Laravel_app/tests/Feature/CreateTeamTest.php b/Laravel_app/tests/Feature/CreateTeamTest.php new file mode 100644 index 0000000..e52e0d0 --- /dev/null +++ b/Laravel_app/tests/Feature/CreateTeamTest.php @@ -0,0 +1,26 @@ +actingAs($user = User::factory()->withPersonalTeam()->create()); + + Livewire::test(CreateTeamForm::class) + ->set(['state' => ['name' => 'Test Team']]) + ->call('createTeam'); + + $this->assertCount(2, $user->fresh()->ownedTeams); + $this->assertEquals('Test Team', $user->fresh()->ownedTeams()->latest('id')->first()->name); + } +} diff --git a/Laravel_app/tests/Feature/DeleteAccountTest.php b/Laravel_app/tests/Feature/DeleteAccountTest.php new file mode 100644 index 0000000..bcaf59f --- /dev/null +++ b/Laravel_app/tests/Feature/DeleteAccountTest.php @@ -0,0 +1,50 @@ +markTestSkipped('Account deletion is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->create()); + + $component = Livewire::test(DeleteUserForm::class) + ->set('password', 'password') + ->call('deleteUser'); + + $this->assertNull($user->fresh()); + } + + public function test_correct_password_must_be_provided_before_account_can_be_deleted(): void + { + if (! Features::hasAccountDeletionFeatures()) { + $this->markTestSkipped('Account deletion is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->create()); + + Livewire::test(DeleteUserForm::class) + ->set('password', 'wrong-password') + ->call('deleteUser') + ->assertHasErrors(['password']); + + $this->assertNotNull($user->fresh()); + } +} diff --git a/Laravel_app/tests/Feature/DeleteApiTokenTest.php b/Laravel_app/tests/Feature/DeleteApiTokenTest.php new file mode 100644 index 0000000..6ed0861 --- /dev/null +++ b/Laravel_app/tests/Feature/DeleteApiTokenTest.php @@ -0,0 +1,39 @@ +markTestSkipped('API support is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + $token = $user->tokens()->create([ + 'name' => 'Test Token', + 'token' => Str::random(40), + 'abilities' => ['create', 'read'], + ]); + + Livewire::test(ApiTokenManager::class) + ->set(['apiTokenIdBeingDeleted' => $token->id]) + ->call('deleteApiToken'); + + $this->assertCount(0, $user->fresh()->tokens); + } +} diff --git a/Laravel_app/tests/Feature/DeleteTeamTest.php b/Laravel_app/tests/Feature/DeleteTeamTest.php new file mode 100644 index 0000000..5dc2888 --- /dev/null +++ b/Laravel_app/tests/Feature/DeleteTeamTest.php @@ -0,0 +1,45 @@ +actingAs($user = User::factory()->withPersonalTeam()->create()); + + $user->ownedTeams()->save($team = Team::factory()->make([ + 'personal_team' => false, + ])); + + $team->users()->attach( + $otherUser = User::factory()->create(), ['role' => 'test-role'] + ); + + $component = Livewire::test(DeleteTeamForm::class, ['team' => $team->fresh()]) + ->call('deleteTeam'); + + $this->assertNull($team->fresh()); + $this->assertCount(0, $otherUser->fresh()->teams); + } + + public function test_personal_teams_cant_be_deleted(): void + { + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + $component = Livewire::test(DeleteTeamForm::class, ['team' => $user->currentTeam]) + ->call('deleteTeam') + ->assertHasErrors(['team']); + + $this->assertNotNull($user->currentTeam->fresh()); + } +} diff --git a/Laravel_app/tests/Feature/EmailVerificationTest.php b/Laravel_app/tests/Feature/EmailVerificationTest.php new file mode 100644 index 0000000..ad0257d --- /dev/null +++ b/Laravel_app/tests/Feature/EmailVerificationTest.php @@ -0,0 +1,79 @@ +markTestSkipped('Email verification not enabled.'); + + return; + } + + $user = User::factory()->withPersonalTeam()->unverified()->create(); + + $response = $this->actingAs($user)->get('/email/verify'); + + $response->assertStatus(200); + } + + public function test_email_can_be_verified(): void + { + if (! Features::enabled(Features::emailVerification())) { + $this->markTestSkipped('Email verification not enabled.'); + + return; + } + + Event::fake(); + + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + + $this->assertTrue($user->fresh()->hasVerifiedEmail()); + $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); + } + + public function test_email_can_not_verified_with_invalid_hash(): void + { + if (! Features::enabled(Features::emailVerification())) { + $this->markTestSkipped('Email verification not enabled.'); + + return; + } + + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + $this->assertFalse($user->fresh()->hasVerifiedEmail()); + } +} diff --git a/Laravel_app/tests/Feature/InviteTeamMemberTest.php b/Laravel_app/tests/Feature/InviteTeamMemberTest.php new file mode 100644 index 0000000..bd04de5 --- /dev/null +++ b/Laravel_app/tests/Feature/InviteTeamMemberTest.php @@ -0,0 +1,67 @@ +markTestSkipped('Team invitations not enabled.'); + + return; + } + + Mail::fake(); + + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->set('addTeamMemberForm', [ + 'email' => 'test@example.com', + 'role' => 'admin', + ])->call('addTeamMember'); + + Mail::assertSent(TeamInvitation::class); + + $this->assertCount(1, $user->currentTeam->fresh()->teamInvitations); + } + + public function test_team_member_invitations_can_be_cancelled(): void + { + if (! Features::sendsTeamInvitations()) { + $this->markTestSkipped('Team invitations not enabled.'); + + return; + } + + Mail::fake(); + + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + // Add the team member... + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->set('addTeamMemberForm', [ + 'email' => 'test@example.com', + 'role' => 'admin', + ])->call('addTeamMember'); + + $invitationId = $user->currentTeam->fresh()->teamInvitations->first()->id; + + // Cancel the team invitation... + $component->call('cancelTeamInvitation', $invitationId); + + $this->assertCount(0, $user->currentTeam->fresh()->teamInvitations); + } +} diff --git a/Laravel_app/tests/Feature/LeaveTeamTest.php b/Laravel_app/tests/Feature/LeaveTeamTest.php new file mode 100644 index 0000000..68e69e6 --- /dev/null +++ b/Laravel_app/tests/Feature/LeaveTeamTest.php @@ -0,0 +1,41 @@ +withPersonalTeam()->create(); + + $user->currentTeam->users()->attach( + $otherUser = User::factory()->create(), ['role' => 'admin'] + ); + + $this->actingAs($otherUser); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->call('leaveTeam'); + + $this->assertCount(0, $user->currentTeam->fresh()->users); + } + + public function test_team_owners_cant_leave_their_own_team(): void + { + $this->actingAs($user = User::factory()->withPersonalTeam()->create()); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->call('leaveTeam') + ->assertHasErrors(['team']); + + $this->assertNotNull($user->currentTeam->fresh()); + } +} diff --git a/Laravel_app/tests/Feature/PasswordConfirmationTest.php b/Laravel_app/tests/Feature/PasswordConfirmationTest.php new file mode 100644 index 0000000..34c860a --- /dev/null +++ b/Laravel_app/tests/Feature/PasswordConfirmationTest.php @@ -0,0 +1,44 @@ +withPersonalTeam()->create(); + + $response = $this->actingAs($user)->get('/user/confirm-password'); + + $response->assertStatus(200); + } + + public function test_password_can_be_confirmed(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/user/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); + } + + public function test_password_is_not_confirmed_with_invalid_password(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/user/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); + } +} diff --git a/Laravel_app/tests/Feature/PasswordResetTest.php b/Laravel_app/tests/Feature/PasswordResetTest.php new file mode 100644 index 0000000..7c65edb --- /dev/null +++ b/Laravel_app/tests/Feature/PasswordResetTest.php @@ -0,0 +1,102 @@ +markTestSkipped('Password updates are not enabled.'); + + return; + } + + $response = $this->get('/forgot-password'); + + $response->assertStatus(200); + } + + public function test_reset_password_link_can_be_requested(): void + { + if (! Features::enabled(Features::resetPasswords())) { + $this->markTestSkipped('Password updates are not enabled.'); + + return; + } + + Notification::fake(); + + $user = User::factory()->create(); + + $response = $this->post('/forgot-password', [ + 'email' => $user->email, + ]); + + Notification::assertSentTo($user, ResetPassword::class); + } + + public function test_reset_password_screen_can_be_rendered(): void + { + if (! Features::enabled(Features::resetPasswords())) { + $this->markTestSkipped('Password updates are not enabled.'); + + return; + } + + Notification::fake(); + + $user = User::factory()->create(); + + $response = $this->post('/forgot-password', [ + 'email' => $user->email, + ]); + + Notification::assertSentTo($user, ResetPassword::class, function (object $notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); + } + + public function test_password_can_be_reset_with_valid_token(): void + { + if (! Features::enabled(Features::resetPasswords())) { + $this->markTestSkipped('Password updates are not enabled.'); + + return; + } + + Notification::fake(); + + $user = User::factory()->create(); + + $response = $this->post('/forgot-password', [ + 'email' => $user->email, + ]); + + Notification::assertSentTo($user, ResetPassword::class, function (object $notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response->assertSessionHasNoErrors(); + + return true; + }); + } +} diff --git a/Laravel_app/tests/Feature/ProfileInformationTest.php b/Laravel_app/tests/Feature/ProfileInformationTest.php new file mode 100644 index 0000000..ce1a8e7 --- /dev/null +++ b/Laravel_app/tests/Feature/ProfileInformationTest.php @@ -0,0 +1,36 @@ +actingAs($user = User::factory()->create()); + + $component = Livewire::test(UpdateProfileInformationForm::class); + + $this->assertEquals($user->name, $component->state['name']); + $this->assertEquals($user->email, $component->state['email']); + } + + public function test_profile_information_can_be_updated(): void + { + $this->actingAs($user = User::factory()->create()); + + Livewire::test(UpdateProfileInformationForm::class) + ->set('state', ['name' => 'Test Name', 'email' => 'test@example.com']) + ->call('updateProfileInformation'); + + $this->assertEquals('Test Name', $user->fresh()->name); + $this->assertEquals('test@example.com', $user->fresh()->email); + } +} diff --git a/Laravel_app/tests/Feature/RegistrationTest.php b/Laravel_app/tests/Feature/RegistrationTest.php new file mode 100644 index 0000000..90b0245 --- /dev/null +++ b/Laravel_app/tests/Feature/RegistrationTest.php @@ -0,0 +1,60 @@ +markTestSkipped('Registration support is not enabled.'); + + return; + } + + $response = $this->get('/register'); + + $response->assertStatus(200); + } + + public function test_registration_screen_cannot_be_rendered_if_support_is_disabled(): void + { + if (Features::enabled(Features::registration())) { + $this->markTestSkipped('Registration support is enabled.'); + + return; + } + + $response = $this->get('/register'); + + $response->assertStatus(404); + } + + public function test_new_users_can_register(): void + { + if (! Features::enabled(Features::registration())) { + $this->markTestSkipped('Registration support is not enabled.'); + + return; + } + + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + 'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature(), + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(RouteServiceProvider::HOME); + } +} diff --git a/Laravel_app/tests/Feature/RemoveTeamMemberTest.php b/Laravel_app/tests/Feature/RemoveTeamMemberTest.php new file mode 100644 index 0000000..cdd500f --- /dev/null +++ b/Laravel_app/tests/Feature/RemoveTeamMemberTest.php @@ -0,0 +1,45 @@ +actingAs($user = User::factory()->withPersonalTeam()->create()); + + $user->currentTeam->users()->attach( + $otherUser = User::factory()->create(), ['role' => 'admin'] + ); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->set('teamMemberIdBeingRemoved', $otherUser->id) + ->call('removeTeamMember'); + + $this->assertCount(0, $user->currentTeam->fresh()->users); + } + + public function test_only_team_owner_can_remove_team_members(): void + { + $user = User::factory()->withPersonalTeam()->create(); + + $user->currentTeam->users()->attach( + $otherUser = User::factory()->create(), ['role' => 'admin'] + ); + + $this->actingAs($otherUser); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->set('teamMemberIdBeingRemoved', $user->id) + ->call('removeTeamMember') + ->assertStatus(403); + } +} diff --git a/Laravel_app/tests/Feature/TwoFactorAuthenticationSettingsTest.php b/Laravel_app/tests/Feature/TwoFactorAuthenticationSettingsTest.php new file mode 100644 index 0000000..686bb02 --- /dev/null +++ b/Laravel_app/tests/Feature/TwoFactorAuthenticationSettingsTest.php @@ -0,0 +1,82 @@ +markTestSkipped('Two factor authentication is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->create()); + + $this->withSession(['auth.password_confirmed_at' => time()]); + + Livewire::test(TwoFactorAuthenticationForm::class) + ->call('enableTwoFactorAuthentication'); + + $user = $user->fresh(); + + $this->assertNotNull($user->two_factor_secret); + $this->assertCount(8, $user->recoveryCodes()); + } + + public function test_recovery_codes_can_be_regenerated(): void + { + if (! Features::canManageTwoFactorAuthentication()) { + $this->markTestSkipped('Two factor authentication is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->create()); + + $this->withSession(['auth.password_confirmed_at' => time()]); + + $component = Livewire::test(TwoFactorAuthenticationForm::class) + ->call('enableTwoFactorAuthentication') + ->call('regenerateRecoveryCodes'); + + $user = $user->fresh(); + + $component->call('regenerateRecoveryCodes'); + + $this->assertCount(8, $user->recoveryCodes()); + $this->assertCount(8, array_diff($user->recoveryCodes(), $user->fresh()->recoveryCodes())); + } + + public function test_two_factor_authentication_can_be_disabled(): void + { + if (! Features::canManageTwoFactorAuthentication()) { + $this->markTestSkipped('Two factor authentication is not enabled.'); + + return; + } + + $this->actingAs($user = User::factory()->create()); + + $this->withSession(['auth.password_confirmed_at' => time()]); + + $component = Livewire::test(TwoFactorAuthenticationForm::class) + ->call('enableTwoFactorAuthentication'); + + $this->assertNotNull($user->fresh()->two_factor_secret); + + $component->call('disableTwoFactorAuthentication'); + + $this->assertNull($user->fresh()->two_factor_secret); + } +} diff --git a/Laravel_app/tests/Feature/UpdatePasswordTest.php b/Laravel_app/tests/Feature/UpdatePasswordTest.php new file mode 100644 index 0000000..57dbe2d --- /dev/null +++ b/Laravel_app/tests/Feature/UpdatePasswordTest.php @@ -0,0 +1,62 @@ +actingAs($user = User::factory()->create()); + + Livewire::test(UpdatePasswordForm::class) + ->set('state', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]) + ->call('updatePassword'); + + $this->assertTrue(Hash::check('new-password', $user->fresh()->password)); + } + + public function test_current_password_must_be_correct(): void + { + $this->actingAs($user = User::factory()->create()); + + Livewire::test(UpdatePasswordForm::class) + ->set('state', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]) + ->call('updatePassword') + ->assertHasErrors(['current_password']); + + $this->assertTrue(Hash::check('password', $user->fresh()->password)); + } + + public function test_new_passwords_must_match(): void + { + $this->actingAs($user = User::factory()->create()); + + Livewire::test(UpdatePasswordForm::class) + ->set('state', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'wrong-password', + ]) + ->call('updatePassword') + ->assertHasErrors(['password']); + + $this->assertTrue(Hash::check('password', $user->fresh()->password)); + } +} diff --git a/Laravel_app/tests/Feature/UpdateTeamMemberRoleTest.php b/Laravel_app/tests/Feature/UpdateTeamMemberRoleTest.php new file mode 100644 index 0000000..47c9593 --- /dev/null +++ b/Laravel_app/tests/Feature/UpdateTeamMemberRoleTest.php @@ -0,0 +1,53 @@ +actingAs($user = User::factory()->withPersonalTeam()->create()); + + $user->currentTeam->users()->attach( + $otherUser = User::factory()->create(), ['role' => 'admin'] + ); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->set('managingRoleFor', $otherUser) + ->set('currentRole', 'editor') + ->call('updateRole'); + + $this->assertTrue($otherUser->fresh()->hasTeamRole( + $user->currentTeam->fresh(), 'editor' + )); + } + + public function test_only_team_owner_can_update_team_member_roles(): void + { + $user = User::factory()->withPersonalTeam()->create(); + + $user->currentTeam->users()->attach( + $otherUser = User::factory()->create(), ['role' => 'admin'] + ); + + $this->actingAs($otherUser); + + $component = Livewire::test(TeamMemberManager::class, ['team' => $user->currentTeam]) + ->set('managingRoleFor', $otherUser) + ->set('currentRole', 'editor') + ->call('updateRole') + ->assertStatus(403); + + $this->assertTrue($otherUser->fresh()->hasTeamRole( + $user->currentTeam->fresh(), 'admin' + )); + } +} diff --git a/Laravel_app/tests/Feature/UpdateTeamNameTest.php b/Laravel_app/tests/Feature/UpdateTeamNameTest.php new file mode 100644 index 0000000..d18f205 --- /dev/null +++ b/Laravel_app/tests/Feature/UpdateTeamNameTest.php @@ -0,0 +1,26 @@ +actingAs($user = User::factory()->withPersonalTeam()->create()); + + Livewire::test(UpdateTeamNameForm::class, ['team' => $user->currentTeam]) + ->set(['state' => ['name' => 'Test Team']]) + ->call('updateTeamName'); + + $this->assertCount(1, $user->fresh()->ownedTeams); + $this->assertEquals('Test Team', $user->currentTeam->fresh()->name); + } +} diff --git a/Laravel_app/vite.config.js b/Laravel_app/vite.config.js index 421b569..f190771 100644 --- a/Laravel_app/vite.config.js +++ b/Laravel_app/vite.config.js @@ -1,11 +1,17 @@ import { defineConfig } from 'vite'; -import laravel from 'laravel-vite-plugin'; +import laravel, { refreshPaths } from 'laravel-vite-plugin'; export default defineConfig({ plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], - refresh: true, + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + ], + refresh: [ + ...refreshPaths, + 'app/Livewire/**', + ], }), ], });