diff --git a/database/seeders/EmailTemplateSeeder.php b/database/seeders/EmailTemplateSeeder.php
new file mode 100644
index 0000000..f3c75b5
--- /dev/null
+++ b/database/seeders/EmailTemplateSeeder.php
@@ -0,0 +1,306 @@
+ [
+ 'subject' => 'New login detected',
+ 'body' => <<<'HTML'
+ # New login detected
+
+ A new login was detected on your account.
+
+ - IP: {{ $ip }}
+ - Device: {{ $device }}
+ - Time: {{ $time }}
+
+ **If this was you**
+ You can ignore this message, there is no need to take any action.
+
+ **If this wasn't you**
+ Please reset your password [here]({{ route('password.request') }}).
+ HTML,
+ 'in_app_title' => 'New login detected',
+ 'in_app_body' => 'A new login was detected on your account from IP: {{ $ip }} using {{ $device }} at {{ $time }}.',
+ 'mail_enabled' => 'force',
+ 'in_app_enabled' => 'choice_off',
+ 'edit_preference_message' => 'Alert me about new login attempts',
+ 'in_app_url' => '{{ route("profile.security") }}',
+ ],
+ 'new_invoice_created' => [
+ 'subject' => 'New invoice created',
+ 'body' => <<<'HTML'
+ # New invoice created
+
+ A new invoice was created on your account.
+
+ Total amount: **{{ $total }}**
+
+
+
+
+ | Item | Quantity | Price |
+ | :------: | :------: | :------: |
+ @foreach ($items as $item)
+ | {{ $item->description }} | {{ $item->quantity }} | {{ $item->price }} |
+ @endforeach
+
+
+
+
+ @if($has_subscription)
+ You have a active subscription, the invoice will be automatically paid.
+ @endif
+ HTML,
+ 'in_app_title' => 'New invoice created',
+ 'in_app_body' => 'A new invoice was created on your account with total amount: {{ $total }}.',
+ 'mail_enabled' => 'choice_on',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Notify me about new invoices',
+ 'in_app_url' => '{{ route("invoices.show", $invoice) }}',
+ ],
+ 'invoice_paid' => [
+ 'subject' => 'Invoice paid',
+ 'body' => <<<'HTML'
+ # Invoice paid
+
+ Your invoice has been successfully paid.
+
+ Total amount: **{{ $invoice->formattedTotal }}**
+
+ You can view your invoice details by clicking the button below.
+
+
+ HTML,
+ 'in_app_title' => 'Invoice paid',
+ 'in_app_body' => 'Your invoice #{{ $invoice->id }} has been successfully paid with total amount: {{ $invoice->formattedTotal }}.',
+ 'mail_enabled' => 'choice_on',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Notify me about successful payments',
+ 'in_app_url' => '{{ route("invoices.show", $invoice) }}',
+ ],
+ 'invoice_payment_failed' => [
+ 'subject' => 'Invoice payment failed',
+ 'body' => <<<'HTML'
+ # Invoice payment failed
+
+ Your invoice payment has failed.
+
+ Total amount: **{{ $invoice->formattedTotal }}**
+
+ Please pay the invoice to avoid service interruptions.
+
+
+ HTML,
+ 'in_app_title' => 'Invoice payment failed',
+ 'in_app_body' => 'Your invoice #{{ $invoice->id }} payment has failed. Please pay the invoice to avoid service interruptions.',
+ 'mail_enabled' => 'choice_on',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Alert me about payment failures',
+ 'in_app_url' => '{{ route("invoices.show", $invoice) }}',
+ ],
+ 'new_order_created' => [
+ 'subject' => 'New order created',
+ 'body' => <<<'HTML'
+ # New order created
+
+ A new order was created on your account.
+
+ **Order details**
+
+
+ | Item | Quantity | Price |
+ | :------: | :------: | :------: |
+ @foreach ($items as $item)
+ | {{ $item->product->name }} | {{ $item->quantity }} | {{ $item->formattedPrice }} |
+ @endforeach
+
+ HTML,
+ 'in_app_title' => 'New order created',
+ 'in_app_body' => 'A new order was created on your account.',
+ 'mail_enabled' => 'choice_on',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Send me order confirmations',
+ 'in_app_url' => '{{ route("services") }}',
+ ],
+ 'new_server_created' => [
+ 'subject' => 'Service activated',
+ 'body' => <<<'HTML'
+ # Service activated
+
+ Your service has been activated.
+
+ **Service details**
+ - Name: {{ $service->product->name }}
+
+ @isset($service->product->email_template)
+ **Service information**
+ {!! Str::markdown(Illuminate\View\Compilers\BladeCompiler::render($service->product->email_template, get_defined_vars()['__data'])) !!}
+ @endisset
+ HTML,
+ 'in_app_title' => 'Service activated',
+ 'in_app_body' => 'Your service {{ $service->product->name }} has been activated.',
+ 'mail_enabled' => 'force',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Notify me about new service activations',
+ 'in_app_url' => '{{ route("services.show", $service) }}',
+ ],
+ 'server_suspended' => [
+ 'subject' => 'Service suspended',
+ 'body' => <<<'HTML'
+ # Service suspended
+
+ Your service has been suspended due to a payment failure.
+
+ **Service details**
+ - Name: {{ $service->product->name }}
+
+ Please pay the invoice to reactivate the service.
+ HTML,
+ 'in_app_title' => 'Service suspended',
+ 'in_app_body' => 'Your service {{ $service->product->name }} has been suspended due to a payment failure. Please pay the invoice to reactivate the service.',
+ 'mail_enabled' => 'force',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Alert me about service suspensions',
+ 'in_app_url' => '{{ route("services.show", $service) }}',
+ ],
+ 'server_terminated' => [
+ 'subject' => 'Service terminated',
+ 'body' => <<<'HTML'
+ # Service terminated
+
+ Your service has been terminated.
+
+ **Service details**
+ - Name: {{ $service->product->name }}
+
+ Do you consider it a mistake?
+
+ HTML,
+ 'in_app_title' => 'Server terminated',
+ 'in_app_body' => 'Your server {{ $service->product->name }} has been terminated.',
+ 'mail_enabled' => 'force',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Alert me about service terminations',
+ 'in_app_url' => '{{ route("services.show", $service) }}',
+ ],
+ 'new_ticket_message' => [
+ 'subject' => '[Ticket #{{ $ticketMessage->ticket_id }}] New reply',
+ 'body' => <<<'HTML'
+ # New ticket reply
+
+ {{ $ticketMessage->user->name }} replied to your ticket.
+
+ **Message**
+ {!! Str::markdown($ticketMessage->message, [
+ 'html_input' => 'strip',
+ 'allow_unsafe_links' => false,
+ ]) !!}
+ HTML,
+ 'in_app_title' => 'New ticket reply',
+ 'in_app_body' => 'You have a new reply on your ticket #{{ $ticketMessage->ticket_id }}.',
+ 'mail_enabled' => 'choice_on',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Notify me about ticket replies',
+ 'in_app_url' => '{{ route("tickets.show", $ticketMessage->ticket_id) }}',
+ ],
+ 'email_verification' => [
+ 'subject' => 'Email verification',
+ 'body' => <<<'HTML'
+ # Email verification
+ Please verify your email address by clicking the link below.
+
+ This link will expire in 60 minutes.
+ If you did not create an account, you can ignore this email.
+ HTML,
+ 'mail_enabled' => 'force',
+ 'in_app_enabled' => 'never',
+ ],
+ 'password_reset' => [
+ 'subject' => 'Password reset',
+ 'body' => <<<'HTML'
+ # Password reset
+ You are receiving this email because we received a password reset request for your account.
+
+ **Reset password**
+
+
+ This password reset link will expire in 60 minutes.
+
+ If you did not request a password reset, no further action is required.
+ HTML,
+ 'mail_enabled' => 'force',
+ 'in_app_enabled' => 'never',
+ ],
+ 'service_cancellation_received' => [
+ 'subject' => 'Service cancellation received',
+ 'body' => <<<'HTML'
+ # Server Cancellation Received
+
+ We're sorry to see you go! Your server cancellation has been successfully received.
+
+ **Cancellation Details**
+ - Server: {{ $service->product->name }}
+ @if($cancellation->reason)
+ - Reason: {{ $cancellation->reason }}
+ @endif
+ - Requested at: {{ $cancellation->created_at->format('F j, Y, g:i A') }}
+
+ @if($cancellation->type === 'end_of_period')
+ Your server will remain active until {{ $service->expires_at->format('F j, Y') }} (end of your current billing period).
+ @else
+ Your server has been terminated immediately.
+ @endif
+
+ HTML,
+ 'in_app_title' => 'Service cancellation received',
+ 'in_app_body' => 'Your server cancellation has been successfully received.',
+ 'mail_enabled' => 'choice_on',
+ 'in_app_enabled' => 'choice_on',
+ 'edit_preference_message' => 'Notify me about service cancellations',
+ 'in_app_url' => '{{ route("services.show", $service) }}',
+ ],
+ ];
+
+ /**
+ * Run the database seeds.
+ */
+ public function run(): void
+ {
+
+ foreach (self::mapping as $key => $data) {
+ DB::table('notification_templates')->insertOrIgnore(
+ array_merge($data, ['key' => $key, 'enabled' => true])
+ );
+ }
+ }
+}
diff --git a/tests/Feature/Auth/LoginTest.php b/tests/Feature/Auth/LoginTest.php
new file mode 100644
index 0000000..365eb2f
--- /dev/null
+++ b/tests/Feature/Auth/LoginTest.php
@@ -0,0 +1,132 @@
+assertHasNoErrors()
+ ->assertSee('Email')
+ ->assertSee('Password');
+ }
+
+ /**
+ * Test if the user can't login with invalid credentials
+ */
+ public function test_cant_login_with_invalid_credentials()
+ {
+ Livewire::test(Login::class)
+ ->set('email', 'admin@paymenter')
+ ->set('password', 'password')
+ ->call('submit')
+ ->assertHasErrors('email');
+ }
+
+ /**
+ * Test if the user can login with valid credentials
+ */
+ public function test_can_login_with_valid_credentials()
+ {
+ User::factory()->create([
+ 'email' => 'tests@paymenter.org',
+ 'password' => Hash::make('password'),
+ ]);
+
+ Livewire::test(Login::class)
+ ->set('email', 'tests@paymenter.org')
+ ->set('password', 'password')
+ ->call('submit')
+ ->assertHasNoErrors()
+ ->assertRedirect(route('dashboard'));
+
+ $this->assertAuthenticated();
+ }
+
+ /**
+ * Test if the user can't login with valid credentials but wrong captcha
+ */
+ public function test_cant_login_with_captcha_enabled()
+ {
+ // Use captcha 6Lfi6dkkAAAAAMoS7Ya74nsiIQv840dQpPdDIYqT and 6Lfi6dkkAAAAAEdMgMWZB0VqTh6lpeBHYoxff78n
+ config([
+ 'settings.captcha' => 'recaptcha-v3',
+ 'settings.captcha_site_key' => '6Lfi6dkkAAAAAMoS7Ya74nsiIQv840dQpPdDIYqT',
+ 'settings.captcha_secret' => '6Lfi6dkkAAAAAEdMgMWZB0VqTh6lpeBHYoxff78n',
+ ]);
+
+ Livewire::test(Login::class)
+ ->set('email', 'test@paymenter.org')
+ ->set('password', 'password')
+ ->set('captcha', 'wrong')
+ ->call('submit')
+ ->assertHasErrors('captcha');
+
+ $this->assertGuest();
+
+ // Test without captcha
+ Livewire::test(Login::class)
+ ->set('email', 'test@paymenter.org')
+ ->set('password', 'password')
+ ->call('submit')
+ ->assertHasErrors('captcha');
+
+ $this->assertGuest();
+ }
+
+ /**
+ * Test if the user can login with valid credentials and redirect to intended url
+ */
+ public function test_can_login_with_valid_credentials_and_redirect_to_intended_url()
+ {
+ User::factory()->create([
+ 'email' => 'test@paymenter.org',
+ 'password' => Hash::make('password'),
+ 'role_id' => 1,
+ ]);
+
+ $this->get('/admin/settings')->assertRedirect(route('login'));
+
+ Livewire::test(Login::class)
+ ->set('email', 'test@paymenter.org')
+ ->set('password', 'password')
+ ->call('submit')
+ ->assertHasNoErrors()
+ ->assertRedirect('/admin/settings');
+
+ $this->assertAuthenticated();
+
+ $this->get('/admin/settings')->assertOk();
+ }
+
+ /**
+ * Test if user gets redirected to the login page if they are not authenticated
+ */
+ public function test_cant_go_to_admin_dashboard_if_not_authenticated()
+ {
+ $this->get('/admin/settings')->assertRedirect(route('login'));
+ }
+
+ /**
+ * Test if the user is authenticated but not an admin and tries to access the admin dashboard
+ */
+ public function test_cant_go_to_admin_dashboard_if_authenticated_but_not_admin()
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user)->withSession($this->loginUser($user))->get('/admin/settings')->assertForbidden();
+ }
+}