fix: mobile receipt capture not working - missing name attribute on file input + htmx targetError on OTP verification
This commit is contained in:
parent
6b6196a59a
commit
64f5b9a65b
3 changed files with 17 additions and 19 deletions
|
|
@ -124,7 +124,7 @@ func (h *AuthHandler) RequestOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the OTP verification form as an HTMX fragment.
|
// Render the OTP verification form as an HTMX fragment.
|
||||||
renderOTPForm(w, emailAddr)
|
renderOTPForm(w, emailAddr, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyOTP handles OTP code verification and session creation.
|
// VerifyOTP handles OTP code verification and session creation.
|
||||||
|
|
@ -140,7 +140,7 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
otpCode := collectOTP(r)
|
otpCode := collectOTP(r)
|
||||||
|
|
||||||
if emailAddr == "" || otpCode == "" {
|
if emailAddr == "" || otpCode == "" {
|
||||||
renderError(w, "Email and OTP code are required.")
|
renderOTPForm(w, emailAddr, "Email and OTP code are required.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,11 +148,11 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
stored, err := database.GetOTP(h.DB, emailAddr)
|
stored, err := database.GetOTP(h.DB, emailAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ERROR [%s] handlers: VerifyOTP GetOTP(%s): %v", time.Now().Format(time.RFC3339), emailAddr, err)
|
log.Printf("ERROR [%s] handlers: VerifyOTP GetOTP(%s): %v", time.Now().Format(time.RFC3339), emailAddr, err)
|
||||||
renderError(w, "An error occurred. Please try again.")
|
renderOTPForm(w, emailAddr, "An error occurred. Please try again.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if stored == nil {
|
if stored == nil {
|
||||||
renderError(w, "No OTP found for this email. Please request a new code.")
|
renderOTPForm(w, emailAddr, "No OTP found for this email. Please request a new code.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,14 +160,14 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
expiresAt, err := time.Parse(time.RFC3339, stored.ExpiresAt)
|
expiresAt, err := time.Parse(time.RFC3339, stored.ExpiresAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ERROR [%s] handlers: VerifyOTP parse expiry(%s): %v", time.Now().Format(time.RFC3339), stored.ExpiresAt, err)
|
log.Printf("ERROR [%s] handlers: VerifyOTP parse expiry(%s): %v", time.Now().Format(time.RFC3339), stored.ExpiresAt, err)
|
||||||
renderError(w, "An error occurred. Please try again.")
|
renderOTPForm(w, emailAddr, "An error occurred. Please try again.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the OTP code and expiry.
|
// Validate the OTP code and expiry.
|
||||||
if !auth.ValidateOTP(otpCode, stored.OTPCode, expiresAt) {
|
if !auth.ValidateOTP(otpCode, stored.OTPCode, expiresAt) {
|
||||||
h.FailureTracker.RecordFailure(emailAddr)
|
h.FailureTracker.RecordFailure(emailAddr)
|
||||||
renderError(w, "Invalid or expired OTP code. Please try again.")
|
renderOTPForm(w, emailAddr, "Invalid or expired OTP code. Please try again.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +182,7 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := database.GetUserByEmail(h.DB, emailAddr)
|
user, err := database.GetUserByEmail(h.DB, emailAddr)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
log.Printf("ERROR [%s] handlers: VerifyOTP GetUserByEmail(%s): err=%v", time.Now().Format(time.RFC3339), emailAddr, err)
|
log.Printf("ERROR [%s] handlers: VerifyOTP GetUserByEmail(%s): err=%v", time.Now().Format(time.RFC3339), emailAddr, err)
|
||||||
renderError(w, "An error occurred. Please try again.")
|
renderOTPForm(w, emailAddr, "An error occurred. Please try again.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,7 +190,7 @@ func (h *AuthHandler) VerifyOTP(w http.ResponseWriter, r *http.Request) {
|
||||||
token, err := h.Sessions.Generate(user.ID)
|
token, err := h.Sessions.Generate(user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("ERROR [%s] handlers: VerifyOTP Session Generate(%s): %v", time.Now().Format(time.RFC3339), user.ID, err)
|
log.Printf("ERROR [%s] handlers: VerifyOTP Session Generate(%s): %v", time.Now().Format(time.RFC3339), user.ID, err)
|
||||||
renderError(w, "An error occurred. Please try again.")
|
renderOTPForm(w, emailAddr, "An error occurred. Please try again.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,10 +257,12 @@ func renderError(w http.ResponseWriter, message string) {
|
||||||
// renderOTPForm writes the OTP verification form partial as an HTMX fragment.
|
// renderOTPForm writes the OTP verification form partial as an HTMX fragment.
|
||||||
// It renders 6 individual digit input boxes for a better mobile UX, plus a
|
// It renders 6 individual digit input boxes for a better mobile UX, plus a
|
||||||
// hidden email field. The handler combines the 6 digits server-side.
|
// hidden email field. The handler combines the 6 digits server-side.
|
||||||
func renderOTPForm(w http.ResponseWriter, email string) {
|
// If errMsg is non-empty, it is displayed as an error banner above the form.
|
||||||
|
func renderOTPForm(w http.ResponseWriter, email string, errMsg string) {
|
||||||
tmpl := template.Must(template.New("otp_form").Parse(`
|
tmpl := template.Must(template.New("otp_form").Parse(`
|
||||||
<form hx-post="/verify-otp" hx-target="#otp-form" hx-swap="outerHTML">
|
<form hx-post="/verify-otp" hx-target="#otp-form" hx-swap="innerHTML">
|
||||||
<input type="hidden" name="email" value="{{.Email}}">
|
<input type="hidden" name="email" value="{{.Email}}">
|
||||||
|
{{if .Error}}<div class="error-message" style="color: #dc2626; background: #fef2f2; border: 1px solid #fecaca; padding: 0.75rem; border-radius: 0.5rem; margin-bottom: 1rem;">{{.Error}}</div>{{end}}
|
||||||
<div style="display: flex; gap: 0.5rem; justify-content: center; margin: 1rem 0;">
|
<div style="display: flex; gap: 0.5rem; justify-content: center; margin: 1rem 0;">
|
||||||
<input type="text" name="digit_0" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="one-time-code" required
|
<input type="text" name="digit_0" maxlength="1" pattern="[0-9]" inputmode="numeric" autocomplete="one-time-code" required
|
||||||
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #d1d5db; border-radius: 0.5rem;">
|
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #d1d5db; border-radius: 0.5rem;">
|
||||||
|
|
@ -282,7 +284,7 @@ func renderOTPForm(w http.ResponseWriter, email string) {
|
||||||
`))
|
`))
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
if err := tmpl.Execute(w, map[string]string{"Email": email}); err != nil {
|
if err := tmpl.Execute(w, map[string]string{"Email": email, "Error": errMsg}); err != nil {
|
||||||
log.Printf("ERROR [%s] handlers: renderOTPForm execute: %v", time.Now().Format(time.RFC3339), err)
|
log.Printf("ERROR [%s] handlers: renderOTPForm execute: %v", time.Now().Format(time.RFC3339), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,13 @@
|
||||||
</svg>
|
</svg>
|
||||||
Capture Receipt
|
Capture Receipt
|
||||||
</label>
|
</label>
|
||||||
<input type="file" id="receipt-upload" accept="image/*" capture="environment" style="display: none;"
|
<input type="file" id="receipt-upload" name="receipt" accept="image/*" capture="environment" style="display: none;"
|
||||||
hx-post="/expenses/upload"
|
hx-post="/expenses/upload"
|
||||||
hx-encoding="multipart/form-data"
|
hx-encoding="multipart/form-data"
|
||||||
hx-target="#receipt-form"
|
hx-target="#receipt-form"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-indicator="#upload-indicator">
|
hx-indicator="#upload-indicator"
|
||||||
|
hx-trigger="change">
|
||||||
<div id="upload-indicator" class="htmx-indicator" style="text-align: center; padding: 1rem;">
|
<div id="upload-indicator" class="htmx-indicator" style="text-align: center; padding: 1rem;">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<p>Analyzing receipt...</p>
|
<p>Analyzing receipt...</p>
|
||||||
|
|
@ -130,11 +131,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Auto-trigger file input on label click
|
|
||||||
document.querySelector('label[for="receipt-upload"]')?.addEventListener('click', function() {
|
|
||||||
document.getElementById('receipt-upload').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal on overlay click
|
// Close modal on overlay click
|
||||||
document.querySelector('.modal-overlay')?.addEventListener('click', function(e) {
|
document.querySelector('.modal-overlay')?.addEventListener('click', function(e) {
|
||||||
if (e.target === this) {
|
if (e.target === this) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="otp-form">
|
<div id="otp-form">
|
||||||
<form hx-post="/request-otp" hx-target="#otp-form" hx-swap="outerHTML">
|
<form hx-post="/request-otp" hx-target="#otp-form" hx-swap="innerHTML">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">Email Address</label>
|
<label for="email">Email Address</label>
|
||||||
<input type="email" id="email" name="email" placeholder="you@example.com" required autocomplete="email" inputmode="email">
|
<input type="email" id="email" name="email" placeholder="you@example.com" required autocomplete="email" inputmode="email">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue