Compare commits
6 Commits
14925f6cb5
...
2ec7dd23eb
Author | SHA1 | Date |
---|---|---|
philipp lang | 2ec7dd23eb | |
philipp lang | 4548407d55 | |
philipp lang | cbb45a0452 | |
philipp lang | 3e92a7c40a | |
philipp lang | 310f3c9727 | |
philipp lang | 8b3b3de9a8 |
|
@ -32,6 +32,9 @@ class UserResource extends JsonResource
|
||||||
'avatar' => [
|
'avatar' => [
|
||||||
'src' => Storage::url('avatar.png'),
|
'src' => Storage::url('avatar.png'),
|
||||||
],
|
],
|
||||||
|
'links' => [
|
||||||
|
'update' => route('user.update', ['user' => $this->getModel()]),
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,8 +44,11 @@ class UserResource extends JsonResource
|
||||||
'default' => [
|
'default' => [
|
||||||
'firstname' => '',
|
'firstname' => '',
|
||||||
'lastname' => '',
|
'lastname' => '',
|
||||||
|
'email' => '',
|
||||||
],
|
],
|
||||||
'links' => []
|
'links' => [
|
||||||
|
'store' => route('user.store'),
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,4 +27,9 @@ class User extends Authenticatable
|
||||||
{
|
{
|
||||||
return 'https://www.gravatar.com/avatar/' . hash('sha256', $this->email);
|
return 'https://www.gravatar.com/avatar/' . hash('sha256', $this->email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFullname(): string
|
||||||
|
{
|
||||||
|
return $this->firstname . ' ' . $this->lastname;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Actions;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use App\User\Mail\WelcomeMail;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class StoreAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'firstname' => '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([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Actions;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Lorisleiva\Actions\ActionRequest;
|
||||||
|
use Lorisleiva\Actions\Concerns\AsAction;
|
||||||
|
|
||||||
|
class UpdateAction
|
||||||
|
{
|
||||||
|
use AsAction;
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'firstname' => 'required|string|max:255',
|
||||||
|
'lastname' => 'required|string|max:255',
|
||||||
|
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')->ignore(request()->route('user')->id)],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(User $user, ActionRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user->update($request->validated());
|
||||||
|
|
||||||
|
return response()->json([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\User\Mail;
|
||||||
|
|
||||||
|
use App\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 WelcomeMail extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public string $resetUrl;
|
||||||
|
public string $home;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new message instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(public User $user, public string $token)
|
||||||
|
{
|
||||||
|
$this->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 [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,4 +27,15 @@ class UserFactory extends Factory
|
||||||
'lastname' => $this->faker->lastName,
|
'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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ return new class extends Migration
|
||||||
|
|
||||||
foreach (DB::table('users')->get() as $user) {
|
foreach (DB::table('users')->get() as $user) {
|
||||||
[$firstname, $lastname] = explode(' ', $user->name);
|
[$firstname, $lastname] = explode(' ', $user->name);
|
||||||
DB::table('users')->where('id', $user->id)->update(['firstname' => $firstname, 'lastname' => $lastname]);
|
DB::table('users')->where('id', $user->id)->update(['firstname' => $firstname ?? '', 'lastname' => $lastname ?? '']);
|
||||||
}
|
}
|
||||||
|
|
||||||
Schema::table('users', function (Blueprint $table) {
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
<section class="grid grid-cols-2 gap-3 mt-6">
|
<section class="grid grid-cols-2 gap-3 mt-6">
|
||||||
<f-text id="firstname" v-model="single.firstname" name="firstname" label="Vorname" required></f-text>
|
<f-text id="firstname" v-model="single.firstname" name="firstname" label="Vorname" required></f-text>
|
||||||
<f-text id="lastname" v-model="single.lastname" name="lastname" label="Nachname" required></f-text>
|
<f-text id="lastname" v-model="single.lastname" name="lastname" label="Nachname" required></f-text>
|
||||||
|
<f-text id="email" v-model="single.email" name="email" label="E-Mail-Adresse" required></f-text>
|
||||||
</section>
|
</section>
|
||||||
<section class="flex mt-4 space-x-2">
|
<section class="flex mt-4 space-x-2">
|
||||||
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
|
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
|
||||||
|
@ -49,5 +50,5 @@ import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
|
||||||
|
|
||||||
const props = defineProps(indexProps);
|
const props = defineProps(indexProps);
|
||||||
|
|
||||||
const {data, cancel, meta, single, create, edit} = useIndex(props.data);
|
const {data, cancel, meta, single, create, submit, edit} = useIndex(props.data);
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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
|
|
@ -77,6 +77,8 @@ use App\Membership\Actions\MembershipDestroyAction;
|
||||||
use App\Membership\Actions\MembershipStoreAction;
|
use App\Membership\Actions\MembershipStoreAction;
|
||||||
use App\Membership\Actions\MembershipUpdateAction;
|
use App\Membership\Actions\MembershipUpdateAction;
|
||||||
use App\Payment\SubscriptionController;
|
use App\Payment\SubscriptionController;
|
||||||
|
use App\User\Actions\StoreAction as UserStoreAction;
|
||||||
|
use App\User\Actions\UpdateAction as UserUpdateAction;
|
||||||
|
|
||||||
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
|
Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void {
|
||||||
Auth::routes(['register' => false]);
|
Auth::routes(['register' => false]);
|
||||||
|
@ -182,4 +184,8 @@ Route::group(['middleware' => 'auth:web'], function (): void {
|
||||||
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');
|
Route::post('/fileshare', FileshareStoreAction::class)->name('fileshare.store');
|
||||||
Route::patch('/fileshare/{fileshare}', FileshareUpdateAction::class)->name('fileshare.update');
|
Route::patch('/fileshare/{fileshare}', FileshareUpdateAction::class)->name('fileshare.update');
|
||||||
Route::post('/api/fileshare/{fileshare}/files', ListFilesAction::class)->name('api.fileshare.files');
|
Route::post('/api/fileshare/{fileshare}/files', ListFilesAction::class)->name('api.fileshare.files');
|
||||||
|
|
||||||
|
// -------------------------------------- user -------------------------------------
|
||||||
|
Route::post('/user', UserStoreAction::class)->name('user.store');
|
||||||
|
Route::patch('/user/{user}', UserUpdateAction::class)->name('user.update');
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,8 +23,11 @@ class UserIndexTest extends TestCase
|
||||||
->assertInertiaPath('data.data.0.firstname', 'Anna')
|
->assertInertiaPath('data.data.0.firstname', 'Anna')
|
||||||
->assertInertiaPath('data.data.0.lastname', 'Doe')
|
->assertInertiaPath('data.data.0.lastname', 'Doe')
|
||||||
->assertInertiaPath('data.data.0.id', $anna->id)
|
->assertInertiaPath('data.data.0.id', $anna->id)
|
||||||
|
->assertInertiaPath('data.data.0.links.update', route('user.update', ['user' => $anna]))
|
||||||
->assertInertiaPath('data.data.1.firstname', 'Jane')
|
->assertInertiaPath('data.data.1.firstname', 'Jane')
|
||||||
->assertInertiaPath('data.data.2.firstname', 'John')
|
->assertInertiaPath('data.data.2.firstname', 'John')
|
||||||
->assertInertiaPath('data.meta.default.firstname', '');
|
->assertInertiaPath('data.meta.default.firstname', '')
|
||||||
|
->assertInertiaPath('data.meta.default.email', '')
|
||||||
|
->assertInertiaPath('data.meta.links.store', route('user.store'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Permission;
|
||||||
|
|
||||||
|
use App\Invoice\InvoiceSettings;
|
||||||
|
use App\User;
|
||||||
|
use App\User\Mail\WelcomeMail;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Tests\RequestFactories\UserRequestFactory;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class UserStoreTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function testItStoresUser(): void
|
||||||
|
{
|
||||||
|
Mail::fake();
|
||||||
|
$this->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']));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Permission;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Tests\RequestFactories\UserRequestFactory;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class UserUpdateTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
use DatabaseTransactions;
|
||||||
|
|
||||||
|
public function testItUpdatesAUser(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->patchJson(route('user.update', ['user' => $user]), UserRequestFactory::new()->name('Max Muster')->email('max@muster.de')->create())
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('users', [
|
||||||
|
'email' => 'max@muster.de',
|
||||||
|
'firstname' => 'Max',
|
||||||
|
'lastname' => 'Muster',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseCount('users', 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCannotUseAGivenMailAddress(): void
|
||||||
|
{
|
||||||
|
$this->login()->loginNami();
|
||||||
|
|
||||||
|
$jane = User::factory()->email('jane@muster.de')->create();
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->patchJson(route('user.update', ['user' => $user]), UserRequestFactory::new()->email('jane@muster.de')->create())
|
||||||
|
->assertJsonValidationErrors('email');
|
||||||
|
$this->patchJson(route('user.update', ['user' => $jane]), UserRequestFactory::new()->email('jane@muster.de')->create())
|
||||||
|
->assertJsonMissingValidationErrors('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItNeedsNameAndEmail(): void
|
||||||
|
{
|
||||||
|
Mail::fake();
|
||||||
|
$this->login()->loginNami();
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
User::factory()->email('jane@doe.de')->create();
|
||||||
|
$this->patchJson(route('user.update', ['user' => $user]), UserRequestFactory::new()->name('')->email('max@muster.de')->create())->assertJsonValidationErrors('firstname');
|
||||||
|
$this->patchJson(route('user.update', ['user' => $user]), UserRequestFactory::new()->name('Max Muster')->email('maxusterde')->create())->assertJsonValidationErrors('email');
|
||||||
|
$this->patchJson(route('user.update', ['user' => $user]), UserRequestFactory::new()->name('Max Muster')->email('')->create())->assertJsonValidationErrors('email');
|
||||||
|
$this->patchJson(route('user.update', ['user' => $user]), UserRequestFactory::new()->email('jane@doe.de')->create())->assertJsonValidationErrors('email');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\RequestFactories;
|
||||||
|
|
||||||
|
use Worksome\RequestFactories\RequestFactory;
|
||||||
|
|
||||||
|
class UserRequestFactory extends RequestFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{fee_id: int, name: string, children: array<int, array{amount: int, name: string}>}
|
||||||
|
*/
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue