[#75] added option to test s3 connection and send test emails

This commit is contained in:
Gani Georgiev
2022-08-21 14:30:36 +03:00
parent 3f4f4cf031
commit 587cfc335c
49 changed files with 1539 additions and 838 deletions
@@ -146,7 +146,8 @@
}
confirm(`Do you really want to delete collection "${original?.name}" and all its records?`, () => {
return ApiClient.collections.delete(original?.id)
return ApiClient.collections
.delete(original?.id)
.then(() => {
hide();
addSuccessToast(`Successfully deleted collection "${original?.name}".`);
@@ -149,7 +149,7 @@
{:else if field.type === "url"}
URL address.
{:else if field.type === "file"}
FormData object.<br />
File object.<br />
Set to <code>null</code> to delete already uploaded file(s).
{:else if field.type === "relation"}
Relation record {field.options?.maxSelect > 1 ? "ids" : "id"}.
@@ -163,7 +163,7 @@
{:else if field.type === "url"}
URL address.
{:else if field.type === "file"}
FormData object.<br />
File object.<br />
Set to <code>null</code> to delete already uploaded file(s).
{:else if field.type === "relation"}
Relation record {field.options?.maxSelect > 1 ? "ids" : "id"}.
+5 -4
View File
@@ -47,10 +47,11 @@
clearList();
}
return ApiClient.records.getList(collection.id, page, 50, {
sort: sort,
filter: filter,
})
return ApiClient.records
.getList(collection.id, page, 50, {
sort: sort,
filter: filter,
})
.then((result) => {
isLoading = false;
records = records.concat(result.items);
@@ -6,6 +6,7 @@
import CommonHelper from "@/utils/CommonHelper";
import Field from "@/components/base/Field.svelte";
import Accordion from "@/components/base/Accordion.svelte";
import { onMount } from "svelte";
export let key;
export let title;
@@ -45,12 +46,12 @@
isEditorComponentLoading = false;
}
loadEditorComponent();
function copy(param) {
CommonHelper.copyToClipboard(param);
addInfoToast(`Copied ${param} to clipboard`, 2000);
}
loadEditorComponent();
</script>
<Accordion bind:this={accordion} on:expand on:collapse on:toggle {...$$restProps}>
@@ -108,18 +109,13 @@
<label for={uniqueId}>Body (HTML)</label>
{#if editorComponent && !isEditorComponentLoading}
<svelte:component
this={editorComponent}
id={uniqueId}
language="html"
bind:value={config.body}
/>
<svelte:component this={editorComponent} id={uniqueId} language="html" bind:value={config.body} />
{:else}
<textarea
id={uniqueId}
class="txt-mono"
spellcheck="false"
rows="12"
rows="14"
required
bind:value={config.body}
/>
@@ -0,0 +1,133 @@
<script>
import { createEventDispatcher, tick } from "svelte";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import { addErrorToast, addSuccessToast } from "@/stores/toasts";
import { setErrors } from "@/stores/errors";
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
import Field from "@/components/base/Field.svelte";
const dispatch = createEventDispatcher();
const formId = "email_test_" + CommonHelper.randomString(5);
const emailStorageKey = "last_email_test";
const testRequestKey = "email_test_request";
const templateOptions = [
{ label: '"Verification" template', value: "verification" },
{ label: '"Password reset" template', value: "password-reset" },
{ label: '"Confirm email change" template', value: "email-change" },
];
let panel;
let email = localStorage.getItem(emailStorageKey);
let template = templateOptions[0].value;
let isSubmitting = false;
let testTimeoutId = null;
$: canSubmit = !!email && !!template;
export function show(emailArg = "", templateArg = "") {
email = emailArg || localStorage.getItem(emailStorageKey);
template = templateArg || templateOptions[0].value;
setErrors({}); // reset any previous errors
panel?.show();
}
export function hide() {
clearTimeout(testTimeoutId);
return panel?.hide();
}
async function submit() {
if (!canSubmit || isSubmitting) {
return;
}
isSubmitting = true;
// store in local storage for later use
localStorage?.setItem(emailStorageKey, email);
// auto cancel the test request after 30sec
clearTimeout(testTimeoutId);
testTimeoutId = setTimeout(() => {
ApiClient.cancelRequest(testRequestKey);
addErrorToast("Test email send timeout.");
}, 30000);
try {
await ApiClient.settings.testEmail(email, template, {
$cancelKey: testRequestKey,
});
addSuccessToast("Successfully sent test email.");
dispatch("submit");
isSubmitting = false;
await tick();
hide();
} catch (err) {
isSubmitting = false;
ApiClient.errorResponseHandler(err);
}
clearTimeout(testTimeoutId);
}
</script>
<OverlayPanel
bind:this={panel}
class="overlay-panel-sm email-test-popup"
overlayClose={!isSubmitting}
escClose={!isSubmitting}
beforeHide={() => !isSubmitting}
popup
on:show
on:hide
>
<svelte:fragment slot="header">
<h4 class="center txt-break">Send test email</h4>
</svelte:fragment>
<form id={formId} autocomplete="off" on:submit|preventDefault={() => submit()}>
<Field class="form-field required" name="template" let:uniqueId>
{#each templateOptions as option (option.value)}
<div class="form-field-block">
<input
type="radio"
name="template"
id={uniqueId + option.value}
value={option.value}
bind:group={template}
/>
<label for={uniqueId + option.value}>{option.label}</label>
</div>
{/each}
</Field>
<Field class="form-field required m-0" name="email" let:uniqueId>
<label for={uniqueId}>To email address</label>
<!-- svelte-ignore a11y-autofocus -->
<input type="email" id={uniqueId} autofocus required bind:value={email} />
</Field>
</form>
<svelte:fragment slot="footer">
<button type="button" class="btn btn-secondary" on:click={hide} disabled={isSubmitting}>Close</button>
<button
type="submit"
form={formId}
class="btn btn-expanded"
class:btn-loading={isSubmitting}
disabled={!canSubmit || isSubmitting}
on:click={() => submit()}
>
<i class="ri-mail-send-line" />
<span class="txt">Send</span>
</button>
</svelte:fragment>
</OverlayPanel>
@@ -120,7 +120,7 @@
<OverlayPanel
bind:this={panel}
class="full-width-popup import-popup"
class="full-width-popup import-popup"
overlayClose={false}
escClose={!isImporting}
beforeHide={() => !isImporting}
@@ -117,7 +117,7 @@
<i
class="ri-information-line link-hint"
use:tooltip={{
text: `This is useful to prevent making accidental schema changes on a production environment.`,
text: `This could prevent making accidental schema changes when in production environment.`,
position: "right",
}}
/>
@@ -50,7 +50,8 @@
$: canImport = !isLoadingOldCollections && isValid && hasChanges;
$: idReplacableCollections = newCollections.filter((collection) => {
let old = CommonHelper.findByKey(oldCollections, "name", collection.name) ||
let old =
CommonHelper.findByKey(oldCollections, "name", collection.name) ||
CommonHelper.findByKey(oldCollections, "id", collection.id);
if (!old) {
@@ -149,7 +150,8 @@
function replaceIds() {
for (let collection of newCollections) {
const old = CommonHelper.findByKey(oldCollections, "name", collection.name) ||
const old =
CommonHelper.findByKey(oldCollections, "name", collection.name) ||
CommonHelper.findByKey(oldCollections, "id", collection.id);
if (!old) {
@@ -327,7 +329,8 @@
<span class="label label-warning list-label">Changed</span>
<div class="inline-flex flex-gap-5">
{#if pair.old.name !== pair.new.name}
<strong class="txt-strikethrough txt-hint">{pair.old.name}</strong>
<strong class="txt-strikethrough txt-hint">{pair.old.name}</strong
>
<i class="ri-arrow-right-line txt-sm" />
{/if}
<strong>
+23 -9
View File
@@ -12,6 +12,7 @@
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
import EmailTemplateAccordion from "@/components/settings/EmailTemplateAccordion.svelte";
import EmailTestPopup from "@/components/settings/EmailTestPopup.svelte";
const tlsOptions = [
{ label: "Auto (StartTLS)", value: false },
@@ -20,6 +21,7 @@
$pageTitle = "Mail settings";
let testPopup;
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
@@ -217,6 +219,7 @@
<div class="flex">
<div class="flex-fill" />
{#if hasChanges}
<button
type="button"
@@ -226,18 +229,29 @@
>
<span class="txt">Cancel</span>
</button>
<button
type="submit"
class="btn btn-expanded"
class:btn-loading={isSaving}
disabled={!hasChanges || isSaving}
on:click={() => save()}
>
<span class="txt">Save changes</span>
</button>
{:else}
<button
type="button"
class="btn btn-expanded btn-outline"
on:click={() => testPopup?.show()}
>
<i class="ri-mail-check-line" />
<span class="txt">Send test email</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
class:btn-loading={isSaving}
disabled={!hasChanges || isSaving}
on:click={() => save()}
>
<span class="txt">Save changes</span>
</button>
</div>
{/if}
</form>
</div>
</PageWrapper>
<EmailTestPopup bind:this={testPopup} />
+74 -8
View File
@@ -4,7 +4,7 @@
import CommonHelper from "@/utils/CommonHelper";
import { pageTitle } from "@/stores/app";
import { setErrors } from "@/stores/errors";
import { addSuccessToast } from "@/stores/toasts";
import { removeAllToasts, addWarningToast, addSuccessToast } from "@/stores/toasts";
import tooltip from "@/actions/tooltip";
import PageWrapper from "@/components/base/PageWrapper.svelte";
import Field from "@/components/base/Field.svelte";
@@ -13,10 +13,15 @@
$pageTitle = "Files storage";
const testRequestKey = "s3_test_request";
let originalFormSettings = {};
let formSettings = {};
let isLoading = false;
let isSaving = false;
let isTesting = false;
let testS3Error = null;
let testS3TimeoutId = null;
$: initialHash = JSON.stringify(originalFormSettings);
@@ -37,6 +42,24 @@
isLoading = false;
}
async function testS3() {
testS3Error = null;
if (!formSettings.s3.enabled) {
return; // nothing to test
}
isTesting = true;
try {
await ApiClient.settings.testS3({ $cancelKey: testRequestKey });
} catch (err) {
testS3Error = err;
}
isTesting = false;
}
async function save() {
if (isSaving || !hasChanges) {
return;
@@ -44,27 +67,49 @@
isSaving = true;
// auto cancel the test request after 30sec
clearTimeout(testS3TimeoutId);
testS3TimeoutId = setTimeout(() => {
ApiClient.cancelRequest(testRequestKey);
addErrorToast("S3 test connection timeout.");
}, 30000);
try {
ApiClient.cancelRequest(testRequestKey);
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
init(settings);
setErrors({});
addSuccessToast("Successfully saved files storage settings.");
await init(settings);
removeAllToasts();
if (testS3Error) {
addWarningToast("Successfully saved but failed to establish S3 connection.");
} else {
addSuccessToast("Successfully saved files storage settings.");
}
} catch (err) {
ApiClient.errorResponseHandler(err);
}
clearTimeout(testS3TimeoutId);
isSaving = false;
}
function init(settings = {}) {
async function init(settings = {}) {
formSettings = {
s3: settings?.s3 || {},
};
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
await testS3();
}
function reset() {
async function reset() {
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
await testS3();
}
</script>
@@ -136,7 +181,7 @@
{#if formSettings.s3.enabled}
<div class="grid" transition:slide|local={{ duration: 150 }}>
<div class="col-lg-12">
<div class="col-lg-6">
<Field class="form-field required" name="s3.endpoint" let:uniqueId>
<label for={uniqueId}>Endpoint</label>
<input
@@ -147,7 +192,7 @@
/>
</Field>
</div>
<div class="col-lg-6">
<div class="col-lg-3">
<Field class="form-field required" name="s3.bucket" let:uniqueId>
<label for={uniqueId}>Bucket</label>
<input
@@ -158,7 +203,7 @@
/>
</Field>
</div>
<div class="col-lg-6">
<div class="col-lg-3">
<Field class="form-field required" name="s3.region" let:uniqueId>
<label for={uniqueId}>Region</label>
<input
@@ -216,6 +261,26 @@
<div class="flex">
<div class="flex-fill" />
{#if formSettings.s3?.enabled && !hasChanges && !isSaving}
{#if isTesting}
<span class="loader loader-sm" />
{:else if testS3Error}
<div
class="label label-sm label-warning entrance-right"
use:tooltip={testS3Error.data?.message}
>
<i class="ri-error-warning-line txt-warning" />
<span class="txt">Failed to establish S3 connection</span>
</div>
{:else}
<div class="label label-sm label-success entrance-right">
<i class="ri-checkbox-circle-line txt-success" />
<span class="txt">S3 connected successfully</span>
</div>
{/if}
{/if}
{#if hasChanges}
<button
type="button"
@@ -226,6 +291,7 @@
<span class="txt">Cancel</span>
</button>
{/if}
<button
type="submit"
class="btn btn-expanded"
@@ -1,4 +1,5 @@
<script>
import PocketBase from "pocketbase";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import FullPage from "@/components/base/FullPage.svelte";
@@ -19,8 +20,11 @@
isLoading = true;
// init a custom client to avoid interfering with the admin state
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
try {
await ApiClient.users.confirmEmailChange(params?.token, password);
await client.users.confirmEmailChange(params?.token, password);
success = true;
} catch (err) {
ApiClient.errorResponseHandler(err);
@@ -45,13 +49,13 @@
</button>
{:else}
<form on:submit|preventDefault={submit}>
<div class="content txt-center m-b-sm">
<h4 class="m-b-xs">
<div class="content txt-center m-b-base">
<h5>
Type your password to confirm changing your email address
{#if newEmail}
to <strong class="txt-nowrap">{newEmail}</strong>
{/if}
</h4>
</h5>
</div>
<Field class="form-field required" name="password" let:uniqueId>
@@ -1,4 +1,5 @@
<script>
import PocketBase from "pocketbase";
import ApiClient from "@/utils/ApiClient";
import CommonHelper from "@/utils/CommonHelper";
import FullPage from "@/components/base/FullPage.svelte";
@@ -20,8 +21,11 @@
isLoading = true;
// init a custom client to avoid interfering with the admin state
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
try {
await ApiClient.users.confirmPasswordReset(params?.token, newPassword, newPasswordConfirm);
await client.users.confirmPasswordReset(params?.token, newPassword, newPasswordConfirm);
success = true;
} catch (err) {
ApiClient.errorResponseHandler(err);
@@ -46,13 +50,13 @@
</button>
{:else}
<form on:submit|preventDefault={submit}>
<div class="content txt-center m-b-sm">
<h4 class="m-b-xs">
<div class="content txt-center m-b-base">
<h5>
Reset your user password
{#if email}
for <strong>{email}</strong>
{/if}
</h4>
</h5>
</div>
<Field class="form-field required" name="password" let:uniqueId>
@@ -1,5 +1,5 @@
<script>
import ApiClient from "@/utils/ApiClient";
import PocketBase from "pocketbase";
import FullPage from "@/components/base/FullPage.svelte";
export let params;
@@ -12,11 +12,13 @@
async function send() {
isLoading = true;
// init a custom client to avoid interfering with the admin state
const client = new PocketBase(import.meta.env.PB_BACKEND_URL);
try {
await ApiClient.users.confirmVerification(params?.token);
await client.users.confirmVerification(params?.token);
success = true;
} catch (err) {
console.warn(err);
success = false;
}