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