···
"github.com/go-chi/chi/v5"
"tangled.sh/tangled.sh/core/api/tangled"
+
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/config"
"tangled.sh/tangled.sh/core/appview/db"
+
"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"
+
verify "tangled.sh/tangled.sh/core/appview/spindleverify"
"tangled.sh/tangled.sh/core/rbac"
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
+
Enforcer *rbac.Enforcer
+
IdResolver *idresolver.Resolver
func (s *Spindles) Router() http.Handler {
+
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)
···
+
func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) {
+
l := s.Logger.With("handler", "dashboard")
+
user := s.OAuth.GetUser(r)
+
l = l.With("user", user.Did)
+
instance := chi.URLParam(r, "instance")
+
l = l.With("instance", instance)
+
spindles, err := db.GetSpindles(
+
db.FilterEq("instance", instance),
+
db.FilterEq("owner", user.Did),
+
db.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)
+
members, err := s.Enforcer.GetSpindleUsersByRole("server:member", spindle.Instance)
+
l.Error("failed to get spindle members", "err", err)
+
http.Error(w, "Not found", http.StatusInternalServerError)
+
repos, err := db.GetRepos(
+
db.FilterEq("spindle", instance),
+
l.Error("failed to get spindle repos", "err", err)
+
http.Error(w, "Not found", http.StatusInternalServerError)
+
identsToResolve := make([]string, len(members))
+
for i, member := range members {
+
identsToResolve[i] = member
+
resolvedIds := s.IdResolver.ResolveIdents(r.Context(), identsToResolve)
+
didHandleMap := make(map[string]string)
+
for _, identity := range resolvedIds {
+
if !identity.Handle.IsInvalidHandle() {
+
didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+
didHandleMap[identity.DID.String()] = identity.DID.String()
+
// organize repos by did
+
repoMap := make(map[string][]db.Repo)
+
for _, r := range repos {
+
repoMap[r.Did] = append(repoMap[r.Did], r)
+
s.Pages.SpindleDashboard(w, pages.SpindleDashboardParams{
+
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.")
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
···
+
s.Enforcer.E.LoadPolicy()
err = db.AddSpindle(tx, db.Spindle{
Owner: syntax.DID(user.Did),
···
+
err = s.Enforcer.AddSpindle(instance)
+
l.Error("failed to create spindle", "err", err)
client, err := s.OAuth.AuthorizedClient(r)
···
+
err = s.Enforcer.E.SavePolicy()
l.Error("failed to update ACL", "err", err)
+
err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
+
l.Error("verification failed", "err", err)
+
_, err = verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did)
+
l.Error("failed to mark verified", "err", err)
···
func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) {
user := s.OAuth.GetUser(r)
+
l := s.Logger.With("handler", "delete")
noticeId := "operation-error"
defaultErr := "Failed to delete spindle. Try again later."
···
+
spindles, err := db.GetSpindles(
+
db.FilterEq("owner", user.Did),
+
db.FilterEq("instance", instance),
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
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.")
l.Error("failed to start txn", "err", err)
+
s.Enforcer.E.LoadPolicy()
···
l.Error("failed to delete spindle", "err", err)
+
err = s.Enforcer.RemoveSpindle(instance)
+
l.Error("failed to update ACL", "err", err)
···
+
err = s.Enforcer.E.SavePolicy()
+
l.Error("failed to update ACL", "err", err)
+
shouldRedirect := r.Header.Get("shouldRedirect")
+
if shouldRedirect == "true" {
+
s.Pages.HxRedirect(w, "/spindles")
func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) {
user := s.OAuth.GetUser(r)
+
l := s.Logger.With("handler", "retry")
noticeId := "operation-error"
defaultErr := "Failed to verify spindle. Try again later."
···
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
+
spindles, err := db.GetSpindles(
+
db.FilterEq("owner", user.Did),
+
db.FilterEq("instance", instance),
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
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.")
+
err = verify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev)
l.Error("verification failed", "err", err)
+
if errors.Is(err, verify.FetchError) {
+
s.Pages.Notice(w, noticeId, err.Error())
+
if e, ok := err.(*verify.OwnerMismatch); ok {
+
s.Pages.Notice(w, noticeId, e.Error())
+
rowId, err := verify.MarkVerified(s.Db, s.Enforcer, instance, user.Did)
+
l.Error("failed to mark verified", "err", err)
+
s.Pages.Notice(w, noticeId, err.Error())
+
verifiedSpindle, err := db.GetSpindles(
+
db.FilterEq("id", rowId),
+
if err != nil || len(verifiedSpindle) != 1 {
+
l.Error("failed get new spindle", "err", err)
+
shouldRefresh := r.Header.Get("shouldRefresh")
+
if shouldRefresh == "true" {
+
w.Header().Set("HX-Reswap", "outerHTML")
+
s.Pages.SpindleListing(w, pages.SpindleListingParams{verifiedSpindle[0]})
+
func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) {
+
user := s.OAuth.GetUser(r)
+
l := s.Logger.With("handler", "addMember")
+
instance := chi.URLParam(r, "instance")
+
l.Error("empty instance")
+
http.Error(w, "Not found", http.StatusNotFound)
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
+
spindles, err := db.GetSpindles(
db.FilterEq("owner", user.Did),
db.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)
+
noticeId := fmt.Sprintf("add-member-error-%d", spindles[0].Id)
+
defaultErr := "Failed to add member. Try again later."
+
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.")
+
member := r.FormValue("member")
+
l.Error("empty member")
+
s.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
+
l = l.With("member", member)
+
memberId, err := s.IdResolver.ResolveIdent(r.Context(), member)
+
l.Error("failed to resolve member identity to handle", "err", err)
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
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.")
+
client, err := s.OAuth.AuthorizedClient(r)
+
l.Error("failed to authorize client", "err", err)
+
tx, err := s.Db.Begin()
+
l.Error("failed to start txn", "err", err)
+
s.Enforcer.E.LoadPolicy()
+
if err = db.AddSpindleMember(tx, db.SpindleMember{
+
Did: syntax.DID(user.Did),
+
l.Error("failed to add spindle member", "err", err)
+
if err = s.Enforcer.AddSpindleMember(instance, memberId.DID.String()); err != nil {
+
l.Error("failed to add member to ACLs")
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.SpindleMemberNSID,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.SpindleMember{
+
CreatedAt: time.Now().Format(time.RFC3339),
+
Subject: memberId.DID.String(),
+
l.Error("failed to add record to PDS", "err", err)
+
s.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.")
+
if err = tx.Commit(); err != nil {
+
l.Error("failed to commit txn", "err", err)
+
if err = s.Enforcer.E.SavePolicy(); err != nil {
+
l.Error("failed to add member to ACLs", "err", err)
+
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
+
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
+
user := s.OAuth.GetUser(r)
+
l := s.Logger.With("handler", "removeMember")
+
noticeId := "operation-error"
+
defaultErr := "Failed to add member. Try again later."
+
s.Pages.Notice(w, noticeId, defaultErr)
+
instance := chi.URLParam(r, "instance")
+
l.Error("empty instance")
+
l = l.With("instance", instance)
+
l = l.With("user", user.Did)
+
spindles, err := db.GetSpindles(
+
db.FilterEq("owner", user.Did),
+
db.FilterEq("instance", instance),
+
if err != nil || len(spindles) != 1 {
+
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
+
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.")
+
member := r.FormValue("member")
+
l.Error("empty member")
+
s.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
+
l = l.With("member", member)
+
memberId, err := s.IdResolver.ResolveIdent(r.Context(), member)
+
l.Error("failed to resolve member identity to handle", "err", err)
+
s.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
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.")
+
tx, err := s.Db.Begin()
+
l.Error("failed to start txn", "err", err)
+
s.Enforcer.E.LoadPolicy()
+
// get the record from the DB first:
+
members, err := db.GetSpindleMembers(
+
db.FilterEq("did", user.Did),
+
db.FilterEq("instance", instance),
+
db.FilterEq("subject", memberId.DID),
+
if err != nil || len(members) != 1 {
+
l.Error("failed to get member", "err", err)
+
if err = db.RemoveSpindleMember(
+
db.FilterEq("did", user.Did),
+
db.FilterEq("instance", instance),
+
db.FilterEq("subject", memberId.DID),
+
l.Error("failed to remove spindle member", "err", err)
+
// remove from enforcer
+
if err = s.Enforcer.RemoveSpindleMember(instance, memberId.DID.String()); err != nil {
+
l.Error("failed to update ACLs", "err", err)
+
client, err := s.OAuth.AuthorizedClient(r)
+
l.Error("failed to authorize client", "err", err)
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.SpindleMemberNSID,
+
l.Error("failed to delete record", "err", err)
+
if err = tx.Commit(); err != nil {
+
l.Error("failed to commit txn", "err", err)
+
if err = s.Enforcer.E.SavePolicy(); err != nil {
+
l.Error("failed to save ACLs", "err", err)