diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 898441d9..6908e10a 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -38,7 +38,14 @@ class UserResource extends JsonResource public static function meta(): array { return [ - 'links' => [] + 'default' => [ + 'firstname' => '', + 'lastname' => '', + 'email' => '', + ], + 'links' => [ + 'store' => route('user.store'), + ] ]; } } diff --git a/app/User.php b/app/User.php index bb1bdc3c..8f21b830 100644 --- a/app/User.php +++ b/app/User.php @@ -27,4 +27,9 @@ class User extends Authenticatable { return 'https://www.gravatar.com/avatar/' . hash('sha256', $this->email); } + + public function getFullname(): string + { + return $this->firstname . ' ' . $this->lastname; + } } diff --git a/app/User/Actions/IndexAction.php b/app/User/Actions/IndexAction.php deleted file mode 100644 index d6461057..00000000 --- a/app/User/Actions/IndexAction.php +++ /dev/null @@ -1,18 +0,0 @@ -get()); - } -} diff --git a/app/User/Actions/StoreAction.php b/app/User/Actions/StoreAction.php new file mode 100644 index 00000000..e80b859e --- /dev/null +++ b/app/User/Actions/StoreAction.php @@ -0,0 +1,41 @@ + 'required|string|max:255', + 'lastname' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email', + ]; + } + + public function handle(ActionRequest $request): JsonResponse + { + User::create([ + ...$request->validated(), + 'password' => Hash::make(str()->random(32)), + ]); + + Password::broker()->sendResetLink( + $request->safe()->only('email'), + fn ($user, $token) => Mail::to($user)->send(new WelcomeMail($user, $token)) + ); + + return response()->json([]); + } +} diff --git a/app/User/Mail/WelcomeMail.php b/app/User/Mail/WelcomeMail.php new file mode 100644 index 00000000..4eaf3c4d --- /dev/null +++ b/app/User/Mail/WelcomeMail.php @@ -0,0 +1,64 @@ +resetUrl = route('password.reset', ['token' => $token, 'email' => $user->email]); + $this->home = url(''); + } + + /** + * Get the message envelope. + * + * @return \Illuminate\Mail\Mailables\Envelope + */ + public function envelope() + { + return new Envelope( + subject: 'Willkommen bei Adrema', + ); + } + + /** + * Get the message content definition. + * + * @return \Illuminate\Mail\Mailables\Content + */ + public function content() + { + return new Content( + markdown: 'mail.user.welcome', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments() + { + return []; + } +} diff --git a/app/User/UserSettings.php b/app/User/UserSettings.php index 1bea49f5..b69dbd15 100644 --- a/app/User/UserSettings.php +++ b/app/User/UserSettings.php @@ -18,19 +18,11 @@ class UserSettings extends LocalSettings return 'Benutzer'; } - /** - * @inheritdoc - */ - public function meta(): array - { - return UserResource::meta(); - } - /** * @inheritdoc */ public function data() { - return UserResource::collection(User::orderByRaw('lastname, firstname')->get())->toArray(request()); + return UserResource::collection(User::orderByRaw('lastname, firstname')->get()); } } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a8bd37bb..9182f1e1 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -27,4 +27,15 @@ class UserFactory extends Factory 'lastname' => $this->faker->lastName, ]; } + + public function name(string $name): self + { + [$firstname, $lastname] = explode(' ', $name); + return $this->state(['firstname' => $firstname, 'lastname' => $lastname]); + } + + public function email(string $email): self + { + return $this->state(['email' => $email]); + } } diff --git a/resources/js/views/setting/User.vue b/resources/js/views/setting/User.vue index f7a60176..69169c2f 100644 --- a/resources/js/views/setting/User.vue +++ b/resources/js/views/setting/User.vue @@ -5,6 +5,11 @@
+
+ + + +
Speichern Abbrechen @@ -41,21 +46,9 @@ diff --git a/resources/views/mail/user/welcome.blade.php b/resources/views/mail/user/welcome.blade.php new file mode 100644 index 00000000..c7d03da1 --- /dev/null +++ b/resources/views/mail/user/welcome.blade.php @@ -0,0 +1,18 @@ +@component('mail::message') +# Hallo {{ $user->getFullname() }}, + +Für dich wurde vor kurzem bei Adrema ({{$home}}) ein neues Konto mit der E-Mail-Adresse {{$user->email}} angelegt. + +Bitte klicke auf nachfolgenden Link, um für dieses Konto ein Passwort zu vergeben: + +@component('mail::button', ['url' => $resetUrl]) + Passwort setzen +@endcomponent + +@component('mail::subcopy') +Herzliche Grüße und gut Pfad + +{{app(\App\Invoice\InvoiceSettings::class)->from_long}} +@endcomponent + +@endcomponent diff --git a/routes/api.php b/routes/api.php index 2f417d6e..70730f99 100644 --- a/routes/api.php +++ b/routes/api.php @@ -4,10 +4,8 @@ use App\Contribution\Actions\GenerateApiAction as ContributionGenerateApiAction; use App\Form\Actions\FormApiListAction; use App\Form\Actions\RegisterAction; use App\Group\Actions\GroupApiIndexAction; -use App\User\Actions\IndexAction as UserIndexAction; Route::post('/contribution-generate', ContributionGenerateApiAction::class)->name('api.contribution.generate')->middleware('client:contribution-generate'); Route::post('/form/{form}/register', RegisterAction::class)->name('form.register'); Route::get('/group/{group?}', GroupApiIndexAction::class)->name('api.group'); Route::get('/form', FormApiListAction::class)->name('api.form.index'); -Route::get('/user', UserIndexAction::class)->name('api.user.index'); diff --git a/routes/web.php b/routes/web.php index 7f3aae8f..98c2afa8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -77,6 +77,7 @@ use App\Membership\Actions\MembershipDestroyAction; use App\Membership\Actions\MembershipStoreAction; use App\Membership\Actions\MembershipUpdateAction; use App\Payment\SubscriptionController; +use App\User\Actions\StoreAction as UserStoreAction; Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void { Auth::routes(['register' => false]); @@ -182,4 +183,7 @@ Route::group(['middleware' => 'auth:web'], function (): void { Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store'); Route::patch('/fileshare/{fileshare}', FileshareUpdateAction::class)->name('fileshare.update'); Route::post('/api/fileshare/{fileshare}/files', ListFilesAction::class)->name('api.fileshare.files'); + + // -------------------------------------- user ------------------------------------- + Route::post('/user', UserStoreAction::class)->name('user.store'); }); diff --git a/tests/Feature/Permission/UserIndexTest.php b/tests/Feature/Permission/UserIndexTest.php index 0ceb1364..4346c816 100644 --- a/tests/Feature/Permission/UserIndexTest.php +++ b/tests/Feature/Permission/UserIndexTest.php @@ -12,24 +12,21 @@ class UserIndexTest extends TestCase use DatabaseTransactions; public function testItOpensSettingsPage(): void - { - $this->login()->loginNami(); - $this->get(route('setting.view', ['settingGroup' => 'user'])) - ->assertOk() - ->assertComponent('setting/User'); - } - - public function testItListsUsers(): void { $this->login()->loginNami(); auth()->user()->update(['firstname' => 'Jane', 'lastname' => 'Doe']); User::factory()->create(['firstname' => 'John', 'lastname' => 'Doe']); $anna = User::factory()->create(['firstname' => 'Anna', 'lastname' => 'Doe']); - $this->get(route('api.user.index')) - ->assertJsonPath('data.0.firstname', 'Anna') - ->assertJsonPath('data.0.lastname', 'Doe') - ->assertJsonPath('data.0.id', $anna->id) - ->assertJsonPath('data.1.firstname', 'Jane') - ->assertJsonPath('data.2.firstname', 'John'); + $this->get(route('setting.view', ['settingGroup' => 'user'])) + ->assertOk() + ->assertComponent('setting/User') + ->assertInertiaPath('data.data.0.firstname', 'Anna') + ->assertInertiaPath('data.data.0.lastname', 'Doe') + ->assertInertiaPath('data.data.0.id', $anna->id) + ->assertInertiaPath('data.data.1.firstname', 'Jane') + ->assertInertiaPath('data.data.2.firstname', 'John') + ->assertInertiaPath('data.meta.default.firstname', '') + ->assertInertiaPath('data.meta.default.email', '') + ->assertInertiaPath('data.meta.links.store', route('user.store')); } } diff --git a/tests/Feature/Permission/UserStoreTest.php b/tests/Feature/Permission/UserStoreTest.php new file mode 100644 index 00000000..8b076fad --- /dev/null +++ b/tests/Feature/Permission/UserStoreTest.php @@ -0,0 +1,63 @@ +login()->loginNami(); + + $this->postJson(route('user.store'), UserRequestFactory::new()->name('Max Muster')->email('max@muster.de')->create()) + ->assertOk(); + + $this->assertDatabaseHas('users', [ + 'email' => 'max@muster.de', + 'firstname' => 'Max', + 'lastname' => 'Muster', + ]); + + $this->assertDatabaseHas('password_resets', ['email' => 'max@muster.de']); + Mail::assertQueued( + WelcomeMail::class, + fn ($mail) => Hash::check($mail->token, DB::table('password_resets')->where('email', 'max@muster.de')->first()->token) + ); + } + + public function testItNeedsNameAndEmail(): void + { + Mail::fake(); + $this->login()->loginNami(); + + User::factory()->email('jane@doe.de')->create(); + $this->postJson(route('user.store'), UserRequestFactory::new()->name('')->email('max@muster.de')->create())->assertJsonValidationErrors('firstname'); + $this->postJson(route('user.store'), UserRequestFactory::new()->name('Max Muster')->email('maxusterde')->create())->assertJsonValidationErrors('email'); + $this->postJson(route('user.store'), UserRequestFactory::new()->name('Max Muster')->email('')->create())->assertJsonValidationErrors('email'); + $this->postJson(route('user.store'), UserRequestFactory::new()->email('jane@doe.de')->create())->assertJsonValidationErrors('email'); + } + + public function testWelcomeMailHasPasswordResetLink(): void + { + app(InvoiceSettings::class)->fill(['from_long' => 'Stamm BiPi'])->save(); + (new WelcomeMail(User::factory()->email('max@muster.de')->name('Max Muster')->create(), 'RESETToken')) + ->assertSeeInHtml('Hallo Max Muster') + ->assertSeeInHtml(url('')) + ->assertSeeInHtml('Stamm BiPi') + ->assertSeeInHtml('max@muster.de') + ->assertSeeInHtml(route('password.reset', ['token' => 'RESETToken', 'email' => 'max@muster.de'])); + } +} diff --git a/tests/RequestFactories/UserRequestFactory.php b/tests/RequestFactories/UserRequestFactory.php new file mode 100644 index 00000000..6c9a9246 --- /dev/null +++ b/tests/RequestFactories/UserRequestFactory.php @@ -0,0 +1,31 @@ +} + */ + public function definition(): array + { + return [ + 'firstname' => $this->faker->firstName(), + 'lastname' => $this->faker->lastName(), + 'email' => $this->faker->safeEmail(), + ]; + } + + public function name(string $name): self + { + [$firstname, $lastname] = explode(' ', $name ?: ' '); + return $this->state(['firstname' => $firstname, 'lastname' => $lastname]); + } + + public function email(string $email): self + { + return $this->state(['email' => $email]); + } +}