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.
+
+
- {{ 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)
-}