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