persona-community-1/pkg/email/dev_handler.go
jordan 4004f88f4a
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-23 10:20:59 +00:00

123 lines
3.6 KiB
Go

package email
import (
"fmt"
"html/template"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
)
// DevHandler serves live HTML previews of email templates.
// It is ONLY safe to mount in development environments — never in production.
type DevHandler struct {
renderer *Renderer
}
// NewDevHandler creates a new DevHandler backed by the given renderer.
func NewDevHandler(r *Renderer) *DevHandler {
return &DevHandler{renderer: r}
}
// List serves an HTML page listing all available email template previews.
// Mount at GET /dev/emails.
func (h *DevHandler) List(w http.ResponseWriter, r *http.Request) {
purposes := h.renderer.Purposes()
var sb strings.Builder
sb.WriteString(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Email Templates — Dev Preview</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 640px; margin: 48px auto; padding: 0 24px; color: #111827; }
h1 { font-size: 24px; font-weight: 700; margin-bottom: 8px; }
p.hint { color: #6b7280; font-size: 14px; margin-bottom: 32px; }
ul { list-style: none; padding: 0; }
li { margin-bottom: 12px; }
a { display: block; padding: 14px 18px; border: 1px solid #e5e7eb; border-radius: 8px; text-decoration: none; color: #374151; font-size: 15px; transition: background 0.1s; }
a:hover { background: #f9fafb; border-color: #d1d5db; }
.badge { float: right; font-size: 12px; color: #6366f1; background: #eef2ff; padding: 2px 8px; border-radius: 4px; }
</style>
</head>
<body>
<h1>Email Templates</h1>
<p class="hint">Development preview — not visible in production.</p>
<ul>`)
for _, p := range purposes {
sb.WriteString(fmt.Sprintf(
` <li><a href="/dev/emails/%s"><span class="badge">preview</span>%s</a></li>`,
template.HTMLEscapeString(p),
template.HTMLEscapeString(p),
))
sb.WriteByte('\n')
}
sb.WriteString(` </ul>
</body>
</html>`)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprint(w, sb.String())
}
// Preview renders an email template with placeholder data and serves the HTML.
// Mount at GET /dev/emails/{purpose}.
func (h *DevHandler) Preview(w http.ResponseWriter, r *http.Request) {
purpose := chi.URLParam(r, "purpose")
ctx := placeholderContext(purpose)
rendered, err := h.renderer.Render(purpose, ctx)
if err != nil {
http.Error(w, fmt.Sprintf("render %q: %v", purpose, err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = fmt.Fprint(w, rendered.HTML)
}
// placeholderContext returns realistic placeholder data for each email type.
// These values are used by the dev preview only — never sent to real users.
func placeholderContext(purpose string) EmailContext {
switch purpose {
case "login_otp":
return EmailContext{
Code: "482916",
ExpiresIn: 10,
Purpose: "sign in",
}
case "magic_link":
return EmailContext{
ActionURL: "https://example.com/auth/verify?token=preview-token",
ButtonText: "Sign In \u2192",
ExpiresIn: 15,
}
case "password_reset":
return EmailContext{
ActionURL: "https://example.com/auth/reset?token=preview-token",
ButtonText: "Reset Password \u2192",
ExpiresIn: 60,
}
case "verify_email":
return EmailContext{
Code: "738201",
ExpiresIn: 30,
Purpose: "verify your email",
}
case "welcome":
return EmailContext{
ActionURL: "https://example.com/dashboard",
ButtonText: "Get Started \u2192",
Name: "Jordan",
}
default:
return EmailContext{}
}
}