Monorepo for Tangled tangled.org

add /knots/{domain} pages

Changed files
+203 -13
appview
+1 -5
appview/db/db.go
··· 189 189 where domain = ?; 190 190 `, domain) 191 191 192 - if err != nil { 193 - return err 194 - } 195 - 196 - return nil 192 + return err 197 193 }
+37
appview/pages/knot.html
··· 1 + {{define "title"}}knot{{end}} 2 + 3 + {{define "content"}} 4 + <a href="/">back to timeline</a> 5 + <a href="/knots">back to all knots</a> 6 + <h1>{{.Registration.Domain}}</h1> 7 + <p> 8 + <code> 9 + opened by: {{.Registration.ByDid}} 10 + {{ if $.IsOwner }} 11 + (you) 12 + {{ end }} 13 + </code><br> 14 + <code>on: {{.Registration.Created}}</code><br> 15 + {{ if .Registration.Registered }} 16 + <code>registered on: {{.Registration.Registered}}</code> 17 + {{ else }} 18 + <code>pending registration</code> 19 + <button hx-post="/knots/{{.Domain}}/init">initialize</button> 20 + {{ end }} 21 + </p> 22 + 23 + {{ if .Registration.Registered }} 24 + <h3> members </h3> 25 + {{ range $.Members }} 26 + <ol> 27 + <li>{{.}}</li> 28 + </ol> 29 + {{ else }} 30 + <p>no members</p> 31 + {{ end }} 32 + {{ end }} 33 + 34 + {{ if $.IsOwner }} 35 + <a href="/knots/{{.Registration.Domain}}/member">add member</a> 36 + {{ end }} 37 + {{end}}
+20 -7
appview/pages/knots.html
··· 13 13 <button type="domain">generate key</button> 14 14 </form> 15 15 16 - <h3>existing registrations</h3> 17 - <ul id="registrations"> 16 + <h3>my knots</h3> 17 + <ul id="my-knots"> 18 + {{range .Registrations}} 19 + {{ if .Registered }} 20 + <li> 21 + <p>domain: <a href="/knots/{{.Domain}}">{{.Domain}}</a></p><br> 22 + <code>opened by: {{.ByDid}}</code><br> 23 + <code>on: {{.Created}}</code><br> 24 + <code>registered on: {{.Registered}}</code> 25 + </li> 26 + {{ end }} 27 + {{else}} 28 + <p>you don't have any knots yet</p> 29 + {{end}} 30 + </ul> 31 + <h3>pending registrations</h3> 32 + <ul id="pending-registrations"> 18 33 {{range .Registrations}} 34 + {{ if not .Registered }} 19 35 <li> 20 36 <code>domain: {{.Domain}}</code><br> 21 37 <code>opened by: {{.ByDid}}</code><br> 22 38 <code>on: {{.Created}}</code><br> 23 - {{if .Registered}} 24 - <code>registered on: {{.Registered}}</code> 25 - {{else}} 26 39 <code>pending registration</code> 27 - <button hx-post="/knots/init/{{.Domain}}">initialize</button> 28 - {{end}} 40 + <button hx-post="/knots/{{.Domain}}/init">initialize</button> 29 41 </li> 42 + {{ end }} 30 43 {{else}} 31 44 <p>no registrations yet</p> 32 45 {{end}}
+11
appview/pages/pages.go
··· 66 66 func Knots(w io.Writer, p KnotsParams) error { 67 67 return parse("knots.html").Execute(w, p) 68 68 } 69 + 70 + type KnotParams struct { 71 + User *auth.User 72 + Registration *db.Registration 73 + Members []string 74 + IsOwner bool 75 + } 76 + 77 + func Knot(w io.Writer, p KnotParams) error { 78 + return parse("knot.html").Execute(w, p) 79 + }
+31
appview/state/middleware.go
··· 7 7 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/xrpc" 10 + "github.com/go-chi/chi/v5" 10 11 "github.com/sotangled/tangled/appview" 11 12 "github.com/sotangled/tangled/appview/auth" 12 13 ) ··· 68 69 }) 69 70 } 70 71 } 72 + 73 + func RoleMiddleware(s *State, group string) Middleware { 74 + return func(next http.Handler) http.Handler { 75 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 76 + // requires auth also 77 + actor := s.auth.GetUser(r) 78 + if actor == nil { 79 + // we need a logged in user 80 + log.Printf("not logged in, redirecting") 81 + http.Error(w, "Forbiden", http.StatusUnauthorized) 82 + return 83 + } 84 + domain := chi.URLParam(r, "domain") 85 + if domain == "" { 86 + http.Error(w, "malformed url", http.StatusBadRequest) 87 + return 88 + } 89 + 90 + ok, err := s.enforcer.E.HasGroupingPolicy(actor.Did, group, domain) 91 + if err != nil || !ok { 92 + // we need a logged in user 93 + log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain) 94 + http.Error(w, "Forbiden", http.StatusUnauthorized) 95 + return 96 + } 97 + 98 + next.ServeHTTP(w, r) 99 + }) 100 + } 101 + }
+103 -1
appview/state/state.go
··· 7 7 "fmt" 8 8 "log" 9 9 "net/http" 10 + "strings" 10 11 "time" 11 12 12 13 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 283 284 w.Write([]byte("check success")) 284 285 } 285 286 287 + func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 288 + domain := chi.URLParam(r, "domain") 289 + if domain == "" { 290 + http.Error(w, "malformed url", http.StatusBadRequest) 291 + return 292 + } 293 + 294 + user := s.auth.GetUser(r) 295 + reg, err := s.db.RegistrationByDomain(domain) 296 + if err != nil { 297 + w.Write([]byte("failed to pull up registration info")) 298 + return 299 + } 300 + 301 + var members []string 302 + if reg.Registered != nil { 303 + members, err = s.enforcer.E.GetUsersForRole("server:member", domain) 304 + if err != nil { 305 + w.Write([]byte("failed to fetch member list")) 306 + return 307 + } 308 + } 309 + 310 + ok, err := s.enforcer.E.HasGroupingPolicy(user.Did, "server:owner", domain) 311 + isOwner := err == nil && ok 312 + 313 + p := pages.KnotParams{ 314 + User: user, 315 + Registration: reg, 316 + Members: members, 317 + IsOwner: isOwner, 318 + } 319 + 320 + pages.Knot(w, p) 321 + } 322 + 286 323 // get knots registered by this user 287 324 func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 288 325 // for now, this is just pubkeys ··· 298 335 }) 299 336 } 300 337 338 + // list members of domain, requires auth and requires owner status 339 + func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 340 + domain := chi.URLParam(r, "domain") 341 + if domain == "" { 342 + http.Error(w, "malformed url", http.StatusBadRequest) 343 + return 344 + } 345 + 346 + // list all members for this domain 347 + memberDids, err := s.enforcer.E.GetUsersForRole("server:member", domain) 348 + if err != nil { 349 + w.Write([]byte("failed to fetch member list")) 350 + return 351 + } 352 + 353 + w.Write([]byte(strings.Join(memberDids, "\n"))) 354 + return 355 + } 356 + 357 + // add member to domain, requires auth and requires invite access 358 + func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 359 + domain := chi.URLParam(r, "domain") 360 + if domain == "" { 361 + http.Error(w, "malformed url", http.StatusBadRequest) 362 + return 363 + } 364 + 365 + memberDid := r.FormValue("member") 366 + if memberDid == "" { 367 + http.Error(w, "malformed form", http.StatusBadRequest) 368 + return 369 + } 370 + 371 + // TODO: validate member did? 372 + memberIdent, err := auth.ResolveIdent(r.Context(), memberDid) 373 + if err != nil { 374 + w.Write([]byte("failed to resolve member did to a handle")) 375 + return 376 + } 377 + 378 + log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 379 + 380 + err = s.enforcer.AddMember(domain, memberDid) 381 + if err != nil { 382 + w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 383 + return 384 + } 385 + 386 + w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 387 + } 388 + 389 + // list members of domain, requires auth and requires owner status 390 + func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 391 + } 392 + 301 393 func buildPingRequest(url, secret string) (*http.Request, error) { 302 394 pingRequest, err := http.NewRequest("GET", url, nil) 303 395 if err != nil { ··· 327 419 r.Route("/knots", func(r chi.Router) { 328 420 r.Use(AuthMiddleware(s)) 329 421 r.Get("/", s.Knots) 330 - r.Post("/init/{domain}", s.InitKnotServer) 331 422 r.Post("/key", s.RegistrationKey) 423 + 424 + r.Route("/{domain}", func(r chi.Router) { 425 + r.Get("/", s.KnotServerInfo) 426 + r.Post("/init", s.InitKnotServer) 427 + r.Route("/member", func(r chi.Router) { 428 + r.Use(RoleMiddleware(s, "server:owner")) 429 + r.Get("/", s.ListMembers) 430 + r.Put("/", s.AddMember) 431 + r.Delete("/", s.RemoveMember) 432 + }) 433 + }) 332 434 }) 333 435 334 436 r.Group(func(r chi.Router) {