Add Optional 2FA Support (#61)
* Add TOTP MFA Support * Add Passkey MFA Support It's not impossible I missed some minor cleanup, but most things make sense and there isn't a lot of obvious duplication anymore. --------- Co-authored-by: Bruno Bernardino <me@brunobernardino.com>
This commit is contained in:
94
components/auth/MultiFactorAuthVerifyForm.tsx
Normal file
94
components/auth/MultiFactorAuthVerifyForm.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { MultiFactorAuthMethodType } from '/lib/types.ts';
|
||||
import PasswordlessPasskeyLogin from '/islands/auth/PasswordlessPasskeyLogin.tsx';
|
||||
|
||||
interface MultiFactorAuthVerifyFormProps {
|
||||
email: string;
|
||||
redirectUrl: string;
|
||||
availableMethods: MultiFactorAuthMethodType[];
|
||||
error?: { title: string; message: string };
|
||||
}
|
||||
|
||||
export default function MultiFactorAuthVerifyForm(
|
||||
{ email, redirectUrl, availableMethods, error }: MultiFactorAuthVerifyFormProps,
|
||||
) {
|
||||
const hasPasskey = availableMethods.includes('passkey');
|
||||
const hasTotp = availableMethods.includes('totp');
|
||||
|
||||
return (
|
||||
<section class='max-w-md w-full mb-12'>
|
||||
<section class='mb-6'>
|
||||
<h2 class='mt-6 text-center text-3xl font-extrabold text-white'>
|
||||
Multi-Factor Authentication
|
||||
</h2>
|
||||
<p class='mt-2 text-center text-sm text-gray-300'>
|
||||
You are required to authenticate with an additional method
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{error
|
||||
? (
|
||||
<section class='bg-red-900 border border-red-700 text-red-100 px-4 py-3 rounded relative mb-4'>
|
||||
<strong class='font-bold'>{error.title}:</strong>
|
||||
<span class='block sm:inline'>{error.message}</span>
|
||||
</section>
|
||||
)
|
||||
: null}
|
||||
|
||||
{hasTotp
|
||||
? (
|
||||
<form
|
||||
class='mb-6'
|
||||
method='POST'
|
||||
action={`/mfa-verify?redirect=${encodeURIComponent(redirectUrl)}`}
|
||||
>
|
||||
<fieldset class='block mb-4'>
|
||||
<label class='text-slate-300 block pb-1' for='token'>
|
||||
Authentication Token or Backup Code
|
||||
</label>
|
||||
<input
|
||||
type='text'
|
||||
id='token'
|
||||
name='token'
|
||||
placeholder='123456 or backup code'
|
||||
class='mt-1 input-field'
|
||||
autocomplete='one-time-code'
|
||||
required
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<section class='flex justify-center mt-8 mb-4'>
|
||||
<button
|
||||
type='submit'
|
||||
class='button'
|
||||
>
|
||||
Verify Code
|
||||
</button>
|
||||
</section>
|
||||
</form>
|
||||
)
|
||||
: null}
|
||||
|
||||
{hasTotp && hasPasskey
|
||||
? (
|
||||
<section class='text-center -mt-10 mb-6 block'>
|
||||
<p class='text-gray-400 text-sm'>or</p>
|
||||
</section>
|
||||
)
|
||||
: null}
|
||||
|
||||
{hasPasskey && email
|
||||
? (
|
||||
<section class='mb-8'>
|
||||
<PasswordlessPasskeyLogin email={email} redirectUrl={redirectUrl} />
|
||||
</section>
|
||||
)
|
||||
: null}
|
||||
|
||||
<section class='text-center mt-6'>
|
||||
<a href='/login' class='text-blue-400 hover:text-blue-300 text-sm'>
|
||||
Back to Login
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -160,23 +160,25 @@ export default function ExpenseModal(
|
||||
}}
|
||||
placeholder='Lunch'
|
||||
/>
|
||||
{showSuggestions.value && suggestions.value.length > 0 && (
|
||||
<ul class='absolute z-50 w-full bg-slate-700 rounded-md mt-1 max-h-40 overflow-y-auto ring-1 ring-slate-800 shadow-lg'>
|
||||
{suggestions.value.map((suggestion) => (
|
||||
<li
|
||||
key={suggestion}
|
||||
class='px-4 py-2 hover:bg-slate-600 cursor-pointer'
|
||||
onClick={() => {
|
||||
newExpenseDescription.value = suggestion;
|
||||
showSuggestions.value = false;
|
||||
suggestions.value = [];
|
||||
}}
|
||||
>
|
||||
{suggestion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{showSuggestions.value && suggestions.value.length > 0
|
||||
? (
|
||||
<ul class='absolute z-50 w-full bg-slate-700 rounded-md mt-1 max-h-40 overflow-y-auto ring-1 ring-slate-800 shadow-lg'>
|
||||
{suggestions.value.map((suggestion) => (
|
||||
<li
|
||||
key={suggestion}
|
||||
class='px-4 py-2 hover:bg-slate-600 cursor-pointer'
|
||||
onClick={() => {
|
||||
newExpenseDescription.value = suggestion;
|
||||
showSuggestions.value = false;
|
||||
suggestions.value = [];
|
||||
}}
|
||||
>
|
||||
{suggestion}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
: null}
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='block mb-2'>
|
||||
|
||||
Reference in New Issue
Block a user