Files
Claude (gsc-ops-api init) 3847eb2036 Initial import — snapshot from admin host /srv/gosec/gsc-ops-api
This repo had no version control prior to this commit. The import is a
straight snapshot of the working tree at 2026-05-03; the deployed
binary on fihelvop01 was being rebuilt from this source via `make
build` + scp into place, with no upstream review path.

The snapshot already includes one in-flight fix made on 2026-05-03 to
internal/service/persona.go:GetSelfModel — the handler queried
`source` and `strength` columns plus an `is_active = true` filter on
persona.persona_commitments, none of which exist on that table (its
shape is session-bound commitments with `status`, `commitment_meta`,
etc.). The query returned a 500 every time SynapseHub bootstrapped a
persona's self-model, dropping the IdentityConstraints / Commitments /
ConscienceStandards layer from the assembled prompt. The patched
query reads existing columns only (commitment_text, commitment_type),
filters on `status='active'`, and synthesises Source="learned" /
Strength=1.0 to keep the SelfModel response shape stable for callers.

Verified live: `GET /api/v1/personas/70f7cfd9-.../self-model` now
returns 200 with `{identityConstraints:[],commitments:[],
conscienceStandards:[]}` instead of 500.

Future changes go through PRs against this repo — no more bin-only
deploys.

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

604 lines
20 KiB
Go

