clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / internal / catalog / hosts.go
hosts.go 4.6K
1package catalog23import (4 "context"5 "database/sql"6 "errors"7 "fmt"8 "time"9)1011// HostMapping is an operator-owned hostname routed to one tenant. Site is an12// optional NIP-5A label; an empty site routes the relay itself.13type HostMapping struct {14 TenantID string `json:"tenantId"`15 Host string `json:"host"`16 Site string `json:"site,omitempty"`17 Status string `json:"status"`18 CreatedAt time.Time `json:"createdAt"`19 UpdatedAt time.Time `json:"updatedAt"`20}2122var ErrHostMappingNotFound = errors.New("host mapping not found")2324func (c *Catalog) AddHost(ctx context.Context, tenantID, host, site string) (HostMapping, error) {25 if err := contextError(ctx); err != nil {26 return HostMapping{}, err27 }28 normal, err := normalizeHost(host)29 if err != nil {30 return HostMapping{}, err31 }32 if tenantID == "" {33 return HostMapping{}, errors.New("tenant id is required")34 }35 if _, err := c.GetByID(ctx, tenantID); err != nil {36 return HostMapping{}, err37 }38 var primaryTenant string39 if err := c.db.QueryRowContext(ctx, "SELECT id FROM tenants WHERE host=?", normal).Scan(&primaryTenant); err == nil {40 return HostMapping{}, ErrHostTaken41 } else if !errors.Is(err, sql.ErrNoRows) {42 return HostMapping{}, fmt.Errorf("check primary host: %w", err)43 }44 now := time.Now().UTC()45 _, err = c.db.ExecContext(ctx, `INSERT INTO tenant_hosts(tenant_id,host,site,status,created_at,updated_at) VALUES(?,?,?,?,?,?)`, tenantID, normal, site, "configured", now.UnixNano(), now.UnixNano())46 if err != nil {47 if isConstraint(err) {48 return HostMapping{}, ErrHostTaken49 }50 return HostMapping{}, fmt.Errorf("add host mapping: %w", err)51 }52 return HostMapping{TenantID: tenantID, Host: normal, Site: site, Status: "configured", CreatedAt: now, UpdatedAt: now}, nil53}5455func (c *Catalog) SetHostSite(ctx context.Context, tenantID, host, site string) (HostMapping, error) {56 if err := contextError(ctx); err != nil {57 return HostMapping{}, err58 }59 normal, err := normalizeHost(host)60 if err != nil {61 return HostMapping{}, err62 }63 now := time.Now().UTC()64 result, err := c.db.ExecContext(ctx, `UPDATE tenant_hosts SET site=?,updated_at=? WHERE tenant_id=? AND host=?`, site, now.UnixNano(), tenantID, normal)65 if err != nil {66 return HostMapping{}, fmt.Errorf("set host site: %w", err)67 }68 if n, _ := result.RowsAffected(); n != 1 {69 return HostMapping{}, ErrHostMappingNotFound70 }71 return c.hostMapping(ctx, tenantID, normal)72}7374func (c *Catalog) RemoveHost(ctx context.Context, tenantID, host string) error {75 if err := contextError(ctx); err != nil {76 return err77 }78 normal, err := normalizeHost(host)79 if err != nil {80 return err81 }82 result, err := c.db.ExecContext(ctx, `DELETE FROM tenant_hosts WHERE tenant_id=? AND host=?`, tenantID, normal)83 if err != nil {84 return fmt.Errorf("remove host mapping: %w", err)85 }86 if n, _ := result.RowsAffected(); n != 1 {87 return ErrHostMappingNotFound88 }89 return nil90}9192func (c *Catalog) ListHosts(ctx context.Context, tenantID string) ([]HostMapping, error) {93 if err := contextError(ctx); err != nil {94 return nil, err95 }96 rows, err := c.db.QueryContext(ctx, `SELECT tenant_id,host,site,status,created_at,updated_at FROM tenant_hosts WHERE tenant_id=? ORDER BY host`, tenantID)97 if err != nil {98 return nil, fmt.Errorf("list host mappings: %w", err)99 }100 defer rows.Close()101 result := []HostMapping{}102 for rows.Next() {103 mapping, scanErr := scanHostMapping(rows)104 if scanErr != nil {105 return nil, scanErr106 }107 result = append(result, mapping)108 }109 return result, rows.Err()110}111112func (c *Catalog) ResolveHostMapping(ctx context.Context, host string) (HostMapping, error) {113 if err := contextError(ctx); err != nil {114 return HostMapping{}, err115 }116 normal, err := normalizeHost(host)117 if err != nil {118 return HostMapping{}, err119 }120 return c.hostMapping(ctx, "", normal)121}122123func (c *Catalog) hostMapping(ctx context.Context, tenantID, host string) (HostMapping, error) {124 query := `SELECT tenant_id,host,site,status,created_at,updated_at FROM tenant_hosts WHERE host=?`125 args := []any{host}126 if tenantID != "" {127 query += " AND tenant_id=?"128 args = append(args, tenantID)129 }130 row := c.db.QueryRowContext(ctx, query, args...)131 mapping, err := scanHostMapping(row)132 if errors.Is(err, sql.ErrNoRows) {133 return HostMapping{}, ErrHostMappingNotFound134 }135 if err != nil {136 return HostMapping{}, fmt.Errorf("get host mapping: %w", err)137 }138 return mapping, nil139}140141func scanHostMapping(row scanner) (HostMapping, error) {142 var m HostMapping143 var created, updated int64144 if err := row.Scan(&m.TenantID, &m.Host, &m.Site, &m.Status, &created, &updated); err != nil {145 return HostMapping{}, err146 }147 m.CreatedAt, m.UpdatedAt = time.Unix(0, created).UTC(), time.Unix(0, updated).UTC()148 return m, nil149}