Files
EvoBGP/internal/repository/postgres_entities.go
T
Denozordec db79820df0
CI / changes (push) Successful in 10s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 28s
CI / web (push) Successful in 57s
CI / go (push) Successful in 1m9s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 3m57s
feat(auth): introduce demo token support and enhance API token handling
Added a local demo token for development purposes and improved the API token management by normalizing input tokens. Updated the authentication flow to utilize the new token handling, allowing for better session management and user experience. Enhanced the settings component to support the demo token and provide clear instructions for its use in local development.
2026-07-06 22:55:30 +07:00

647 lines
18 KiB
Go

package repository
import (
"context"
"encoding/json"
"errors"
"strings"
"time"
"evobgp/internal/store"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text, last_refreshed_at
FROM module_cdn_source WHERE module_id=$1 ORDER BY url`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.CDNSource
for rows.Next() {
var s store.CDNSource
s.ModuleID = moduleID
var ri *int32
var comm *string
var last *time.Time
if err := rows.Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm, &last); err != nil {
continue
}
if ri != nil {
v := int(*ri)
s.RefreshIntervalSec = &v
}
s.CommunityID = strOrNil(comm)
if last != nil {
t := last.UTC()
s.LastRefreshedAt = &t
}
out = append(out, &s)
}
return out, nil
}
func (p *Postgres) CreateCDNSource(tenantID, moduleID string, in *store.CDNSource) (*store.CDNSource, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "CDN_CIDRS" {
return nil, store.ErrInvalidInput
}
if in == nil || strings.TrimSpace(in.URL) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_cdn_source (id, module_id, source_kind, url, prefix_path, etag, refresh_interval_sec, community_id, last_refreshed_at)
VALUES ($1,$2,$3,$4,$5,$6,$7, NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), $9)`,
id, moduleID, in.SourceKind, strings.TrimSpace(in.URL), strings.TrimSpace(in.PrefixPath), in.Etag, nullInt32Ptr(in.RefreshIntervalSec), uuidOrNilPtr(in.CommunityID), nullTimePtr(in.LastRefreshedAt))
if err != nil {
return nil, err
}
return p.getCDNSource(ctx, moduleID, id)
}
func (p *Postgres) getCDNSource(ctx context.Context, moduleID, id string) (*store.CDNSource, error) {
var s store.CDNSource
s.ModuleID = moduleID
var ri *int32
var comm *string
var last *time.Time
err := p.pool.QueryRow(ctx, `
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text, last_refreshed_at
FROM module_cdn_source WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm, &last)
if err != nil {
return nil, err
}
if ri != nil {
v := int(*ri)
s.RefreshIntervalSec = &v
}
s.CommunityID = strOrNil(comm)
if last != nil {
t := last.UTC()
s.LastRefreshedAt = &t
}
return &s, nil
}
func uuidOrNilPtr(s *string) any {
if s == nil || strings.TrimSpace(*s) == "" {
return nil
}
return strings.TrimSpace(*s)
}
func (p *Postgres) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *store.CDNSourcePatch) (*store.CDNSource, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getCDNSource(context.Background(), moduleID, sourceID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.SourceKind != nil {
cur.SourceKind = *patch.SourceKind
}
if patch.URL != nil {
cur.URL = strings.TrimSpace(*patch.URL)
}
if patch.PrefixPath != nil {
cur.PrefixPath = strings.TrimSpace(*patch.PrefixPath)
}
if patch.Etag != nil {
cur.Etag = *patch.Etag
}
if patch.RefreshIntervalSec != nil {
cur.RefreshIntervalSec = patch.RefreshIntervalSec
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
if patch.LastRefreshedAt != nil {
t := patch.LastRefreshedAt.UTC()
cur.LastRefreshedAt = &t
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_cdn_source SET source_kind=$3, url=$4, prefix_path=$5, etag=$6, refresh_interval_sec=$7,
community_id=NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), last_refreshed_at=$9, updated_at=now()
WHERE id=$1 AND module_id=$2`,
sourceID, moduleID, cur.SourceKind, cur.URL, cur.PrefixPath, cur.Etag, nullInt32Ptr(cur.RefreshIntervalSec), uuidOrNilPtr(cur.CommunityID), nullTimePtr(cur.LastRefreshedAt))
if err != nil {
return nil, err
}
return p.getCDNSource(ctx, moduleID, sourceID)
}
func (p *Postgres) DeleteCDNSource(tenantID, moduleID, sourceID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_cdn_source WHERE id=$1 AND module_id=$2`, sourceID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListASEntries(tenantID, moduleID string) ([]*store.ASEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `
SELECT id::text, asn, community_id::text, asn_name, prefix_count, asn_resolved_at
FROM module_as_entry WHERE module_id=$1 ORDER BY asn`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.ASEntry
for rows.Next() {
var e store.ASEntry
e.ModuleID = moduleID
var comm, asnName *string
var pc *int64
var at *time.Time
if err := rows.Scan(&e.ID, &e.ASN, &comm, &asnName, &pc, &at); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
if asnName != nil {
e.ASNName = *asnName
}
e.PrefixCount = pc
e.ASNResolvedAt = at
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateASEntry(tenantID, moduleID string, in *store.ASEntry) (*store.ASEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "AS_PREFIXES" {
return nil, store.ErrInvalidInput
}
if in == nil || !store.ValidASN(in.ASN) {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_as_entry (id, module_id, asn, community_id)
VALUES ($1,$2,$3,NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, in.ASN, uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getASEntry(ctx, moduleID, id)
}
func (p *Postgres) getASEntry(ctx context.Context, moduleID, id string) (*store.ASEntry, error) {
var e store.ASEntry
e.ModuleID = moduleID
var comm, asnName *string
var pc *int64
var at *time.Time
err := p.pool.QueryRow(ctx, `
SELECT id::text, asn, community_id::text, asn_name, prefix_count, asn_resolved_at
FROM module_as_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(
&e.ID, &e.ASN, &comm, &asnName, &pc, &at)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
if asnName != nil {
e.ASNName = *asnName
}
e.PrefixCount = pc
e.ASNResolvedAt = at
return &e, nil
}
func (p *Postgres) UpdateASEntry(tenantID, moduleID, entryID string, patch *store.ASEntryPatch) (*store.ASEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getASEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
prevASN := cur.ASN
if patch.ASN != nil {
cur.ASN = *patch.ASN
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
if !store.ValidASN(cur.ASN) {
return nil, store.ErrInvalidInput
}
clearResolve := patch.ASN != nil && cur.ASN != prevASN
ctx := context.Background()
if clearResolve {
_, err = p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid),
asn_name=NULL, prefix_count=NULL, asn_resolved_at=NULL, updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
} else {
_, err = p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.ASN, uuidOrNilPtr(cur.CommunityID))
}
if err != nil {
return nil, err
}
return p.getASEntry(ctx, moduleID, entryID)
}
func (p *Postgres) UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
var nameArg any
sn := strings.TrimSpace(asnName)
if sn == "" {
nameArg = nil
} else {
nameArg = sn
}
tag, err := p.pool.Exec(ctx, `
UPDATE module_as_entry SET asn_name=$3, prefix_count=$4, asn_resolved_at=$5, updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, nameArg, prefixCount, resolvedAt.UTC())
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) UpdateASEntryResolveMetaBatch(tenantID, moduleID string, updates []store.ASEntryResolveMetaUpdate, resolvedAt time.Time) error {
if len(updates) == 0 {
return nil
}
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
batch := &pgx.Batch{}
for _, u := range updates {
var nameArg any
sn := strings.TrimSpace(u.ASNName)
if sn == "" {
nameArg = nil
} else {
nameArg = sn
}
batch.Queue(`
UPDATE module_as_entry SET asn_name=$3, prefix_count=$4, asn_resolved_at=$5, updated_at=now()
WHERE id=$1 AND module_id=$2`,
u.EntryID, moduleID, nameArg, u.PrefixCount, resolvedAt.UTC())
}
br := p.pool.SendBatch(ctx, batch)
defer func() { _ = br.Close() }()
for range updates {
if _, err := br.Exec(); err != nil {
return err
}
}
return nil
}
func (p *Postgres) DeleteASEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_as_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListDomainEntries(tenantID, moduleID string) ([]*store.DomainEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE module_id=$1`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.DomainEntry
for rows.Next() {
var e store.DomainEntry
e.ModuleID = moduleID
var comm *string
if err := rows.Scan(&e.ID, &e.FQDN, &comm); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateDomainEntry(tenantID, moduleID string, in *store.DomainEntry) (*store.DomainEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "DOMAINS" || in == nil || strings.TrimSpace(in.FQDN) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_domain_entry (id, module_id, fqdn, community_id)
VALUES ($1,$2,$3, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, strings.TrimSpace(in.FQDN), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getDomainEntry(ctx, moduleID, id)
}
func (p *Postgres) getDomainEntry(ctx context.Context, moduleID, id string) (*store.DomainEntry, error) {
var e store.DomainEntry
e.ModuleID = moduleID
var comm *string
err := p.pool.QueryRow(ctx, `SELECT id::text, fqdn, community_id::text FROM module_domain_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.FQDN, &comm)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
return &e, nil
}
func (p *Postgres) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *store.DomainEntryPatch) (*store.DomainEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getDomainEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.FQDN != nil {
cur.FQDN = strings.TrimSpace(*patch.FQDN)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_domain_entry SET fqdn=$3, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.FQDN, uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
return p.getDomainEntry(ctx, moduleID, entryID)
}
func (p *Postgres) DeleteDomainEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_domain_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListIPRangeEntries(tenantID, moduleID string) ([]*store.IPRangeEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE module_id=$1`, moduleID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*store.IPRangeEntry
for rows.Next() {
var e store.IPRangeEntry
e.ModuleID = moduleID
var comm *string
if err := rows.Scan(&e.ID, &e.Prefix, &comm); err != nil {
continue
}
e.CommunityID = strOrNil(comm)
out = append(out, &e)
}
return out, nil
}
func (p *Postgres) CreateIPRangeEntry(tenantID, moduleID string, in *store.IPRangeEntry) (*store.IPRangeEntry, error) {
mod, err := p.GetModule(tenantID, moduleID)
if err != nil {
return nil, err
}
if mod.Type != "IP_RANGES" || in == nil || strings.TrimSpace(in.Prefix) == "" {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
id := uuid.NewString()
_, err = p.pool.Exec(ctx, `
INSERT INTO module_ip_range_entry (id, module_id, prefix, community_id)
VALUES ($1,$2,$3::cidr, NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
id, moduleID, strings.TrimSpace(in.Prefix), uuidOrNilPtr(in.CommunityID))
if err != nil {
return nil, err
}
return p.getIPRangeEntry(ctx, moduleID, id)
}
func (p *Postgres) getIPRangeEntry(ctx context.Context, moduleID, id string) (*store.IPRangeEntry, error) {
var e store.IPRangeEntry
e.ModuleID = moduleID
var comm *string
err := p.pool.QueryRow(ctx, `SELECT id::text, prefix::text, community_id::text FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&e.ID, &e.Prefix, &comm)
if err != nil {
return nil, err
}
e.CommunityID = strOrNil(comm)
return &e, nil
}
func (p *Postgres) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *store.IPRangePatch) (*store.IPRangeEntry, error) {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return nil, err
}
cur, err := p.getIPRangeEntry(context.Background(), moduleID, entryID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, store.ErrNotFound
}
return nil, err
}
if patch.Prefix != nil {
cur.Prefix = strings.TrimSpace(*patch.Prefix)
}
if patch.CommunityID != nil {
v := strings.TrimSpace(*patch.CommunityID)
if v == "" {
cur.CommunityID = nil
} else {
cur.CommunityID = &v
}
}
ctx := context.Background()
_, err = p.pool.Exec(ctx, `
UPDATE module_ip_range_entry SET prefix=$3::cidr, community_id=NULLIF($4::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
WHERE id=$1 AND module_id=$2`,
entryID, moduleID, cur.Prefix, uuidOrNilPtr(cur.CommunityID))
if err != nil {
return nil, err
}
return p.getIPRangeEntry(ctx, moduleID, entryID)
}
func (p *Postgres) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error {
if _, err := p.GetModule(tenantID, moduleID); err != nil {
return err
}
ctx := context.Background()
tag, err := p.pool.Exec(ctx, `DELETE FROM module_ip_range_entry WHERE id=$1 AND module_id=$2`, entryID, moduleID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return store.ErrNotFound
}
return nil
}
func (p *Postgres) ListGlobalSettings(tenantID string) (map[string]any, error) {
if _, err := uuid.Parse(tenantID); err != nil {
return nil, store.ErrInvalidInput
}
ctx := context.Background()
rows, err := p.pool.Query(ctx, `SELECT key, value_json FROM global_settings WHERE tenant_id=$1`, tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
out := make(map[string]any)
for rows.Next() {
var k string
var vj []byte
if err := rows.Scan(&k, &vj); err != nil {
continue
}
var v any
_ = json.Unmarshal(vj, &v)
out[k] = v
}
return out, nil
}
func (p *Postgres) PatchGlobalSettings(tenantID string, patch map[string]any) error {
if _, err := uuid.Parse(tenantID); err != nil {
return store.ErrInvalidInput
}
if patch == nil {
return nil
}
ctx := context.Background()
for k, v := range patch {
if strings.TrimSpace(k) == "" {
continue
}
if v == nil {
_, _ = p.pool.Exec(ctx, `DELETE FROM global_settings WHERE tenant_id=$1 AND key=$2`, tenantID, k)
continue
}
b, err := json.Marshal(v)
if err != nil {
return err
}
_, err = p.pool.Exec(ctx, `
INSERT INTO global_settings (tenant_id, key, value_json) VALUES ($1,$2,$3::jsonb)
ON CONFLICT (tenant_id, key) DO UPDATE SET value_json = EXCLUDED.value_json, updated_at = now()`,
tenantID, k, string(b))
if err != nil {
return err
}
}
return nil
}
var _ store.Backend = (*Postgres)(nil)