From d7c101e0257e73064c16c5d371d19e8641443570 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Tue, 28 Jun 2022 21:44:19 +0300 Subject: [PATCH 1/9] company_id dropped on user invitations --- app/Listeners/Update/V30/Version304.php | 69 +++++++++++++++++++ app/Models/Auth/UserInvitation.php | 2 +- app/Providers/Event.php | 1 + app/Traits/Users.php | 18 ++--- .../2022_06_28_000000_core_v304.php | 30 ++++++++ 5 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 app/Listeners/Update/V30/Version304.php create mode 100644 database/migrations/2022_06_28_000000_core_v304.php diff --git a/app/Listeners/Update/V30/Version304.php b/app/Listeners/Update/V30/Version304.php new file mode 100644 index 000000000..fd836f850 --- /dev/null +++ b/app/Listeners/Update/V30/Version304.php @@ -0,0 +1,69 @@ +skipThisUpdate($event)) { + return; + } + + Log::channel('stderr')->info('Starting the Akaunting 3.0.4 update...'); + + $this->updateDatabase(); + + $this->deleteOldFiles(); + + Log::channel('stderr')->info('Akaunting 3.0.4 update finished.'); + } + + public function updateDatabase() + { + Log::channel('stderr')->info('Updating database...'); + + DB::table('migrations')->insert([ + 'id' => DB::table('migrations')->max('id') + 1, + 'migration' => '2022_06_28_000000_core_v304', + 'batch' => DB::table('migrations')->max('batch') + 1, + ]); + + Artisan::call('migrate', ['--force' => true]); + + Log::channel('stderr')->info('Database updated.'); + } + + public function deleteOldFiles() + { + Log::channel('stderr')->info('Deleting old files...'); + + $files = [ + 'app/Events/Auth/InvitationCreated.php', + 'app/Listeners/Auth/SendUserInvitation.php', + ]; + + foreach ($files as $file) { + File::delete(base_path($file)); + } + + Log::channel('stderr')->info('Old files deleted.'); + } +} diff --git a/app/Models/Auth/UserInvitation.php b/app/Models/Auth/UserInvitation.php index 661088a5f..2b8a85b23 100644 --- a/app/Models/Auth/UserInvitation.php +++ b/app/Models/Auth/UserInvitation.php @@ -20,7 +20,7 @@ class UserInvitation extends Model * * @var string[] */ - protected $fillable = ['user_id', 'company_id', 'token']; + protected $fillable = ['user_id', 'token']; public function user() { diff --git a/app/Providers/Event.php b/app/Providers/Event.php index 0e9351031..ae239981c 100644 --- a/app/Providers/Event.php +++ b/app/Providers/Event.php @@ -17,6 +17,7 @@ class Event extends Provider 'App\Listeners\Module\UpdateExtraModules', 'App\Listeners\Update\V30\Version300', 'App\Listeners\Update\V30\Version303', + 'App\Listeners\Update\V30\Version304', ], 'Illuminate\Auth\Events\Login' => [ 'App\Listeners\Auth\Login', diff --git a/app/Traits/Users.php b/app/Traits/Users.php index 4ef8dc26e..c127d214b 100644 --- a/app/Traits/Users.php +++ b/app/Traits/Users.php @@ -110,31 +110,25 @@ trait Users } /** - * Checks if the given user has a pending invitation for the - * provided Company. + * Checks if the given user has a pending invitation. * * @return bool */ - public function hasPendingInvitation($company_id = null) + public function hasPendingInvitation() { - $company_id = $company_id ?: company_id(); - - $invitation = UserInvitation::where('user_id', $this->id)->where('company_id', $company_id)->first(); + $invitation = UserInvitation::where('user_id', $this->id)->first(); return $invitation ? true : false; } /** - * Returns if the given user has a pending invitation for the - * provided Company. + * Returns if the given user has a pending invitation. * * @return null|UserInvitation */ - public function getPendingInvitation($company_id = null) + public function getPendingInvitation() { - $company_id = $company_id ?: company_id(); - - $invitation = UserInvitation::where('user_id', $this->id)->where('company_id', $company_id)->first(); + $invitation = UserInvitation::where('user_id', $this->id)->first(); return $invitation; } diff --git a/database/migrations/2022_06_28_000000_core_v304.php b/database/migrations/2022_06_28_000000_core_v304.php new file mode 100644 index 000000000..95dccd039 --- /dev/null +++ b/database/migrations/2022_06_28_000000_core_v304.php @@ -0,0 +1,30 @@ +dropColumn('company_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; From db60980dce0e749fbe24e81eb736e5f130906534 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Tue, 28 Jun 2022 21:45:14 +0300 Subject: [PATCH 2/9] user creating flow updated --- app/Events/Auth/InvitationCreated.php | 20 ---------------- app/Jobs/Auth/CreateInvitation.php | 28 +++++++++++------------ app/Jobs/Auth/CreateUser.php | 8 +++---- app/Listeners/Auth/SendUserInvitation.php | 22 ------------------ 4 files changed, 17 insertions(+), 61 deletions(-) delete mode 100644 app/Events/Auth/InvitationCreated.php delete mode 100644 app/Listeners/Auth/SendUserInvitation.php diff --git a/app/Events/Auth/InvitationCreated.php b/app/Events/Auth/InvitationCreated.php deleted file mode 100644 index 07741f8a3..000000000 --- a/app/Events/Auth/InvitationCreated.php +++ /dev/null @@ -1,20 +0,0 @@ -invitation = $invitation; - } -} diff --git a/app/Jobs/Auth/CreateInvitation.php b/app/Jobs/Auth/CreateInvitation.php index 6ca036b88..3d0fb9f2d 100644 --- a/app/Jobs/Auth/CreateInvitation.php +++ b/app/Jobs/Auth/CreateInvitation.php @@ -3,9 +3,11 @@ namespace App\Jobs\Auth; use App\Abstracts\Job; -use App\Events\Auth\InvitationCreated; use App\Models\Auth\UserInvitation; +use App\Notifications\Auth\Invitation as Notification; +use Exception; use Illuminate\Support\Str; +use Symfony\Component\Mailer\Exception\TransportException; class CreateInvitation extends Job { @@ -13,31 +15,29 @@ class CreateInvitation extends Job protected $user; - protected $company; - - public function __construct($user, $company) + public function __construct($user) { $this->user = $user; - $this->company = $company; } public function handle(): UserInvitation { \DB::transaction(function () { - if ($this->user->hasPendingInvitation($this->company->id)) { - $pending_invitation = $this->user->getPendingInvitation($this->company->id); - - $this->dispatch(new DeleteInvitation($pending_invitation)); - } - $this->invitation = UserInvitation::create([ 'user_id' => $this->user->id, - 'company_id' => $this->company->id, 'token' => (string) Str::uuid(), ]); - }); - event(new InvitationCreated($this->invitation)); + $notification = new Notification($this->invitation); + + try { + $this->dispatch(new NotifyUser($this->user, $notification)); + } catch (TransportException $e) { + $message = trans('errors.title.500'); + + throw new Exception($message); + } + }); return $this->invitation; } diff --git a/app/Jobs/Auth/CreateUser.php b/app/Jobs/Auth/CreateUser.php index 2d653080c..1e688d146 100644 --- a/app/Jobs/Auth/CreateUser.php +++ b/app/Jobs/Auth/CreateUser.php @@ -69,12 +69,10 @@ class CreateUser extends Job implements HasOwner, HasSource, ShouldCreate 'user' => $this->model->id, 'company' => $company->id, ]); + } - if (app()->runningInConsole() || request()->isInstall()) { - continue; - } - - $this->dispatch(new CreateInvitation($this->model, $company)); + if (! app()->runningInConsole() && ! request()->isInstall()) { + $this->dispatch(new CreateInvitation($this->model)); } }); diff --git a/app/Listeners/Auth/SendUserInvitation.php b/app/Listeners/Auth/SendUserInvitation.php deleted file mode 100644 index 9d9d9cdf8..000000000 --- a/app/Listeners/Auth/SendUserInvitation.php +++ /dev/null @@ -1,22 +0,0 @@ -invitation; - - $invitation->user->notify(new Notification($invitation)); - } -} From 4291c3c594948da84c4d8027fb237bb797f7cd9b Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Tue, 28 Jun 2022 21:58:44 +0300 Subject: [PATCH 3/9] user listing flow updated --- app/Models/Auth/User.php | 14 ++++++-------- resources/views/auth/users/index.blade.php | 14 +++++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index 2d4d21c8a..e51b4fa1b 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -311,14 +311,12 @@ class User extends Authenticatable implements HasLocalePreference return $actions; } - if (! $this->hasPendingInvitation()) { - $actions[] = [ - 'title' => trans('general.edit'), - 'icon' => 'edit', - 'url' => route('users.edit', $this->id), - 'permission' => 'update-auth-users', - ]; - } + $actions[] = [ + 'title' => trans('general.edit'), + 'icon' => 'edit', + 'url' => route('users.edit', $this->id), + 'permission' => 'update-auth-users', + ]; if ($this->hasPendingInvitation()) { $actions[] = [ diff --git a/resources/views/auth/users/index.blade.php b/resources/views/auth/users/index.blade.php index dc7b0e3c3..48c00e447 100644 --- a/resources/views/auth/users/index.blade.php +++ b/resources/views/auth/users/index.blade.php @@ -48,15 +48,11 @@ @foreach($users as $item) From 2b2bf2c160d40e8f844ebd6e0ad0eba7bd43d796 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Tue, 28 Jun 2022 22:48:54 +0300 Subject: [PATCH 4/9] user editing flow updated --- resources/views/auth/users/edit.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/auth/users/edit.blade.php b/resources/views/auth/users/edit.blade.php index 412fede3c..3908a8787 100644 --- a/resources/views/auth/users/edit.blade.php +++ b/resources/views/auth/users/edit.blade.php @@ -13,7 +13,7 @@
- + @if (user()->id == $user->id) From 127d85d0d99dcb27c5a0b870e6a968c7548fd152 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Wed, 29 Jun 2022 10:38:23 +0300 Subject: [PATCH 5/9] user updating flow updated --- app/Jobs/Auth/UpdateUser.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/app/Jobs/Auth/UpdateUser.php b/app/Jobs/Auth/UpdateUser.php index 7d3940e7a..56cc7c3d5 100644 --- a/app/Jobs/Auth/UpdateUser.php +++ b/app/Jobs/Auth/UpdateUser.php @@ -67,20 +67,6 @@ class UpdateUser extends Job implements ShouldUpdate 'user' => $this->model->id, 'company' => $company->id, ]); - - $this->dispatch(new CreateInvitation($this->model, $company)); - } - } - - if (isset($sync) && !empty($sync['detached'])) { - foreach ($sync['detached'] as $id) { - $company = Company::find($id); - - if ($this->model->hasPendingInvitation($company->id)) { - $pending_invitation = $this->model->getPendingInvitation($company->id); - - $this->dispatch(new DeleteInvitation($pending_invitation)); - } } } }); From c846aa0005e24e23dba0f072824e2ba1d9068454 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Wed, 29 Jun 2022 10:48:57 +0300 Subject: [PATCH 6/9] user destroying flow updated --- app/Jobs/Auth/DeleteUser.php | 2 ++ app/Listeners/Auth/DeleteUserInvitation.php | 28 --------------------- app/Listeners/Update/V30/Version304.php | 1 + app/Models/Auth/User.php | 5 ++++ app/Providers/Event.php | 6 ----- 5 files changed, 8 insertions(+), 34 deletions(-) delete mode 100644 app/Listeners/Auth/DeleteUserInvitation.php diff --git a/app/Jobs/Auth/DeleteUser.php b/app/Jobs/Auth/DeleteUser.php index c71b20302..e2dc1a722 100644 --- a/app/Jobs/Auth/DeleteUser.php +++ b/app/Jobs/Auth/DeleteUser.php @@ -16,6 +16,8 @@ class DeleteUser extends Job implements ShouldDelete event(new UserDeleting($this->model)); \DB::transaction(function () { + $this->deleteRelationships($this->model, ['invitation']); + $this->model->delete(); $this->model->flushCache(); diff --git a/app/Listeners/Auth/DeleteUserInvitation.php b/app/Listeners/Auth/DeleteUserInvitation.php deleted file mode 100644 index b4eb8597a..000000000 --- a/app/Listeners/Auth/DeleteUserInvitation.php +++ /dev/null @@ -1,28 +0,0 @@ -user->id)->get(); - - foreach ($invitations as $invitation) { - $this->dispatch(new DeleteInvitation($invitation)); - } - } -} diff --git a/app/Listeners/Update/V30/Version304.php b/app/Listeners/Update/V30/Version304.php index fd836f850..de4f6e069 100644 --- a/app/Listeners/Update/V30/Version304.php +++ b/app/Listeners/Update/V30/Version304.php @@ -58,6 +58,7 @@ class Version304 extends Listener $files = [ 'app/Events/Auth/InvitationCreated.php', 'app/Listeners/Auth/SendUserInvitation.php', + 'app/Listeners/Auth/DeleteUserInvitation.php', ]; foreach ($files as $file) { diff --git a/app/Models/Auth/User.php b/app/Models/Auth/User.php index e51b4fa1b..bed0d6aa0 100644 --- a/app/Models/Auth/User.php +++ b/app/Models/Auth/User.php @@ -89,6 +89,11 @@ class User extends Authenticatable implements HasLocalePreference return $this->belongsToMany('App\Models\Common\Dashboard', 'App\Models\Auth\UserDashboard'); } + public function invitation() + { + return $this->hasOne('App\Models\Auth\UserInvitation', 'user_id', 'id'); + } + /** * Always capitalize the name when we retrieve it */ diff --git a/app/Providers/Event.php b/app/Providers/Event.php index ae239981c..05ad6759e 100644 --- a/app/Providers/Event.php +++ b/app/Providers/Event.php @@ -32,12 +32,6 @@ class Event extends Provider 'App\Events\Auth\LandingPageShowing' => [ 'App\Listeners\Auth\AddLandingPages', ], - 'App\Events\Auth\InvitationCreated' => [ - 'App\Listeners\Auth\SendUserInvitation', - ], - 'App\Events\Auth\UserDeleted' => [ - 'App\Listeners\Auth\DeleteUserInvitation', - ], 'App\Events\Document\DocumentCreated' => [ 'App\Listeners\Document\CreateDocumentCreatedHistory', 'App\Listeners\Document\IncreaseNextDocumentNumber', From 67db8d113f77195f8de2f4f974a504bb4b5c0368 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Wed, 29 Jun 2022 22:39:33 +0300 Subject: [PATCH 7/9] existence of invitation should be checked --- app/Http/Controllers/Auth/Register.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Http/Controllers/Auth/Register.php b/app/Http/Controllers/Auth/Register.php index 808cbce1f..4b8c711ed 100644 --- a/app/Http/Controllers/Auth/Register.php +++ b/app/Http/Controllers/Auth/Register.php @@ -46,6 +46,10 @@ class Register extends Controller { $invitation = UserInvitation::token($request->get('token'))->first(); + if (!$invitation) { + abort(403); + } + $user = $invitation->user; $this->dispatch(new DeleteInvitation($invitation)); From 62513384e8d8b4501713c799fb2973e84202b0f3 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Wed, 29 Jun 2022 22:39:57 +0300 Subject: [PATCH 8/9] invitations should work on tests --- app/Jobs/Auth/CreateUser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/Auth/CreateUser.php b/app/Jobs/Auth/CreateUser.php index 1e688d146..ce341f03b 100644 --- a/app/Jobs/Auth/CreateUser.php +++ b/app/Jobs/Auth/CreateUser.php @@ -71,7 +71,7 @@ class CreateUser extends Job implements HasOwner, HasSource, ShouldCreate ]); } - if (! app()->runningInConsole() && ! request()->isInstall()) { + if ((! app()->runningInConsole() && ! request()->isInstall()) || app()->runningUnitTests()) { $this->dispatch(new CreateInvitation($this->model)); } }); From cf26bc5998316e7e5a4b708107310d1aaabd1d59 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Wed, 29 Jun 2022 22:40:14 +0300 Subject: [PATCH 9/9] user tests updated --- tests/Feature/Auth/UsersTest.php | 108 +++++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/tests/Feature/Auth/UsersTest.php b/tests/Feature/Auth/UsersTest.php index b03acacb7..0e05e4d30 100644 --- a/tests/Feature/Auth/UsersTest.php +++ b/tests/Feature/Auth/UsersTest.php @@ -4,6 +4,8 @@ namespace Tests\Feature\Auth; use App\Jobs\Auth\CreateUser; use App\Models\Auth\User; +use App\Notifications\Auth\Invitation; +use Illuminate\Support\Facades\Notification; use Tests\Feature\FeatureTestCase; class UsersTest extends FeatureTestCase @@ -16,6 +18,22 @@ class UsersTest extends FeatureTestCase ->assertSeeText(trans_choice('general.users', 2)); } + public function testItShouldSeePendingUserListPage() + { + $request = $this->getRequest(); + + $user = $this->dispatch(new CreateUser($request)); + + $this->loginAs() + ->get(route('users.index')) + ->assertOk() + ->assertSeeTextInOrder([ + $user->name, + trans('documents.statuses.pending') + ]) + ->assertSee(route('users.invite', $user->id)); + } + public function testItShouldSeeUserCreatePage() { $this->loginAs() @@ -26,15 +44,30 @@ class UsersTest extends FeatureTestCase public function testItShouldCreateUser() { + Notification::fake(); + $request = $this->getRequest(); - $this->loginAs() + $response = $this->loginAs() ->post(route('users.store'), $request) - ->assertOk(); + ->assertOk() + ->assertJson([ + 'success' => true, + 'error' => false, + 'message' => '', + 'redirect' => route('users.index'), + ]) + ->json(); + + $user = User::findOrFail($response['data']['id']); $this->assertFlashLevel('success'); - $this->assertDatabaseHas('users', $this->getAssertRequest($request)); + $this->assertModelExists($user); + + $this->assertModelExists($user->invitation); + + Notification::assertSentTo([$user], Invitation::class); } public function testItShouldSeeUserUpdatePage() @@ -60,7 +93,7 @@ class UsersTest extends FeatureTestCase $this->loginAs() ->patch(route('users.update', $user->id), $request) ->assertOk() - ->assertSee($request['email']); + ->assertSee($request['email']); $this->assertFlashLevel('success'); @@ -80,6 +113,8 @@ class UsersTest extends FeatureTestCase $this->assertFlashLevel('success'); $this->assertSoftDeleted('users', $this->getAssertRequest($request)); + + $this->assertSoftDeleted('user_invitations', ['user_id' => $user->id]); } public function testItShouldSeeLoginPage() @@ -127,6 +162,71 @@ class UsersTest extends FeatureTestCase $this->assertGuest(); } + public function testItShouldSeeRegisterPage() + { + $request = $this->getRequest(); + + $user = $this->dispatch(new CreateUser($request)); + + $this->get(route('register', ['token' => $user->invitation->token])) + ->assertOk(); + + $this->assertGuest(); + } + + public function testItShouldNotSeeRegisterPage() + { + $this->withExceptionHandling() + ->get(route('register', ['token' => $this->faker->uuid])) + ->assertForbidden(); + + $this->assertGuest(); + } + + public function testItShouldRegisterUser() + { + $request = $this->getRequest(); + + $user = $this->dispatch(new CreateUser($request)); + + $password = $this->faker->password; + + $data = [ + 'token' => $user->invitation->token, + 'password' => $password, + 'password_confirmation' => $password, + ]; + + $this->post(route('register.store'), $data) + ->assertOk() + ->assertJson([ + 'redirect' => url('/'), + ]); + + $this->assertFlashLevel('success'); + + $this->assertSoftDeleted('user_invitations', ['user_id' => $user->id]); + + $this->isAuthenticated($user->user); + } + + public function testItShouldNotRegisterUser() + { + $password = $this->faker->password; + + $data = [ + 'token' => $this->faker->uuid, + 'password' => $password, + 'password_confirmation' => $password, + ]; + + $this->withExceptionHandling() + ->post(route('register.store'), $data) + ->assertForbidden(); + + $this->assertGuest(); + } + public function getRequest() { return User::factory()->enabled()->raw();