Merge pull request #92 from fossyy/staging

Redesign TOTP/2FA page
This commit is contained in:
2024-10-03 17:05:27 +07:00
committed by GitHub
2 changed files with 102 additions and 59 deletions

View File

@ -1,67 +1,110 @@
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"> switch msg.Code {
<div> case 0:
switch msg.Code { <div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert">
case 0: <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<div class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50" role="alert"> <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
<svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> </svg>
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/> <span class="sr-only">Info</span>
</svg> <div>
<span class="sr-only">Info</span> <span class="font-medium">Error!</span> {msg.Message}
<div> </div>
<span class="font-medium">Error!</span> {msg.Message}
</div>
</div>
}
<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> </div>
<form class="space-y-6" method="POST"> }
<div> <h1 class="text-2xl font-bold mb-2">Two-Factor Authentication</h1>
<label for="code" class="block text-sm font-medium text-muted-foreground"> <p class="text-gray-600 mb-6">Enter the 6-digit code from your authenticator app</p>
Verification Code <div class="flex justify-between mb-4" id="otpInputs">
</label> <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
<div class="mt-1"> class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
<input required>
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" <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
id="code" class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
autocomplete="one-time-code" required>
required="" <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
placeholder="123456" class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
pattern="[0-9]{6}" required>
maxlength="6" <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
type="text" class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
name="code" required>
/> <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
</div> class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
</div> required>
<div> <input type="text" inputmode="numeric" pattern="\d{1}" maxlength="1"
<button class="w-12 h-12 text-center text-2xl border-2 border-gray-300 rounded focus:border-blue-500 focus:outline-none"
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" required>
type="submit" </div>
> <form id="otpForm" class="space-y-6" method="POST">
Verify Code <input type="hidden" id="otpValue" name="code">
</button> <button type="submit"
</div> 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"
</form> id="submitButton" disabled>
</div> Verify
</div> </button>
</main> </form>
} </div>
</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
} }