From 63e8747de3a1caf09e020aefbda04fa5c9b6da6c Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Tue, 15 Jul 2025 15:27:39 +0100 Subject: [PATCH] appview: refactor knot endpoints into separate router Change-Id: ksrqlywwnnvxmyvpwsukvxmxowulvnzp Signed-off-by: oppiliappan --- appview/db/registration.go | 9 +- appview/knots/knots.go | 482 +++++++++++++++++++++++++++ appview/state/router.go | 37 ++- appview/state/state.go | 649 +++++++++++++++++++------------------ 4 files changed, 832 insertions(+), 345 deletions(-) create mode 100644 appview/knots/knots.go diff --git a/appview/db/registration.go b/appview/db/registration.go index 4798b60..a1d1774 100644 --- a/appview/db/registration.go +++ b/appview/db/registration.go @@ -10,6 +10,7 @@ import ( ) type Registration struct { + Id int64 Domain string ByDid string Created *time.Time @@ -36,7 +37,7 @@ func RegistrationsByDid(e Execer, did string) ([]Registration, error) { var registrations []Registration rows, err := e.Query(` - select domain, did, created, registered from registrations + select id, domain, did, created, registered from registrations where did = ? `, did) if err != nil { @@ -47,7 +48,7 @@ func RegistrationsByDid(e Execer, did string) ([]Registration, error) { var createdAt *string var registeredAt *string var registration Registration - err = rows.Scan(®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt) + err = rows.Scan(®istration.Id, ®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt) if err != nil { log.Println(err) @@ -75,9 +76,9 @@ func RegistrationByDomain(e Execer, domain string) (*Registration, error) { var registration Registration err := e.QueryRow(` - select domain, did, created, registered from registrations + select id, domain, did, created, registered from registrations where domain = ? - `, domain).Scan(®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt) + `, domain).Scan(®istration.Id, ®istration.Domain, ®istration.ByDid, &createdAt, ®isteredAt) if err != nil { if err == sql.ErrNoRows { diff --git a/appview/knots/knots.go b/appview/knots/knots.go new file mode 100644 index 0000000..cfdced3 --- /dev/null +++ b/appview/knots/knots.go @@ -0,0 +1,482 @@ +package knots + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "log/slog" + "net/http" + "strings" + "time" + + "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" + "tangled.sh/tangled.sh/core/eventconsumer" + "tangled.sh/tangled.sh/core/knotclient" + "tangled.sh/tangled.sh/core/rbac" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + lexutil "github.com/bluesky-social/indigo/lex/util" +) + +type Knots struct { + Db *db.DB + OAuth *oauth.OAuth + Pages *pages.Pages + Config *config.Config + Enforcer *rbac.Enforcer + IdResolver *idresolver.Resolver + Logger *slog.Logger + Knotstream *eventconsumer.Consumer +} + +func (k *Knots) Router(mw *middleware.Middleware) http.Handler { + r := chi.NewRouter() + + r.Use(middleware.AuthMiddleware(k.OAuth)) + + r.Get("/", k.index) + r.Post("/key", k.generateKey) + + r.Route("/{domain}", func(r chi.Router) { + r.Post("/init", k.init) + r.Get("/", k.dashboard) + r.Route("/member", func(r chi.Router) { + r.Use(mw.KnotOwner()) + r.Get("/", k.members) + r.Put("/", k.addMember) + r.Delete("/", k.removeMember) + }) + }) + + return r +} + +// get knots registered by this user +func (k *Knots) index(w http.ResponseWriter, r *http.Request) { + l := k.Logger.With("handler", "index") + + user := k.OAuth.GetUser(r) + registrations, err := db.RegistrationsByDid(k.Db, user.Did) + if err != nil { + l.Error("failed to get registrations by did", "err", err) + } + + k.Pages.Knots(w, pages.KnotsParams{ + LoggedInUser: user, + Registrations: registrations, + }) +} + +// requires auth +func (k *Knots) generateKey(w http.ResponseWriter, r *http.Request) { + l := k.Logger.With("handler", "generateKey") + + user := k.OAuth.GetUser(r) + did := user.Did + l = l.With("did", did) + + // check if domain is valid url, and strip extra bits down to just host + domain := r.FormValue("domain") + if domain == "" { + l.Error("empty domain") + http.Error(w, "Invalid form", http.StatusBadRequest) + return + } + l = l.With("domain", domain) + + noticeId := "registration-error" + fail := func() { + k.Pages.Notice(w, noticeId, "Failed to generate registration key.") + } + + key, err := db.GenerateRegistrationKey(k.Db, domain, did) + if err != nil { + l.Error("failed to generate registration key", "err", err) + fail() + return + } + + allRegs, err := db.RegistrationsByDid(k.Db, did) + if err != nil { + l.Error("failed to generate registration key", "err", err) + fail() + return + } + + k.Pages.KnotListingFull(w, pages.KnotListingFullParams{ + Registrations: allRegs, + }) + k.Pages.KnotSecret(w, pages.KnotSecretParams{ + Secret: key, + }) +} + +// create a signed request and check if a node responds to that +func (k *Knots) init(w http.ResponseWriter, r *http.Request) { + l := k.Logger.With("handler", "init") + user := k.OAuth.GetUser(r) + + noticeId := "operation-error" + defaultErr := "Failed to initialize knot. Try again later." + fail := func() { + k.Pages.Notice(w, noticeId, defaultErr) + } + + domain := chi.URLParam(r, "domain") + if domain == "" { + http.Error(w, "malformed url", http.StatusBadRequest) + return + } + l = l.With("domain", domain) + + l.Info("checking domain") + + secret, err := db.GetRegistrationKey(k.Db, domain) + if err != nil { + l.Error("failed to get registration key for domain", "err", err) + fail() + return + } + + client, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) + if err != nil { + l.Error("failed to create knotclient", "err", err) + fail() + return + } + + resp, err := client.Init(user.Did) + if err != nil { + k.Pages.Notice(w, noticeId, fmt.Sprintf("Failed to make request: %s", err.Error())) + l.Error("failed to make init request", "err", err) + return + } + + if resp.StatusCode == http.StatusConflict { + k.Pages.Notice(w, noticeId, "This knot is already registered") + l.Error("knot already registered", "statuscode", resp.StatusCode) + return + } + + if resp.StatusCode != http.StatusNoContent { + k.Pages.Notice(w, noticeId, fmt.Sprintf("Received status %d from knot, expected %d", resp.StatusCode, http.StatusNoContent)) + l.Error("incorrect statuscode returned", "statuscode", resp.StatusCode, "expected", http.StatusNoContent) + return + } + + // verify response mac + signature := resp.Header.Get("X-Signature") + signatureBytes, err := hex.DecodeString(signature) + if err != nil { + return + } + + expectedMac := hmac.New(sha256.New, []byte(secret)) + expectedMac.Write([]byte("ok")) + + if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { + k.Pages.Notice(w, noticeId, "Response signature mismatch, consider regenerating the secret and retrying.") + l.Error("signature mismatch", "bytes", signatureBytes) + return + } + + tx, err := k.Db.BeginTx(r.Context(), nil) + if err != nil { + l.Error("failed to start tx", "err", err) + fail() + return + } + defer func() { + tx.Rollback() + err = k.Enforcer.E.LoadPolicy() + if err != nil { + l.Error("rollback failed", "err", err) + } + }() + + // mark as registered + err = db.Register(tx, domain) + if err != nil { + l.Error("failed to register domain", "err", err) + fail() + return + } + + // set permissions for this did as owner + reg, err := db.RegistrationByDomain(tx, domain) + if err != nil { + l.Error("failed get registration by domain", "err", err) + fail() + return + } + + // add basic acls for this domain + err = k.Enforcer.AddKnot(domain) + if err != nil { + l.Error("failed to add knot to enforcer", "err", err) + fail() + return + } + + // add this did as owner of this domain + err = k.Enforcer.AddKnotOwner(domain, reg.ByDid) + if err != nil { + l.Error("failed to add knot owner to enforcer", "err", err) + fail() + return + } + + err = tx.Commit() + if err != nil { + l.Error("failed to commit changes", "err", err) + fail() + return + } + + err = k.Enforcer.E.SavePolicy() + if err != nil { + l.Error("failed to update ACLs", "err", err) + fail() + return + } + + // add this knot to knotstream + go k.Knotstream.AddSource( + context.Background(), + eventconsumer.NewKnotSource(domain), + ) + + k.Pages.KnotListing(w, pages.KnotListingParams{ + Registration: *reg, + }) +} + +func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { + l := k.Logger.With("handler", "dashboard") + fail := func() { + w.WriteHeader(http.StatusInternalServerError) + } + + domain := chi.URLParam(r, "domain") + if domain == "" { + http.Error(w, "malformed url", http.StatusBadRequest) + return + } + l = l.With("domain", domain) + + user := k.OAuth.GetUser(r) + l = l.With("did", user.Did) + + // dashboard is only available to owners + ok, err := k.Enforcer.IsKnotOwner(user.Did, domain) + if err != nil { + l.Error("failed to query enforcer", "err", err) + fail() + } + if !ok { + http.Error(w, "only owners can view dashboards", http.StatusUnauthorized) + return + } + + reg, err := db.RegistrationByDomain(k.Db, domain) + if err != nil { + l.Error("failed to get registration by domain", "err", err) + fail() + return + } + + var members []string + if reg.Registered != nil { + members, err = k.Enforcer.GetUserByRole("server:member", domain) + if err != nil { + l.Error("failed to get members list", "err", err) + fail() + return + } + } + + repos, err := db.GetRepos( + k.Db, + db.FilterEq("knot", domain), + db.FilterIn("did", members), + ) + if err != nil { + l.Error("failed to get repos list", "err", err) + fail() + return + } + // convert to map + repoByMember := make(map[string][]db.Repo) + for _, r := range repos { + repoByMember[r.Did] = append(repoByMember[r.Did], r) + } + + var didsToResolve []string + for _, m := range members { + didsToResolve = append(didsToResolve, m) + } + didsToResolve = append(didsToResolve, reg.ByDid) + resolvedIds := k.IdResolver.ResolveIdents(r.Context(), didsToResolve) + didHandleMap := make(map[string]string) + for _, identity := range resolvedIds { + if !identity.Handle.IsInvalidHandle() { + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) + } else { + didHandleMap[identity.DID.String()] = identity.DID.String() + } + } + + k.Pages.Knot(w, pages.KnotParams{ + LoggedInUser: user, + DidHandleMap: didHandleMap, + Registration: reg, + Members: members, + Repos: repoByMember, + IsOwner: true, + }) +} + +// list members of domain, requires auth and requires owner status +func (k *Knots) members(w http.ResponseWriter, r *http.Request) { + l := k.Logger.With("handler", "members") + + domain := chi.URLParam(r, "domain") + if domain == "" { + http.Error(w, "malformed url", http.StatusBadRequest) + return + } + l = l.With("domain", domain) + + // list all members for this domain + memberDids, err := k.Enforcer.GetUserByRole("server:member", domain) + if err != nil { + w.Write([]byte("failed to fetch member list")) + return + } + + w.Write([]byte(strings.Join(memberDids, "\n"))) + return +} + +// add member to domain, requires auth and requires invite access +func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { + l := k.Logger.With("handler", "members") + + domain := chi.URLParam(r, "domain") + if domain == "" { + http.Error(w, "malformed url", http.StatusBadRequest) + return + } + l = l.With("domain", domain) + + reg, err := db.RegistrationByDomain(k.Db, domain) + if err != nil { + l.Error("failed to get registration by domain", "err", err) + http.Error(w, "malformed url", http.StatusBadRequest) + return + } + + noticeId := fmt.Sprintf("add-member-error-%d", reg.Id) + l = l.With("notice-id", noticeId) + defaultErr := "Failed to add member. Try again later." + fail := func() { + k.Pages.Notice(w, noticeId, defaultErr) + } + + subjectIdentifier := r.FormValue("subject") + if subjectIdentifier == "" { + http.Error(w, "malformed form", http.StatusBadRequest) + return + } + l = l.With("subjectIdentifier", subjectIdentifier) + + subjectIdentity, err := k.IdResolver.ResolveIdent(r.Context(), subjectIdentifier) + if err != nil { + l.Error("failed to resolve identity", "err", err) + k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.") + return + } + l = l.With("subjectDid", subjectIdentity.DID) + + l.Info("adding member to knot") + + // announce this relation into the firehose, store into owners' pds + client, err := k.OAuth.AuthorizedClient(r) + if err != nil { + l.Error("failed to create client", "err", err) + fail() + return + } + + currentUser := k.OAuth.GetUser(r) + createdAt := time.Now().Format(time.RFC3339) + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ + Collection: tangled.KnotMemberNSID, + Repo: currentUser.Did, + Rkey: appview.TID(), + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.KnotMember{ + Subject: subjectIdentity.DID.String(), + Domain: domain, + CreatedAt: createdAt, + }}, + }) + // invalid record + if err != nil { + l.Error("failed to write to PDS", "err", err) + fail() + return + } + l = l.With("at-uri", resp.Uri) + l.Info("wrote record to PDS") + + secret, err := db.GetRegistrationKey(k.Db, domain) + if err != nil { + l.Error("failed to get registration key", "err", err) + fail() + return + } + + ksClient, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev) + if err != nil { + l.Error("failed to create client", "err", err) + fail() + return + } + + ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) + if err != nil { + l.Error("failed to reach knotserver", "err", err) + k.Pages.Notice(w, noticeId, "Failed to reach to knotserver.") + return + } + + if ksResp.StatusCode != http.StatusNoContent { + l.Error("status mismatch", "got", ksResp.StatusCode, "expected", http.StatusNoContent) + k.Pages.Notice(w, noticeId, fmt.Sprintf("Unexpected status code from knotserver %d, expected %d", ksResp.StatusCode, http.StatusNoContent)) + return + } + + err = k.Enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) + if err != nil { + l.Error("failed to add member to enforcer", "err", err) + fail() + return + } + + // success + k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain)) +} + +func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { +} diff --git a/appview/state/router.go b/appview/state/router.go index 58efc91..826c3be 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -7,6 +7,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/gorilla/sessions" "tangled.sh/tangled.sh/core/appview/issues" + "tangled.sh/tangled.sh/core/appview/knots" "tangled.sh/tangled.sh/core/appview/middleware" oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" "tangled.sh/tangled.sh/core/appview/pipelines" @@ -101,23 +102,6 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.Get("/", s.Timeline) - r.Route("/knots", func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.oauth)) - r.Get("/", s.Knots) - r.Post("/key", s.RegistrationKey) - - r.Route("/{domain}", func(r chi.Router) { - r.Post("/init", s.InitKnotServer) - r.Get("/", s.KnotServerInfo) - r.Route("/member", func(r chi.Router) { - r.Use(mw.KnotOwner()) - r.Get("/", s.ListMembers) - r.Put("/", s.AddMember) - r.Delete("/", s.RemoveMember) - }) - }) - }) - r.Route("/repo", func(r chi.Router) { r.Route("/new", func(r chi.Router) { r.Use(middleware.AuthMiddleware(s.oauth)) @@ -151,6 +135,7 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { }) r.Mount("/settings", s.SettingsRouter()) + r.Mount("/knots", s.KnotsRouter(mw)) r.Mount("/spindles", s.SpindlesRouter()) r.Mount("/", s.OAuthRouter()) @@ -195,10 +180,26 @@ func (s *State) SpindlesRouter() http.Handler { return spindles.Router() } +func (s *State) KnotsRouter(mw *middleware.Middleware) http.Handler { + logger := log.New("knots") + + knots := &knots.Knots{ + Db: s.db, + OAuth: s.oauth, + Pages: s.pages, + Config: s.config, + Enforcer: s.enforcer, + IdResolver: s.idResolver, + Knotstream: s.knotstream, + Logger: logger, + } + + return knots.Router(mw) +} + func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler { issues := issues.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.posthog) return issues.Router(mw) - } func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler { diff --git a/appview/state/state.go b/appview/state/state.go index 7e4709b..a5b30a6 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -2,9 +2,6 @@ package state import ( "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" "fmt" "log" "log/slog" @@ -202,40 +199,40 @@ func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { } // requires auth -func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - // list open registrations under this did - - return - case http.MethodPost: - session, err := s.oauth.Stores().Get(r, oauth.SessionName) - if err != nil || session.IsNew { - log.Println("unauthorized attempt to generate registration key") - http.Error(w, "Forbidden", http.StatusUnauthorized) - return - } - - did := session.Values[oauth.SessionDid].(string) - - // check if domain is valid url, and strip extra bits down to just host - domain := r.FormValue("domain") - if domain == "" { - http.Error(w, "Invalid form", http.StatusBadRequest) - return - } - - key, err := db.GenerateRegistrationKey(s.db, domain, did) - - if err != nil { - log.Println(err) - http.Error(w, "unable to register this domain", http.StatusNotAcceptable) - return - } - - w.Write([]byte(key)) - } -} +// func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { +// switch r.Method { +// case http.MethodGet: +// // list open registrations under this did +// +// return +// case http.MethodPost: +// session, err := s.oauth.Stores().Get(r, oauth.SessionName) +// if err != nil || session.IsNew { +// log.Println("unauthorized attempt to generate registration key") +// http.Error(w, "Forbidden", http.StatusUnauthorized) +// return +// } +// +// did := session.Values[oauth.SessionDid].(string) +// +// // check if domain is valid url, and strip extra bits down to just host +// domain := r.FormValue("domain") +// if domain == "" { +// http.Error(w, "Invalid form", http.StatusBadRequest) +// return +// } +// +// key, err := db.GenerateRegistrationKey(s.db, domain, did) +// +// if err != nil { +// log.Println(err) +// http.Error(w, "unable to register this domain", http.StatusNotAcceptable) +// return +// } +// +// w.Write([]byte(key)) +// } +// } func (s *State) Keys(w http.ResponseWriter, r *http.Request) { user := chi.URLParam(r, "user") @@ -270,298 +267,304 @@ func (s *State) Keys(w http.ResponseWriter, r *http.Request) { } // create a signed request and check if a node responds to that -func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { - user := s.oauth.GetUser(r) - - domain := chi.URLParam(r, "domain") - if domain == "" { - http.Error(w, "malformed url", http.StatusBadRequest) - return - } - log.Println("checking ", domain) - - secret, err := db.GetRegistrationKey(s.db, domain) - if err != nil { - log.Printf("no key found for domain %s: %s\n", domain, err) - return - } - - client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) - if err != nil { - log.Println("failed to create client to ", domain) - } - - resp, err := client.Init(user.Did) - if err != nil { - w.Write([]byte("no dice")) - log.Println("domain was unreachable after 5 seconds") - return - } - - if resp.StatusCode == http.StatusConflict { - log.Println("status conflict", resp.StatusCode) - w.Write([]byte("already registered, sorry!")) - return - } - - if resp.StatusCode != http.StatusNoContent { - log.Println("status nok", resp.StatusCode) - w.Write([]byte("no dice")) - return - } - - // verify response mac - signature := resp.Header.Get("X-Signature") - signatureBytes, err := hex.DecodeString(signature) - if err != nil { - return - } - - expectedMac := hmac.New(sha256.New, []byte(secret)) - expectedMac.Write([]byte("ok")) - - if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { - log.Printf("response body signature mismatch: %x\n", signatureBytes) - return - } - - tx, err := s.db.BeginTx(r.Context(), nil) - if err != nil { - log.Println("failed to start tx", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer func() { - tx.Rollback() - err = s.enforcer.E.LoadPolicy() - if err != nil { - log.Println("failed to rollback policies") - } - }() - - // mark as registered - err = db.Register(tx, domain) - if err != nil { - log.Println("failed to register domain", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // set permissions for this did as owner - reg, err := db.RegistrationByDomain(tx, domain) - if err != nil { - log.Println("failed to register domain", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // add basic acls for this domain - err = s.enforcer.AddKnot(domain) - if err != nil { - log.Println("failed to setup owner of domain", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // add this did as owner of this domain - err = s.enforcer.AddKnotOwner(domain, reg.ByDid) - if err != nil { - log.Println("failed to setup owner of domain", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = tx.Commit() - if err != nil { - log.Println("failed to commit changes", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - err = s.enforcer.E.SavePolicy() - if err != nil { - log.Println("failed to update ACLs", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - // add this knot to knotstream - go s.knotstream.AddSource( - context.Background(), - eventconsumer.NewKnotSource(domain), - ) - - w.Write([]byte("check success")) -} - -func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { - domain := chi.URLParam(r, "domain") - if domain == "" { - http.Error(w, "malformed url", http.StatusBadRequest) - return - } - - user := s.oauth.GetUser(r) - reg, err := db.RegistrationByDomain(s.db, domain) - if err != nil { - w.Write([]byte("failed to pull up registration info")) - return - } - - var members []string - if reg.Registered != nil { - members, err = s.enforcer.GetUserByRole("server:member", domain) - if err != nil { - w.Write([]byte("failed to fetch member list")) - return - } - } - - var didsToResolve []string - for _, m := range members { - didsToResolve = append(didsToResolve, m) - } - didsToResolve = append(didsToResolve, reg.ByDid) - resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve) - didHandleMap := make(map[string]string) - for _, identity := range resolvedIds { - if !identity.Handle.IsInvalidHandle() { - didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) - } else { - didHandleMap[identity.DID.String()] = identity.DID.String() - } - } - - ok, err := s.enforcer.IsKnotOwner(user.Did, domain) - isOwner := err == nil && ok - - p := pages.KnotParams{ - LoggedInUser: user, - DidHandleMap: didHandleMap, - Registration: reg, - Members: members, - IsOwner: isOwner, - } - - s.pages.Knot(w, p) -} +// func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { +// user := s.oauth.GetUser(r) +// +// noticeId := "operation-error" +// defaultErr := "Failed to register spindle. Try again later." +// fail := func() { +// s.pages.Notice(w, noticeId, defaultErr) +// } +// +// domain := chi.URLParam(r, "domain") +// if domain == "" { +// http.Error(w, "malformed url", http.StatusBadRequest) +// return +// } +// log.Println("checking ", domain) +// +// secret, err := db.GetRegistrationKey(s.db, domain) +// if err != nil { +// log.Printf("no key found for domain %s: %s\n", domain, err) +// return +// } +// +// client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) +// if err != nil { +// log.Println("failed to create client to ", domain) +// } +// +// resp, err := client.Init(user.Did) +// if err != nil { +// w.Write([]byte("no dice")) +// log.Println("domain was unreachable after 5 seconds") +// return +// } +// +// if resp.StatusCode == http.StatusConflict { +// log.Println("status conflict", resp.StatusCode) +// w.Write([]byte("already registered, sorry!")) +// return +// } +// +// if resp.StatusCode != http.StatusNoContent { +// log.Println("status nok", resp.StatusCode) +// w.Write([]byte("no dice")) +// return +// } +// +// // verify response mac +// signature := resp.Header.Get("X-Signature") +// signatureBytes, err := hex.DecodeString(signature) +// if err != nil { +// return +// } +// +// expectedMac := hmac.New(sha256.New, []byte(secret)) +// expectedMac.Write([]byte("ok")) +// +// if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { +// log.Printf("response body signature mismatch: %x\n", signatureBytes) +// return +// } +// +// tx, err := s.db.BeginTx(r.Context(), nil) +// if err != nil { +// log.Println("failed to start tx", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// defer func() { +// tx.Rollback() +// err = s.enforcer.E.LoadPolicy() +// if err != nil { +// log.Println("failed to rollback policies") +// } +// }() +// +// // mark as registered +// err = db.Register(tx, domain) +// if err != nil { +// log.Println("failed to register domain", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// // set permissions for this did as owner +// reg, err := db.RegistrationByDomain(tx, domain) +// if err != nil { +// log.Println("failed to register domain", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// // add basic acls for this domain +// err = s.enforcer.AddKnot(domain) +// if err != nil { +// log.Println("failed to setup owner of domain", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// // add this did as owner of this domain +// err = s.enforcer.AddKnotOwner(domain, reg.ByDid) +// if err != nil { +// log.Println("failed to setup owner of domain", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// err = tx.Commit() +// if err != nil { +// log.Println("failed to commit changes", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// err = s.enforcer.E.SavePolicy() +// if err != nil { +// log.Println("failed to update ACLs", err) +// http.Error(w, err.Error(), http.StatusInternalServerError) +// return +// } +// +// // add this knot to knotstream +// go s.knotstream.AddSource( +// context.Background(), +// eventconsumer.NewKnotSource(domain), +// ) +// +// w.Write([]byte("check success")) +// } + +// func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { +// domain := chi.URLParam(r, "domain") +// if domain == "" { +// http.Error(w, "malformed url", http.StatusBadRequest) +// return +// } +// +// user := s.oauth.GetUser(r) +// reg, err := db.RegistrationByDomain(s.db, domain) +// if err != nil { +// w.Write([]byte("failed to pull up registration info")) +// return +// } +// +// var members []string +// if reg.Registered != nil { +// members, err = s.enforcer.GetUserByRole("server:member", domain) +// if err != nil { +// w.Write([]byte("failed to fetch member list")) +// return +// } +// } +// +// var didsToResolve []string +// for _, m := range members { +// didsToResolve = append(didsToResolve, m) +// } +// didsToResolve = append(didsToResolve, reg.ByDid) +// resolvedIds := s.idResolver.ResolveIdents(r.Context(), didsToResolve) +// didHandleMap := make(map[string]string) +// for _, identity := range resolvedIds { +// if !identity.Handle.IsInvalidHandle() { +// didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) +// } else { +// didHandleMap[identity.DID.String()] = identity.DID.String() +// } +// } +// +// ok, err := s.enforcer.IsKnotOwner(user.Did, domain) +// isOwner := err == nil && ok +// +// p := pages.KnotParams{ +// LoggedInUser: user, +// DidHandleMap: didHandleMap, +// Registration: reg, +// Members: members, +// IsOwner: isOwner, +// } +// +// s.pages.Knot(w, p) +// } // get knots registered by this user -func (s *State) Knots(w http.ResponseWriter, r *http.Request) { - // for now, this is just pubkeys - user := s.oauth.GetUser(r) - registrations, err := db.RegistrationsByDid(s.db, user.Did) - if err != nil { - log.Println(err) - } - - s.pages.Knots(w, pages.KnotsParams{ - LoggedInUser: user, - Registrations: registrations, - }) -} +// func (s *State) Knots(w http.ResponseWriter, r *http.Request) { +// // for now, this is just pubkeys +// user := s.oauth.GetUser(r) +// registrations, err := db.RegistrationsByDid(s.db, user.Did) +// if err != nil { +// log.Println(err) +// } +// +// s.pages.Knots(w, pages.KnotsParams{ +// LoggedInUser: user, +// Registrations: registrations, +// }) +// } // list members of domain, requires auth and requires owner status -func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { - domain := chi.URLParam(r, "domain") - if domain == "" { - http.Error(w, "malformed url", http.StatusBadRequest) - return - } - - // list all members for this domain - memberDids, err := s.enforcer.GetUserByRole("server:member", domain) - if err != nil { - w.Write([]byte("failed to fetch member list")) - return - } - - w.Write([]byte(strings.Join(memberDids, "\n"))) - return -} +// func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { +// domain := chi.URLParam(r, "domain") +// if domain == "" { +// http.Error(w, "malformed url", http.StatusBadRequest) +// return +// } +// +// // list all members for this domain +// memberDids, err := s.enforcer.GetUserByRole("server:member", domain) +// if err != nil { +// w.Write([]byte("failed to fetch member list")) +// return +// } +// +// w.Write([]byte(strings.Join(memberDids, "\n"))) +// return +// } // add member to domain, requires auth and requires invite access -func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { - domain := chi.URLParam(r, "domain") - if domain == "" { - http.Error(w, "malformed url", http.StatusBadRequest) - return - } - - subjectIdentifier := r.FormValue("subject") - if subjectIdentifier == "" { - http.Error(w, "malformed form", http.StatusBadRequest) - return - } - - subjectIdentity, err := s.idResolver.ResolveIdent(r.Context(), subjectIdentifier) - if err != nil { - w.Write([]byte("failed to resolve member did to a handle")) - return - } - log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) - - // announce this relation into the firehose, store into owners' pds - client, err := s.oauth.AuthorizedClient(r) - if err != nil { - http.Error(w, "failed to authorize client", http.StatusInternalServerError) - return - } - currentUser := s.oauth.GetUser(r) - createdAt := time.Now().Format(time.RFC3339) - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ - Collection: tangled.KnotMemberNSID, - Repo: currentUser.Did, - Rkey: appview.TID(), - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.KnotMember{ - Subject: subjectIdentity.DID.String(), - Domain: domain, - CreatedAt: createdAt, - }}, - }) - - // invalid record - if err != nil { - log.Printf("failed to create record: %s", err) - return - } - log.Println("created atproto record: ", resp.Uri) - - secret, err := db.GetRegistrationKey(s.db, domain) - if err != nil { - log.Printf("no key found for domain %s: %s\n", domain, err) - return - } - - ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) - if err != nil { - log.Println("failed to create client to ", domain) - return - } - - ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) - if err != nil { - log.Printf("failed to make request to %s: %s", domain, err) - return - } - - if ksResp.StatusCode != http.StatusNoContent { - w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) - return - } - - err = s.enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) - if err != nil { - w.Write([]byte(fmt.Sprint("failed to add member: ", err))) - return - } - - w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String()))) -} - -func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { -} +// func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { +// domain := chi.URLParam(r, "domain") +// if domain == "" { +// http.Error(w, "malformed url", http.StatusBadRequest) +// return +// } +// +// subjectIdentifier := r.FormValue("subject") +// if subjectIdentifier == "" { +// http.Error(w, "malformed form", http.StatusBadRequest) +// return +// } +// +// subjectIdentity, err := s.idResolver.ResolveIdent(r.Context(), subjectIdentifier) +// if err != nil { +// w.Write([]byte("failed to resolve member did to a handle")) +// return +// } +// log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) +// +// // announce this relation into the firehose, store into owners' pds +// client, err := s.oauth.AuthorizedClient(r) +// if err != nil { +// http.Error(w, "failed to authorize client", http.StatusInternalServerError) +// return +// } +// currentUser := s.oauth.GetUser(r) +// createdAt := time.Now().Format(time.RFC3339) +// resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ +// Collection: tangled.KnotMemberNSID, +// Repo: currentUser.Did, +// Rkey: appview.TID(), +// Record: &lexutil.LexiconTypeDecoder{ +// Val: &tangled.KnotMember{ +// Subject: subjectIdentity.DID.String(), +// Domain: domain, +// CreatedAt: createdAt, +// }}, +// }) +// +// // invalid record +// if err != nil { +// log.Printf("failed to create record: %s", err) +// return +// } +// log.Println("created atproto record: ", resp.Uri) +// +// secret, err := db.GetRegistrationKey(s.db, domain) +// if err != nil { +// log.Printf("no key found for domain %s: %s\n", domain, err) +// return +// } +// +// ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) +// if err != nil { +// log.Println("failed to create client to ", domain) +// return +// } +// +// ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) +// if err != nil { +// log.Printf("failed to make request to %s: %s", domain, err) +// return +// } +// +// if ksResp.StatusCode != http.StatusNoContent { +// w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) +// return +// } +// +// err = s.enforcer.AddKnotMember(domain, subjectIdentity.DID.String()) +// if err != nil { +// w.Write([]byte(fmt.Sprint("failed to add member: ", err))) +// return +// } +// +// w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String()))) +// } + +// func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { +// } func validateRepoName(name string) error { // check for path traversal attempts -- 2.43.0