Redesign TOTP/2FA page

This commit is contained in:
2024-10-03 17:05:05 +07:00
parent 93c44f40cc
commit 0c0aa045dc
2 changed files with 102 additions and 59 deletions

View File

@ -1,16 +1,14 @@
package totpView package totpView
import ( import (
"github.com/fossyy/filekeeper/view/client/layout" "github.com/fossyy/filekeeper/view/client/layout"
"github.com/fossyy/filekeeper/types" "github.com/fossyy/filekeeper/types"
) )
templ content(title string, msg types.Message) { templ content(title string, msg types.Message) {
@layout.Base(title){ @layout.Base(title){
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10"> <main class="bg-gray-100 flex items-center justify-center min-h-screen">
<div class="flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8"> <div class="bg-white shadow-md rounded-lg p-8 max-w-sm w-full">
<div class="w-full max-w-md space-y-8">
<div>
switch msg.Code { switch msg.Code {
case 0: case 0:
<div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert"> <div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert">
@ -23,45 +21,90 @@ templ content(title string, msg types.Message) {
</div> </div>
</div> </div>
} }
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">Verify Your Identity</h2> <h1 class="text-2xl font-bold mb-2">Two-Factor Authentication</h1>
<p class="mt-2 text-center text-sm text-muted-foreground"> <p class="text-gray-600 mb-6">Enter the 6-digit code from your authenticator app</p>
Please enter the 6-digit code generated by your authentication app to complete the login process. <div class="flex justify-between mb-4" id="otpInputs">
</p> <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
required>
<input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
required>
<input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
required>
<input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
required>
<input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
required>
<input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
required>
</div> </div>
<form class="space-y-6" method="POST"> <form id="otpForm" class="space-y-6" method="POST">
<div> <input type="hidden" id="otpValue" name="code">
<label for="code" class="block text-sm font-medium text-muted-foreground"> <button type="submit"
Verification Code class="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
</label> id="submitButton" disabled>
<div class="mt-1"> Verify
<input
class="h-10 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 block w-full appearance-none rounded-md border border-input bg-background px-3 py-2 placeholder-muted-foreground shadow-sm focus:border-primary focus:outline-none focus:ring-primary sm:text-sm"
id="code"
autocomplete="one-time-code"
required=""
placeholder="123456"
pattern="[0-9]{6}"
maxlength="6"
type="text"
name="code"
/>
</div>
</div>
<div>
<button
class="items-center whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 flex w-full justify-center rounded-md bg-black py-2 px-4 text-sm font-medium text-primary-foreground shadow-sm text-white hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
type="submit"
>
Verify Code
</button> </button>
</div>
</form> </form>
</div> </div>
</div>
</main> </main>
<script>
const otpInputs = document.getElementById('otpInputs');
const inputs = otpInputs.querySelectorAll('input[type="text"]');
const form = document.getElementById('otpForm');
const otpValue = document.getElementById('otpValue');
const submitButton = document.getElementById('submitButton');
function updateOtpValue() {
otpValue.value = Array.from(inputs).map(input => input.value).join('');
submitButton.disabled = otpValue.value.length !== 6;
}
inputs.forEach((input, index) => {
input.addEventListener('input', function (e) {
if (this.value.length === 1) {
if (index < inputs.length - 1) {
inputs[index + 1].focus();
}
} else if (this.value.length > 1) {
this.value = this.value.slice(0, 1);
}
updateOtpValue();
});
input.addEventListener('keydown', function (e) {
if (e.key === 'Backspace' && this.value === '' && index > 0) {
inputs[index - 1].focus();
inputs[index - 1].value = '';
updateOtpValue();
}
});
input.addEventListener('paste', function (e) {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').slice(0, 6);
for (let i = 0; i < pastedData.length; i++) {
if (i + index < inputs.length) {
inputs[i + index].value = pastedData[i];
}
}
updateOtpValue();
if (pastedData.length + index < inputs.length) {
inputs[pastedData.length + index].focus();
}
});
});
</script>
} }
} }
templ Main(title string, msg types.Message) { templ Main(title string, msg types.Message) {
@content(title, msg) @content(title, msg)
} }

View File

@ -46,7 +46,7 @@ func content(title string, msg types.Message) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10\"><div class=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\"><div class=\"w-full max-w-md space-y-8\"><div>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<main class=\"bg-gray-100 flex items-center justify-center min-h-screen\"><div class=\"bg-white shadow-md rounded-lg p-8 max-w-sm w-full\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -59,7 +59,7 @@ func content(title string, msg types.Message) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(msg.Message) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(msg.Message)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/client/totp/totp.templ`, Line: 22, Col: 80} return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/client/totp/totp.templ`, Line: 20, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -70,7 +70,7 @@ func content(title string, msg types.Message) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h2 class=\"mt-6 text-center text-3xl font-bold tracking-tight text-foreground\">Verify Your Identity</h2><p class=\"mt-2 text-center text-sm text-muted-foreground\">Please enter the 6-digit code generated by your authentication app to complete the login process.</p></div><form class=\"space-y-6\" method=\"POST\"><div><label for=\"code\" class=\"block text-sm font-medium text-muted-foreground\">Verification Code</label><div class=\"mt-1\"><input class=\"h-10 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 block w-full appearance-none rounded-md border border-input bg-background px-3 py-2 placeholder-muted-foreground shadow-sm focus:border-primary focus:outline-none focus:ring-primary sm:text-sm\" id=\"code\" autocomplete=\"one-time-code\" required=\"\" placeholder=\"123456\" pattern=\"[0-9]{6}\" maxlength=\"6\" type=\"text\" name=\"code\"></div></div><div><button class=\"items-center whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 flex w-full justify-center rounded-md bg-black py-2 px-4 text-sm font-medium text-primary-foreground shadow-sm text-white hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2\" type=\"submit\">Verify Code</button></div></form></div></div></main>") _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<h1 class=\"text-2xl font-bold mb-2\">Two-Factor Authentication</h1><p class=\"text-gray-600 mb-6\">Enter the 6-digit code from your authenticator app</p><div class=\"flex justify-between mb-4\" id=\"otpInputs\"><input type=\"text\" inputmode=\"numeric\" pattern=\"\\d{1}\" maxlength=\"1\" class=\"w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none\" required> <input type=\"text\" inputmode=\"numeric\" pattern=\"\\d{1}\" maxlength=\"1\" class=\"w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none\" required> <input type=\"text\" inputmode=\"numeric\" pattern=\"\\d{1}\" maxlength=\"1\" class=\"w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none\" required> <input type=\"text\" inputmode=\"numeric\" pattern=\"\\d{1}\" maxlength=\"1\" class=\"w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none\" required> <input type=\"text\" inputmode=\"numeric\" pattern=\"\\d{1}\" maxlength=\"1\" class=\"w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none\" required> <input type=\"text\" inputmode=\"numeric\" pattern=\"\\d{1}\" maxlength=\"1\" class=\"w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none\" required></div><form id=\"otpForm\" class=\"space-y-6\" method=\"POST\"><input type=\"hidden\" id=\"otpValue\" name=\"code\"> <button type=\"submit\" class=\"w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed\" id=\"submitButton\" disabled>Verify</button></form></div></main><script>\n const otpInputs = document.getElementById('otpInputs');\n const inputs = otpInputs.querySelectorAll('input[type=\"text\"]');\n const form = document.getElementById('otpForm');\n const otpValue = document.getElementById('otpValue');\n const submitButton = document.getElementById('submitButton');\n\n function updateOtpValue() {\n otpValue.value = Array.from(inputs).map(input => input.value).join('');\n submitButton.disabled = otpValue.value.length !== 6;\n }\n\n inputs.forEach((input, index) => {\n input.addEventListener('input', function (e) {\n if (this.value.length === 1) {\n if (index < inputs.length - 1) {\n inputs[index + 1].focus();\n }\n } else if (this.value.length > 1) {\n this.value = this.value.slice(0, 1);\n }\n updateOtpValue();\n });\n\n input.addEventListener('keydown', function (e) {\n if (e.key === 'Backspace' && this.value === '' && index > 0) {\n inputs[index - 1].focus();\n inputs[index - 1].value = '';\n updateOtpValue();\n }\n });\n\n input.addEventListener('paste', function (e) {\n e.preventDefault();\n const pastedData = e.clipboardData.getData('text').slice(0, 6);\n for (let i = 0; i < pastedData.length; i++) {\n if (i + index < inputs.length) {\n inputs[i + index].value = pastedData[i];\n }\n }\n updateOtpValue();\n if (pastedData.length + index < inputs.length) {\n inputs[pastedData.length + index].focus();\n }\n });\n });\n </script>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }