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