Skip to content

Commit

Permalink
fix: Add security key flow to profile page
Browse files Browse the repository at this point in the history
  • Loading branch information
karthiksivanadiyan committed Jun 27, 2024
1 parent cc37494 commit 3b5d2c5
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 136 deletions.
8 changes: 6 additions & 2 deletions apps/console/src/lib/schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,16 @@ export const changeEmailSchema = userSchema.pick({ email: true });

export const webAuthnSchema = z.object({
nickname: z
.string({ required_error: 'Nickname is required' })
.min(2, { message: 'Nickname must contain at least 2 character(s)' })
.string({ required_error: 'A security key nickname is required' })
.min(2, { message: 'The security key nickname must contain at least 2 character(s)' })
.max(256)
.trim(),
});

export type webAuthnSchema = typeof webAuthnSchema;
export type WebAuthn = z.infer<typeof webAuthnSchema>;
export const addWebAuthnKeys = webAuthnSchema.keyof().Enum;

/**
* Refine functions
*/
Expand Down
214 changes: 86 additions & 128 deletions apps/console/src/routes/(app)/profile/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,59 +1,70 @@
<script lang="ts">
import { page } from '$app/stores';
import { Meta } from '$lib/components';
import { elevate, nhost, user } from '$lib/stores/user';
import type { AuthErrorPayload } from '@nhost/nhost-js';
import { Debug } from '@spectacular/skeleton/components';
import type { PageData } from './$houdini';
import PersonalAccessTokens from './personal-access-tokens.svelte';
import Providers from './providers.svelte';
import SecurityKeys from './security-keys.svelte';
import UserOrgRoles from './user-org-roles.svelte';
// https://github.com/nhost/nhost/blob/main/examples/react-apollo/src/profile/security-keys.tsx
export let data: PageData;
$: ({ GetUser } = data);
$: userOrgRoles = $GetUser.data?.user?.userOrgRoles ?? [];
$: userProviders = $GetUser.data?.user?.userProviders ?? [];
$: personalAccessTokens = $GetUser.data?.user?.personalAccessTokens ?? [];
$: securityKeys = $GetUser.data?.user?.securityKeys ?? [];
// Variables
let nickname: string;
let error: AuthErrorPayload | null;
// Functions
async function addSecurityKey() {
error = await elevate();
if (error) {
console.log(error);
return;
import { page } from "$app/stores";
import { Meta } from "$lib/components";
import { nhost, user, elevate } from "$lib/stores/user";
import type { AuthErrorPayload } from "@nhost/nhost-js";
import type { PageData } from "./$houdini";
import { invalidateAll } from "$app/navigation";
import AddSecurityKeyForm from "./AddSecurityKeyForm.svelte";
import { Trash } from "lucide-svelte";
// https://github.com/nhost/nhost/blob/main/examples/react-apollo/src/profile/security-keys.tsx
export let data: PageData;
$: ({ GetUser } = data);
// $: userOrgRoles = $GetUser.data?.user?.userOrgRoles ?? [];
// $: userProviders = $GetUser.data?.user?.userProviders ?? [];
// $: personalAccessTokens = $GetUser.data?.user?.personalAccessTokens ?? [];
$: securityKeys = $GetUser.data?.user?.securityKeys ?? [];
// Variables
let nickname: string;
let error: AuthErrorPayload | null;
async function handleAdd() {
console.log({ nickname });
const { key, error: addKeyError } =
await nhost.auth.addSecurityKey(nickname);
if (error) {
console.log(error);
error = addKeyError;
return;
}
await invalidateAll();
}
const { key, error: addKeyError } = await nhost.auth.addSecurityKey(nickname);
// Something unexpected happened
if (error) {
console.log(error);
error = addKeyError;
return;
async function handleElevate() {
error = await elevate();
if (!error) {
// TODO notify
console.log("elevated successfully");
}
}
// Successfully added a new security key
console.log(key?.id);
}
async function handleElevate() {
error = await elevate();
if (!error) {
// TODO notify
console.log('elevated successfully');
async function handleDelete(id: string) {
const error = await elevate();
if (error) {
console.log(error);
return;
}
const { data, error: removeError } = await nhost.graphql.request(
"mutation RemoveSecurityKey($id: uuid!) {\r\n deleteAuthUserSecurityKey(id: $id) {\r\n id\r\n }\r\n}",
{ id },
);
if (removeError) {
console.log(error);
}
if (data) {
console.log(data);
await invalidateAll();
}
}
}
// Reactivity
$: meta = {
title: 'Datablocks | Profile',
canonical: $page.url.toString(),
};
// Reactivity
$: meta = {
title: "Datablocks | Profile",
canonical: $page.url.toString(),
};
</script>

<Meta {...meta} />
Expand All @@ -62,81 +73,28 @@ $: meta = {
<meta name="description" content="Edit Profile" />
</svelte:head>

<div class="page-container">
<section class="space-y-4">
<h1 class="h1">Profile</h1>
<p>Update your profile details</p>
</section>

{#if error}
<section class="space-y-4">
<h2 class="h2">Error</h2>
<pre>{JSON.stringify(error, null, 2)}</pre>
</section>
{/if}

{#if $GetUser.fetching}
<span>loading...</span>
{:else}

<section class="space-y-4">
<h2 class="h2">Contact</h2>
<p>Update user contact details</p>
<div class="card p-4">
<pre>{JSON.stringify($GetUser.data?.user, null, 2)}</pre>
</div>
</section>

<section class="space-y-4">
<h2 class="h2">User Org Roles</h2>
<p>Add or delete user org roles</p>
<UserOrgRoles {userOrgRoles} ></UserOrgRoles>
</section>

<section class="space-y-4">
<h2 class="h2">Auth Providers</h2>
<p>Add or delete auth providers</p>
<Providers {userProviders} ></Providers>
</section>

<section class="space-y-4">
<h2 class="h2">Personal Access Tokens</h2>
<p>Add are delete your personal access tokens(PAT)</p>
<PersonalAccessTokens {personalAccessTokens} ></PersonalAccessTokens>
</section>

<section class="space-y-4">
<h2 class="h2">Security Keys</h2>
<p>Add are delete your security keys like TouchID, FaceID, YubiKeys etc</p>
<SecurityKeys {securityKeys} />
</section>


<section class="space-y-4">
<h2 class="h2">WebAuthn</h2>
<p>Add are delete your security keys</p>
<div class="card p-4">
<title>Security keys</title>
<form class="space-y-4" on:submit|preventDefault={addSecurityKey}>
<input
bind:value={nickname}
placeholder='Nickname for the device (optional)'
class="block w-full p-3 border rounded-md border-slate-300 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
/>
<button type="submit" class="btn variant-filled">Add a new device</button>
</form>
</div>
</section>

<section class="space-y-4">
<h2 class="h2">Elevate</h2>
<p>Add are delete your security keys</p>
<div class="card p-4">
<!-- <span>Elevated permissions: {String(elevated)}</span> -->
<button type="button" class="btn variant-filled" on:click={handleElevate} >Elevate</button>
</div>
</section>

{/if}
</div>

{#if $GetUser.fetching}
<span>loading...</span>
{:else}
<div class="page-container">
<h1 class="h1">Profile</h1>
<h2 class="h2">Security Keys</h2>

<AddSecurityKeyForm data={data.addSecurityKeyForm} />

<ul class="list">
{#each securityKeys as { id, nickname }}
<li>
<button
type="button"
class="btn-icon btn-icon-sm variant-filled"
on:click={handleDelete(id)}
>
<Trash class="text-red-500 w-5 h-5" />
</button>
<span class="flex-auto">{nickname}</span>
</li>
{/each}
</ul>
</div>
{/if}
7 changes: 2 additions & 5 deletions apps/console/src/routes/(app)/profile/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export async function _houdini_beforeLoad({ url }: BeforeLoadEvent) {
if (!get(isAuthenticated)) redirect(302, i18n.resolveRoute('/signin?redirectTo=/profile'));
const cpForm = await superValidate(zod(changePasswordSchema));
const ceForm = await superValidate(zod(changeEmailSchema));
const waForm = await superValidate(zod(webAuthnSchema));
return { cpForm, ceForm, waForm };
const addSecurityKeyForm = await superValidate(zod(webAuthnSchema));
return { cpForm, ceForm, addSecurityKeyForm };
}

// export async function _houdini_afterLoad({ event, input, data }: AfterLoadEvent) {
// }
97 changes: 97 additions & 0 deletions apps/console/src/routes/(app)/profile/AddSecurityKeyForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script lang="ts">
import type { SuperValidated, Infer } from "sveltekit-superforms";
import { setError, setMessage, superForm } from "sveltekit-superforms";
import {
addWebAuthnKeys,
webAuthnSchema,
type WebAuthn,
} from "$lib/schema/user";
import InputFieldWithErrors from "./InputFieldWithErrors.svelte";
import { zod } from "sveltekit-superforms/adapters";
import { nhost, elevate } from "$lib/stores/user";
import type { AuthErrorPayload } from "@nhost/nhost-js";
import { Debug } from "@spectacular/skeleton";
export let data: SuperValidated<WebAuthn>;
let error: AuthErrorPayload | null;
const superform = superForm(data, {
SPA: true,
resetForm: true,
validators: zod(webAuthnSchema),
async onUpdate({ form }) {
error = await elevate();
if (error) {
console.log(error);
return;
}
const { key, error: addKeyError } = await nhost.auth.addSecurityKey(
$formData.nickname,
);
// Something unexpected happened
if (error) {
console.log(error);
error = addKeyError;
return;
}
// Successfully added a new security key
console.log(key?.id);
},
});
const {
allErrors,
capture,
constraints,
delayed,
enhance,
errors,
form: formData,
message,
posted,
restore,
reset,
submitting,
tainted,
} = superform;
</script>

<Debug
data={{
$allErrors,
$constraints,
$delayed,
$errors,
$formData,
$message,
$posted,
$submitting,
$tainted,
}}
class="mb-5"
/>

{#if $message || $errors._errors}
<aside class="alert variant-ghost">
<div class="alert-message">
{#if $message}
<h3 class="h3">{message}</h3>
{:else if $errors._errors}
<ul class="ml-2 list-outside list-none">
{#each $errors._errors as gqlErrorMsg}
<li>{gqlErrorMsg.message}</li>
{/each}
</ul>
{/if}
</div>
</aside>
{/if}

<form method="post" use:enhance>
<InputFieldWithErrors
form={superform}
name={addWebAuthnKeys["nickname"]}
placeholder={"Add a security key"}
/>
</form>
Loading

0 comments on commit 3b5d2c5

Please sign in to comment.