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