Merge pull request #2498 from sevannerse/user-works

User works
This commit is contained in:
Cüneyt Şentürk 2022-07-03 10:21:18 +03:00 committed by GitHub
commit b9e50a6ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 252 additions and 144 deletions

View File

@ -1,20 +0,0 @@
<?php
namespace App\Events\Auth;
use App\Abstracts\Event;
class InvitationCreated extends Event
{
public $invitation;
/**
* Create a new event instance.
*
* @param $invitation
*/
public function __construct($invitation)
{
$this->invitation = $invitation;
}
}

View File

@ -46,6 +46,10 @@ class Register extends Controller
{ {
$invitation = UserInvitation::token($request->get('token'))->first(); $invitation = UserInvitation::token($request->get('token'))->first();
if (!$invitation) {
abort(403);
}
$user = $invitation->user; $user = $invitation->user;
$this->dispatch(new DeleteInvitation($invitation)); $this->dispatch(new DeleteInvitation($invitation));

View File

@ -3,9 +3,11 @@
namespace App\Jobs\Auth; namespace App\Jobs\Auth;
use App\Abstracts\Job; use App\Abstracts\Job;
use App\Events\Auth\InvitationCreated;
use App\Models\Auth\UserInvitation; use App\Models\Auth\UserInvitation;
use App\Notifications\Auth\Invitation as Notification;
use Exception;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Symfony\Component\Mailer\Exception\TransportException;
class CreateInvitation extends Job class CreateInvitation extends Job
{ {
@ -13,31 +15,29 @@ class CreateInvitation extends Job
protected $user; protected $user;
protected $company; public function __construct($user)
public function __construct($user, $company)
{ {
$this->user = $user; $this->user = $user;
$this->company = $company;
} }
public function handle(): UserInvitation public function handle(): UserInvitation
{ {
\DB::transaction(function () { \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([ $this->invitation = UserInvitation::create([
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'company_id' => $this->company->id,
'token' => (string) Str::uuid(), '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; return $this->invitation;
} }

View File

@ -69,12 +69,10 @@ class CreateUser extends Job implements HasOwner, HasSource, ShouldCreate
'user' => $this->model->id, 'user' => $this->model->id,
'company' => $company->id, 'company' => $company->id,
]); ]);
if (app()->runningInConsole() || request()->isInstall()) {
continue;
} }
$this->dispatch(new CreateInvitation($this->model, $company)); if ((! app()->runningInConsole() && ! request()->isInstall()) || app()->runningUnitTests()) {
$this->dispatch(new CreateInvitation($this->model));
} }
}); });

View File

@ -16,6 +16,8 @@ class DeleteUser extends Job implements ShouldDelete
event(new UserDeleting($this->model)); event(new UserDeleting($this->model));
\DB::transaction(function () { \DB::transaction(function () {
$this->deleteRelationships($this->model, ['invitation']);
$this->model->delete(); $this->model->delete();
$this->model->flushCache(); $this->model->flushCache();

View File

@ -67,20 +67,6 @@ class UpdateUser extends Job implements ShouldUpdate
'user' => $this->model->id, 'user' => $this->model->id,
'company' => $company->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));
}
} }
} }
}); });

View File

