This repository has no description
1package state
2
3import (
4 "context"
5 "database/sql"
6 "errors"
7 "net/http"
8 "strings"
9
10 "github.com/go-chi/chi/v5"
11 "tangled.org/core/appview/db"
12 "tangled.org/core/appview/issues"
13 "tangled.org/core/appview/knotacl"
14 "tangled.org/core/appview/knots"
15 "tangled.org/core/appview/labels"
16 "tangled.org/core/appview/metrics"
17 "tangled.org/core/appview/middleware"
18 "tangled.org/core/appview/migration"
19 "tangled.org/core/appview/notifications"
20 "tangled.org/core/appview/pipelines"
21 "tangled.org/core/appview/pulls"
22 "tangled.org/core/appview/repo"
23 "tangled.org/core/appview/settings"
24 "tangled.org/core/appview/signup"
25 "tangled.org/core/appview/spindles"
26 "tangled.org/core/appview/state/userutil"
27 avstrings "tangled.org/core/appview/strings"
28 "tangled.org/core/log"
29)
30
31func (s *State) Router() http.Handler {
32 router := chi.NewRouter()
33 middleware := middleware.New(
34 s.oauth,
35 s.db,
36 s.enforcer,
37 s.aclService,
38 s.repoResolver,
39 s.idResolver,
40 s.pages,
41 s.rdb,
42 s.logger,
43 )
44
45 router.Use(metrics.Middleware)
46 router.Use(knotacl.MemoMiddleware)
47
48 if err := db.ReapStaleRunningMigrations(context.Background(), s.db); err != nil {
49 s.logger.Warn("failed to reap stale running migrations", "err", err)
50 }
51 m := migration.NewMigration(s.db, s.oauth, s.idResolver.Directory(), s.logger)
52 router.Use(m.BackgroundMigrationMiddleware)
53
54 router.Get("/pwa-manifest.json", s.WebAppManifest)
55 router.Get("/robots.txt", s.RobotsTxt)
56 router.Get("/.well-known/security.txt", s.SecurityTxt)
57
58 userRouter := s.UserRouter(&middleware)
59 standardRouter := s.StandardRouter(&middleware)
60
61 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
62 pat := chi.URLParam(r, "*")
63 pathParts := strings.SplitN(pat, "/", 2)
64
65 if len(pathParts) > 0 {
66 firstPart := pathParts[0]
67
68 if userutil.IsDid(firstPart) {
69 repo, err := db.GetRepoByDid(s.db, firstPart)
70 switch {
71 case err == nil:
72 remaining := ""
73 if len(pathParts) > 1 {
74 remaining = "/" + pathParts[1]
75 }
76 rewritten := "/" + repo.Did + "/" + repo.Rkey + remaining
77 r2 := r.Clone(r.Context())
78 r2.URL.Path = rewritten
79 r2.URL.RawPath = rewritten
80 userRouter.ServeHTTP(w, r2)
81 case errors.Is(err, sql.ErrNoRows):
82 userRouter.ServeHTTP(w, r)
83 default:
84 s.logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err)
85 http.Error(w, "internal server error", http.StatusInternalServerError)
86 }
87 return
88 }
89
90 if userutil.IsHandle(firstPart) {
91 userRouter.ServeHTTP(w, r)
92 return
93 }
94
95 // if using a flattened DID (like you would in go modules), unflatten
96 if userutil.IsFlattenedDid(firstPart) {
97 unflattenedDid := userutil.UnflattenDid(firstPart)
98 redirectPath := strings.Join(append([]string{unflattenedDid}, pathParts[1:]...), "/")
99
100 redirectURL := *r.URL
101 redirectURL.Path = "/" + redirectPath
102
103 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
104 return
105 }
106
107 // if using a handle with @, rewrite to work without @
108 if normalized := strings.TrimPrefix(firstPart, "@"); userutil.IsHandle(normalized) {
109 redirectPath := strings.Join(append([]string{normalized}, pathParts[1:]...), "/")
110
111 redirectURL := *r.URL
112 redirectURL.Path = "/" + redirectPath
113
114 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
115 return
116 }
117
118 }
119
120 standardRouter.ServeHTTP(w, r)
121 })
122
123 return router
124}
125
126func (s *State) UserRouter(mw *middleware.Middleware) http.Handler {
127 r := chi.NewRouter()
128
129 r.With(mw.ResolveIdent()).Route("/{user}", func(r chi.Router) {
130 r.Get("/", s.Profile)
131 r.Get("/feed.atom", s.AtomFeedPage)
132
133 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) {
134 r.Use(mw.GoImport())
135
136 // These routes get proxied to the knot
137 r.Get("/info/refs", s.InfoRefs)
138 r.Post("/git-upload-archive", s.UploadArchive)
139 r.Post("/git-upload-pack", s.UploadPack)
140 r.Post("/git-receive-pack", s.ReceivePack)
141
142 r.Group(func(r chi.Router) {
143 r.Use(mw.CanonicalizeRepoURL())
144 r.Mount("/issues", s.IssuesRouter(mw))
145 r.Mount("/pulls", s.PullsRouter(mw))
146 r.Mount("/pipelines", s.PipelinesRouter(mw))
147 r.Mount("/labels", s.LabelsRouter())
148 r.Mount("/", s.RepoRouter(mw))
149 })
150 })
151 })
152
153 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
154 w.WriteHeader(http.StatusNotFound)
155 s.pages.Error404(w)
156 })
157
158 return r
159}
160
161func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler {
162 r := chi.NewRouter()
163
164 r.Handle("/static/*", s.pages.Static())
165
166 r.Get("/", s.HomeOrTimeline)
167 r.Get("/home", s.Home)
168 r.Get("/timeline", s.Timeline)
169 r.Get("/upgradeBanner", s.UpgradeBanner)
170 r.Post("/newsletter/signup", s.NewsletterSignup)
171 r.Post("/newsletter/dismiss", s.NewsletterDismiss)
172
173 // special-case handler for serving tangled.org/core
174 r.Get("/core", s.Core())
175
176 r.Get("/login", s.Login)
177 r.Post("/login", s.Login)
178 r.Post("/logout", s.Logout)
179
180 r.With(middleware.Paginate).Get("/search", s.Search)
181 r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick", s.SearchQuick)
182 r.With(middleware.AuthMiddleware(s.oauth)).Get("/search/quick/mobile", s.SearchQuickMobile)
183
184 r.Post("/account/switch", s.SwitchAccount)
185 r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount)
186
187 r.Route("/repo", func(r chi.Router) {
188 r.Route("/new", func(r chi.Router) {
189 r.Use(middleware.AuthMiddleware(s.oauth))
190 r.Get("/", s.NewRepo)
191 r.Post("/", s.NewRepo)
192 })
193 // r.Post("/import", s.ImportRepo)
194 })
195
196 r.With(middleware.Paginate).Get("/goodfirstissues", s.GoodFirstIssues)
197
198 r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) {
199 r.Post("/", s.Follow)
200 r.Delete("/", s.Follow)
201 })
202
203 r.With(middleware.AuthMiddleware(s.oauth)).Route("/vouch", func(r chi.Router) {
204 r.Post("/", s.Vouch)
205 r.Post("/skip", s.SkipVouchSuggestion)
206 })
207
208 r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) {
209 r.Post("/", s.Star)
210 r.Delete("/", s.Star)
211 })
212
213 r.With(middleware.AuthMiddleware(s.oauth)).Route("/react", func(r chi.Router) {
214 r.Post("/", s.React)
215 r.Delete("/", s.React)
216 })
217
218 r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) {
219 r.Get("/", s.CommentBodyFragment)
220 r.Get("/edit", s.EditCommentFragment)
221 r.Get("/reply", s.NewReplyCommentFragment)
222 r.Get("/reply/placeholder", s.ReplyPlaceholderFragment)
223 r.Post("/", s.NewComment)
224 r.Patch("/", s.EditComment)
225 r.Delete("/", s.DeleteComment)
226 })
227
228 r.Get("/profile/popover", s.ProfilePopover)
229
230 r.Route("/profile", func(r chi.Router) {
231 r.Use(middleware.AuthMiddleware(s.oauth))
232 r.Get("/edit-bio", s.EditBioFragment)
233 r.Get("/edit-pins", s.EditPinsFragment)
234 r.Post("/bio", s.UpdateProfileBio)
235 r.Post("/pins", s.UpdateProfilePins)
236 r.Post("/avatar", s.UploadProfileAvatar)
237 r.Delete("/avatar", s.RemoveProfileAvatar)
238 r.Post("/punchcard", s.UpdateProfilePunchcardSetting)
239 })
240
241 r.Mount("/settings", s.SettingsRouter())
242 r.Mount("/strings", s.StringsRouter(mw))
243
244 r.Mount("/settings/knots", s.KnotsRouter())
245 r.Mount("/settings/spindles", s.SpindlesRouter())
246
247 r.Mount("/notifications", s.NotificationsRouter(mw))
248
249 r.Mount("/signup", s.SignupRouter())
250 r.Mount("/", s.oauth.Router())
251
252 r.Get("/keys/{user}", s.Keys)
253 r.Get("/terms", s.TermsOfService)
254 r.Get("/privacy", s.PrivacyPolicy)
255 r.Get("/brand", s.Brand)
256
257 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
258 w.WriteHeader(http.StatusNotFound)
259 s.pages.Error404(w)
260 })
261 return r
262}
263
264// Core serves tangled.org/core go-import meta tags, and redirects
265// to the core repository if accessed normally.
266func (s *State) Core() http.HandlerFunc {
267 return func(w http.ResponseWriter, r *http.Request) {
268 if r.URL.Query().Get("go-get") == "1" {
269 w.Header().Set("Content-Type", "text/html")
270 w.Write([]byte(`<meta name="go-import" content="tangled.org/core git https://tangled.org/@tangled.org/core">`))
271 return
272 }
273
274 http.Redirect(w, r, "/@tangled.org/core", http.StatusFound)
275 }
276}
277
278func (s *State) SettingsRouter() http.Handler {
279 settings := &settings.Settings{
280 Db: s.db,
281 OAuth: s.oauth,
282 Pages: s.pages,
283 Config: s.config,
284 CfClient: s.cfClient,
285 Logger: log.SubLogger(s.logger, "settings"),
286 IdResolver: s.idResolver,
287 }
288
289 return settings.Router()
290}
291
292func (s *State) SpindlesRouter() http.Handler {
293 logger := log.SubLogger(s.logger, "spindles")
294
295 spindles := &spindles.Spindles{
296 Db: s.db,
297 OAuth: s.oauth,
298 Pages: s.pages,
299 Config: s.config,
300 Enforcer: s.enforcer,
301 IdResolver: s.idResolver,
302 Logger: logger,
303 }
304
305 return spindles.Router()
306}
307
308func (s *State) KnotsRouter() http.Handler {
309 logger := log.SubLogger(s.logger, "knots")
310
311 knots := &knots.Knots{
312 Db: s.db,
313 OAuth: s.oauth,
314 Pages: s.pages,
315 Config: s.config,
316 Enforcer: s.enforcer,
317 Acl: s.aclService,
318 IdResolver: s.idResolver,
319 Knotstream: s.knotstream,
320 Logger: logger,
321 }
322
323 return knots.Router()
324}
325
326func (s *State) StringsRouter(mw *middleware.Middleware) http.Handler {
327 logger := log.SubLogger(s.logger, "strings")
328
329 strs := &avstrings.Strings{
330 Db: s.db,
331 OAuth: s.oauth,
332 Pages: s.pages,
333 IdResolver: s.idResolver,
334 Notifier: s.notifier,
335 Logger: logger,
336 }
337
338 return strs.Router(mw)
339}
340
341func (s *State) IssuesRouter(mw *middleware.Middleware) http.Handler {
342 issues := issues.New(
343 s.oauth,
344 s.repoResolver,
345 s.aclService,
346 s.pages,
347 s.idResolver,
348 s.mentionsResolver,
349 s.db,
350 s.config,
351 s.notifier,
352 s.validator,
353 s.indexer.Issues,
354 log.SubLogger(s.logger, "issues"),
355 )
356 return issues.Router(mw)
357}
358
359func (s *State) PullsRouter(mw *middleware.Middleware) http.Handler {
360 pulls := pulls.New(
361 s.oauth,
362 s.repoResolver,
363 s.pages,
364 s.idResolver,
365 s.mentionsResolver,
366 s.db,
367 s.config,
368 s.notifier,
369 s.aclService,
370 s.validator,
371 s.indexer.Pulls,
372 log.SubLogger(s.logger, "pulls"),
373 )
374 return pulls.Router(mw)
375}
376
377func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
378 repo := repo.New(
379 s.oauth,
380 s.repoResolver,
381 s.pages,
382 s.spindlestream,
383 s.idResolver,
384 s.db,
385 s.config,
386 s.notifier,
387 s.enforcer,
388 s.aclService,
389 log.SubLogger(s.logger, "repo"),
390 s.validator,
391 s.cfClient,
392 )
393 return repo.Router(mw)
394}
395
396func (s *State) PipelinesRouter(mw *middleware.Middleware) http.Handler {
397 pipes := pipelines.New(
398 s.oauth,
399 s.repoResolver,
400 s.pages,
401 s.spindlestream,
402 s.pipelineNotifier,
403 s.idResolver,
404 s.db,
405 s.config,
406 s.enforcer,
407 log.SubLogger(s.logger, "pipelines"),
408 )
409 return pipes.Router(mw)
410}
411
412func (s *State) LabelsRouter() http.Handler {
413 ls := labels.New(
414 s.oauth,
415 s.pages,
416 s.db,
417 s.validator,
418 s.enforcer,
419 s.notifier,
420 log.SubLogger(s.logger, "labels"),
421 )
422 return ls.Router()
423}
424
425func (s *State) NotificationsRouter(mw *middleware.Middleware) http.Handler {
426 notifs := notifications.New(s.db, s.oauth, s.pages, log.SubLogger(s.logger, "notifications"))
427 return notifs.Router(mw)
428}
429
430func (s *State) SignupRouter() http.Handler {
431 sig := signup.New(s.config, s.db, s.posthog, s.idResolver, s.pages, log.SubLogger(s.logger, "signup"))
432 return sig.Router()
433}