Jelajahi Sumber

Add a test email feature to the admin panel - Closes #307

Bubka 1 tahun lalu
induk
melakukan
88d37394d3

+ 18 - 0
app/Http/Controllers/SystemController.php

@@ -3,10 +3,12 @@
 namespace App\Http\Controllers;
 
 use App\Facades\Settings;
+use App\Notifications\TestEmailSettingNotification;
 use App\Services\ReleaseRadarService;
 use Illuminate\Http\Request;
 use Illuminate\Support\Carbon;
 use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
 
 class SystemController extends Controller
 {
@@ -58,4 +60,20 @@ class SystemController extends Controller
 
         return response()->json(['newRelease' => $release]);
     }
+
+    /**
+     * Send a test email.
+     *
+     * @return \Illuminate\Http\JsonResponse
+     */
+    public function testEmail(Request $request)
+    {
+        try {
+            $request->user()->notify(new TestEmailSettingNotification());
+        } catch (\Throwable $th) {
+            Log::error($th->getMessage());
+        }
+
+        return response()->json(['message' => 'Ok']);
+    }
 }

+ 64 - 0
app/Notifications/TestEmailSettingNotification.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Notifications;
+
+use Closure;
+use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Notifications\Notification;
+use Illuminate\Support\Facades\Lang;
+
+class TestEmailSettingNotification extends Notification
+{
+
+    // /**
+    //  * The callback that should be used to create the reset password URL.
+    //  *
+    //  * @var \Closure|null
+    //  */
+    // protected static ?Closure $createUrlCallback;
+
+    // /**
+    //  * The callback that should be used to build the mail message.
+    //  *
+    //  * @var \Closure|null
+    //  */
+    // protected static ?Closure $toMailCallback;
+
+    /**
+     * TestEmailSettingNotification constructor.
+     */
+    public function __construct()
+    {
+
+    }
+
+    /**
+     * Get the notification's delivery channels.
+     *
+     * @param  mixed  $notifiable
+     * @return array
+     */
+    public function via($notifiable)
+    {
+        return ['mail'];
+    }
+
+    /**
+     * Get the mail representation of the notification.
+     *
+     * @param  mixed  $notifiable
+     * @return \Illuminate\Notifications\Messages\MailMessage
+     */
+    public function toMail($notifiable)
+    {
+        return (new MailMessage)
+            ->subject(Lang::get('notifications.test_email_settings.subject'))
+            ->greeting(Lang::get('notifications.hello'))
+            ->line(
+                Lang::get('notifications.test_email_settings.reason')
+            )
+            ->line(
+                Lang::get('notifications.test_email_settings.success')
+            );
+    }
+}

+ 3 - 1
resources/js/icons.js