package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// PBXHandler handles PBX management endpoints
type PBXHandler struct {
svc *service.PBXService
}
// NewPBXHandler creates a new PBX handler
func NewPBXHandler(svc *service.PBXService) *PBXHandler {
return &PBXHandler{svc: svc}
}
// ============================================================================
// Trunks
// ============================================================================
// ListTrunks handles GET /api/v1/pbx/trunks
func (h *PBXHandler) ListTrunks(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
params := types.ListParams{
Limit: c.QueryInt("limit", 50),
Offset: c.QueryInt("offset", 0),
Search: c.Query("search"),
Status: c.Query("status"),
}
trunks, total, err := h.svc.ListTrunks(c.Context(), params)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(trunks, total, params.Limit, params.Offset, reqID))
}
// GetTrunk handles GET /api/v1/pbx/trunks/:id
func (h *PBXHandler) GetTrunk(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
trunk, err := h.svc.GetTrunk(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("Trunk not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(trunk, reqID))
}
// CreateTrunk handles POST /api/v1/pbx/trunks
func (h *PBXHandler) CreateTrunk(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.PBXTrunkCreate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if req.Name == "" || req.Host == "" || req.TenantID == uuid.Nil {
apiErr := types.NewValidation("tenantId, name, and host are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
trunk, err := h.svc.CreateTrunk(c.Context(), &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.Status(fiber.StatusCreated).JSON(types.NewDataResponse(trunk, reqID))
}
// UpdateTrunk handles PUT /api/v1/pbx/trunks/:id
func (h *PBXHandler) UpdateTrunk(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.PBXTrunkUpdate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
trunk, err := h.svc.UpdateTrunk(c.Context(), id, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(trunk, reqID))
}
// DeleteTrunk handles DELETE /api/v1/pbx/trunks/:id
func (h *PBXHandler) DeleteTrunk(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteTrunk(c.Context(), id); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"id": id, "deleted": true}, reqID))
}
// ActivateTrunk handles POST /api/v1/pbx/trunks/:id/activate
func (h *PBXHandler) ActivateTrunk(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
trunk, err := h.svc.ActivateTrunk(c.Context(), id)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(trunk, reqID))
}
// DeactivateTrunk handles POST /api/v1/pbx/trunks/:id/deactivate
func (h *PBXHandler) DeactivateTrunk(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
trunk, err := h.svc.DeactivateTrunk(c.Context(), id)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(trunk, reqID))
}
// ============================================================================
// Trunk DIDs
// ============================================================================
// ListTrunkDIDs handles GET /api/v1/pbx/trunks/:id/dids
func (h *PBXHandler) ListTrunkDIDs(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
trunkID, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
dids, err := h.svc.ListTrunkDIDs(c.Context(), trunkID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(dids, reqID))
}
// CreateTrunkDID handles POST /api/v1/pbx/trunks/:id/dids
func (h *PBXHandler) CreateTrunkDID(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
trunkID, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.PBXTrunkDIDCreate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if req.DIDNumber == "" || req.TenantID == uuid.Nil {
apiErr := types.NewValidation("tenantId and didNumber are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
did, err := h.svc.CreateTrunkDID(c.Context(), trunkID, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.Status(fiber.StatusCreated).JSON(types.NewDataResponse(did, reqID))
}
// DeleteTrunkDID handles DELETE /api/v1/pbx/trunks/:id/dids/:didId
func (h *PBXHandler) DeleteTrunkDID(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
trunkID, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid trunk ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
didID, err := uuid.Parse(c.Params("didId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid DID ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteTrunkDID(c.Context(), trunkID, didID); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"id": didID, "deleted": true}, reqID))
}
// ============================================================================
// Extensions
// ============================================================================
// ListExtensions handles GET /api/v1/pbx/extensions
func (h *PBXHandler) ListExtensions(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
params := types.ListParams{
Limit: c.QueryInt("limit", 50),
Offset: c.QueryInt("offset", 0),
Search: c.Query("search"),
Status: c.Query("status"),
}
exts, total, err := h.svc.ListExtensions(c.Context(), params)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(exts, total, params.Limit, params.Offset, reqID))
}
// GetExtension handles GET /api/v1/pbx/extensions/:id
func (h *PBXHandler) GetExtension(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid extension ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
ext, err := h.svc.GetExtension(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("Extension not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(ext, reqID))
}
// CreateExtension handles POST /api/v1/pbx/extensions
func (h *PBXHandler) CreateExtension(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.PBXExtensionCreate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if req.Extension == "" || req.Name == "" || req.TenantID == uuid.Nil {
apiErr := types.NewValidation("tenantId, extension, and name are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
ext, err := h.svc.CreateExtension(c.Context(), &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.Status(fiber.StatusCreated).JSON(types.NewDataResponse(ext, reqID))
}
// UpdateExtension handles PUT /api/v1/pbx/extensions/:id
func (h *PBXHandler) UpdateExtension(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid extension ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.PBXExtensionUpdate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
ext, err := h.svc.UpdateExtension(c.Context(), id, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(ext, reqID))
}
// DeleteExtension handles DELETE /api/v1/pbx/extensions/:id
func (h *PBXHandler) DeleteExtension(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid extension ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteExtension(c.Context(), id); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"id": id, "deleted": true}, reqID))
}
// ============================================================================
// Inbound Routes
// ============================================================================
// ListInboundRoutes handles GET /api/v1/pbx/inbound-routes
func (h *PBXHandler) ListInboundRoutes(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
params := types.ListParams{
Limit: c.QueryInt("limit", 50),
Offset: c.QueryInt("offset", 0),
Search: c.Query("search"),
}
routes, total, err := h.svc.ListInboundRoutes(c.Context(), params)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(routes, total, params.Limit, params.Offset, reqID))
}
// GetInboundRoute handles GET /api/v1/pbx/inbound-routes/:id
func (h *PBXHandler) GetInboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid route ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
route, err := h.svc.GetInboundRoute(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("Inbound route not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(route, reqID))
}
// CreateInboundRoute handles POST /api/v1/pbx/inbound-routes
func (h *PBXHandler) CreateInboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.PBXInboundRouteCreate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if req.Name == "" || req.DestinationType == "" || req.TenantID == uuid.Nil {
apiErr := types.NewValidation("tenantId, name, and destinationType are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
route, err := h.svc.CreateInboundRoute(c.Context(), &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.Status(fiber.StatusCreated).JSON(types.NewDataResponse(route, reqID))
}
// UpdateInboundRoute handles PUT /api/v1/pbx/inbound-routes/:id
func (h *PBXHandler) UpdateInboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid route ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.PBXInboundRouteUpdate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
route, err := h.svc.UpdateInboundRoute(c.Context(), id, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(route, reqID))
}
// DeleteInboundRoute handles DELETE /api/v1/pbx/inbound-routes/:id
func (h *PBXHandler) DeleteInboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid route ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteInboundRoute(c.Context(), id); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"id": id, "deleted": true}, reqID))
}
// ============================================================================
// Outbound Routes
// ============================================================================
// ListOutboundRoutes handles GET /api/v1/pbx/outbound-routes
func (h *PBXHandler) ListOutboundRoutes(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
params := types.ListParams{
Limit: c.QueryInt("limit", 50),
Offset: c.QueryInt("offset", 0),
Search: c.Query("search"),
}
routes, total, err := h.svc.ListOutboundRoutes(c.Context(), params)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(routes, total, params.Limit, params.Offset, reqID))
}
// GetOutboundRoute handles GET /api/v1/pbx/outbound-routes/:id
func (h *PBXHandler) GetOutboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid route ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
route, err := h.svc.GetOutboundRoute(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("Outbound route not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(route, reqID))
}
// CreateOutboundRoute handles POST /api/v1/pbx/outbound-routes
func (h *PBXHandler) CreateOutboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.PBXOutboundRouteCreate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if req.Name == "" || len(req.DialPatterns) == 0 || req.TenantID == uuid.Nil {
apiErr := types.NewValidation("tenantId, name, and dialPatterns are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
route, err := h.svc.CreateOutboundRoute(c.Context(), &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.Status(fiber.StatusCreated).JSON(types.NewDataResponse(route, reqID))
}
// UpdateOutboundRoute handles PUT /api/v1/pbx/outbound-routes/:id
func (h *PBXHandler) UpdateOutboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid route ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.PBXOutboundRouteUpdate
if err := c.BodyParser(&req); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
route, err := h.svc.UpdateOutboundRoute(c.Context(), id, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(route, reqID))
}
// DeleteOutboundRoute handles DELETE /api/v1/pbx/outbound-routes/:id
func (h *PBXHandler) DeleteOutboundRoute(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid route ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteOutboundRoute(c.Context(), id); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"id": id, "deleted": true}, reqID))
}
// ============================================================================
// System Operations
// ============================================================================
// Status handles GET /api/v1/pbx/status
func (h *PBXHandler) Status(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
status, err := h.svc.GetStatus(c.Context())
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(status, reqID))
}
// Reload handles POST /api/v1/pbx/reload
func (h *PBXHandler) Reload(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
result, err := h.svc.Reload(c.Context())
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(result, reqID))
}