this repo has no description
1package state
2
3import (
4 "context"
5 "crypto/hmac"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "fmt"
10 "log"
11 "log/slog"
12 "net/http"
13 "strings"
14 "time"
15
16 comatproto "github.com/bluesky-social/indigo/api/atproto"
17 "github.com/bluesky-social/indigo/atproto/syntax"
18 lexutil "github.com/bluesky-social/indigo/lex/util"
19 "github.com/bluesky-social/jetstream/pkg/models"
20 securejoin "github.com/cyphar/filepath-securejoin"
21 "github.com/go-chi/chi/v5"
22 tangled "github.com/sotangled/tangled/api/tangled"
23 "github.com/sotangled/tangled/appview"
24 "github.com/sotangled/tangled/appview/auth"
25 "github.com/sotangled/tangled/appview/db"
26 "github.com/sotangled/tangled/appview/pages"
27 "github.com/sotangled/tangled/jetstream"
28 "github.com/sotangled/tangled/rbac"
29)
30
31type State struct {
32 db *db.DB
33 auth *auth.Auth
34 enforcer *rbac.Enforcer
35 tidClock *syntax.TIDClock
36 pages *pages.Pages
37 resolver *appview.Resolver
38 jc *jetstream.JetstreamClient
39}
40
41func Make() (*State, error) {
42 db, err := db.Make(appview.SqliteDbPath)
43 if err != nil {
44 return nil, err
45 }
46
47 auth, err := auth.Make()
48 if err != nil {
49 return nil, err
50 }
51
52 enforcer, err := rbac.NewEnforcer(appview.SqliteDbPath)
53 if err != nil {
54 return nil, err
55 }
56
57 clock := syntax.NewTIDClock(0)
58
59 pgs := pages.NewPages()
60
61 resolver := appview.NewResolver()
62
63 jc, err := jetstream.NewJetstreamClient("appview", []string{tangled.GraphFollowNSID}, nil, slog.Default(), db, false)
64 if err != nil {
65 return nil, fmt.Errorf("failed to create jetstream client: %w", err)
66 }
67 err = jc.StartJetstream(context.Background(), func(ctx context.Context, e *models.Event) error {
68 if e.Kind != models.EventKindCommit {
69 return nil
70 }
71
72 did := e.Did
73 raw := json.RawMessage(e.Commit.Record)
74
75 switch e.Commit.Collection {
76 case tangled.GraphFollowNSID:
77 record := tangled.GraphFollow{}
78 err := json.Unmarshal(raw, &record)
79 if err != nil {
80 log.Println("invalid record")
81 return err
82 }
83 err = db.AddFollow(did, record.Subject, e.Commit.RKey)
84 if err != nil {
85 return fmt.Errorf("failed to add follow to db: %w", err)
86 }
87 return db.UpdateLastTimeUs(e.TimeUS)
88 }
89
90 return nil
91 })
92 if err != nil {
93 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err)
94 }
95
96 state := &State{
97 db,
98 auth,
99 enforcer,
100 clock,
101 pgs,
102 resolver,
103 jc,
104 }
105
106 return state, nil
107}
108
109func (s *State) TID() string {
110 return s.tidClock.Next().String()
111}
112
113func (s *State) Login(w http.ResponseWriter, r *http.Request) {
114 ctx := r.Context()
115
116 switch r.Method {
117 case http.MethodGet:
118 err := s.pages.Login(w, pages.LoginParams{})
119 if err != nil {
120 log.Printf("rendering login page: %s", err)
121 }
122 return
123 case http.MethodPost:
124 handle := strings.TrimPrefix(r.FormValue("handle"), "@")
125 appPassword := r.FormValue("app_password")
126
127 resolved, err := s.resolver.ResolveIdent(ctx, handle)
128 if err != nil {
129 log.Println("failed to resolve handle:", err)
130 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle))
131 return
132 }
133
134 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword)
135 if err != nil {
136 s.pages.Notice(w, "login-msg", "Invalid handle or password.")
137 return
138 }
139 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession}
140
141 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint())
142 if err != nil {
143 s.pages.Notice(w, "login-msg", "Failed to login, try again later.")
144 return
145 }
146
147 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did)
148 s.pages.HxRedirect(w, "/")
149 return
150 }
151}
152
153func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
154 s.auth.ClearSession(r, w)
155 s.pages.HxRedirect(w, "/")
156}
157
158func (s *State) Timeline(w http.ResponseWriter, r *http.Request) {
159 user := s.auth.GetUser(r)
160
161 timeline, err := s.db.MakeTimeline()
162 if err != nil {
163 log.Println(err)
164 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.")
165 }
166
167 var didsToResolve []string
168 for _, ev := range timeline {
169 if ev.Repo != nil {
170 didsToResolve = append(didsToResolve, ev.Repo.Did)
171 }
172 if ev.Follow != nil {
173 didsToResolve = append(didsToResolve, ev.Follow.UserDid)
174 didsToResolve = append(didsToResolve, ev.Follow.SubjectDid)
175 }
176 }
177
178 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
179 didHandleMap := make(map[string]string)
180 for _, identity := range resolvedIds {
181 if !identity.Handle.IsInvalidHandle() {
182 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
183 } else {
184 didHandleMap[identity.DID.String()] = identity.DID.String()
185 }
186 }
187
188 s.pages.Timeline(w, pages.TimelineParams{
189 LoggedInUser: user,
190 Timeline: timeline,
191 DidHandleMap: didHandleMap,
192 })
193
194 return
195}
196
197// requires auth
198func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) {
199 switch r.Method {
200 case http.MethodGet:
201 // list open registrations under this did
202
203 return
204 case http.MethodPost:
205 session, err := s.auth.Store.Get(r, appview.SessionName)
206 if err != nil || session.IsNew {
207 log.Println("unauthorized attempt to generate registration key")
208 http.Error(w, "Forbidden", http.StatusUnauthorized)
209 return
210 }
211
212 did := session.Values[appview.SessionDid].(string)
213
214 // check if domain is valid url, and strip extra bits down to just host
215 domain := r.FormValue("domain")
216 if domain == "" {
217 http.Error(w, "Invalid form", http.StatusBadRequest)
218 return
219 }
220
221 key, err := s.db.GenerateRegistrationKey(domain, did)
222
223 if err != nil {
224 log.Println(err)
225 http.Error(w, "unable to register this domain", http.StatusNotAcceptable)
226 return
227 }
228
229 w.Write([]byte(key))
230 }
231}
232
233func (s *State) Keys(w http.ResponseWriter, r *http.Request) {
234 user := chi.URLParam(r, "user")
235 user = strings.TrimPrefix(user, "@")
236
237 if user == "" {
238 w.WriteHeader(http.StatusBadRequest)
239 return
240 }
241
242 id, err := s.resolver.ResolveIdent(r.Context(), user)
243 if err != nil {
244 w.WriteHeader(http.StatusInternalServerError)
245 return
246 }
247
248 pubKeys, err := s.db.GetPublicKeys(id.DID.String())
249 if err != nil {
250 w.WriteHeader(http.StatusNotFound)
251 return
252 }
253
254 if len(pubKeys) == 0 {
255 w.WriteHeader(http.StatusNotFound)
256 return
257 }
258
259 for _, k := range pubKeys {
260 key := strings.TrimRight(k.Key, "\n")
261 w.Write([]byte(fmt.Sprintln(key)))
262 }
263}
264
265// create a signed request and check if a node responds to that
266func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) {
267 user := s.auth.GetUser(r)
268
269 domain := chi.URLParam(r, "domain")
270 if domain == "" {
271 http.Error(w, "malformed url", http.StatusBadRequest)
272 return
273 }
274 log.Println("checking ", domain)
275
276 secret, err := s.db.GetRegistrationKey(domain)
277 if err != nil {
278 log.Printf("no key found for domain %s: %s\n", domain, err)
279 return
280 }
281
282 client, err := NewSignedClient(domain, secret)
283 if err != nil {
284 log.Println("failed to create client to ", domain)
285 }
286
287 resp, err := client.Init(user.Did)
288 if err != nil {
289 w.Write([]byte("no dice"))
290 log.Println("domain was unreachable after 5 seconds")
291 return
292 }
293
294 if resp.StatusCode == http.StatusConflict {
295 log.Println("status conflict", resp.StatusCode)
296 w.Write([]byte("already registered, sorry!"))
297 return
298 }
299
300 if resp.StatusCode != http.StatusNoContent {
301 log.Println("status nok", resp.StatusCode)
302 w.Write([]byte("no dice"))
303 return
304 }
305
306 // verify response mac
307 signature := resp.Header.Get("X-Signature")
308 signatureBytes, err := hex.DecodeString(signature)
309 if err != nil {
310 return
311 }
312
313 expectedMac := hmac.New(sha256.New, []byte(secret))
314 expectedMac.Write([]byte("ok"))
315
316 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) {
317 log.Printf("response body signature mismatch: %x\n", signatureBytes)
318 return
319 }
320
321 // mark as registered
322 err = s.db.Register(domain)
323 if err != nil {
324 log.Println("failed to register domain", err)
325 http.Error(w, err.Error(), http.StatusInternalServerError)
326 return
327 }
328
329 // set permissions for this did as owner
330 reg, err := s.db.RegistrationByDomain(domain)
331 if err != nil {
332 log.Println("failed to register domain", err)
333 http.Error(w, err.Error(), http.StatusInternalServerError)
334 return
335 }
336
337 // add basic acls for this domain
338 err = s.enforcer.AddDomain(domain)
339 if err != nil {
340 log.Println("failed to setup owner of domain", err)
341 http.Error(w, err.Error(), http.StatusInternalServerError)
342 return
343 }
344
345 // add this did as owner of this domain
346 err = s.enforcer.AddOwner(domain, reg.ByDid)
347 if err != nil {
348 log.Println("failed to setup owner of domain", err)
349 http.Error(w, err.Error(), http.StatusInternalServerError)
350 return
351 }
352
353 w.Write([]byte("check success"))
354}
355
356func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) {
357 domain := chi.URLParam(r, "domain")
358 if domain == "" {
359 http.Error(w, "malformed url", http.StatusBadRequest)
360 return
361 }
362
363 user := s.auth.GetUser(r)
364 reg, err := s.db.RegistrationByDomain(domain)
365 if err != nil {
366 w.Write([]byte("failed to pull up registration info"))
367 return
368 }
369
370 var members []string
371 if reg.Registered != nil {
372 members, err = s.enforcer.GetUserByRole("server:member", domain)
373 if err != nil {
374 w.Write([]byte("failed to fetch member list"))
375 return
376 }
377 }
378
379 ok, err := s.enforcer.IsServerOwner(user.Did, domain)
380 isOwner := err == nil && ok
381
382 p := pages.KnotParams{
383 LoggedInUser: user,
384 Registration: reg,
385 Members: members,
386 IsOwner: isOwner,
387 }
388
389 s.pages.Knot(w, p)
390}
391
392// get knots registered by this user
393func (s *State) Knots(w http.ResponseWriter, r *http.Request) {
394 // for now, this is just pubkeys
395 user := s.auth.GetUser(r)
396 registrations, err := s.db.RegistrationsByDid(user.Did)
397 if err != nil {
398 log.Println(err)
399 }
400
401 s.pages.Knots(w, pages.KnotsParams{
402 LoggedInUser: user,
403 Registrations: registrations,
404 })
405}
406
407// list members of domain, requires auth and requires owner status
408func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) {
409 domain := chi.URLParam(r, "domain")
410 if domain == "" {
411 http.Error(w, "malformed url", http.StatusBadRequest)
412 return
413 }
414
415 // list all members for this domain
416 memberDids, err := s.enforcer.GetUserByRole("server:member", domain)
417 if err != nil {
418 w.Write([]byte("failed to fetch member list"))
419 return
420 }
421
422 w.Write([]byte(strings.Join(memberDids, "\n")))
423 return
424}
425
426// add member to domain, requires auth and requires invite access
427func (s *State) AddMember(w http.ResponseWriter, r *http.Request) {
428 domain := chi.URLParam(r, "domain")
429 if domain == "" {
430 http.Error(w, "malformed url", http.StatusBadRequest)
431 return
432 }
433
434 memberDid := r.FormValue("member")
435 if memberDid == "" {
436 http.Error(w, "malformed form", http.StatusBadRequest)
437 return
438 }
439
440 memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid)
441 if err != nil {
442 w.Write([]byte("failed to resolve member did to a handle"))
443 return
444 }
445 log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain)
446
447 // announce this relation into the firehose, store into owners' pds
448 client, _ := s.auth.AuthorizedClient(r)
449 currentUser := s.auth.GetUser(r)
450 addedAt := time.Now().Format(time.RFC3339)
451 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
452 Collection: tangled.KnotMemberNSID,
453 Repo: currentUser.Did,
454 Rkey: s.TID(),
455 Record: &lexutil.LexiconTypeDecoder{
456 Val: &tangled.KnotMember{
457 Member: memberIdent.DID.String(),
458 Domain: domain,
459 AddedAt: &addedAt,
460 }},
461 })
462
463 // invalid record
464 if err != nil {
465 log.Printf("failed to create record: %s", err)
466 return
467 }
468 log.Println("created atproto record: ", resp.Uri)
469
470 secret, err := s.db.GetRegistrationKey(domain)
471 if err != nil {
472 log.Printf("no key found for domain %s: %s\n", domain, err)
473 return
474 }
475
476 ksClient, err := NewSignedClient(domain, secret)
477 if err != nil {
478 log.Println("failed to create client to ", domain)
479 return
480 }
481
482 ksResp, err := ksClient.AddMember(memberIdent.DID.String())
483 if err != nil {
484 log.Printf("failed to make request to %s: %s", domain, err)
485 return
486 }
487
488 if ksResp.StatusCode != http.StatusNoContent {
489 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err)))
490 return
491 }
492
493 err = s.enforcer.AddMember(domain, memberIdent.DID.String())
494 if err != nil {
495 w.Write([]byte(fmt.Sprint("failed to add member: ", err)))
496 return
497 }
498
499 w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String())))
500}
501
502func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) {
503}
504
505func (s *State) AddRepo(w http.ResponseWriter, r *http.Request) {
506 switch r.Method {
507 case http.MethodGet:
508 user := s.auth.GetUser(r)
509 knots, err := s.enforcer.GetDomainsForUser(user.Did)
510
511 if err != nil {
512 s.pages.Notice(w, "repo", "Invalid user account.")
513 return
514 }
515
516 s.pages.NewRepo(w, pages.NewRepoParams{
517 LoggedInUser: user,
518 Knots: knots,
519 })
520 case http.MethodPost:
521 user := s.auth.GetUser(r)
522
523 domain := r.FormValue("domain")
524 if domain == "" {
525 s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
526 return
527 }
528
529 repoName := r.FormValue("name")
530 if repoName == "" {
531 s.pages.Notice(w, "repo", "Invalid repo name.")
532 return
533 }
534
535 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create")
536 if err != nil || !ok {
537 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
538 return
539 }
540
541 secret, err := s.db.GetRegistrationKey(domain)
542 if err != nil {
543 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain))
544 return
545 }
546
547 client, err := NewSignedClient(domain, secret)
548 if err != nil {
549 s.pages.Notice(w, "repo", "Failed to connect to knot server.")
550 return
551 }
552
553 resp, err := client.NewRepo(user.Did, repoName)
554 if err != nil {
555 s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
556 return
557 }
558
559 switch resp.StatusCode {
560 case http.StatusConflict:
561 s.pages.Notice(w, "repo", "A repository with that name already exists.")
562 return
563 case http.StatusInternalServerError:
564 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
565 case http.StatusNoContent:
566 // continue
567 }
568
569 rkey := s.TID()
570 repo := &db.Repo{
571 Did: user.Did,
572 Name: repoName,
573 Knot: domain,
574 Rkey: rkey,
575 }
576
577 xrpcClient, _ := s.auth.AuthorizedClient(r)
578
579 addedAt := time.Now().Format(time.RFC3339)
580 atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
581 Collection: tangled.RepoNSID,
582 Repo: user.Did,
583 Rkey: rkey,
584 Record: &lexutil.LexiconTypeDecoder{
585 Val: &tangled.Repo{
586 Knot: repo.Knot,
587 Name: repoName,
588 AddedAt: &addedAt,
589 Owner: user.Did,
590 }},
591 })
592 if err != nil {
593 log.Printf("failed to create record: %s", err)
594 s.pages.Notice(w, "repo", "Failed to announce repository creation.")
595 return
596 }
597 log.Println("created repo record: ", atresp.Uri)
598
599 repo.AtUri = atresp.Uri
600
601 err = s.db.AddRepo(repo)
602 if err != nil {
603 log.Println(err)
604 s.pages.Notice(w, "repo", "Failed to save repository information.")
605 return
606 }
607
608 // acls
609 p, _ := securejoin.SecureJoin(user.Did, repoName)
610 err = s.enforcer.AddRepo(user.Did, domain, p)
611 if err != nil {
612 log.Println(err)
613 s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
614 return
615 }
616
617 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName))
618 return
619 }
620}
621
622func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) {
623 didOrHandle := chi.URLParam(r, "user")
624 if didOrHandle == "" {
625 http.Error(w, "Bad request", http.StatusBadRequest)
626 return
627 }
628
629 ident, err := s.resolver.ResolveIdent(r.Context(), didOrHandle)
630 if err != nil {
631 log.Printf("resolving identity: %s", err)
632 w.WriteHeader(http.StatusNotFound)
633 return
634 }
635
636 repos, err := s.db.GetAllReposByDid(ident.DID.String())
637 if err != nil {
638 log.Printf("getting repos for %s: %s", ident.DID.String(), err)
639 }
640
641 collaboratingRepos, err := s.db.CollaboratingIn(ident.DID.String())
642 if err != nil {
643 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
644 }
645 var didsToResolve []string
646 for _, r := range collaboratingRepos {
647 didsToResolve = append(didsToResolve, r.Did)
648 }
649 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve)
650 didHandleMap := make(map[string]string)
651 for _, identity := range resolvedIds {
652 if !identity.Handle.IsInvalidHandle() {
653 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
654 } else {
655 didHandleMap[identity.DID.String()] = identity.DID.String()
656 }
657 }
658
659 followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
660 if err != nil {
661 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
662 }
663
664 loggedInUser := s.auth.GetUser(r)
665 followStatus := db.IsNotFollowing
666 if loggedInUser != nil {
667 followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
668 }
669
670 s.pages.ProfilePage(w, pages.ProfilePageParams{
671 LoggedInUser: loggedInUser,
672 UserDid: ident.DID.String(),
673 UserHandle: ident.Handle.String(),
674 Repos: repos,
675 CollaboratingRepos: collaboratingRepos,
676 ProfileStats: pages.ProfileStats{
677 Followers: followers,
678 Following: following,
679 },
680 FollowStatus: db.FollowStatus(followStatus),
681 DidHandleMap: didHandleMap,
682 })
683}
684
685func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
686 currentUser := s.auth.GetUser(r)
687
688 subject := r.URL.Query().Get("subject")
689 if subject == "" {
690 log.Println("invalid form")
691 return
692 }
693
694 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
695 if err != nil {
696 log.Println("failed to follow, invalid did")
697 }
698
699 if currentUser.Did == subjectIdent.DID.String() {
700 log.Println("cant follow or unfollow yourself")
701 return
702 }
703
704 client, _ := s.auth.AuthorizedClient(r)
705
706 switch r.Method {
707 case http.MethodPost:
708 createdAt := time.Now().Format(time.RFC3339)
709 rkey := s.TID()
710 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
711 Collection: tangled.GraphFollowNSID,
712 Repo: currentUser.Did,
713 Rkey: rkey,
714 Record: &lexutil.LexiconTypeDecoder{
715 Val: &tangled.GraphFollow{
716 Subject: subjectIdent.DID.String(),
717 CreatedAt: createdAt,
718 }},
719 })
720 if err != nil {
721 log.Println("failed to create atproto record", err)
722 return
723 }
724
725 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey)
726 if err != nil {
727 log.Println("failed to follow", err)
728 return
729 }
730
731 log.Println("created atproto record: ", resp.Uri)
732
733 w.Write([]byte(fmt.Sprintf(`
734 <button id="followBtn"
735 class="btn mt-2"
736 hx-delete="/follow?subject=%s"
737 hx-trigger="click"
738 hx-target="#followBtn"
739 hx-swap="outerHTML">
740 Unfollow
741 </button>
742 `, subjectIdent.DID.String())))
743
744 return
745 case http.MethodDelete:
746 // find the record in the db
747 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
748 if err != nil {
749 log.Println("failed to get follow relationship")
750 return
751 }
752
753 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
754 Collection: tangled.GraphFollowNSID,
755 Repo: currentUser.Did,
756 Rkey: follow.RKey,
757 })
758
759 if err != nil {
760 log.Println("failed to unfollow")
761 return
762 }
763
764 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
765 if err != nil {
766 log.Println("failed to delete follow from DB")
767 // this is not an issue, the firehose event might have already done this
768 }
769
770 w.Write([]byte(fmt.Sprintf(`
771 <button id="followBtn"
772 class="btn mt-2"
773 hx-post="/follow?subject=%s"
774 hx-trigger="click"
775 hx-target="#followBtn"
776 hx-swap="outerHTML">
777 Follow
778 </button>
779 `, subjectIdent.DID.String())))
780 return
781 }
782
783}
784
785func (s *State) Router() http.Handler {
786 router := chi.NewRouter()
787
788 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
789 pat := chi.URLParam(r, "*")
790 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
791 s.UserRouter().ServeHTTP(w, r)
792 } else {
793 s.StandardRouter().ServeHTTP(w, r)
794 }
795 })
796
797 return router
798}
799
800func (s *State) UserRouter() http.Handler {
801 r := chi.NewRouter()
802
803 // strip @ from user
804 r.Use(StripLeadingAt)
805
806 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
807 r.Get("/", s.ProfilePage)
808 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
809 r.Get("/", s.RepoIndex)
810 r.Get("/commits/{ref}", s.RepoLog)
811 r.Route("/tree/{ref}", func(r chi.Router) {
812 r.Get("/", s.RepoIndex)
813 r.Get("/*", s.RepoTree)
814 })
815 r.Get("/commit/{ref}", s.RepoCommit)
816 r.Get("/branches", s.RepoBranches)
817 r.Get("/tags", s.RepoTags)
818 r.Get("/blob/{ref}/*", s.RepoBlob)
819
820 r.Route("/issues", func(r chi.Router) {
821 r.Get("/", s.RepoIssues)
822 r.Get("/{issue}", s.RepoSingleIssue)
823 r.Get("/new", s.NewIssue)
824 r.Post("/new", s.NewIssue)
825 r.Post("/{issue}/comment", s.IssueComment)
826 r.Post("/{issue}/close", s.CloseIssue)
827 r.Post("/{issue}/reopen", s.ReopenIssue)
828 })
829
830 r.Route("/pulls", func(r chi.Router) {
831 r.Get("/", s.RepoPulls)
832 })
833
834 // These routes get proxied to the knot
835 r.Get("/info/refs", s.InfoRefs)
836 r.Post("/git-upload-pack", s.UploadPack)
837
838 // settings routes, needs auth
839 r.Group(func(r chi.Router) {
840 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
841 r.Get("/", s.RepoSettings)
842 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
843 })
844 })
845 })
846 })
847
848 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
849 s.pages.Error404(w)
850 })
851
852 return r
853}
854
855func (s *State) StandardRouter() http.Handler {
856 r := chi.NewRouter()
857
858 r.Handle("/static/*", s.pages.Static())
859
860 r.Get("/", s.Timeline)
861
862 r.Get("/logout", s.Logout)
863
864 r.Get("/login", s.Login)
865 r.Post("/login", s.Login)
866
867 r.Route("/knots", func(r chi.Router) {
868 r.Use(AuthMiddleware(s))
869 r.Get("/", s.Knots)
870 r.Post("/key", s.RegistrationKey)
871
872 r.Route("/{domain}", func(r chi.Router) {
873 r.Post("/init", s.InitKnotServer)
874 r.Get("/", s.KnotServerInfo)
875 r.Route("/member", func(r chi.Router) {
876 r.Use(RoleMiddleware(s, "server:owner"))
877 r.Get("/", s.ListMembers)
878 r.Put("/", s.AddMember)
879 r.Delete("/", s.RemoveMember)
880 })
881 })
882 })
883
884 r.Route("/repo", func(r chi.Router) {
885 r.Route("/new", func(r chi.Router) {
886 r.Get("/", s.AddRepo)
887 r.Post("/", s.AddRepo)
888 })
889 // r.Post("/import", s.ImportRepo)
890 })
891
892 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
893 r.Post("/", s.Follow)
894 r.Delete("/", s.Follow)
895 })
896
897 r.Route("/settings", func(r chi.Router) {
898 r.Use(AuthMiddleware(s))
899 r.Get("/", s.Settings)
900 r.Put("/keys", s.SettingsKeys)
901 })
902
903 r.Get("/keys/{user}", s.Keys)
904
905 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
906 s.pages.Error404(w)
907 })
908 return r
909}