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>
This commit is contained in:
Claude (gsc-ops-api init)
2026-05-03 20:06:02 +02:00
commit 3847eb2036
68 changed files with 12982 additions and 0 deletions

330
internal/handler/carddav.go Normal file
View File

@@ -0,0 +1,330 @@
package handler
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// CardDAVHandler handles CardDAV endpoints
type CardDAVHandler struct {
svc *service.CardDAVService
}
// NewCardDAVHandler creates a new CardDAV handler
func NewCardDAVHandler(svc *service.CardDAVService) *CardDAVHandler {
return &CardDAVHandler{svc: svc}
}
// --- Principals ---
// ListPrincipals handles GET /api/v1/carddav/principals
func (h *CardDAVHandler) ListPrincipals(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
principals, err := h.svc.ListPrincipals(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(principals, reqID))
}
// GetPrincipal handles GET /api/v1/carddav/principals/:username
func (h *CardDAVHandler) GetPrincipal(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
username := c.Params("username")
principal, err := h.svc.GetPrincipal(c.Context(), username)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if principal == nil {
apiErr := types.NewNotFound("Principal not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(principal, reqID))
}
// CreatePrincipal handles POST /api/v1/carddav/principals
func (h *CardDAVHandler) CreatePrincipal(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.CardDAVPrincipalCreate
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.Username == "" {
apiErr := types.NewValidation("username is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
principal, err := h.svc.CreatePrincipal(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(principal, reqID))
}
// DeletePrincipal handles DELETE /api/v1/carddav/principals/:username
func (h *CardDAVHandler) DeletePrincipal(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
username := c.Params("username")
if err := h.svc.DeletePrincipal(c.Context(), username); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"username": username, "deleted": true}, reqID))
}
// --- Address Books ---
// ListAddressBooks handles GET /api/v1/carddav/addressbooks
func (h *CardDAVHandler) ListAddressBooks(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
principal := c.Query("principal")
books, err := h.svc.ListAddressBooks(c.Context(), principal)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(books, reqID))
}
// GetAddressBook handles GET /api/v1/carddav/addressbooks/:id
func (h *CardDAVHandler) GetAddressBook(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
book, err := h.svc.GetAddressBook(c.Context(), id)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if book == nil {
apiErr := types.NewNotFound("Address book not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(book, reqID))
}
// CreateAddressBook handles POST /api/v1/carddav/addressbooks
func (h *CardDAVHandler) CreateAddressBook(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.AddressBookCreate
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.PrincipalURI == "" || req.DisplayName == "" || req.URI == "" {
apiErr := types.NewValidation("principalUri, displayName, and uri are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
book, err := h.svc.CreateAddressBook(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(book, reqID))
}
// UpdateAddressBook handles PUT /api/v1/carddav/addressbooks/:id
func (h *CardDAVHandler) UpdateAddressBook(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.AddressBookUpdate
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))
}
book, err := h.svc.UpdateAddressBook(c.Context(), id, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if book == nil {
apiErr := types.NewNotFound("Address book not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(book, reqID))
}
// DeleteAddressBook handles DELETE /api/v1/carddav/addressbooks/:id
func (h *CardDAVHandler) DeleteAddressBook(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteAddressBook(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))
}
// --- Contacts ---
// ListContacts handles GET /api/v1/carddav/addressbooks/:id/contacts
func (h *CardDAVHandler) ListContacts(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
contacts, err := h.svc.ListContacts(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(contacts, reqID))
}
// GetContact handles GET /api/v1/carddav/addressbooks/:id/contacts/:uri
func (h *CardDAVHandler) GetContact(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
uri := c.Params("uri")
contact, err := h.svc.GetContact(c.Context(), id, uri)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if contact == nil {
apiErr := types.NewNotFound("Contact not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(contact, reqID))
}
// CreateContact handles POST /api/v1/carddav/addressbooks/:id/contacts
func (h *CardDAVHandler) CreateContact(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.ContactCreate
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.URI == "" || req.CardData == "" {
apiErr := types.NewValidation("uri and cardData are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
contact, err := h.svc.CreateContact(c.Context(), id, &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(contact, reqID))
}
// UpdateContact handles PUT /api/v1/carddav/addressbooks/:id/contacts/:uri
func (h *CardDAVHandler) UpdateContact(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
uri := c.Params("uri")
var req types.ContactUpdate
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.CardData == "" {
apiErr := types.NewValidation("cardData is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
contact, err := h.svc.UpdateContact(c.Context(), id, uri, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if contact == nil {
apiErr := types.NewNotFound("Contact not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(contact, reqID))
}
// DeleteContact handles DELETE /api/v1/carddav/addressbooks/:id/contacts/:uri
func (h *CardDAVHandler) DeleteContact(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid address book ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
uri := c.Params("uri")
if err := h.svc.DeleteContact(c.Context(), id, uri); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"addressbookId": id, "uri": uri, "deleted": true}, reqID))
}

140
internal/handler/certs.go Normal file
View File

@@ -0,0 +1,140 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// CertHandler handles certificate endpoints
type CertHandler struct {
svc *service.CertificateService
}
// NewCertHandler creates a new certificate handler
func NewCertHandler(svc *service.CertificateService) *CertHandler {
return &CertHandler{svc: svc}
}
// List handles GET /api/v1/certs
func (h *CertHandler) List(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
search := c.Query("search")
limit := c.QueryInt("limit", 50)
certs, err := h.svc.ListCertificates(search, limit)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(certs, reqID))
}
// Get handles GET /api/v1/certs/:serialNumber
func (h *CertHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
serialNumber := c.Params("serialNumber")
issuerDN := c.Query("issuerDn")
if issuerDN == "" {
apiErr := types.NewValidation("issuerDn query parameter is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
cert, err := h.svc.GetCertificate(serialNumber, issuerDN)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(cert, reqID))
}
// Request handles POST /api/v1/certs/request
func (h *CertHandler) Request(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.CertRequest
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.SubjectDN == "" || req.CAName == "" || req.CertProfileName == "" || req.EndEntityName == "" {
apiErr := types.NewValidation("subjectDn, caName, certProfileName, and endEntityName are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
cert, err := h.svc.RequestCertificate(&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(cert, reqID))
}
// Renew handles POST /api/v1/certs/:serialNumber/renew
func (h *CertHandler) Renew(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
serialNumber := c.Params("serialNumber")
// For renewal, we re-request with the same parameters
// The caller should provide the original cert's issuer DN
issuerDN := c.Query("issuerDn")
if issuerDN == "" {
apiErr := types.NewValidation("issuerDn query parameter is required for renewal")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
// Get the existing cert to extract parameters
existing, err := h.svc.GetCertificate(serialNumber, issuerDN)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
// Re-request with same subject
cert, err := h.svc.RequestCertificate(&types.CertRequest{
SubjectDN: existing.SubjectDN,
CAName: existing.CAName,
CertProfileName: "SERVER",
EndEntityName: existing.SubjectDN,
})
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(cert, reqID))
}
// Revoke handles POST /api/v1/certs/:serialNumber/revoke
func (h *CertHandler) Revoke(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
serialNumber := c.Params("serialNumber")
var req types.CertRevoke
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.IssuerDN == "" {
apiErr := types.NewValidation("issuerDn is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.RevokeCertificate(serialNumber, &req); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{
"serialNumber": serialNumber,
"revoked": true,
}, reqID))
}

View File

@@ -0,0 +1,126 @@
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"
)
// DBTenantHandler handles database tenant endpoints
type DBTenantHandler struct {
svc *service.DatabaseService
}
// NewDBTenantHandler creates a new DB tenant handler
func NewDBTenantHandler(svc *service.DatabaseService) *DBTenantHandler {
return &DBTenantHandler{svc: svc}
}
// List handles GET /api/v1/db/tenants
func (h *DBTenantHandler) List(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"),
}
tenants, total, err := h.svc.ListTenants(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(tenants, total, params.Limit, params.Offset, reqID))
}
// Get handles GET /api/v1/db/tenants/:id
func (h *DBTenantHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid tenant ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenant, err := h.svc.GetTenant(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("Tenant not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(tenant, reqID))
}
// Create handles POST /api/v1/db/tenants
func (h *DBTenantHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.TenantCreate
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.Code == "" || req.CustomerID == uuid.Nil {
apiErr := types.NewValidation("customerId, code, and name are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenant, err := h.svc.CreateTenant(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(tenant, reqID))
}
// Update handles PUT /api/v1/db/tenants/:id
func (h *DBTenantHandler) Update(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid tenant ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.TenantUpdate
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))
}
tenant, err := h.svc.UpdateTenant(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(tenant, reqID))
}
// Delete handles DELETE /api/v1/db/tenants/:id (soft delete)
func (h *DBTenantHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid tenant ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.SoftDeleteTenant(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))
}

View File

@@ -0,0 +1,126 @@
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"
)
// DBUserHandler handles database user endpoints
type DBUserHandler struct {
svc *service.DatabaseService
}
// NewDBUserHandler creates a new DB user handler
func NewDBUserHandler(svc *service.DatabaseService) *DBUserHandler {
return &DBUserHandler{svc: svc}
}
// List handles GET /api/v1/db/users
func (h *DBUserHandler) List(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"),
}
users, total, err := h.svc.ListUsers(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(users, total, params.Limit, params.Offset, reqID))
}
// Get handles GET /api/v1/db/users/:id
func (h *DBUserHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid user ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
user, err := h.svc.GetUser(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("User not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(user, reqID))
}
// Create handles POST /api/v1/db/users
func (h *DBUserHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.DBUserCreate
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.GscSID == "" {
apiErr := types.NewValidation("gscsid is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
user, err := h.svc.CreateUser(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(user, reqID))
}
// Update handles PUT /api/v1/db/users/:id
func (h *DBUserHandler) Update(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid user ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.DBUserUpdate
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))
}
user, err := h.svc.UpdateUser(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(user, reqID))
}
// Delete handles DELETE /api/v1/db/users/:id (deactivate)
func (h *DBUserHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid user ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeactivateUser(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, "deactivated": true}, reqID))
}

View File

@@ -0,0 +1,167 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// DNSRecordHandler handles DNS record endpoints
type DNSRecordHandler struct {
svc *service.DNSService
}
// NewDNSRecordHandler creates a new DNS record handler
func NewDNSRecordHandler(svc *service.DNSService) *DNSRecordHandler {
return &DNSRecordHandler{svc: svc}
}
// List handles GET /api/v1/dns/zones/:zoneId/records
func (h *DNSRecordHandler) List(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
records, err := h.svc.ListRecords(zoneID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(records, reqID))
}
// Create handles POST /api/v1/dns/zones/:zoneId/records
func (h *DNSRecordHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
var changes []types.DNSRecordChange
if err := c.BodyParser(&changes); err != nil {
// Try single change
var single types.DNSRecordChange
if err2 := c.BodyParser(&single); err2 != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
changes = []types.DNSRecordChange{single}
}
// Set changetype to REPLACE for creates
for i := range changes {
if changes[i].ChangeType == "" {
changes[i].ChangeType = "REPLACE"
}
}
if err := h.svc.ChangeRecords(zoneID, changes); 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(fiber.Map{
"zoneId": zoneID,
"changes": len(changes),
}, reqID))
}
// Replace handles PUT /api/v1/dns/zones/:zoneId/records
func (h *DNSRecordHandler) Replace(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
var changes []types.DNSRecordChange
if err := c.BodyParser(&changes); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
for i := range changes {
changes[i].ChangeType = "REPLACE"
}
if err := h.svc.ChangeRecords(zoneID, changes); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{
"zoneId": zoneID,
"replaced": len(changes),
}, reqID))
}
// Delete handles DELETE /api/v1/dns/zones/:zoneId/records
func (h *DNSRecordHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
var changes []types.DNSRecordChange
if err := c.BodyParser(&changes); err != nil {
apiErr := types.NewBadRequest("Invalid request body: " + err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
for i := range changes {
changes[i].ChangeType = "DELETE"
}
if err := h.svc.ChangeRecords(zoneID, changes); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{
"zoneId": zoneID,
"deleted": len(changes),
}, reqID))
}
// DomainSetup handles POST /api/v1/dns/domains/setup
func (h *DNSRecordHandler) DomainSetup(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.DomainSetup
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.Domain == "" {
apiErr := types.NewValidation("domain is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
zone, err := h.svc.SetupDomain(&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(zone, reqID))
}
// DomainVerify handles POST /api/v1/dns/domains/verify
func (h *DNSRecordHandler) DomainVerify(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.DomainVerify
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.Domain == "" {
apiErr := types.NewValidation("domain is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
result, err := h.svc.VerifyDomain(req.Domain)
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))
}

View File

@@ -0,0 +1,115 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// DNSZoneHandler handles DNS zone endpoints
type DNSZoneHandler struct {
svc *service.DNSService
}
// NewDNSZoneHandler creates a new DNS zone handler
func NewDNSZoneHandler(svc *service.DNSService) *DNSZoneHandler {
return &DNSZoneHandler{svc: svc}
}
// List handles GET /api/v1/dns/zones
func (h *DNSZoneHandler) List(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zones, err := h.svc.ListZones()
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(zones, reqID))
}
// Get handles GET /api/v1/dns/zones/:zoneId
func (h *DNSZoneHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
zone, err := h.svc.GetZone(zoneID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(zone, reqID))
}
// Create handles POST /api/v1/dns/zones
func (h *DNSZoneHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.DNSZoneCreate
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 == "" {
apiErr := types.NewValidation("name is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
zone, err := h.svc.CreateZone(&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(zone, reqID))
}
// Update handles PUT /api/v1/dns/zones/:zoneId
func (h *DNSZoneHandler) Update(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
var req types.DNSZoneUpdate
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 err := h.svc.UpdateZone(zoneID, &req); 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": zoneID, "updated": true}, reqID))
}
// Delete handles DELETE /api/v1/dns/zones/:zoneId
func (h *DNSZoneHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
if err := h.svc.DeleteZone(zoneID); 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": zoneID, "deleted": true}, reqID))
}
// Notify handles POST /api/v1/dns/zones/:zoneId/notify
func (h *DNSZoneHandler) Notify(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
zoneID := c.Params("zoneId")
if err := h.svc.NotifyZone(zoneID); 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": zoneID, "notified": true}, reqID))
}

View File

@@ -0,0 +1,94 @@
package handler
import (
"context"
"time"
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/client"
"github.com/gosec/gsc-ops-api/internal/database"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// HealthHandler handles health check endpoints
type HealthHandler struct {
db *database.DB
ldap *client.LDAPClient
pdns *client.PowerDNSClient
carddav *client.CardDAVClient
}
// NewHealthHandler creates a new health handler
func NewHealthHandler(db *database.DB, ldap *client.LDAPClient, pdns *client.PowerDNSClient, carddav *client.CardDAVClient) *HealthHandler {
return &HealthHandler{db: db, ldap: ldap, pdns: pdns, carddav: carddav}
}
// Liveness returns 200 if the server is running
func (h *HealthHandler) Liveness(c *fiber.Ctx) error {
return c.JSON(types.NewDataResponse(fiber.Map{
"status": "ok",
"time": time.Now().UTC(),
}, middleware.GetRequestID(c)))
}
// Readiness checks all backend dependencies
func (h *HealthHandler) Readiness(c *fiber.Ctx) error {
ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
defer cancel()
checks := make(map[string]string)
allOK := true
// Database
if err := h.db.Health(ctx); err != nil {
checks["database"] = "error: " + err.Error()
allOK = false
} else {
checks["database"] = "ok"
}
// LDAP
if h.ldap != nil {
if err := h.ldap.Health(); err != nil {
checks["ldap"] = "error: " + err.Error()
allOK = false
} else {
checks["ldap"] = "ok"
}
}
// PowerDNS
if h.pdns != nil {
if err := h.pdns.Health(); err != nil {
checks["powerdns"] = "error: " + err.Error()
allOK = false
} else {
checks["powerdns"] = "ok"
}
}
// CardDAV
if h.carddav != nil {
if err := h.carddav.Health(ctx); err != nil {
checks["carddav"] = "error: " + err.Error()
allOK = false
} else {
checks["carddav"] = "ok"
}
}
status := "ok"
httpStatus := fiber.StatusOK
if !allOK {
status = "degraded"
httpStatus = fiber.StatusServiceUnavailable
}
return c.Status(httpStatus).JSON(types.NewDataResponse(fiber.Map{
"status": status,
"checks": checks,
"time": time.Now().UTC(),
}, middleware.GetRequestID(c)))
}

View File

@@ -0,0 +1,178 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/schema"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// LDAPEntityHandler handles generic LDAP entity endpoints
type LDAPEntityHandler struct {
svc *service.LDAPEntityService
registry *schema.Registry
}
// NewLDAPEntityHandler creates a new entity handler
func NewLDAPEntityHandler(svc *service.LDAPEntityService, registry *schema.Registry) *LDAPEntityHandler {
return &LDAPEntityHandler{svc: svc, registry: registry}
}
// ListTypes handles GET /api/v1/ldap/entities — list available entity types
func (h *LDAPEntityHandler) ListTypes(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
allTypes := h.registry.AllEntityTypes()
result := make([]fiber.Map, 0, len(allTypes))
for name, et := range allTypes {
result = append(result, fiber.Map{
"name": name,
"description": et.Description,
"rdnAttribute": et.RDNAttribute,
"domain": et.Domain,
"requiredAttrs": et.RequiredAttrs,
})
}
return c.JSON(types.NewDataResponse(result, reqID))
}
// List handles GET /api/v1/ldap/entities/:type — list entities of a type
func (h *LDAPEntityHandler) List(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
typeName := c.Params("type")
search := c.Query("search")
limit := c.QueryInt("limit", 50)
if limit > 500 {
limit = 500
}
if h.registry.GetEntityType(typeName) == nil {
apiErr := types.NewBadRequest("Unknown entity type: " + typeName)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
entities, err := h.svc.ListEntities(typeName, search, limit)
if err != nil {
apiErr := classifyAPIError(err)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(entities, int64(len(entities)), limit, 0, reqID))
}
// Get handles GET /api/v1/ldap/entities/:type/:rdn — get a single entity
func (h *LDAPEntityHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
typeName := c.Params("type")
rdn := c.Params("rdn")
if h.registry.GetEntityType(typeName) == nil {
apiErr := types.NewBadRequest("Unknown entity type: " + typeName)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
entity, err := h.svc.GetEntity(typeName, rdn)
if err != nil {
apiErr := classifyAPIError(err)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if entity == nil {
apiErr := types.NewNotFound("Entity not found: " + rdn)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(entity, reqID))
}
// Create handles POST /api/v1/ldap/entities/:type — create an entity
func (h *LDAPEntityHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
typeName := c.Params("type")
if h.registry.GetEntityType(typeName) == nil {
apiErr := types.NewBadRequest("Unknown entity type: " + typeName)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.LDAPEntityCreate
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 len(req.Attributes) == 0 {
apiErr := types.NewValidation("attributes are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
entity, err := h.svc.CreateEntity(typeName, &req)
if err != nil {
apiErr := classifyAPIError(err)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.Status(fiber.StatusCreated).JSON(types.NewDataResponse(entity, reqID))
}
// Update handles PUT /api/v1/ldap/entities/:type/:rdn — update an entity
func (h *LDAPEntityHandler) Update(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
typeName := c.Params("type")
rdn := c.Params("rdn")
if h.registry.GetEntityType(typeName) == nil {
apiErr := types.NewBadRequest("Unknown entity type: " + typeName)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.LDAPEntityUpdate
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))
}
entity, err := h.svc.UpdateEntity(typeName, rdn, &req)
if err != nil {
apiErr := classifyAPIError(err)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(entity, reqID))
}
// Delete handles DELETE /api/v1/ldap/entities/:type/:rdn — delete an entity
func (h *LDAPEntityHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
typeName := c.Params("type")
rdn := c.Params("rdn")
if h.registry.GetEntityType(typeName) == nil {
apiErr := types.NewBadRequest("Unknown entity type: " + typeName)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteEntity(typeName, rdn); err != nil {
apiErr := classifyAPIError(err)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"type": typeName, "rdn": rdn, "deleted": true}, reqID))
}
// classifyAPIError maps service errors to appropriate HTTP error responses
func classifyAPIError(err error) *types.APIError {
kind, msg := service.ClassifyError(err)
switch kind {
case "conflict":
return types.NewConflict(msg)
case "not_found":
return types.NewNotFound(msg)
case "validation":
return types.NewValidation(msg)
default:
return types.NewInternal(msg)
}
}

View File

@@ -0,0 +1,168 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// LDAPGroupHandler handles LDAP group endpoints
type LDAPGroupHandler struct {
svc *service.LDAPService
}
// NewLDAPGroupHandler creates a new LDAP group handler
func NewLDAPGroupHandler(svc *service.LDAPService) *LDAPGroupHandler {
return &LDAPGroupHandler{svc: svc}
}
// List handles GET /api/v1/ldap/groups
func (h *LDAPGroupHandler) List(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
search := c.Query("search")
limit := c.QueryInt("limit", 50)
if limit > 500 {
limit = 500
}
groups, err := h.svc.ListGroups(search, limit)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(groups, int64(len(groups)), limit, 0, reqID))
}
// Get handles GET /api/v1/ldap/groups/:cn
func (h *LDAPGroupHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
cn := c.Params("cn")
group, err := h.svc.GetGroup(cn)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if group == nil {
apiErr := types.NewNotFound("Group not found: " + cn)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(group, reqID))
}
// Create handles POST /api/v1/ldap/groups
func (h *LDAPGroupHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.LDAPGroupCreate
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.CN == "" {
apiErr := types.NewValidation("cn is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
group, err := h.svc.CreateGroup(&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(group, reqID))
}
// Update handles PUT /api/v1/ldap/groups/:cn
func (h *LDAPGroupHandler) Update(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
cn := c.Params("cn")
var req types.LDAPGroupUpdate
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))
}
group, err := h.svc.UpdateGroup(cn, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(group, reqID))
}
// Delete handles DELETE /api/v1/ldap/groups/:cn
func (h *LDAPGroupHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
cn := c.Params("cn")
if err := h.svc.DeleteGroup(cn); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"cn": cn, "deleted": true}, reqID))
}
// ListMembers handles GET /api/v1/ldap/groups/:cn/members
func (h *LDAPGroupHandler) ListMembers(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
cn := c.Params("cn")
members, err := h.svc.GetGroupMembers(cn)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if members == nil {
apiErr := types.NewNotFound("Group not found: " + cn)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(members, reqID))
}
// AddMembers handles POST /api/v1/ldap/groups/:cn/members
func (h *LDAPGroupHandler) AddMembers(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
cn := c.Params("cn")
var req types.LDAPGroupMemberAdd
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 len(req.Members) == 0 {
apiErr := types.NewValidation("members array is required and must not be empty")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.AddGroupMembers(cn, req.Members); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"cn": cn, "added": req.Members}, reqID))
}
// RemoveMember handles DELETE /api/v1/ldap/groups/:cn/members/:uid
func (h *LDAPGroupHandler) RemoveMember(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
cn := c.Params("cn")
uid := c.Params("uid")
if err := h.svc.RemoveGroupMember(cn, uid); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"cn": cn, "removed": uid}, reqID))
}

