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

This commit is contained in:
Bubka 2024-02-26 15:06:26 +01:00
parent 04078b09aa
commit 88d37394d3
10 changed files with 191 additions and 5 deletions

View File

@ -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 @@ public function latestRelease(Request $request, ReleaseRadarService $releaseRada
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']);
}
}

View File

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

View File

@ -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

View File

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

View File

@ -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" />

View File

@ -79,6 +79,11 @@
'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>',
]
],

View File

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

View File

@ -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 :)'
],
];

View File

@ -83,6 +83,7 @@
*/
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');

View File

@ -4,8 +4,9 @@
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 @@
#[CoversClass(SystemController::class)]
class SystemControllerTest extends FeatureTestCase
{
//use WithoutMiddleware;
/**
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
*/
@ -116,4 +115,38 @@ public function test_latestrelease_runs_manual_scan()
'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();
}
}