mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'main' into fix-1011-show-a-default-runtime-when-length-is-one
This commit is contained in:
commit
7899eebafd
19 files changed
+635
-248
No files matched your search
Generated
+9
-9
@@ -7,8 +7,8 @@
|
||||
"name": "@appwrite/console",
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.1",
|
||||
"@appwrite.io/pink": "0.8.0",
|
||||
"@appwrite.io/pink-icons": "0.8.0",
|
||||
"@appwrite.io/pink": "0.11.0",
|
||||
"@appwrite.io/pink-icons": "0.11.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
@@ -166,19 +166,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@appwrite.io/pink": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.8.0.tgz",
|
||||
"integrity": "sha512-UZKwkUtAo2N0JiTiKDPWC8GHubTF2905DOzuCGl716fYua2gNIlS8D+HA/wd15qyRjZlgQh4MtVUAdHI8GLf8g==",
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink/-/pink-0.11.0.tgz",
|
||||
"integrity": "sha512-0/ztJxeV6Cy1FnXy2ClLELzDnR4eRtGAhQ24napCg0VwOFevSIc0S7jcRrWVyA6WRuwex4eGiEn2JeYMTq70Xg==",
|
||||
"dependencies": {
|
||||
"@appwrite.io/pink-icons": "0.8.0",
|
||||
"@appwrite.io/pink-icons": "0.11.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"the-new-css-reset": "^1.11.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@appwrite.io/pink-icons": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.8.0.tgz",
|
||||
"integrity": "sha512-UQd4mXOohGggXHNhIlYt4u2NLB/ubI6pvkdllrBQnHZEb7JL2iklff8Ne5moW8F9DeXLBt/ZQLRe240lvVjm6Q=="
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@appwrite.io/pink-icons/-/pink-icons-0.11.0.tgz",
|
||||
"integrity": "sha512-FOvP87m4S17QRQPrLzsSlwrbIeJNJi5RliGUQYONIhmI0PiwE4Xm5LXFE+NqFYsR56w2X3jYdJIHdIdZlH3XjA=="
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.24.2",
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.6.1",
|
||||
"@appwrite.io/pink": "0.8.0",
|
||||
"@appwrite.io/pink-icons": "0.8.0",
|
||||
"@appwrite.io/pink": "0.11.0",
|
||||
"@appwrite.io/pink-icons": "0.11.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
|
||||
@@ -151,7 +151,12 @@ export enum Submit {
|
||||
AccountDelete = 'submit_account_delete',
|
||||
AccountDeleteSession = 'submit_account_delete_session',
|
||||
AccountDeleteAllSessions = 'submit_account_delete_all_sessions',
|
||||
AccountUpdateMfa = 'submit_account_update_mfa',
|
||||
AccountAuthenticatorCreate = 'submit_account_authenticator_create',
|
||||
AccountAuthenticatorUpdate = 'submit_account_authenticator_update',
|
||||
AccountAuthenticatorDelete = 'submit_account_authenticator_delete',
|
||||
AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create',
|
||||
AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update',
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
|
||||
@@ -1,46 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Label } from '$lib/elements/forms';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { Copy, Drop } from '.';
|
||||
|
||||
export let value: string;
|
||||
export let label: string = null;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = false;
|
||||
export let labelTooltip: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let appendTo: Parameters<typeof tooltip>['1']['appendTo'] = undefined;
|
||||
|
||||
let content = 'Click to copy';
|
||||
|
||||
const handleCopy = async () => {
|
||||
const success = await copy(value);
|
||||
|
||||
if (success) {
|
||||
content = 'Copied';
|
||||
} else {
|
||||
addNotification({
|
||||
message: 'Unable to copy to clipboard',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<label class:u-hide={!showLabel} class="label" for={label}>{label}</label>
|
||||
{#if label}
|
||||
<Label tooltip={labelTooltip} {optionalText} hide={!showLabel} for={label}>
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1">
|
||||
<input {value} id={label} type="text" class="input-text" readonly />
|
||||
<input id={label} type="text" {value} readonly />
|
||||
<div class="options-list">
|
||||
<button
|
||||
type="button"
|
||||
class="options-list-button"
|
||||
aria-label="Click to copy."
|
||||
on:click={handleCopy}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false
|
||||
}}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</button>
|
||||
{#key appendTo}
|
||||
<Copy {value} {appendTo}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,11 +64,13 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
{#if description.length > 0}
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
{/if}
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let fullWidth = false;
|
||||
export let autoSubmit = true;
|
||||
|
||||
let element: HTMLOListElement;
|
||||
|
||||
@@ -21,9 +22,12 @@
|
||||
placeholder: '',
|
||||
defaultValue: value.split(''),
|
||||
onValueChange: ({ next }) => {
|
||||
if (!autoSubmit) {
|
||||
return next;
|
||||
}
|
||||
value = next.join('');
|
||||
|
||||
if (value.length === 6 && !autoSubmitted) {
|
||||
if (value.length === length && !autoSubmitted) {
|
||||
autoSubmitted = true;
|
||||
const firstInputElement = element.querySelector('input');
|
||||
firstInputElement?.form.requestSubmit();
|
||||
@@ -47,15 +51,15 @@
|
||||
</script>
|
||||
|
||||
<FormItem {fullWidth}>
|
||||
<ol class="u-flex u-main-center u-gap-16" use:melt={$root} bind:this={element}>
|
||||
<ol class="u-flex u-main-center" use:melt={$root} bind:this={element}>
|
||||
{#each Array.from({ length }) as _}
|
||||
<li>
|
||||
<input
|
||||
type="number"
|
||||
class="verification-code-input u-remove-input-number-buttons"
|
||||
class="verification-code-input u-bold u-remove-input-number-buttons"
|
||||
inputmode="numeric"
|
||||
maxlength="1"
|
||||
style:--p-input-size="3.75rem"
|
||||
style:font-size="2rem"
|
||||
style:border-radius="var(--border-radius-small)"
|
||||
use:melt={$input()}
|
||||
{required}
|
||||
{readonly}
|
||||
@@ -64,3 +68,29 @@
|
||||
{/each}
|
||||
</ol>
|
||||
</FormItem>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
/* Default (including mobile) */
|
||||
ol {
|
||||
gap: 0.5rem;
|
||||
|
||||
input {
|
||||
--p-input-size: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* for smaller screens */
|
||||
@media #{$break2open} {
|
||||
ol {
|
||||
gap: 1rem;
|
||||
|
||||
input {
|
||||
--p-input-size: 3.75rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -23,17 +23,6 @@
|
||||
function setFiles(value: FileList) {
|
||||
if (!value) return;
|
||||
|
||||
const hasInvalidExt = Array.from(value).some((file) => {
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
return allowedFileExtensions?.length
|
||||
? !allowedFileExtensions.includes(fileExtension)
|
||||
: false;
|
||||
});
|
||||
if (hasInvalidExt) {
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
}
|
||||
|
||||
files = value;
|
||||
input.files = value;
|
||||
}
|
||||
@@ -42,11 +31,22 @@
|
||||
setFiles(new DataTransfer().files);
|
||||
}
|
||||
|
||||
function isFileExtensionAllowed(fileExtension: string) {
|
||||
if (allowedFileExtensions.length && !allowedFileExtensions.includes(fileExtension)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function dropHandler(ev: DragEvent) {
|
||||
ev.dataTransfer.dropEffect = 'move';
|
||||
hovering = false;
|
||||
if (!ev.dataTransfer.items) return;
|
||||
for (let i = 0; i < ev.dataTransfer.items.length; i++) {
|
||||
const fileExtension = ev.dataTransfer.items[i].getAsFile().name.split('.')[1];
|
||||
if (!isFileExtensionAllowed(fileExtension)) {
|
||||
error = 'Invalid file extension';
|
||||
return;
|
||||
}
|
||||
if (ev.dataTransfer.items[i].kind === 'file') {
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(ev.dataTransfer.items[i].getAsFile());
|
||||
@@ -81,6 +81,18 @@
|
||||
|
||||
const handleChange = (event: Event) => {
|
||||
const target = event.currentTarget as HTMLInputElement;
|
||||
|
||||
const isValidFiles = Array.from(target.files).every((file) => {
|
||||
const fileExtension = file.name.split('.').pop();
|
||||
return isFileExtensionAllowed(fileExtension);
|
||||
});
|
||||
|
||||
if (!isValidFiles) {
|
||||
error = 'Invalid file extension';
|
||||
target.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
setFiles(target.files);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<section
|
||||
class="u-flex u-flex-vertical"
|
||||
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
|
||||
<div class="logo u-flex u-gap-16">
|
||||
<div class="logo u-flex u-gap-16 is-not-mobile">
|
||||
<a href={user ? '/console' : '/'}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
@@ -38,20 +38,44 @@
|
||||
<p>Build like a team of hundreds<span class="underscore">_</span></p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="grid-1-1-col-2 u-flex u-main-center u-cross-center">
|
||||
<section class="grid-1-1-col-2 u-flex u-flex-vertical u-cross-center u-main-center">
|
||||
<slot name="top" />
|
||||
<div class="container u-flex u-flex-vertical u-cross-center u-main-center">
|
||||
<div class="u-max-width-500 u-width-full-line">
|
||||
<h1 class="heading-level-2 u-margin-block-start-auto">
|
||||
<div class="form-container u-width-full-line">
|
||||
<h1 class="heading-level-4 u-margin-block-start-auto is-not-mobile">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<h1 class="heading-level-5 u-margin-block-start-auto is-only-mobile">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<div class="u-margin-block-start-24">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<ul class="inline-links is-center is-with-sep u-margin-block-start-32">
|
||||
<ul
|
||||
class="inline-links is-center is-with-sep u-margin-block-start-32"
|
||||
class:u-hide={!$$slots.links}>
|
||||
<slot name="links" />
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="logo u-flex u-gap-16 u-margin-inline-auto is-only-mobile u-margin-block-start-32">
|
||||
<a href={user ? '/console' : '/'}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="93"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="93"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -127,4 +151,8 @@
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
max-inline-size: 27.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
let code: string;
|
||||
let error: string;
|
||||
|
||||
async function deleteProvider() {
|
||||
async function deleteAuthenticator() {
|
||||
try {
|
||||
await sdk.forConsole.account.deleteMfaAuthenticator(AuthenticatorType.Totp, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
@@ -41,18 +41,14 @@
|
||||
<Modal
|
||||
title="Delete authentication method"
|
||||
bind:show={showDelete}
|
||||
onSubmit={deleteProvider}
|
||||
onSubmit={deleteAuthenticator}
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
bind:error
|
||||
headerDivider={false}>
|
||||
<p class="u-bold">
|
||||
Deleting the authentication method will disable multi-factor authentication for your
|
||||
account. To re-enable it, you'll need to add a new authentication method.
|
||||
</p>
|
||||
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
|
||||
<FormList>
|
||||
<InputDigits autofocus required bind:value={code} />
|
||||
<InputDigits autofocus required bind:value={code} autoSubmit={false} />
|
||||
</FormList>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError } from '$lib/actions/analytics';
|
||||
import { Modal, Output, Copy, Alert } from '$lib/components';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { Modal, CopyInput } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, FormList, InputDigits } from '$lib/elements/forms';
|
||||
import { Table, TableBody, TableCell, TableRow } from '$lib/elements/table';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AuthenticatorType, type Models } from '@appwrite.io/console';
|
||||
import QrFrame from '$lib/images/qr2.svg';
|
||||
|
||||
export let showSetup = false;
|
||||
export let showRecoveryCodes = false;
|
||||
|
||||
let copyParent: HTMLElement;
|
||||
let code: string;
|
||||
let type: Models.MfaType = null;
|
||||
let step = 1;
|
||||
let codes: Models.MfaRecoveryCodes = null;
|
||||
let error = '';
|
||||
|
||||
async function addAuthenticator(): Promise<URL> {
|
||||
type = await sdk.forConsole.account.createMfaAuthenticator(AuthenticatorType.Totp);
|
||||
trackEvent(Submit.AccountAuthenticatorCreate);
|
||||
|
||||
return sdk.forConsole.avatars.getQR(type.uri, 192 * 2);
|
||||
}
|
||||
@@ -27,32 +26,27 @@
|
||||
async function verifyAuthenticator() {
|
||||
try {
|
||||
await sdk.forConsole.account.updateMfaAuthenticator(AuthenticatorType.Totp, code);
|
||||
codes = await sdk.forConsole.account.createMfaRecoveryCodes();
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
await invalidate(Dependencies.FACTORS);
|
||||
await Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
|
||||
showSetup = false;
|
||||
showRecoveryCodes = true;
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountDelete);
|
||||
trackEvent(Submit.AccountAuthenticatorUpdate);
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
trackError(e, Submit.AccountAuthenticatorUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Scan QR code"
|
||||
description="Open your authentication app and scan the QR code."
|
||||
title={step == 1 ? 'Scan QR code' : 'Enter verification code'}
|
||||
bind:show={showSetup}
|
||||
onSubmit={verifyAuthenticator}>
|
||||
onSubmit={verifyAuthenticator}
|
||||
{error}>
|
||||
{#key showSetup}
|
||||
<p>
|
||||
Install an authenticator app on your mobile device, open it and scan the provided QR
|
||||
code or enter it manually.
|
||||
</p>
|
||||
{#if step === 1}
|
||||
<p>
|
||||
Install an authenticator app on your mobile device, open it and scan the provided QR
|
||||
code or enter it manually.
|
||||
</p>
|
||||
{#await addAuthenticator()}
|
||||
<div class="loading">
|
||||
<div class="loader"></div>
|
||||
@@ -69,26 +63,12 @@
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
|
||||
<div bind:this={copyParent}>
|
||||
<label class="label" for="manual-code">Manual entry code</label>
|
||||
<button class="tooltip" aria-label="sec retinfo">
|
||||
<span class="icon-info" aria-hidden="true"></span>
|
||||
<span class="tooltip-popup" role="tooltip">
|
||||
<p class="text u-margin-block-start-8">
|
||||
Manually enter the following code into the authenticator app
|
||||
</p>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1">
|
||||
<input id="manual-code" type="text" value={type.secret} readonly={true} />
|
||||
<div class="options-list">
|
||||
{#key copyParent}
|
||||
<Copy value={type.secret} appendTo={copyParent}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
<CopyInput
|
||||
showLabel={true}
|
||||
label="Manual entry code"
|
||||
labelTooltip="Manually enter the following code into the authenticator app"
|
||||
value={type.secret}
|
||||
appendTo={copyParent} />
|
||||
</div>
|
||||
{/await}
|
||||
{:else}
|
||||
@@ -108,68 +88,6 @@
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="Save recovery codes"
|
||||
description="Learn more about multi-factor authentication in our documentation."
|
||||
bind:show={showRecoveryCodes}
|
||||
onSubmit={verifyAuthenticator}>
|
||||
{#if type && codes}
|
||||
{@const formattedBackupCodes = codes.recoveryCodes.join('\n')}
|
||||
<Alert type="info">
|
||||
<span slot="title">
|
||||
It is highly recommended to securely store your recovery codes
|
||||
</span>
|
||||
<p>
|
||||
Use security codes for emergency sign-ins in case you've lost access to your mobile
|
||||
device. Each recovery code can only be used once, but you can re-generate a new set
|
||||
of 6 codes anytime.
|
||||
</p>
|
||||
</Alert>
|
||||
<div
|
||||
style:flex-direction="row-reverse"
|
||||
class="u-flex u-flex-vertical-mobile u-main-space-between u-gap-16">
|
||||
<ul class="buttons-list">
|
||||
<li class="buttons-list-item">
|
||||
<Button
|
||||
download="backups.txt"
|
||||
href={`data:application/octet-stream;charset=utf-8,${formattedBackupCodes}`}
|
||||
text>
|
||||
<span class="icon-download" />
|
||||
<span class="text">Download</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li class="buttons-list-item">
|
||||
<Copy value={formattedBackupCodes} appendTo="parent">
|
||||
<Button text>
|
||||
<span class="icon-duplicate" />
|
||||
<span class="text">Copy all</span>
|
||||
</Button>
|
||||
</Copy>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Table noMargin noStyles>
|
||||
<TableBody>
|
||||
{#each codes.recoveryCodes as code}
|
||||
<TableRow>
|
||||
<TableCell title="code">
|
||||
<Output value={code} hideCopyIcon>{code}</Output>
|
||||
</TableCell>
|
||||
<TableCell title="actions" width={24}>
|
||||
<Copy value={code} appendTo="parent">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (showRecoveryCodes = false)}>Close</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.loading {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { Modal, Output, Copy, Alert } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { Table, TableBody, TableCell, TableRow } from '$lib/elements/table';
|
||||
import { type Models } from '@appwrite.io/console';
|
||||
|
||||
export let showRecoveryCodes = false;
|
||||
export let codes: Models.MfaRecoveryCodes = null;
|
||||
</script>
|
||||
|
||||
<Modal title="Recovery codes" bind:show={showRecoveryCodes}>
|
||||
{#if codes}
|
||||
{@const formattedBackupCodes = codes.recoveryCodes.join('\n')}
|
||||
<Alert type="warning">
|
||||
<span slot="title">
|
||||
Securely store your recovery codes as they won't be visible again for security
|
||||
purposes
|
||||
</span>
|
||||
<p>
|
||||
Use security codes for emergency sign-ins in case you've lost access to your mobile
|
||||
device. Each recovery code can only be used once.
|
||||
</p>
|
||||
</Alert>
|
||||
<div
|
||||
style:flex-direction="row-reverse"
|
||||
class="u-flex u-flex-vertical-mobile u-main-space-between u-gap-16">
|
||||
<ul class="buttons-list">
|
||||
<li class="buttons-list-item">
|
||||
<Button
|
||||
download="backups.txt"
|
||||
href={`data:application/octet-stream;charset=utf-8,${formattedBackupCodes}`}
|
||||
text>
|
||||
<span class="icon-download u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Download .txt</span>
|
||||
</Button>
|
||||
</li>
|
||||
<li class="buttons-list-item">
|
||||
<Copy value={formattedBackupCodes} appendTo="parent">
|
||||
<Button text>
|
||||
<span class="icon-duplicate" />
|
||||
<span class="text">Copy all</span>
|
||||
</Button>
|
||||
</Copy>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Table noMargin noStyles>
|
||||
<TableBody>
|
||||
{#each codes.recoveryCodes as code}
|
||||
<TableRow>
|
||||
<TableCell title="code">
|
||||
<Output value={code} hideCopyIcon>{code}</Output>
|
||||
</TableCell>
|
||||
<TableCell title="actions" width={24}>
|
||||
<Copy value={code} appendTo="parent">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
</Modal>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputDigits } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { AuthenticationFactor } from '@appwrite.io/console';
|
||||
|
||||
export let show = false;
|
||||
export let regenerateRecoveryCodes: () => Promise<void>;
|
||||
|
||||
let code = '';
|
||||
let error = '';
|
||||
|
||||
async function verify() {
|
||||
try {
|
||||
const challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
show = false;
|
||||
regenerateRecoveryCodes();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
title="Regenerate recovery codes"
|
||||
icon="exclamation"
|
||||
state="warning"
|
||||
headerDivider={false}
|
||||
{error}
|
||||
onSubmit={verify}
|
||||
bind:show>
|
||||
<p>
|
||||
Are you sure you want to regenerate all recovery codes? All <b
|
||||
>previously generated recovery codes will become invalid.</b>
|
||||
</p>
|
||||
<p>Enter the 6-digit verification code generated by your authenticator app to continue.</p>
|
||||
<FormList>
|
||||
<InputDigits bind:value={code} required autofocus autoSubmit={false} />
|
||||
</FormList>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button secondary submit>Regenerate</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
@@ -2,4 +2,7 @@ import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
|
||||
export const factors = derived(page, ($page) => $page.data.factors as Models.MfaFactors);
|
||||
export const factors = derived(
|
||||
page,
|
||||
($page) => $page.data.factors as Models.MfaFactors & { recoveryCode: boolean }
|
||||
);
|
||||
@@ -1,39 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
|
||||
import { CardGrid, Heading, Empty } from '$lib/components';
|
||||
import { CardGrid, EyebrowHeading, Heading } from '$lib/components';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Button, FormList, InputChoice } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { user } from '$lib/stores/user';
|
||||
import Mfa from './mfa.svelte';
|
||||
import DeleteMfa from './deleteMfa.svelte';
|
||||
import { factors } from './store';
|
||||
import MfaRecoveryCodes from './mfaRecoveryCodes.svelte';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import MfaRegenerateCodes from './mfaRegenerateCodes.svelte';
|
||||
|
||||
let showSetup: boolean = false;
|
||||
let showDelete: boolean = false;
|
||||
let showRecoveryCodes = false;
|
||||
let showRegenerateRecoveryCodes = false;
|
||||
let codes: Models.MfaRecoveryCodes = null;
|
||||
|
||||
async function updateMfa() {
|
||||
try {
|
||||
await sdk.forConsole.account.updateMFA(!$user.mfa);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountUpdateEmail);
|
||||
trackEvent(Submit.AccountUpdateMfa, { mfa: !$user.mfa });
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
message: error.message,
|
||||
type: 'error'
|
||||
});
|
||||
trackError(error, Submit.AccountUpdateEmail);
|
||||
trackError(error, Submit.AccountUpdateMfa);
|
||||
}
|
||||
}
|
||||
|
||||
async function createRecoveryCodes() {
|
||||
try {
|
||||
codes = await sdk.forConsole.account.createMfaRecoveryCodes();
|
||||
showRecoveryCodes = true;
|
||||
Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
|
||||
trackEvent(Submit.AccountRecoveryCodesCreate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountRecoveryCodesCreate);
|
||||
}
|
||||
}
|
||||
|
||||
async function regenerateRecoveryCodes() {
|
||||
try {
|
||||
codes = await sdk.forConsole.account.updateMfaRecoveryCodes();
|
||||
showRecoveryCodes = true;
|
||||
Promise.all([invalidate(Dependencies.ACCOUNT), invalidate(Dependencies.FACTORS)]);
|
||||
trackEvent(Submit.AccountRecoveryCodesUpdate);
|
||||
} catch (error) {
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountRecoveryCodesUpdate);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -42,49 +69,118 @@
|
||||
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
|
||||
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputChoice
|
||||
on:change={updateMfa}
|
||||
type="switchbox"
|
||||
id="mfa"
|
||||
label="Multi-factor authentication"
|
||||
value={$user.mfa} />
|
||||
</FormList>
|
||||
<p>Enhance the security of your account by adding authentication factors.</p>
|
||||
{#if $user.mfa}
|
||||
{#if $factors.totp}
|
||||
<Table noMargin noStyles transparent>
|
||||
<TableHeader>
|
||||
<TableCellHead>Authenticator</TableCellHead>
|
||||
<TableCellHead width={40} />
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCellText title="Authenticator">
|
||||
TOTP (One-time code)
|
||||
</TableCellText>
|
||||
<TableCell>
|
||||
<Button
|
||||
on:click={() => (showDelete = true)}
|
||||
round
|
||||
text
|
||||
ariaLabel="Delete authenticator">
|
||||
<span class="icon-trash" aria-hidden="true" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button disabled text noMargin>
|
||||
<span class="icon-plus" />
|
||||
<span class="text">Add authentication factor</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<Empty on:click={() => (showSetup = true)}>
|
||||
<p class="text">Add authentication factor</p>
|
||||
</Empty>
|
||||
{/if}
|
||||
{/if}
|
||||
<div class="u-flex u-flex-vertical u-gap-32">
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<FormList>
|
||||
<InputChoice
|
||||
on:change={updateMfa}
|
||||
type="switchbox"
|
||||
id="mfa"
|
||||
label="Multi-factor authentication"
|
||||
value={$user.mfa} />
|
||||
</FormList>
|
||||
<p>
|
||||
Two-factor authentication enhances your account's security by requiring a second
|
||||
sign-in method. <Button
|
||||
link
|
||||
external
|
||||
href="https://appwrite.io/docs/products/auth/2fa">Learn more.</Button>
|
||||
</p>
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<div class="u-sep-block-end" style="padding-block-end: 8px;">
|
||||
<EyebrowHeading tag="h6" size={3} class="u-normal">Methods</EyebrowHeading>
|
||||
</div>
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-device-mobile" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Authenticator app</span>
|
||||
<span
|
||||
>Use an authentication app to generate two-factor authentication
|
||||
codes.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-button">
|
||||
{#if $factors.totp}
|
||||
<Button text secondary on:click={() => (showDelete = true)}
|
||||
>Delete</Button>
|
||||
{:else}
|
||||
<Button secondary on:click={() => (showSetup = true)}>Add</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $factors.email}
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-mail" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Email verification</span>
|
||||
<span>One-time codes will be sent to: {$user.email}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $factors.phone}
|
||||
<div
|
||||
class="u-flex u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-send" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">SMS verification</span>
|
||||
<span>One-time codes will be sent to: {$user.phone}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="u-flex-vertical u-gap-16">
|
||||
<div class="u-sep-block-end" style="padding-block-end: 8px;">
|
||||
<EyebrowHeading tag="h6" size={3} class="u-normal">Recovery</EyebrowHeading>
|
||||
</div>
|
||||
<div
|
||||
class="method u-flex u-flex-vertical-mobile u-gap-16 u-main-space-between u-sep-block-end"
|
||||
style="padding-block-end: 16px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<div class="avatar is-size-x-small">
|
||||
<span class="icon-lock-open" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-flex-vertical u-gap-4 body-text-2">
|
||||
<span class="u-bold">Recovery codes</span>
|
||||
<span
|
||||
>Use in case you can't receive two-factor authentication codes.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-button">
|
||||
{#if $factors.recoveryCode}
|
||||
<Button
|
||||
class="method-button"
|
||||
text
|
||||
secondary
|
||||
on:click={() => (showRegenerateRecoveryCodes = true)}
|
||||
>Regenerate</Button>
|
||||
{:else}
|
||||
<Button class="method-button" secondary on:click={createRecoveryCodes}
|
||||
>View</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</CardGrid>
|
||||
|
||||
@@ -92,3 +188,25 @@
|
||||
<Mfa bind:showSetup />
|
||||
{/if}
|
||||
<DeleteMfa bind:showDelete />
|
||||
|
||||
<MfaRecoveryCodes bind:showRecoveryCodes {codes} />
|
||||
<MfaRegenerateCodes bind:show={showRegenerateRecoveryCodes} {regenerateRecoveryCodes} />
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
/* Default (including mobile) */
|
||||
.method {
|
||||
align-items: start;
|
||||
.method-button {
|
||||
margin-inline-start: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* for smaller screens */
|
||||
@media #{$break2open} {
|
||||
.method {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -50,7 +50,7 @@
|
||||
<Form onSubmit={updateMfa}>
|
||||
<CardGrid>
|
||||
<Heading tag="h6" size="7">Multi-factor authentication</Heading>
|
||||
<p class="text">MFA allow users to enhance the security of their account in you app.</p>
|
||||
<p class="text">MFA allows users to enhance the security of their accounts in your app.</p>
|
||||
<svelte:fragment slot="aside">
|
||||
<FormList>
|
||||
<InputChoice
|
||||
@@ -88,8 +88,8 @@
|
||||
{:else}
|
||||
<EmptySearch hidePagination hidePages>
|
||||
<p class="text u-text-center">
|
||||
No authenticators have been enabled. Once the user adds an authenticator,
|
||||
you’ll see it here.
|
||||
No authenticators have been enabled.<br />Once the user adds an
|
||||
authenticator, you'll see it here.
|
||||
</p>
|
||||
</EmptySearch>
|
||||
{/if}
|
||||
|
||||
+152
-9
@@ -1,24 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Button, Form, FormItem, FormList } from '$lib/elements/forms';
|
||||
import { Button, Form, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Unauthenticated } from '$lib/layout';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { page } from '$app/stores';
|
||||
import { AuthenticationFactor } from '@appwrite.io/console';
|
||||
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
|
||||
let code: string, disabled: boolean;
|
||||
export let data;
|
||||
|
||||
let code: string;
|
||||
let disabled: boolean;
|
||||
let challenge: Models.MfaChallenge;
|
||||
let challengeType: AuthenticationFactor;
|
||||
|
||||
const factors = data.factors as Models.MfaFactors & { recoveryCode: boolean };
|
||||
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
|
||||
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const enabledNonRecoveryFactors = enabledFactors.filter(
|
||||
([factor, _]) => factor != 'recoveryCode'
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length == 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function back() {
|
||||
await sdk.forConsole.account.deleteSession('current');
|
||||
await goto(`${base}/`);
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
try {
|
||||
disabled = true;
|
||||
const challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
if (challenge == null) {
|
||||
await createChallenge(AuthenticationFactor.Totp);
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
@@ -49,16 +80,128 @@
|
||||
</svelte:head>
|
||||
|
||||
<Unauthenticated>
|
||||
<svelte:fragment slot="top">
|
||||
<div class="top u-flex u-position-absolute u-main-center">
|
||||
<div class="flex u-width-full-line">
|
||||
<Button text noMargin class="u-border-width-0" on:click={back}>
|
||||
<span class="icon-cheveron-left u-font-size-20" aria-hidden="true" />
|
||||
Back</Button>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">Verify your identity</svelte:fragment>
|
||||
<svelte:fragment>
|
||||
<Form onSubmit={verify}>
|
||||
<FormList>
|
||||
<p>Enter below a 6-digit one-time code generated by your authentication app.</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
|
||||
<p class="body-text-1">
|
||||
Enter below a 6-digit one-time code generated by your authentication app.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Email}
|
||||
<p class="body-text-1">
|
||||
A 6-digit verification code was sent to your email, enter it below.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p class="body-text-1">
|
||||
A 6-digit verification code was sent to your phone, enter it below.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Recoverycode}
|
||||
<p class="body-text-1">
|
||||
Enter below one of the recovery codes you received when enabling MFA for
|
||||
this account.
|
||||
</p>
|
||||
<InputText id="recovery-code" bind:value={code} required autofocus />
|
||||
{/if}
|
||||
<FormItem>
|
||||
<Button fullWidth submit {disabled}>Verify</Button>
|
||||
</FormItem>
|
||||
{#if enabledFactors.length > 1}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
{#if factors.totp && challengeType != null && challengeType != AuthenticationFactor.Totp}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
|
||||
<span
|
||||
class="icon-device-mobile u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
<span class="text">Authenticator app</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.email && challengeType != AuthenticationFactor.Email}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Email)}>
|
||||
<span class="icon-mail u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Email verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.phone && challengeType != AuthenticationFactor.Phone}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
|
||||
<span class="icon-chat-alt u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Phone verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.recoveryCode && challengeType != AuthenticationFactor.Recoverycode}
|
||||
<FormItem>
|
||||
<Button
|
||||
text
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() =>
|
||||
createChallenge(AuthenticationFactor.Recoverycode)}>
|
||||
<span class="text">Use recovery code</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
</Form>
|
||||
</svelte:fragment>
|
||||
</Unauthenticated>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
.top {
|
||||
inset-block-start: 5.85rem;
|
||||
inline-size: 100%;
|
||||
max-inline-size: 27.5rem;
|
||||
|
||||
div {
|
||||
max-inline-size: 27.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.top {
|
||||
inset-block-start: 0rem;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* for smaller screens */
|
||||
@media #{$break2open} {
|
||||
.top {
|
||||
inset-block-start: 5.85rem;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
return {
|
||||
factors: await sdk.forConsole.account.listMfaFactors()
|
||||
};
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 86 KiB |
Reference in new issue
Block a user