View File

@@ -0,0 +1,215 @@
package handler
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// LDAPUserHandler handles LDAP user endpoints
type LDAPUserHandler struct {
svc *service.LDAPService
}
// NewLDAPUserHandler creates a new LDAP user handler
func NewLDAPUserHandler(svc *service.LDAPService) *LDAPUserHandler {
return &LDAPUserHandler{svc: svc}
}
// List handles GET /api/v1/ldap/users
//
// Query parameters:
// - search: free-text search across uid, givenName, sn, mail
// - services: comma-separated service domains (mail,calendar)
// - attr.<ldapAttr>: filter by any LDAP attribute (e.g. attr.gscTenantId=abc123)
// - limit: max results (default 50, max 500)
func (h *LDAPUserHandler) List(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
search := c.Query("search")
limit := c.QueryInt("limit", 50)
if limit > 500 {
limit = 500
}
// Parse services filter: ?services=mail,calendar
var serviceFilters []string
if svcParam := c.Query("services"); svcParam != "" {
serviceFilters = strings.Split(svcParam, ",")
}
// Parse dynamic attribute filters: ?attr.gscTenantId=abc&attr.mail=*@example.com
attrFilters := make(map[string]string)
c.Context().QueryArgs().VisitAll(func(key, value []byte) {
k := string(key)
if strings.HasPrefix(k, "attr.") && len(k) > 5 {
attrName := k[5:]
attrFilters[attrName] = string(value)
}
})
users, err := h.svc.ListUsers(search, limit, serviceFilters, attrFilters)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(users, int64(len(users)), limit, 0, reqID))
}
// Get handles GET /api/v1/ldap/users/:uid
func (h *LDAPUserHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
user, err := h.svc.GetUser(uid)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if user == nil {
apiErr := types.NewNotFound("User not found: " + uid)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(user, reqID))
}
// Create handles POST /api/v1/ldap/users
func (h *LDAPUserHandler) Create(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.LDAPUserCreate
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.UID == "" || req.FirstName == "" || req.LastName == "" {
apiErr := types.NewValidation("uid, firstName, and lastName are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
user, err := h.svc.CreateUser(&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(user, reqID))
}
// Update handles PUT /api/v1/ldap/users/:uid
func (h *LDAPUserHandler) Update(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
var req types.LDAPUserUpdate
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))
}
user, err := h.svc.UpdateUser(uid, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(user, reqID))
}
// Delete handles DELETE /api/v1/ldap/users/:uid (disables the user)
func (h *LDAPUserHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
if err := h.svc.DisableUser(uid); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"uid": uid, "disabled": true}, reqID))
}
// ResetPassword handles POST /api/v1/ldap/users/:uid/password
func (h *LDAPUserHandler) ResetPassword(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
var req types.PasswordReset
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.NewPassword == "" {
apiErr := types.NewValidation("newPassword is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.ResetPassword(uid, req.NewPassword); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{"uid": uid, "passwordReset": true}, reqID))
}
// ListGroups handles GET /api/v1/ldap/users/:uid/groups
func (h *LDAPUserHandler) ListGroups(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
groups, err := h.svc.GetUserGroups(uid)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if groups == nil {
apiErr := types.NewNotFound("User not found: " + uid)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(groups, reqID))
}
// ListServices handles GET /api/v1/ldap/users/:uid/services
func (h *LDAPUserHandler) ListServices(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
services, err := h.svc.GetUserServices(uid, "")
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if services == nil {
apiErr := types.NewNotFound("User not found: " + uid)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(services, reqID))
}
// GetService handles GET /api/v1/ldap/users/:uid/services/:domain
func (h *LDAPUserHandler) GetService(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
uid := c.Params("uid")
domain := c.Params("domain")
services, err := h.svc.GetUserServices(uid, domain)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if services == nil {
apiErr := types.NewNotFound("User not found: " + uid)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(services, reqID))
}

603
internal/handler/pbx.go Normal file
View File

@@ -0,0 +1,603 @@
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))
}

