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}