NextExpense/internal/handlers/templates.go
cclohmar 92f070440f refactor: implement best-practice recommendations from code review
MUST FIX:
- M1: Fixed ignored errors in AI providers (json.Marshal, http.NewRequest, json.Unmarshal)
- M2: Template cache — pre-parse all templates once at startup, reuse via getTemplate()
- M3: Fixed silent ParseFloat error fallbacks — now returns HTTP 400 on invalid amounts
- M4: Wrapped readFile errors with context (fmt.Errorf with %w)
- M5: Deleted stale llm.go placeholder file
- M6: Renamed utils.New() to utils.NewUUID() for clarity
- M7: Validate current_event_id cookie UUID format, prevent tampering

SHOULD FIX:
- S4: Added utils.Timestamp() helper to replace repeated time.Now().Format() calls
- S6: Added request ID middleware for concurrent request log tracing
- S7: Increased DB pool from 1 to 4 connections (HTMX concurrency)
- S8: Graceful shutdown via http.Server.Shutdown() on SIGINT/SIGTERM
- S9: Storage served behind auth middleware with path traversal check

COULD FIX:
- C2: renderOTPForm uses cached template (not per-request Must)
- C3: CSP pinned to unpkg.com/htmx.org@1.9.10
- C4: Added ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout
- C7: PDF generation auto-adds page breaks when content overflows

ADDITIONAL:
- Pass config to AI provider constructors (newGeminiProvider, newOpenAIProvider)
- Value receivers on geminiProvider/openaiProvider (empty structs)
- Added envOrDefault() helper in ai/receipt.go
- Session cleanup goroutine started in main.go
- Removed duplicate imports and unused html/template from handlers
2026-05-31 02:13:07 +00:00

72 lines
3.5 KiB
Go

package handlers
import (
"html/template"
"log"
"path/filepath"
"sync"
)
var (
templatesOnce sync.Once
templates map[string]*template.Template
)
// getTemplate returns a cached template by filename (e.g. "dashboard.html").
// Templates are parsed once from the templates/ directory on first call.
func getTemplate(name string) *template.Template {
templatesOnce.Do(loadTemplates)
t := templates[name]
if t == nil {
log.Panicf("template %q not found in cache — did you delete templates/%s?", name, name)
}
return t
}
// loadTemplates walks the templates/ directory and pre-parses all .html files.
func loadTemplates() {
templates = make(map[string]*template.Template)
files, err := filepath.Glob("templates/*.html")
if err != nil {
log.Panicf("list templates: %v", err)
}
// Parse each file into its own named template.
for _, f := range files {
name := filepath.Base(f)
t, err := template.ParseFiles(f)
if err != nil {
log.Panicf("parse template %s: %v", f, err)
}
templates[name] = t
}
// Also register the inline OTP form template.
otpTmpl := template.Must(template.New("otp_form").Parse(otpFormHTML))
templates["otp_form"] = otpTmpl
log.Printf("Loaded %d templates", len(templates))
}
// otpFormHTML is the inline OTP form template fragment.
const otpFormHTML = `
<form hx-post="/verify-otp" hx-target="#otp-form" hx-swap="innerHTML">
<input type="hidden" name="email" value="{{.Email}}">
{{if .Error}}<div class="error-message" style="color: #fca5a5; background: #450a0a; border: 1px solid #7f1d1d; 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;">
<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 #475569; border-radius: 0.5rem; background: #1e293b; color: #f8fafc;">
<input type="text" name="digit_1" maxlength="1" pattern="[0-9]" inputmode="numeric" required
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #475569; border-radius: 0.5rem; background: #1e293b; color: #f8fafc;">
<input type="text" name="digit_2" maxlength="1" pattern="[0-9]" inputmode="numeric" required
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #475569; border-radius: 0.5rem; background: #1e293b; color: #f8fafc;">
<input type="text" name="digit_3" maxlength="1" pattern="[0-9]" inputmode="numeric" required
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #475569; border-radius: 0.5rem; background: #1e293b; color: #f8fafc;">
<input type="text" name="digit_4" maxlength="1" pattern="[0-9]" inputmode="numeric" required
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #475569; border-radius: 0.5rem; background: #1e293b; color: #f8fafc;">
<input type="text" name="digit_5" maxlength="1" pattern="[0-9]" inputmode="numeric" required
style="width: 3rem; height: 3rem; text-align: center; font-size: 1.5rem; border: 2px solid #475569; border-radius: 0.5rem; background: #1e293b; color: #f8fafc;">
</div>
<button type="submit" class="btn btn-primary btn-block">Verify Code</button>
</form>`