259
internal/handler/persona.go Normal file
View File

@@ -0,0 +1,259 @@
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"
)
// PersonaHandler handles persona management endpoints
type PersonaHandler struct {
svc *service.PersonaService
}
// NewPersonaHandler creates a new persona handler
func NewPersonaHandler(svc *service.PersonaService) *PersonaHandler {
return &PersonaHandler{svc: svc}
}
// parseTenantID extracts and validates tenantId from query or body
func parseTenantID(c *fiber.Ctx) (uuid.UUID, *types.APIError) {
tid := c.Query("tenantId")
if tid == "" {
return uuid.Nil, types.NewBadRequest("tenantId query parameter is required")
}
parsed, err := uuid.Parse(tid)
if err != nil {
return uuid.Nil, types.NewBadRequest("Invalid tenantId")
}
return parsed, nil
}
// ListPersonas handles GET /api/v1/personas
func (h *PersonaHandler) ListPersonas(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
params := types.ListParams{
Limit: c.QueryInt("limit", 50),
Offset: c.QueryInt("offset", 0),
Status: c.Query("status"),
}
personas, total, err := h.svc.ListPersonas(c.Context(), tenantID, params)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(personas, total, params.Limit, params.Offset, reqID))
}
// GetPersona handles GET /api/v1/personas/:id
func (h *PersonaHandler) GetPersona(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid persona ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
persona, err := h.svc.GetPersona(c.Context(), id, tenantID)
if err != nil {
apiErr := types.NewNotFound("Persona not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(persona, reqID))
}
// CreatePersona handles POST /api/v1/personas
func (h *PersonaHandler) CreatePersona(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.PersonaCreate
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.TenantID == uuid.Nil {
apiErr := types.NewValidation("tenantId is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if req.Name == "" {
apiErr := types.NewValidation("name is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
persona, err := h.svc.CreatePersona(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(persona, reqID))
}
// UpdatePersona handles PUT /api/v1/personas/:id
func (h *PersonaHandler) UpdatePersona(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid persona ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.PersonaUpdate
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))
}
persona, err := h.svc.UpdatePersona(c.Context(), id, tenantID, &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(persona, reqID))
}
// DeletePersona handles DELETE /api/v1/personas/:id
func (h *PersonaHandler) DeletePersona(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid persona ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeletePersona(c.Context(), id, tenantID); err != nil {
apiErr := types.NewNotFound("Persona not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.SendStatus(fiber.StatusNoContent)
}
// GetSelfModel handles GET /api/v1/personas/:id/self-model
func (h *PersonaHandler) GetSelfModel(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid persona ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
snapshot, err := h.svc.GetSelfModel(c.Context(), id, tenantID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(snapshot, reqID))
}
// GetExperiences handles GET /api/v1/personas/:id/experiences
func (h *PersonaHandler) GetExperiences(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid persona ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
limit := c.QueryInt("limit", 20)
experiences, err := h.svc.SearchExperiences(c.Context(), id, tenantID, limit)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(experiences, reqID))
}
// GetEvaluations handles GET /api/v1/personas/:id/evaluations/:sessionId
func (h *PersonaHandler) GetEvaluations(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
sessionID, err := uuid.Parse(c.Params("sessionId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid session ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
limit := c.QueryInt("limit", 10)
evaluations, err := h.svc.GetEvaluations(c.Context(), sessionID, limit)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(evaluations, reqID))
}
// GetMoralPattern handles GET /api/v1/personas/:id/moral-pattern/:sessionId
func (h *PersonaHandler) GetMoralPattern(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
sessionID, err := uuid.Parse(c.Params("sessionId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid session ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, apiErr := parseTenantID(c)
if apiErr != nil {
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
assessments, err := h.svc.GetMoralPattern(c.Context(), sessionID, tenantID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(map[string]interface{}{
"assessments": assessments,
}, reqID))
}

View File

@@ -0,0 +1,98 @@
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"
)
// PersonalAgentHandler handles personal agent config endpoints
type PersonalAgentHandler struct {
svc *service.PersonalAgentService
}
// NewPersonalAgentHandler creates a new personal agent handler
func NewPersonalAgentHandler(svc *service.PersonalAgentService) *PersonalAgentHandler {
return &PersonalAgentHandler{svc: svc}
}
// GetMyConfig handles GET /api/v1/agents/me?userId=X&tenantId=Y
func (h *PersonalAgentHandler) GetMyConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
userID, err := uuid.Parse(c.Query("userId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid or missing userId")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, err := uuid.Parse(c.Query("tenantId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid or missing tenantId")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
config, err := h.svc.GetConfig(c.Context(), userID, tenantID)
if err != nil {
apiErr := types.NewNotFound("Personal agent config not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(config, reqID))
}
// UpsertMyConfig handles PUT /api/v1/agents/me
func (h *PersonalAgentHandler) UpsertMyConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.UserAgentConfigUpsert
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.UserID == uuid.Nil || req.TenantID == uuid.Nil {
apiErr := types.NewValidation("userId and tenantId are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if len(req.Config) == 0 {
apiErr := types.NewValidation("config is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
config, err := h.svc.UpsertConfig(c.Context(), &req)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(config, reqID))
}
// DeleteMyConfig handles DELETE /api/v1/agents/me?userId=X&tenantId=Y
func (h *PersonalAgentHandler) DeleteMyConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
userID, err := uuid.Parse(c.Query("userId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid or missing userId")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID, err := uuid.Parse(c.Query("tenantId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid or missing tenantId")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteConfig(c.Context(), userID, tenantID); err != nil {
apiErr := types.NewNotFound("Personal agent config not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.SendStatus(fiber.StatusNoContent)
}

100
internal/handler/pgp.go Normal file
View File

@@ -0,0 +1,100 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/gosec/gsc-ops-api/internal/middleware"
"github.com/gosec/gsc-ops-api/internal/service"
"github.com/gosec/gsc-ops-api/pkg/types"
)
// PGPHandler handles PGP key endpoints
type PGPHandler struct {
svc *service.PGPService
}
// NewPGPHandler creates a new PGP handler
func NewPGPHandler(svc *service.PGPService) *PGPHandler {
return &PGPHandler{svc: svc}
}
// Search handles GET /api/v1/pgp/keys
func (h *PGPHandler) Search(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
query := c.Query("search")
if query == "" {
query = c.Query("q")
}
if query == "" {
apiErr := types.NewValidation("search or q query parameter is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
keys, err := h.svc.SearchKeys(query)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(keys, reqID))
}
// Get handles GET /api/v1/pgp/keys/:keyId
func (h *PGPHandler) Get(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
keyID := c.Params("keyId")
key, err := h.svc.GetKey(keyID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if key == nil {
apiErr := types.NewNotFound("PGP key not found: " + keyID)
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(key, reqID))
}
// Upload handles POST /api/v1/pgp/keys
func (h *PGPHandler) Upload(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.PGPKeyUpload
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.KeyText == "" {
apiErr := types.NewValidation("keyText is required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.UploadKey(req.KeyText); 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(fiber.Map{
"uploaded": true,
}, reqID))
}
// Delete handles DELETE /api/v1/pgp/keys/:keyId
func (h *PGPHandler) Delete(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
keyID := c.Params("keyId")
if err := h.svc.DeleteKey(keyID); err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(fiber.Map{
"keyId": keyID,
"deleted": true,
}, reqID))
}

View File

@@ -0,0 +1,186 @@
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"
)
// VoiceAgentHandler handles voice agent management endpoints
type VoiceAgentHandler struct {
svc *service.VoiceAgentService
}
// NewVoiceAgentHandler creates a new voice agent handler
func NewVoiceAgentHandler(svc *service.VoiceAgentService) *VoiceAgentHandler {
return &VoiceAgentHandler{svc: svc}
}
// ============================================================================
// Voice Agent Configs
// ============================================================================
// ListConfigs handles GET /api/v1/voice-agents
func (h *VoiceAgentHandler) ListConfigs(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"),
}
var tenantID *uuid.UUID
if tid := c.Query("tenantId"); tid != "" {
parsed, err := uuid.Parse(tid)
if err != nil {
apiErr := types.NewBadRequest("Invalid tenantId")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
tenantID = &parsed
}
configs, total, err := h.svc.ListConfigs(c.Context(), params, tenantID)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(configs, total, params.Limit, params.Offset, reqID))
}
// GetConfig handles GET /api/v1/voice-agents/:id
func (h *VoiceAgentHandler) GetConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid voice agent config ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
config, err := h.svc.GetConfig(c.Context(), id)
if err != nil {
apiErr := types.NewNotFound("Voice agent config not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(config, reqID))
}
// CreateConfig handles POST /api/v1/voice-agents
func (h *VoiceAgentHandler) CreateConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
var req types.VoiceAgentConfigCreate
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.TenantID == uuid.Nil || req.AgentID == uuid.Nil {
apiErr := types.NewValidation("tenantId and agentId are required")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
config, err := h.svc.CreateConfig(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(config, reqID))
}
// UpdateConfig handles PUT /api/v1/voice-agents/:id
func (h *VoiceAgentHandler) UpdateConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid voice agent config ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
var req types.VoiceAgentConfigUpdate
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))
}
config, err := h.svc.UpdateConfig(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(config, reqID))
}
// DeleteConfig handles DELETE /api/v1/voice-agents/:id
func (h *VoiceAgentHandler) DeleteConfig(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
id, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid voice agent config ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
if err := h.svc.DeleteConfig(c.Context(), id); err != nil {
apiErr := types.NewNotFound("Voice agent config not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.SendStatus(fiber.StatusNoContent)
}
// ============================================================================
// Voice Sessions
// ============================================================================
// ListSessions handles GET /api/v1/voice-agents/:id/sessions
func (h *VoiceAgentHandler) ListSessions(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
agentID, err := uuid.Parse(c.Params("id"))
if err != nil {
apiErr := types.NewBadRequest("Invalid voice agent config ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
params := types.ListParams{
Limit: c.QueryInt("limit", 50),
Offset: c.QueryInt("offset", 0),
}
sessions, total, err := h.svc.ListSessions(c.Context(), agentID, params)
if err != nil {
apiErr := types.NewInternal(err.Error())
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewPagedResponse(sessions, total, params.Limit, params.Offset, reqID))
}
// GetSession handles GET /api/v1/voice-agents/sessions/:sessionId
func (h *VoiceAgentHandler) GetSession(c *fiber.Ctx) error {
reqID := middleware.GetRequestID(c)
sessionID, err := uuid.Parse(c.Params("sessionId"))
if err != nil {
apiErr := types.NewBadRequest("Invalid session ID")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
session, err := h.svc.GetSession(c.Context(), sessionID)
if err != nil {
apiErr := types.NewNotFound("Voice session not found")
return c.Status(apiErr.Status).JSON(types.NewErrorResponse(apiErr, reqID))
}
return c.JSON(types.NewDataResponse(session, reqID))
}