@@ -49,7 +49,8 @@ import {
 } from '@fortawesome/free-solid-svg-icons'
 
 import {
-    faStar
+    faStar,
+    faPaperPlane,
 } from '@fortawesome/free-regular-svg-icons'
 
 import {
@@ -105,6 +106,7 @@ library.add(
     faStar,
     faChevronRight,
     faOpenid,
+    faPaperPlane,
 );
 
 export default FontAwesomeIcon

+ 8 - 0
resources/js/services/systemService.js

@@ -17,6 +17,14 @@ export default {
      */
     getLastRelease(config = {}) {
         return webClient.get('latestRelease', { ...config })
+    },
+
+    /**
+     * 
+     * @returns Promise
+     */
+    sendTestEmail(config = {}) {
+        return webClient.post('testEmail', { ...config })
     }
     
 }

+ 31 - 0
resources/js/views/admin/AppSetup.vue

@@ -4,16 +4,19 @@
     import systemService from '@/services/systemService'
     import { useAppSettingsStore } from '@/stores/appSettings'
     import { useNotifyStore } from '@/stores/notify'
+    import { useUserStore } from '@/stores/user'
     import VersionChecker from '@/components/VersionChecker.vue'
     import CopyButton from '@/components/CopyButton.vue'
 
     const $2fauth = inject('2fauth')
+    const user = useUserStore()
     const notify = useNotifyStore()
     const appSettings = useAppSettingsStore()
     const returnTo = useStorage($2fauth.prefix + 'returnTo', 'accounts')
 
     const infos = ref()
     const listInfos = ref(null)
+    const isSendingTestEmail = ref(false)
 
     /**
      * Saves a setting on the backend
@@ -26,6 +29,18 @@
         })
     }
 
+    /**
+     * Sends a test email
+     */
+    function sendTestEmail() {
+        isSendingTestEmail.value = true;
+
+        systemService.sendTestEmail()
+        .finally(() => {
+            isSendingTestEmail.value = false;
+        })
+    }
+
     onBeforeRouteLeave((to) => {
         if (! to.name.startsWith('admin.')) {
             notify.clear()
@@ -53,6 +68,22 @@
                     <!-- Check for update -->
                     <FormCheckbox v-model="appSettings.checkForUpdate" @update:model-value="val => saveSetting('checkForUpdate', val)" fieldName="checkForUpdate" label="commons.check_for_update" help="commons.check_for_update_help" />
                     <VersionChecker />
+                    <div class="field">
+                        <!-- <h5 class="title is-5">{{ $t('settings.security') }}</h5> -->
+                        <label class="label"  v-html="$t('admin.forms.test_email.label')" />
+                        <p class="help" v-html="$t('admin.forms.test_email.help')" />
+                        <p class="help" v-html="$t('admin.forms.test_email.email_will_be_send_to_x', { email: user.email })" />
+                    </div>
+                    <div class="columns is-mobile is-vcentered">
+                        <div class="column is-narrow">
+                            <button type="button" :class="isSendingTestEmail ? 'is-loading' : ''" class="button is-link is-rounded is-small" @click="sendTestEmail">
+                                <span class="icon is-small">
+                                    <FontAwesomeIcon :icon="['far', 'paper-plane']" />
+                                </span>
+                                <span>{{ $t('commons.send') }}</span>
+                            </button>   
+                        </div>
+                    </div>                   
                     <h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
                     <!-- protect db -->
                     <FormCheckbox v-model="appSettings.useEncryption" @update:model-value="val => saveSetting('useEncryption', val)" fieldName="useEncryption" label="admin.forms.use_encryption.label" help="admin.forms.use_encryption.help" />

+ 5 - 0
resources/lang/en/admin.php

@@ -79,6 +79,11 @@ return [
         'is_admin' => [
             'label' => 'Is administrator',
             'help' => 'Give administrator rights to the user. Administrators have permissions to manage the whole app, i.e. settings and other users, but cannot generate password for a 2FA they don\'t own.'
+        ],
+        'test_email' => [
+            'label' => 'Email configuration test',
+            'help' => 'Send a test email to control your instance\'s email configuration. It is important to have a working configuration, otherwise users will not be able to request a reset password.',
+            'email_will_be_send_to_x' => 'The email will be send to <span class="is-family-code has-text-info">:email</span>',
         ]
     ],
 

+ 2 - 1
resources/lang/en/commons.php

@@ -79,5 +79,6 @@ return [
     'nothing' => 'nothing',
     'no_result' => 'No result',
     'information' => 'Information',
-    'permissions' => 'Permissions'
+    'permissions' => 'Permissions',
+    'send' => 'Send',
 ];

+ 23 - 0
resources/lang/en/notifications.php

@@ -0,0 +1,23 @@
+<?php
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Notifications Language Lines
+    |--------------------------------------------------------------------------
+    |
+    | The following language lines are used during authentication for various
+    | messages that we need to display to the user. You are free to modify
+    | these language lines according to your application's requirements.
+    |
+    */
+
+    'hello' => 'Hello',
+    'test_email_settings' => [
+        'subject' => '2FAuth test email',
+        'reason' => 'You are receiving this email because you requested a test email to validate the email settings of your 2FAuth instance.',
+        'success' => 'Good news, it works :)'
+    ],
+
+];

+ 1 - 0
routes/web.php

@@ -83,6 +83,7 @@ Route::get('refresh-csrf', function () {
  */
 Route::group(['middleware' => ['behind-auth', 'admin']], function () {   
     Route::get('infos', [SystemController::class, 'infos'])->name('system.infos');
+    Route::post('testEmail', [SystemController::class, 'testEmail'])->name('system.testEmail');
 });
 
 Route::get('latestRelease', [SystemController::class, 'latestRelease'])->name('system.latestRelease');

+ 36 - 3
tests/Feature/Http/SystemControllerTest.php

@@ -4,8 +4,9 @@ namespace Tests\Feature\Http;
 
 use App\Http\Controllers\SystemController;
 use App\Models\User;
+use App\Notifications\TestEmailSettingNotification;
 use App\Services\ReleaseRadarService;
-use Illuminate\Foundation\Testing\WithoutMiddleware;
+use Illuminate\Support\Facades\Notification;
 use PHPUnit\Framework\Attributes\CoversClass;
 use Tests\FeatureTestCase;
 
@@ -15,8 +16,6 @@ use Tests\FeatureTestCase;
 #[CoversClass(SystemController::class)]
 class SystemControllerTest extends FeatureTestCase
 {
-    //use WithoutMiddleware;
-
     /**
      * @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
      */
@@ -116,4 +115,38 @@ class SystemControllerTest extends FeatureTestCase
                 'newRelease' => 'new_release',
             ]);
     }
+
+    /**
+     * @test
+     */
+    public function test_testEmail_sends_a_notification()
+    {
+        Notification::fake();
+
+        $response = $this->actingAs($this->admin, 'web-guard')
+            ->json('POST', '/testEmail', []);
+
+        $response->assertStatus(200);
+
+        Notification::assertSentTo($this->admin, TestEmailSettingNotification::class);
+    }
+
+    /**
+     * @test
+     */
+    public function test_testEmail_returns_unauthorized()
+    {
+        $response = $this->json('GET', '/infos')
+            ->assertUnauthorized();
+    }
+
+    /**
+     * @test
+     */
+    public function test_testEmail_returns_forbidden()
+    {
+        $response = $this->actingAs($this->user, 'api-guard')
+            ->json('GET', '/infos')
+            ->assertForbidden();
+    }
 }