diff --git a/src/AuthProvider.tsx b/src/AuthProvider.tsx index 651bce6..8d9b224 100644 --- a/src/AuthProvider.tsx +++ b/src/AuthProvider.tsx @@ -1,4 +1,5 @@ import { InternalAuthProvider } from '@/context/InternalAuthContext'; +import { startAuthentication } from '@simplewebauthn/browser'; import React, { createContext, ReactNode, @@ -42,6 +43,8 @@ export interface AuthContextType { credentials: Credential[]; updateCredential: (credential: Credential) => Promise; deleteCredential: (credentialId: string) => Promise; + login: (identifier: string, passkeyAvailable: boolean) => Promise; + handlePasskeyLogin: () => Promise; } export interface Credential { @@ -97,6 +100,54 @@ export const AuthProvider: React.FC = ({ authHost: apiHost, }); + const login = async ( + identifier: string, + passkeyAvailable: boolean + ): Promise => { + const response = await fetchWithAuth(`/login`, { + method: 'POST', + body: JSON.stringify({ identifier, passkeyAvailable }), + }); + + return response; + }; + + const handlePasskeyLogin = async () => { + try { + const response = await fetchWithAuth(`/webAuthn/login/start`, { + method: 'POST', + }); + + const options = await response.json(); + const credential = await startAuthentication({ optionsJSON: options }); + + const verificationResponse = await fetchWithAuth(`/webAuthn/login/finish`, { + method: 'POST', + body: JSON.stringify({ assertionResponse: credential }), + }); + + if (!verificationResponse.ok) { + console.error('Failed to verify passkey'); + } + + const verificationResult = await verificationResponse.json(); + + if (verificationResult.message === 'Success') { + if (verificationResult.mfaLogin) { + return true; + } + await validateToken(); + return false; + } else { + console.error('Passkey login failed:', verificationResult.message); + return false; + } + } catch (error) { + console.error('Passkey login error:', error); + return false; + } + }; + const logout = useCallback(async () => { if (user) { try { @@ -221,6 +272,8 @@ export const AuthProvider: React.FC = ({ credentials, updateCredential, deleteCredential, + login, + handlePasskeyLogin, }} > diff --git a/src/AuthRoutes.tsx b/src/AuthRoutes.tsx index e1c0506..5318d59 100644 --- a/src/AuthRoutes.tsx +++ b/src/AuthRoutes.tsx @@ -16,7 +16,7 @@ export const AuthRoutes = () => ( } /> } /> } /> - } /> + } /> } /> ); diff --git a/src/utils.ts b/src/utils.ts index 6cdda15..2f155b8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,4 @@ import parsePhoneNumberFromString from 'libphonenumber-js'; - /** * isValidEmail * diff --git a/src/views/Login.tsx b/src/views/Login.tsx index 489dad1..ea26f3d 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -1,10 +1,7 @@ -import { startAuthentication } from '@simplewebauthn/browser'; import { useAuth } from '@/AuthProvider'; import PhoneInputWithCountryCode from '@/components/phoneInput'; -import { useInternalAuth } from '@/context/InternalAuthContext'; import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; - import styles from '@/styles/login.module.css'; import { isPasskeySupported, isValidEmail, isValidPhoneNumber } from '../utils'; import { createFetchWithAuth } from '@/fetchWithAuth'; @@ -12,8 +9,13 @@ import AuthFallbackOptions from '@/components/AuthFallbackOptions'; const Login: React.FC = () => { const navigate = useNavigate(); - const { apiHost, hasSignedInBefore, mode: authMode } = useAuth(); - const { validateToken } = useInternalAuth(); + const { + apiHost, + hasSignedInBefore, + mode: authMode, + login, + handlePasskeyLogin, + } = useAuth(); const [identifier, setIdentifier] = useState(''); const [email, setEmail] = useState(''); const [mode, setMode] = useState<'login' | 'register'>('register'); @@ -61,73 +63,6 @@ const Login: React.FC = () => { return isValidEmail(email) && isValidPhoneNumber(phone); }; - const handlePasskeyLogin = async () => { - try { - const response = await fetchWithAuth(`/webAuthn/login/start`, { - method: 'POST', - }); - - if (!response.ok) { - console.error('Something went wrong getting webauthn options'); - return; - } - - const options = await response.json(); - const credential = await startAuthentication({ optionsJSON: options }); - - const verificationResponse = await fetchWithAuth(`/webAuthn/login/finish`, { - method: 'POST', - body: JSON.stringify({ assertionResponse: credential }), - }); - - if (!verificationResponse.ok) { - console.error('Failed to verify passkey'); - } - - const verificationResult = await verificationResponse.json(); - - if (verificationResult.message === 'Success') { - if (verificationResult.mfaLogin) { - navigate('/mfaLogin'); - return; - } - await validateToken(); - navigate('/'); - return; - } else { - console.error('Passkey login failed:', verificationResult.message); - } - } catch (error) { - console.error('Passkey login error:', error); - } - }; - - const login = async () => { - setFormErrors(''); - - const response = await fetchWithAuth(`/login`, { - method: 'POST', - body: JSON.stringify({ identifier, passkeyAvailable }), - }); - - if (!response.ok) { - setFormErrors('Failed to send login link. Please try again.'); - return; - } - - if (!passkeyAvailable) { - setShowFallbackOptions(true); - return; - } - - try { - await handlePasskeyLogin(); - } catch (err) { - console.error('Passkey login failed', err); - setShowFallbackOptions(true); - } - }; - const register = async () => { setFormErrors(''); @@ -169,7 +104,7 @@ const Login: React.FC = () => { return; } - navigate('/magic-link-sent'); + navigate('/magiclinks-sent'); } catch (err) { console.error(err); setFormErrors('Failed to send magic link.'); @@ -198,7 +133,18 @@ const Login: React.FC = () => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (mode === 'login') login(); + if (mode === 'login') { + const loginRes = await login(identifier, passkeyAvailable); + + if (loginRes.ok && passkeyAvailable) { + const passkeyResult = await handlePasskeyLogin(); + if (passkeyResult) { + navigate('/'); + } + } else { + setShowFallbackOptions(true); + } + } if (mode === 'register') register(); }; diff --git a/src/views/VerifyMagicLink.tsx b/src/views/VerifyMagicLink.tsx index 2488a77..d5c5279 100644 --- a/src/views/VerifyMagicLink.tsx +++ b/src/views/VerifyMagicLink.tsx @@ -21,17 +21,21 @@ const VerifyMagicLink: React.FC = () => { useEffect(() => { const verify = async () => { - const response = await fetchWithAuth(`/magic-link/verify/${token}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - console.error('Failed to verify token'); - setError('Failed to verify token'); - return; + try { + const response = await fetchWithAuth(`/magic-link/verify/${token}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + console.error('Failed to verify token'); + setError('Failed to verify token'); + return; + } + } catch (error) { + console.error(error); } const channel = new BroadcastChannel('seamless-auth');