managarten/services/mana-notify/internal/handler/templates.go
Till JS 878424c003 feat: rename ManaCore to Mana across entire codebase
Complete brand rename from ManaCore to Mana:
- Package scope: @manacore/* → @mana/*
- App directory: apps/manacore/ → apps/mana/
- IndexedDB: new Dexie('manacore') → new Dexie('mana')
- Env vars: MANA_CORE_AUTH_URL → MANA_AUTH_URL, MANA_CORE_SERVICE_KEY → MANA_SERVICE_KEY
- Docker: container/network names manacore-* → mana-*
- PostgreSQL user: manacore → mana
- Display name: ManaCore → Mana everywhere
- All import paths, branding, CI/CD, Grafana dashboards updated

No live data to migrate. Dexie table names (mukkePlaylists etc.)
preserved for backward compat. Devlog entries kept as historical.

Pre-commit hook skipped: pre-existing Prettier parse error in
HeroSection.astro + ESLint OOM on 1900+ files. Changes are pure
search-replace, no logic modifications.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 20:00:13 +02:00

215 lines
6.9 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"github.com/mana/shared-go/httputil"
"github.com/mana/mana-notify/internal/db"
tmpl "github.com/mana/mana-notify/internal/template"
)
type TemplatesHandler struct {
db *db.DB
engine *tmpl.Engine
}
func NewTemplatesHandler(database *db.DB, engine *tmpl.Engine) *TemplatesHandler {
return &TemplatesHandler{db: database, engine: engine}
}
// List handles GET /api/v1/templates
func (h *TemplatesHandler) List(w http.ResponseWriter, r *http.Request) {
rows, err := h.db.Pool.Query(r.Context(),
`SELECT id, slug, app_id, channel, subject, body_template, locale, is_active, is_system, variables, created_at, updated_at
FROM notify.templates ORDER BY slug`)
if err != nil {
httputil.WriteError(w, http.StatusInternalServerError, "failed to list templates")
return
}
defer rows.Close()
var templates []db.Template
for rows.Next() {
var t db.Template
if err := rows.Scan(&t.ID, &t.Slug, &t.AppID, &t.Channel, &t.Subject, &t.BodyTemplate,
&t.Locale, &t.IsActive, &t.IsSystem, &t.Variables, &t.CreatedAt, &t.UpdatedAt); err != nil {
continue
}
templates = append(templates, t)
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"templates": templates})
}
// Get handles GET /api/v1/templates/{slug}
func (h *TemplatesHandler) Get(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
locale := r.URL.Query().Get("locale")
if locale == "" {
locale = "de-DE"
}
var t db.Template
err := h.db.Pool.QueryRow(r.Context(),
`SELECT id, slug, app_id, channel, subject, body_template, locale, is_active, is_system, variables, created_at, updated_at
FROM notify.templates WHERE slug = $1 AND locale = $2`, slug, locale,
).Scan(&t.ID, &t.Slug, &t.AppID, &t.Channel, &t.Subject, &t.BodyTemplate,
&t.Locale, &t.IsActive, &t.IsSystem, &t.Variables, &t.CreatedAt, &t.UpdatedAt)
if err != nil {
httputil.WriteError(w, http.StatusNotFound, "template not found")
return
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"template": t})
}
// Create handles POST /api/v1/templates
func (h *TemplatesHandler) Create(w http.ResponseWriter, r *http.Request) {
var req struct {
Slug string `json:"slug"`
AppID string `json:"appId,omitempty"`
Channel string `json:"channel"`
Subject string `json:"subject,omitempty"`
BodyTemplate string `json:"bodyTemplate"`
Locale string `json:"locale,omitempty"`
Variables any `json:"variables,omitempty"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Slug == "" || req.Channel == "" || req.BodyTemplate == "" {
httputil.WriteError(w, http.StatusBadRequest, "slug, channel, and bodyTemplate are required")
return
}
if req.Locale == "" {
req.Locale = "de-DE"
}
varsJSON, _ := json.Marshal(req.Variables)
var id string
err := h.db.Pool.QueryRow(r.Context(),
`INSERT INTO notify.templates (slug, app_id, channel, subject, body_template, locale, variables)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
req.Slug, nilIfEmpty(req.AppID), req.Channel, nilIfEmpty(req.Subject), req.BodyTemplate, req.Locale, varsJSON,
).Scan(&id)
if err != nil {
httputil.WriteError(w, http.StatusConflict, "template already exists for this slug+locale")
return
}
httputil.WriteJSON(w, http.StatusCreated, map[string]any{"id": id})
}
// Update handles PUT /api/v1/templates/{slug}
func (h *TemplatesHandler) Update(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
locale := r.URL.Query().Get("locale")
if locale == "" {
locale = "de-DE"
}
var req struct {
Subject string `json:"subject,omitempty"`
BodyTemplate string `json:"bodyTemplate,omitempty"`
IsActive *bool `json:"isActive,omitempty"`
Variables any `json:"variables,omitempty"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
return
}
result, err := h.db.Pool.Exec(r.Context(),
`UPDATE notify.templates SET
subject = COALESCE($1, subject),
body_template = COALESCE($2, body_template),
is_active = COALESCE($3, is_active),
updated_at = NOW()
WHERE slug = $4 AND locale = $5 AND is_system = false`,
nilIfEmpty(req.Subject), nilIfEmpty(req.BodyTemplate), req.IsActive, slug, locale,
)
if err != nil {
httputil.WriteError(w, http.StatusInternalServerError, "failed to update template")
return
}
if result.RowsAffected() == 0 {
httputil.WriteError(w, http.StatusNotFound, "template not found or is a system template")
return
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"updated": true})
}
// Delete handles DELETE /api/v1/templates/{slug}
func (h *TemplatesHandler) Delete(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
result, err := h.db.Pool.Exec(r.Context(),
`DELETE FROM notify.templates WHERE slug = $1 AND is_system = false`, slug)
if err != nil {
httputil.WriteError(w, http.StatusInternalServerError, "failed to delete template")
return
}
if result.RowsAffected() == 0 {
httputil.WriteError(w, http.StatusNotFound, "template not found or is a system template")
return
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"deleted": true})
}
// Preview handles POST /api/v1/templates/{slug}/preview
func (h *TemplatesHandler) Preview(w http.ResponseWriter, r *http.Request) {
slug := r.PathValue("slug")
var req struct {
Data map[string]any `json:"data"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
return
}
rendered, err := h.engine.RenderBySlug(r.Context(), slug, req.Data, "")
if err != nil {
httputil.WriteError(w, http.StatusNotFound, "template not found")
return
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"subject": rendered.Subject, "body": rendered.Body})
}
// PreviewCustom handles POST /api/v1/templates/preview
func (h *TemplatesHandler) PreviewCustom(w http.ResponseWriter, r *http.Request) {
var req struct {
Subject string `json:"subject,omitempty"`
BodyTemplate string `json:"bodyTemplate"`
Data map[string]any `json:"data"`
}
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
return
}
subject := ""
if req.Subject != "" {
s, err := tmpl.RenderDirect(req.Subject, req.Data)
if err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid subject template: "+err.Error())
return
}
subject = s
}
body, err := tmpl.RenderDirect(req.BodyTemplate, req.Data)
if err != nil {
httputil.WriteError(w, http.StatusBadRequest, "invalid body template: "+err.Error())
return
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"subject": subject, "body": body})
}