Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions app/Filament/Resources/Users/Pages/CreateUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,40 @@

namespace App\Filament\Resources\Users\Pages;

use App\Mail\UserCreated;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Hash;
use App\Filament\Resources\Users\UserResource;
use Filament\Resources\Pages\CreateRecord;

class CreateUser extends CreateRecord
{
protected static string $resource = UserResource::class;

protected function mutateFormDataBeforeCreate(array $data): array
{
$plainPassword = Str::password(16, letters: true, numbers: true, symbols: true);

$data['password'] = Hash::make($plainPassword);

$this->plainPassword = $plainPassword;

return $data;
}

protected function afterCreate(): void
{
$loginUrl = route('login');

Mail::to($this->record->email)->send(
new UserCreated(
user: $this->record,
password: $this->plainPassword,
loginUrl: $loginUrl
)
);
}

private string $plainPassword;
}
8 changes: 8 additions & 0 deletions app/Filament/Resources/Users/Pages/ListUsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@
namespace App\Filament\Resources\Users\Pages;

use App\Filament\Resources\Users\UserResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;

class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;

protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
57 changes: 57 additions & 0 deletions app/Mail/UserCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;

class UserCreated extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;

/**
* Create a new message instance.
*/
public function __construct(
public User $user,
public string $password,
public string $loginUrl
) {
//
}

/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: trans('mail.user-created.subject', ['app_name' => config('app.name')]),
);
}

/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.user-created',
);
}

/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}
17 changes: 17 additions & 0 deletions lang/en/mail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

return [
'user-created' => [
'subject' => 'Welcome to :app_name',
'greeting' => 'Hello :name,',
'intro' => 'Your account has been created! We\'re excited to have you on board.',
'credentials-title' => 'Your login credentials',
'credentials-intro' => 'You can use the following credentials to log in to your account:',
'email-label' => 'Email address',
'password-label' => 'Temporary password',
'login-button' => 'Log in to your account',
'security-notice' => 'For security reasons, we recommend changing your password after your first login.',
'help-text' => 'If you have any questions or need assistance, feel free to reach out to our support team.',
'closing' => 'Best regards',
],
];
17 changes: 17 additions & 0 deletions lang/nl/mail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

return [
'user-created' => [
'subject' => 'Welkom bij :app_name',
'greeting' => 'Hallo :name,',
'intro' => 'Je account is succesvol aangemaakt! We zijn blij dat je er bij bent.',
'credentials-title' => 'Jouw inloggegevens',
'credentials-intro' => 'Je kunt met de volgende gegevens inloggen op je account:',
'email-label' => 'E-mailadres',
'password-label' => 'Tijdelijk wachtwoord',
'login-button' => 'Inloggen op je account',
'security-notice' => 'Voor de veiligheid raden we aan om je wachtwoord te wijzigen na je eerste inlog.',
'help-text' => 'Als je vragen hebt of hulp nodig hebt, neem gerust contact op met ons supportteam.',
'closing' => 'Met vriendelijke groet',
],
];
25 changes: 25 additions & 0 deletions resources/views/emails/user-created.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<x-mail::message>
# {{ __('mail.user-created.greeting', ['name' => $user->name]) }}

{{ __('mail.user-created.intro') }}

## {{ __('mail.user-created.credentials-title') }}

{{ __('mail.user-created.credentials-intro') }}

**{{ __('mail.user-created.email-label') }}:** {{ $user->email }}<br />
**{{ __('mail.user-created.password-label') }}:** {{ $password }}

<x-mail::button :url="$loginUrl">
{{ __('mail.user-created.login-button') }}
</x-mail::button>

---

{{ __('mail.user-created.security-notice') }}

{{ __('mail.user-created.help-text') }}

{{ __('mail.user-created.closing') }},<br>
{{ config('app.name') }}
</x-mail::message>
131 changes: 131 additions & 0 deletions tests/Feature/Filament/CreateUserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

use App\Enums\UserRole;
use App\Mail\UserCreated;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
use App\Filament\Resources\Users\Pages\CreateUser as CreateUserPage;

test('admin can access create user page', function () {
$admin = createAndLoginUser(['role' => UserRole::Admin]);

Livewire::test(CreateUserPage::class)
->assertSuccessful();
});

test('regular user cannot access create user page', function () {
$user = createAndLoginUser(['role' => UserRole::User]);

Livewire::test(CreateUserPage::class)
->assertForbidden();
});

