captcha cleanup and added 2fa logins
This commit is contained in:
@@ -42,6 +42,79 @@ $shipping_same_billing = $user->shipping_address === $user->billing_address
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ open: false }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
<i :class="{'transform': !open, '-rotate-90': !open, 'translate-y-0.5': true}"
|
||||
class="fa-solid fa-angle-down text-lg transition-transform mr-2"></i>
|
||||
<h3 class="text-lg font-bold mt-4 mb-3">Two Factor Authentication</h3>
|
||||
</a>
|
||||
<div class="px-4 mb-4" x-show="open">
|
||||
<div class="flex items-center border border-gray-300 rounded bg-white pl-2 pr-4 py-3 mb-4">
|
||||
<div class="bg-gray-200 rounded-full w-14 h-14 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-envelope text-2xl"></i>
|
||||
</div>
|
||||
<div class="mx-4 flex-grow">
|
||||
<p class="flex mb-2">
|
||||
<span class="text-sm font-bold mr-2">Use Email</span>
|
||||
<span class="text-xs bg-green-500 text-white rounded px-2 py-0.5">Enabled</span>
|
||||
</p>
|
||||
<p class="text-xs">Use the security link sent to your email address as your two-factor authentication (2FA). The security link will be sent to the address associated with your account.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border border-gray-300 rounded bg-white pl-2 pr-4 py-3">
|
||||
<div class="flex items-center">
|
||||
<div class="bg-gray-200 rounded-full w-14 h-14 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fa-solid fa-mobile-screen-button text-2xl"></i>
|
||||
</div>
|
||||
<div class="mx-4 flex-grow">
|
||||
<p class="flex mb-2">
|
||||
<span class="text-sm font-bold mr-2">Use Authenticator App</span>
|
||||
<span x-cloak x-show="!$store.tfa.enabled" class="text-xs bg-red-500 text-white rounded px-2 py-0.5">Disabled</span>
|
||||
<span x-cloak x-show="$store.tfa.enabled" class="text-xs bg-green-500 text-white rounded px-2 py-0.5">Enabled</span>
|
||||
</p>
|
||||
<p class="text-xs">Use an Authenticator App as your two-factor authenticator. When you sign in you'll be asked to use the security code provided by your Authenticator App.</p>
|
||||
</div>
|
||||
<div class="flex flex-col text-nowrap gap-2">
|
||||
<x-ui.button x-show="!$store.tfa.enabled" id="tfa_button" type="button" color="primary-outline" x-data x-on:click.prevent="setupTFA()">Setup</x-ui.button>
|
||||
<x-ui.button x-show="$store.tfa.enabled" type="button" color="danger-outline" x-data x-on:click.prevent="destroyTFA()">Disable</x-ui.button>
|
||||
<a href="#" x-show="$store.tfa.enabled" x-on:click.prevent="resetBackupCodes($event)" class="text-xs link">Reset Backup Codes</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t flex items-center justify-center gap-4" x-cloak x-show="$store.tfa.show && !$store.tfa.loading">
|
||||
<img src="/loading.gif" id="tfa_image_loader" alt="loading" width="100" height="100"/>
|
||||
<img src="" id="tfa_image" alt="QR Code" width="150" height="150" style="display:none" onload="this.style.display='block';document.getElementById('tfa_image_loader').style.display='none';"/>
|
||||
<div>
|
||||
<p class="text-xs mb-2">Scan the QR Code into your Authenticator App and enter the code provided below</p>
|
||||
<div class="flex items-center gap-4 justify-center">
|
||||
<x-ui.input name="code" id="code" class="mb-0" />
|
||||
<x-ui.button class="mt-1" type="button" color="primary-outline" x-on:click.prevent="linkTFA()">Link</x-ui.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t flex justify-center" x-cloak x-show="$store.tfa.loading">
|
||||
<img src="/loading.gif" alt="loading" width="100" height="100"/>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t flex justify-center" x-cloak x-show="$store.tfa.codes && !$store.tfa.loading">
|
||||
<div class="w-[34rem] flex items-center gap-4">
|
||||
<div class="w-[18rem] mx-auto">
|
||||
<p class="text-sm font-bold mb-1">Save your Backup Codes</p>
|
||||
<ul class="ml-6 mb-4 text-xs list-disc">
|
||||
<li>Keep these backup codes safe</li>
|
||||
<li>You can only use each one once</li>
|
||||
<li>They will not be shown again</li>
|
||||
<li>Any existing codes can no longer be used</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="w-[16rem] bg-gray-200 p-4 text-sm font-mono flex flex-wrap justify-center">
|
||||
<template x-for="(code, idx) in $store.tfa.codes" :key="idx">
|
||||
<p class="mx-4" x-text="code"></p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section x-data="{ open: true }">
|
||||
<a href="#" class="flex items-center" @click.prevent="open = !open">
|
||||
@@ -97,3 +170,106 @@ $shipping_same_billing = $user->shipping_address === $user->billing_address
|
||||
</form>
|
||||
</x-container>
|
||||
</x-layout>
|
||||
|
||||
{{ $codes ?? '' }}
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('tfa', {
|
||||
show: false,
|
||||
secret: null,
|
||||
enabled: {{ $user->tfa_secret !== null ? 'true' : 'false'}},
|
||||
codes: null,
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
|
||||
function setupTFA() {
|
||||
document.getElementById('tfa_button').disabled = true;
|
||||
axios.get('/account/2fa')
|
||||
.then(response => {
|
||||
if(response.data.secret) {
|
||||
Alpine.store('tfa').show = true;
|
||||
Alpine.store('tfa').secret = response.data.secret;
|
||||
document.getElementById('tfa_image').src = '/account/2fa/image?secret=' + response.data.secret;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while setting up two-factor authentication. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while setting up two-factor authentication. Please try again later', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function linkTFA() {
|
||||
Alpine.store('tfa').loading = true;
|
||||
axios.post('/account/2fa', {
|
||||
code: document.getElementById('code').value,
|
||||
secret: Alpine.store('tfa').secret,
|
||||
})
|
||||
.then(response => {
|
||||
console.log(response.data);
|
||||
if(response.data.success) {
|
||||
SM.alert('2FA Linked', 'Two-factor authentication has been successfully linked to your account', 'success');
|
||||
document.getElementById('tfa_button').disabled = false;
|
||||
document.getElementById('code').value = '';
|
||||
document.getElementById('tfa_image').src = '';
|
||||
Alpine.store('tfa').show = false;
|
||||
Alpine.store('tfa').enabled = true;
|
||||
Alpine.store('tfa').codes = response.data.codes;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while linking two-factor authentication. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while linking two-factor authentication. Please try again later', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
Alpine.store('tfa').loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function resetBackupCodes(event) {
|
||||
event.target.classList.add('disabled');
|
||||
Alpine.store('tfa').codes = null;
|
||||
Alpine.store('tfa').loading = true;
|
||||
|
||||
axios.post('/account/2fa/reset-backup-codes')
|
||||
.then(response => {
|
||||
if(response.data.success) {
|
||||
Alpine.store('tfa').codes = response.data.codes;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while resetting your backup codes. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while resetting your backup codes. Please try again later', 'danger');
|
||||
})
|
||||
.finally(() => {
|
||||
event.target.classList.remove('disabled');
|
||||
Alpine.store('tfa').loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function destroyTFA() {
|
||||
SM.confirm('Disable 2FA', 'Are you sure you want to remove two-factor authentication from your account?', 'Disable', (confirm) => {
|
||||
if(confirm) {
|
||||
axios.delete('/account/2fa')
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
SM.alert('2FA Disabled', 'Two-factor authentication has been successfully disabled on your account', 'success');
|
||||
Alpine.store('tfa').enabled = false;
|
||||
Alpine.store('tfa').codes = null;
|
||||
} else {
|
||||
SM.alert('2FA Error', 'An error occurred while disabling two-factor authentication. Please try again later', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
SM.alert('2FA Error', 'An error occurred while disabling two-factor authentication. Please try again later', 'danger');
|
||||
});
|
||||
}
|
||||
}, {
|
||||
confirmButtonText: 'Disable'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
68
resources/views/auth/login-2fa.blade.php
Normal file
68
resources/views/auth/login-2fa.blade.php
Normal file
@@ -0,0 +1,68 @@
|
||||
@php
|
||||
if(!isset($email)) {
|
||||
$email = '';
|
||||
if(isset($user)) {
|
||||
$email = $user->email;
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<x-layout :bodyClass="'image-background'">
|
||||
<div x-data="{show:'{{ $method ?? 'tfa' }}'}">
|
||||
<x-dialog x-cloak x-show="show==='tfa'" formaction="{{ route('login.store') }}">
|
||||
<x-slot:title>
|
||||
<a class="link absolute left-0" href="{{ route('login') }}"><i class="fa-solid fa-angle-left"></i></a>
|
||||
Please enter 2FA code
|
||||
</x-slot:title>
|
||||
<x-slot:header>
|
||||
<p class="text-sm">Two-factor authentication (2FA) is enabled for your account. Please enter a code to log in.</p>
|
||||
</x-slot:header>
|
||||
<input type="hidden" name="email" value="{{ $email }}"/>
|
||||
<x-ui.input type="text" name="code" label="Code" floating autofocus error="{{ $errors->first('code') }}"/>
|
||||
<x-slot:footer>
|
||||
<div class="text-xs">
|
||||
Having trouble? <a class="link" href="#" x-on:click.prevent="show='other'">Sign in another way</a>
|
||||
</div>
|
||||
<x-ui.button type="submit">Verify</x-ui.button>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
|
||||
<x-dialog x-cloak x-show="show==='other'">
|
||||
@captcha
|
||||
<x-slot:title>
|
||||
<a class="link absolute left-0" href="#" x-on:click.prevent="show='tfa'"><i class="fa-solid fa-angle-left"></i></a>
|
||||
Sign in another way
|
||||
</x-slot:title>
|
||||
<x-slot:header>Select the method to sign in to your account</x-slot:header>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<form method="post" action="{{ route('login.store') }}">
|
||||
@csrf
|
||||
@captcha
|
||||
<input type="hidden" name="email" value="{{ $email }}" />
|
||||
<input type="hidden" name="method" value="email" />
|
||||
<x-ui.button type="submit" class="w-full">Email Link</x-ui.button>
|
||||
</form>
|
||||
<x-ui.button type="button" x-on:click.prevent="show='backup'">Enter Backup Code</x-ui.button>
|
||||
</div>
|
||||
<x-slot:footer>
|
||||
<div class="text-xs">If you need support for accessing your account, please contact STEMMechanics support at <a href="mailto:hello@stemmechanics.com.au" class="link">hello@stemmechanics.com.au</a></div>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
|
||||
<x-dialog x-cloak x-show="show==='backup'" formaction="{{ route('login.store') }}">
|
||||
<x-slot:title>
|
||||
<a class="link absolute left-0" href="#" x-on:click.prevent="show='other'"><i class="fa-solid fa-angle-left"></i></a>
|
||||
Please enter a backup code
|
||||
</x-slot:title>
|
||||
<x-slot:header>
|
||||
<p class="text-sm">Enter one of your backup codes below to log in. Once a backup codes are a 1 time use only.</p>
|
||||
</x-slot:header>
|
||||
@captcha
|
||||
<input type="hidden" name="email" value="{{ $email }}"/>
|
||||
<x-ui.input type="text" name="backup_code" label="Backup Code" floating autofocus error="{{ $errors->first('backup_code') }}" />
|
||||
<x-slot:footer center>
|
||||
<x-ui.button class="self-end" type="submit">Verify</x-ui.button>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
|
||||
</div>
|
||||
</x-layout>
|
||||
14
resources/views/auth/login-alt.blade.php
Normal file
14
resources/views/auth/login-alt.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<x-layout :bodyClass="'image-background'">
|
||||
<x-dialog formaction="{{ route('login.store') }}">
|
||||
@captcha
|
||||
<x-slot:title>Sign in another way</x-slot:title>
|
||||
<x-slot:header>Select the method to sign in to your account</x-slot:header>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<x-ui.button type="button" onclick="loginUsingEmail()">Email Link</x-ui.button>
|
||||
<x-ui.button type="button" onclick="loginUsingEmail()">Enter Backup Code</x-ui.button>
|
||||
</div>
|
||||
<x-slot:footer>
|
||||
<div class="text-xs">If you need support for accessing your account, please contact STEMMechanics support at <a href="mailto:hello@stemmechanics.com.au" class="link">hello@stemmechanics.com.au</a></div>
|
||||
</x-slot:footer>
|
||||
</x-dialog>
|
||||
</x-layout>
|
||||
@@ -1,5 +1,6 @@
|
||||
<x-layout :bodyClass="'image-background'">
|
||||
<x-dialog formaction="{{ route('login.store') }}">
|
||||
@captcha
|
||||
@if(session('status') == 'not-found')
|
||||
<x-slot:title>Sorry, we didn't recognize that email</x-slot:title>
|
||||
<x-slot:header>
|
||||
@@ -8,7 +9,7 @@
|
||||
@else
|
||||
<x-slot:title>Sign in with email</x-slot:title>
|
||||
<x-slot:header>
|
||||
<p>Enter the email address associated with your account, and we'll send a magic link to your inbox.</p>
|
||||
<p>Enter the email address associated with your account</p>
|
||||
</x-slot:header>
|
||||
@endif
|
||||
<x-ui.input type="email" name="email" label="Email" floating autofocus />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex items-center justify-center flex-grow py-24">
|
||||
<div class="flex items-center justify-center flex-grow py-24" {{ $attributes }}>
|
||||
<div class="w-full mx-2 max-w-lg p-8 pb-6 bg-white rounded-md shadow-deep">
|
||||
@isset($title)
|
||||
<h2 class="text-2xl font-bold mb-4 text-center">{{ $title }}</h2>
|
||||
<h2 class="text-2xl font-bold mb-4 text-center relative">{{ $title }}</h2>
|
||||
@endisset
|
||||
@isset($header)
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
</script>
|
||||
@endif
|
||||
@stack('scripts')
|
||||
@captchaScripts
|
||||
@livewireScripts
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
$colorClasses = [
|
||||
'outline' => 'hover:bg-gray-500 focus-visible:outline-primary-color text-gray-800 border border-gray-400 bg-white hover:text-white',
|
||||
'primary' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
|
||||
'primary-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
|
||||
'primary-outline' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
||||
'primary-outline-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
||||
'danger' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color bg-danger-color text-white',
|
||||
'danger-outline' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color text-danger-color border border-danger-color bg-white hover:text-white',
|
||||
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white'
|
||||
][$color];
|
||||
$commonClasses = @twMerge(['whitespace-nowrap', 'text-center','justify-center','rounded-md','px-8','py-1.5','text-sm','font-semibold','leading-6','shadow-sm','focus-visible:outline','focus-visible:outline-2','focus-visible:outline-offset-2','transition'], ($class ?? ''));
|
||||
@endphp
|
||||
|
||||
@if($type == 'submit' || $type == 'button')
|
||||
@if($type === 'submit' || $type === 'button')
|
||||
<button type="{{ $type }}" class="{{ $colorClasses . ' ' . $commonClasses }}" {{ $attributes }}>{{ $slot }}</button>
|
||||
@elseif($type == 'link')
|
||||
@elseif($type === 'link')
|
||||
<a href="{{ $href ?? '#' }}" target="{{ $target ?? '_self' }}" class="{{ $colorClasses . ' ' . $commonClasses }}" {{ $attributes }}">{{ $slot }}</a>
|
||||
@endif
|
||||
|
||||
13
resources/views/emails/login-backup-code.blade.php
Normal file
13
resources/views/emails/login-backup-code.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>We just wanted to let you know that someone just logged in using a backup code.</p>
|
||||
<p>If this was you, then it is all good!</p>
|
||||
<p>If it's not, we recommend you let us know by replying to this email and resetting your backup codes by:</p>
|
||||
<ul>
|
||||
<li>Logging into your account on STEMMechanics</li>
|
||||
<li>Visit your account page</li>
|
||||
<li>Under <strong>Two Factor Authentication</strong> - Click <i>Reset Backup Codes</i></li>
|
||||
</ul>
|
||||
<p>Warm regards,</p>
|
||||
<p>—James 😁</p>
|
||||
@endcomponent
|
||||
8
resources/views/emails/login-tfa-disabled.blade.php
Normal file
8
resources/views/emails/login-tfa-disabled.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>We just wanted to let you know that using an <strong>Authenticator App</strong> to log in to your account on STEMMechanics has been <strong>Disabled</strong>.</p>
|
||||
<p>If this was you, then it is all good! - Any previous <i>Backup TFA Codes</i> can no longer be used.</p>
|
||||
<p>If it's not, we recommend you let us know by replying to this email.</p>
|
||||
<p>Regards,</p>
|
||||
<p>—James 😁</p>
|
||||
@endcomponent
|
||||
8
resources/views/emails/login-tfa-enabled.blade.php
Normal file
8
resources/views/emails/login-tfa-enabled.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
@component('mail::message', ['email' => $email])
|
||||
<p>Hey there!</p>
|
||||
<p>We just wanted to let you know that using an <strong>Authenticator App</strong> to log in to your account on STEMMechanics has been <strong>Enabled</strong>.</p>
|
||||
<p>If this was you, then it is all good!</p>
|
||||
<p>If it's not, we recommend you let us know by replying to this email.</p>
|
||||
<p>Regards,</p>
|
||||
<p>—James 😁</p>
|
||||
@endcomponent
|
||||
@@ -67,7 +67,7 @@ h4 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
p {
|
||||
p, li {
|
||||
font-size: 14px;
|
||||
line-height: 1.2em;
|
||||
margin-top: 0;
|
||||
|
||||
Reference in New Issue
Block a user