Merge branch 1.4.x into command-center-and-migrations

This commit is contained in:
tglide
2023-08-10 00:10:16 +01:00
30 changed files with 1268 additions and 372 deletions
+5 -354
View File
@@ -8,7 +8,7 @@
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@analytics/google-tag-manager": "^0.5.3",
"@appwrite.io/console": "0.2.0",
"@appwrite.io/console": "npm:christy-console@^0.4.0",
"@appwrite.io/pink": "^0.0.6-rc.14",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
@@ -165,9 +165,10 @@
"integrity": "sha512-TD+xbmsBLyYy/IxFimW/YL/9L2IEnM7/EoV9Aeh56U64Ify8o27HJcKjo38XY9Tcn0uOq1AX3thkKgvtWvwFQg=="
},
"node_modules/@appwrite.io/console": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@appwrite.io/console/-/console-0.2.0.tgz",
"integrity": "sha512-jgFYoskSCWaesF8zfsgYoTEKfyn6Kq3R9c5sM4AH2H+B/hmT8jwu9c3UU+LHBZ3BLdB9Zm3xOMa4bPyiQwzSeQ==",
"name": "christy-console",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/christy-console/-/christy-console-0.4.0.tgz",
"integrity": "sha512-PypVyV0eg79uju7pLe8zLEXQpD3wTDyGEpfzbEt/uYlt7RMSZUris9kL75wiw8pULYzmUGC33lgMwELcIXgK/Q==",
"dependencies": {
"cross-fetch": "3.1.5",
"isomorphic-form-data": "2.0.0"
@@ -820,246 +821,6 @@
"dev": true,
"peer": true
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.11.tgz",
"integrity": "sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.11.tgz",
"integrity": "sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.11.tgz",
"integrity": "sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.11.tgz",
"integrity": "sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.11.tgz",
"integrity": "sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.11.tgz",
"integrity": "sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.11.tgz",
"integrity": "sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.11.tgz",
"integrity": "sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.11.tgz",
"integrity": "sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.11.tgz",
"integrity": "sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.11.tgz",
"integrity": "sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==",
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.11.tgz",
"integrity": "sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.11.tgz",
"integrity": "sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.11.tgz",
"integrity": "sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==",
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.11.tgz",
"integrity": "sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==",
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.11.tgz",
@@ -1076,102 +837,6 @@
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.11.tgz",
"integrity": "sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.11.tgz",
"integrity": "sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.11.tgz",
"integrity": "sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.11.tgz",
"integrity": "sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.11.tgz",
"integrity": "sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.18.11",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.11.tgz",
"integrity": "sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -4465,20 +4130,6 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+1 -1
View File
@@ -19,8 +19,8 @@
},
"dependencies": {
"@analytics/google-analytics": "^1.0.5",
"@appwrite.io/console": "npm:christy-console@^0.4.0",
"@appwrite.io/pink": "^0.0.6-rc.14",
"@appwrite.io/console": "0.2.0",
"@analytics/google-tag-manager": "^0.5.3",
"@popperjs/core": "^2.11.6",
"@sentry/svelte": "^7.44.2",
+14 -1
View File
@@ -142,6 +142,7 @@ export enum Submit {
ProjectUpdateName = 'submit_project_update_name',
ProjectUpdateTeam = 'submit_project_update_team',
ProjectService = 'submit_project_service',
ProjectUpdateSMTP = 'submit_project_update_smtp',
MemberCreate = 'submit_member_create',
MemberDelete = 'submit_member_delete',
MembershipUpdateStatus = 'submit_membership_update_status',
@@ -153,6 +154,7 @@ export enum Submit {
AuthStatusUpdate = 'submit_auth_status_update',
AuthPasswordHistoryUpdate = 'submit_auth_password_history_limit_update',
AuthPasswordDictionaryUpdate = 'submit_auth_password_dictionary_update',
AuthPersonalDataCheckUpdate = 'submit_auth_personal_data_check_update',
SessionsLengthUpdate = 'submit_sessions_length_update',
SessionsLimitUpdate = 'submit_sessions_limit_update',
SessionDelete = 'submit_session_delete',
@@ -220,5 +222,16 @@ export enum Submit {
BucketUpdateExtensions = 'submit_bucket_update_extensions',
FileCreate = 'submit_file_create',
FileDelete = 'submit_file_delete',
FileUpdatePermissions = 'submit_file_update_permissions'
FileUpdatePermissions = 'submit_file_update_permissions',
EmailChangeLocale = 'submit_email_change_locale',
EmailResetTemplate = 'submit_email_reset_template',
EmailUpdateInviteTemplate = 'submit_email_update_invite_template',
EmailUpdateMagicUrlTemplate = 'submit_email_update_magic_url_template',
EmailUpdateRecoveryTemplate = 'submit_email_update_recovery_template',
EmailUpdateVerificationTemplate = 'submit_email_update_verification_template',
SmsChangeLocale = 'submit_sms_change_locale',
SmsResetTemplate = 'submit_sms_reset_template',
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
}
+15 -3
View File
@@ -6,17 +6,20 @@
export let type: 'info' | 'success' | 'warning' | 'error' = 'info';
export let standalone = false;
export let buttons: Buttons[] = [];
export let isAction = false;
export let isStandalone = false;
const dispatch = createEventDispatcher();
</script>
<section
class="alert"
class:is-action={isAction}
class:is-standalone={isStandalone}
class:is-success={type === 'success'}
class:is-warning={type === 'warning'}
class:is-danger={type === 'error'}
class:is-info={type === 'info'}
class:is-standalone={standalone}>
class:is-info={type === 'info'}>
<div class="alert-grid">
{#if dismissible}
<button
@@ -41,7 +44,7 @@
</h6>
{/if}
<p class="alert-message"><slot /></p>
{#if buttons?.length}
{#if buttons?.length && !isAction}
<div class="alert-buttons u-flex">
{#each buttons as button}
<button class="button is-text" on:click={button.method}>
@@ -51,5 +54,14 @@
</div>
{/if}
</div>
{#if buttons?.length && isAction}
<div class="alert-buttons u-flex u-gap-16 u-cross-child-center">
{#each buttons as button}
<button class="button is-text" on:click={button.method}>
<span class="text">{button.name}</span>
</button>
{/each}
</div>
{/if}
</div>
</section>
+3 -1
View File
@@ -1,11 +1,13 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
export let withIndentation = false;
export let open = false;
</script>
<li class="collapsible-item">
<details class="collapsible-wrapper" {open}>
<summary class="collapsible-button">
<summary class="collapsible-button" on:keyup={clickOnEnter} on:click>
<slot name="beforetitle" />
<div>
<span class="text"><slot name="title" /></span>
+2
View File
@@ -3,6 +3,7 @@
export let value: string;
export let event: string = null;
export let centered = true;
function truncateText(node: HTMLElement) {
const MAX_TRIES = 100;
@@ -49,6 +50,7 @@
<Copy {value} {event}>
<div
class="interactive-text-output is-buttons-on-top"
class:u-text-center={centered}
style:min-inline-size="0"
style:display="inline-flex">
<span
@@ -20,9 +20,11 @@
export let placeholder = '';
export let required = false;
export let disabled = false;
export let fullWidth = false;
export let autofocus = false;
export let interactiveOutput = false;
// Input value
// stretch is used inside of a flex container to give the element flex:1
export let stretch = true;
export let search = '';
// The actual selected value
export let value: Option['value'];
@@ -82,9 +84,12 @@
value = option.value;
search = option.label;
// It's not working without this line.
!$$slots.output && (element.value = search);
if ($$slots.output) {
search = '';
} else {
element.value = search;
}
hasFocus = false;
search = '';
dispatch('select', option);
}
@@ -99,7 +104,10 @@
$: showClearBtn = (hasFocus && search) || value;
</script>
<li class="u-position-relative form-item u-stretch">
<li
class="u-position-relative form-item"
class:u-width-full-line={fullWidth}
class:u-stretch={stretch}>
<DropList
bind:show={hasFocus}
noStyle
+2 -1
View File
@@ -3,6 +3,7 @@
import { FormItem, Helper, Label } from '.';
import NullCheckbox from './nullCheckbox.svelte';
import TextCounter from './textCounter.svelte';
import { clickOnEnter } from '$lib/helpers/a11y';
export let label: string;
export let showLabel = true;
@@ -61,7 +62,7 @@
{label}
</Label>
<div class="input-text-wrapper">
<div class="input-text-wrapper" on:click on:keyup={clickOnEnter}>
<textarea
{id}
{name}
@@ -19,17 +19,23 @@
hasChildren: true,
event: 'teams'
},
{
href: `${path}/security`,
title: 'Security',
event: 'security'
},
{
href: `${path}/templates`,
title: 'Templates',
hasChildren: false,
event: 'templates'
},
{
href: `${path}/usage`,
title: 'Usage',
hasChildren: true,
event: 'usage'
},
{
href: `${path}/security`,
title: 'Security',
event: 'security'
},
{
href: `${path}/settings`,
title: 'Settings',
@@ -2,6 +2,7 @@
import { Container } from '$lib/layout';
import UpdatePasswordDictionary from './updatePasswordDictionary.svelte';
import UpdatePasswordHistory from './updatePasswordHistory.svelte';
import UpdatePersonalDataCheck from './updatePersonalDataCheck.svelte';
import UpdateSessionLength from './updateSessionLength.svelte';
import UpdateSessionsLimit from './updateSessionsLimit.svelte';
import UpdateUsersLimit from './updateUsersLimit.svelte';
@@ -13,4 +14,5 @@
<UpdateSessionsLimit />
<UpdatePasswordHistory />
<UpdatePasswordDictionary />
<UpdatePersonalDataCheck />
</Container>
@@ -44,7 +44,7 @@
<FormList>
<InputSwitch
bind:value={passwordHistoryEnabled}
id="passwordHisotryEnabled"
id="passwordHistoryEnabled"
label="Password History" />
</FormList>
<p class="text">
@@ -0,0 +1,57 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputSwitch } from '$lib/elements/forms';
import { FormList } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
let authPersonalDataCheck = $project.authPersonalDataCheck ?? false;
async function updatePersonalDataCheck() {
try {
await sdk.forConsole.projects.updatePersonalDataCheck(
$project.$id,
authPersonalDataCheck
);
await invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'Toggled personal data checks for passwords'
});
trackEvent(Submit.AuthPersonalDataCheckUpdate);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AuthPersonalDataCheckUpdate);
}
}
</script>
<Form onSubmit={updatePersonalDataCheck}>
<CardGrid>
<Heading tag="h2" size="7">Personal Data</Heading>
<svelte:fragment slot="aside">
<FormList>
<InputSwitch
bind:value={authPersonalDataCheck}
id="personalDataCheck"
label="Disallow Personal Data" />
</FormList>
<p class="text">
Do now allow passwords that contain any part of the user's personal data. This
includes the user's <code>name</code>, <code>email</code>, or <code>phone</code>.
</p>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button disabled={authPersonalDataCheck === $project.authPersonalDataCheck} submit
>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
@@ -0,0 +1,223 @@
<script context="module" lang="ts">
export async function loadEmailTemplate(projectId: string, type: string, locale: string) {
try {
return await sdk.forConsole.projects.getEmailTemplate(projectId, type, locale);
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
}
}
export async function loadSmsTemplate(projectId: string, type: string, locale: string) {
try {
return await sdk.forConsole.projects.getSmsTemplate(projectId, type, locale);
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
}
}
</script>
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Alert, CardGrid, Collapsible, CollapsibleItem, Heading } from '$lib/components';
import { Container } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { addNotification } from '$lib/stores/notifications';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import EmailVerificationTemplate from './emailVerificationTemplate.svelte';
import EmailMagicUrlTemplate from './emailMagicUrlTemplate.svelte';
import EmailRecoveryTemplate from './emailRecoveryTemplate.svelte';
import EmailInviteTemplate from './emailInviteTemplate.svelte';
import SmsVerificationTemplate from './smsVerificationTemplate.svelte';
import SmsLoginTemplate from './smsLoginTemplate.svelte';
import { baseEmailTemplate, baseSmsTemplate, emailTemplate, smsTemplate } from './store';
import { Button } from '$lib/elements/forms';
export let data: PageData;
const projectId = $page.params.project;
let emailOpen = 'verification';
$: emailVerificationOpen = emailOpen === 'verification';
$: emailMagicSessionOpen = emailOpen === 'magicSession';
$: emailResetPassword = emailOpen === 'recovery';
$: emailInviteUser = emailOpen === 'invitation';
let smsOpen = 'verification';
$: smsVerificationOpen = smsOpen === 'verification';
$: smsLoginOpen = smsOpen === 'login';
$: smsInvitationOpen = smsOpen === 'invitation';
onMount(async () => {
openEmail('verification');
openSms('verification');
});
async function openEmail(type: string) {
type === emailOpen ? (emailOpen = null) : (emailOpen = type);
$emailTemplate = await loadEmailTemplate(projectId, type, 'en');
$baseEmailTemplate = { ...$emailTemplate };
}
async function openSms(type: string) {
type === smsOpen ? (smsOpen = null) : (smsOpen = type);
$smsTemplate = await loadSmsTemplate(projectId, type, 'en');
$baseSmsTemplate = { ...$smsTemplate };
}
</script>
<Container>
<div class="u-flex u-gap-8 u-cross-center">
<Heading tag="h2" size="5">Templates</Heading>
<div class="tag eyebrow-heading-3">
<span class="text u-x-small">Beta</span>
</div>
</div>
{#if !$project.smtpEnabled}
<div class="u-margin-block-start-24">
<Alert
isStandalone
dismissible
type="info"
buttons={[
{
name: 'Add SMTP server',
method: () => {
goto(`${base}/console/project-${$project.$id}/settings/smtp`);
}
}
]}>
<svelte:fragment slot="title">
Custom SMTP server is required for customizing emails
</svelte:fragment>
Configure a custom SMTP server to enable custom email templates and prevent emails from
being labeled as spam.
</Alert>
</div>
{/if}
<CardGrid>
<Heading size="7" tag="h3">Email templates</Heading>
<p class="text">
Use templates to send and process account management emails. <a
href="https://appwrite.io/docs"
class="link">
Learn more about email templates.
</a>
<!-- TODO Docs link -->
</p>
<svelte:fragment slot="aside">
<Collapsible>
<CollapsibleItem
bind:open={emailVerificationOpen}
on:click={(e) => {
// preventing default and propagation to open the collapsible correctly
e.preventDefault();
e.stopImmediatePropagation();
openEmail('verification');
}}>
<svelte:fragment slot="title">Verification</svelte:fragment>
<p class="text">
Send a verification email to users that sign in with their email and
password.
</p>
<EmailVerificationTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
<CollapsibleItem
bind:open={emailMagicSessionOpen}
on:click={(e) => {
e.preventDefault();
openEmail('magicSession');
}}>
<svelte:fragment slot="title">Magic URL</svelte:fragment>
<p class="text">Send an email to users that sign in with a magic URL.</p>
<EmailMagicUrlTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
<CollapsibleItem
bind:open={emailResetPassword}
on:click={(e) => {
e.preventDefault();
openEmail('recovery');
}}>
<svelte:fragment slot="title">Reset password</svelte:fragment>
<p class="text">Send a recovery email to users that forget their password.</p>
<EmailRecoveryTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
<CollapsibleItem
bind:open={emailInviteUser}
on:click={(e) => {
e.preventDefault();
openEmail('invitation');
}}>
<svelte:fragment slot="title">Invite user</svelte:fragment>
<p class="text">Send an invitation email to become a member of your project.</p>
<EmailInviteTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button href={`${base}/console/project-${$project.$id}/settings/smtp`} secondary>
Add SMTP server
</Button>
</svelte:fragment>
</CardGrid>
<CardGrid>
<Heading size="7" tag="h3">SMS templates</Heading>
<p class="text">
Use templates to send and process account management mobile messages. <a
href="https://appwrite.io/docs"
class="link">
Learn more about SMS templates</a
>. <!-- TODO Docs link -->
</p>
<svelte:fragment slot="aside">
<Collapsible>
<CollapsibleItem
bind:open={smsVerificationOpen}
on:click={(e) => {
e.preventDefault();
openSms('verification');
}}>
<svelte:fragment slot="title">Verification</svelte:fragment>
<p class="text">
Send a verification SMS to users that sign in with their phone
</p>
<SmsVerificationTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
<CollapsibleItem
bind:open={smsLoginOpen}
on:click={(e) => {
e.preventDefault();
openSms('login');
}}>
<svelte:fragment slot="title">Login</svelte:fragment>
<p class="text">
Send a one-time passcode to users' mobile phones to allow them to sign in.
</p>
<SmsLoginTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
<CollapsibleItem
bind:open={smsInvitationOpen}
on:click={(e) => {
e.preventDefault();
openSms('invitation');
}}>
<svelte:fragment slot="title">Invitation</svelte:fragment>
<p class="text">Send an invitation SMS to become a member of your project.</p>
<SmsLoginTemplate localeCodes={data.localeCodes} />
</CollapsibleItem>
</Collapsible>
</svelte:fragment>
</CardGrid>
</Container>
@@ -0,0 +1,8 @@
import { sdk } from '$lib/stores/sdk';
export const load = async () => {
const codes = await sdk.forProject.locale.listCodes();
return {
localeCodes: codes.localeCodes
};
};
@@ -0,0 +1,48 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { loadEmailTemplate } from './+page.svelte';
import { page } from '$app/stores';
import { baseEmailTemplate, emailTemplate } from './store';
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadEmailTemplate(projectId, 'invitation', locale);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: 'invitation' });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</div>
@@ -0,0 +1,49 @@
<script lang="ts">
import { loadEmailTemplate } from './+page.svelte';
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { baseEmailTemplate, emailTemplate } from './store';
import { page } from '$app/stores';
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadEmailTemplate(projectId, 'magicSession', locale);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: 'magicSession' });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</div>
@@ -0,0 +1,48 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { baseEmailTemplate, emailTemplate } from './store';
import { loadEmailTemplate } from './+page.svelte';
import { page } from '$app/stores';
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadEmailTemplate(projectId, 'recovery', locale);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: 'recovery' });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</div>
@@ -0,0 +1,139 @@
<script lang="ts">
import {
Button,
Form,
FormList,
InputEmail,
InputText,
InputTextarea
} from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import ResetEmail from './resetEmail.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
import { deepEqual } from '$lib/helpers/object';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let loading = false;
let openResetModal = false;
let eventType = Submit.EmailUpdateInviteTemplate;
async function saveEmailTemplate() {
if (!$emailTemplate.locale) {
addNotification({
type: 'error',
message: 'Locale is required'
});
return;
}
try {
switch ($emailTemplate.type) {
case 'invitation':
eventType = Submit.EmailUpdateInviteTemplate;
break;
case 'magicSession':
eventType = Submit.EmailUpdateMagicUrlTemplate;
break;
case 'recovery':
eventType = Submit.EmailUpdateRecoveryTemplate;
break;
case 'verification':
eventType = Submit.EmailUpdateVerificationTemplate;
break;
}
await sdk.forConsole.projects.updateEmailTemplate(
$project.$id,
$emailTemplate.type,
$emailTemplate.locale,
$emailTemplate.senderName,
$emailTemplate.senderEmail,
$emailTemplate.subject,
$emailTemplate.message,
$emailTemplate.replyTo ? $emailTemplate.replyTo : undefined
);
$baseEmailTemplate = {
...$emailTemplate
};
addNotification({
type: 'success',
message: `Email ${$emailTemplate.type} template for ${$emailTemplate.locale} updated`
});
trackEvent(eventType, {
locale: $emailTemplate.locale
});
} catch (e) {
trackError(e, eventType);
addNotification({
type: 'error',
message: e.message
});
}
}
$: isButtonDisabled = deepEqual($emailTemplate, $baseEmailTemplate);
</script>
<div class="box u-position-relative">
{#if loading}
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader" />
<p class="text">Loading template...</p>
</div>
{/if}
<div class:u-opacity-0={loading} style={loading ? 'pointer-events: none' : ''}>
<Form onSubmit={saveEmailTemplate}>
<FormList gap={8}>
<InputText
id="senderName"
label="Sender name"
bind:value={$emailTemplate.senderName}
placeholder={'{{project}}'} />
<InputEmail
bind:value={$emailTemplate.senderEmail}
id="senderEmail"
label="Sender email"
placeholder="Enter sender email" />
<InputEmail id="replyTo" label="Reply to" placeholder="noreply@appwrite.io" />
{#if $$slots.default}
<li style="margin-block: 1rem;">
<p class="text">
Click to copy variables for the fields below. Learn more <a
class="link"
href="/#">here</a
>.
<!-- TODO: add link to docs -->
</p>
<div class="u-margin-block-start-16 u-flex u-gap-8">
<slot />
</div>
</li>
{/if}
<InputText
bind:value={$emailTemplate.subject}
id="subject"
label="Subject"
placeholder="Enter subject" />
<InputTextarea
bind:value={$emailTemplate.message}
id="message"
label="Message"
placeholder="Enter your message"
tooltip="Set up an SMTP server to edit the message body"
readonly={!$project.smtpEnabled} />
</FormList>
<div class="u-sep-block-start u-margin-block-start-24" />
<div class="u-flex u-gap-16 u-main-end u-margin-block-start-24">
<Button on:click={() => (openResetModal = true)} text>Reset changes</Button>
<Button submit disabled={isButtonDisabled}>Update</Button>
</div>
</Form>
</div>
</div>
<ResetEmail bind:show={openResetModal} />
@@ -0,0 +1,49 @@
<script lang="ts">
import EmailTemplate from './emailTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { loadEmailTemplate } from './+page.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
import { page } from '$app/stores';
import { Id } from '$lib/components';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadEmailTemplate(projectId, 'verification', locale);
emailTemplate.set(template);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailChangeLocale, { locale, type: 'verification' });
} catch (error) {
trackError(error, Submit.EmailChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<EmailTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</EmailTemplate>
</div>
@@ -0,0 +1,39 @@
<script lang="ts">
import { InputSelectSearch } from '$lib/elements/forms';
import type { Models } from '@appwrite.io/console';
export let localeCodes: Models.LocaleCode[];
export let value = 'en';
let search = 'English';
$: options =
localeCodes
?.map((code) => ({
label: code.name,
value: code.code
}))
?.filter((option) => option.label?.toLowerCase().startsWith(search?.toLowerCase())) ??
[];
</script>
<div class="box u-flex u-gap-16" style:--box-border-radius="var(--border-radius-small)">
<div class="u-un-break-text">
<span class="icon-translate" />
<span class="text">Template language</span>
</div>
<InputSelectSearch
on:select
label="Language"
id="language"
placeholder="Select a language"
name="locale"
bind:value
bind:search
required
fullWidth
stretch={false}
showLabel={false}
interactiveOutput
{options} />
</div>
@@ -0,0 +1,58 @@
<script lang="ts">
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { loadEmailTemplate } from './+page.svelte';
import { baseEmailTemplate, emailTemplate } from './store';
export let show = false;
let error: string;
async function reset() {
try {
await sdk.forConsole.projects.deleteEmailTemplate(
$project.$id,
$emailTemplate.type,
$emailTemplate.locale
);
$emailTemplate = await loadEmailTemplate(
$project.$id,
$emailTemplate.type,
$emailTemplate.locale
);
$baseEmailTemplate = { ...$emailTemplate };
trackEvent(Submit.EmailResetTemplate, {
locale: $emailTemplate.locale,
type: $emailTemplate.type
});
show = false;
} catch (e) {
trackError(e, Submit.EmailResetTemplate);
error = e.message;
}
}
</script>
<Modal
icon="exclamation"
state="warning"
onSubmit={reset}
size="big"
{error}
bind:show
headerDivider={false}>
<svelte:fragment slot="header">Reset Email Template?</svelte:fragment>
<p class="text">
Are you sure you want to reset the email template?
<b>Default values will be set in all inputs.</b>
</p>
<svelte:fragment slot="footer">
<Button text secondary on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit>Reset</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,58 @@
<script lang="ts">
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { sdk } from '$lib/stores/sdk';
import { project } from '../../store';
import { loadSmsTemplate } from './+page.svelte';
import { baseSmsTemplate, smsTemplate } from './store';
export let show = false;
let error: string;
async function reset() {
try {
await sdk.forConsole.projects.deleteSmsTemplate(
$project.$id,
$smsTemplate.type,
$smsTemplate.locale
);
$smsTemplate = await loadSmsTemplate(
$project.$id,
$smsTemplate.type,
$smsTemplate.locale
);
$baseSmsTemplate = { ...$smsTemplate };
trackEvent(Submit.SmsResetTemplate, {
locale: $smsTemplate.locale,
type: $smsTemplate.type
});
show = false;
} catch (e) {
trackError(e, Submit.SmsResetTemplate);
error = e.message;
}
}
</script>
<Modal
icon="exclamation"
state="warning"
onSubmit={reset}
size="big"
{error}
bind:show
headerDivider={false}>
<svelte:fragment slot="header">Reset SMS Template?</svelte:fragment>
<p class="text">
Are you sure you want to reset the SMS template?
<b>Default values will be set in all inputs.</b>
</p>
<svelte:fragment slot="footer">
<Button text secondary on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit>Reset</Button>
</svelte:fragment>
</Modal>
@@ -0,0 +1,48 @@
<script lang="ts">
import SmsTemplate from './smsTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { baseSmsTemplate, smsTemplate } from './store';
import { page } from '$app/stores';
import { loadSmsTemplate } from './+page.svelte';
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadSmsTemplate(projectId, 'invitation', locale);
smsTemplate.set(template);
$baseSmsTemplate = { ...$smsTemplate };
trackEvent(Submit.SmsChangeLocale, { locale, type: 'invitation' });
} catch (error) {
trackError(error, Submit.SmsChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<SmsTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</SmsTemplate>
</div>
@@ -0,0 +1,49 @@
<script lang="ts">
import SmsTemplate from './smsTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { loadSmsTemplate } from './+page.svelte';
import { baseSmsTemplate, smsTemplate } from './store';
import { addNotification } from '$lib/stores/notifications';
import { Id } from '$lib/components';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadSmsTemplate(projectId, 'login', locale);
smsTemplate.set(template);
$baseSmsTemplate = { ...$smsTemplate };
trackEvent(Submit.SmsChangeLocale, { locale, type: 'login' });
} catch (error) {
trackError(error, Submit.SmsChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<SmsTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</SmsTemplate>
</div>
@@ -0,0 +1,94 @@
<script lang="ts">
import { page } from '$app/stores';
import { Button, Form, FormList, InputTextarea } from '$lib/elements/forms';
import ResetSms from './resetSms.svelte';
import { baseSmsTemplate, smsTemplate } from './store';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { deepEqual } from '$lib/helpers/object';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let loading = false;
const projectId = $page.params.project;
let openResetModal = false;
let eventType = Submit.SmsUpdateInviteTemplate;
async function saveSmsTemplate() {
if (!$smsTemplate.locale) {
addNotification({
type: 'error',
message: 'Locale is required'
});
return;
}
try {
switch ($smsTemplate.type) {
case 'invitation':
eventType = Submit.SmsUpdateInviteTemplate;
break;
case 'login':
eventType = Submit.SmsUpdateLoginTemplate;
break;
case 'verification':
eventType = Submit.SmsUpdateVerificationTemplate;
break;
}
await sdk.forConsole.projects.updateSmsTemplate(
projectId,
$smsTemplate.type,
$smsTemplate.locale,
$smsTemplate.message
);
$baseSmsTemplate = {
...$smsTemplate
};
addNotification({
type: 'success',
message: `SMS ${$smsTemplate.type} template for ${$smsTemplate.locale} updated`
});
trackEvent(eventType, {
locale: $smsTemplate.locale
});
} catch (e) {
trackError(e, eventType);
addNotification({
type: 'error',
message: e.message
});
}
}
$: isButtonDisabled = deepEqual($smsTemplate, $baseSmsTemplate);
</script>
<div class="box u-position-relative">
{#if loading}
<div
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
style="inset-inline-start: 0;">
<div class="loader" />
<p class="text">Loading template...</p>
</div>
{/if}
<div class:u-opacity-0={loading} style={loading ? 'pointer-events: none' : ''}>
<Form onSubmit={saveSmsTemplate}>
<FormList gap={8}>
<InputTextarea
bind:value={$smsTemplate.message}
id="message"
label="Message"
placeholder="Enter your message" />
</FormList>
<div class="u-sep-block-start u-margin-block-start-24" />
<div class="u-flex u-gap-16 u-main-end u-margin-block-start-24">
<Button on:click={() => (openResetModal = true)} text>Reset changes</Button>
<Button submit disabled={isButtonDisabled}>Update</Button>
</div>
</Form>
</div>
</div>
<ResetSms bind:show={openResetModal} />
@@ -0,0 +1,48 @@
<script lang="ts">
import SmsTemplate from './smsTemplate.svelte';
import LocaleOptions from './localeOptions.svelte';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { loadSmsTemplate } from './+page.svelte';
import { baseSmsTemplate, smsTemplate } from './store';
import { Id } from '$lib/components';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
export let localeCodes: Models.LocaleCode[];
const projectId = $page.params.project;
let locale = 'en';
let loading = false;
async function onLocaleChange() {
const timeout = setTimeout(() => {
loading = true;
}, 1000);
try {
const template = await loadSmsTemplate(projectId, 'verification', locale);
smsTemplate.set(template);
$baseSmsTemplate = { ...$smsTemplate };
trackEvent(Submit.SmsChangeLocale, { locale, type: 'verification' });
} catch (error) {
trackError(error, Submit.SmsChangeLocale);
addNotification({
type: 'error',
message: error.message
});
} finally {
clearTimeout(timeout);
loading = false;
}
}
</script>
<div class="boxes-wrapper u-margin-block-start-16">
<LocaleOptions {localeCodes} on:select={onLocaleChange} bind:value={locale} />
<SmsTemplate bind:loading>
<Id value={'{{team}}'}>{'{{team}}'}</Id>
<Id value={'{{user}}'}>{'{{user}}'}</Id>
<Id value={'{{project}}'}>{'{{project}}'}</Id>
<Id value={'{{redirect}}'}>{'{{redirect}}'}</Id>
</SmsTemplate>
</div>
@@ -0,0 +1,33 @@
import type { Models } from '@appwrite.io/console';
import { writable } from 'svelte/store';
export const emailTemplate = writable<Models.EmailTemplate>({
type: null,
locale: null,
message: null,
senderName: '',
senderEmail: null,
replyTo: null,
subject: null
});
export const baseEmailTemplate = writable<Models.EmailTemplate>({
type: null,
locale: null,
message: null,
senderName: null,
senderEmail: null,
replyTo: null,
subject: null
});
export const smsTemplate = writable<Models.SmsTemplate>({
type: null,
locale: null,
message: null
});
export const baseSmsTemplate = writable<Models.SmsTemplate>({
type: null,
locale: null,
message: null
});
@@ -26,6 +26,11 @@
href: `${path}/migrations`,
title: 'Migrations',
event: 'migrations'
},
{
href: `${path}/smtp`,
title: 'SMTP Settings',
event: 'smtp'
}
];
</script>
@@ -97,7 +97,7 @@
<Modal bind:show {onSubmit}>
<svelte:fragment slot="header">Export to self-hosted instance</svelte:fragment>
<div class="modal-contents">
<Alert standalone>
<Alert isStandalone>
<svelte:fragment slot="title">API key creation</svelte:fragment>
By initiating the transfer, an API key will be automatically generated in the background,
which you can delete after completion
@@ -0,0 +1,146 @@
<script lang="ts">
import { CardGrid, Heading } from '$lib/components';
import { Button, Form, FormList, InputText, InputChoice } from '$lib/elements/forms';
import { Container } from '$lib/layout';
import { project } from '../../store';
import { onMount } from 'svelte/internal';
import InputPassword from '$lib/elements/forms/inputPassword.svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { addNotification } from '$lib/stores/notifications';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import InputNumber from '$lib/elements/forms/inputNumber.svelte';
import { base } from '$app/paths';
import { deepEqual } from '$lib/helpers/object';
let enabled = false;
let sender: string;
let host: string;
let port: number;
let username: string;
let password: string;
onMount(() => {
enabled = $project.smtpEnabled ?? false;
sender = $project.smtpSender;
host = $project.smtpHost;
port = $project.smtpPort;
username = $project.smtpUsername;
password = $project.smtpPassword;
});
async function updateSmtp() {
try {
await sdk.forConsole.projects.updateSmtpConfiguration(
$project.$id,
enabled,
sender,
host,
port,
username,
password
);
invalidate(Dependencies.PROJECT);
addNotification({
type: 'success',
message: 'SMTP server has been ' + (enabled ? 'enabled.' : 'disabled.')
});
trackEvent(Submit.ProjectUpdateSMTP);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.ProjectUpdateSMTP);
}
}
$: isButtonDisabled = deepEqual(
{ enabled, sender, host, port, username, password },
{
enabled: $project.smtpEnabled,
sender: $project.smtpSender,
host: $project.smtpHost,
port: $project.smtpPort,
username: $project.smtpUsername,
password: $project.smtpPassword
}
);
$: if (!enabled) {
sender = undefined;
host = undefined;
port = undefined;
username = undefined;
password = undefined;
}
</script>
<Container>
<Form onSubmit={updateSmtp}>
<CardGrid>
<Heading tag="h6" size="7">SMTP server</Heading>
<p class="text">
You can customize the email service by providing your own SMTP server. View your
email templates <a
href="{base}/console/project-{$project.$id}/auth/templates"
class="link">here</a>
</p>
<svelte:fragment slot="aside">
<FormList>
<InputChoice
type="switchbox"
id="enabled"
bind:value={enabled}
label="Custom SMTP server">
Enabling this option allows customizing email templates and prevents emails
from being labeled as spam.
</InputChoice>
{#if enabled}
<InputText
id="senderEmail"
label="Sender email"
bind:value={sender}
required
placeholder="user@example.io" />
<InputText
id="serverHost"
label="Server host"
bind:value={host}
required
placeholder="smtp.server.com" />
<InputNumber
id="serverPort"
label="Server port"
bind:value={port}
required
placeholder="587" />
<InputText
id="username"
label="Username"
bind:value={username}
required
placeholder="Enter username" />
<InputPassword
showPasswordButton
id="passwort"
label="Password"
bind:value={password}
required
placeholder="Enter password" />
<InputChoice id="tls" label="TLS secure protocol">
Enable if TLS is supported on your SMTP server.
</InputChoice>
{/if}
</FormList>
</svelte:fragment>
<svelte:fragment slot="actions">
<Button submit disabled={isButtonDisabled}>Update</Button>
</svelte:fragment>
</CardGrid>
</Form>
</Container>