managarten/services/mana-notify/internal/handler/devices.go
Till JS d3d11e661d 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>
2026-03-28 16:16:57 +01:00

123 lines
3.6 KiB
Go

package handler
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"
)
type DevicesHandler struct {
db *db.DB
}
func NewDevicesHandler(database *db.DB) *DevicesHandler {
return &DevicesHandler{db: database}
}
// Register handles POST /api/v1/devices/register
func (h *DevicesHandler) Register(w http.ResponseWriter, r *http.Request) {
user := auth.GetUser(r)
if user == nil {
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
return
}
var req struct {
PushToken string `json:"pushToken"`
TokenType string `json:"tokenType,omitempty"`
Platform string `json:"platform,omitempty"`
DeviceName string `json:"deviceName,omitempty"`
AppID string `json:"appId,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.PushToken == "" {
httputil.WriteError(w, http.StatusBadRequest, "pushToken is required")
return
}
tokenType := req.TokenType
if tokenType == "" {
tokenType = "expo"
}
// Upsert: transfer ownership if token exists for different user
var id string
err := h.db.Pool.QueryRow(r.Context(),
`INSERT INTO notify.devices (user_id, push_token, token_type, platform, device_name, app_id)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (push_token) DO UPDATE SET
user_id = EXCLUDED.user_id,
is_active = true,
last_seen_at = NOW(),
updated_at = NOW()
RETURNING id`,
user.UserID, req.PushToken, tokenType, nilIfEmpty(req.Platform), nilIfEmpty(req.DeviceName), nilIfEmpty(req.AppID),
).Scan(&id)
if err != nil {
httputil.WriteError(w, http.StatusInternalServerError, "failed to register device")
return
}
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 {
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
return
}
rows, err := h.db.Pool.Query(r.Context(),
`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 {
httputil.WriteError(w, http.StatusInternalServerError, "failed to list devices")
return
}
defer rows.Close()
var devices []db.Device
for rows.Next() {
var d db.Device
if err := rows.Scan(&d.ID, &d.UserID, &d.PushToken, &d.TokenType, &d.Platform,
&d.DeviceName, &d.AppID, &d.IsActive, &d.LastSeenAt, &d.CreatedAt, &d.UpdatedAt); err != nil {
continue
}
devices = append(devices, d)
}
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 {
httputil.WriteError(w, http.StatusUnauthorized, "unauthorized")
return
}
id := r.PathValue("id")
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 {
httputil.WriteError(w, http.StatusInternalServerError, "failed to delete device")
return
}
if result.RowsAffected() == 0 {
httputil.WriteError(w, http.StatusNotFound, "device not found")
return
}
httputil.WriteJSON(w, http.StatusOK, map[string]any{"deleted": true})
}