this repo has no description
1package web
2
3import (
4 "log/slog"
5 "net/http"
6
7 "github.com/go-chi/chi/v5"
8 "tangled.org/core/appview/config"
9 "tangled.org/core/appview/db"
10 "tangled.org/core/appview/indexer"
11 "tangled.org/core/appview/mentions"
12 "tangled.org/core/appview/notify"
13 "tangled.org/core/appview/oauth"
14 "tangled.org/core/appview/pages"
15 isvc "tangled.org/core/appview/service/issue"
16 rsvc "tangled.org/core/appview/service/repo"
17 "tangled.org/core/appview/state"
18 "tangled.org/core/appview/validator"
19 "tangled.org/core/appview/web/handler"
20 "tangled.org/core/appview/web/middleware"
21 "tangled.org/core/idresolver"
22 "tangled.org/core/rbac"
23)
24
25// Rules
26// - Use single function for each endpoints (unless it doesn't make sense.)
27// - Name handler files following the related path (ancestor paths can be
28// trimmed.)
29// - Pass dependencies to each handlers, don't create structs with shared
30// dependencies unless it serves some domain-specific roles like
31// service/issue. Same rule goes to middlewares.
32
33// RouterFromState creates a web router from `state.State`. This exist to
34// bridge between legacy web routers under `State` and new architecture
35func RouterFromState(s *state.State) http.Handler {
36 config, db, enforcer, idResolver, refResolver, indexer, logger, notifier, oauth, pages, validator := s.Expose()
37
38 return Router(
39 logger,
40 config,
41 db,
42 enforcer,
43 idResolver,
44 refResolver,
45 indexer,
46 notifier,
47 oauth,
48 pages,
49 validator,
50 s,
51 )
52}
53
54func Router(
55 // NOTE: put base dependencies (db, idResolver, oauth etc)
56 logger *slog.Logger,
57 config *config.Config,
58 db *db.DB,
59 enforcer *rbac.Enforcer,
60 idResolver *idresolver.Resolver,
61 mentionsResolver *mentions.Resolver,
62 indexer *indexer.Indexer,
63 notifier notify.Notifier,
64 oauth *oauth.OAuth,
65 pages *pages.Pages,
66 validator *validator.Validator,
67 // to use legacy web handlers. will be removed later
68 s *state.State,
69) http.Handler {
70 repo := rsvc.NewService(
71 logger,
72 config,
73 db,
74 enforcer,
75 )
76 issue := isvc.NewService(
77 logger,
78 config,
79 db,
80 enforcer,
81 notifier,
82 idResolver,
83 mentionsResolver,
84 indexer.Issues,
85 validator,
86 )
87
88 i := s.ExposeIssue()
89
90 r := chi.NewRouter()
91
92 mw := s.Middleware()
93 auth := middleware.AuthMiddleware()
94
95 r.Use(middleware.WithLogger(logger))
96 r.Use(middleware.WithSession(oauth))
97
98 r.Use(middleware.Normalize())
99
100 r.Get("/favicon.svg", s.Favicon)
101 r.Get("/favicon.ico", s.Favicon)
102 r.Get("/pwa-manifest.json", s.PWAManifest)
103 r.Get("/robots.txt", s.RobotsTxt)
104
105 r.Handle("/static/*", pages.Static())
106
107 r.Get("/", s.HomeOrTimeline)
108 r.Get("/timeline", s.Timeline)
109 r.Get("/upgradeBanner", s.UpgradeBanner)
110
111 r.Get("/terms", s.TermsOfService)
112 r.Get("/privacy", s.PrivacyPolicy)
113 r.Get("/brand", s.Brand)
114 // special-case handler for serving tangled.org/core
115 r.Get("/core", s.Core())
116
117 r.Get("/login", s.Login)
118 r.Post("/login", s.Login)
119 r.Post("/logout", s.Logout)
120
121 r.Get("/goodfirstissues", s.GoodFirstIssues)
122
123 r.With(auth).Get("/repo/new", s.NewRepo)
124 r.With(auth).Post("/repo/new", s.NewRepo)
125
126 r.With(auth).Post("/follow", s.Follow)
127 r.With(auth).Delete("/follow", s.Follow)
128
129 r.With(auth).Post("/star", s.Star)
130 r.With(auth).Delete("/star", s.Star)
131
132 r.With(auth).Post("/react", s.React)
133 r.With(auth).Delete("/react", s.React)
134
135 r.With(auth).Get("/profile/edit-bio", s.EditBioFragment)
136 r.With(auth).Get("/profile/edit-pins", s.EditPinsFragment)
137 r.With(auth).Post("/profile/bio", s.UpdateProfileBio)
138 r.With(auth).Post("/profile/pins", s.UpdateProfilePins)
139
140 r.Mount("/settings", s.SettingsRouter())
141 r.Mount("/strings", s.StringsRouter(mw))
142 r.Mount("/settings/knots", s.KnotsRouter())
143 r.Mount("/settings/spindles", s.SpindlesRouter())
144 r.Mount("/notifications", s.NotificationsRouter(mw))
145
146 r.Mount("/signup", s.SignupRouter())
147 r.Get("/oauth/client-metadata.json", handler.OauthClientMetadata(oauth))
148 r.Get("/oauth/jwks.json", handler.OauthJwks(oauth))
149 r.Get("/oauth/callback", oauth.Callback)
150
151 // special-case handler. should replace with xrpc later
152 r.Get("/keys/{user}", s.Keys)
153
154 r.HandleFunc("/@*", func(w http.ResponseWriter, r *http.Request) {
155 http.Redirect(w, r, "/"+chi.URLParam(r, "*"), http.StatusFound)
156 })
157
158 r.Route("/{user}", func(r chi.Router) {
159 r.Use(middleware.EnsureDidOrHandle(pages))
160 r.Use(middleware.ResolveIdent(idResolver, pages))
161
162 r.Get("/", s.Profile)
163 r.Get("/feed.atom", s.AtomFeedPage)
164
165 r.Route("/{repo}", func(r chi.Router) {
166 r.Use(middleware.ResolveRepo(db, pages))
167
168 r.Mount("/", s.RepoRouter(mw))
169
170 // /{user}/{repo}/issues/*
171 r.With(middleware.Paginate).Get("/issues", handler.RepoIssues(issue, repo, pages, db))
172 r.With(auth).Get("/issues/new", handler.NewIssue(repo, pages))
173 r.With(auth).Post("/issues/new", handler.NewIssuePost(issue, pages))
174 r.Route("/issues/{issue}", func(r chi.Router) {
175 r.Use(middleware.ResolveIssue(db, pages))
176
177 r.Get("/", handler.Issue(issue, repo, pages, db))
178 r.Get("/opengraph", i.IssueOpenGraphSummary)
179
180 r.With(auth).Delete("/", handler.IssueDelete(issue, pages))
181
182 r.With(auth).Get("/edit", handler.IssueEdit(issue, repo, pages))
183 r.With(auth).Post("/edit", handler.IssueEditPost(issue, pages))
184
185 r.With(auth).Post("/close", handler.CloseIssue(issue, pages))
186 r.With(auth).Post("/reopen", handler.ReopenIssue(issue, pages))
187
188 r.With(auth).Post("/comment", i.NewIssueComment)
189 r.With(auth).Route("/comment/{commentId}/", func(r chi.Router) {
190 r.Get("/", i.IssueComment)
191 r.Delete("/", i.DeleteIssueComment)
192 r.Get("/edit", i.EditIssueComment)
193 r.Post("/edit", i.EditIssueComment)
194 r.Get("/reply", i.ReplyIssueComment)
195 r.Get("/replyPlaceholder", i.ReplyIssueCommentPlaceholder)
196 })
197 })
198
199 r.Mount("/pulls", s.PullsRouter(mw))
200 r.Mount("/pipelines", s.PipelinesRouter())
201 r.Mount("/labels", s.LabelsRouter())
202
203 // These routes get proxied to the knot
204 r.Get("/info/refs", s.InfoRefs)
205 r.Post("/git-upload-pack", s.UploadPack)
206 r.Post("/git-receive-pack", s.ReceivePack)
207 })
208 })
209
210 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
211 pages.Error404(w)
212 })
213
214 return r
215}