From 86b2068f19195471a99c75420c5915474be42408 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/pages/pages.go | 44 +- appview/pages/templates/repo/new.html | 1 - .../pages/templates/spindles/dashboard.html | 133 ----- .../spindles/fragments/addMemberModal.html | 65 -- .../spindles/fragments/spindleListing.html | 77 --- appview/pages/templates/spindles/index.html | 75 +-- appview/serververify/verify.go | 44 -- appview/spindles/spindles.go | 560 ------------------ 8 files changed, 10 insertions(+), 989 deletions(-) delete mode 100644 appview/pages/templates/spindles/dashboard.html delete mode 100644 appview/pages/templates/spindles/fragments/addMemberModal.html delete mode 100644 appview/pages/templates/spindles/fragments/spindleListing.html diff --git a/appview/pages/pages.go b/appview/pages/pages.go index e20d77af..8cfa4d90 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -17,7 +17,6 @@ import ( "sync" "time" - "tangled.org/core/api/tangled" "tangled.org/core/appview/cache" "tangled.org/core/appview/commitverify" "tangled.org/core/appview/config" @@ -675,8 +674,7 @@ func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error { type SpindlesParams struct { BaseParams - Spindles []models.Spindle - Tab string + Tab string } func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { @@ -684,27 +682,6 @@ func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { return p.execute("spindles/index", w, params) } -type SpindleListingParams struct { - models.Spindle - Tab string -} - -func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { - return p.executePlain("spindles/fragments/spindleListing", w, params) -} - -type SpindleDashboardParams struct { - BaseParams - Spindle models.Spindle - Members []string - Repos map[string][]models.Repo - Tab string -} - -func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error { - return p.execute("spindles/dashboard", w, params) -} - type NewRepoParams struct { BaseParams Knots []string @@ -1205,25 +1182,6 @@ type Collaborator struct { Role string } -type RepoSettingsParams struct { - BaseParams - RepoInfo repoinfo.RepoInfo - Collaborators []Collaborator - Active string - Branches []types.Branch - Spindles []string - CurrentSpindle string - Secrets []*tangled.RepoListSecrets_Secret - - // TODO: use repoinfo.roles - IsCollaboratorInviteAllowed bool -} - -func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { - params.Active = "settings" - return p.executeRepo("repo/settings", w, params) -} - type RepoGeneralSettingsParams struct { BaseParams RepoInfo repoinfo.RepoInfo diff --git a/appview/pages/templates/repo/new.html b/appview/pages/templates/repo/new.html index 6a375e6f..42b1cf29 100644 --- a/appview/pages/templates/repo/new.html +++ b/appview/pages/templates/repo/new.html @@ -229,7 +229,6 @@

A spindle runs your CI workflows. - You can also register your own spindle.

{{ end }} diff --git a/appview/pages/templates/spindles/dashboard.html b/appview/pages/templates/spindles/dashboard.html deleted file mode 100644 index a79abad5..00000000 --- a/appview/pages/templates/spindles/dashboard.html +++ /dev/null @@ -1,133 +0,0 @@ -{{ define "title" }}{{ .Tab | capitalize }} · {{.Spindle.Instance}} · Tangled{{ end }} - -{{ define "content" }} -
-

Settings

-
-
-
-
- {{ template "user/settings/fragments/sidebar" . }} -
-
- {{ template "spindleDash" . }} -
-
-
-{{ end }} - -{{ define "spindleDash" }} -
-
-

{{ .Tab | capitalize }} · {{ .Spindle.Instance }}

-
- {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }} - {{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }} - {{ if .Spindle.Verified }} - {{ i "shield-check" "w-4 h-4" }} Verified - {{ if $isOwner }} - {{ template "spindles/fragments/addMemberModal" .Spindle }} - {{ end }} - {{ else }} - {{ i "shield-off" "w-4 h-4" }} Unverified - {{ if $isOwner }} - {{ block "retryButton" .Spindle }} {{ end }} - {{ end }} - {{ end }} - - {{ if $isOwner }} - {{ block "deleteButton" .Spindle }} {{ end }} - {{ end }} -
-
-
-
- -{{ if .Members }} -
-
- {{ block "member" . }} {{ end }} -
-
-{{ end }} -{{ end }} - - -{{ define "member" }} - {{ range .Members }} -
-
-
- {{ template "user/fragments/picHandleLink" . }} -
- {{ if ne $.LoggedInUser.Did . }} - {{ block "removeMemberButton" (list $ . ) }} {{ end }} - {{ end }} -
-
- {{ $repos := index $.Repos . }} - {{ range $repos }} -
- {{ i "book-marked" "size-4" }} - - {{ .Name }} - -
- {{ else }} -
- No repositories configured yet. -
- {{ end }} -
-
- {{ end }} -{{ end }} - -{{ define "deleteButton" }} - -{{ end }} - - -{{ define "retryButton" }} - -{{ end }} - - -{{ define "removeMemberButton" }} - {{ $root := index . 0 }} - {{ $member := index . 1 }} - -{{ end }} - diff --git a/appview/pages/templates/spindles/fragments/addMemberModal.html b/appview/pages/templates/spindles/fragments/addMemberModal.html deleted file mode 100644 index f999bd84..00000000 --- a/appview/pages/templates/spindles/fragments/addMemberModal.html +++ /dev/null @@ -1,65 +0,0 @@ -{{ define "spindles/fragments/addMemberModal" }} - - -
- {{ block "addSpindleMemberPopover" . }} {{ end }} -
-{{ end }} - -{{ define "addSpindleMemberPopover" }} -
- -

Members can register repositories and run workflows on this spindle.

- - - -
- - -
-
-
-{{ end }} diff --git a/appview/pages/templates/spindles/fragments/spindleListing.html b/appview/pages/templates/spindles/fragments/spindleListing.html deleted file mode 100644 index b74f206e..00000000 --- a/appview/pages/templates/spindles/fragments/spindleListing.html +++ /dev/null @@ -1,77 +0,0 @@ -{{ define "spindles/fragments/spindleListing" }} -
- {{ block "spindleLeftSide" . }} {{ end }} - {{ block "spindleRightSide" . }} {{ end }} -
-{{ end }} - -{{ define "spindleLeftSide" }} - {{ if .Verified }} - - {{ i "hard-drive" "w-4 h-4" }} - - {{ .Instance }} - - - {{ template "repo/fragments/shortTimeAgo" .Created }} - - - {{ else }} -
- {{ i "hard-drive" "w-4 h-4" }} - {{ .Instance }} - - {{ template "repo/fragments/shortTimeAgo" .Created }} - -
- {{ end }} -{{ end }} - -{{ define "spindleRightSide" }} -
- {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }} - - {{ if .NeedsUpgrade }} - {{ i "shield-alert" "w-4 h-4" }} Needs upgrade - {{ block "spindleRetryButton" . }} {{ end }} - {{ else if .Verified }} - {{ i "shield-check" "w-4 h-4" }} Verified - {{ template "spindles/fragments/addMemberModal" . }} - {{ else }} - {{ i "shield-off" "w-4 h-4" }} Unverified - {{ block "spindleRetryButton" . }} {{ end }} - {{ end }} - - {{ block "spindleDeleteButton" . }} {{ end }} -
-{{ end }} - -{{ define "spindleDeleteButton" }} - -{{ end }} - - -{{ define "spindleRetryButton" }} - -{{ end }} 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/serververify/verify.go b/appview/serververify/verify.go index f7d2a466..d089659b 100644 --- a/appview/serververify/verify.go +++ b/appview/serververify/verify.go @@ -82,50 +82,6 @@ func RunVerification(ctx context.Context, domain, expectedOwner string, dev bool return nil } -// MarkSpindleVerified marks a spindle as verified in the DB and adds the user as its owner -func MarkSpindleVerified(d *db.DB, e *rbac.Enforcer, instance, owner string) (int64, error) { - tx, err := d.Begin() - if err != nil { - return 0, fmt.Errorf("failed to create txn: %w", err) - } - committed := false - defer func() { - if committed { - return - } - tx.Rollback() - e.E.LoadPolicy() - }() - - // mark this spindle as verified in the db - rowId, err := db.VerifySpindle( - tx, - orm.FilterEq("owner", owner), - orm.FilterEq("instance", instance), - ) - if err != nil { - return 0, fmt.Errorf("failed to write to DB: %w", err) - } - - err = e.AddSpindleOwner(instance, owner) - if err != nil { - return 0, fmt.Errorf("failed to update ACL: %w", err) - } - - err = tx.Commit() - if err != nil { - return 0, fmt.Errorf("failed to commit txn: %w", err) - } - - err = e.E.SavePolicy() - if err != nil { - return 0, fmt.Errorf("failed to update ACL: %w", err) - } - committed = true - - return rowId, nil -} - // MarkKnotVerified marks a knot as verified and sets up ownership/permissions func MarkKnotVerified(d *db.DB, e *rbac.Enforcer, domain, owner string) error { tx, err := d.BeginTx(context.Background(), nil) 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