···
"github.com/go-chi/chi/v5"
···
"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/idresolver"
-
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/tid"
···
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{
···
-
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")
-
registration, err := db.RegistrationByDomain(k.Db, domain)
-
l.Error("failed to get registration for domain", "err", err)
-
if registration.ByDid != user.Did {
-
l.Error("unauthorized", "wantedDid", registration.ByDid, "gotDid", user.Did)
-
w.WriteHeader(http.StatusUnauthorized)
-
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) {
···
"github.com/go-chi/chi/v5"
···
"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/appview/serververify"
"tangled.sh/tangled.sh/core/eventconsumer"
"tangled.sh/tangled.sh/core/idresolver"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/tid"
···
Knotstream *eventconsumer.Consumer
+
func (k *Knots) Router() http.Handler {
+
r.With(middleware.AuthMiddleware(k.OAuth)).Get("/", k.knots)
+
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/register", k.register)
+
r.With(middleware.AuthMiddleware(k.OAuth)).Get("/{domain}", k.dashboard)
+
r.With(middleware.AuthMiddleware(k.OAuth)).Delete("/{domain}", k.delete)
+
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry)
+
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
+
r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
+
func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
user := k.OAuth.GetUser(r)
registrations, err := db.RegistrationsByDid(k.Db, user.Did)
+
k.Logger.Error("failed to fetch knot registrations", "err", err)
+
w.WriteHeader(http.StatusInternalServerError)
k.Pages.Knots(w, pages.KnotsParams{
···
+
func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
+
l := k.Logger.With("handler", "dashboard")
user := k.OAuth.GetUser(r)
+
l = l.With("user", user.Did)
+
domain := chi.URLParam(r, "domain")
l = l.With("domain", domain)
+
registrations, err := db.RegistrationsByDid(k.Db, user.Did)
+
l.Error("failed to get registrations", "err", err)
+
http.Error(w, "Not found", http.StatusNotFound)
+
// Find the specific registration for this domain
+
var registration *db.Registration
+
for _, reg := range registrations {
+
if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil {
+
if registration == nil {
+
l.Error("registration not found or not verified")
+
http.Error(w, "Not found", http.StatusNotFound)
+
members, err := k.Enforcer.GetUserByRole("server:member", domain)
+
l.Error("failed to get knot members", "err", err)
+
http.Error(w, "Not found", http.StatusInternalServerError)
+
repos, err := db.GetRepos(
+
db.FilterEq("knot", domain),
+
l.Error("failed to get knot repos", "err", err)
+
http.Error(w, "Not found", http.StatusInternalServerError)
+
identsToResolve := make([]string, len(members))
+
copy(identsToResolve, members)
+
resolvedIds := k.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)
+
k.Pages.Knot(w, pages.KnotParams{
+
Registration: registration,
+
DidHandleMap: didHandleMap,
+
func (k *Knots) register(w http.ResponseWriter, r *http.Request) {
user := k.OAuth.GetUser(r)
+
l := k.Logger.With("handler", "register")
+
noticeId := "register-error"
+
defaultErr := "Failed to register knot. Try again later."
k.Pages.Notice(w, noticeId, defaultErr)
+
domain := r.FormValue("domain")
+
k.Pages.Notice(w, noticeId, "Incomplete form.")
l = l.With("domain", domain)
+
l = l.With("user", user.Did)
+
tx, err := k.Db.Begin()
+
l.Error("failed to start transaction", "err", err)
+
k.Enforcer.E.LoadPolicy()
+
err = db.AddKnot(tx, domain, user.Did)
+
l.Error("failed to insert", "err", err)
+
err = k.Enforcer.AddKnot(domain)
+
l.Error("failed to create knot", "err", err)
+
// create record on pds
+
client, err := k.OAuth.AuthorizedClient(r)
+
l.Error("failed to authorize client", "err", err)
+
ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain)
+
// re-announce by registering under same rkey
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
+
Collection: tangled.KnotNSID,
+
Record: &lexutil.LexiconTypeDecoder{
+
CreatedAt: time.Now().Format(time.RFC3339),
+
l.Error("failed to put record", "err", err)
+
l.Error("failed to commit transaction", "err", err)
+
err = k.Enforcer.E.SavePolicy()
+
l.Error("failed to update ACL", "err", err)
+
err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
+
l.Error("verification failed", "err", err)
+
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
+
l.Error("failed to mark verified", "err", err)
+
// add this knot to knotstream
+
go k.Knotstream.AddSource(
+
eventconsumer.NewKnotSource(domain),
+
func (k *Knots) delete(w http.ResponseWriter, r *http.Request) {
+
user := k.OAuth.GetUser(r)
+
l := k.Logger.With("handler", "delete")
+
noticeId := "operation-error"
+
defaultErr := "Failed to delete knot. Try again later."
+
k.Pages.Notice(w, noticeId, defaultErr)
+
domain := chi.URLParam(r, "domain")
+
l.Error("empty domain")
+
registration, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed to retrieve domain registration", "err", err)
+
if registration.ByDid != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid)
+
k.Pages.Notice(w, noticeId, "Failed to delete knot, unauthorized deletion attempt.")
+
tx, err := k.Db.Begin()
+
l.Error("failed to start txn", "err", err)
+
k.Enforcer.E.LoadPolicy()
+
db.FilterEq("did", user.Did),
+
db.FilterEq("domain", domain),
+
l.Error("failed to delete registration", "err", err)
+
// delete from enforcer if it was registered
+
if registration.Registered != nil {
+
err = k.Enforcer.RemoveKnot(domain)
+
l.Error("failed to update ACL", "err", err)
+
client, err := k.OAuth.AuthorizedClient(r)
+
l.Error("failed to authorize client", "err", err)
+
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.KnotNSID,
+
l.Error("failed to delete record", "err", err)
+
l.Error("failed to delete knot", "err", err)
err = k.Enforcer.E.SavePolicy()
+
l.Error("failed to update ACL", "err", err)
+
shouldRedirect := r.Header.Get("shouldRedirect")
+
if shouldRedirect == "true" {
+
k.Pages.HxRedirect(w, "/knots")
+
func (k *Knots) retry(w http.ResponseWriter, r *http.Request) {
+
user := k.OAuth.GetUser(r)
+
l := k.Logger.With("handler", "retry")
+
noticeId := "operation-error"
+
defaultErr := "Failed to verify knot. Try again later."
+
k.Pages.Notice(w, noticeId, defaultErr)
domain := chi.URLParam(r, "domain")
+
l.Error("empty domain")
l = l.With("domain", domain)
+
l = l.With("user", user.Did)
+
registration, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed to retrieve domain registration", "err", err)
+
if registration.ByDid != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid)
+
k.Pages.Notice(w, noticeId, "Failed to verify knot, unauthorized verification attempt.")
+
err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
+
l.Error("verification failed", "err", err)
+
if errors.Is(err, serververify.FetchError) {
+
k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
+
if e, ok := err.(*serververify.OwnerMismatch); ok {
+
k.Pages.Notice(w, noticeId, e.Error())
+
err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
+
l.Error("failed to mark verified", "err", err)
+
k.Pages.Notice(w, noticeId, err.Error())
+
// add this knot to knotstream
+
go k.Knotstream.AddSource(
+
eventconsumer.NewKnotSource(domain),
+
shouldRefresh := r.Header.Get("shouldRefresh")
+
if shouldRefresh == "true" {
+
// Get updated registration to show
+
updatedRegistration, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed get updated registration", "err", err)
+
w.Header().Set("HX-Reswap", "outerHTML")
+
k.Pages.KnotListing(w, pages.KnotListingParams{
+
Registration: *updatedRegistration,
func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
+
user := k.OAuth.GetUser(r)
+
l := k.Logger.With("handler", "addMember")
domain := chi.URLParam(r, "domain")
+
l.Error("empty domain")
+
http.Error(w, "Not found", http.StatusNotFound)
l = l.With("domain", domain)
+
l = l.With("user", user.Did)
+
registration, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed to retrieve domain registration", "err", err)
+
http.Error(w, "Not found", http.StatusNotFound)
+
noticeId := fmt.Sprintf("add-member-error-%d", registration.Id)
defaultErr := "Failed to add member. Try again later."
k.Pages.Notice(w, noticeId, defaultErr)
+
if registration.ByDid != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid)
+
k.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.")
+
member := r.FormValue("member")
+
l.Error("empty member")
+
k.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
+
l = l.With("member", member)
+
memberId, err := k.IdResolver.ResolveIdent(r.Context(), member)
+
l.Error("failed to resolve member identity to handle", "err", err)
+
k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
+
if memberId.Handle.IsInvalidHandle() {
+
l.Error("failed to resolve member identity to handle")
k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
client, err := k.OAuth.AuthorizedClient(r)
+
l.Error("failed to authorize client", "err", err)
+
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
Collection: tangled.KnotMemberNSID,
Record: &lexutil.LexiconTypeDecoder{
Val: &tangled.KnotMember{
+
CreatedAt: time.Now().Format(time.RFC3339),
+
Subject: memberId.DID.String(),
+
l.Error("failed to add record to PDS", "err", err)
+
k.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.")
+
err = k.Enforcer.AddKnotMember(domain, memberId.DID.String())
+
l.Error("failed to add member to ACLs", "err", err)
+
err = k.Enforcer.E.SavePolicy()
+
l.Error("failed to save ACL policy", "err", err)
+
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
+
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
+
user := k.OAuth.GetUser(r)
+
l := k.Logger.With("handler", "removeMember")
+
noticeId := "operation-error"
+
defaultErr := "Failed to remove member. Try again later."
+
k.Pages.Notice(w, noticeId, defaultErr)
+
domain := chi.URLParam(r, "domain")
+
l.Error("empty domain")
+
l = l.With("domain", domain)
+
l = l.With("user", user.Did)
+
registration, err := db.RegistrationByDomain(k.Db, domain)
+
l.Error("failed to retrieve domain registration", "err", err)
+
if registration.ByDid != user.Did {
+
l.Error("unauthorized", "user", user.Did, "owner", registration.ByDid)
+
k.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.")
+
member := r.FormValue("member")
+
l.Error("empty member")
+
k.Pages.Notice(w, noticeId, "Failed to remove member, empty form.")
+
l = l.With("member", member)
+
memberId, err := k.IdResolver.ResolveIdent(r.Context(), member)
+
l.Error("failed to resolve member identity to handle", "err", err)
+
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
+
if memberId.Handle.IsInvalidHandle() {
+
l.Error("failed to resolve member identity to handle")
+
k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
+
// remove from enforcer
+
err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
+
l.Error("failed to update ACLs", "err", err)
+
client, err := k.OAuth.AuthorizedClient(r)
+
l.Error("failed to authorize client", "err", err)
+
// TODO: We need to track the rkey for knot members to delete the record
+
// For now, just remove from ACLs
+
err = k.Enforcer.E.SavePolicy()
+
l.Error("failed to save ACLs", "err", err)