From 003e30d423c091b774d0d9380a16c2e055419aec Mon Sep 17 00:00:00 2001 From: philipp lang <philipp@aweos.de> Date: Sat, 7 Dec 2024 14:55:14 +0100 Subject: [PATCH] Add Login module --- app/Http/Controllers/Auth/LoginController.php | 60 ---------------- .../Controllers/Auth/RegisterController.php | 71 ------------------- app/View/Page/Full.php | 32 +++++++++ app/View/Ui/Button.php | 22 ++++++ config/app.php | 1 + database/factories/UserFactory.php | 5 ++ modules/Auth/AuthServiceProvider.php | 31 ++++++++ modules/Auth/Components/LoginForm.php | 62 ++++++++++++++++ modules/Auth/Components/LoginFormTest.php | 69 ++++++++++++++++++ .../js/components/page/FullHeadingBanner.vue | 10 --- resources/js/views/VLogin.vue | 36 ---------- resources/js/views/authentication/VLogin.vue | 39 ---------- resources/lang/de/auth.php | 2 +- .../views/components/layouts/full.blade.php | 8 +++ routes/web.php | 4 +- 15 files changed, 232 insertions(+), 220 deletions(-) delete mode 100644 app/Http/Controllers/Auth/LoginController.php delete mode 100644 app/Http/Controllers/Auth/RegisterController.php create mode 100644 app/View/Page/Full.php create mode 100644 app/View/Ui/Button.php create mode 100644 modules/Auth/AuthServiceProvider.php create mode 100644 modules/Auth/Components/LoginForm.php create mode 100644 modules/Auth/Components/LoginFormTest.php delete mode 100644 resources/js/components/page/FullHeadingBanner.vue delete mode 100644 resources/js/views/VLogin.vue delete mode 100644 resources/js/views/authentication/VLogin.vue create mode 100644 resources/views/components/layouts/full.blade.php diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php deleted file mode 100644 index 8dec071d..00000000 --- a/app/Http/Controllers/Auth/LoginController.php +++ /dev/null @@ -1,60 +0,0 @@ -<?php - -namespace App\Http\Controllers\Auth; - -use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; -use Illuminate\Foundation\Auth\AuthenticatesUsers; -use Illuminate\Http\Request; -use Inertia\Response; - -class LoginController extends Controller -{ - /* - |-------------------------------------------------------------------------- - | Login Controller - |-------------------------------------------------------------------------- - | - | This controller handles authenticating users for the application and - | redirecting them to your home screen. The controller uses a trait - | to conveniently provide its functionality to your applications. - | - */ - - use AuthenticatesUsers; - - /** - * Where to redirect users after login. - * - * @var string - */ - protected $redirectTo = RouteServiceProvider::HOME; - - /** - * Create a new controller instance. - * - * @return void - */ - public function __construct() - { - $this->middleware('guest')->except('logout'); - } - - public function showLoginForm(): Response - { - session()->put('title', 'Anmelden'); - - return \Inertia::render('authentication/VLogin'); - } - - /** - * Validate the user login request. - */ - protected function validateLogin(Request $request): void - { - $request->validate([ - $this->username() => 'required|max:255|string|email', - 'password' => 'required|string', - ]); - } -} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php deleted file mode 100644 index edb9c4a4..00000000 --- a/app/Http/Controllers/Auth/RegisterController.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php - -namespace App\Http\Controllers\Auth; - -use App\Http\Controllers\Controller; -use App\Providers\RouteServiceProvider; -use App\User; -use Illuminate\Foundation\Auth\RegistersUsers; -use Illuminate\Support\Facades\Hash; -use Illuminate\Support\Facades\Validator; - -class RegisterController extends Controller -{ - /* - |-------------------------------------------------------------------------- - | Register Controller - |-------------------------------------------------------------------------- - | - | This controller handles the registration of new users as well as their - | validation and creation. By default this controller uses a trait to - | provide this functionality without requiring any additional code. - | - */ - - use RegistersUsers; - - /** - * Where to redirect users after registration. - * - * @var string - */ - protected $redirectTo = RouteServiceProvider::HOME; - - /** - * Create a new controller instance. - * - * @return void - */ - public function __construct() - { - $this->middleware('guest'); - } - - /** - * Get a validator for an incoming registration request. - * - * @return \Illuminate\Contracts\Validation\Validator - */ - protected function validator(array $data) - { - return Validator::make($data, [ - 'name' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], - 'password' => ['required', 'string', 'min:8', 'confirmed'], - ]); - } - - /** - * Create a new user instance after a valid registration. - * - * @return \App\User - */ - protected function create(array $data) - { - return User::create([ - 'name' => $data['name'], - 'email' => $data['email'], - 'password' => Hash::make($data['password']), - ]); - } -} diff --git a/app/View/Page/Full.php b/app/View/Page/Full.php new file mode 100644 index 00000000..27ca702c --- /dev/null +++ b/app/View/Page/Full.php @@ -0,0 +1,32 @@ +<?php + +namespace App\View\Page; + +use Illuminate\View\Component; + +class Full extends Component +{ + + public function __construct(public string $title = '', public ?string $heading = null) + { + session()->put('title', $title); + } + + public function render() + { + return <<<'HTML' + <div class="min-w-[16rem] sm:min-w-[18rem] md:min-w-[24rem] bg-gray-800 rounded-xl overflow-hidden shadow-lg @if($heading === null) p-6 md:p-10 @endif"> + @if ($heading) + <div class="h-24 p-6 md:px-10 bg-primary-800 flex justify-between items-center w-full"> + <span class="text-primary-500 text-xl">{{$heading}}</span> + <img src="{{asset('img/dpsg.gif')}}" class="w-24" /> + </div> + @endif + + <div @if($heading !== null) class="p-6 md:p-10" @endif> + {{ $slot }} + </div> + </div> + HTML; + } +} diff --git a/app/View/Ui/Button.php b/app/View/Ui/Button.php new file mode 100644 index 00000000..e1f1129e --- /dev/null +++ b/app/View/Ui/Button.php @@ -0,0 +1,22 @@ +<?php + +namespace App\View\Ui; + +use Illuminate\View\Component; + +class Button extends Component +{ + + public function __construct(public string $type = 'button') + { + } + + public function render() + { + return <<<'HTML' + <button type="{{$type}}" class="px-3 py-2 uppercase no-underline text-sm items-center justify-center bg-primary-700 rounded text-primary-300"> + {{$slot}} + </button> + HTML; + } +} diff --git a/config/app.php b/config/app.php index 175422f4..67ccda3b 100644 --- a/config/app.php +++ b/config/app.php @@ -183,6 +183,7 @@ return [ Modules\Invoice\InvoiceServiceProvider::class, Modules\Mailgateway\MailgatewayServiceProvider::class, Modules\Nami\NamiServiceProvider::class, + Modules\Auth\AuthServiceProvider::class, ], /* diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index a8bd37bb..fc90d337 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -27,4 +27,9 @@ class UserFactory extends Factory 'lastname' => $this->faker->lastName, ]; } + + public function loginData(string $email, string $password): self + { + return $this->state(['email' => $email, 'password' => Hash::make($password)]); + } } diff --git a/modules/Auth/AuthServiceProvider.php b/modules/Auth/AuthServiceProvider.php new file mode 100644 index 00000000..85e58e7d --- /dev/null +++ b/modules/Auth/AuthServiceProvider.php @@ -0,0 +1,31 @@ +<?php + +namespace Modules\Auth; + +use Illuminate\Routing\Router; +use Illuminate\Support\ServiceProvider; +use Modules\Auth\Components\LoginForm; + +class AuthServiceProvider extends ServiceProvider +{ + /** + * Register services. + * + * @return void + */ + public function register() + { + } + + /** + * Bootstrap services. + * + * @return void + */ + public function boot() + { + app(Router::class)->middleware(['web', 'guest'])->group(function ($router) { + $router->get('/login', LoginForm::class)->name('login'); + }); + } +} diff --git a/modules/Auth/Components/LoginForm.php b/modules/Auth/Components/LoginForm.php new file mode 100644 index 00000000..3a6d2f8f --- /dev/null +++ b/modules/Auth/Components/LoginForm.php @@ -0,0 +1,62 @@ +<?php + +namespace Modules\Auth\Components; + +use Illuminate\Foundation\Auth\AuthenticatesUsers; +use Illuminate\Foundation\Auth\ThrottlesLogins; +use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; +use Livewire\Attributes\Layout; +use Livewire\Component; + +class LoginForm extends Component +{ + + use AuthenticatesUsers; + use ThrottlesLogins; + + public string $email = ''; + public string $password = ''; + + public function validateLogin(Request $request) + { + $this->validate([ + 'email' => 'required|max:255|string|email', + 'password' => 'required|string', + ]); + } + + protected function credentials(Request $request) + { + return ['email' => $this->email, 'password' => $this->password]; + } + + protected function sendLoginResponse(Request $request) + { + return redirect()->intended('/'); + } + + public function submit() + { + return $this->login(request()); + } + + #[Layout('components.layouts.full')] + public function render(): string + { + return <<<'HTML' + <x-page::full heading="Login" title="Login"> + <form wire:submit="submit"> + <div class="grid gap-5"> + <x-form::text name="email" wire:model="email" label="E-Mail-Adresse"></x-form::text> + <x-form::text name="password" wire:model="password" type="password" label="Passwort"></x-form::text> + <x-ui::button type="submit">Login</x-ui::button> + <div class="flex justify-center"> + <button type="button" class="text-gray-500 text-sm hover:text-gray-300" @click.prevent="$inertia.visit('/password/reset')">Passwort vergessen?</button> + </div> + </div> + </form> + </x-page::full> + HTML; + } +} diff --git a/modules/Auth/Components/LoginFormTest.php b/modules/Auth/Components/LoginFormTest.php new file mode 100644 index 00000000..318598cd --- /dev/null +++ b/modules/Auth/Components/LoginFormTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Modules\Auth\Components; + +use App\User; +use Illuminate\Auth\Events\Lockout; +use Tests\TestCase; +use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Event; +use Livewire\Livewire; + +uses(TestCase::class); +uses(DatabaseTransactions::class); + +it('redirects to login', function () { + test()->get('/')->assertRedirect('/login'); +}); + +it('displays component', function () { + test()->get('/login')->assertSeeLivewire(LoginForm::class)->assertDontSee('Dashboard'); +}); + +it('displays form', function () { + Livewire::test(LoginForm::class) + ->assertSee('Login') + ->assertSee('Passwort vergessen'); +}); + +it('loggs in', function () { + User::factory()->loginData('admin@example.com', 'secret')->create(); + Livewire::test(LoginForm::class) + ->set('email', 'admin@example.com') + ->set('password', 'secret') + ->call('submit') + ->assertRedirect('/'); + + $this->assertEquals('admin@example.com', auth()->user()->email); +}); + +it('displays failed login response', function () { + Livewire::test(LoginForm::class) + ->set('email', 'admin@example.com') + ->set('password', 'secret') + ->call('submit') + ->assertHasErrors(['email' => 'Login fehlgeschlagen.']); +}); + +it('increments login attempts', function () { + Event::fake([Lockout::class]); + Livewire::test(LoginForm::class) + ->set('email', 'admin@example.com') + ->set('password', 'secret') + ->call('submit') + ->call('submit') + ->call('submit') + ->call('submit') + ->call('submit') + ->call('submit'); + Event::assertDispatchedTimes(Lockout::class); +}); + +it('requires email and password', function () { + User::factory()->loginData('admin@example.com', 'secret')->create(); + Livewire::test(LoginForm::class) + ->set('email', '') + ->set('password', '') + ->call('submit') + ->assertHasErrors(['email', 'password']); +}); diff --git a/resources/js/components/page/FullHeadingBanner.vue b/resources/js/components/page/FullHeadingBanner.vue deleted file mode 100644 index f686f8bb..00000000 --- a/resources/js/components/page/FullHeadingBanner.vue +++ /dev/null @@ -1,10 +0,0 @@ -<template> - <div class="h-24 p-6 md:px-10 bg-primary-800 flex justify-between items-center w-full"> - <span class="text-primary-500 text-xl"><slot></slot></span> - <img src="../../../img/dpsg.gif" class="w-24" /> - </div> -</template> - -<script> -export default {}; -</script> diff --git a/resources/js/views/VLogin.vue b/resources/js/views/VLogin.vue deleted file mode 100644 index c65ea511..00000000 --- a/resources/js/views/VLogin.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> - <page-full-layout banner> - <template #heading> - <page-full-heading-banner>Login</page-full-heading-banner> - </template> - <form @submit.prevent="submit"> - <div class="grid gap-5"> - <f-text id="email" v-model="values.email" label="E-Mail-Adresse"></f-text> - <f-text id="password" v-model="values.password" type="password" label="Passwort"></f-text> - <button type="submit" class="btn btn-primary">Login</button> - </div> - </form> - </page-full-layout> -</template> - -<script> -import FullLayout from '../layouts/FullLayout.vue'; - -export default { - layout: FullLayout, - - data: function () { - return { - values: { - email: '', - password: '', - }, - }; - }, - methods: { - submit() { - this.$inertia.post('/login', this.values); - }, - }, -}; -</script> diff --git a/resources/js/views/authentication/VLogin.vue b/resources/js/views/authentication/VLogin.vue deleted file mode 100644 index 9d4ee1d4..00000000 --- a/resources/js/views/authentication/VLogin.vue +++ /dev/null @@ -1,39 +0,0 @@ -<template> - <page-full-layout banner> - <template #heading> - <page-full-heading-banner>Login</page-full-heading-banner> - </template> - <form @submit.prevent="submit"> - <div class="grid gap-5"> - <f-text id="email" v-model="values.email" label="E-Mail-Adresse"></f-text> - <f-text id="password" v-model="values.password" type="password" label="Passwort"></f-text> - <button type="submit" class="btn btn-primary">Login</button> - <div class="flex justify-center"> - <button type="button" class="text-gray-500 text-sm hover:text-gray-300" @click.prevent="$inertia.visit('/password/reset')">Passwort vergessen?</button> - </div> - </div> - </form> - </page-full-layout> -</template> - -<script> -import FullLayout from '../../layouts/FullLayout.vue'; - -export default { - layout: FullLayout, - - data: function () { - return { - values: { - email: '', - password: '', - }, - }; - }, - methods: { - submit() { - this.$inertia.post('/login', this.values); - }, - }, -}; -</script> diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php index 24d74c85..cf287535 100644 --- a/resources/lang/de/auth.php +++ b/resources/lang/de/auth.php @@ -12,6 +12,6 @@ return [ | */ - 'failed' => 'Diese Kombination aus Zugangsdaten wurde nicht in unserer Datenbank gefunden.', + 'failed' => 'Login fehlgeschlagen.', 'throttle' => 'Zu viele Loginversuche. Versuchen Sie es bitte in :seconds Sekunden nochmal.', ]; diff --git a/resources/views/components/layouts/full.blade.php b/resources/views/components/layouts/full.blade.php new file mode 100644 index 00000000..a2102f9d --- /dev/null +++ b/resources/views/components/layouts/full.blade.php @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html class="h-full" lang="de"> + <x-head></x-head> + <body class="min-h-full flex justify-center items-center bg-gray-900"> + {{ $slot }} + @livewireScriptConfig + </body> +</html> diff --git a/routes/web.php b/routes/web.php index 21d0ac81..cb4725d3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -76,9 +76,7 @@ use App\Membership\Actions\MembershipStoreAction; use App\Membership\Actions\MembershipUpdateAction; use App\Payment\SubscriptionController; -Route::group(['namespace' => 'App\\Http\\Controllers'], function (): void { - Auth::routes(['register' => false]); -}); + Route::group(['middleware' => 'auth:web'], function (): void { Route::post('/nami/login-check', NamiLoginCheckAction::class)->name('nami.login-check');