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
646 followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
647 if err != nil {
648 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
649 }
650
651 loggedInUser := s.auth.GetUser(r)
652 followStatus := db.IsNotFollowing
653 if loggedInUser != nil {
654 followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
655 }
656
657 s.pages.ProfilePage(w, pages.ProfilePageParams{
658 LoggedInUser: loggedInUser,
659 UserDid: ident.DID.String(),
660 UserHandle: ident.Handle.String(),
661 Repos: repos,
662 CollaboratingRepos: collaboratingRepos,
663 ProfileStats: pages.ProfileStats{
664 Followers: followers,
665 Following: following,
666 },
667 FollowStatus: db.FollowStatus(followStatus),
668 })
669}
670
671func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
672 currentUser := s.auth.GetUser(r)
673
674 subject := r.URL.Query().Get("subject")
675 if subject == "" {
676 log.Println("invalid form")
677 return
678 }
679
680 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)
681 if err != nil {
682 log.Println("failed to follow, invalid did")
683 }
684
685 if currentUser.Did == subjectIdent.DID.String() {
686 log.Println("cant follow or unfollow yourself")
687 return
688 }
689
690 client, _ := s.auth.AuthorizedClient(r)
691
692 switch r.Method {
693 case http.MethodPost:
694 createdAt := time.Now().Format(time.RFC3339)
695 rkey := s.TID()
696 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
697 Collection: tangled.GraphFollowNSID,
698 Repo: currentUser.Did,
699 Rkey: rkey,
700 Record: &lexutil.LexiconTypeDecoder{
701 Val: &tangled.GraphFollow{
702 Subject: subjectIdent.DID.String(),
703 CreatedAt: createdAt,
704 }},
705 })
706 if err != nil {
707 log.Println("failed to create atproto record", err)
708 return
709 }
710
711 err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), rkey)
712 if err != nil {
713 log.Println("failed to follow", err)
714 return
715 }
716
717 log.Println("created atproto record: ", resp.Uri)
718
719 w.Write([]byte(fmt.Sprintf(`
720 <button id="followBtn"
721 class="btn mt-2"
722 hx-delete="/follow?subject=%s"
723 hx-trigger="click"
724 hx-target="#followBtn"
725 hx-swap="outerHTML">
726 Unfollow
727 </button>
728 `, subjectIdent.DID.String())))
729
730 return
731 case http.MethodDelete:
732 // find the record in the db
733 follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
734 if err != nil {
735 log.Println("failed to get follow relationship")
736 return
737 }
738
739 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
740 Collection: tangled.GraphFollowNSID,
741 Repo: currentUser.Did,
742 Rkey: follow.RKey,
743 })
744
745 if err != nil {
746 log.Println("failed to unfollow")
747 return
748 }
749
750 err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())
751 if err != nil {
752 log.Println("failed to delete follow from DB")
753 // this is not an issue, the firehose event might have already done this
754 }
755
756 w.Write([]byte(fmt.Sprintf(`
757 <button id="followBtn"
758 class="btn mt-2"
759 hx-post="/follow?subject=%s"
760 hx-trigger="click"
761 hx-target="#followBtn"
762 hx-swap="outerHTML">
763 Follow
764 </button>
765 `, subjectIdent.DID.String())))
766 return
767 }
768
769}
770
771func (s *State) Router() http.Handler {
772 router := chi.NewRouter()
773
774 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
775 pat := chi.URLParam(r, "*")
776 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
777 s.UserRouter().ServeHTTP(w, r)
778 } else {
779 s.StandardRouter().ServeHTTP(w, r)
780 }
781 })
782
783 return router
784}
785
786func (s *State) UserRouter() http.Handler {
787 r := chi.NewRouter()
788
789 // strip @ from user
790 r.Use(StripLeadingAt)
791
792 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
793 r.Get("/", s.ProfilePage)
794 r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
795 r.Get("/", s.RepoIndex)
796 r.Get("/commits/{ref}", s.RepoLog)
797 r.Route("/tree/{ref}", func(r chi.Router) {
798 r.Get("/", s.RepoIndex)
799 r.Get("/*", s.RepoTree)
800 })
801 r.Get("/commit/{ref}", s.RepoCommit)
802 r.Get("/branches", s.RepoBranches)
803 r.Get("/tags", s.RepoTags)
804 r.Get("/blob/{ref}/*", s.RepoBlob)
805
806 r.Route("/issues", func(r chi.Router) {
807 r.Get("/", s.RepoIssues)
808 r.Get("/{issue}", s.RepoSingleIssue)
809 r.Get("/new", s.NewIssue)
810 r.Post("/new", s.NewIssue)
811 r.Post("/{issue}/comment", s.IssueComment)
812 r.Post("/{issue}/close", s.CloseIssue)
813 r.Post("/{issue}/reopen", s.ReopenIssue)
814 })
815
816 // These routes get proxied to the knot
817 r.Get("/info/refs", s.InfoRefs)
818 r.Post("/git-upload-pack", s.UploadPack)
819
820 // settings routes, needs auth
821 r.Group(func(r chi.Router) {
822 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
823 r.Get("/", s.RepoSettings)
824 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
825 })
826 })
827 })
828 })
829
830 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
831 s.pages.Error404(w)
832 })
833
834 return r
835}
836
837func (s *State) StandardRouter() http.Handler {
838 r := chi.NewRouter()
839
840 r.Handle("/static/*", s.pages.Static())
841
842 r.Get("/", s.Timeline)
843
844 r.Get("/logout", s.Logout)
845
846 r.Get("/login", s.Login)
847 r.Post("/login", s.Login)
848
849 r.Route("/knots", func(r chi.Router) {
850 r.Use(AuthMiddleware(s))
851 r.Get("/", s.Knots)
852 r.Post("/key", s.RegistrationKey)
853
854 r.Route("/{domain}", func(r chi.Router) {
855 r.Post("/init", s.InitKnotServer)
856 r.Get("/", s.KnotServerInfo)
857 r.Route("/member", func(r chi.Router) {
858 r.Use(RoleMiddleware(s, "server:owner"))
859 r.Get("/", s.ListMembers)
860 r.Put("/", s.AddMember)
861 r.Delete("/", s.RemoveMember)
862 })
863 })
864 })
865
866 r.Route("/repo", func(r chi.Router) {
867 r.Route("/new", func(r chi.Router) {
868 r.Get("/", s.AddRepo)
869 r.Post("/", s.AddRepo)
870 })
871 // r.Post("/import", s.ImportRepo)
872 })
873
874 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
875 r.Post("/", s.Follow)
876 r.Delete("/", s.Follow)
877 })
878
879 r.Route("/settings", func(r chi.Router) {
880 r.Use(AuthMiddleware(s))
881 r.Get("/", s.Settings)
882 r.Put("/keys", s.SettingsKeys)
883 })
884
885 r.Get("/keys/{user}", s.Keys)
886
887 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
888 s.pages.Error404(w)
889 })
890 return r
891}