Add User index and store user

This commit is contained in:
philipp lang 2024-08-02 15:12:47 +02:00
parent cbb45a0452
commit 4548407d55
14 changed files with 265 additions and 59 deletions

View File

@ -38,7 +38,14 @@ class UserResource extends JsonResource
public static function meta(): array
{
return [
'links' => []
'default' => [
'firstname' => '',
'lastname' => '',
'email' => '',
],
'links' => [
'store' => route('user.store'),
]
];
}
}

View File

@ -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;
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\User\Actions;
use App\Http\Resources\UserResource;
use App\User;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Lorisleiva\Actions\Concerns\AsAction;
class IndexAction
{
use AsAction;
public function handle(): AnonymousResourceCollection
{
return UserResource::collection(User::orderByRaw('lastname, firstname')->get());
}
}

View File

@ -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([]);
}
}

View File

@ -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 [];
}
}

View File

@ -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());
}
}

View File

@ -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]);
}
}

View File

@ -5,6 +5,11 @@
</template>
<ui-popup v-if="single !== null" :heading="single.id ? 'Benutzer bearbeiten' : 'Neuer Benutzer'" @close="cancel">
<form @submit.prevent="submit">
<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="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 class="flex mt-4 space-x-2">
<ui-button type="submit" class="btn-danger">Speichern</ui-button>
<ui-button class="btn-primary" @click.prevent="single = null">Abbrechen</ui-button>
@ -41,21 +46,9 @@
<script lang="js" setup>
import SettingLayout from '../setting/Layout.vue';
const props = defineProps({
data: {
type: Object,
required: true,
},
meta: {
type: Object,
required: false,
default: () => {
return {};
},
},
});
import {indexProps, useIndex} from '../../composables/useInertiaApiIndex.js';
const {data, meta, single, create} = useIndex(props);
const props = defineProps(indexProps);
const {data, cancel, meta, single, create, submit, edit} = useIndex(props.data);
</script>

View File

@ -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

View File

@ -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');

View File

@ -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');
});

View File

@ -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'));
}
}

View File

@ -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']));
}
}

View File

@ -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]);
}
}