+2
api/tangled/repodelete.go
+2
api/tangled/repodelete.go
···
20
20
Did string `json:"did" cborgen:"did"`
21
21
// name: Name of the repository to delete
22
22
Name string `json:"name" cborgen:"name"`
23
+
// rkey: Rkey of the repository record
24
+
Rkey string `json:"rkey" cborgen:"rkey"`
23
25
}
24
26
25
27
// RepoDelete calls the XRPC method "sh.tangled.repo.delete".
+1
-1
appview/config/config.go
+1
-1
appview/config/config.go
+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
+
}
+98
-82
appview/oauth/handler/handler.go
+98
-82
appview/oauth/handler/handler.go
···
8
8
"log"
9
9
"net/http"
10
10
"net/url"
11
+
"slices"
11
12
"strings"
12
13
"time"
13
14
···
25
26
"tangled.sh/tangled.sh/core/appview/oauth/client"
26
27
"tangled.sh/tangled.sh/core/appview/pages"
27
28
"tangled.sh/tangled.sh/core/idresolver"
28
-
"tangled.sh/tangled.sh/core/knotclient"
29
29
"tangled.sh/tangled.sh/core/rbac"
30
30
"tangled.sh/tangled.sh/core/tid"
31
31
)
···
353
353
return pubKey, nil
354
354
}
355
355
356
+
var (
357
+
tangledHandle = "tangled.sh"
358
+
tangledDid = "did:plc:wshs7t2adsemcrrd4snkeqli"
359
+
defaultSpindle = "spindle.tangled.sh"
360
+
defaultKnot = "knot1.tangled.sh"
361
+
)
362
+
356
363
func (o *OAuthHandler) addToDefaultSpindle(did string) {
357
364
// use the tangled.sh app password to get an accessJwt
358
365
// and create an sh.tangled.spindle.member record with that
359
-
360
-
defaultSpindle := "spindle.tangled.sh"
361
-
appPassword := o.config.Core.AppPassword
362
-
363
366
spindleMembers, err := db.GetSpindleMembers(
364
367
o.db,
365
368
db.FilterEq("instance", "spindle.tangled.sh"),
···
375
378
return
376
379
}
377
380
378
-
// TODO: hardcoded tangled handle and did for now
379
-
tangledHandle := "tangled.sh"
380
-
tangledDid := "did:plc:wshs7t2adsemcrrd4snkeqli"
381
+
log.Printf("adding %s to default spindle", did)
382
+
session, err := o.createAppPasswordSession()
383
+
if err != nil {
384
+
log.Printf("failed to create session: %s", err)
385
+
return
386
+
}
387
+
388
+
record := tangled.SpindleMember{
389
+
LexiconTypeID: "sh.tangled.spindle.member",
390
+
Subject: did,
391
+
Instance: defaultSpindle,
392
+
CreatedAt: time.Now().Format(time.RFC3339),
393
+
}
394
+
395
+
if err := session.putRecord(record); err != nil {
396
+
log.Printf("failed to add member to default knot: %s", err)
397
+
return
398
+
}
399
+
400
+
log.Printf("successfully added %s to default spindle", did)
401
+
}
402
+
403
+
func (o *OAuthHandler) addToDefaultKnot(did string) {
404
+
// use the tangled.sh app password to get an accessJwt
405
+
// and create an sh.tangled.spindle.member record with that
406
+
407
+
allKnots, err := o.enforcer.GetKnotsForUser(did)
408
+
if err != nil {
409
+
log.Printf("failed to get knot members for did %s: %v", did, err)
410
+
return
411
+
}
412
+
413
+
if slices.Contains(allKnots, defaultKnot) {
414
+
log.Printf("did %s is already a member of the default knot", did)
415
+
return
416
+
}
381
417
382
-
if appPassword == "" {
383
-
log.Println("no app password configured, skipping spindle member addition")
418
+
log.Printf("adding %s to default knot", did)
419
+
session, err := o.createAppPasswordSession()
420
+
if err != nil {
421
+
log.Printf("failed to create session: %s", err)
384
422
return
385
423
}
386
424
387
-
log.Printf("adding %s to default spindle", did)
425
+
record := tangled.KnotMember{
426
+
LexiconTypeID: "sh.tangled.knot.member",
427
+
Subject: did,
428
+
Domain: defaultKnot,
429
+
CreatedAt: time.Now().Format(time.RFC3339),
430
+
}
431
+
432
+
if err := session.putRecord(record); err != nil {
433
+
log.Printf("failed to add member to default knot: %s", err)
434
+
return
435
+
}
436
+
437
+
log.Printf("successfully added %s to default Knot", did)
438
+
}
439
+
440
+
// create a session using apppasswords
441
+
type session struct {
442
+
AccessJwt string `json:"accessJwt"`
443
+
PdsEndpoint string
444
+
}
445
+
446
+
func (o *OAuthHandler) createAppPasswordSession() (*session, error) {
447
+
appPassword := o.config.Core.AppPassword
448
+
if appPassword == "" {
449
+
return nil, fmt.Errorf("no app password configured, skipping member addition")
450
+
}
388
451
389
452
resolved, err := o.idResolver.ResolveIdent(context.Background(), tangledDid)
390
453
if err != nil {
391
-
log.Printf("failed to resolve tangled.sh DID %s: %v", tangledDid, err)
392
-
return
454
+
return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", tangledDid, err)
393
455
}
394
456
395
457
pdsEndpoint := resolved.PDSEndpoint()
396
458
if pdsEndpoint == "" {
397
-
log.Printf("no PDS endpoint found for tangled.sh DID %s", tangledDid)
398
-
return
459
+
return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", tangledDid)
399
460
}
400
461
401
462
sessionPayload := map[string]string{
···
404
465
}
405
466
sessionBytes, err := json.Marshal(sessionPayload)
406
467
if err != nil {
407
-
log.Printf("failed to marshal session payload: %v", err)
408
-
return
468
+
return nil, fmt.Errorf("failed to marshal session payload: %v", err)
409
469
}
410
470
411
471
sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession"
412
472
sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes))
413
473
if err != nil {
414
-
log.Printf("failed to create session request: %v", err)
415
-
return
474
+
return nil, fmt.Errorf("failed to create session request: %v", err)
416
475
}
417
476
sessionReq.Header.Set("Content-Type", "application/json")
418
477
419
478
client := &http.Client{Timeout: 30 * time.Second}
420
479
sessionResp, err := client.Do(sessionReq)
421
480
if err != nil {
422
-
log.Printf("failed to create session: %v", err)
423
-
return
481
+
return nil, fmt.Errorf("failed to create session: %v", err)
424
482
}
425
483
defer sessionResp.Body.Close()
426
484
427
485
if sessionResp.StatusCode != http.StatusOK {
428
-
log.Printf("failed to create session: HTTP %d", sessionResp.StatusCode)
429
-
return
486
+
return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode)
430
487
}
431
488
432
-
var session struct {
433
-
AccessJwt string `json:"accessJwt"`
434
-
}
489
+
var session session
435
490
if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil {
436
-
log.Printf("failed to decode session response: %v", err)
437
-
return
491
+
return nil, fmt.Errorf("failed to decode session response: %v", err)
438
492
}
439
493
440
-
record := tangled.SpindleMember{
441
-
LexiconTypeID: "sh.tangled.spindle.member",
442
-
Subject: did,
443
-
Instance: defaultSpindle,
444
-
CreatedAt: time.Now().Format(time.RFC3339),
445
-
}
494
+
session.PdsEndpoint = pdsEndpoint
495
+
496
+
return &session, nil
497
+
}
446
498
499
+
func (s *session) putRecord(record any) error {
447
500
recordBytes, err := json.Marshal(record)
448
501
if err != nil {
449
-
log.Printf("failed to marshal spindle member record: %v", err)
450
-
return
502
+
return fmt.Errorf("failed to marshal knot member record: %w", err)
451
503
}
452
504
453
-
payload := map[string]interface{}{
505
+
payload := map[string]any{
454
506
"repo": tangledDid,
455
-
"collection": tangled.SpindleMemberNSID,
507
+
"collection": tangled.KnotMemberNSID,
456
508
"rkey": tid.TID(),
457
509
"record": json.RawMessage(recordBytes),
458
510
}
459
511
460
512
payloadBytes, err := json.Marshal(payload)
461
513
if err != nil {
462
-
log.Printf("failed to marshal request payload: %v", err)
463
-
return
514
+
return fmt.Errorf("failed to marshal request payload: %w", err)
464
515
}
465
516
466
-
url := pdsEndpoint + "/xrpc/com.atproto.repo.putRecord"
517
+
url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord"
467
518
req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes))
468
519
if err != nil {
469
-
log.Printf("failed to create HTTP request: %v", err)
470
-
return
520
+
return fmt.Errorf("failed to create HTTP request: %w", err)
471
521
}
472
522
473
523
req.Header.Set("Content-Type", "application/json")
474
-
req.Header.Set("Authorization", "Bearer "+session.AccessJwt)
524
+
req.Header.Set("Authorization", "Bearer "+s.AccessJwt)
475
525
526
+
client := &http.Client{Timeout: 30 * time.Second}
476
527
resp, err := client.Do(req)
477
528
if err != nil {
478
-
log.Printf("failed to add user to default spindle: %v", err)
479
-
return
529
+
return fmt.Errorf("failed to add user to default Knot: %w", err)
480
530
}
481
531
defer resp.Body.Close()
482
532
483
533
if resp.StatusCode != http.StatusOK {
484
-
log.Printf("failed to add user to default spindle: HTTP %d", resp.StatusCode)
485
-
return
486
-
}
487
-
488
-
log.Printf("successfully added %s to default spindle", did)
489
-
}
490
-
491
-
func (o *OAuthHandler) addToDefaultKnot(did string) {
492
-
defaultKnot := "knot1.tangled.sh"
493
-
494
-
log.Printf("adding %s to default knot", did)
495
-
err := o.enforcer.AddKnotMember(defaultKnot, did)
496
-
if err != nil {
497
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
498
-
return
499
-
}
500
-
err = o.enforcer.E.SavePolicy()
501
-
if err != nil {
502
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
503
-
return
504
-
}
505
-
506
-
secret, err := db.GetRegistrationKey(o.db, defaultKnot)
507
-
if err != nil {
508
-
log.Println("failed to get registration key for knot1.tangled.sh")
509
-
return
510
-
}
511
-
signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.config.Core.Dev)
512
-
resp, err := signedClient.AddMember(did)
513
-
if err != nil {
514
-
log.Println("failed to add user to knot1.tangled.sh: ", err)
515
-
return
534
+
return fmt.Errorf("failed to add user to default Knot: HTTP %d", resp.StatusCode)
516
535
}
517
536
518
-
if resp.StatusCode != http.StatusNoContent {
519
-
log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)
520
-
return
521
-
}
537
+
return nil
522
538
}
+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/repo/settings/general.html
+3
-1
appview/pages/templates/repo/settings/general.html
···
8
8
<div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2">
9
9
{{ template "branchSettings" . }}
10
10
{{ template "deleteRepo" . }}
11
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
11
12
</div>
12
13
</section>
13
14
{{ end }}
···
22
23
unless you specify a different branch.
23
24
</p>
24
25
</div>
25
-
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch">
26
+
<form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" hx-swap="none" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch">
26
27
<select id="branch" name="branch" required class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700">
27
28
<option value="" disabled selected >
28
29
Choose a default branch
···
54
55
<button
55
56
class="btn group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex gap-2 items-center"
56
57
type="button"
58
+
hx-swap="none"
57
59
hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete"
58
60
hx-confirm="Are you sure you want to delete {{ $.RepoInfo.FullName }}?">
59
61
{{ i "trash-2" "size-4" }}
+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>
+19
-43
appview/pulls/pulls.go
+19
-43
appview/pulls/pulls.go
···
19
19
"tangled.sh/tangled.sh/core/appview/pages"
20
20
"tangled.sh/tangled.sh/core/appview/pages/markup"
21
21
"tangled.sh/tangled.sh/core/appview/reporesolver"
22
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
22
23
"tangled.sh/tangled.sh/core/idresolver"
23
24
"tangled.sh/tangled.sh/core/knotclient"
24
25
"tangled.sh/tangled.sh/core/patchutil"
···
28
29
"github.com/bluekeyes/go-gitdiff/gitdiff"
29
30
comatproto "github.com/bluesky-social/indigo/api/atproto"
30
31
lexutil "github.com/bluesky-social/indigo/lex/util"
32
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
31
33
"github.com/go-chi/chi/v5"
32
34
"github.com/google/uuid"
33
35
)
···
218
220
return types.MergeCheckResponse{}
219
221
}
220
222
221
-
client, err := s.oauth.ServiceClient(
222
-
r,
223
-
oauth.WithService(f.Knot),
224
-
oauth.WithLxm(tangled.RepoMergeCheckNSID),
225
-
oauth.WithDev(s.config.Core.Dev),
226
-
)
227
-
if err != nil {
228
-
log.Printf("failed to connect to knot server: %v", err)
229
-
return types.MergeCheckResponse{
230
-
Error: "failed to check merge status: could not connect to knot server",
231
-
}
223
+
scheme := "https"
224
+
if s.config.Core.Dev {
225
+
scheme = "http"
226
+
}
227
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
228
+
229
+
xrpcc := indigoxrpc.Client{
230
+
Host: host,
232
231
}
233
232
234
233
patch := pull.LatestPatch()
···
893
892
Repo: fork.RepoAt().String(),
894
893
},
895
894
)
896
-
if err != nil {
897
-
xe, parseErr := xrpcerr.Unmarshal(err.Error())
898
-
if parseErr != nil {
899
-
log.Printf("failed to create hidden ref: %v", err)
900
-
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
901
-
} else {
902
-
log.Printf("failed to create hidden ref: %s", xe.Error())
903
-
if xe.Tag == "AccessControl" {
904
-
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
905
-
} else {
906
-
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create pull request: %s", xe.Message))
907
-
}
908
-
}
895
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
896
+
s.pages.Notice(w, "pull", err.Error())
909
897
return
910
898
}
911
899
···
1501
1489
Repo: forkRepo.RepoAt().String(),
1502
1490
},
1503
1491
)
1504
-
if err != nil || !resp.Success {
1505
-
if err != nil {
1506
-
log.Printf("failed to update tracking branch: %s", err)
1507
-
} else {
1508
-
log.Printf("failed to update tracking branch: success=false")
1509
-
}
1510
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
1492
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
1493
+
s.pages.Notice(w, "resubmit-error", err.Error())
1494
+
return
1495
+
}
1496
+
if !resp.Success {
1497
+
log.Println("Failed to update tracking ref.", "err", resp.Error)
1498
+
s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.")
1511
1499
return
1512
1500
}
1513
1501
···
1932
1920
}
1933
1921
1934
1922
patch := pullsToMerge.CombinedPatch()
1935
-
1936
-
client, err := s.oauth.ServiceClient(
1937
-
r,
1938
-
oauth.WithService(f.Knot),
1939
-
oauth.WithLxm(tangled.RepoMergeNSID),
1940
-
oauth.WithDev(s.config.Core.Dev),
1941
-
)
1942
-
if err != nil {
1943
-
log.Printf("failed to connect to knot server: %v", err)
1944
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
1945
-
return
1946
-
}
1947
1923
1948
1924
ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid)
1949
1925
if err != nil {
-157
appview/repo/index.go
-157
appview/repo/index.go
···
101
101
user := rp.oauth.GetUser(r)
102
102
repoInfo := f.RepoInfo(user)
103
103
104
-
// secret, err := db.GetRegistrationKey(rp.db, f.Knot)
105
-
// if err != nil {
106
-
// log.Printf("failed to get registration key for %s: %s", f.Knot, err)
107
-
// rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
108
-
// }
109
-
110
-
// signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
111
-
// if err != nil {
112
-
// log.Printf("failed to create signed client for %s: %s", f.Knot, err)
113
-
// return
114
-
// }
115
-
116
-
// var forkInfo *types.ForkInfo
117
-
// if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {
118
-
// forkInfo, err = getForkInfo(r, repoInfo, rp, f, result.Ref, user, signedClient)
119
-
// if err != nil {
120
-
// log.Printf("Failed to fetch fork information: %v", err)
121
-
// return
122
-
// }
123
-
// }
124
-
125
104
// TODO: a bit dirty
126
105
languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "")
127
106
if err != nil {
···
227
206
228
207
return languageStats, nil
229
208
}
230
-
231
-
// func getForkInfo(
232
-
// r *http.Request,
233
-
// repoInfo repoinfo.RepoInfo,
234
-
// rp *Repo,
235
-
// f *reporesolver.ResolvedRepo,
236
-
// currentRef string,
237
-
// user *oauth.User,
238
-
// signedClient *knotclient.SignedClient,
239
-
// ) (*types.ForkInfo, error) {
240
-
// if user == nil {
241
-
// return nil, nil
242
-
// }
243
-
//
244
-
// forkInfo := types.ForkInfo{
245
-
// IsFork: repoInfo.Source != nil,
246
-
// Status: types.UpToDate,
247
-
// }
248
-
//
249
-
// if !forkInfo.IsFork {
250
-
// forkInfo.IsFork = false
251
-
// return &forkInfo, nil
252
-
// }
253
-
//
254
-
// us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev)
255
-
// if err != nil {
256
-
// log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)
257
-
// return nil, err
258
-
// }
259
-
//
260
-
// result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name)
261
-
// if err != nil {
262
-
// log.Println("failed to reach knotserver", err)
263
-
// return nil, err
264
-
// }
265
-
//
266
-
// if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {
267
-
// return branch.Name == currentRef
268
-
// }) {
269
-
// forkInfo.Status = types.MissingBranch
270
-
// return &forkInfo, nil
271
-
// }
272
-
//
273
-
// <<<<<<< Conflict 1 of 2
274
-
// %%%%%%% Changes from base #1 to side #1
275
-
// client, err := rp.oauth.ServiceClient(
276
-
// r,
277
-
// oauth.WithService(f.Knot),
278
-
// oauth.WithLxm(tangled.RepoHiddenRefNSID),
279
-
// oauth.WithDev(rp.config.Core.Dev),
280
-
// )
281
-
// if err != nil {
282
-
// log.Printf("failed to connect to knot server: %v", err)
283
-
// %%%%%%% Changes from base #2 to side #2
284
-
// - newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, currentRef, currentRef)
285
-
// + newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref)
286
-
// if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent {
287
-
// log.Printf("failed to update tracking branch: %s", err)
288
-
// +++++++ Contents of side #3
289
-
// client, err := rp.oauth.ServiceClient(
290
-
// r,
291
-
// oauth.WithService(f.Knot),
292
-
// oauth.WithLxm(tangled.RepoHiddenRefNSID),
293
-
// oauth.WithDev(rp.config.Core.Dev),
294
-
// )
295
-
// if err != nil {
296
-
// log.Printf("failed to connect to knot server: %v", err)
297
-
// >>>>>>> Conflict 1 of 2 ends
298
-
// return nil, err
299
-
// }
300
-
//
301
-
// <<<<<<< Conflict 2 of 2
302
-
// %%%%%%% Changes from base #1 to side #1
303
-
// resp, err := tangled.RepoHiddenRef(
304
-
// r.Context(),
305
-
// client,
306
-
// &tangled.RepoHiddenRef_Input{
307
-
// - ForkRef: f.Ref,
308
-
// - RemoteRef: f.Ref,
309
-
// + ForkRef: currentRef,
310
-
// + RemoteRef: currentRef,
311
-
// Repo: f.RepoAt().String(),
312
-
// },
313
-
// )
314
-
// if err != nil || !resp.Success {
315
-
// if err != nil {
316
-
// log.Printf("failed to update tracking branch: %s", err)
317
-
// } else {
318
-
// log.Printf("failed to update tracking branch: success=false")
319
-
// }
320
-
// return nil, fmt.Errorf("failed to update tracking branch")
321
-
// }
322
-
//
323
-
// - hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)
324
-
// + hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef)
325
-
//
326
-
// %%%%%%% Changes from base #2 to side #2
327
-
// - hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef)
328
-
// + hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)
329
-
//
330
-
// +++++++ Contents of side #3
331
-
// resp, err := tangled.RepoHiddenRef(
332
-
// r.Context(),
333
-
// client,
334
-
// &tangled.RepoHiddenRef_Input{
335
-
// ForkRef: currentRef,
336
-
// RemoteRef: currentRef,
337
-
// Repo: f.RepoAt().String(),
338
-
// },
339
-
// )
340
-
// if err != nil || !resp.Success {
341
-
// if err != nil {
342
-
// log.Printf("failed to update tracking branch: %s", err)
343
-
// } else {
344
-
// log.Printf("failed to update tracking branch: success=false")
345
-
// }
346
-
// return nil, fmt.Errorf("failed to update tracking branch")
347
-
// }
348
-
//
349
-
// hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef)
350
-
// >>>>>>> Conflict 2 of 2 ends
351
-
// var status types.AncestorCheckResponse
352
-
// forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt()), repoInfo.Name, currentRef, hiddenRef)
353
-
// if err != nil {
354
-
// log.Printf("failed to check if fork is ahead/behind: %s", err)
355
-
// return nil, err
356
-
// }
357
-
//
358
-
// if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil {
359
-
// log.Printf("failed to decode fork status: %s", err)
360
-
// return nil, err
361
-
// }
362
-
//
363
-
// forkInfo.Status = status.Status
364
-
// return &forkInfo, nil
365
-
// }
+35
-114
appview/repo/repo.go
+35
-114
appview/repo/repo.go
···
863
863
fail("Failed to write record to PDS.", err)
864
864
return
865
865
}
866
-
l = l.With("at-uri", resp.Uri)
866
+
867
+
aturi := resp.Uri
868
+
l = l.With("at-uri", aturi)
867
869
l.Info("wrote record to PDS")
868
870
869
-
l.Info("adding to knot")
870
-
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
871
+
tx, err := rp.db.BeginTx(r.Context(), nil)
871
872
if err != nil {
872
-
fail("Failed to add to knot.", err)
873
+
fail("Failed to add collaborator.", err)
873
874
return
874
875
}
875
876
876
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
877
-
if err != nil {
878
-
fail("Failed to add to knot.", err)
879
-
return
880
-
}
877
+
rollback := func() {
878
+
err1 := tx.Rollback()
879
+
err2 := rp.enforcer.E.LoadPolicy()
880
+
err3 := rollbackRecord(context.Background(), aturi, client)
881
881
882
-
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String())
883
-
if err != nil {
884
-
fail("Knot was unreachable.", err)
885
-
return
886
-
}
882
+
// ignore txn complete errors, this is okay
883
+
if errors.Is(err1, sql.ErrTxDone) {
884
+
err1 = nil
885
+
}
887
886
888
-
if ksResp.StatusCode != http.StatusNoContent {
889
-
fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)
890
-
return
887
+
if errs := errors.Join(err1, err2, err3); errs != nil {
888
+
l.Error("failed to rollback changes", "errs", errs)
889
+
return
890
+
}
891
891
}
892
-
893
-
tx, err := rp.db.BeginTx(r.Context(), nil)
894
-
if err != nil {
895
-
fail("Failed to add collaborator.", err)
896
-
return
897
-
}
898
-
defer func() {
899
-
tx.Rollback()
900
-
err = rp.enforcer.E.LoadPolicy()
901
-
if err != nil {
902
-
fail("Failed to add collaborator.", err)
903
-
}
904
-
}()
892
+
defer rollback()
905
893
906
894
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
907
895
if err != nil {
···
933
921
return
934
922
}
935
923
924
+
// clear aturi to when everything is successful
925
+
aturi = ""
926
+
936
927
rp.pages.HxRefresh(w)
937
928
}
938
929
939
930
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
940
931
user := rp.oauth.GetUser(r)
941
932
933
+
noticeId := "operation-error"
942
934
f, err := rp.repoResolver.Resolve(r)
943
935
if err != nil {
944
936
log.Println("failed to get repo and knot", err)
···
958
950
})
959
951
if err != nil {
960
952
log.Printf("failed to delete record: %s", err)
961
-
rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
953
+
rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.")
962
954
return
963
955
}
964
956
log.Println("removed repo record ", f.RepoAt().String())
···
980
972
&tangled.RepoDelete_Input{
981
973
Did: f.OwnerDid(),
982
974
Name: f.Name,
975
+
Rkey: f.Rkey,
983
976
},
984
977
)
985
-
if err != nil {
986
-
xe, parseErr := xrpcerr.Unmarshal(err.Error())
987
-
if parseErr != nil {
988
-
log.Printf("failed to delete repo from knot %s: %s", f.Knot, err)
989
-
} else {
990
-
log.Printf("failed to delete repo from knot %s: %s", f.Knot, xe.Error())
991
-
}
992
-
// Continue anyway since we want to clean up local state
993
-
} else {
994
-
log.Println("removed repo from knot ", f.Knot)
978
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
979
+
rp.pages.Notice(w, noticeId, err.Error())
980
+
return
995
981
}
982
+
log.Println("deleted repo from knot")
996
983
997
984
tx, err := rp.db.BeginTx(r.Context(), nil)
998
985
if err != nil {
···
1011
998
// remove collaborator RBAC
1012
999
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
1013
1000
if err != nil {
1014
-
rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
1001
+
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
1015
1002
return
1016
1003
}
1017
1004
for _, c := range repoCollaborators {
···
1023
1010
// remove repo RBAC
1024
1011
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
1025
1012
if err != nil {
1026
-
rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
1013
+
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
1027
1014
return
1028
1015
}
1029
1016
1030
1017
// remove repo from db
1031
1018
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
1032
1019
if err != nil {
1033
-
rp.pages.Notice(w, "settings-delete", "Failed to update appview")
1020
+
rp.pages.Notice(w, noticeId, "Failed to update appview")
1034
1021
return
1035
1022
}
1036
1023
log.Println("removed repo from db")
···
1059
1046
return
1060
1047
}
1061
1048
1049
+
noticeId := "operation-error"
1062
1050
branch := r.FormValue("branch")
1063
1051
if branch == "" {
1064
1052
http.Error(w, "malformed form", http.StatusBadRequest)
···
1091
1079
return
1092
1080
}
1093
1081
1094
-
w.Write(fmt.Append(nil, "default branch set to: ", branch))
1082
+
rp.pages.HxRefresh(w)
1095
1083
}
1096
1084
1097
1085
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
···
1207
1195
case "pipelines":
1208
1196
rp.pipelineSettings(w, r)
1209
1197
}
1210
-
1211
-
// user := rp.oauth.GetUser(r)
1212
-
// repoCollaborators, err := f.Collaborators(r.Context())
1213
-
// if err != nil {
1214
-
// log.Println("failed to get collaborators", err)
1215
-
// }
1216
-
1217
-
// isCollaboratorInviteAllowed := false
1218
-
// if user != nil {
1219
-
// ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
1220
-
// if err == nil && ok {
1221
-
// isCollaboratorInviteAllowed = true
1222
-
// }
1223
-
// }
1224
-
1225
-
// us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
1226
-
// if err != nil {
1227
-
// log.Println("failed to create unsigned client", err)
1228
-
// return
1229
-
// }
1230
-
1231
-
// result, err := us.Branches(f.OwnerDid(), f.Name)
1232
-
// if err != nil {
1233
-
// log.Println("failed to reach knotserver", err)
1234
-
// return
1235
-
// }
1236
-
1237
-
// // all spindles that this user is a member of
1238
-
// spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)
1239
-
// if err != nil {
1240
-
// log.Println("failed to fetch spindles", err)
1241
-
// return
1242
-
// }
1243
-
1244
-
// var secrets []*tangled.RepoListSecrets_Secret
1245
-
// if f.Spindle != "" {
1246
-
// if spindleClient, err := rp.oauth.ServiceClient(
1247
-
// r,
1248
-
// oauth.WithService(f.Spindle),
1249
-
// oauth.WithLxm(tangled.RepoListSecretsNSID),
1250
-
// oauth.WithDev(rp.config.Core.Dev),
1251
-
// ); err != nil {
1252
-
// log.Println("failed to create spindle client", err)
1253
-
// } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil {
1254
-
// log.Println("failed to fetch secrets", err)
1255
-
// } else {
1256
-
// secrets = resp.Secrets
1257
-
// }
1258
-
// }
1259
-
1260
-
// rp.pages.RepoSettings(w, pages.RepoSettingsParams{
1261
-
// LoggedInUser: user,
1262
-
// RepoInfo: f.RepoInfo(user),
1263
-
// Collaborators: repoCollaborators,
1264
-
// IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
1265
-
// Branches: result.Branches,
1266
-
// Spindles: spindles,
1267
-
// CurrentSpindle: f.Spindle,
1268
-
// Secrets: secrets,
1269
-
// })
1270
1198
}
1271
1199
1272
1200
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
···
1413
1341
Branch: ref,
1414
1342
},
1415
1343
)
1416
-
if err != nil {
1417
-
xe, parseErr := xrpcerr.Unmarshal(err.Error())
1418
-
if parseErr != nil {
1419
-
log.Printf("failed to sync repository fork: %s", err)
1420
-
rp.pages.Notice(w, "repo", "Failed to sync repository fork.")
1421
-
} else {
1422
-
log.Printf("failed to sync repository fork: %s", xe.Error())
1423
-
rp.pages.Notice(w, "repo", fmt.Sprintf("Failed to sync repository fork: %s", xe.Message))
1424
-
}
1344
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
1345
+
rp.pages.Notice(w, "repo", err.Error())
1425
1346
return
1426
1347
}
1427
1348
+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
+
}
+19
-19
docs/hacking.md
+19
-19
docs/hacking.md
···
55
55
quite cumbersome. So the nix flake provides a
56
56
`nixosConfiguration` to do so.
57
57
58
-
To begin, head to `http://localhost:3000/knots` in the browser
59
-
and create a knot with hostname `localhost:6000`. This will
60
-
generate a knot secret. Set `$TANGLED_VM_KNOT_SECRET` to it,
61
-
ideally in a `.envrc` with [direnv](https://direnv.net) so you
62
-
don't lose it.
58
+
To begin, grab your DID from http://localhost:3000/settings.
59
+
Then, set `TANGLED_VM_KNOT_OWNER` and
60
+
`TANGLED_VM_SPINDLE_OWNER` to your DID.
63
61
64
-
You will also need to set the `$TANGLED_VM_SPINDLE_OWNER`
65
-
variable to some value. If you don't want to [set up a
66
-
spindle](#running-a-spindle), you can use any placeholder
67
-
value.
62
+
If you don't want to [set up a spindle](#running-a-spindle),
63
+
you can use any placeholder value.
68
64
69
65
You can now start a lightweight NixOS VM like so:
70
66
···
75
71
```
76
72
77
73
This starts a knot on port 6000, a spindle on port 6555
78
-
with `ssh` exposed on port 2222. You can push repositories
79
-
to this VM with this ssh config block on your main machine:
74
+
with `ssh` exposed on port 2222.
75
+
76
+
Once the services are running, head to
77
+
http://localhost:3000/knots and hit verify (and similarly,
78
+
http://localhost:3000/spindles to verify your spindle). It
79
+
should verify the ownership of the services instantly if
80
+
everything went smoothly.
81
+
82
+
You can push repositories to this VM with this ssh config
83
+
block on your main machine:
80
84
81
85
```bash
82
86
Host nixos-shell
···
95
99
96
100
## running a spindle
97
101
98
-
You will need to find out your DID by entering your login handle into
99
-
<https://pdsls.dev/>. Set `$TANGLED_VM_SPINDLE_OWNER` to your DID.
100
-
101
-
The above VM should already be running a spindle on `localhost:6555`.
102
-
You can head to the spindle dashboard on `http://localhost:3000/spindles`,
103
-
and register a spindle with hostname `localhost:6555`. It should instantly
104
-
be verified. You can then configure each repository to use this spindle
105
-
and run CI jobs.
102
+
The above VM should already be running a spindle on
103
+
`localhost:6555`. Head to http://localhost:3000/spindles and
104
+
hit verify. You can then configure each repository to use
105
+
this spindle and run CI jobs.
106
106
107
107
Of interest when debugging spindles:
108
108
+7
-5
docs/knot-hosting.md
+7
-5
docs/knot-hosting.md
···
73
73
```
74
74
75
75
Create `/home/git/.knot.env` with the following, updating the values as
76
-
necessary. The `KNOT_SERVER_SECRET` can be obtained from the
77
-
[/knots](https://tangled.sh/knots) page on Tangled.
76
+
necessary. The `KNOT_SERVER_OWNER` should be set to your
77
+
DID, you can find your DID in the [Settings](https://tangled.sh/settings) page.
78
78
79
79
```
80
80
KNOT_REPO_SCAN_PATH=/home/git
81
81
KNOT_SERVER_HOSTNAME=knot.example.com
82
82
APPVIEW_ENDPOINT=https://tangled.sh
83
-
KNOT_SERVER_SECRET=secret
83
+
KNOT_SERVER_OWNER=did:plc:foobar
84
84
KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444
85
85
KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555
86
86
```
···
128
128
Remember to use Let's Encrypt or similar to procure a certificate for your
129
129
knot domain.
130
130
131
-
You should now have a running knot server! You can finalize your registration by hitting the
132
-
`initialize` button on the [/knots](https://tangled.sh/knots) page.
131
+
You should now have a running knot server! You can finalize
132
+
your registration by hitting the `verify` button on the
133
+
[/knots](https://tangled.sh/knots) page. This simply creates
134
+
a record on your PDS to announce the existence of the knot.
133
135
134
136
### custom paths
135
137
+39
docs/migrations/knot-1.7.0.md
+39
docs/migrations/knot-1.7.0.md
···
1
+
# Upgrading from v1.7.0
2
+
3
+
After v1.7.0, knot secrets have been deprecated. You no
4
+
longer need a secret from the appview to run a knot. All
5
+
authorized commands between services to knots are managed
6
+
via [Service
7
+
Auth](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
8
+
Knots will be read-only until upgraded.
9
+
10
+
Upgrading is quite easy, in essence:
11
+
12
+
- `KNOT_SERVER_SECRET` is no more, you can remove this
13
+
environment variable entirely
14
+
- `KNOT_SERVER_OWNER` is now required on boot, set this to
15
+
your DID. You can find your DID in the
16
+
[settings](https://tangled.sh/settings) page.
17
+
- Restart your knot once you have replace the environment
18
+
variable
19
+
- Head to the [knot dashboard](https://tangled.sh/knots) and
20
+
hit the "retry" button to verify your knot. This simply
21
+
writes a `sh.tangled.knot` record to your PDS.
22
+
23
+
## Nix
24
+
25
+
If you use the nix module, simply bump the flake to the
26
+
latest revision, and change your config block like so:
27
+
28
+
```diff
29
+
services.tangled-knot = {
30
+
enable = true;
31
+
server = {
32
+
- secretFile = /path/to/secret;
33
+
+ owner = "did:plc:foo";
34
+
.
35
+
.
36
+
.
37
+
};
38
+
};
39
+
```
-299
knotclient/signer.go
-299
knotclient/signer.go
···
1
-
package knotclient
2
-
3
-
import (
4
-
"bytes"
5
-
"crypto/hmac"
6
-
"crypto/sha256"
7
-
"encoding/hex"
8
-
"encoding/json"
9
-
"fmt"
10
-
"net/http"
11
-
"net/url"
12
-
"time"
13
-
14
-
"tangled.sh/tangled.sh/core/types"
15
-
)
16
-
17
-
type SignerTransport struct {
18
-
Secret string
19
-
}
20
-
21
-
func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) {
22
-
timestamp := time.Now().Format(time.RFC3339)
23
-
mac := hmac.New(sha256.New, []byte(s.Secret))
24
-
message := req.Method + req.URL.Path + timestamp
25
-
mac.Write([]byte(message))
26
-
signature := hex.EncodeToString(mac.Sum(nil))
27
-
req.Header.Set("X-Signature", signature)
28
-
req.Header.Set("X-Timestamp", timestamp)
29
-
return http.DefaultTransport.RoundTrip(req)
30
-
}
31
-
32
-
type SignedClient struct {
33
-
Secret string
34
-
Url *url.URL
35
-
client *http.Client
36
-
}
37
-
38
-
func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) {
39
-
client := &http.Client{
40
-
Timeout: 5 * time.Second,
41
-
Transport: SignerTransport{
42
-
Secret: secret,
43
-
},
44
-
}
45
-
46
-
scheme := "https"
47
-
if dev {
48
-
scheme = "http"
49
-
}
50
-
url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain))
51
-
if err != nil {
52
-
return nil, err
53
-
}
54
-
55
-
signedClient := &SignedClient{
56
-
Secret: secret,
57
-
client: client,
58
-
Url: url,
59
-
}
60
-
61
-
return signedClient, nil
62
-
}
63
-
64
-
func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) {
65
-
return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body))
66
-
}
67
-
68
-
func (s *SignedClient) Init(did string) (*http.Response, error) {
69
-
const (
70
-
Method = "POST"
71
-
Endpoint = "/init"
72
-
)
73
-
74
-
body, _ := json.Marshal(map[string]any{
75
-
"did": did,
76
-
})
77
-
78
-
req, err := s.newRequest(Method, Endpoint, body)
79
-
if err != nil {
80
-
return nil, err
81
-
}
82
-
83
-
return s.client.Do(req)
84
-
}
85
-
86
-
func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) {
87
-
const (
88
-
Method = "PUT"
89
-
Endpoint = "/repo/new"
90
-
)
91
-
92
-
body, _ := json.Marshal(map[string]any{
93
-
"did": did,
94
-
"name": repoName,
95
-
"default_branch": defaultBranch,
96
-
})
97
-
98
-
req, err := s.newRequest(Method, Endpoint, body)
99
-
if err != nil {
100
-
return nil, err
101
-
}
102
-
103
-
return s.client.Do(req)
104
-
}
105
-
106
-
func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) {
107
-
const (
108
-
Method = "GET"
109
-
)
110
-
endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch))
111
-
112
-
body, _ := json.Marshal(map[string]any{
113
-
"did": ownerDid,
114
-
"source": source,
115
-
"name": name,
116
-
"hiddenref": hiddenRef,
117
-
})
118
-
119
-
req, err := s.newRequest(Method, endpoint, body)
120
-
if err != nil {
121
-
return nil, err
122
-
}
123
-
124
-
return s.client.Do(req)
125
-
}
126
-
127
-
func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) {
128
-
const (
129
-
Method = "POST"
130
-
)
131
-
endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch))
132
-
133
-
body, _ := json.Marshal(map[string]any{
134
-
"did": ownerDid,
135
-
"source": source,
136
-
"name": name,
137
-
})
138
-
139
-
req, err := s.newRequest(Method, endpoint, body)
140
-
if err != nil {
141
-
return nil, err
142
-
}
143
-
144
-
return s.client.Do(req)
145
-
}
146
-
147
-
func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) {
148
-
const (
149
-
Method = "POST"
150
-
Endpoint = "/repo/fork"
151
-
)
152
-
153
-
body, _ := json.Marshal(map[string]any{
154
-
"did": ownerDid,
155
-
"source": source,
156
-
"name": name,
157
-
})
158
-
159
-
req, err := s.newRequest(Method, Endpoint, body)
160
-
if err != nil {
161
-
return nil, err
162
-
}
163
-
164
-
return s.client.Do(req)
165
-
}
166
-
167
-
func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) {
168
-
const (
169
-
Method = "DELETE"
170
-
Endpoint = "/repo"
171
-
)
172
-
173
-
body, _ := json.Marshal(map[string]any{
174
-
"did": did,
175
-
"name": repoName,
176
-
})
177
-
178
-
req, err := s.newRequest(Method, Endpoint, body)
179
-
if err != nil {
180
-
return nil, err
181
-
}
182
-
183
-
return s.client.Do(req)
184
-
}
185
-
186
-
func (s *SignedClient) AddMember(did string) (*http.Response, error) {
187
-
const (
188
-
Method = "PUT"
189
-
Endpoint = "/member/add"
190
-
)
191
-
192
-
body, _ := json.Marshal(map[string]any{
193
-
"did": did,
194
-
})
195
-
196
-
req, err := s.newRequest(Method, Endpoint, body)
197
-
if err != nil {
198
-
return nil, err
199
-
}
200
-
201
-
return s.client.Do(req)
202
-
}
203
-
204
-
func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) {
205
-
const (
206
-
Method = "PUT"
207
-
)
208
-
endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName)
209
-
210
-
body, _ := json.Marshal(map[string]any{
211
-
"branch": branch,
212
-
})
213
-
214
-
req, err := s.newRequest(Method, endpoint, body)
215
-
if err != nil {
216
-
return nil, err
217
-
}
218
-
219
-
return s.client.Do(req)
220
-
}
221
-
222
-
func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) {
223
-
const (
224
-
Method = "POST"
225
-
)
226
-
endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName)
227
-
228
-
body, _ := json.Marshal(map[string]any{
229
-
"did": memberDid,
230
-
})
231
-
232
-
req, err := s.newRequest(Method, endpoint, body)
233
-
if err != nil {
234
-
return nil, err
235
-
}
236
-
237
-
return s.client.Do(req)
238
-
}
239
-
240
-
func (s *SignedClient) Merge(
241
-
patch []byte,
242
-
ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string,
243
-
) (*http.Response, error) {
244
-
const (
245
-
Method = "POST"
246
-
)
247
-
endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo)
248
-
249
-
mr := types.MergeRequest{
250
-
Branch: branch,
251
-
CommitMessage: commitMessage,
252
-
CommitBody: commitBody,
253
-
AuthorName: authorName,
254
-
AuthorEmail: authorEmail,
255
-
Patch: string(patch),
256
-
}
257
-
258
-
body, _ := json.Marshal(mr)
259
-
260
-
req, err := s.newRequest(Method, endpoint, body)
261
-
if err != nil {
262
-
return nil, err
263
-
}
264
-
265
-
return s.client.Do(req)
266
-
}
267
-
268
-
func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
269
-
const (
270
-
Method = "POST"
271
-
)
272
-
endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
273
-
274
-
body, _ := json.Marshal(map[string]any{
275
-
"patch": string(patch),
276
-
"branch": branch,
277
-
})
278
-
279
-
req, err := s.newRequest(Method, endpoint, body)
280
-
if err != nil {
281
-
return nil, err
282
-
}
283
-
284
-
return s.client.Do(req)
285
-
}
286
-
287
-
func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) {
288
-
const (
289
-
Method = "POST"
290
-
)
291
-
endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch))
292
-
293
-
req, err := s.newRequest(Method, endpoint, nil)
294
-
if err != nil {
295
-
return nil, err
296
-
}
297
-
298
-
return s.client.Do(req)
299
-
}
-10
knotserver/http_util.go
-10
knotserver/http_util.go
···
20
20
func notFound(w http.ResponseWriter) {
21
21
writeError(w, "not found", http.StatusNotFound)
22
22
}
23
-
24
-
func writeMsg(w http.ResponseWriter, msg string) {
25
-
writeJSON(w, map[string]string{"msg": msg})
26
-
}
27
-
28
-
func writeConflict(w http.ResponseWriter, data interface{}) {
29
-
w.Header().Set("Content-Type", "application/json")
30
-
w.WriteHeader(http.StatusConflict)
31
-
json.NewEncoder(w).Encode(data)
32
-
}
+10
-6
knotserver/ingester.go
+10
-6
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
···
255
255
didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name)
256
256
257
257
// check perms for this user
258
-
if ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo); !ok || err != nil {
259
-
return fmt.Errorf("insufficient permissions: %w", err)
258
+
ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo)
259
+
if err != nil {
260
+
return fmt.Errorf("failed to check permissions: %w", err)
261
+
}
262
+
if !ok {
263
+
return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo)
260
264
}
261
265
262
266
if err := h.db.AddDid(subjectId.DID.String()); err != nil {
···
298
302
return fmt.Errorf("error reading response body: %w", err)
299
303
}
300
304
301
-
for _, key := range strings.Split(string(plaintext), "\n") {
305
+
for key := range strings.SplitSeq(string(plaintext), "\n") {
302
306
if key == "" {
303
307
continue
304
308
}
-2
knotserver/internal.go
-2
knotserver/internal.go
+93
-79
knotserver/xrpc/delete_repo.go
+93
-79
knotserver/xrpc/delete_repo.go
···
1
1
package xrpc
2
2
3
-
// import (
4
-
// "encoding/json"
5
-
// "fmt"
6
-
// "net/http"
7
-
// "os"
8
-
// "path/filepath"
9
-
//
10
-
// "github.com/bluesky-social/indigo/atproto/syntax"
11
-
// securejoin "github.com/cyphar/filepath-securejoin"
12
-
// "tangled.sh/tangled.sh/core/api/tangled"
13
-
// "tangled.sh/tangled.sh/core/rbac"
14
-
// xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
15
-
// )
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"os"
8
+
"path/filepath"
16
9
17
-
// func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) {
18
-
// l := x.Logger.With("handler", "DeleteRepo")
19
-
// fail := func(e xrpcerr.XrpcError) {
20
-
// l.Error("failed", "kind", e.Tag, "error", e.Message)
21
-
// writeError(w, e, http.StatusBadRequest)
22
-
// }
23
-
//
24
-
// actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
25
-
// if !ok {
26
-
// fail(xrpcerr.MissingActorDidError)
27
-
// return
28
-
// }
29
-
//
30
-
// isMember, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer)
31
-
// if err != nil {
32
-
// fail(xrpcerr.GenericError(err))
33
-
// return
34
-
// }
35
-
// if !isMember {
36
-
// fail(xrpcerr.AccessControlError(actorDid.String()))
37
-
// return
38
-
// }
39
-
//
40
-
// var data tangled.RepoDelete_Input
41
-
// if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
42
-
// fail(xrpcerr.GenericError(err))
43
-
// return
44
-
// }
45
-
//
46
-
// did := data.Did
47
-
// name := data.Name
48
-
//
49
-
// if did == "" || name == "" {
50
-
// fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
51
-
// return
52
-
// }
53
-
//
54
-
// relativeRepoPath := filepath.Join(did, name)
55
-
// if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {
56
-
// l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)
57
-
// writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
58
-
// return
59
-
// }
60
-
//
61
-
// repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
62
-
// if err != nil {
63
-
// fail(xrpcerr.GenericError(err))
64
-
// return
65
-
// }
66
-
//
67
-
// err = os.RemoveAll(repoPath)
68
-
// if err != nil {
69
-
// l.Error("deleting repo", "error", err.Error())
70
-
// writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
71
-
// return
72
-
// }
73
-
//
74
-
// err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath)
75
-
// if err != nil {
76
-
// l.Error("failed to delete repo from enforcer", "error", err.Error())
77
-
// writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
78
-
// return
79
-
// }
80
-
//
81
-
// w.WriteHeader(http.StatusOK)
82
-
// }
10
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
12
+
"github.com/bluesky-social/indigo/xrpc"
13
+
securejoin "github.com/cyphar/filepath-securejoin"
14
+
"tangled.sh/tangled.sh/core/api/tangled"
15
+
"tangled.sh/tangled.sh/core/rbac"
16
+
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
17
+
)
18
+
19
+
func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) {
20
+
l := x.Logger.With("handler", "DeleteRepo")
21
+
fail := func(e xrpcerr.XrpcError) {
22
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
23
+
writeError(w, e, http.StatusBadRequest)
24
+
}
25
+
26
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
27
+
if !ok {
28
+
fail(xrpcerr.MissingActorDidError)
29
+
return
30
+
}
31
+
32
+
var data tangled.RepoDelete_Input
33
+
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
34
+
fail(xrpcerr.GenericError(err))
35
+
return
36
+
}
37
+
38
+
did := data.Did
39
+
name := data.Name
40
+
rkey := data.Rkey
41
+
42
+
if did == "" || name == "" {
43
+
fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
44
+
return
45
+
}
46
+
47
+
ident, err := x.Resolver.ResolveIdent(r.Context(), actorDid.String())
48
+
if err != nil || ident.Handle.IsInvalidHandle() {
49
+
fail(xrpcerr.GenericError(err))
50
+
return
51
+
}
52
+
53
+
xrpcc := xrpc.Client{
54
+
Host: ident.PDSEndpoint(),
55
+
}
56
+
57
+
// ensure that the record does not exists
58
+
_, err = comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey)
59
+
if err == nil {
60
+
fail(xrpcerr.RecordExistsError(rkey))
61
+
return
62
+
}
63
+
64
+
relativeRepoPath := filepath.Join(did, name)
65
+
isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath)
66
+
if err != nil {
67
+
fail(xrpcerr.GenericError(err))
68
+
return
69
+
}
70
+
if !isDeleteAllowed {
71
+
fail(xrpcerr.AccessControlError(actorDid.String()))
72
+
return
73
+
}
74
+
75
+
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
76
+
if err != nil {
77
+
fail(xrpcerr.GenericError(err))
78
+
return
79
+
}
80
+
81
+
err = os.RemoveAll(repoPath)
82
+
if err != nil {
83
+
l.Error("deleting repo", "error", err.Error())
84
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
85
+
return
86
+
}
87
+
88
+
err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath)
89
+
if err != nil {
90
+
l.Error("failed to delete repo from enforcer", "error", err.Error())
91
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
92
+
return
93
+
}
94
+
95
+
w.WriteHeader(http.StatusOK)
96
+
}
+1
knotserver/xrpc/xrpc.go
+1
knotserver/xrpc/xrpc.go
···
37
37
38
38
r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
39
39
r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)
40
+
r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)
40
41
r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)
41
42
r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync)
42
43
r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
+5
-1
lexicons/repo/delete.json
+5
-1
lexicons/repo/delete.json
···
9
9
"encoding": "application/json",
10
10
"schema": {
11
11
"type": "object",
12
-
"required": ["did", "name"],
12
+
"required": ["did", "name", "rkey"],
13
13
"properties": {
14
14
"did": {
15
15
"type": "string",
···
19
19
"name": {
20
20
"type": "string",
21
21
"description": "Name of the repository to delete"
22
+
},
23
+
"rkey": {
24
+
"type": "string",
25
+
"description": "Rkey of the repository record"
22
26
}
23
27
}
24
28
}
-7
nix/modules/knot.nix
-7
nix/modules/knot.nix
···
99
99
description = "DID of owner (required)";
100
100
};
101
101
102
-
secretFile = mkOption {
103
-
type = lib.types.path;
104
-
example = "KNOT_SERVER_SECRET=<hash>";
105
-
description = "File containing secret key provided by appview (required)";
106
-
};
107
-
108
102
dbPath = mkOption {
109
103
type = types.path;
110
104
default = "${cfg.stateDir}/knotserver.db";
···
207
201
"KNOT_SERVER_HOSTNAME=${cfg.server.hostname}"
208
202
"KNOT_SERVER_OWNER=${cfg.server.owner}"
209
203
];
210
-
EnvironmentFile = cfg.server.secretFile;
211
204
ExecStart = "${cfg.package}/bin/knot server";
212
205
Restart = "always";
213
206
};
+2
-2
rbac/rbac.go
+2
-2
rbac/rbac.go
···
281
281
return e.E.Enforce(user, domain, domain, "repo:create")
282
282
}
283
283
284
-
func (e *Enforcer) IsRepoDeleteAllowed(user, domain string) (bool, error) {
285
-
return e.E.Enforce(user, domain, domain, "repo:delete")
284
+
func (e *Enforcer) IsRepoDeleteAllowed(user, domain, repo string) (bool, error) {
285
+
return e.E.Enforce(user, domain, repo, "repo:delete")
286
286
}
287
287
288
288
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
+7
xrpc/errors/errors.go
+7
xrpc/errors/errors.go
···
86
86
)
87
87
}
88
88
89
+
var RecordExistsError = func(r string) XrpcError {
90
+
return NewXrpcError(
91
+
WithTag("RecordExists"),
92
+
WithError(fmt.Errorf("repo already exists: %s", r)),
93
+
)
94
+
}
95
+
89
96
func GenericError(err error) XrpcError {
90
97
return NewXrpcError(
91
98
WithTag("Generic"),