merge v0.23.0-rc changes
This commit is contained in:
@@ -1,46 +0,0 @@
|
||||
<script>
|
||||
import AuthProviderPanel from "@/components/settings/AuthProviderPanel.svelte";
|
||||
|
||||
export let provider = {};
|
||||
export let config = {};
|
||||
|
||||
let providerPanel;
|
||||
</script>
|
||||
|
||||
<div class="provider-card">
|
||||
<figure class="provider-logo">
|
||||
{#if provider.logo}
|
||||
<img src="{import.meta.env.BASE_URL}images/oauth2/{provider.logo}" alt="{provider.title} logo" />
|
||||
{/if}
|
||||
</figure>
|
||||
<div class="title">{provider.title}</div>
|
||||
<em class="txt-hint txt-sm m-r-auto">({provider.key.slice(0, -4)})</em>
|
||||
{#if config.enabled}
|
||||
<div class="label label-success">Enabled</div>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-circle btn-hint btn-transparent"
|
||||
aria-label="Provider settings"
|
||||
on:click={() => {
|
||||
providerPanel?.show(
|
||||
provider,
|
||||
Object.assign({}, config, {
|
||||
enabled: config.clientId ? config.enabled : true,
|
||||
pkce: config.clientId ? config.pkce : null,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<i class="ri-settings-4-line" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<AuthProviderPanel
|
||||
bind:this={providerPanel}
|
||||
on:submit={(e) => {
|
||||
if (e.detail[provider.key]) {
|
||||
config = e.detail[provider.key];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -1,136 +0,0 @@
|
||||
<script>
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const formId = "provider_popup_" + CommonHelper.randomString(5);
|
||||
|
||||
let panel;
|
||||
let provider = {};
|
||||
let config = {};
|
||||
let isSubmitting = false;
|
||||
let initialHash = "";
|
||||
|
||||
$: hasChanges = JSON.stringify(config) != initialHash;
|
||||
|
||||
export function show(showProvider, showConfig) {
|
||||
setErrors({}); // reset any previous errors
|
||||
|
||||
provider = Object.assign({}, showProvider);
|
||||
config = Object.assign({ enabled: true }, showConfig);
|
||||
initialHash = JSON.stringify(config);
|
||||
|
||||
panel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return panel?.hide();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
isSubmitting = true;
|
||||
|
||||
try {
|
||||
const data = {};
|
||||
data[provider.key] = CommonHelper.filterRedactedProps(config);
|
||||
|
||||
const result = await ApiClient.settings.update(data);
|
||||
|
||||
setErrors({});
|
||||
|
||||
addSuccessToast("Successfully updated provider settings.");
|
||||
|
||||
dispatch("submit", result);
|
||||
|
||||
hide();
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
}
|
||||
|
||||
isSubmitting = false;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
for (let k in config) {
|
||||
config[k] = CommonHelper.zeroValue(config[k]);
|
||||
}
|
||||
|
||||
// set to false only for the oidc providers
|
||||
// (@todo remove after the refactoring)
|
||||
if (provider.key?.startsWith("oidc")) {
|
||||
config.pkce = false;
|
||||
} else {
|
||||
config.pkce = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel bind:this={panel} overlayClose={!isSubmitting} escClose={!isSubmitting} on:show on:hide>
|
||||
<svelte:fragment slot="header">
|
||||
<h4 class="center txt-break">{provider.title || provider.key} provider</h4>
|
||||
</svelte:fragment>
|
||||
|
||||
<form id={formId} autocomplete="off" on:submit|preventDefault={() => submit()}>
|
||||
<div class="flex m-b-base">
|
||||
<Field class="form-field form-field-toggle m-b-0" name="{provider.key}.enabled" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={config.enabled} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-transparent btn-hint m-l-auto" on:click={clear}>
|
||||
<span class="txt">Clear all fields</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Field
|
||||
class="form-field {config.enabled ? 'required' : ''}"
|
||||
name="{provider.key}.clientId"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>Client ID</label>
|
||||
<input type="text" id={uniqueId} bind:value={config.clientId} required={config.enabled} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
class="form-field {config.enabled ? 'required' : ''}"
|
||||
name="{provider.key}.clientSecret"
|
||||
let:uniqueId
|
||||
>
|
||||
<label for={uniqueId}>Client secret</label>
|
||||
<RedactedPasswordInput bind:value={config.clientSecret} id={uniqueId} required={config.enabled} />
|
||||
</Field>
|
||||
|
||||
{#if provider.optionsComponent}
|
||||
<div class="col-lg-12">
|
||||
<svelte:component
|
||||
this={provider.optionsComponent}
|
||||
key={provider.key}
|
||||
bind:config
|
||||
{...provider.optionsComponentProps || {}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-transparent" on:click={hide} disabled={isSubmitting}>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
form={formId}
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSubmitting}
|
||||
disabled={!hasChanges || isSubmitting}
|
||||
>
|
||||
<span class="txt">Save changes</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -88,7 +88,7 @@
|
||||
the backup and will restart the application process.
|
||||
</p>
|
||||
<p>
|
||||
This means that on success all of your data (including app settings, users, admins, etc.) will
|
||||
This means that on success all of your data (including app settings, users, superusers, etc.) will
|
||||
be replaced with the ones from the backup.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script>
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import { addSuccessToast, addErrorToast } from "@/stores/toasts";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { addSuccessToast, addErrorToast } from "@/stores/toasts";
|
||||
import { confirm } from "@/stores/confirmation";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const backupRequestKey = "upload_backup";
|
||||
@@ -13,20 +14,42 @@
|
||||
let fileInput;
|
||||
let isUploading = false;
|
||||
|
||||
async function upload(e) {
|
||||
if (isUploading || !e?.target?.files?.length) {
|
||||
function resetSelectedFile() {
|
||||
if (fileInput) {
|
||||
fileInput.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function uploadConfirm(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
confirm(
|
||||
`Note that we don't perform validations for the uploaded backup files. Proceed with caution and only if you trust the source.\n\n` +
|
||||
`Do you really want to upload "${file.name}"?`,
|
||||
() => {
|
||||
upload(file);
|
||||
},
|
||||
() => {
|
||||
resetSelectedFile();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function upload(file) {
|
||||
if (isUploading || !file) {
|
||||
return;
|
||||
}
|
||||
|
||||
isUploading = true;
|
||||
|
||||
const data = new FormData();
|
||||
data.set("file", e.target.files[0]);
|
||||
data.set("file", file);
|
||||
|
||||
try {
|
||||
await ApiClient.backups.upload(data, { requestKey: backupRequestKey });
|
||||
isUploading = false;
|
||||
|
||||
dispatch("success");
|
||||
addSuccessToast("Successfully uploaded a new backup.");
|
||||
} catch (err) {
|
||||
@@ -39,6 +62,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetSelectedFile();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -58,4 +83,12 @@
|
||||
<i class="ri-upload-cloud-line" />
|
||||
</button>
|
||||
|
||||
<input bind:this={fileInput} type="file" accept="application/zip" class="hidden" on:change={upload} />
|
||||
<input
|
||||
bind:this={fileInput}
|
||||
type="file"
|
||||
accept="application/zip"
|
||||
class="hidden"
|
||||
on:change={(e) => {
|
||||
uploadConfirm(e?.target?.files?.[0]);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
isDownloading[name] = true;
|
||||
|
||||
try {
|
||||
const token = await ApiClient.getAdminFileToken();
|
||||
const url = ApiClient.backups.getDownloadUrl(token, name);
|
||||
const token = await ApiClient.getSuperuserFileToken();
|
||||
const url = ApiClient.backups.getDownloadURL(token, name);
|
||||
CommonHelper.download(url);
|
||||
} catch (err) {
|
||||
if (!err.isAbort) {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import { errors } from "@/stores/errors";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { scale } from "svelte/transition";
|
||||
|
||||
export let formSettings;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty($errors?.batch);
|
||||
|
||||
$: isEnabled = !!formSettings.batch?.enabled;
|
||||
</script>
|
||||
|
||||
<Accordion single>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-archive-stack-line"></i>
|
||||
<span class="txt">Batch API</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if isEnabled}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
|
||||
{#if hasErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-sm" name="batch.enabled" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={formSettings.batch.enabled} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-lg-4">
|
||||
<Field class="form-field {isEnabled ? 'required' : ''}" name="batch.maxRequests" let:uniqueId>
|
||||
<label for={uniqueId}>Max allowed batch requests</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
min="0"
|
||||
step="1"
|
||||
required={isEnabled}
|
||||
bind:value={formSettings.batch.maxRequests}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<Field class="form-field {isEnabled ? 'required' : ''}" name="batch.timeout" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Max processing time (in seconds)</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
min="0"
|
||||
step="1"
|
||||
required={isEnabled}
|
||||
bind:value={formSettings.batch.timeout}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<Field class="form-field" name="batch.maxBodySize" let:uniqueId>
|
||||
<label for={uniqueId}>Max body size (in bytes)</label>
|
||||
<input
|
||||
type="number"
|
||||
id={uniqueId}
|
||||
min="0"
|
||||
step="1"
|
||||
placeholder="Default to 128MB"
|
||||
value={formSettings.batch.maxBodySize || ""}
|
||||
on:input={(e) => (formSettings.batch.maxBodySize = e.target.value << 0)}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -1,181 +0,0 @@
|
||||
<script context="module">
|
||||
let cachedEditorComponent;
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { scale } from "svelte/transition";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import { errors, removeError } from "@/stores/errors";
|
||||
import { addInfoToast } from "@/stores/toasts";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
|
||||
export let key;
|
||||
export let title;
|
||||
export let config = {};
|
||||
|
||||
let accordion;
|
||||
let editorComponent = cachedEditorComponent;
|
||||
let isEditorComponentLoading = false;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty(CommonHelper.getNestedVal($errors, key));
|
||||
|
||||
$: if (!config.enabled) {
|
||||
removeError(key);
|
||||
}
|
||||
|
||||
export function expand() {
|
||||
accordion?.expand();
|
||||
}
|
||||
|
||||
export function collapse() {
|
||||
accordion?.collapse();
|
||||
}
|
||||
|
||||
export function collapseSiblings() {
|
||||
accordion?.collapseSiblings();
|
||||
}
|
||||
|
||||
async function loadEditorComponent() {
|
||||
if (editorComponent || isEditorComponentLoading) {
|
||||
return; // already loaded or in the process
|
||||
}
|
||||
|
||||
isEditorComponentLoading = true;
|
||||
|
||||
editorComponent = (await import("@/components/base/CodeEditor.svelte")).default;
|
||||
|
||||
cachedEditorComponent = editorComponent;
|
||||
|
||||
isEditorComponentLoading = false;
|
||||
}
|
||||
|
||||
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}>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-draft-line" />
|
||||
<span class="txt">{title}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field required" name="{key}.subject" let:uniqueId>
|
||||
<label for={uniqueId}>Subject</label>
|
||||
<input type="text" id={uniqueId} bind:value={config.subject} spellcheck="false" required />
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_NAME}")}
|
||||
>
|
||||
{"{APP_NAME}"}
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_URL}")}
|
||||
>
|
||||
{"{APP_URL}"}
|
||||
</button>.
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field class="form-field required" name="{key}.actionUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Action URL</label>
|
||||
<input type="text" id={uniqueId} bind:value={config.actionUrl} spellcheck="false" required />
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_NAME}")}
|
||||
>
|
||||
{"{APP_NAME}"}
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_URL}")}
|
||||
>
|
||||
{"{APP_URL}"}
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
title="Required parameter"
|
||||
on:click={() => copy("{TOKEN}")}
|
||||
>
|
||||
{"{TOKEN}"}
|
||||
</button>.
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
<Field class="form-field m-0 required" name="{key}.body" let:uniqueId>
|
||||
<label for={uniqueId}>Body (HTML)</label>
|
||||
|
||||
{#if editorComponent && !isEditorComponentLoading}
|
||||
<svelte:component this={editorComponent} id={uniqueId} language="html" bind:value={config.body} />
|
||||
{:else}
|
||||
<textarea
|
||||
id={uniqueId}
|
||||
class="txt-mono"
|
||||
spellcheck="false"
|
||||
rows="14"
|
||||
required
|
||||
bind:value={config.body}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="help-block">
|
||||
Available placeholder parameters:
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_NAME}")}
|
||||
>
|
||||
{"{APP_NAME}"}
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{APP_URL}")}
|
||||
>
|
||||
{"{APP_URL}"}
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => copy("{TOKEN}")}
|
||||
>
|
||||
{"{TOKEN}"}
|
||||
</button>,
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
title="Required parameter"
|
||||
on:click={() => copy("{ACTION_URL}")}
|
||||
>
|
||||
{"{ACTION_URL}"}
|
||||
</button>.
|
||||
</div>
|
||||
</Field>
|
||||
</Accordion>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script>
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { addErrorToast, addSuccessToast } from "@/stores/toasts";
|
||||
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";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
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" },
|
||||
{ label: "Verification", value: "verification" },
|
||||
{ label: "Password reset", value: "password-reset" },
|
||||
{ label: "Confirm email change", value: "email-change" },
|
||||
{ label: "OTP", value: "otp" },
|
||||
{ label: "Login alert", value: "login-alert" },
|
||||
];
|
||||
|
||||
let panel;
|
||||
let collectionIdOrName = "";
|
||||
let email = localStorage.getItem(emailStorageKey);
|
||||
let template = templateOptions[0].value;
|
||||
let isSubmitting = false;
|
||||
@@ -27,7 +30,8 @@
|
||||
|
||||
$: canSubmit = !!email && !!template;
|
||||
|
||||
export function show(emailArg = "", templateArg = "") {
|
||||
export function show(collectionArg = "", emailArg = "", templateArg = "") {
|
||||
collectionIdOrName = collectionArg || "_superusers";
|
||||
email = emailArg || localStorage.getItem(emailStorageKey);
|
||||
template = templateArg || templateOptions[0].value;
|
||||
|
||||
@@ -59,7 +63,7 @@
|
||||
}, 30000);
|
||||
|
||||
try {
|
||||
await ApiClient.settings.testEmail(email, template, {
|
||||
await ApiClient.settings.testEmail(collectionIdOrName, email, template, {
|
||||
$cancelKey: testRequestKey,
|
||||
});
|
||||
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
deletedFieldNames.push(old.name + ".*");
|
||||
} else {
|
||||
// add only deleted fields
|
||||
const schema = Array.isArray(old.schema) ? old.schema : [];
|
||||
for (const field of schema) {
|
||||
if (!CommonHelper.findByKey(imported.schema, "id", field.id)) {
|
||||
const fields = Array.isArray(old.fields) ? old.fields : [];
|
||||
for (const field of fields) {
|
||||
if (!CommonHelper.findByKey(imported.fields, "id", field.id)) {
|
||||
deletedFieldNames.push(`${old.name}.${field.name} (${field.id})`);
|
||||
}
|
||||
}
|
||||
@@ -86,11 +86,11 @@
|
||||
if (deletedFieldNames.length) {
|
||||
confirm(
|
||||
`Do you really want to delete the following collection fields and their related records data:\n- ${deletedFieldNames.join(
|
||||
"\n- "
|
||||
"\n- ",
|
||||
)}`,
|
||||
() => {
|
||||
submit();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
submit();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { pageTitle, appName, hideControls } from "@/stores/app";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import { appName, hideControls, pageTitle } from "@/stores/app";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import BatchAccordion from "@/components/settings/BatchAccordion.svelte";
|
||||
import TrustedProxyAccordion from "@/components/settings/TrustedProxyAccordion.svelte";
|
||||
import RateLimitAccordion from "@/components/settings/RateLimitAccordion.svelte";
|
||||
|
||||
$pageTitle = "Application settings";
|
||||
|
||||
@@ -15,6 +18,7 @@
|
||||
let isLoading = false;
|
||||
let isSaving = false;
|
||||
let initialHash = "";
|
||||
let healthData = {};
|
||||
|
||||
$: initialHash = JSON.stringify(originalFormSettings);
|
||||
|
||||
@@ -22,12 +26,22 @@
|
||||
|
||||
loadSettings();
|
||||
|
||||
async function loadHealthData() {
|
||||
try {
|
||||
healthData = ((await ApiClient.health.check()) || {})?.data || {};
|
||||
} catch (err) {
|
||||
console.warn("Health check failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const settings = (await ApiClient.settings.getAll()) || {};
|
||||
init(settings);
|
||||
|
||||
await loadHealthData();
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
}
|
||||
@@ -45,6 +59,9 @@
|
||||
try {
|
||||
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
|
||||
init(settings);
|
||||
|
||||
await loadHealthData();
|
||||
|
||||
addSuccessToast("Successfully saved application settings.");
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
@@ -59,6 +76,9 @@
|
||||
|
||||
formSettings = {
|
||||
meta: settings?.meta || {},
|
||||
batch: settings.batch || {},
|
||||
trustedProxy: settings.trustedProxy || { headers: [] },
|
||||
rateLimits: settings.rateLimits || { tags: [] },
|
||||
};
|
||||
|
||||
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
|
||||
@@ -98,49 +118,62 @@
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="meta.appUrl" let:uniqueId>
|
||||
<Field class="form-field required" name="meta.appURL" let:uniqueId>
|
||||
<label for={uniqueId}>Application URL</label>
|
||||
<input type="text" id={uniqueId} required bind:value={formSettings.meta.appUrl} />
|
||||
<input type="text" id={uniqueId} required bind:value={formSettings.meta.appURL} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field class="form-field form-field-toggle" name="meta.hideControls" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={formSettings.meta.hideControls} />
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Hide collection create and edit controls</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: `This could prevent making accidental schema changes when in production environment.`,
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
|
||||
<div class="col-lg-12 flex">
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasChanges}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent btn-hint"
|
||||
disabled={isSaving}
|
||||
on:click={() => reset()}
|
||||
>
|
||||
<span class="txt">Cancel</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 class="col-lg-12">
|
||||
<div class="accordions">
|
||||
<TrustedProxyAccordion bind:formSettings {healthData} />
|
||||
<RateLimitAccordion bind:formSettings />
|
||||
<BatchAccordion bind:formSettings />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<Field class="form-field form-field-toggle m-0" name="meta.hideControls" let:uniqueId>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={uniqueId}
|
||||
bind:checked={formSettings.meta.hideControls}
|
||||
/>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Hide collection create and edit controls</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: `This could prevent making accidental schema changes when in production environment.`,
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex m-t-base">
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasChanges}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent btn-hint"
|
||||
disabled={isSaving}
|
||||
on:click={() => reset()}
|
||||
>
|
||||
<span class="txt">Cancel</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>
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import AuthProviderCard from "@/components/settings/AuthProviderCard.svelte";
|
||||
import providersList from "@/providers.js";
|
||||
|
||||
$pageTitle = "Auth providers";
|
||||
|
||||
let isLoading = false;
|
||||
let formSettings = {};
|
||||
|
||||
$: enabledProviders = providersList.filter((provider) => formSettings[provider.key]?.enabled);
|
||||
|
||||
$: disabledProviders = providersList.filter((provider) => !formSettings[provider.key]?.enabled);
|
||||
|
||||
loadSettings();
|
||||
|
||||
async function loadSettings() {
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const result = (await ApiClient.settings.getAll()) || {};
|
||||
initSettings(result);
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function initSettings(data) {
|
||||
data = data || {};
|
||||
formSettings = {};
|
||||
|
||||
for (const provider of providersList) {
|
||||
formSettings[provider.key] = Object.assign({ enabled: false }, data[provider.key]);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsSidebar />
|
||||
|
||||
<PageWrapper>
|
||||
<header class="page-header">
|
||||
<nav class="breadcrumbs">
|
||||
<div class="breadcrumb-item">Settings</div>
|
||||
<div class="breadcrumb-item">{$pageTitle}</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="panel">
|
||||
<h6 class="m-b-base">Manage the allowed users OAuth2 sign-in/sign-up methods.</h6>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="loader" />
|
||||
{:else}
|
||||
<div class="grid grid-sm">
|
||||
{#each enabledProviders as provider (provider.key)}
|
||||
<div class="col-lg-6">
|
||||
<AuthProviderCard {provider} bind:config={formSettings[provider.key]} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if enabledProviders.length > 0 && disabledProviders.length > 0}
|
||||
<hr />
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-sm">
|
||||
{#each disabledProviders as provider (provider.key)}
|
||||
<div class="col-lg-6">
|
||||
<AuthProviderCard {provider} bind:config={formSettings[provider.key]} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
@@ -3,7 +3,7 @@
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import { removeError } from "@/stores/errors";
|
||||
import { removeError, setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
@@ -62,6 +62,8 @@
|
||||
try {
|
||||
const settings = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
|
||||
|
||||
setErrors({});
|
||||
|
||||
await refreshList();
|
||||
|
||||
init(settings);
|
||||
@@ -139,7 +141,7 @@
|
||||
transition:slide={{ duration: 150 }}
|
||||
>
|
||||
<Field class="form-field form-field-toggle m-t-base m-b-0" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} required bind:checked={enableAutoBackups} />
|
||||
<input type="checkbox" id={uniqueId} bind:checked={enableAutoBackups} />
|
||||
<label for={uniqueId}>Enable auto backups</label>
|
||||
</Field>
|
||||
|
||||
@@ -274,7 +276,7 @@
|
||||
|
||||
{#if hasChanges}
|
||||
<button
|
||||
type="submit"
|
||||
type="button"
|
||||
class="btn btn-hint btn-transparent"
|
||||
disabled={!hasChanges || isSaving}
|
||||
on:click={() => reset()}
|
||||
|
||||
@@ -35,10 +35,13 @@
|
||||
|
||||
collections = CommonHelper.sortCollections(collections);
|
||||
|
||||
// delete timestamps
|
||||
for (let collection of collections) {
|
||||
// delete timestamps
|
||||
delete collection.created;
|
||||
delete collection.updated;
|
||||
|
||||
// unset oauth2 providers
|
||||
delete collection.oauth2?.providers;
|
||||
}
|
||||
|
||||
selectAll();
|
||||
@@ -167,7 +170,7 @@
|
||||
<span class="txt">Copy</span>
|
||||
</button>
|
||||
|
||||
<pre class="code-wrapper">{@html schema}</pre>
|
||||
<pre class="code-wrapper">{schema}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script>
|
||||
import { tick } from "svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import ImportPopup from "@/components/settings/ImportPopup.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { addErrorToast } from "@/stores/toasts";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import { addErrorToast } from "@/stores/toasts";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import ImportPopup from "@/components/settings/ImportPopup.svelte";
|
||||
import { tick } from "svelte";
|
||||
|
||||
$pageTitle = "Import collections";
|
||||
|
||||
@@ -69,15 +69,15 @@
|
||||
}
|
||||
|
||||
// check for matching schema fields
|
||||
const oldSchema = Array.isArray(old.schema) ? old.schema : [];
|
||||
const newSchema = Array.isArray(collection.schema) ? collection.schema : [];
|
||||
for (const field of newSchema) {
|
||||
const oldFieldById = CommonHelper.findByKey(oldSchema, "id", field.id);
|
||||
const oldFields = Array.isArray(old.fields) ? old.fields : [];
|
||||
const newFields = Array.isArray(collection.fields) ? collection.fields : [];
|
||||
for (const field of newFields) {
|
||||
const oldFieldById = CommonHelper.findByKey(oldFields, "id", field.id);
|
||||
if (oldFieldById) {
|
||||
continue; // no need to do any replacements
|
||||
}
|
||||
|
||||
const oldFieldByName = CommonHelper.findByKey(oldSchema, "name", field.name);
|
||||
const oldFieldByName = CommonHelper.findByKey(oldFields, "name", field.name);
|
||||
if (oldFieldByName && field.id != oldFieldByName.id) {
|
||||
return true;
|
||||
}
|
||||
@@ -93,10 +93,13 @@
|
||||
|
||||
try {
|
||||
oldCollections = await ApiClient.collections.getFullList(200);
|
||||
// delete timestamps
|
||||
for (let collection of oldCollections) {
|
||||
// delete timestamps
|
||||
delete collection.created;
|
||||
delete collection.updated;
|
||||
|
||||
// unset oauth2 providers
|
||||
delete collection.oauth2?.providers;
|
||||
}
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
@@ -150,7 +153,7 @@
|
||||
delete collection.updated;
|
||||
|
||||
// merge fields with duplicated ids
|
||||
collection.schema = CommonHelper.filterDuplicatesByKey(collection.schema);
|
||||
collection.fields = CommonHelper.filterDuplicatesByKey(collection.fields);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,10 +172,10 @@
|
||||
collection.id = replacedId;
|
||||
|
||||
// replace field ids
|
||||
const oldSchema = Array.isArray(old.schema) ? old.schema : [];
|
||||
const newSchema = Array.isArray(collection.schema) ? collection.schema : [];
|
||||
for (const field of newSchema) {
|
||||
const oldField = CommonHelper.findByKey(oldSchema, "name", field.name);
|
||||
const oldFields = Array.isArray(old.fields) ? old.fields : [];
|
||||
const newFields = Array.isArray(collection.fields) ? collection.fields : [];
|
||||
for (const field of newFields) {
|
||||
const oldField = CommonHelper.findByKey(oldFields, "name", field.name);
|
||||
if (oldField && oldField.id) {
|
||||
field.id = oldField.id;
|
||||
}
|
||||
@@ -180,12 +183,12 @@
|
||||
|
||||
// update references
|
||||
for (let ref of newCollections) {
|
||||
if (!Array.isArray(ref.schema)) {
|
||||
if (!Array.isArray(ref.fields)) {
|
||||
continue;
|
||||
}
|
||||
for (let field of ref.schema) {
|
||||
if (field.options?.collectionId && field.options?.collectionId === originalId) {
|
||||
field.options.collectionId = replacedId;
|
||||
for (let field of ref.fields) {
|
||||
if (field.collectionId && field.collectionId === originalId) {
|
||||
field.collectionId = replacedId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script>
|
||||
import { slide } from "svelte/transition";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import RedactedPasswordInput from "@/components/base/RedactedPasswordInput.svelte";
|
||||
import EmailTestPopup from "@/components/settings/EmailTestPopup.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
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";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
const tlsOptions = [
|
||||
{ label: "Auto (StartTLS)", value: false },
|
||||
@@ -31,6 +30,7 @@
|
||||
let formSettings = {};
|
||||
let isLoading = false;
|
||||
let isSaving = false;
|
||||
let maskPassword = false;
|
||||
let showMoreOptions = false;
|
||||
|
||||
$: initialHash = JSON.stringify(originalFormSettings);
|
||||
@@ -82,6 +82,8 @@
|
||||
}
|
||||
|
||||
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
|
||||
|
||||
maskPassword = !!formSettings.smtp.username;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -134,37 +136,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordions">
|
||||
{#if !formSettings.meta.verificationTemplate.hidden}
|
||||
<EmailTemplateAccordion
|
||||
single
|
||||
key="meta.verificationTemplate"
|
||||
title={'Default "Verification" email template'}
|
||||
bind:config={formSettings.meta.verificationTemplate}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !formSettings.meta.resetPasswordTemplate.hidden}
|
||||
<EmailTemplateAccordion
|
||||
single
|
||||
key="meta.resetPasswordTemplate"
|
||||
title={'Default "Password reset" email template'}
|
||||
bind:config={formSettings.meta.resetPasswordTemplate}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !formSettings.meta.confirmEmailChangeTemplate.hidden}
|
||||
<EmailTemplateAccordion
|
||||
single
|
||||
key="meta.confirmEmailChangeTemplate"
|
||||
title={'Default "Confirm email change" email template'}
|
||||
bind:config={formSettings.meta.confirmEmailChangeTemplate}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-sm" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} required bind:checked={formSettings.smtp.enabled} />
|
||||
<label for={uniqueId}>
|
||||
@@ -219,6 +190,7 @@
|
||||
<label for={uniqueId}>Password</label>
|
||||
<RedactedPasswordInput
|
||||
id={uniqueId}
|
||||
bind:mask={maskPassword}
|
||||
bind:value={formSettings.smtp.password}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
<script>
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { pageTitle } from "@/stores/app";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import PageWrapper from "@/components/base/PageWrapper.svelte";
|
||||
import SettingsSidebar from "@/components/settings/SettingsSidebar.svelte";
|
||||
import TokenField from "@/components/settings/TokenField.svelte";
|
||||
|
||||
const recordTokensList = [
|
||||
{ key: "recordAuthToken", label: "Auth record authentication token" },
|
||||
{ key: "recordVerificationToken", label: "Auth record email verification token" },
|
||||
{ key: "recordPasswordResetToken", label: "Auth record password reset token" },
|
||||
{ key: "recordEmailChangeToken", label: "Auth record email change token" },
|
||||
{ key: "recordFileToken", label: "Records protected file access token" },
|
||||
];
|
||||
|
||||
const adminTokensList = [
|
||||
{ key: "adminAuthToken", label: "Admins auth token" },
|
||||
{ key: "adminPasswordResetToken", label: "Admins password reset token" },
|
||||
{ key: "adminFileToken", label: "Admins protected file access token" },
|
||||
];
|
||||
|
||||
$pageTitle = "Token options";
|
||||
|
||||
let originalFormSettings = {};
|
||||
let formSettings = {};
|
||||
let isLoading = false;
|
||||
let isSaving = false;
|
||||
|
||||
$: initialHash = JSON.stringify(originalFormSettings);
|
||||
|
||||
$: hasChanges = initialHash != JSON.stringify(formSettings);
|
||||
|
||||
loadSettings();
|
||||
|
||||
async function loadSettings() {
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const result = (await ApiClient.settings.getAll()) || {};
|
||||
initSettings(result);
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (isSaving || !hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
|
||||
try {
|
||||
const result = await ApiClient.settings.update(CommonHelper.filterRedactedProps(formSettings));
|
||||
initSettings(result);
|
||||
addSuccessToast("Successfully saved tokens options.");
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
}
|
||||
|
||||
isSaving = false;
|
||||
}
|
||||
|
||||
function initSettings(data) {
|
||||
data = data || {};
|
||||
formSettings = {};
|
||||
|
||||
const tokensList = recordTokensList.concat(adminTokensList);
|
||||
|
||||
for (const listItem of tokensList) {
|
||||
formSettings[listItem.key] = {
|
||||
duration: data[listItem.key]?.duration || 0,
|
||||
};
|
||||
}
|
||||
|
||||
originalFormSettings = JSON.parse(JSON.stringify(formSettings));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
formSettings = JSON.parse(JSON.stringify(originalFormSettings || {}));
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsSidebar />
|
||||
|
||||
<PageWrapper>
|
||||
<header class="page-header">
|
||||
<nav class="breadcrumbs">
|
||||
<div class="breadcrumb-item">Settings</div>
|
||||
<div class="breadcrumb-item">{$pageTitle}</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="wrapper">
|
||||
<form class="panel" autocomplete="off" on:submit|preventDefault={save}>
|
||||
<div class="content m-b-sm txt-xl">
|
||||
<p>Adjust common token options.</p>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="loader" />
|
||||
{:else}
|
||||
<h3 class="section-title">Record tokens</h3>
|
||||
{#each recordTokensList as token (token.key)}
|
||||
<TokenField
|
||||
key={token.key}
|
||||
label={token.label}
|
||||
bind:duration={formSettings[token.key].duration}
|
||||
bind:secret={formSettings[token.key].secret}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<hr />
|
||||
|
||||
<h3 class="section-title">Admin tokens</h3>
|
||||
{#each adminTokensList as token (token.key)}
|
||||
<TokenField
|
||||
key={token.key}
|
||||
label={token.label}
|
||||
bind:duration={formSettings[token.key].duration}
|
||||
bind:secret={formSettings[token.key].secret}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex-fill" />
|
||||
{#if hasChanges}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent btn-hint"
|
||||
disabled={isSaving}
|
||||
on:click={() => reset()}
|
||||
>
|
||||
<span class="txt">Cancel</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>
|
||||
@@ -0,0 +1,237 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import AutocompleteInput from "@/components/base/AutocompleteInput.svelte";
|
||||
import { errors, setErrors } from "@/stores/errors";
|
||||
import { collections, loadCollections } from "@/stores/collections";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { scale } from "svelte/transition";
|
||||
|
||||
export let formSettings;
|
||||
|
||||
const basePredefinedTags = [
|
||||
{ value: "*:list" },
|
||||
{ value: "*:view" },
|
||||
{ value: "*:create" },
|
||||
{ value: "*:update" },
|
||||
{ value: "*:delete" },
|
||||
{ value: "*:file" },
|
||||
{ value: "*:listAuthMethods" },
|
||||
{ value: "*:authRefresh" },
|
||||
{ value: "*:auth" },
|
||||
{ value: "*:authWithPassword" },
|
||||
{ value: "*:authWithOAuth2" },
|
||||
{ value: "*:authWithOTP" },
|
||||
{ value: "*:requestOTP" },
|
||||
{ value: "*:requestPasswordReset" },
|
||||
{ value: "*:confirmPasswordReset" },
|
||||
{ value: "*:requestVerification" },
|
||||
{ value: "*:confirmVerification" },
|
||||
{ value: "*:requestEmailChange" },
|
||||
{ value: "*:confirmEmailChange" },
|
||||
];
|
||||
|
||||
let predefinedTags = basePredefinedTags;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty($errors?.rateLimits);
|
||||
|
||||
loadPredefinedTags();
|
||||
|
||||
async function loadPredefinedTags() {
|
||||
await loadCollections();
|
||||
|
||||
predefinedTags = [];
|
||||
|
||||
for (let collection of $collections) {
|
||||
if (collection.system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
predefinedTags.push({ value: collection.name + ":list" });
|
||||
predefinedTags.push({ value: collection.name + ":view" });
|
||||
|
||||
if (collection.type != "view") {
|
||||
predefinedTags.push({ value: collection.name + ":create" });
|
||||
predefinedTags.push({ value: collection.name + ":update" });
|
||||
predefinedTags.push({ value: collection.name + ":delete" });
|
||||
}
|
||||
|
||||
if (collection.type == "auth") {
|
||||
predefinedTags.push({ value: collection.name + ":listAuthMethods" });
|
||||
predefinedTags.push({ value: collection.name + ":authRefresh" });
|
||||
predefinedTags.push({ value: collection.name + ":auth" });
|
||||
predefinedTags.push({ value: collection.name + ":authWithPassword" });
|
||||
predefinedTags.push({ value: collection.name + ":authWithOAuth2" });
|
||||
predefinedTags.push({ value: collection.name + ":authWithOTP" });
|
||||
predefinedTags.push({ value: collection.name + ":requestOTP" });
|
||||
predefinedTags.push({ value: collection.name + ":requestPasswordReset" });
|
||||
predefinedTags.push({ value: collection.name + ":confirmPasswordReset" });
|
||||
predefinedTags.push({ value: collection.name + ":requestVerification" });
|
||||
predefinedTags.push({ value: collection.name + ":confirmVerification" });
|
||||
predefinedTags.push({ value: collection.name + ":requestEmailChange" });
|
||||
predefinedTags.push({ value: collection.name + ":confirmEmailChange" });
|
||||
}
|
||||
|
||||
if (collection.fields.find((f) => f.type == "file")) {
|
||||
predefinedTags.push({ value: collection.name + ":file" });
|
||||
}
|
||||
}
|
||||
|
||||
predefinedTags = predefinedTags.concat(basePredefinedTags);
|
||||
}
|
||||
|
||||
function newRule() {
|
||||
setErrors({}); // reset
|
||||
|
||||
if (!Array.isArray(formSettings.rateLimits.rules)) {
|
||||
formSettings.rateLimits.rules = [];
|
||||
}
|
||||
|
||||
formSettings.rateLimits.rules.push({
|
||||
label: "",
|
||||
maxRequests: 300,
|
||||
duration: 10,
|
||||
});
|
||||
|
||||
formSettings.rateLimits.rules = formSettings.rateLimits.rules;
|
||||
|
||||
if (formSettings.rateLimits.rules.length == 1) {
|
||||
formSettings.rateLimits.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
function removeRule(i) {
|
||||
setErrors({}); // reset
|
||||
|
||||
formSettings.rateLimits.rules.splice(i, 1);
|
||||
formSettings.rateLimits.rules = formSettings.rateLimits.rules;
|
||||
|
||||
if (!formSettings.rateLimits.rules.length) {
|
||||
formSettings.rateLimits.enabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Accordion single>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-pulse-fill"></i>
|
||||
<span class="txt">Rate limiting</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if hasErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if formSettings.rateLimits.enabled}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<Field class="form-field form-field-toggle m-b-xs" name="rateLimits.enabled" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={formSettings.rateLimits.enabled} />
|
||||
<label for={uniqueId}>Enable</label>
|
||||
</Field>
|
||||
|
||||
{#if !CommonHelper.isEmpty(formSettings.rateLimits.rules)}
|
||||
<table class="rate-limit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rate limit label</th>
|
||||
<th>Max requests (per IP)</th>
|
||||
<th>Interval (in seconds)</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each formSettings.rateLimits.rules || [] as rule, i}
|
||||
<tr class="rate-limit-row">
|
||||
<td class="col-tag">
|
||||
<Field class="form-field" name={"rateLimits.rules." + i + ".label"} inlineError>
|
||||
<AutocompleteInput
|
||||
required
|
||||
placeholder="tag (users:create) or path (/api/)"
|
||||
options={predefinedTags}
|
||||
bind:value={rule.label}
|
||||
/>
|
||||
</Field>
|
||||
</td>
|
||||
<td class="col-requests">
|
||||
<Field
|
||||
class="form-field"
|
||||
name={"rateLimits.rules." + i + ".maxRequests"}
|
||||
inlineError
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
placeholder="Max requests*"
|
||||
min="1"
|
||||
step="1"
|
||||
bind:value={rule.maxRequests}
|
||||
/>
|
||||
</Field>
|
||||
</td>
|
||||
<td class="col-burst">
|
||||
<Field
|
||||
class="form-field"
|
||||
name={"rateLimits.rules." + i + ".duration"}
|
||||
inlineError
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
required
|
||||
placeholder="Interval*"
|
||||
min="1"
|
||||
step="1"
|
||||
bind:value={rule.duration}
|
||||
/>
|
||||
</Field>
|
||||
</td>
|
||||
<td class="col-action">
|
||||
<button
|
||||
type="button"
|
||||
title="Remove rule"
|
||||
aria-label="Remove rule"
|
||||
class="btn btn-xs btn-circle btn-hint btn-transparent"
|
||||
on:click={() => removeRule(i)}
|
||||
>
|
||||
<i class="ri-close-line"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
|
||||
<div class="flex m-t-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary m-r-auto"
|
||||
class:btn-danger={$errors?.rateLimits?.rules?.message}
|
||||
on:click={() => newRule()}
|
||||
>
|
||||
<i class="ri-add-line"></i>
|
||||
<span class="txt">Add rate limit rule</span>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href={import.meta.env.PB_RATE_LIMIT_DOCS}
|
||||
class="txt-nowrap txt-sm link-hint"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<em>Learn more about the rate limit rules</em>
|
||||
</a>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -19,8 +19,10 @@
|
||||
|
||||
let testTimeoutId = null;
|
||||
let testDebounceId = null;
|
||||
let maskSecret = false;
|
||||
|
||||
$: if (originalConfig?.enabled) {
|
||||
refreshMaskSecret();
|
||||
testConnectionWithDebounce(100);
|
||||
}
|
||||
|
||||
@@ -29,6 +31,10 @@
|
||||
removeError(configKey);
|
||||
}
|
||||
|
||||
function refreshMaskSecret() {
|
||||
maskSecret = !!originalConfig?.accessKey;
|
||||
}
|
||||
|
||||
function testConnectionWithDebounce(timeout) {
|
||||
isTesting = true;
|
||||
clearTimeout(testDebounceId);
|
||||
@@ -119,7 +125,12 @@
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="{configKey}.secret" let:uniqueId>
|
||||
<label for={uniqueId}>Secret</label>
|
||||
<RedactedPasswordInput id={uniqueId} required bind:value={config.secret} />
|
||||
<RedactedPasswordInput
|
||||
required
|
||||
id={uniqueId}
|
||||
bind:mask={maskSecret}
|
||||
bind:value={config.secret}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import { link } from "svelte-spa-router";
|
||||
import active from "svelte-spa-router/active";
|
||||
import PageSidebar from "@/components/base/PageSidebar.svelte";
|
||||
import { hideControls } from "@/stores/app";
|
||||
import { link } from "svelte-spa-router";
|
||||
import active from "svelte-spa-router/active";
|
||||
</script>
|
||||
|
||||
<PageSidebar class="settings-sidebar">
|
||||
@@ -63,34 +63,5 @@
|
||||
<span class="txt">Import collections</span>
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<div class="sidebar-title">Authentication</div>
|
||||
<a
|
||||
href="/settings/auth-providers"
|
||||
class="sidebar-list-item"
|
||||
use:active={{ path: "/settings/auth-providers/?.*" }}
|
||||
use:link
|
||||
>
|
||||
<i class="ri-lock-password-line" aria-hidden="true" />
|
||||
<span class="txt">Auth providers</span>
|
||||
</a>
|
||||
<a
|
||||
href="/settings/tokens"
|
||||
class="sidebar-list-item"
|
||||
use:active={{ path: "/settings/tokens/?.*" }}
|
||||
use:link
|
||||
>
|
||||
<i class="ri-key-line" aria-hidden="true" />
|
||||
<span class="txt">Token options</span>
|
||||
</a>
|
||||
<a
|
||||
href="/settings/admins"
|
||||
class="sidebar-list-item"
|
||||
use:active={{ path: "/settings/admins/?.*" }}
|
||||
use:link
|
||||
>
|
||||
<i class="ri-shield-user-line" aria-hidden="true" />
|
||||
<span class="txt">Admins</span>
|
||||
</a>
|
||||
</div>
|
||||
</PageSidebar>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<script>
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let key;
|
||||
export let label;
|
||||
export let duration;
|
||||
export let secret;
|
||||
</script>
|
||||
|
||||
<Field class="form-field required" name="{key}.duration" let:uniqueId>
|
||||
<label for={uniqueId}>{label} duration (in seconds)</label>
|
||||
<input type="number" id={uniqueId} required bind:value={duration} />
|
||||
<div class="help-block">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="link-primary"
|
||||
class:txt-success={!!secret}
|
||||
on:click={() => {
|
||||
// toggle
|
||||
if (secret) {
|
||||
secret = undefined;
|
||||
} else {
|
||||
secret = CommonHelper.randomSecret(50);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Invalidate all previously issued tokens
|
||||
</span>
|
||||
</div>
|
||||
</Field>
|
||||
@@ -0,0 +1,172 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Accordion from "@/components/base/Accordion.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import MultipleValueInput from "@/components/base/MultipleValueInput.svelte";
|
||||
import ObjectSelect from "@/components/base/ObjectSelect.svelte";
|
||||
import { errors } from "@/stores/errors";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { scale } from "svelte/transition";
|
||||
|
||||
const commonProxyHeaders = ["X-Forward-For", "Fly-Client-IP", "CF-Connecting-IP"];
|
||||
|
||||
export let formSettings;
|
||||
export let healthData;
|
||||
|
||||
let initialSettingsHash = "";
|
||||
|
||||
$: settingsHash = JSON.stringify(formSettings);
|
||||
|
||||
$: if (initialSettingsHash != settingsHash) {
|
||||
initialSettingsHash = settingsHash;
|
||||
}
|
||||
|
||||
$: hasChanges = initialSettingsHash != settingsHash;
|
||||
|
||||
$: hasErrors = !CommonHelper.isEmpty($errors?.trustedProxy);
|
||||
|
||||
$: isEnabled = !CommonHelper.isEmpty(formSettings.trustedProxy.headers);
|
||||
|
||||
$: suggestedProxyHeaders = !healthData.possibleProxyHeader
|
||||
? commonProxyHeaders
|
||||
: [healthData.possibleProxyHeader].concat(
|
||||
commonProxyHeaders.filter((h) => h != healthData.possibleProxyHeader),
|
||||
);
|
||||
|
||||
function setHeader(val) {
|
||||
formSettings.trustedProxy.headers = [val];
|
||||
}
|
||||
|
||||
const ipOptions = [
|
||||
{ label: "Use leftmost IP", value: true },
|
||||
{ label: "Use rightmost IP", value: false },
|
||||
];
|
||||
</script>
|
||||
|
||||
<Accordion single>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="inline-flex">
|
||||
<i class="ri-route-line"></i>
|
||||
<span class="txt">User IP proxy headers</span>
|
||||
{#if !isEnabled && healthData.possibleProxyHeader}
|
||||
<i
|
||||
class="ri-alert-line txt-sm txt-warning"
|
||||
use:tooltip={"Detected proxy header.\nIt is recommend to list it as trusted."}
|
||||
/>
|
||||
{:else if isEnabled && !hasChanges && !formSettings.trustedProxy.headers.includes(healthData.possibleProxyHeader)}
|
||||
<i
|
||||
class="ri-alert-line txt-sm txt-hint"
|
||||
use:tooltip={"The configured proxy header doesn't match with the detected one."}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-fill" />
|
||||
|
||||
{#if isEnabled}
|
||||
<span class="label label-success">Enabled</span>
|
||||
{:else}
|
||||
<span class="label">Disabled</span>
|
||||
{/if}
|
||||
|
||||
{#if hasErrors}
|
||||
<i
|
||||
class="ri-error-warning-fill txt-danger"
|
||||
transition:scale={{ duration: 150, start: 0.7 }}
|
||||
use:tooltip={{ text: "Has errors", position: "left" }}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="alert alert-info m-b-sm">
|
||||
<div class="content">
|
||||
<div class="inline-flex flex-gap-5">
|
||||
<span>Resolved user IP:</span>
|
||||
<strong>{healthData.realIP || "N/A"}</strong>
|
||||
<i
|
||||
class="ri-information-line txt-sm link-hint"
|
||||
use:tooltip={"Must show your actual IP.\nIf not, set the correct proxy header."}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
<div class="inline-flex flex-gap-5">
|
||||
<span>Detected proxy header:</span>
|
||||
<strong>{healthData.possibleProxyHeader || "N/A"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content m-b-sm">
|
||||
<p>
|
||||
When PocketBase is deployed on platforms like Fly or it is accessible through proxies such as
|
||||
NGINX, requests from different users will originate from the same IP address (the IP of the proxy
|
||||
connecting to your PocketBase app).
|
||||
</p>
|
||||
<p>
|
||||
In this case to retrieve the actual user IP (used for rate limiting, logging, etc.) you need to
|
||||
properly configure your proxy and list below the trusted headers that PocketBase could use to
|
||||
extract the user IP.
|
||||
</p>
|
||||
<p class="txt-bold">When using such proxy, to avoid spoofing it is recommended to:</p>
|
||||
<ul class="m-t-0 txt-bold">
|
||||
<li>use headers that are controlled only by the proxy and cannot be manually set by the users</li>
|
||||
<li>make sure that the PocketBase server can be accessed only through the proxy</li>
|
||||
</ul>
|
||||
<p>You can clear the headers field if PocketBase is not deployed behind a proxy.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-sm">
|
||||
<div class="col-lg-9">
|
||||
<Field class="form-field m-b-0" name="trustedProxy.headers" let:uniqueId>
|
||||
<label for={uniqueId}>Trusted proxy headers</label>
|
||||
<MultipleValueInput
|
||||
id={uniqueId}
|
||||
placeholder="Leave empty to disable"
|
||||
bind:value={formSettings.trustedProxy.headers}
|
||||
/>
|
||||
<div class="form-field-addon">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-hint btn-transparent btn-clear"
|
||||
class:hidden={CommonHelper.isEmpty(formSettings.trustedProxy.headers)}
|
||||
on:click={() => (formSettings.trustedProxy.headers = [])}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="help-block">
|
||||
<p>
|
||||
Comma separated list of headers such as:
|
||||
{#each suggestedProxyHeaders as header}
|
||||
<button
|
||||
type="button"
|
||||
class="label label-sm link-primary txt-mono"
|
||||
on:click={() => setHeader(header)}
|
||||
>
|
||||
{header}
|
||||
</button>
|
||||
{/each}
|
||||
</p>
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<Field class="form-field m-0" name="trustedProxy.useLeftmostIP" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">IP priority selection</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "This is in case the proxy returns more than 1 IP as header value. The rightmost IP is usually considered to be the more trustworthy but this could vary depending on the proxy.",
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<ObjectSelect
|
||||
items={ipOptions}
|
||||
bind:keyOfSelected={formSettings.trustedProxy.useLeftmostIP}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</Accordion>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script>
|
||||
import AppleSecretPopup from "@/components/settings/providers/AppleSecretPopup.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let config = {};
|
||||
|
||||
let generatorPopup;
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary btn-provider-{key}"
|
||||
on:click={() => generatorPopup?.show({ clientId: config.clientId })}
|
||||
>
|
||||
<i class="ri-key-line" />
|
||||
<span class="txt">Generate secret</span>
|
||||
</button>
|
||||
|
||||
<AppleSecretPopup
|
||||
bind:this={generatorPopup}
|
||||
on:submit={(e) => {
|
||||
config.clientSecret = e.detail?.secret || "";
|
||||
}}
|
||||
/>
|
||||
@@ -1,152 +0,0 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import ApiClient from "@/utils/ApiClient";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
import { addSuccessToast } from "@/stores/toasts";
|
||||
import { setErrors } from "@/stores/errors";
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import OverlayPanel from "@/components/base/OverlayPanel.svelte";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const formId = "apple_secret_" + CommonHelper.randomString(5);
|
||||
|
||||
const maxDuration = 15777000; // 6 months
|
||||
|
||||
let panel;
|
||||
let clientId;
|
||||
let teamId;
|
||||
let keyId;
|
||||
let privateKey;
|
||||
let duration;
|
||||
let isSubmitting = false;
|
||||
|
||||
$: canSubmit = true;
|
||||
|
||||
export function show(config = {}) {
|
||||
clientId = config.clientId || "";
|
||||
teamId = config.teamId || "";
|
||||
keyId = config.keyId || "";
|
||||
privateKey = config.privateKey || "";
|
||||
duration = config.duration || maxDuration;
|
||||
|
||||
setErrors({}); // reset any previous errors
|
||||
|
||||
panel?.show();
|
||||
}
|
||||
|
||||
export function hide() {
|
||||
return panel?.hide();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
isSubmitting = true;
|
||||
|
||||
try {
|
||||
const result = await ApiClient.settings.generateAppleClientSecret(
|
||||
clientId,
|
||||
teamId,
|
||||
keyId,
|
||||
privateKey.trim(),
|
||||
duration
|
||||
);
|
||||
|
||||
isSubmitting = false;
|
||||
|
||||
addSuccessToast("Successfully generated client secret.");
|
||||
|
||||
dispatch("submit", result);
|
||||
|
||||
panel?.hide();
|
||||
} catch (err) {
|
||||
ApiClient.error(err);
|
||||
}
|
||||
|
||||
isSubmitting = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<OverlayPanel
|
||||
bind:this={panel}
|
||||
overlayClose={!isSubmitting}
|
||||
escClose={!isSubmitting}
|
||||
beforeHide={() => !isSubmitting}
|
||||
popup
|
||||
on:show
|
||||
on:hide
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
<h4 class="center txt-break">Generate Apple client secret</h4>
|
||||
</svelte:fragment>
|
||||
|
||||
<form id={formId} autocomplete="off" on:submit|preventDefault={() => submit()}>
|
||||
<div class="grid">
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="clientId" let:uniqueId>
|
||||
<label for={uniqueId}>Client ID</label>
|
||||
<input type="text" id={uniqueId} bind:value={clientId} required />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="teamId" let:uniqueId>
|
||||
<label for={uniqueId}>Team ID</label>
|
||||
<input type="text" id={uniqueId} bind:value={teamId} required />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="keyId" let:uniqueId>
|
||||
<label for={uniqueId}>Key ID</label>
|
||||
<input type="text" id={uniqueId} bind:value={keyId} required />
|
||||
</Field>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<Field class="form-field required" name="duration" let:uniqueId>
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Duration (in seconds)</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: `Max ${maxDuration} seconds (~${
|
||||
(maxDuration / (60 * 60 * 24 * 30)) << 0
|
||||
} months).`,
|
||||
position: "top",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<input type="text" id={uniqueId} max={maxDuration} bind:value={duration} required />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field class="form-field required" name="privateKey" let:uniqueId>
|
||||
<label for={uniqueId}>Private key</label>
|
||||
<textarea
|
||||
id={uniqueId}
|
||||
required
|
||||
rows="8"
|
||||
placeholder={"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"}
|
||||
bind:value={privateKey}
|
||||
/>
|
||||
<div class="help-block">
|
||||
The key is not stored on the server and it is used only for generating the signed JWT.
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<button type="button" class="btn btn-transparent" on:click={hide} disabled={isSubmitting}
|
||||
>Close</button
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
form={formId}
|
||||
class="btn btn-expanded"
|
||||
class:btn-loading={isSubmitting}
|
||||
disabled={!canSubmit || isSubmitting}
|
||||
>
|
||||
<i class="ri-key-line" />
|
||||
<span class="txt">Generate and set secret</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</OverlayPanel>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script>
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let config = {};
|
||||
</script>
|
||||
|
||||
<div class="section-title">Azure AD endpoints</div>
|
||||
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Auth URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.authUrl} required={config.enabled} />
|
||||
<div class="help-block">
|
||||
Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/authorize`}
|
||||
</div>
|
||||
</Field>
|
||||
<Field class="form-field {config.enabled ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Token URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={config.enabled} />
|
||||
<div class="help-block">
|
||||
Eg. {`https://login.microsoftonline.com/YOUR_DIRECTORY_TENANT_ID/oauth2/v2.0/token`}
|
||||
</div>
|
||||
</Field>
|
||||
@@ -1,54 +0,0 @@
|
||||
<script>
|
||||
import tooltip from "@/actions/tooltip";
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
import CommonHelper from "@/utils/CommonHelper";
|
||||
|
||||
export let key = "";
|
||||
export let config = {};
|
||||
|
||||
$: isRequired = !!config.enabled;
|
||||
|
||||
if (CommonHelper.isEmpty(config.pkce)) {
|
||||
config.pkce = true;
|
||||
}
|
||||
|
||||
if (!config.displayName) {
|
||||
config.displayName = "OIDC";
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.displayName" let:uniqueId>
|
||||
<label for={uniqueId}>Display name</label>
|
||||
<input type="text" id={uniqueId} bind:value={config.displayName} required={isRequired} />
|
||||
</Field>
|
||||
|
||||
<div class="section-title">Endpoints</div>
|
||||
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Auth URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.authUrl} required={isRequired} />
|
||||
</Field>
|
||||
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Token URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={isRequired} />
|
||||
</Field>
|
||||
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.userApiUrl" let:uniqueId>
|
||||
<label for={uniqueId}>User API URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.userApiUrl} required={isRequired} />
|
||||
</Field>
|
||||
|
||||
<Field class="form-field" name="{key}.pkce" let:uniqueId>
|
||||
<input type="checkbox" id={uniqueId} bind:checked={config.pkce} />
|
||||
<label for={uniqueId}>
|
||||
<span class="txt">Support PKCE</span>
|
||||
<i
|
||||
class="ri-information-line link-hint"
|
||||
use:tooltip={{
|
||||
text: "Usually it should be safe to be always enabled as most providers will just ignore the extra query parameters if they don't support PKCE.",
|
||||
position: "right",
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</Field>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script>
|
||||
import Field from "@/components/base/Field.svelte";
|
||||
|
||||
export let key = "";
|
||||
export let config = {};
|
||||
export let required = false;
|
||||
export let title = "Provider endpoints";
|
||||
|
||||
$: isRequired = required && config?.enabled;
|
||||
</script>
|
||||
|
||||
<div class="section-title">{title}</div>
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.authUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Auth URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.authUrl} required={isRequired} />
|
||||
</Field>
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.tokenUrl" let:uniqueId>
|
||||
<label for={uniqueId}>Token URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.tokenUrl} required={isRequired} />
|
||||
</Field>
|
||||
<Field class="form-field {isRequired ? 'required' : ''}" name="{key}.userApiUrl" let:uniqueId>
|
||||
<label for={uniqueId}>User API URL</label>
|
||||
<input type="url" id={uniqueId} bind:value={config.userApiUrl} required={isRequired} />
|
||||
</Field>
|
||||
Reference in New Issue
Block a user