From 83f1fc8c7c9bb6f5b4f6682ce43cdcb3623c425a Mon Sep 17 00:00:00 2001 From: Seongmin Lee Date: Thu, 23 Jul 2026 01:26:07 +0900 Subject: [PATCH] wip: appview: deprecate spindle dashboard Signed-off-by: Seongmin Lee --- appview/ingester.go | 1 - appview/oauth/handler.go | 43 -- appview/pages/templates/spindles/index.html | 75 +-- appview/spindles/spindles.go | 560 -------------------- 4 files changed, 9 insertions(+), 670 deletions(-) diff --git a/appview/ingester.go b/appview/ingester.go index cf2d6037..213dff89 100644 --- a/appview/ingester.go +++ b/appview/ingester.go @@ -30,7 +30,6 @@ import ( "tangled.org/core/appview/mentions" "tangled.org/core/appview/models" "tangled.org/core/appview/notify" - "tangled.org/core/appview/serververify" "tangled.org/core/consts" "tangled.org/core/idresolver" "tangled.org/core/orm" diff --git a/appview/oauth/handler.go b/appview/oauth/handler.go index 04f2a77f..578ac5b1 100644 --- a/appview/oauth/handler.go +++ b/appview/oauth/handler.go @@ -119,7 +119,6 @@ func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { o.ensureProfileRecord(sessData.AccountDID, sessData.SessionID) go o.addToDefaultKnot(sessData.AccountDID) - go o.addToDefaultSpindle(sessData.AccountDID.String()) go o.autoClaimTnglShDomain(sessData.AccountDID.String()) if !o.Config.Core.Dev { @@ -167,48 +166,6 @@ func (o *OAuth) isAccountDeactivated(sessData *oauth.ClientSessionData) bool { xrpcBody.ErrStr == "RepoDeactivated" } -func (o *OAuth) addToDefaultSpindle(did string) { - l := o.Logger.With("subject", did) - - // use the tangled.sh app password to get an accessJwt - // and create an sh.tangled.spindle.member record with that - spindleMembers, err := db.GetSpindleMembers( - o.Db, - orm.FilterEq("instance", "spindle.tangled.sh"), - orm.FilterEq("subject", did), - ) - if err != nil { - l.Error("failed to get spindle members", "err", err) - return - } - - if len(spindleMembers) != 0 { - l.Warn("already a member of the default spindle") - return - } - - l.Debug("adding to default spindle") - session, err := o.getAppPasswordSession() - if err != nil { - l.Error("failed to create session", "err", err) - return - } - - record := tangled.SpindleMember{ - LexiconTypeID: tangled.SpindleMemberNSID, - Subject: did, - Instance: consts.DefaultSpindle, - CreatedAt: time.Now().Format(time.RFC3339), - } - - if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { - l.Error("failed to add to default spindle", "err", err) - return - } - - l.Debug("successfully added to default spindle", "did", did) -} - type onboardAction int const ( diff --git a/appview/pages/templates/spindles/index.html b/appview/pages/templates/spindles/index.html index 9f1f9d88..a302ef27 100644 --- a/appview/pages/templates/spindles/index.html +++ b/appview/pages/templates/spindles/index.html @@ -20,29 +20,26 @@

Spindle

- {{ block "about" . }} {{ end }} +
+

+ Spindles are small CI runners. +

+
- {{ template "docsButton" . }} + + {{ i "book" "size-4" }} + Docs +
- {{ block "list" . }} {{ end }} - {{ block "register" . }} {{ end }}
{{ end }} -{{ define "about" }} -
-

- Spindles are small CI runners. -

-
-{{ end }} - {{ define "list" }}

Your spindles

@@ -58,57 +55,3 @@
{{ end }} - -{{ define "register" }} -
-

Register a spindle

-

Enter the hostname of your spindle to get started.

-
-
- - -
- -
-
- -
-{{ end }} - -{{ define "docsButton" }} - -{{ i "book" "size-4" }} -Docs - -
-
-{{ end }} diff --git a/appview/spindles/spindles.go b/appview/spindles/spindles.go index 6840fb39..e842beff 100644 --- a/appview/spindles/spindles.go +++ b/appview/spindles/spindles.go @@ -1,32 +1,17 @@ package spindles import ( - "errors" - "fmt" "log/slog" "net/http" - "slices" - "strings" - "time" "github.com/go-chi/chi/v5" - "tangled.org/core/api/tangled" "tangled.org/core/appview/config" "tangled.org/core/appview/db" "tangled.org/core/appview/middleware" - "tangled.org/core/appview/models" "tangled.org/core/appview/oauth" "tangled.org/core/appview/pages" - "tangled.org/core/appview/serververify" "tangled.org/core/idresolver" - "tangled.org/core/orm" "tangled.org/core/rbac" - "tangled.org/core/tid" - "tangled.org/core/xrpc/xrpcclient" - - comatproto "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/atproto/syntax" - lexutil "github.com/bluesky-social/indigo/lex/util" ) type Spindles struct { @@ -43,558 +28,13 @@ func (s *Spindles) Router() http.Handler { r := chi.NewRouter() r.With(middleware.AuthMiddleware(s.OAuth)).Get("/", s.spindles) - r.With(middleware.AuthMiddleware(s.OAuth)).Post("/register", s.register) - - r.With(middleware.AuthMiddleware(s.OAuth)).Get("/{instance}", s.dashboard) - r.With(middleware.AuthMiddleware(s.OAuth)).Delete("/{instance}", s.delete) - - r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/retry", s.retry) - r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/add", s.addMember) - r.With(middleware.AuthMiddleware(s.OAuth)).Post("/{instance}/remove", s.removeMember) return r } func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) { - user := s.OAuth.GetMultiAccountUser(r) - all, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("owner", user.Did), - ) - if err != nil { - s.Logger.Error("failed to fetch spindles", "err", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - s.Pages.Spindles(w, pages.SpindlesParams{ BaseParams: pages.BaseParamsFromContext(r.Context()), - Spindles: all, - Tab: "spindles", - }) -} - -func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { - l := s.Logger.With("handler", "dashboard") - - user := s.OAuth.GetMultiAccountUser(r) - l = l.With("user", user.Did) - - instance := chi.URLParam(r, "instance") - if instance == "" { - return - } - l = l.With("instance", instance) - - spindles, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("instance", instance), - orm.FilterEq("owner", user.Did), - orm.FilterIsNot("verified", "null"), - ) - if err != nil || len(spindles) != 1 { - l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles)) - http.Error(w, "Not found", http.StatusNotFound) - return - } - - spindle := spindles[0] - members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance) - if err != nil { - l.Error("failed to get spindle members", "err", err) - http.Error(w, "Not found", http.StatusInternalServerError) - return - } - slices.Sort(members) - - repos, err := db.GetRepos( - s.Db, - orm.FilterEq("spindle", instance), - ) - if err != nil { - l.Error("failed to get spindle repos", "err", err) - http.Error(w, "Not found", http.StatusInternalServerError) - return - } - - // organize repos by did - repoMap := make(map[string][]models.Repo) - for _, r := range repos { - repoMap[r.Did] = append(repoMap[r.Did], r) - } - - s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{ - BaseParams: pages.BaseParamsFromContext(r.Context()), - Spindle: spindle, - Members: members, - Repos: repoMap, Tab: "spindles", }) } - -// 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, -// and if so, marks the spindle as verified. -// -// if the spindle is not up yet, the user is free to retry verification at a later point -func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { - user := s.OAuth.GetMultiAccountUser(r) - l := s.Logger.With("handler", "register") - - noticeId := "register-error" - defaultErr := "Failed to register spindle. Try again later." - fail := func() { - s.Pages.Notice(w, noticeId, defaultErr) - } - - instance := r.FormValue("instance") - // Strip protocol, trailing slashes, and whitespace - // Rkey cannot contain slashes - instance = strings.TrimSpace(instance) - instance = strings.TrimPrefix(instance, "https://") - instance = strings.TrimPrefix(instance, "http://") - instance = strings.TrimSuffix(instance, "/") - if instance == "" { - s.Pages.Notice(w, noticeId, "Incomplete form.") - return - } - l = l.With("instance", instance) - l = l.With("user", user.Did) - - tx, err := s.Db.Begin() - if err != nil { - l.Error("failed to start transaction", "err", err) - fail() - return - } - defer tx.Rollback() - - if err := db.AddSpindle(tx, models.Spindle{ - Owner: syntax.DID(user.Did), - Instance: instance, - }); err != nil { - l.Error("failed to insert", "err", err) - fail() - return - } - - client, err := s.OAuth.AuthorizedClient(r) - if err != nil { - l.Error("failed to authorize client", "err", err) - fail() - return - } - - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance) - var exCid *string - if ex != nil { - exCid = ex.Cid - } - - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.SpindleNSID, - Repo: user.Did, - Rkey: instance, - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.Spindle{ - CreatedAt: time.Now().Format(time.RFC3339), - }, - }, - SwapRecord: exCid, - }) - if err != nil { - l.Error("failed to put record", "err", err) - fail() - return - } - - if err := tx.Commit(); err != nil { - l.Error("failed to commit transaction", "err", err) - fail() - return - } - - s.Pages.HxRefresh(w) -} - -func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { - user := s.OAuth.GetMultiAccountUser(r) - l := s.Logger.With("handler", "delete") - - noticeId := "operation-error" - defaultErr := "Failed to delete spindle. Try again later." - fail := func() { - s.Pages.Notice(w, noticeId, defaultErr) - } - - instance := chi.URLParam(r, "instance") - if instance == "" { - l.Error("empty instance") - fail() - return - } - - spindles, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("owner", user.Did), - orm.FilterEq("instance", instance), - ) - if err != nil || len(spindles) != 1 { - l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) - fail() - return - } - - if string(spindles[0].Owner) != user.Did { - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) - s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") - return - } - - tx, err := s.Db.Begin() - if err != nil { - l.Error("failed to start txn", "err", err) - fail() - return - } - defer func() { - tx.Rollback() - s.Enforcer.E.LoadPolicy() - }() - - // remove spindle members first - err = db.RemoveSpindleMember( - tx, - orm.FilterEq("did", user.Did), - orm.FilterEq("instance", instance), - ) - if err != nil { - l.Error("failed to remove spindle members", "err", err) - fail() - return - } - - err = db.DeleteSpindle( - tx, - orm.FilterEq("owner", user.Did), - orm.FilterEq("instance", instance), - ) - if err != nil { - l.Error("failed to delete spindle", "err", err) - fail() - return - } - - // delete from enforcer - if spindles[0].Verified != nil { - err = s.Enforcer.RemoveSpindle(instance) - if err != nil { - l.Error("failed to update ACL", "err", err) - fail() - return - } - } - - client, err := s.OAuth.AuthorizedClient(r) - if err != nil { - l.Error("failed to authorize client", "err", err) - fail() - return - } - - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ - Collection: tangled.SpindleNSID, - Repo: user.Did, - Rkey: instance, - }) - if err != nil { - // non-fatal - l.Error("failed to delete record", "err", err) - } - - err = tx.Commit() - if err != nil { - l.Error("failed to delete spindle", "err", err) - fail() - return - } - - err = s.Enforcer.E.SavePolicy() - if err != nil { - l.Error("failed to update ACL", "err", err) - s.Pages.HxRefresh(w) - return - } - - shouldRedirect := r.Header.Get("shouldRedirect") - if shouldRedirect == "true" { - s.Pages.HxRedirect(w, "/settings/spindles") - return - } - - w.Write([]byte{}) -} - -func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { - user := s.OAuth.GetMultiAccountUser(r) - l := s.Logger.With("handler", "retry") - - noticeId := "operation-error" - defaultErr := "Failed to verify spindle. Try again later." - fail := func() { - s.Pages.Notice(w, noticeId, defaultErr) - } - - instance := chi.URLParam(r, "instance") - if instance == "" { - l.Error("empty instance") - fail() - return - } - l = l.With("instance", instance) - l = l.With("user", user.Did) - - spindles, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("owner", user.Did), - orm.FilterEq("instance", instance), - ) - if err != nil || len(spindles) != 1 { - l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) - fail() - return - } - - if string(spindles[0].Owner) != user.Did { - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) - s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") - return - } - - // begin verification - err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) - if err != nil { - l.Error("verification failed", "err", err) - - if errors.Is(err, xrpcclient.ErrXrpcUnsupported) { - s.Pages.Notice(w, noticeId, "Failed to verify spindle, XRPC queries are unsupported on this spindle, consider upgrading!") - return - } - - if e, ok := err.(*serververify.OwnerMismatch); ok { - s.Pages.Notice(w, noticeId, e.Error()) - return - } - - fail() - return - } - - rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) - if err != nil { - l.Error("failed to mark verified", "err", err) - s.Pages.Notice(w, noticeId, err.Error()) - return - } - - verifiedSpindle, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("id", rowId), - ) - if err != nil || len(verifiedSpindle) != 1 { - l.Error("failed get new spindle", "err", err) - s.Pages.HxRefresh(w) - return - } - - shouldRefresh := r.Header.Get("shouldRefresh") - if shouldRefresh == "true" { - s.Pages.HxRefresh(w) - return - } - - w.Header().Set("HX-Reswap", "outerHTML") - s.Pages.SpindleListing(w, pages.SpindleListingParams{Spindle: verifiedSpindle[0]}) -} - -func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { - user := s.OAuth.GetMultiAccountUser(r) - l := s.Logger.With("handler", "addMember") - - instance := chi.URLParam(r, "instance") - if instance == "" { - l.Error("empty instance") - http.Error(w, "Not found", http.StatusNotFound) - return - } - l = l.With("instance", instance) - l = l.With("user", user.Did) - - spindles, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("owner", user.Did), - orm.FilterEq("instance", instance), - ) - if err != nil || len(spindles) != 1 { - l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) - http.Error(w, "Not found", http.StatusNotFound) - return - } - - noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id) - defaultErr := "Failed to add member. Try again later." - fail := func() { - s.Pages.Notice(w, noticeId, defaultErr) - } - - if string(spindles[0].Owner) != user.Did { - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) - s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") - return - } - - member := r.FormValue("member") - member = strings.TrimPrefix(member, "@") - if member == "" { - l.Error("empty member") - s.Pages.Notice(w, noticeId, "Failed to add member, empty form.") - return - } - l = l.With("member", member) - - memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) - if err != nil { - l.Error("failed to resolve member identity to handle", "err", err) - s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") - return - } - if memberId.Handle.IsInvalidHandle() { - l.Error("failed to resolve member identity to handle") - s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") - return - } - - // write to pds - client, err := s.OAuth.AuthorizedClient(r) - if err != nil { - l.Error("failed to authorize client", "err", err) - fail() - return - } - - rkey := tid.TID() - - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.SpindleMemberNSID, - Repo: user.Did, - Rkey: rkey, - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.SpindleMember{ - CreatedAt: time.Now().Format(time.RFC3339), - Instance: instance, - Subject: memberId.DID.String(), - }, - }, - }) - if err != nil { - l.Error("failed to add record to PDS", "err", err) - s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.") - return - } - - // success - s.Pages.HxRedirect(w, fmt.Sprintf("/settings/spindles/%s", instance)) -} - -func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { - user := s.OAuth.GetMultiAccountUser(r) - l := s.Logger.With("handler", "removeMember") - - noticeId := "operation-error" - defaultErr := "Failed to remove member. Try again later." - fail := func() { - s.Pages.Notice(w, noticeId, defaultErr) - } - - instance := chi.URLParam(r, "instance") - if instance == "" { - l.Error("empty instance") - fail() - return - } - l = l.With("instance", instance) - l = l.With("user", user.Did) - - spindles, err := db.GetSpindles( - r.Context(), - s.Db, - orm.FilterEq("owner", user.Did), - orm.FilterEq("instance", instance), - ) - if err != nil || len(spindles) != 1 { - l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles)) - fail() - return - } - - if string(spindles[0].Owner) != user.Did { - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) - s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") - return - } - - member := r.FormValue("member") - member = strings.TrimPrefix(member, "@") - if member == "" { - l.Error("empty member") - s.Pages.Notice(w, noticeId, "Failed to remove member, empty form.") - return - } - l = l.With("member", member) - - memberId, err := s.IdResolver.ResolveIdent(r.Context(), member) - if err != nil { - l.Error("failed to resolve member identity to handle", "err", err) - s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") - return - } - - members, err := db.GetSpindleMembers( - s.Db, - orm.FilterEq("did", user.Did), - orm.FilterEq("instance", instance), - orm.FilterEq("subject", memberId.DID), - ) - if err != nil || len(members) != 1 { - l.Error("failed to get member", "err", err) - fail() - return - } - - client, err := s.OAuth.AuthorizedClient(r) - if err != nil { - l.Error("failed to authorize client", "err", err) - fail() - return - } - - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ - Collection: tangled.SpindleMemberNSID, - Repo: user.Did, - Rkey: members[0].Rkey, - }) - if err != nil { - l.Error("failed to delete record", "err", err) - fail() - return - } - - s.Pages.HxRefresh(w) -} -- 2.51.2