···
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/api/tangled"
13
+
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
16
+
"tangled.sh/tangled.sh/core/appview/idresolver"
"tangled.sh/tangled.sh/core/appview/middleware"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
20
+
verify "tangled.sh/tangled.sh/core/appview/spindleverify"
"tangled.sh/tangled.sh/core/rbac"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
31
-
Config *config.Config
32
-
Enforcer *rbac.Enforcer
32
+
Config *config.Config
33
+
Enforcer *rbac.Enforcer
34
+
IdResolver *idresolver.Resolver
func (s *Spindles) Router() http.Handler {
39
-
r.Use(middleware.AuthMiddleware(s.OAuth))
41
+
r.With(middleware.AuthMiddleware(s.OAuth)).Get("/", s.spindles)
42
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/register", s.register)
41
-
r.Get("/", s.spindles)
42
-
r.Post("/register", s.register)
43
-
r.Delete("/{instance}", s.delete)
44
-
r.Post("/{instance}/retry", s.retry)
44
+
r.With(middleware.AuthMiddleware(s.OAuth)).Get("/{instance}", s.dashboard)
45
+
r.With(middleware.AuthMiddleware(s.OAuth)).Delete("/{instance}", s.delete)
47
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/retry", s.retry)
48
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/add", s.addMember)
49
+
r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/remove", s.removeMember)
···
72
+
func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) {
73
+
l := s.Logger.With("handler", "dashboard")
75
+
user := s.OAuth.GetUser(r)
76
+
l = l.With("user", user.Did)
78
+
instance := chi.URLParam(r, "instance")
82
+
l = l.With("instance", instance)
84
+
spindles, err := db.GetSpindles(
86
+
db.FilterEq("instance", instance),
87
+
db.FilterEq("owner", user.Did),
88
+
db.FilterIsNot("verified", "null"),
90
+
if err != nil || len(spindles) != 1 {
91
+
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
92
+
http.Error(w, "Not found", http.StatusNotFound)
96
+
spindle := spindles[0]
97
+
members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance)
99
+
l.Error("failed to get spindle members", "err", err)
100
+
http.Error(w, "Not found", http.StatusInternalServerError)
103
+
slices.Sort(members)
105
+
repos, err := db.GetRepos(
107
+
db.FilterEq("spindle", instance),
110
+
l.Error("failed to get spindle repos", "err", err)
111
+
http.Error(w, "Not found", http.StatusInternalServerError)
115
+
identsToResolve := make([]string, len(members))
116
+
for i, member := range members {
117
+
identsToResolve[i] = member
119
+
resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve)
120
+
didHandleMap := make(map[string]string)
121
+
for _, identity := range resolvedIds {
122
+
if !identity.Handle.IsInvalidHandle() {
123
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
125
+
didHandleMap[identity.DID.String()] = identity.DID.String()
129
+
// organize repos by did
130
+
repoMap := make(map[string][]db.Repo)
131
+
for _, r := range repos {
132
+
repoMap[r.Did] = append(repoMap[r.Did], r)
135
+
s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{
136
+
LoggedInUser: user,
140
+
DidHandleMap: didHandleMap,
// this endpoint inserts a record on behalf of the user to register that domain
// when registered, it also makes a request to see if the spindle declares this users as its owner,
···
s.Pages.Notice(w, noticeId, "Incomplete form.")
165
+
l = l.With("instance", instance)
166
+
l = l.With("user", user.Did)
···
176
+
s.Enforcer.E.LoadPolicy()
err = db.AddSpindle(tx, db.Spindle{
Owner: syntax.DID(user.Did),
···
189
+
err = s.Enforcer.AddSpindle(instance)
191
+
l.Error("failed to create spindle", "err", err)
client, err := s.OAuth.AuthorizedClient(r)
···
147
-
// begin verification
148
-
expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev)
150
-
l.Error("verification failed", "err", err)
152
-
// just refresh the page
153
-
s.Pages.HxRefresh(w)
157
-
if expectedOwner != user.Did {
158
-
// verification failed
159
-
l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did)
160
-
s.Pages.HxRefresh(w)
164
-
tx, err = s.Db.Begin()
166
-
l.Error("failed to commit verification info", "err", err)
167
-
s.Pages.HxRefresh(w)
172
-
s.Enforcer.E.LoadPolicy()
175
-
// mark this spindle as verified in the db
176
-
_, err = db.VerifySpindle(
178
-
db.FilterEq("owner", user.Did),
179
-
db.FilterEq("instance", instance),
182
-
err = s.Enforcer.AddSpindleOwner(instance, user.Did)
236
+
err = s.Enforcer.E.SavePolicy()
l.Error("failed to update ACL", "err", err)
243
+
// begin verification
244
+
err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
191
-
l.Error("failed to commit verification info", "err", err)
246
+
l.Error("verification failed", "err", err)
196
-
err = s.Enforcer.E.SavePolicy()
251
+
_, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did)
198
-
l.Error("failed to update ACL", "err", err)
253
+
l.Error("failed to mark verified", "err", err)
···
func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) {
user := s.OAuth.GetUser(r)
210
-
l := s.Logger.With("handler", "register")
265
+
l := s.Logger.With("handler", "delete")
noticeId := "operation-error"
defaultErr := "Failed to delete spindle. Try again later."
···
280
+
spindles, err := db.GetSpindles(
282
+
db.FilterEq("owner", user.Did),
283
+
db.FilterEq("instance", instance),
285
+
if err != nil || len(spindles) != 1 {
286
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
291
+
if string(spindles[0].Owner) != user.Did {
292
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
293
+
s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.")
l.Error("failed to start txn", "err", err)
231
-
defer tx.Rollback()
305
+
s.Enforcer.E.LoadPolicy()
···
l.Error("failed to delete spindle", "err", err)
319
+
err = s.Enforcer.RemoveSpindle(instance)
321
+
l.Error("failed to update ACL", "err", err)
···
350
+
err = s.Enforcer.E.SavePolicy()
352
+
l.Error("failed to update ACL", "err", err)
353
+
s.Pages.HxRefresh(w)
357
+
shouldRedirect := r.Header.Get("shouldRedirect")
358
+
if shouldRedirect == "true" {
359
+
s.Pages.HxRedirect(w, "/spindles")
func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) {
user := s.OAuth.GetUser(r)
273
-
l := s.Logger.With("handler", "register")
368
+
l := s.Logger.With("handler", "retry")
noticeId := "operation-error"
defaultErr := "Failed to verify spindle. Try again later."
···
382
+
l = l.With("instance", instance)
383
+
l = l.With("user", user.Did)
385
+
spindles, err := db.GetSpindles(
387
+
db.FilterEq("owner", user.Did),
388
+
db.FilterEq("instance", instance),
390
+
if err != nil || len(spindles) != 1 {
391
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
396
+
if string(spindles[0].Owner) != user.Did {
397
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
398
+
s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.")
289
-
expectedOwner, err := fetchOwner(r.Context(), instance, s.Config.Core.Dev)
403
+
err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
l.Error("verification failed", "err", err)
407
+
if errors.Is(err, verify.FetchError) {
408
+
s.Pages.Notice(w, noticeId, err.Error())
412
+
if e, ok := err.(*verify.OwnerMismatch); ok {
413
+
s.Pages.Notice(w, noticeId, e.Error())
296
-
if expectedOwner != user.Did {
297
-
l.Error("verification failed", "expectedOwner", expectedOwner, "observedOwner", user.Did)
298
-
s.Pages.Notice(w, noticeId, fmt.Sprintf("Owner did not match, expected %s, got %s", expectedOwner, user.Did))
421
+
rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did)
423
+
l.Error("failed to mark verified", "err", err)
424
+
s.Pages.Notice(w, noticeId, err.Error())
302
-
// mark this spindle as verified in the db
303
-
rowId, err := db.VerifySpindle(
428
+
verifiedSpindle, err := db.GetSpindles(
430
+
db.FilterEq("id", rowId),
432
+
if err != nil || len(verifiedSpindle) != 1 {
433
+
l.Error("failed get new spindle", "err", err)
434
+
s.Pages.HxRefresh(w)
438
+
shouldRefresh := r.Header.Get("shouldRefresh")
439
+
if shouldRefresh == "true" {
440
+
s.Pages.HxRefresh(w)
444
+
w.Header().Set("HX-Reswap", "outerHTML")
445
+
s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]})
448
+
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
449
+
user := s.OAuth.GetUser(r)
450
+
l := s.Logger.With("handler", "addMember")
452
+
instance := chi.URLParam(r, "instance")
453
+
if instance == "" {
454
+
l.Error("empty instance")
455
+
http.Error(w, "Not found", http.StatusNotFound)
458
+
l = l.With("instance", instance)
459
+
l = l.With("user", user.Did)
461
+
spindles, err := db.GetSpindles(
db.FilterEq("owner", user.Did),
db.FilterEq("instance", instance),
466
+
if err != nil || len(spindles) != 1 {
467
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
468
+
http.Error(w, "Not found", http.StatusNotFound)
472
+
noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id)
473
+
defaultErr := "Failed to add member. Try again later."
475
+
s.Pages.Notice(w, noticeId, defaultErr)
478
+
if string(spindles[0].Owner) != user.Did {
479
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
480
+
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
484
+
member := r.FormValue("member")
486
+
l.Error("empty member")
487
+
s.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
490
+
l = l.With("member", member)
492
+
memberId, err := s.IdResolver.ResolveIdent(r.Context(), member)
494
+
l.Error("failed to resolve member identity to handle", "err", err)
495
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
498
+
if memberId.Handle.IsInvalidHandle() {
499
+
l.Error("failed to resolve member identity to handle")
500
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
505
+
client, err := s.OAuth.AuthorizedClient(r)
507
+
l.Error("failed to authorize client", "err", err)
512
+
tx, err := s.Db.Begin()
309
-
l.Error("verification failed", "err", err)
514
+
l.Error("failed to start txn", "err", err)
520
+
s.Enforcer.E.LoadPolicy()
314
-
verifiedSpindle := db.Spindle{
316
-
Owner: syntax.DID(user.Did),
523
+
rkey := appview.TID()
525
+
// add member to db
526
+
if err = db.AddSpindleMember(tx, db.SpindleMember{
527
+
Did: syntax.DID(user.Did),
530
+
Subject: memberId.DID,
532
+
l.Error("failed to add spindle member", "err", err)
320
-
w.Header().Set("HX-Reswap", "outerHTML")
321
-
s.Pages.SpindleListing(w, pages.SpindleListingParams{
322
-
LoggedInUser: user,
323
-
Spindle: verifiedSpindle,
537
+
if err = s.Enforcer.AddSpindleMember(instance, memberId.DID.String()); err != nil {
538
+
l.Error("failed to add member to ACLs")
543
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
544
+
Collection: tangled.SpindleMemberNSID,
547
+
Record: &lexutil.LexiconTypeDecoder{
548
+
Val: &tangled.SpindleMember{
549
+
CreatedAt: time.Now().Format(time.RFC3339),
550
+
Instance: instance,
551
+
Subject: memberId.DID.String(),
556
+
l.Error("failed to add record to PDS", "err", err)
557
+
s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.")
561
+
if err = tx.Commit(); err != nil {
562
+
l.Error("failed to commit txn", "err", err)
567
+
if err = s.Enforcer.E.SavePolicy(); err != nil {
568
+
l.Error("failed to add member to ACLs", "err", err)
574
+
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
327
-
func fetchOwner(ctx context.Context, domain string, dev bool) (string, error) {
577
+
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
578
+
user := s.OAuth.GetUser(r)
579
+
l := s.Logger.With("handler", "removeMember")
581
+
noticeId := "operation-error"
582
+
defaultErr := "Failed to add member. Try again later."
584
+
s.Pages.Notice(w, noticeId, defaultErr)
333
-
url := fmt.Sprintf("%s://%s/owner", scheme, domain)
334
-
req, err := http.NewRequest("GET", url, nil)
587
+
instance := chi.URLParam(r, "instance")
588
+
if instance == "" {
589
+
l.Error("empty instance")
593
+
l = l.With("instance", instance)
594
+
l = l.With("user", user.Did)
596
+
spindles, err := db.GetSpindles(
598
+
db.FilterEq("owner", user.Did),
599
+
db.FilterEq("instance", instance),
601
+
if err != nil || len(spindles) != 1 {
602
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
607
+
if string(spindles[0].Owner) != user.Did {
608
+
l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner)
609
+
s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
613
+
member := r.FormValue("member")
615
+
l.Error("empty member")
616
+
s.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
619
+
l = l.With("member", member)
621
+
memberId, err := s.IdResolver.ResolveIdent(r.Context(), member)
623
+
l.Error("failed to resolve member identity to handle", "err", err)
624
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
627
+
if memberId.Handle.IsInvalidHandle() {
628
+
l.Error("failed to resolve member identity to handle")
629
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
633
+
tx, err := s.Db.Begin()
635
+
l.Error("failed to start txn", "err", err)
641
+
s.Enforcer.E.LoadPolicy()
644
+
// get the record from the DB first:
645
+
members, err := db.GetSpindleMembers(
647
+
db.FilterEq("did", user.Did),
648
+
db.FilterEq("instance", instance),
649
+
db.FilterEq("subject", memberId.DID),
651
+
if err != nil || len(members) != 1 {
652
+
l.Error("failed to get member", "err", err)
339
-
client := &http.Client{
340
-
Timeout: 1 * time.Second,
658
+
if err = db.RemoveSpindleMember(
660
+
db.FilterEq("did", user.Did),
661
+
db.FilterEq("instance", instance),
662
+
db.FilterEq("subject", memberId.DID),
664
+
l.Error("failed to remove spindle member", "err", err)
343
-
resp, err := client.Do(req.WithContext(ctx))
344
-
if err != nil || resp.StatusCode != 200 {
345
-
return "", errors.New("failed to fetch /owner")
669
+
// remove from enforcer
670
+
if err = s.Enforcer.RemoveSpindleMember(instance, memberId.DID.String()); err != nil {
671
+
l.Error("failed to update ACLs", "err", err)
348
-
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024)) // read atmost 1kb of data
676
+
client, err := s.OAuth.AuthorizedClient(r)
350
-
return "", fmt.Errorf("failed to read /owner response: %w", err)
678
+
l.Error("failed to authorize client", "err", err)
353
-
did := strings.TrimSpace(string(body))
355
-
return "", errors.New("empty DID in /owner response")
684
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
685
+
Collection: tangled.SpindleMemberNSID,
687
+
Rkey: members[0].Rkey,
691
+
l.Error("failed to delete record", "err", err)
694
+
// commit everything
695
+
if err = tx.Commit(); err != nil {
696
+
l.Error("failed to commit txn", "err", err)
701
+
// commit everything
702
+
if err = s.Enforcer.E.SavePolicy(); err != nil {
703
+
l.Error("failed to save ACLs", "err", err)
709
+
s.Pages.HxRefresh(w)