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:
330
internal/handler/carddav.go
Normal file
330
internal/handler/carddav.go
Normal 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
140
internal/handler/certs.go
Normal 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))
|
||||
}
|
||||
126
internal/handler/db_tenants.go
Normal file
126
internal/handler/db_tenants.go
Normal 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))
|
||||
}
|
||||
126
internal/handler/db_users.go
Normal file
126
internal/handler/db_users.go
Normal 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))
|
||||
}
|
||||
167
internal/handler/dns_records.go
Normal file
167
internal/handler/dns_records.go
Normal 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))
|
||||
}
|
||||
115
internal/handler/dns_zones.go
Normal file
115
internal/handler/dns_zones.go
Normal 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))
|
||||
}
|
||||
94
internal/handler/health.go
Normal file
94
internal/handler/health.go
Normal 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)))
|
||||
}
|
||||
178
internal/handler/ldap_entities.go
Normal file
178
internal/handler/ldap_entities.go
Normal 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)
|
||||
}
|
||||
}
|
||||
168
internal/handler/ldap_groups.go
Normal file
168
internal/handler/ldap_groups.go
Normal 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))
|
||||
}
|
||||
215
internal/handler/ldap_users.go
Normal file
215
internal/handler/ldap_users.go
Normal 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
603
internal/handler/pbx.go
Normal 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
259
internal/handler/persona.go
Normal 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))
|
||||
}
|
||||
98
internal/handler/personal_agent.go
Normal file
98
internal/handler/personal_agent.go
Normal 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
100
internal/handler/pgp.go
Normal 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))
|
||||
}
|
||||
186
internal/handler/voice_agent.go
Normal file
186
internal/handler/voice_agent.go
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user