mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:01:09 +02:00
feat(apps): create Hono compute servers for Traces, Planta, NutriPhi
Add lightweight Hono + Bun servers for server-only compute endpoints. CRUD is handled by mana-sync, these handle AI + file upload only. Traces: AI guide generation, location sync (Port 3026) Planta: Photo upload (S3), AI plant analysis (Port 3022) NutriPhi: AI meal analysis (photo+text), recommendations (Port 3023) Each uses @manacore/shared-hono for auth/health/errors. ~100-200 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d26196590
commit
d3d11e661d
30 changed files with 1161 additions and 221 deletions
|
|
@ -3,6 +3,7 @@ module github.com/manacore/mana-notify
|
|||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/manacore/shared-go v0.0.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
|
|
@ -24,3 +25,5 @@ require (
|
|||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
)
|
||||
|
||||
replace github.com/manacore/shared-go => ../../packages/shared-go
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/manacore/shared-go/envutil"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
|
@ -18,7 +16,7 @@ type Config struct {
|
|||
RedisPassword string
|
||||
|
||||
// Auth
|
||||
ServiceKey string
|
||||
ServiceKey string
|
||||
ManaCoreAuthURL string
|
||||
|
||||
// SMTP (Brevo)
|
||||
|
|
@ -45,54 +43,31 @@ type Config struct {
|
|||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Port: getEnvInt("PORT", 3040),
|
||||
Port: envutil.GetInt("PORT", 3040),
|
||||
|
||||
DatabaseURL: getEnv("DATABASE_URL", "postgresql://manacore:manacore@localhost:5432/mana_notify"),
|
||||
DatabaseURL: envutil.Get("DATABASE_URL", "postgresql://manacore:manacore@localhost:5432/mana_notify"),
|
||||
|
||||
RedisHost: getEnv("REDIS_HOST", "localhost"),
|
||||
RedisPort: getEnvInt("REDIS_PORT", 6379),
|
||||
RedisPassword: getEnv("REDIS_PASSWORD", ""),
|
||||
RedisHost: envutil.Get("REDIS_HOST", "localhost"),
|
||||
RedisPort: envutil.GetInt("REDIS_PORT", 6379),
|
||||
RedisPassword: envutil.Get("REDIS_PASSWORD", ""),
|
||||
|
||||
ServiceKey: getEnv("SERVICE_KEY", "dev-service-key"),
|
||||
ManaCoreAuthURL: getEnv("MANA_CORE_AUTH_URL", "http://localhost:3001"),
|
||||
ServiceKey: envutil.Get("SERVICE_KEY", "dev-service-key"),
|
||||
ManaCoreAuthURL: envutil.Get("MANA_CORE_AUTH_URL", "http://localhost:3001"),
|
||||
|
||||
SMTPHost: getEnv("SMTP_HOST", "smtp-relay.brevo.com"),
|
||||
SMTPPort: getEnvInt("SMTP_PORT", 587),
|
||||
SMTPUser: getEnv("SMTP_USER", ""),
|
||||
SMTPPassword: getEnv("SMTP_PASSWORD", ""),
|
||||
SMTPFrom: getEnv("SMTP_FROM", "ManaCore <noreply@mana.how>"),
|
||||
SMTPHost: envutil.Get("SMTP_HOST", "smtp-relay.brevo.com"),
|
||||
SMTPPort: envutil.GetInt("SMTP_PORT", 587),
|
||||
SMTPUser: envutil.Get("SMTP_USER", ""),
|
||||
SMTPPassword: envutil.Get("SMTP_PASSWORD", ""),
|
||||
SMTPFrom: envutil.Get("SMTP_FROM", "ManaCore <noreply@mana.how>"),
|
||||
|
||||
ExpoAccessToken: getEnv("EXPO_ACCESS_TOKEN", ""),
|
||||
ExpoAccessToken: envutil.Get("EXPO_ACCESS_TOKEN", ""),
|
||||
|
||||
MatrixHomeserverURL: getEnv("MATRIX_HOMESERVER_URL", ""),
|
||||
MatrixAccessToken: getEnv("MATRIX_ACCESS_TOKEN", ""),
|
||||
MatrixHomeserverURL: envutil.Get("MATRIX_HOMESERVER_URL", ""),
|
||||
MatrixAccessToken: envutil.Get("MATRIX_ACCESS_TOKEN", ""),
|
||||
|
||||
RateLimitEmailPerMinute: getEnvInt("RATE_LIMIT_EMAIL_PER_MINUTE", 10),
|
||||
RateLimitPushPerMinute: getEnvInt("RATE_LIMIT_PUSH_PER_MINUTE", 100),
|
||||
RateLimitEmailPerMinute: envutil.GetInt("RATE_LIMIT_EMAIL_PER_MINUTE", 10),
|
||||
RateLimitPushPerMinute: envutil.GetInt("RATE_LIMIT_PUSH_PER_MINUTE", 100),
|
||||
|
||||
CORSOrigins: getEnvSlice("CORS_ORIGINS", []string{"http://localhost:3000", "http://localhost:5173"}),
|
||||
CORSOrigins: envutil.GetSlice("CORS_ORIGINS", []string{"http://localhost:3000", "http://localhost:5173"}),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnvSlice(key string, fallback []string) []string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return strings.Split(v, ",")
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, message string) {
|
||||
writeJSON(w, status, map[string]any{
|
||||
"success": false,
|
||||
"error": map[string]any{
|
||||
"statusCode": status,
|
||||
"message": message,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
|
||||
"github.com/manacore/mana-notify/internal/auth"
|
||||
"github.com/manacore/mana-notify/internal/db"
|
||||
)
|
||||
|
|
@ -20,7 +22,7 @@ func NewDevicesHandler(database *db.DB) *DevicesHandler {
|
|||
func (h *DevicesHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.GetUser(r)
|
||||
if user == nil {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -32,12 +34,12 @@ func (h *DevicesHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|||
AppID string `json:"appId,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.PushToken == "" {
|
||||
writeError(w, http.StatusBadRequest, "pushToken is required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "pushToken is required")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -60,18 +62,18 @@ func (h *DevicesHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|||
user.UserID, req.PushToken, tokenType, nilIfEmpty(req.Platform), nilIfEmpty(req.DeviceName), nilIfEmpty(req.AppID),
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to register device")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to register device")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"device": map[string]any{"id": id}})
|
||||
httputil.WriteJSON(w, http.StatusCreated, map[string]any{"device": map[string]any{"id": id}})
|
||||
}
|
||||
|
||||
// List handles GET /api/v1/devices
|
||||
func (h *DevicesHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.GetUser(r)
|
||||
if user == nil {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -79,7 +81,7 @@ func (h *DevicesHandler) List(w http.ResponseWriter, r *http.Request) {
|
|||
`SELECT id, user_id, push_token, token_type, platform, device_name, app_id, is_active, last_seen_at, created_at, updated_at
|
||||
FROM notify.devices WHERE user_id = $1 AND is_active = true ORDER BY created_at DESC`, user.UserID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to list devices")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to list devices")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
|
@ -94,14 +96,14 @@ func (h *DevicesHandler) List(w http.ResponseWriter, r *http.Request) {
|
|||
devices = append(devices, d)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"devices": devices})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"devices": devices})
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/v1/devices/{id}
|
||||
func (h *DevicesHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.GetUser(r)
|
||||
if user == nil {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -109,13 +111,13 @@ func (h *DevicesHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
result, err := h.db.Pool.Exec(r.Context(),
|
||||
`UPDATE notify.devices SET is_active = false, updated_at = NOW() WHERE id = $1 AND user_id = $2`, id, user.UserID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete device")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to delete device")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
writeError(w, http.StatusNotFound, "device not found")
|
||||
httputil.WriteError(w, http.StatusNotFound, "device not found")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"deleted": true})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"deleted": true})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package handler
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/manacore/mana-notify/internal/db"
|
||||
|
|
@ -24,7 +26,7 @@ func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
|
|||
status = "unhealthy"
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"status": status,
|
||||
"version": "1.0.0",
|
||||
"service": "mana-notify",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import (
|
|||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
"time"
|
||||
|
||||
"github.com/manacore/mana-notify/internal/db"
|
||||
|
|
@ -77,12 +79,12 @@ type BatchRequest struct {
|
|||
func (h *NotificationsHandler) Send(w http.ResponseWriter, r *http.Request) {
|
||||
var req SendRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateSendRequest(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
httputil.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +95,7 @@ func (h *NotificationsHandler) Send(w http.ResponseWriter, r *http.Request) {
|
|||
`SELECT id FROM notify.notifications WHERE external_id = $1`, req.ExternalID,
|
||||
).Scan(&existingID)
|
||||
if err == nil {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"notification": map[string]any{"id": existingID, "status": "existing"},
|
||||
"deduplicated": true,
|
||||
})
|
||||
|
|
@ -105,7 +107,7 @@ func (h *NotificationsHandler) Send(w http.ResponseWriter, r *http.Request) {
|
|||
if req.UserID != "" {
|
||||
blocked, reason := h.checkPreferences(r.Context(), req.UserID, req.Channel)
|
||||
if blocked {
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"notification": map[string]any{"status": "cancelled", "reason": reason},
|
||||
})
|
||||
return
|
||||
|
|
@ -146,7 +148,7 @@ func (h *NotificationsHandler) Send(w http.ResponseWriter, r *http.Request) {
|
|||
).Scan(¬ificationID)
|
||||
if err != nil {
|
||||
slog.Error("create notification failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "failed to create notification")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to create notification")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +187,7 @@ func (h *NotificationsHandler) Send(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
h.pool.Enqueue(job)
|
||||
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusAccepted, map[string]any{
|
||||
"notification": map[string]any{
|
||||
"id": notificationID,
|
||||
"status": "pending",
|
||||
|
|
@ -197,22 +199,22 @@ func (h *NotificationsHandler) Send(w http.ResponseWriter, r *http.Request) {
|
|||
func (h *NotificationsHandler) Schedule(w http.ResponseWriter, r *http.Request) {
|
||||
var req ScheduleRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
scheduledFor, err := time.Parse(time.RFC3339, req.ScheduledFor)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "scheduledFor must be a valid RFC3339 timestamp")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "scheduledFor must be a valid RFC3339 timestamp")
|
||||
return
|
||||
}
|
||||
if scheduledFor.Before(time.Now()) {
|
||||
writeError(w, http.StatusBadRequest, "scheduledFor must be in the future")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "scheduledFor must be in the future")
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateSendRequest(&req.SendRequest); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
httputil.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +248,7 @@ func (h *NotificationsHandler) Schedule(w http.ResponseWriter, r *http.Request)
|
|||
priority, nilIfEmpty(req.Recipient), nilIfEmpty(req.ExternalID), scheduledFor,
|
||||
).Scan(¬ificationID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create notification")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to create notification")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +264,7 @@ func (h *NotificationsHandler) Schedule(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
h.pool.Enqueue(job)
|
||||
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusAccepted, map[string]any{
|
||||
"notification": map[string]any{
|
||||
"id": notificationID,
|
||||
"status": "pending",
|
||||
|
|
@ -275,16 +277,16 @@ func (h *NotificationsHandler) Schedule(w http.ResponseWriter, r *http.Request)
|
|||
func (h *NotificationsHandler) Batch(w http.ResponseWriter, r *http.Request) {
|
||||
var req BatchRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 5<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Notifications) == 0 {
|
||||
writeError(w, http.StatusBadRequest, "notifications array is required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "notifications array is required")
|
||||
return
|
||||
}
|
||||
if len(req.Notifications) > 100 {
|
||||
writeError(w, http.StatusBadRequest, "maximum 100 notifications per batch")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "maximum 100 notifications per batch")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -352,7 +354,7 @@ func (h *NotificationsHandler) Batch(w http.ResponseWriter, r *http.Request) {
|
|||
succeeded++
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusAccepted, map[string]any{
|
||||
"results": results,
|
||||
"succeeded": succeeded,
|
||||
"failed": failed,
|
||||
|
|
@ -363,7 +365,7 @@ func (h *NotificationsHandler) Batch(w http.ResponseWriter, r *http.Request) {
|
|||
func (h *NotificationsHandler) GetNotification(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if id == "" {
|
||||
writeError(w, http.StatusBadRequest, "notification id required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "notification id required")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -376,33 +378,33 @@ func (h *NotificationsHandler) GetNotification(w http.ResponseWriter, r *http.Re
|
|||
&n.Attempts, &n.DeliveredAt, &n.ErrorMessage, &n.CreatedAt, &n.UpdatedAt)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "notification not found")
|
||||
httputil.WriteError(w, http.StatusNotFound, "notification not found")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"notification": n})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"notification": n})
|
||||
}
|
||||
|
||||
// CancelNotification handles DELETE /api/v1/notifications/{id}
|
||||
func (h *NotificationsHandler) CancelNotification(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
if id == "" {
|
||||
writeError(w, http.StatusBadRequest, "notification id required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "notification id required")
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.db.Pool.Exec(r.Context(),
|
||||
`UPDATE notify.notifications SET status = 'cancelled', updated_at = NOW() WHERE id = $1 AND status = 'pending'`, id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to cancel notification")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to cancel notification")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
writeError(w, http.StatusNotFound, "notification not found or already processed")
|
||||
httputil.WriteError(w, http.StatusNotFound, "notification not found or already processed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"cancelled": true})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"cancelled": true})
|
||||
}
|
||||
|
||||
func (h *NotificationsHandler) checkPreferences(ctx context.Context, userID, ch string) (bool, string) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
|
||||
"github.com/manacore/mana-notify/internal/auth"
|
||||
"github.com/manacore/mana-notify/internal/db"
|
||||
)
|
||||
|
|
@ -20,7 +22,7 @@ func NewPreferencesHandler(database *db.DB) *PreferencesHandler {
|
|||
func (h *PreferencesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.GetUser(r)
|
||||
if user == nil {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +35,7 @@ func (h *PreferencesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
if err != nil {
|
||||
// Return defaults
|
||||
writeJSON(w, http.StatusOK, map[string]any{
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{
|
||||
"preferences": map[string]any{
|
||||
"emailEnabled": false,
|
||||
"pushEnabled": true,
|
||||
|
|
@ -44,14 +46,14 @@ func (h *PreferencesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"preferences": p})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"preferences": p})
|
||||
}
|
||||
|
||||
// Update handles PUT /api/v1/preferences
|
||||
func (h *PreferencesHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
user := auth.GetUser(r)
|
||||
if user == nil {
|
||||
writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ func (h *PreferencesHandler) Update(w http.ResponseWriter, r *http.Request) {
|
|||
CategoryPreferences map[string]any `json:"categoryPreferences,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -87,9 +89,9 @@ func (h *PreferencesHandler) Update(w http.ResponseWriter, r *http.Request) {
|
|||
req.QuietHoursStart, req.QuietHoursEnd, req.Timezone, catJSON,
|
||||
)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to update preferences")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to update preferences")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"updated": true})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"updated": true})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/manacore/shared-go/httputil"
|
||||
|
||||
"github.com/manacore/mana-notify/internal/db"
|
||||
tmpl "github.com/manacore/mana-notify/internal/template"
|
||||
)
|
||||
|
|
@ -23,7 +25,7 @@ func (h *TemplatesHandler) List(w http.ResponseWriter, r *http.Request) {
|
|||
`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 {
|
||||
writeError(w, http.StatusInternalServerError, "failed to list templates")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to list templates")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
|
@ -38,7 +40,7 @@ func (h *TemplatesHandler) List(w http.ResponseWriter, r *http.Request) {
|
|||
templates = append(templates, t)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"templates": templates})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"templates": templates})
|
||||
}
|
||||
|
||||
// Get handles GET /api/v1/templates/{slug}
|
||||
|
|
@ -56,11 +58,11 @@ func (h *TemplatesHandler) Get(w http.ResponseWriter, r *http.Request) {
|
|||
).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 {
|
||||
writeError(w, http.StatusNotFound, "template not found")
|
||||
httputil.WriteError(w, http.StatusNotFound, "template not found")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"template": t})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"template": t})
|
||||
}
|
||||
|
||||
// Create handles POST /api/v1/templates
|
||||
|
|
@ -75,12 +77,12 @@ func (h *TemplatesHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||
Variables any `json:"variables,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Slug == "" || req.Channel == "" || req.BodyTemplate == "" {
|
||||
writeError(w, http.StatusBadRequest, "slug, channel, and bodyTemplate are required")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "slug, channel, and bodyTemplate are required")
|
||||
return
|
||||
}
|
||||
if req.Locale == "" {
|
||||
|
|
@ -96,11 +98,11 @@ func (h *TemplatesHandler) Create(w http.ResponseWriter, r *http.Request) {
|
|||
req.Slug, nilIfEmpty(req.AppID), req.Channel, nilIfEmpty(req.Subject), req.BodyTemplate, req.Locale, varsJSON,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusConflict, "template already exists for this slug+locale")
|
||||
httputil.WriteError(w, http.StatusConflict, "template already exists for this slug+locale")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]any{"id": id})
|
||||
httputil.WriteJSON(w, http.StatusCreated, map[string]any{"id": id})
|
||||
}
|
||||
|
||||
// Update handles PUT /api/v1/templates/{slug}
|
||||
|
|
@ -118,7 +120,7 @@ func (h *TemplatesHandler) Update(w http.ResponseWriter, r *http.Request) {
|
|||
Variables any `json:"variables,omitempty"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -132,15 +134,15 @@ func (h *TemplatesHandler) Update(w http.ResponseWriter, r *http.Request) {
|
|||
nilIfEmpty(req.Subject), nilIfEmpty(req.BodyTemplate), req.IsActive, slug, locale,
|
||||
)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to update template")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to update template")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
writeError(w, http.StatusNotFound, "template not found or is a system template")
|
||||
httputil.WriteError(w, http.StatusNotFound, "template not found or is a system template")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"updated": true})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"updated": true})
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/v1/templates/{slug}
|
||||
|
|
@ -150,15 +152,15 @@ func (h *TemplatesHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
result, err := h.db.Pool.Exec(r.Context(),
|
||||
`DELETE FROM notify.templates WHERE slug = $1 AND is_system = false`, slug)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to delete template")
|
||||
httputil.WriteError(w, http.StatusInternalServerError, "failed to delete template")
|
||||
return
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
writeError(w, http.StatusNotFound, "template not found or is a system template")
|
||||
httputil.WriteError(w, http.StatusNotFound, "template not found or is a system template")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"deleted": true})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"deleted": true})
|
||||
}
|
||||
|
||||
// Preview handles POST /api/v1/templates/{slug}/preview
|
||||
|
|
@ -168,17 +170,17 @@ func (h *TemplatesHandler) Preview(w http.ResponseWriter, r *http.Request) {
|
|||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
rendered, err := h.engine.RenderBySlug(r.Context(), slug, req.Data, "")
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "template not found")
|
||||
httputil.WriteError(w, http.StatusNotFound, "template not found")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"subject": rendered.Subject, "body": rendered.Body})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"subject": rendered.Subject, "body": rendered.Body})
|
||||
}
|
||||
|
||||
// PreviewCustom handles POST /api/v1/templates/preview
|
||||
|
|
@ -189,7 +191,7 @@ func (h *TemplatesHandler) PreviewCustom(w http.ResponseWriter, r *http.Request)
|
|||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +199,7 @@ func (h *TemplatesHandler) PreviewCustom(w http.ResponseWriter, r *http.Request)
|
|||
if req.Subject != "" {
|
||||
s, err := tmpl.RenderDirect(req.Subject, req.Data)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid subject template: "+err.Error())
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid subject template: "+err.Error())
|
||||
return
|
||||
}
|
||||
subject = s
|
||||
|
|
@ -205,9 +207,9 @@ func (h *TemplatesHandler) PreviewCustom(w http.ResponseWriter, r *http.Request)
|
|||
|
||||
body, err := tmpl.RenderDirect(req.BodyTemplate, req.Data)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid body template: "+err.Error())
|
||||
httputil.WriteError(w, http.StatusBadRequest, "invalid body template: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]any{"subject": subject, "body": body})
|
||||
httputil.WriteJSON(w, http.StatusOK, map[string]any{"subject": subject, "body": body})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue