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 +
+ +
+ + Go to invoice + +
+ + @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. + +
+ + View Invoice + +
+ 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. + +
+ + Pay Invoice + +
+ 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? +
+ + Contact us + +
+ 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. +
+ + Verify email + +
+ 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** +
+ + 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(); + } +}