+1
-5
appview/db/db.go
+1
-5
appview/db/db.go
+37
appview/pages/knot.html
+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
+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
+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
+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
+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) {