From b261e7e4362bc97751a181c7907e5edc9aa79abd Mon Sep 17 00:00:00 2001 From: Vivian Phung Date: Fri, 21 Jun 2024 21:10:31 -0400 Subject: [PATCH] Feat: Access Code Authentication Flow (#211) ## What changed? This Pull Request introduces an access code validation feature to the authentication process. Changes encompass backend route for access code validation, new frontend components for handling access code input, and integration of the access code verification in the signup flow. ### Backend: - Added POST `/accesscode` route in `auth.ts` for validating access codes. ### Frontend: - Created `AccessCode` component for access code input and validation. - Added `AccessSignUp` component that integrates access code verification before signup. - Updated `SignUp` component to check for valid access code on mount. - Modified `SnowballAuth` to use new `AccessSignUp` instead of `SignUp`. - Added `verifyAccessCode` utility function for code verification API call. ## How to test? 1. Run the backend and frontend projects. 2. Navigate to the signup page. You should be prompted to enter an access code. 3. Enter the code `444444` and proceed. Any other code should display an error message. 4. Verify that valid access code routes to the signup component. ## Why make this change? This change improves authentication by adding an extra layer of security through access code verification. --- packages/backend/src/routes/auth.ts | 10 ++ .../frontend/src/pages/auth/AccessCode.tsx | 94 +++++++++++++++++++ .../frontend/src/pages/auth/AccessSignUp.tsx | 20 ++++ packages/frontend/src/pages/auth/SignUp.tsx | 24 +++++ .../frontend/src/pages/auth/SnowballAuth.tsx | 9 +- packages/frontend/src/utils/accessCode.ts | 28 ++++++ 6 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 packages/frontend/src/pages/auth/AccessCode.tsx create mode 100644 packages/frontend/src/pages/auth/AccessSignUp.tsx create mode 100644 packages/frontend/src/utils/accessCode.ts diff --git a/packages/backend/src/routes/auth.ts b/packages/backend/src/routes/auth.ts index 2c73631..9011148 100644 --- a/packages/backend/src/routes/auth.ts +++ b/packages/backend/src/routes/auth.ts @@ -5,6 +5,16 @@ import { authenticateUser, createUser } from '../turnkey-backend'; const router = Router(); +router.post('/accesscode', async (req, res) => { + console.log('Access Code', req.body); + const { accesscode } = req.body; + if (accesscode === '444444') { + return res.send({ isValid: true }); + } else { + return res.sendStatus(204); + } +}); + // // Turnkey // diff --git a/packages/frontend/src/pages/auth/AccessCode.tsx b/packages/frontend/src/pages/auth/AccessCode.tsx new file mode 100644 index 0000000..e35bd89 --- /dev/null +++ b/packages/frontend/src/pages/auth/AccessCode.tsx @@ -0,0 +1,94 @@ +import React, { useState } from 'react'; + +import { Button } from 'components/shared/Button'; +import { + ArrowRightCircleFilledIcon, + LoaderIcon, +} from 'components/shared/CustomIcon'; +import { WavyBorder } from 'components/shared/WavyBorder'; +import { Input } from 'components/shared/Input'; +import { verifyAccessCode } from 'utils/accessCode'; + +type AccessMethod = 'accesscode' | 'passkey'; + +type Err = { type: AccessMethod; message: string }; + +type AccessCodeProps = { + onCorrectAccessCode: () => void; +}; + +export const AccessCode: React.FC = ({ + onCorrectAccessCode, +}) => { + const [accessCode, setAccessCode] = useState(''); + const [error, setError] = useState(); + const [accessMethod, setAccessMethod] = useState(false); + + async function validateAccessCode() { + setAccessMethod('accesscode'); + try { + const isValidAccessCode = await verifyAccessCode(accessCode); + if (isValidAccessCode) { + localStorage.setItem('accessCode', accessCode); + onCorrectAccessCode(); + } else { + setError({ + type: 'accesscode', + message: 'Invalid access code', + }); + } + } catch (err: any) { + setError({ type: 'accesscode', message: err.message }); + } + } + + const loading = accessMethod; + const isValidAccessCodeLength = accessCode.length === 6; + + return ( +
+
+
+ Access Code +
+
+ +
+
+
+ setAccessCode(e.target.value)} + disabled={!!loading} + /> +
+ + {error && error.type === 'accesscode' && ( +
+
+
+ Error: {error.message} +
+
+
+ )} +
+
+
+ ); +}; diff --git a/packages/frontend/src/pages/auth/AccessSignUp.tsx b/packages/frontend/src/pages/auth/AccessSignUp.tsx new file mode 100644 index 0000000..d91fb16 --- /dev/null +++ b/packages/frontend/src/pages/auth/AccessSignUp.tsx @@ -0,0 +1,20 @@ +import React, { useState } from 'react'; + +import { AccessCode } from './AccessCode'; +import { SignUp } from './SignUp'; + +type AccessSignUpProps = { + onDone: () => void; +}; + +export const AccessSignUp: React.FC = ({ onDone }) => { + const [isValidAccessCode, setIsValidAccessCode] = useState( + !!localStorage.getItem('accessCode'), + ); + + return isValidAccessCode ? ( + + ) : ( + setIsValidAccessCode(true)} /> + ); +}; diff --git a/packages/frontend/src/pages/auth/SignUp.tsx b/packages/frontend/src/pages/auth/SignUp.tsx index 581e959..f5564e4 100644 --- a/packages/frontend/src/pages/auth/SignUp.tsx +++ b/packages/frontend/src/pages/auth/SignUp.tsx @@ -21,6 +21,7 @@ import { turnkeySignin, turnkeySignup, } from 'utils/turnkey-frontend'; +import { verifyAccessCode } from 'utils/accessCode'; type Provider = 'google' | 'github' | 'apple' | 'email'; @@ -111,6 +112,29 @@ export const SignUp = ({ onDone }: Props) => { const loading = provider; const emailValid = /.@./.test(email); + useEffect(() => { + const validateAccessCode = async () => { + const accessCode = localStorage.getItem('accessCode'); + if (!accessCode) { + redirectToSignup(); + return; + } + + try { + await verifyAccessCode(accessCode); + } catch (err: any) { + redirectToSignup(); + } + }; + + const redirectToSignup = () => { + localStorage.removeItem('accessCode'); + window.location.href = '/signup'; + }; + + validateAccessCode(); + }, []); + return (
diff --git a/packages/frontend/src/pages/auth/SnowballAuth.tsx b/packages/frontend/src/pages/auth/SnowballAuth.tsx index 69cb798..dd5abf1 100644 --- a/packages/frontend/src/pages/auth/SnowballAuth.tsx +++ b/packages/frontend/src/pages/auth/SnowballAuth.tsx @@ -1,14 +1,15 @@ +import React, { useEffect, useState } from 'react'; + import { snowball } from 'utils/use-snowball'; import { Login } from './Login'; -import { SignUp } from './SignUp'; -import { useEffect, useState } from 'react'; import { Done } from './Done'; +import { AccessSignUp } from './AccessSignUp'; type Screen = 'login' | 'signup' | 'success'; const DASHBOARD_URL = '/'; -export const SnowballAuth = () => { +export const SnowballAuth: React.FC = () => { const path = window.location.pathname; const [screen, setScreen] = useState( path === '/login' ? 'login' : 'signup', @@ -30,7 +31,7 @@ export const SnowballAuth = () => { if (screen === 'signup') { return ( - { setScreen('success'); }} diff --git a/packages/frontend/src/utils/accessCode.ts b/packages/frontend/src/utils/accessCode.ts new file mode 100644 index 0000000..d5e1b68 --- /dev/null +++ b/packages/frontend/src/utils/accessCode.ts @@ -0,0 +1,28 @@ +import { baseUrl } from './constants'; + +export async function verifyAccessCode( + accesscode: string, +): Promise { + const res = await fetch(`${baseUrl}/auth/accesscode`, { + method: 'POST', + body: JSON.stringify({ + accesscode, + }), + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + }); + // If API returns a non-empty 200, this is a valid access code. + if (res.status == 200) { + const isValid = (await res.json()).isValid as boolean; + console.log('isValid', isValid); + return isValid; + } else if (res.status === 204) { + return null; + } else { + throw new Error( + `Unexpected response from access code endpoint: ${res.status}: ${await res.text()}`, + ); + } +}