Skip to content
This repository has been archived by the owner on Sep 29, 2024. It is now read-only.

Commit

Permalink
feat: Add registering passkeys to /settings
Browse files Browse the repository at this point in the history
* useQuery + bare axios -> useMutation x2

Signed-off-by: AlexNg <[email protected]>
  • Loading branch information
caffeine-addictt committed Aug 10, 2024
1 parent 1660549 commit 3a2f2a7
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 70 deletions.
66 changes: 3 additions & 63 deletions client/src/pages/auth/passkey.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@ import * as z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, useFormState } from 'react-hook-form';

import {
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser';
import { startAuthentication } from '@simplewebauthn/browser';

import { AxiosError, isAxiosError } from 'axios';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import httpClient, { APIPayload } from '@utils/http';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import httpClient from '@utils/http';
import { auth, schemas } from '@lib/api-types';

import {
Expand All @@ -37,63 +34,6 @@ import { Button } from '@components/ui/button';
import { useToast } from '@components/ui/use-toast';
import { KeyRoundIcon, LoaderIcon } from 'lucide-react';

export const PasskeyRegisterPage: PageComponent = ({ className, ...props }) => {
const queryClient = useQueryClient();

const { refetch } = useQuery(
{
queryKey: ['register-passkey'],
queryFn: async () => {
const resp = await httpClient
.post<auth.RegisterPasskeysStartSuccAPI, APIPayload>({
uri: '/auth/register/passkeys/start',
withCredentials: 'access',
})
.then((res) => res.data)
.catch((err: AxiosError) => console.error(err));

if (!resp) return false;

let authResp;
try {
authResp = await startRegistration(resp.challenge);
} catch (err) {
console.error('Failed to register passkey:', err);
return false;
}

// Verify passkey
const verifyResp = await httpClient
.post<
auth.RegisterPasskeysFinishSuccAPI,
schemas.auth.passkeyRegisterFinishSchema
>({
uri: '/auth/register/passkeys/finish',
withCredentials: 'access',
payload: {
track: resp.track,
signed: authResp,
},
})
.then((res) => res.data)
.catch((err: AxiosError) => console.error(err));

if (!verifyResp) return false;
console.log('Successfully registered passkey:', verifyResp);
return true;
},
enabled: false,
},
queryClient,
);

return (
<div className={className} {...props}>
<Button onClick={() => refetch()}>Test register passkey</Button>
</div>
);
};

export const PasskeyLoginPage: PageComponent = (props) => {
const queryClient = useQueryClient();

Expand Down
8 changes: 1 addition & 7 deletions client/src/pages/auth/route-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import LoginPage from './legacy-login';
import RegisterPage from './legacy-register';

import ActivatePage from './activate';
import { PasskeyLoginPage, PasskeyRegisterPage } from './passkey';
import { PasskeyLoginPage } from './passkey';
import RedirectToAuth from './auth-redirect';

const authRouteMap: RouteMap = {
Expand Down Expand Up @@ -64,12 +64,6 @@ const authRouteMap: RouteMap = {
component: PasskeyLoginPage,
accessLevel: 'public-only',
},
'/auth0/register': {
title: 'Passkey',
description: 'Passkey',
component: PasskeyRegisterPage,
accessLevel: 'authenticated',
},
'/auth0': {
title: 'Login',
description: 'Login to your account with email and password',
Expand Down
98 changes: 98 additions & 0 deletions client/src/pages/user/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,95 @@ export const AccountPasskeys = () => {
queryClient,
);

// Finish passkey registration
const { mutate: finishRegistrationFlow, isPending: isRegisteringFinish } =
useMutation(
{
mutationKey: ['finish-registration'],
mutationFn: (data: passkeyRegisterFinishSchema) => {
toast({
title: 'Registering passkey...',
description: 'Please wait, this may take a few seconds',
});
return httpClient.post<
RegisterPasskeysFinishSuccAPI,
passkeyRegisterFinishSchema
>({
uri: '/auth/register/passkeys/finish',
withCredentials: 'access',
payload: data,
});
},
onSuccess: () => {
toast({
title: 'Passkey registered',
description: 'Passkey registered successfully',
});
refetch();
},
onError: (err) => {
console.log(err);
toast({
title: 'Something went wrong',
description: isAxiosError(err)
? err.response?.data.errors[0].message
: 'Please try again later',
variant: 'destructive',
});
},
},
queryClient,
);

// Start passkey registration
const { mutate: startRegistrationFlow, isPending: isRegisteringStart } =
useMutation(
{
mutationKey: ['start-registration'],
mutationFn: async () => {
toast({
title: 'Firing up our servers...',
description: 'Please wait, this may take a few seconds',
});
const res = await httpClient.post<
RegisterPasskeysStartSuccAPI,
APIPayload
>({
uri: '/auth/register/passkeys/start',
withCredentials: 'access',
});
return res.data;
},
onSuccess: (data) =>
startRegistration(data.challenge)
.then((authResp) => {
finishRegistrationFlow({
track: data.track,
signed: authResp,
});
})
.catch((err) => {
console.error('Failed to start passkey:', err);
toast({
title: 'Something went wrong',
description: 'Please try again later',
variant: 'destructive',
});
}),
onError: (err) => {
console.log(err);
toast({
title: 'Something went wrong',
description: isAxiosError(err)
? err.response?.data.errors[0].message
: 'Please try again later',
variant: 'destructive',
});
},
},
queryClient,
);

return (
<div className="my-7 flex w-full flex-col justify-center gap-2 rounded-md border-2 border-secondary-light bg-primary-dark p-4">
<h2 className="mb-5 text-3xl font-bold text-text-dark dark:text-text-light">
Expand Down Expand Up @@ -166,6 +255,15 @@ export const AccountPasskeys = () => {
</Button>
</div>
))}

<Button
type="button"
disabled={isRegisteringStart || isRegisteringFinish}
onClick={() => startRegistrationFlow()}
>
<KeyRoundIcon className="mr-2 size-6" />
Register a new passkey
</Button>
</div>
);
};
Expand Down

0 comments on commit 3a2f2a7

Please sign in to comment.