test('creating a user generates a password and sends email', function () {
Mail::fake();
$admin = createAndLoginUser(['role' => UserRole::Admin]);

$userData = [
'name' => 'New Test User',
'email' => 'newuser@example.com',
'role' => UserRole::User->value,
];

Livewire::test(CreateUserPage::class)
->fillForm($userData)
->call('create')
->assertHasNoFormErrors();

$user = \App\Models\User::where('email', 'newuser@example.com')->first();

expect($user)->not->toBeNull()
->and($user->name)->toBe('New Test User')
->and($user->email)->toBe('newuser@example.com')
->and($user->role)->toBe(UserRole::User)
->and($user->password)->not->toBeNull();

Mail::assertQueued(UserCreated::class, function ($mail) use ($user) {
return $mail->hasTo($user->email)
&& $mail->user->id === $user->id
&& !empty($mail->password)
&& $mail->loginUrl === route('login');
});
});

test('created user password is properly hashed', function () {
Mail::fake();
$admin = createAndLoginUser(['role' => UserRole::Admin]);

$userData = [
'name' => 'Test User With Password',
'email' => 'passwordtest@example.com',
'role' => UserRole::Employee->value,
];

Livewire::test(CreateUserPage::class)
->fillForm($userData)
->call('create')
->assertHasNoFormErrors();

$user = \App\Models\User::where('email', 'passwordtest@example.com')->first();

expect($user->password)->not->toBeNull()
->and(Hash::needsRehash($user->password))->toBeFalse();
});

test('email contains the plain password not the hashed one', function () {
Mail::fake();
$admin = createAndLoginUser(['role' => UserRole::Admin]);

$userData = [
'name' => 'Another User',
'email' => 'another@example.com',
'role' => UserRole::Admin->value,
];

Livewire::test(CreateUserPage::class)
->fillForm($userData)
->call('create')
->assertHasNoFormErrors();

$user = \App\Models\User::where('email', 'another@example.com')->first();

Mail::assertQueued(UserCreated::class, function ($mail) use ($user) {
return $mail->user->id === $user->id
&& $mail->password !== $user->password
&& strlen($mail->password) === 16;
});
});

test('employee cannot access create user page', function () {
$employee = createAndLoginUser(['role' => UserRole::Employee]);

Livewire::test(CreateUserPage::class)
->assertForbidden();
});

test('created user receives email with correct translations', function () {
Mail::fake();
$admin = createAndLoginUser(['role' => UserRole::Admin]);

$userData = [
'name' => 'Translation Test User',
'email' => 'translation@example.com',
'role' => UserRole::User->value,
];

Livewire::test(CreateUserPage::class)
->fillForm($userData)
->call('create')
->assertHasNoFormErrors();

$user = \App\Models\User::where('email', 'translation@example.com')->first();

Mail::assertQueued(UserCreated::class, function ($mail) use ($user) {
$mailable = new UserCreated($mail->user, $mail->password, $mail->loginUrl);
$rendered = $mailable->render();

return str_contains($rendered, trans('mail.user-created.greeting', ['name' => $user->name]))
&& str_contains($rendered, trans('mail.user-created.login-button'));
});
});
64 changes: 64 additions & 0 deletions tests/Unit/Mail/UserCreatedTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

use App\Models\User;
use App\Mail\UserCreated;
use Illuminate\Support\Facades\Mail;

it('can create a UserCreated mailable with correct properties', function () {
$user = createUser(['name' => 'Test User', 'email' => 'test@example.com']);
$password = 'TestPassword123!';
$loginUrl = route('login');

$mailable = new UserCreated($user, $password, $loginUrl);

expect($mailable->user)->toBe($user)
->and($mailable->password)->toBe($password)
->and($mailable->loginUrl)->toBe($loginUrl);
});

it('has the correct subject with app name', function () {
$user = createUser(['name' => 'Test User']);
$password = 'TestPassword123!';
$loginUrl = route('login');

$mailable = new UserCreated($user, $password, $loginUrl);
$envelope = $mailable->envelope();

expect($envelope->subject)->toBe(trans('mail.user-created.subject', ['app_name' => config('app.name')]));
});

it('uses the correct markdown view', function () {
$user = createUser(['name' => 'Test User']);
$password = 'TestPassword123!';
$loginUrl = route('login');

$mailable = new UserCreated($user, $password, $loginUrl);
$content = $mailable->content();

expect($content->markdown)->toBe('emails.user-created');
});

it('should be queued', function () {
$user = createUser(['name' => 'Test User']);
$password = 'TestPassword123!';
$loginUrl = route('login');

$mailable = new UserCreated($user, $password, $loginUrl);

expect($mailable)->toBeInstanceOf(\Illuminate\Contracts\Queue\ShouldQueue::class);
});

it('renders the email with correct content', function () {
$user = createUser(['name' => 'Test User', 'email' => 'test@example.com']);
$password = 'TestPassword123!';
$loginUrl = route('login');

$mailable = new UserCreated($user, $password, $loginUrl);
$rendered = $mailable->render();

expect($rendered)
->toContain($user->name)
->toContain($user->email)
->toContain($password)
->toContain($loginUrl);
});
Loading