tinyrelayfile
tinyrelay/internal/catalog/hosts.go/

internal/catalog/hosts.go raw
1package catalog
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "fmt"
8 "time"
9)
10
11// HostMapping is an operator-owned hostname routed to one tenant. Site is an
12// 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}
21
22var ErrHostMappingNotFound = errors.New("host mapping not found")
23
24func (c *Catalog) AddHost(ctx context.Context, tenantID, host, site string) (HostMapping, error) {
25 if err := contextError(ctx); err != nil {
26 return HostMapping{}, err
27 }
28 normal, err := normalizeHost(host)
29 if err != nil {
30 return HostMapping{}, err
31 }
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{}, err
37 }
38 var primaryTenant string
39 if err := c.db.QueryRowContext(ctx, "SELECT id FROM tenants WHERE host=?", normal).Scan(&primaryTenant); err == nil {
40 return HostMapping{}, ErrHostTaken
41 } 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{}, ErrHostTaken
49 }
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}, nil
53}
54
55func (c *Catalog) SetHostSite(ctx context.Context, tenantID, host, site string) (HostMapping, error) {
56 if err := contextError(ctx); err != nil {
57 return HostMapping{}, err
58 }
59 normal, err := normalizeHost(host)
60 if err != nil {
61 return HostMapping{}, err
62 }
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{}, ErrHostMappingNotFound
70 }
71 return c.hostMapping(ctx, tenantID, normal)
72}
73
74func (c *Catalog) RemoveHost(ctx context.Context, tenantID, host string) error {
75 if err := contextError(ctx); err != nil {
76 return err
77 }
78 normal, err := normalizeHost(host)
79 if err != nil {
80 return err
81 }
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 ErrHostMappingNotFound
88 }
89 return nil
90}
91
92func (c *Catalog) ListHosts(ctx context.Context, tenantID string) ([]HostMapping, error) {
93 if err := contextError(ctx); err != nil {
94 return nil, err
95 }
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, scanErr
106 }
107 result = append(result, mapping)
108 }
109 return result, rows.Err()
110}
111
112func (c *Catalog) ResolveHostMapping(ctx context.Context, host string) (HostMapping, error) {
113 if err := contextError(ctx); err != nil {
114 return HostMapping{}, err
115 }
116 normal, err := normalizeHost(host)
117 if err != nil {
118 return HostMapping{}, err
119 }
120 return c.hostMapping(ctx, "", normal)
121}
122
123func (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{}, ErrHostMappingNotFound
134 }
135 if err != nil {
136 return HostMapping{}, fmt.Errorf("get host mapping: %w", err)
137 }
138 return mapping, nil
139}
140
141func scanHostMapping(row scanner) (HostMapping, error) {
142 var m HostMapping
143 var created, updated int64
144 if err := row.Scan(&m.TenantID, &m.Host, &m.Site, &m.Status, &created, &updated); err != nil {
145 return HostMapping{}, err
146 }
147 m.CreatedAt, m.UpdatedAt = time.Unix(0, created).UTC(), time.Unix(0, updated).UTC()
148 return m, nil
149}

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs