123 lines
3.6 KiB
Go
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{}
|
|
}
|
|
}
|