feat: add service catalog management with policy integration for domain-based resource access control

Add ServiceCatalogItem type and services CRUD API endpoints (list, create, update, delete). Extend Policy type to include services array with domain, upstream_ip, proxy_ip, and ports metadata.

Add ServicesPage component with table view and create/edit modals for managing service definitions. Include service name, domain, proxy, and upstream columns with port parsing logic.

Integrate service selection
This commit is contained in:
2026-03-18 13:09:54 +01:00
parent 0ac93dfeb6
commit 6cf49ff3e0
25 changed files with 1375 additions and 99 deletions

View File

@@ -0,0 +1,152 @@
package servicecatalog
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type Repository interface {
List(ctx context.Context) ([]Service, error)
Create(ctx context.Context, input CreateRequest) (Service, error)
Update(ctx context.Context, serviceID uuid.UUID, input UpdateRequest) (Service, error)
Delete(ctx context.Context, serviceID uuid.UUID) error
ByIDs(ctx context.Context, ids []uuid.UUID) ([]Service, error)
}
type PGRepository struct {
db *pgxpool.Pool
}
func NewPGRepository(db *pgxpool.Pool) *PGRepository {
return &PGRepository{db: db}
}
func (r *PGRepository) List(ctx context.Context) ([]Service, error) {
rows, err := r.db.Query(ctx, `
select id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active
from services
where deleted_at is null
order by name asc
`)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Service
for rows.Next() {
var item Service
if err := rows.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (r *PGRepository) Create(ctx context.Context, input CreateRequest) (Service, error) {
row := r.db.QueryRow(ctx, `
insert into services (id, name, description, domain, upstream_ip, proxy_ip, ports, is_active)
values ($1, $2, $3, $4, $5::inet, $6::inet, $7::integer[], coalesce($8, true))
returning id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active
`, uuid.New(), input.Name, input.Description, input.Domain, input.UpstreamIP, input.ProxyIP, normalizePorts(input.Ports), input.IsActive)
var item Service
err := row.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive)
return item, err
}
func (r *PGRepository) Update(ctx context.Context, serviceID uuid.UUID, input UpdateRequest) (Service, error) {
var ports *[]int
if input.Ports != nil {
normalized := normalizePorts(input.Ports)
ports = &normalized
}
row := r.db.QueryRow(ctx, `
update services
set
name = coalesce($2, name),
description = coalesce($3, description),
domain = coalesce($4, domain),
upstream_ip = coalesce($5::inet, upstream_ip),
proxy_ip = coalesce($6::inet, proxy_ip),
ports = coalesce($7::integer[], ports),
is_active = coalesce($8, is_active),
updated_at = now()
where id = $1 and deleted_at is null
returning id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active
`, serviceID, input.Name, input.Description, input.Domain, input.UpstreamIP, input.ProxyIP, ports, input.IsActive)
var item Service
err := row.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive)
return item, err
}
func (r *PGRepository) Delete(ctx context.Context, serviceID uuid.UUID) error {
_, err := r.db.Exec(ctx, `
update services
set deleted_at = now(), updated_at = now()
where id = $1 and deleted_at is null
`, serviceID)
return err
}
func (r *PGRepository) ByIDs(ctx context.Context, ids []uuid.UUID) ([]Service, error) {
if len(ids) == 0 {
return nil, nil
}
rows, err := r.db.Query(ctx, `
select id, name, description, domain, host(upstream_ip), host(proxy_ip), ports, is_active
from services
where deleted_at is null and id = any($1::uuid[])
order by name asc
`, ids)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Service
for rows.Next() {
var item Service
if err := rows.Scan(&item.ID, &item.Name, &item.Description, &item.Domain, &item.UpstreamIP, &item.ProxyIP, &item.Ports, &item.IsActive); err != nil {
return nil, err
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(items) == 0 {
return nil, errors.New("services not found")
}
return items, nil
}
func normalizePorts(ports []int) []int {
if len(ports) == 0 {
return []int{80, 443}
}
seen := make(map[int]struct{}, len(ports))
result := make([]int, 0, len(ports))
for _, port := range ports {
if port <= 0 {
continue
}
if _, ok := seen[port]; ok {
continue
}
seen[port] = struct{}{}
result = append(result, port)
}
if len(result) == 0 {
return []int{80, 443}
}
return result
}