Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mfa #789

Merged
merged 6 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/components/eventModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@
</div>
{:else}
<div class="input-text-wrapper" style="--amount-of-buttons:2" bind:this={copyParent}>
<!--
<!--
This object syntax avoids TS erroring because 'type' isn't a valid HTMLDivElement attribute
(we need to set it to 'text' to add styling)
-->
Expand Down
2 changes: 2 additions & 0 deletions src/lib/elements/forms/button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
export let disabled = false;
export let external = false;
export let href: string = null;
export let download: string = undefined;
export let fullWidth = false;
export let fullWidthMobile = false;
export let ariaLabel: string = null;
Expand Down Expand Up @@ -63,6 +64,7 @@
on:click
on:click={track}
{href}
{download}
target={external ? '_blank' : ''}
rel={external ? 'noopener noreferrer' : ''}
class={resolvedClasses}
Expand Down
18 changes: 18 additions & 0 deletions src/lib/images/qr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 10 additions & 4 deletions src/routes/+layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,19 @@ export const load: LayoutLoad = async ({ depends, url }) => {
'/auth/oauth2/success',
'/auth/oauth2/failure',
'/card',
'/hackathon'
'/hackathon',
'/mfa'
];

const redirectUrl = url.pathname && url.pathname !== '/' ? `redirect=${url.pathname}` : '';
const path = url.search ? `${url.search}&${redirectUrl}` : `?${redirectUrl}`;

if (error.type === 'user_more_factors_required') {
if (url.pathname === '/mfa') return;
throw redirect(303, `/mfa${path}`);
}

if (!acceptedRoutes.some((n) => url.pathname.startsWith(n))) {
const redirectUrl =
url.pathname && url.pathname !== '/' ? `redirect=${url.pathname}` : '';
const path = url.search ? `${url.search}&${redirectUrl}` : `?${redirectUrl}`;
throw redirect(303, `/login${path}`);
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/routes/console/account/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import UpdateName from './updateName.svelte';
import UpdateEmail from './updateEmail.svelte';
import DeleteAccount from './deleteAccount.svelte';
import UpdateMfa from './updateMfa.svelte';
</script>

<Container>
<UpdateName />
<UpdateEmail />
<UpdatePassword />
<UpdateMfa />
<DeleteAccount />
</Container>
59 changes: 59 additions & 0 deletions src/routes/console/account/deleteMfa.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { AuthenticatorProvider } from '@appwrite.io/console';

export let showDelete = false;

let code: string;

async function deleteProvider() {
try {
await sdk.forConsole.account.deleteAuthenticator(AuthenticatorProvider.Totp, code);
await invalidate(Dependencies.ACCOUNT);
showDelete = false;
addNotification({
type: 'success',
message: 'The authenticator has been removed'
});
trackEvent(Submit.AccountDelete);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AccountDelete);
}
}
</script>

<Modal
title="Delete authentication provider"
bind:show={showDelete}
onSubmit={deleteProvider}
icon="exclamation"
state="warning"
headerDivider={false}>
<p>Are you sure you want to delete this authentication method from your account?</p>
<div>
<label for="code">Enter the 6-digit one-time code generated by the app</label>
<input
required
bind:value={code}
id="code"
class="u-margin-block-start-12"
type="text"
placeholder="Enter code"
minlength="6"
maxlength="6" />
</div>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
</svelte:fragment>
</Modal>
163 changes: 163 additions & 0 deletions src/routes/console/account/mfa.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError } from '$lib/actions/analytics';
import { Modal, Output, Copy, Alert } from '$lib/components';
import LoadingDots from '$lib/components/loadingDots.svelte';
import { Dependencies } from '$lib/constants';
import { Button } 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 { AuthenticatorFactor, type Models } from '@appwrite.io/console';

export let showSetup = false;
export let showRecoveryCodes = false;

let code: string;
let provider: Models.MfaProvider = null;
async function addAuthenticator(): Promise<URL> {
provider = await sdk.forConsole.account.addAuthenticator(AuthenticatorFactor.Totp);

return sdk.forConsole.avatars.getQR(provider.uri, 192 * 2);
}

async function verifyAuthenticator() {
try {
await sdk.forConsole.account.verifyAuthenticator(AuthenticatorFactor.Totp, code);
await invalidate(Dependencies.ACCOUNT);
showSetup = false;
showRecoveryCodes = true;
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
trackError(error, Submit.AccountDelete);
}
}
</script>

<Modal
title="Scan QR code"
description="Open your authentication app and scan the QR code."
bind:show={showSetup}
onSubmit={verifyAuthenticator}>
{#key showSetup}
<p>
Install an authenticator app on your mobile device, open it and scan the provided QR
code or enter it manually.
</p>
<div class="qr">
{#await addAuthenticator()}
<LoadingDots />
{:then qr}
<img alt="MFA QR Code" class="code" src={qr.toString()} />
{/await}
</div>
<hr />
<div>
<label for="code">Enter the 6-digit one-time code generated by the app</label>
<input
required
bind:value={code}
id="code"
class="u-margin-block-start-12"
type="text"
placeholder="Enter code"
minlength="6"
maxlength="6" />
</div>
{/key}
<svelte:fragment slot="footer">
<Button text on:click={() => (showSetup = false)}>Cancel</Button>
<Button submit>Continue</Button>
</svelte:fragment>
</Modal>

<Modal
title="Save recovery codes"
description="Learn more about multi-factor authentication in our documentation."
bind:show={showRecoveryCodes}
onSubmit={verifyAuthenticator}>
{#if provider}
{@const formattedBackupCodes = provider.backups.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 provider.backups 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">
.qr {
width: 100%;
height: 220px;
display: flex;
align-items: center;

.code {
width: 100%;
max-width: 192px;
aspect-ratio: 1;
margin: 0 auto;
display: block;
}
}

hr {
height: 1px;
width: calc(100% + 2rem);
background-color: hsl(var(--color-border));

margin-block-start: 1rem;
margin-inline: -1rem;
}
</style>
Loading
Loading