@ -1,28 +0,0 @@
<?php
namespace App\Listeners\Auth;
use App\Events\Auth\UserDeleted as Event;
use App\Jobs\Auth\DeleteInvitation;
use App\Models\Auth\UserInvitation;
use App\Traits\Jobs;
class DeleteUserInvitation
{
use Jobs;
/**
* Handle the event.
*
* @param $event
* @return void
*/
public function handle(Event $event)
{
$invitations = UserInvitation::where('user_id', $event->user->id)->get();
foreach ($invitations as $invitation) {
$this->dispatch(new DeleteInvitation($invitation));
}
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Listeners\Auth;
use App\Events\Auth\InvitationCreated as Event;
use App\Notifications\Auth\Invitation as Notification;
class SendUserInvitation
{
/**
* Handle the event.
*
* @param $event
* @return void
*/
public function handle(Event $event)
{
$invitation = $event->invitation;
$invitation->user->notify(new Notification($invitation));
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace App\Listeners\Update\V30;
use App\Abstracts\Listeners\Update as Listener;
use App\Events\Install\UpdateFinished as Event;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
class Version304 extends Listener
{
const ALIAS = 'core';
const VERSION = '3.0.4';
/**
* Handle the event.
*
* @param $event
* @return void
*/
public function handle(Event $event)
{
if ($this->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',
'app/Listeners/Auth/DeleteUserInvitation.php',
];
foreach ($files as $file) {
File::delete(base_path($file));
}
Log::channel('stderr')->info('Old files deleted.');
}
}

View File

@ -89,6 +89,11 @@ class User extends Authenticatable implements HasLocalePreference
return $this->belongsToMany('App\Models\Common\Dashboard', 'App\Models\Auth\UserDashboard'); 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 * Always capitalize the name when we retrieve it
*/ */
@ -311,14 +316,12 @@ class User extends Authenticatable implements HasLocalePreference
return $actions; return $actions;
} }
if (! $this->hasPendingInvitation()) {
$actions[] = [ $actions[] = [
'title' => trans('general.edit'), 'title' => trans('general.edit'),
'icon' => 'edit', 'icon' => 'edit',
'url' => route('users.edit', $this->id), 'url' => route('users.edit', $this->id),
'permission' => 'update-auth-users', 'permission' => 'update-auth-users',
]; ];
}
if ($this->hasPendingInvitation()) { if ($this->hasPendingInvitation()) {
$actions[] = [ $actions[] = [

View File

@ -20,7 +20,7 @@ class UserInvitation extends Model
* *
* @var string[] * @var string[]
*/ */
protected $fillable = ['user_id', 'company_id', 'token']; protected $fillable = ['user_id', 'token'];
public function user() public function user()
{ {

View File

@ -17,6 +17,7 @@ class Event extends Provider
'App\Listeners\Module\UpdateExtraModules', 'App\Listeners\Module\UpdateExtraModules',
'App\Listeners\Update\V30\Version300', 'App\Listeners\Update\V30\Version300',
'App\Listeners\Update\V30\Version303', 'App\Listeners\Update\V30\Version303',
'App\Listeners\Update\V30\Version304',
], ],
'Illuminate\Auth\Events\Login' => [ 'Illuminate\Auth\Events\Login' => [
'App\Listeners\Auth\Login', 'App\Listeners\Auth\Login',
@ -31,12 +32,6 @@ class Event extends Provider
'App\Events\Auth\LandingPageShowing' => [ 'App\Events\Auth\LandingPageShowing' => [
'App\Listeners\Auth\AddLandingPages', '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\Events\Document\DocumentCreated' => [
'App\Listeners\Document\CreateDocumentCreatedHistory', 'App\Listeners\Document\CreateDocumentCreatedHistory',
'App\Listeners\Document\IncreaseNextDocumentNumber', 'App\Listeners\Document\IncreaseNextDocumentNumber',

View File

@ -110,31 +110,25 @@ trait Users
} }
/** /**
* Checks if the given user has a pending invitation for the * Checks if the given user has a pending invitation.
* provided Company.
* *
* @return bool * @return bool
*/ */
public function hasPendingInvitation($company_id = null) public function hasPendingInvitation()
{ {
$company_id = $company_id ?: company_id(); $invitation = UserInvitation::where('user_id', $this->id)->first();
$invitation = UserInvitation::where('user_id', $this->id)->where('company_id', $company_id)->first();
return $invitation ? true : false; return $invitation ? true : false;
} }
/** /**
* Returns if the given user has a pending invitation for the * Returns if the given user has a pending invitation.
* provided Company.
* *
* @return null|UserInvitation * @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)->first();
$invitation = UserInvitation::where('user_id', $this->id)->where('company_id', $company_id)->first();
return $invitation; return $invitation;
} }

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('user_invitations', function (Blueprint $table) {
$table->dropColumn('company_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -13,7 +13,7 @@
<div class="sm:col-span-3 grid gap-x-8 gap-y-6 {{ user()->id == $user->id ? 'grid-rows-3' : 'grid-rows-2' }}"> <div class="sm:col-span-3 grid gap-x-8 gap-y-6 {{ user()->id == $user->id ? 'grid-rows-3' : 'grid-rows-2' }}">
<x-form.group.text name="name" label="{{ trans('general.name') }}" /> <x-form.group.text name="name" label="{{ trans('general.name') }}" />
<x-form.group.email name="email" label="{{ trans('general.email') }}" /> <x-form.group.email name="email" label="{{ trans('general.email') }}" ::disabled="{{ $user->hasPendingInvitation() ? 'true' : 'false' }}" />
@if (user()->id == $user->id) @if (user()->id == $user->id)
<x-form.group.checkbox name="change_password" :options="['1' => trans('auth.change_password')]" form-group-class="sm:col-span-3" @input="onChangePassword($event)" /> <x-form.group.checkbox name="change_password" :options="['1' => trans('auth.change_password')]" form-group-class="sm:col-span-3" @input="onChangePassword($event)" />

View File

@ -48,15 +48,11 @@
@foreach($users as $item) @foreach($users as $item)
<x-table.tr href="{{ route('users.edit', $item->id) }}"> <x-table.tr href="{{ route('users.edit', $item->id) }}">
<x-table.td class="ltr:pr-6 rtl:pl-6 hidden sm:table-cell" override="class"> <x-table.td class="ltr:pr-6 rtl:pl-6 hidden sm:table-cell" override="class">
@if (user()->id != $item->id)
<x-index.bulkaction.single <x-index.bulkaction.single
id="{{ $item->id }}" id="{{ $item->id }}"
name="{{ $item->name }}" name="{{ $item->name }}"
:disabled="($item->hasPendingInvitation() || $item->multiplexed) ? true : false" :disabled="($item->hasPendingInvitation() || user()->id == $item->id) ? true : false"
/> />
@else
<x-index.bulkaction.single id="{{ $item->id }}" name="{{ $item->name }}" disabled />
@endif
</x-table.td> </x-table.td>
<x-table.td class="w-4/12 sm:w-5/12"> <x-table.td class="w-4/12 sm:w-5/12">

View File

@ -4,6 +4,8 @@ namespace Tests\Feature\Auth;
use App\Jobs\Auth\CreateUser; use App\Jobs\Auth\CreateUser;
use App\Models\Auth\User; use App\Models\Auth\User;
use App\Notifications\Auth\Invitation;
use Illuminate\Support\Facades\Notification;
use Tests\Feature\FeatureTestCase; use Tests\Feature\FeatureTestCase;
class UsersTest extends FeatureTestCase class UsersTest extends FeatureTestCase
@ -16,6 +18,22 @@ class UsersTest extends FeatureTestCase
->assertSeeText(trans_choice('general.users', 2)); ->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() public function testItShouldSeeUserCreatePage()
{ {
$this->loginAs() $this->loginAs()
@ -26,15 +44,30 @@ class UsersTest extends FeatureTestCase
public function testItShouldCreateUser() public function testItShouldCreateUser()
{ {
Notification::fake();
$request = $this->getRequest(); $request = $this->getRequest();
$this->loginAs() $response = $this->loginAs()
->post(route('users.store'), $request) ->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->assertFlashLevel('success');
$this->assertDatabaseHas('users', $this->getAssertRequest($request)); $this->assertModelExists($user);
$this->assertModelExists($user->invitation);
Notification::assertSentTo([$user], Invitation::class);
} }
public function testItShouldSeeUserUpdatePage() public function testItShouldSeeUserUpdatePage()
@ -80,6 +113,8 @@ class UsersTest extends FeatureTestCase
$this->assertFlashLevel('success'); $this->assertFlashLevel('success');
$this->assertSoftDeleted('users', $this->getAssertRequest($request)); $this->assertSoftDeleted('users', $this->getAssertRequest($request));
$this->assertSoftDeleted('user_invitations', ['user_id' => $user->id]);
} }
public function testItShouldSeeLoginPage() public function testItShouldSeeLoginPage()
@ -127,6 +162,71 @@ class UsersTest extends FeatureTestCase
$this->assertGuest(); $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() public function getRequest()
{ {
return User::factory()->enabled()->raw(); return User::factory()->enabled()->raw();