this repo has no description
1package knots
2
3import (
4 "errors"
5 "fmt"
6 "log/slog"
7 "net/http"
8 "slices"
9 "time"
10
11 "github.com/go-chi/chi/v5"
12 "tangled.sh/tangled.sh/core/api/tangled"
13 "tangled.sh/tangled.sh/core/appview/config"
14 "tangled.sh/tangled.sh/core/appview/db"
15 "tangled.sh/tangled.sh/core/appview/middleware"
16 "tangled.sh/tangled.sh/core/appview/oauth"
17 "tangled.sh/tangled.sh/core/appview/pages"
18 "tangled.sh/tangled.sh/core/appview/serververify"
19 "tangled.sh/tangled.sh/core/eventconsumer"
20 "tangled.sh/tangled.sh/core/idresolver"
21 "tangled.sh/tangled.sh/core/rbac"
22 "tangled.sh/tangled.sh/core/tid"
23
24 comatproto "github.com/bluesky-social/indigo/api/atproto"
25 lexutil "github.com/bluesky-social/indigo/lex/util"
26)
27
28type Knots struct {
29 Db *db.DB
30 OAuth *oauth.OAuth
31 Pages *pages.Pages
32 Config *config.Config
33 Enforcer *rbac.Enforcer
34 IdResolver *idresolver.Resolver
35 Logger *slog.Logger
36 Knotstream *eventconsumer.Consumer
37}
38
39func (k *Knots) Router() http.Handler {
40 r := chi.NewRouter()
41
42 r.With(middleware.AuthMiddleware(k.OAuth)).Get("/", k.knots)
43 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/register", k.register)
44
45 r.With(middleware.AuthMiddleware(k.OAuth)).Get("/{domain}", k.dashboard)
46 r.With(middleware.AuthMiddleware(k.OAuth)).Delete("/{domain}", k.delete)
47
48 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/retry", k.retry)
49 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember)
50 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember)
51
52 return r
53}
54
55func (k *Knots) knots(w http.ResponseWriter, r *http.Request) {
56 user := k.OAuth.GetUser(r)
57 registrations, err := db.RegistrationsByDid(k.Db, user.Did)
58 if err != nil {
59 k.Logger.Error("failed to fetch knot registrations", "err", err)
60 w.WriteHeader(http.StatusInternalServerError)
61 return
62 }
63
64 k.Pages.Knots(w, pages.KnotsParams{
65 LoggedInUser: user,
66 Registrations: registrations,
67 })
68}
69
70func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) {
71 l := k.Logger.With("handler", "dashboard")
72
73 user := k.OAuth.GetUser(r)
74 l = l.With("user", user.Did)
75
76 domain := chi.URLParam(r, "domain")
77 if domain == "" {
78 return
79 }
80 l = l.With("domain", domain)
81
82 registrations, err := db.GetRegistrations(
83 k.Db,
84 db.FilterEq("did", user.Did),
85 db.FilterEq("domain", domain),
86 )
87 if err != nil {
88 l.Error("failed to get registrations", "err", err)
89 http.Error(w, "Not found", http.StatusNotFound)
90 return
91 }
92
93 // Find the specific registration for this domain
94 var registration *db.Registration
95 for _, reg := range registrations {
96 if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil {
97 registration = ®
98 break
99 }
100 }
101
102 if registration == nil {
103 l.Error("registration not found or not verified")
104 http.Error(w, "Not found", http.StatusNotFound)
105 return
106 }
107 registration := registrations[0]
108
109 members, err := k.Enforcer.GetUserByRole("server:member", domain)
110 if err != nil {
111 l.Error("failed to get knot members", "err", err)
112 http.Error(w, "Not found", http.StatusInternalServerError)
113 return
114 }
115 slices.Sort(members)
116
117 repos, err := db.GetRepos(
118 k.Db,
119 0,
120 db.FilterEq("knot", domain),
121 )
122 if err != nil {
123 l.Error("failed to get knot repos", "err", err)
124 http.Error(w, "Not found", http.StatusInternalServerError)
125 return
126 }
127
128 // organize repos by did
129 repoMap := make(map[string][]db.Repo)
130 for _, r := range repos {
131 repoMap[r.Did] = append(repoMap[r.Did], r)
132 }
133
134 k.Pages.Knot(w, pages.KnotParams{
135 LoggedInUser: user,
136 Registration: ®istration,
137 Members: members,
138 Repos: repoMap,
139 IsOwner: true,
140 })
141}
142
143func (k *Knots) register(w http.ResponseWriter, r *http.Request) {
144 user := k.OAuth.GetUser(r)
145 l := k.Logger.With("handler", "register")
146
147 noticeId := "register-error"
148 defaultErr := "Failed to register knot. Try again later."
149 fail := func() {
150 k.Pages.Notice(w, noticeId, defaultErr)
151 }
152
153 domain := r.FormValue("domain")
154 if domain == "" {
155 k.Pages.Notice(w, noticeId, "Incomplete form.")
156 return
157 }
158 l = l.With("domain", domain)
159 l = l.With("user", user.Did)
160
161 tx, err := k.Db.Begin()
162 if err != nil {
163 l.Error("failed to start transaction", "err", err)
164 fail()
165 return
166 }
167 defer func() {
168 tx.Rollback()
169 k.Enforcer.E.LoadPolicy()
170 }()
171
172 err = db.AddKnot(tx, domain, user.Did)
173 if err != nil {
174 l.Error("failed to insert", "err", err)
175 fail()
176 return
177 }
178
179 err = k.Enforcer.AddKnot(domain)
180 if err != nil {
181 l.Error("failed to create knot", "err", err)
182 fail()
183 return
184 }
185
186 // create record on pds
187 client, err := k.OAuth.AuthorizedClient(r)
188 if err != nil {
189 l.Error("failed to authorize client", "err", err)
190 fail()
191 return
192 }
193
194 ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain)
195 var exCid *string
196 if ex != nil {
197 exCid = ex.Cid
198 }
199
200 // re-announce by registering under same rkey
201 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
202 Collection: tangled.KnotNSID,
203 Repo: user.Did,
204 Rkey: domain,
205 Record: &lexutil.LexiconTypeDecoder{
206 Val: &tangled.Knot{
207 CreatedAt: time.Now().Format(time.RFC3339),
208 },
209 },
210 SwapRecord: exCid,
211 })
212
213 if err != nil {
214 l.Error("failed to put record", "err", err)
215 fail()
216 return
217 }
218
219 err = tx.Commit()
220 if err != nil {
221 l.Error("failed to commit transaction", "err", err)
222 fail()
223 return
224 }
225
226 err = k.Enforcer.E.SavePolicy()
227 if err != nil {
228 l.Error("failed to update ACL", "err", err)
229 k.Pages.HxRefresh(w)
230 return
231 }
232
233 // begin verification
234 err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
235 if err != nil {
236 l.Error("verification failed", "err", err)
237 k.Pages.HxRefresh(w)
238 return
239 }
240
241 err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
242 if err != nil {
243 l.Error("failed to mark verified", "err", err)
244 k.Pages.HxRefresh(w)
245 return
246 }
247
248 // add this knot to knotstream
249 go k.Knotstream.AddSource(
250 r.Context(),
251 eventconsumer.NewKnotSource(domain),
252 )
253
254 // ok
255 k.Pages.HxRefresh(w)
256}
257
258func (k *Knots) delete(w http.ResponseWriter, r *http.Request) {
259 user := k.OAuth.GetUser(r)
260 l := k.Logger.With("handler", "delete")
261
262 noticeId := "operation-error"
263 defaultErr := "Failed to delete knot. Try again later."
264 fail := func() {
265 k.Pages.Notice(w, noticeId, defaultErr)
266 }
267
268 domain := chi.URLParam(r, "domain")
269 if domain == "" {
270 l.Error("empty domain")
271 fail()
272 return
273 }
274
275 // get record from db first
276 registrations, err := db.GetRegistrations(
277 k.Db,
278 db.FilterEq("did", user.Did),
279 db.FilterEq("domain", domain),
280 )
281 if err != nil {
282 l.Error("failed to get registration", "err", err)
283 fail()
284 return
285 }
286 if len(registrations) != 1 {
287 l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1)
288 fail()
289 return
290 }
291 registration := registrations[0]
292
293 tx, err := k.Db.Begin()
294 if err != nil {
295 l.Error("failed to start txn", "err", err)
296 fail()
297 return
298 }
299 defer func() {
300 tx.Rollback()
301 k.Enforcer.E.LoadPolicy()
302 }()
303
304 err = db.DeleteKnot(
305 tx,
306 db.FilterEq("did", user.Did),
307 db.FilterEq("domain", domain),
308 )
309 if err != nil {
310 l.Error("failed to delete registration", "err", err)
311 fail()
312 return
313 }
314
315 // delete from enforcer if it was registered
316 if registration.Registered != nil {
317 err = k.Enforcer.RemoveKnot(domain)
318 if err != nil {
319 l.Error("failed to update ACL", "err", err)
320 fail()
321 return
322 }
323 }
324
325 client, err := k.OAuth.AuthorizedClient(r)
326 if err != nil {
327 l.Error("failed to authorize client", "err", err)
328 fail()
329 return
330 }
331
332 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
333 Collection: tangled.KnotNSID,
334 Repo: user.Did,
335 Rkey: domain,
336 })
337 if err != nil {
338 // non-fatal
339 l.Error("failed to delete record", "err", err)
340 }
341
342 err = tx.Commit()
343 if err != nil {
344 l.Error("failed to delete knot", "err", err)
345 fail()
346 return
347 }
348
349 err = k.Enforcer.E.SavePolicy()
350 if err != nil {
351 l.Error("failed to update ACL", "err", err)
352 k.Pages.HxRefresh(w)
353 return
354 }
355
356 shouldRedirect := r.Header.Get("shouldRedirect")
357 if shouldRedirect == "true" {
358 k.Pages.HxRedirect(w, "/knots")
359 return
360 }
361
362 w.Write([]byte{})
363}
364
365func (k *Knots) retry(w http.ResponseWriter, r *http.Request) {
366 user := k.OAuth.GetUser(r)
367 l := k.Logger.With("handler", "retry")
368
369 noticeId := "operation-error"
370 defaultErr := "Failed to verify knot. Try again later."
371 fail := func() {
372 k.Pages.Notice(w, noticeId, defaultErr)
373 }
374
375 domain := chi.URLParam(r, "domain")
376 if domain == "" {
377 l.Error("empty domain")
378 fail()
379 return
380 }
381 l = l.With("domain", domain)
382 l = l.With("user", user.Did)
383
384 // get record from db first
385 registrations, err := db.GetRegistrations(
386 k.Db,
387 db.FilterEq("did", user.Did),
388 db.FilterEq("domain", domain),
389 )
390 if err != nil {
391 l.Error("failed to get registration", "err", err)
392 fail()
393 return
394 }
395 if len(registrations) != 1 {
396 l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1)
397 fail()
398 return
399 }
400 registration := registrations[0]
401
402 // begin verification
403 err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev)
404 if err != nil {
405 l.Error("verification failed", "err", err)
406
407 if errors.Is(err, serververify.FetchError) {
408 k.Pages.Notice(w, noticeId, "Failed to verify knot, unable to fetch owner.")
409 return
410 }
411
412 if e, ok := err.(*serververify.OwnerMismatch); ok {
413 k.Pages.Notice(w, noticeId, e.Error())
414 return
415 }
416
417 fail()
418 return
419 }
420
421 err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did)
422 if err != nil {
423 l.Error("failed to mark verified", "err", err)
424 k.Pages.Notice(w, noticeId, err.Error())
425 return
426 }
427
428 // if this knot was previously read-only, then emit a record too
429 //
430 // this is part of migrating from the old knot system to the new one
431 if registration.ReadOnly {
432 // re-announce by registering under same rkey
433 client, err := k.OAuth.AuthorizedClient(r)
434 if err != nil {
435 l.Error("failed to authorize client", "err", err)
436 fail()
437 return
438 }
439
440 ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain)
441 var exCid *string
442 if ex != nil {
443 exCid = ex.Cid
444 }
445
446 // ignore the error here
447 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
448 Collection: tangled.KnotNSID,
449 Repo: user.Did,
450 Rkey: domain,
451 Record: &lexutil.LexiconTypeDecoder{
452 Val: &tangled.Knot{
453 CreatedAt: time.Now().Format(time.RFC3339),
454 },
455 },
456 SwapRecord: exCid,
457 })
458 if err != nil {
459 l.Error("non-fatal: failed to reannouce knot", "err", err)
460 }
461 }
462
463 // add this knot to knotstream
464 go k.Knotstream.AddSource(
465 r.Context(),
466 eventconsumer.NewKnotSource(domain),
467 )
468
469 shouldRefresh := r.Header.Get("shouldRefresh")
470 if shouldRefresh == "true" {
471 k.Pages.HxRefresh(w)
472 return
473 }
474
475 // Get updated registration to show
476 registrations, err = db.GetRegistrations(
477 k.Db,
478 db.FilterEq("did", user.Did),
479 db.FilterEq("domain", domain),
480 )
481 if err != nil {
482 l.Error("failed to get registration", "err", err)
483 fail()
484 return
485 }
486 if len(registrations) != 1 {
487 l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1)
488 fail()
489 return
490 }
491 updatedRegistration := registrations[0]
492
493 log.Println(updatedRegistration)
494
495 w.Header().Set("HX-Reswap", "outerHTML")
496 k.Pages.KnotListing(w, pages.KnotListingParams{
497 Registration: &updatedRegistration,
498 })
499}
500
501func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) {
502 user := k.OAuth.GetUser(r)
503 l := k.Logger.With("handler", "addMember")
504
505 domain := chi.URLParam(r, "domain")
506 if domain == "" {
507 l.Error("empty domain")
508 http.Error(w, "Not found", http.StatusNotFound)
509 return
510 }
511 l = l.With("domain", domain)
512 l = l.With("user", user.Did)
513
514 registrations, err := db.GetRegistrations(
515 k.Db,
516 db.FilterEq("did", user.Did),
517 db.FilterEq("domain", domain),
518 db.FilterIsNot("registered", "null"),
519 )
520 if err != nil {
521 l.Error("failed to retrieve domain registration", "err", err)
522 http.Error(w, "Not found", http.StatusNotFound)
523 return
524 }
525
526 noticeId := fmt.Sprintf("add-member-error-%d", registration.Id)
527 defaultErr := "Failed to add member. Try again later."
528 fail := func() {
529 k.Pages.Notice(w, noticeId, defaultErr)
530 }
531
532 member := r.FormValue("member")
533 if member == "" {
534 l.Error("empty member")
535 k.Pages.Notice(w, noticeId, "Failed to add member, empty form.")
536 return
537 }
538 l = l.With("member", member)
539
540 memberId, err := k.IdResolver.ResolveIdent(r.Context(), member)
541 if err != nil {
542 l.Error("failed to resolve member identity to handle", "err", err)
543 k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
544 return
545 }
546 if memberId.Handle.IsInvalidHandle() {
547 l.Error("failed to resolve member identity to handle")
548 k.Pages.Notice(w, noticeId, "Failed to add member, identity resolution failed.")
549 return
550 }
551
552 // write to pds
553 client, err := k.OAuth.AuthorizedClient(r)
554 if err != nil {
555 l.Error("failed to authorize client", "err", err)
556 fail()
557 return
558 }
559
560 rkey := tid.TID()
561
562 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
563 Collection: tangled.KnotMemberNSID,
564 Repo: user.Did,
565 Rkey: rkey,
566 Record: &lexutil.LexiconTypeDecoder{
567 Val: &tangled.KnotMember{
568 CreatedAt: time.Now().Format(time.RFC3339),
569 Domain: domain,
570 Subject: memberId.DID.String(),
571 },
572 },
573 })
574 if err != nil {
575 l.Error("failed to add record to PDS", "err", err)
576 k.Pages.Notice(w, noticeId, "Failed to add record to PDS, try again later.")
577 return
578 }
579
580 err = k.Enforcer.AddKnotMember(domain, memberId.DID.String())
581 if err != nil {
582 l.Error("failed to add member to ACLs", "err", err)
583 fail()
584 return
585 }
586
587 err = k.Enforcer.E.SavePolicy()
588 if err != nil {
589 l.Error("failed to save ACL policy", "err", err)
590 fail()
591 return
592 }
593
594 // success
595 k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
596}
597
598func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
599 user := k.OAuth.GetUser(r)
600 l := k.Logger.With("handler", "removeMember")
601
602 noticeId := "operation-error"
603 defaultErr := "Failed to remove member. Try again later."
604 fail := func() {
605 k.Pages.Notice(w, noticeId, defaultErr)
606 }
607
608 domain := chi.URLParam(r, "domain")
609 if domain == "" {
610 l.Error("empty domain")
611 fail()
612 return
613 }
614 l = l.With("domain", domain)
615 l = l.With("user", user.Did)
616
617 registrations, err := db.GetRegistrations(
618 k.Db,
619 db.FilterEq("did", user.Did),
620 db.FilterEq("domain", domain),
621 db.FilterIsNot("registered", "null"),
622 )
623 if err != nil {
624 l.Error("failed to get registration", "err", err)
625 return
626 }
627 if len(registrations) != 1 {
628 l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1)
629 return
630 }
631
632 member := r.FormValue("member")
633 if member == "" {
634 l.Error("empty member")
635 k.Pages.Notice(w, noticeId, "Failed to remove member, empty form.")
636 return
637 }
638 l = l.With("member", member)
639
640 memberId, err := k.IdResolver.ResolveIdent(r.Context(), member)
641 if err != nil {
642 l.Error("failed to resolve member identity to handle", "err", err)
643 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
644 return
645 }
646 if memberId.Handle.IsInvalidHandle() {
647 l.Error("failed to resolve member identity to handle")
648 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.")
649 return
650 }
651
652 // remove from enforcer
653 err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
654 if err != nil {
655 l.Error("failed to update ACLs", "err", err)
656 fail()
657 return
658 }
659
660 client, err := k.OAuth.AuthorizedClient(r)
661 if err != nil {
662 l.Error("failed to authorize client", "err", err)
663 fail()
664 return
665 }
666
667 // TODO: We need to track the rkey for knot members to delete the record
668 // For now, just remove from ACLs
669 _ = client
670
671 // commit everything
672 err = k.Enforcer.E.SavePolicy()
673 if err != nil {
674 l.Error("failed to save ACLs", "err", err)
675 fail()
676 return
677 }
678
679 // ok
680 k.Pages.HxRefresh(w)
681}