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