[#75] added option to test s3 connection and send test emails
This commit is contained in:
@@ -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"}.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user