+67
-133
appview/db/registration.go
+67
-133
appview/db/registration.go
···
1
1
package db
2
2
3
3
import (
4
-
"crypto/rand"
5
4
"database/sql"
6
-
"encoding/hex"
7
5
"fmt"
8
-
"log"
9
6
"strings"
10
7
"time"
11
8
)
···
18
15
ByDid string
19
16
Created *time.Time
20
17
Registered *time.Time
18
+
ReadOnly bool
21
19
}
22
20
23
21
func (r *Registration) Status() Status {
24
-
if r.Registered != nil {
22
+
if r.ReadOnly {
23
+
return ReadOnly
24
+
} else if r.Registered != nil {
25
25
return Registered
26
26
} else {
27
27
return Pending
28
28
}
29
29
}
30
30
31
+
func (r *Registration) IsRegistered() bool {
32
+
return r.Status() == Registered
33
+
}
34
+
35
+
func (r *Registration) IsReadOnly() bool {
36
+
return r.Status() == ReadOnly
37
+
}
38
+
39
+
func (r *Registration) IsPending() bool {
40
+
return r.Status() == Pending
41
+
}
42
+
31
43
type Status uint32
32
44
33
45
const (
34
46
Registered Status = iota
35
47
Pending
48
+
ReadOnly
36
49
)
37
50
38
-
// returns registered status, did of owner, error
39
-
func RegistrationsByDid(e Execer, did string) ([]Registration, error) {
51
+
func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) {
40
52
var registrations []Registration
41
53
42
-
rows, err := e.Query(`
43
-
select id, domain, did, created, registered from registrations
44
-
where did = ?
45
-
`, did)
46
-
if err != nil {
47
-
return nil, err
54
+
var conditions []string
55
+
var args []any
56
+
for _, filter := range filters {
57
+
conditions = append(conditions, filter.Condition())
58
+
args = append(args, filter.Arg()...)
48
59
}
49
60
50
-
for rows.Next() {
51
-
var createdAt *string
52
-
var registeredAt *string
53
-
var registration Registration
54
-
err = rows.Scan(®istration.Id, ®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt)
55
-
56
-
if err != nil {
57
-
log.Println(err)
58
-
} else {
59
-
createdAtTime, _ := time.Parse(time.RFC3339, *createdAt)
60
-
var registeredAtTime *time.Time
61
-
if registeredAt != nil {
62
-
x, _ := time.Parse(time.RFC3339, *registeredAt)
63
-
registeredAtTime = &x
64
-
}
65
-
66
-
registration.Created = &createdAtTime
67
-
registration.Registered = registeredAtTime
68
-
registrations = append(registrations, registration)
69
-
}
61
+
whereClause := ""
62
+
if conditions != nil {
63
+
whereClause = " where " + strings.Join(conditions, " and ")
70
64
}
71
65
72
-
return registrations, nil
73
-
}
74
-
75
-
// returns registered status, did of owner, error
76
-
func RegistrationByDomain(e Execer, domain string) (*Registration, error) {
77
-
var createdAt *string
78
-
var registeredAt *string
79
-
var registration Registration
80
-
81
-
err := e.QueryRow(`
82
-
select id, domain, did, created, registered from registrations
83
-
where domain = ?
84
-
`, domain).Scan(®istration.Id, ®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt)
66
+
query := fmt.Sprintf(`
67
+
select id, domain, did, created, registered, read_only
68
+
from registrations
69
+
%s
70
+
order by created
71
+
`,
72
+
whereClause,
73
+
)
85
74
75
+
rows, err := e.Query(query, args...)
86
76
if err != nil {
87
-
if err == sql.ErrNoRows {
88
-
return nil, nil
89
-
} else {
90
-
return nil, err
91
-
}
77
+
return nil, err
92
78
}
93
79
94
-
createdAtTime, _ := time.Parse(time.RFC3339, *createdAt)
95
-
var registeredAtTime *time.Time
96
-
if registeredAt != nil {
97
-
x, _ := time.Parse(time.RFC3339, *registeredAt)
98
-
registeredAtTime = &x
99
-
}
100
-
101
-
registration.Created = &createdAtTime
102
-
registration.Registered = registeredAtTime
103
-
104
-
return ®istration, nil
105
-
}
106
-
107
-
func genSecret() string {
108
-
key := make([]byte, 32)
109
-
rand.Read(key)
110
-
return hex.EncodeToString(key)
111
-
}
80
+
for rows.Next() {
81
+
var createdAt string
82
+
var registeredAt sql.Null[string]
83
+
var readOnly int
84
+
var reg Registration
112
85
113
-
func GenerateRegistrationKey(e Execer, domain, did string) (string, error) {
114
-
// sanity check: does this domain already have a registration?
115
-
reg, err := RegistrationByDomain(e, domain)
116
-
if err != nil {
117
-
return "", err
118
-
}
119
-
120
-
// registration is open
121
-
if reg != nil {
122
-
switch reg.Status() {
123
-
case Registered:
124
-
// already registered by `owner`
125
-
return "", fmt.Errorf("%s already registered by %s", domain, reg.ByDid)
126
-
case Pending:
127
-
// TODO: be loud about this
128
-
log.Printf("%s registered by %s, status pending", domain, reg.ByDid)
86
+
err = rows.Scan(®.Id, ®.Domain, ®.ByDid, &createdAt, ®isteredAt, &readOnly)
87
+
if err != nil {
88
+
return nil, err
129
89
}
130
-
}
131
90
132
-
secret := genSecret()
91
+
if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
92
+
reg.Created = &t
93
+
}
133
94
134
-
_, err = e.Exec(`
135
-
insert into registrations (domain, did, secret)
136
-
values (?, ?, ?)
137
-
on conflict(domain) do update set did = excluded.did, secret = excluded.secret, created = excluded.created
138
-
`, domain, did, secret)
139
-
140
-
if err != nil {
141
-
return "", err
142
-
}
143
-
144
-
return secret, nil
145
-
}
95
+
if registeredAt.Valid {
96
+
if t, err := time.Parse(time.RFC3339, registeredAt.V); err == nil {
97
+
reg.Registered = &t
98
+
}
99
+
}
146
100
147
-
func GetRegistrationKey(e Execer, domain string) (string, error) {
148
-
res := e.QueryRow(`select secret from registrations where domain = ?`, domain)
101
+
if readOnly != 0 {
102
+
reg.ReadOnly = true
103
+
}
149
104
150
-
var secret string
151
-
err := res.Scan(&secret)
152
-
if err != nil || secret == "" {
153
-
return "", err
105
+
registrations = append(registrations, reg)
154
106
}
155
107
156
-
return secret, nil
108
+
return registrations, nil
157
109
}
158
110
159
-
func GetCompletedRegistrations(e Execer) ([]string, error) {
160
-
rows, err := e.Query(`select domain from registrations where registered not null`)
161
-
if err != nil {
162
-
return nil, err
163
-
}
164
-
165
-
var domains []string
166
-
for rows.Next() {
167
-
var domain string
168
-
err = rows.Scan(&domain)
169
-
170
-
if err != nil {
171
-
log.Println(err)
172
-
} else {
173
-
domains = append(domains, domain)
174
-
}
111
+
func MarkRegistered(e Execer, filters ...filter) error {
112
+
var conditions []string
113
+
var args []any
114
+
for _, filter := range filters {
115
+
conditions = append(conditions, filter.Condition())
116
+
args = append(args, filter.Arg()...)
175
117
}
176
118
177
-
if err = rows.Err(); err != nil {
178
-
return nil, err
119
+
query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0"
120
+
if len(conditions) > 0 {
121
+
query += " where " + strings.Join(conditions, " and ")
179
122
}
180
123
181
-
return domains, nil
182
-
}
183
-
184
-
func Register(e Execer, domain string) error {
185
-
_, err := e.Exec(`
186
-
update registrations
187
-
set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
188
-
where domain = ?;
189
-
`, domain)
190
-
124
+
_, err := e.Exec(query, args...)
191
125
return err
192
126
}
193
127
+40
-16
appview/knots/knots.go
+40
-16
appview/knots/knots.go
···
3
3
import (
4
4
"errors"
5
5
"fmt"
6
+
"log"
6
7
"log/slog"
7
8
"net/http"
8
9
"slices"
···
49
50
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
50
51
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
51
52
53
+
r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner)
54
+
52
55
return r
53
56
}
54
57
55
58
func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
56
59
user := k.OAuth.GetUser(r)
57
-
registrations, err := db.RegistrationsByDid(k.Db, user.Did)
60
+
registrations, err := db.GetRegistrations(
61
+
k.Db,
62
+
db.FilterEq("did", user.Did),
63
+
)
58
64
if err != nil {
59
65
k.Logger.Error("failed to fetch knot registrations", "err", err)
60
66
w.WriteHeader(http.StatusInternalServerError)
···
89
95
http.Error(w, "Not found", http.StatusNotFound)
90
96
return
91
97
}
92
-
93
-
// Find the specific registration for this domain
94
-
var registration *db.Registration
95
-
for _, reg := range registrations {
96
-
if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil {
97
-
registration = ®
98
-
break
99
-
}
100
-
}
101
-
102
-
if registration == nil {
103
-
l.Error("registration not found or not verified")
104
-
http.Error(w, "Not found", http.StatusNotFound)
98
+
if len(registrations) != 1 {
99
+
l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1)
105
100
return
106
101
}
107
102
registration := registrations[0]
···
518
513
db.FilterIsNot("registered", "null"),
519
514
)
520
515
if err != nil {
521
-
l.Error("failed to retrieve domain registration", "err", err)
522
-
http.Error(w, "Not found", http.StatusNotFound)
516
+
l.Error("failed to get registration", "err", err)
523
517
return
524
518
}
519
+
if len(registrations) != 1 {
520
+
l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1)
521
+
return
522
+
}
523
+
registration := registrations[0]
525
524
526
525
noticeId := fmt.Sprintf("add-member-error-%d", registration.Id)
527
526
defaultErr := "Failed to add member. Try again later."
···
679
678
// ok
680
679
k.Pages.HxRefresh(w)
681
680
}
681
+
682
+
func (k *Knots) banner(w http.ResponseWriter, r *http.Request) {
683
+
user := k.OAuth.GetUser(r)
684
+
l := k.Logger.With("handler", "removeMember")
685
+
l = l.With("did", user.Did)
686
+
l = l.With("handle", user.Handle)
687
+
688
+
registrations, err := db.GetRegistrations(
689
+
k.Db,
690
+
db.FilterEq("did", user.Did),
691
+
db.FilterEq("read_only", 1),
692
+
)
693
+
if err != nil {
694
+
l.Error("non-fatal: failed to get registrations")
695
+
return
696
+
}
697
+
698
+
if registrations == nil {
699
+
return
700
+
}
701
+
702
+
k.Pages.KnotBanner(w, pages.KnotBannerParams{
703
+
Registrations: registrations,
704
+
})
705
+
}
+9
-1
appview/pages/pages.go
+9
-1
appview/pages/pages.go
···
338
338
return p.execute("user/settings/emails", w, params)
339
339
}
340
340
341
+
type KnotBannerParams struct {
342
+
Registrations []db.Registration
343
+
}
344
+
345
+
func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error {
346
+
return p.executePlain("knots/fragments/banner", w, params)
347
+
}
348
+
341
349
type KnotsParams struct {
342
350
LoggedInUser *oauth.User
343
351
Registrations []db.Registration
···
360
368
}
361
369
362
370
type KnotListingParams struct {
363
-
db.Registration
371
+
*db.Registration
364
372
}
365
373
366
374
func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error {
+11
-2
appview/pages/templates/knots/fragments/knotListing.html
+11
-2
appview/pages/templates/knots/fragments/knotListing.html
···
30
30
{{ define "knotRightSide" }}
31
31
<div id="right-side" class="flex gap-2">
32
32
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }}
33
-
{{ if .Registered }}
34
-
<span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span>
33
+
{{ if .IsRegistered }}
34
+
<span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">
35
+
{{ i "shield-check" "w-4 h-4" }} verified
36
+
</span>
35
37
{{ template "knots/fragments/addMemberModal" . }}
38
+
{{ block "knotDeleteButton" . }} {{ end }}
39
+
{{ else if .IsReadOnly }}
40
+
<span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}">
41
+
{{ i "shield-alert" "w-4 h-4" }} read-only
42
+
</span>
43
+
{{ block "knotRetryButton" . }} {{ end }}
44
+
{{ block "knotDeleteButton" . }} {{ end }}
36
45
{{ else }}
37
46
<span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}">
38
47
{{ i "shield-off" "w-4 h-4" }} unverified
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/knots/index.html
+7
appview/pages/templates/layouts/topbar.html
+7
appview/pages/templates/layouts/topbar.html
+3
-1
appview/pages/templates/spindles/fragments/spindleListing.html
+3
-1
appview/pages/templates/spindles/fragments/spindleListing.html
···
9
9
{{ if .Verified }}
10
10
<a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
11
{{ i "hard-drive" "w-4 h-4" }}
12
-
{{ .Instance }}
12
+
<span class="hover:underline">
13
+
{{ .Instance }}
14
+
</span>
13
15
<span class="text-gray-500">
14
16
{{ template "repo/fragments/shortTimeAgo" .Created }}
15
17
</span>
+5
-2
appview/state/knotstream.go
+5
-2
appview/state/knotstream.go
···
24
24
)
25
25
26
26
func Knotstream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client) (*ec.Consumer, error) {
27
-
knots, err := db.GetCompletedRegistrations(d)
27
+
knots, err := db.GetRegistrations(
28
+
d,
29
+
db.FilterIsNot("registered", "null"),
30
+
)
28
31
if err != nil {
29
32
return nil, err
30
33
}
31
34
32
35
srcs := make(map[ec.Source]struct{})
33
36
for _, k := range knots {
34
-
s := ec.NewKnotSource(k)
37
+
s := ec.NewKnotSource(k.Domain)
35
38
srcs[s] = struct{}{}
36
39
}
37
40
+3
-3
appview/state/state.go
+3
-3
appview/state/state.go
···
435
435
Rkey: rkey,
436
436
},
437
437
)
438
-
if err != nil {
439
-
l.Error("xrpc request failed", "err", err)
440
-
s.pages.Notice(w, "repo", fmt.Sprintf("Failed to create repository on knot server: %s.", err.Error()))
438
+
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
439
+
l.Error("xrpc error", "xe", xe)
440
+
s.pages.Notice(w, "repo", err.Error())
441
441
return
442
442
}
443
443
+25
appview/xrpcclient/xrpc.go
+25
appview/xrpcclient/xrpc.go
···
3
3
import (
4
4
"bytes"
5
5
"context"
6
+
"errors"
7
+
"fmt"
6
8
"io"
9
+
"net/http"
7
10
8
11
"github.com/bluesky-social/indigo/api/atproto"
9
12
"github.com/bluesky-social/indigo/xrpc"
13
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
10
14
oauth "tangled.sh/icyphox.sh/atproto-oauth"
11
15
)
12
16
···
102
106
103
107
return &out, nil
104
108
}
109
+
110
+
// produces a more manageable error
111
+
func HandleXrpcErr(err error) error {
112
+
if err == nil {
113
+
return nil
114
+
}
115
+
116
+
var xrpcerr *indigoxrpc.Error
117
+
if ok := errors.As(err, &xrpcerr); !ok {
118
+
return fmt.Errorf("Recieved invalid XRPC error response.")
119
+
}
120
+
121
+
switch xrpcerr.StatusCode {
122
+
case http.StatusNotFound:
123
+
return fmt.Errorf("XRPC is unsupported on this knot, consider upgrading your knot.")
124
+
case http.StatusUnauthorized:
125
+
return fmt.Errorf("Unauthorized XRPC request.")
126
+
default:
127
+
return fmt.Errorf("Failed to perform operation. Try again later.")
128
+
}
129
+
}
+3
-3
knotserver/ingester.go
+3
-3
knotserver/ingester.go
···
73
73
}
74
74
l.Info("added member from firehose", "member", record.Subject)
75
75
76
-
if err := h.db.AddDid(did); err != nil {
76
+
if err := h.db.AddDid(record.Subject); err != nil {
77
77
l.Error("failed to add did", "error", err)
78
78
return fmt.Errorf("failed to add did: %w", err)
79
79
}
80
-
h.jc.AddDid(did)
80
+
h.jc.AddDid(record.Subject)
81
81
82
-
if err := h.fetchAndAddKeys(ctx, did); err != nil {
82
+
if err := h.fetchAndAddKeys(ctx, record.Subject); err != nil {
83
83
return fmt.Errorf("failed to fetch and add keys: %w", err)
84
84
}
85
85