1package knotserver
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "net/http"
8
9 "github.com/go-chi/chi/v5"
10 "tangled.sh/tangled.sh/core/jetstream"
11 "tangled.sh/tangled.sh/core/knotserver/config"
12 "tangled.sh/tangled.sh/core/knotserver/db"
13 "tangled.sh/tangled.sh/core/rbac"
14)
15
16const (
17 ThisServer = "thisserver" // resource identifier for rbac enforcement
18)
19
20type Handle struct {
21 c *config.Config
22 db *db.DB
23 jc *jetstream.JetstreamClient
24 e *rbac.Enforcer
25 l *slog.Logger
26
27 // init is a channel that is closed when the knot has been initailized
28 // i.e. when the first user (knot owner) has been added.
29 init chan struct{}
30 knotInitialized bool
31}
32
33func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger) (http.Handler, error) {
34 r := chi.NewRouter()
35
36 h := Handle{
37 c: c,
38 db: db,
39 e: e,
40 l: l,
41 jc: jc,
42 init: make(chan struct{}),
43 }
44
45 err := e.AddDomain(ThisServer)
46 if err != nil {
47 return nil, fmt.Errorf("failed to setup enforcer: %w", err)
48 }
49
50 err = h.jc.StartJetstream(ctx, h.processMessages)
51 if err != nil {
52 return nil, fmt.Errorf("failed to start jetstream: %w", err)
53 }
54
55 // Check if the knot knows about any Dids;
56 // if it does, it is already initialized and we can repopulate the
57 // Jetstream subscriptions.
58 dids, err := db.GetAllDids()
59 if err != nil {
60 return nil, fmt.Errorf("failed to get all Dids: %w", err)
61 }
62 if len(dids) > 0 {
63 h.knotInitialized = true
64 close(h.init)
65 // h.jc.UpdateDids(dids)
66 }
67
68 r.Get("/", h.Index)
69 r.Route("/{did}", func(r chi.Router) {
70 // Repo routes
71 r.Route("/{name}", func(r chi.Router) {
72 r.Route("/collaborator", func(r chi.Router) {
73 r.Use(h.VerifySignature)
74 r.Post("/add", h.AddRepoCollaborator)
75 })
76
77 r.Get("/", h.RepoIndex)
78 r.Get("/info/refs", h.InfoRefs)
79 r.Post("/git-upload-pack", h.UploadPack)
80
81 r.Route("/merge", func(r chi.Router) {
82 r.With(h.VerifySignature)
83 r.Post("/", h.Merge)
84 r.Post("/check", h.MergeCheck)
85 })
86
87 r.Route("/tree/{ref}", func(r chi.Router) {
88 r.Get("/", h.RepoIndex)
89 r.Get("/*", h.RepoTree)
90 })
91
92 r.Route("/blob/{ref}", func(r chi.Router) {
93 r.Get("/*", h.Blob)
94 })
95
96 r.Get("/log/{ref}", h.Log)
97 r.Get("/archive/{file}", h.Archive)
98 r.Get("/commit/{ref}", h.Diff)
99 r.Get("/tags", h.Tags)
100 r.Get("/branches", h.Branches)
101 })
102 })
103
104 // Create a new repository.
105 r.Route("/repo", func(r chi.Router) {
106 r.Use(h.VerifySignature)
107 r.Put("/new", h.NewRepo)
108 r.Delete("/", h.RemoveRepo)
109 })
110
111 r.Route("/member", func(r chi.Router) {
112 r.Use(h.VerifySignature)
113 r.Put("/add", h.AddMember)
114 })
115
116 // Initialize the knot with an owner and public key.
117 r.With(h.VerifySignature).Post("/init", h.Init)
118
119 // Health check. Used for two-way verification with appview.
120 r.With(h.VerifySignature).Get("/health", h.Health)
121
122 // All public keys on the knot.
123 r.Get("/keys", h.Keys)
124
125 return r, nil
126}