···
+
"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"
+
Enforcer *rbac.Enforcer
+
IdResolver *idresolver.Resolver
+
Knotstream *eventconsumer.Consumer
+
func (k *Knots) Router(mw *middleware.Middleware) http.Handler {
+
r.Use(middleware.AuthMiddleware(k.OAuth))
+
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.Put("/", k.addMember)
+
r.Delete("/", k.removeMember)
+
// 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)
+
l.Error("failed to get registrations by did", "err", err)
+
k.Pages.Knots(w, pages.KnotsParams{
+
Registrations: registrations,
+
func (k *Knots) generateKey(w http.ResponseWriter, r *http.Request) {
+
l := k.Logger.With("handler", "generateKey")
+
user := k.OAuth.GetUser(r)
+
// check if domain is valid url, and strip extra bits down to just host
+
domain := r.FormValue("domain")
+
l.Error("empty domain")
+
http.Error(w, "Invalid form", http.StatusBadRequest)
+
l = l.With("domain", domain)
+
noticeId := "registration-error"
+
k.Pages.Notice(w, noticeId, "Failed to generate registration key.")
+
key, err := db.GenerateRegistrationKey(k.Db, domain, did)
+
l.Error("failed to generate registration key", "err", err)
+
allRegs, err := db.RegistrationsByDid(k.Db, did)
+
l.Error("failed to generate registration key", "err", err)
+
k.Pages.KnotListingFull(w, pages.KnotListingFullParams{
+
Registrations: allRegs,
+
k.Pages.KnotSecret(w, pages.KnotSecretParams{
+
// 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."
+
k.Pages.Notice(w, noticeId, defaultErr)
+
domain := chi.URLParam(r, "domain")
+
http.Error(w, "malformed url", http.StatusBadRequest)
+
l = l.With("domain", domain)
+
l.Info("checking domain")
+
secret, err := db.GetRegistrationKey(k.Db, domain)
+
l.Error("failed to get registration key for domain", "err", err)
+
client, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev)
+
l.Error("failed to create knotclient", "err", err)
+
resp, err := client.Init(user.Did)
+
k.Pages.Notice(w, noticeId, fmt.Sprintf("Failed to make request: %s", err.Error()))
+
l.Error("failed to make init request", "err", err)
+
if resp.StatusCode == http.StatusConflict {
+
k.Pages.Notice(w, noticeId, "This knot is already registered")
+
l.Error("knot already registered", "statuscode", resp.StatusCode)
+
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)
+
signature := resp.Header.Get("X-Signature")
+
signatureBytes, err := hex.DecodeString(signature)
+
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)
+
tx, err := k.Db.BeginTx(r.Context(), nil)
+
l.Error("failed to start tx", "err", err)
+
err = k.Enforcer.E.LoadPolicy()
+
l.Error("rollback failed", "err", err)
+
err = db.Register(tx, domain)
+
l.Error("failed to register domain", "err", err)
+
// set permissions for this did as owner
+
reg, err := db.RegistrationByDomain(tx, domain)
+
l.Error("failed get registration by domain", "err", err)
+
// add basic acls for this domain
+
err = k.Enforcer.AddKnot(domain)
+
l.Error("failed to add knot to enforcer", "err", err)
+
// add this did as owner of this domain
+
err = k.Enforcer.AddKnotOwner(domain, reg.ByDid)
+
l.Error("failed to add knot owner to enforcer", "err", err)
+
l.Error("failed to commit changes", "err", err)
+
err = k.Enforcer.E.SavePolicy()
+
l.Error("failed to update ACLs", "err", err)
+
// add this knot to knotstream
+
go k.Knotstream.AddSource(
+
eventconsumer.NewKnotSource(domain),
+
k.Pages.KnotListing(w, pages.KnotListingParams{
+
func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
+
l := k.Logger.With("handler", "dashboard")
+
w.WriteHeader(http.StatusInternalServerError)
+
domain := chi.URLParam(r, "domain")
+
http.Error(w, "malformed url", http.StatusBadRequest)
+
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)
+
l.Error("failed to query enforcer", "err", err)
+
http.Error(w, "only owners can view dashboards", http.StatusUnauthorized)
+
reg, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed to get registration by domain", "err", err)
+
if reg.Registered != nil {
+
members, err = k.Enforcer.GetUserByRole("server:member", domain)
+
l.Error("failed to get members list", "err", err)
+
repos, err := db.GetRepos(
+
db.FilterEq("knot", domain),
+
db.FilterIn("did", members),
+
l.Error("failed to get repos list", "err", err)
+
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())
+
didHandleMap[identity.DID.String()] = identity.DID.String()
+
k.Pages.Knot(w, pages.KnotParams{
+
DidHandleMap: didHandleMap,
+
// 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")
+
http.Error(w, "malformed url", http.StatusBadRequest)
+
l = l.With("domain", domain)
+
// list all members for this domain
+
memberDids, err := k.Enforcer.GetUserByRole("server:member", domain)
+
w.Write([]byte("failed to fetch member list"))
+
w.Write([]byte(strings.Join(memberDids, "\n")))
+
// 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")
+
http.Error(w, "malformed url", http.StatusBadRequest)
+
l = l.With("domain", domain)
+
reg, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed to get registration by domain", "err", err)
+
http.Error(w, "malformed url", http.StatusBadRequest)
+
noticeId := fmt.Sprintf("add-member-error-%d", reg.Id)
+
l = l.With("notice-id", noticeId)
+
defaultErr := "Failed to add member. Try again later."
+
k.Pages.Notice(w, noticeId, defaultErr)
+
subjectIdentifier := r.FormValue("subject")
+
if subjectIdentifier == "" {
+
http.Error(w, "malformed form", http.StatusBadRequest)
+
l = l.With("subjectIdentifier", subjectIdentifier)
+
subjectIdentity, err := k.IdResolver.ResolveIdent(r.Context(), subjectIdentifier)
+
l.Error("failed to resolve identity", "err", err)
+
k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
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)
+
l.Error("failed to create client", "err", err)
+
currentUser := k.OAuth.GetUser(r)
+
createdAt := time.Now().Format(time.RFC3339)
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.KnotMemberNSID,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.KnotMember{
+
Subject: subjectIdentity.DID.String(),
+
l.Error("failed to write to PDS", "err", err)
+
l = l.With("at-uri", resp.Uri)
+
l.Info("wrote record to PDS")
+
secret, err := db.GetRegistrationKey(k.Db, domain)
+
l.Error("failed to get registration key", "err", err)
+
ksClient, err := knotclient.NewSignedClient(domain, secret, k.Config.Core.Dev)
+
l.Error("failed to create client", "err", err)
+
ksResp, err := ksClient.AddMember(subjectIdentity.DID.String())
+
l.Error("failed to reach knotserver", "err", err)
+
k.Pages.Notice(w, noticeId, "Failed to reach to knotserver.")
+
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))
+
err = k.Enforcer.AddKnotMember(domain, subjectIdentity.DID.String())
+
l.Error("failed to add member to enforcer", "err", err)
+
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
+
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {