this repo has no description
1package state
2
3import (
4 "net/http"
5 "regexp"
6 "strings"
7
8 "github.com/go-chi/chi/v5"
9)
10
11func (s *State) Router() http.Handler {
12 router := chi.NewRouter()
13
14 router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
15 pat := chi.URLParam(r, "*")
16 if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
17 s.UserRouter().ServeHTTP(w, r)
18 } else {
19 // Check if the first path element is a valid handle without '@' or a flattened DID
20 pathParts := strings.SplitN(pat, "/", 2)
21 if len(pathParts) > 0 {
22 if isHandleNoAt(pathParts[0]) {
23 // Redirect to the same path but with '@' prefixed to the handle
24 redirectPath := "@" + pat
25 http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
26 return
27 } else if isFlattenedDid(pathParts[0]) {
28 // Redirect to the unflattened DID version
29 unflattenedDid := unflattenDid(pathParts[0])
30 var redirectPath string
31 if len(pathParts) > 1 {
32 redirectPath = unflattenedDid + "/" + pathParts[1]
33 } else {
34 redirectPath = unflattenedDid
35 }
36 http.Redirect(w, r, "/"+redirectPath, http.StatusFound)
37 return
38 }
39 }
40 s.StandardRouter().ServeHTTP(w, r)
41 }
42 })
43
44 return router
45}
46
47func isHandleNoAt(s string) bool {
48 // ref: https://atproto.com/specs/handle
49 re := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
50 return re.MatchString(s)
51}
52
53func unflattenDid(s string) string {
54 if !isFlattenedDid(s) {
55 return s
56 }
57
58 parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
59 if len(parts) != 2 {
60 return s
61 }
62
63 return "did:" + parts[0] + ":" + parts[1]
64}
65
66// isFlattenedDid checks if the given string is a flattened DID.
67// A flattened DID is a DID with the :s swapped to -s to satisfy certain
68// application requirements, such as Go module naming conventions.
69func isFlattenedDid(s string) bool {
70 // Check if the string starts with "did-"
71 if !strings.HasPrefix(s, "did-") {
72 return false
73 }
74
75 // Split the string to extract method and identifier
76 parts := strings.SplitN(s[4:], "-", 2) // Skip "did-" prefix and split on first "-"
77 if len(parts) != 2 {
78 return false
79 }
80
81 // Reconstruct as a standard DID format
82 // Example: "did-plc-xyz-abc" becomes "did:plc:xyz-abc"
83 reconstructed := "did:" + parts[0] + ":" + parts[1]
84 re := regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`)
85
86 return re.MatchString(reconstructed)
87}
88
89func (s *State) UserRouter() http.Handler {
90 r := chi.NewRouter()
91
92 // strip @ from user
93 r.Use(StripLeadingAt)
94
95 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
96 r.Get("/", s.ProfilePage)
97 r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
98 r.Get("/", s.RepoIndex)
99 r.Get("/commits/{ref}", s.RepoLog)
100 r.Route("/tree/{ref}", func(r chi.Router) {
101 r.Get("/", s.RepoIndex)
102 r.Get("/*", s.RepoTree)
103 })
104 r.Get("/commit/{ref}", s.RepoCommit)
105 r.Get("/branches", s.RepoBranches)
106 r.Get("/tags", s.RepoTags)
107 r.Get("/blob/{ref}/*", s.RepoBlob)
108
109 r.Route("/issues", func(r chi.Router) {
110 r.Get("/", s.RepoIssues)
111 r.Get("/{issue}", s.RepoSingleIssue)
112
113 r.Group(func(r chi.Router) {
114 r.Use(AuthMiddleware(s))
115 r.Get("/new", s.NewIssue)
116 r.Post("/new", s.NewIssue)
117 r.Post("/{issue}/comment", s.IssueComment)
118 r.Post("/{issue}/close", s.CloseIssue)
119 r.Post("/{issue}/reopen", s.ReopenIssue)
120 })
121 })
122
123 r.Route("/pulls", func(r chi.Router) {
124 r.Get("/", s.RepoPulls)
125 r.With(AuthMiddleware(s)).Route("/new", func(r chi.Router) {
126 r.Get("/", s.NewPull)
127 r.Post("/", s.NewPull)
128 })
129
130 r.Route("/{pull}", func(r chi.Router) {
131 r.Use(ResolvePull(s))
132 r.Get("/", s.RepoSinglePull)
133
134 r.Route("/round/{round}", func(r chi.Router) {
135 r.Get("/", s.RepoPullPatch)
136 r.Get("/actions", s.PullActions)
137 r.Route("/comment", func(r chi.Router) {
138 r.Get("/", s.PullComment)
139 r.Post("/", s.PullComment)
140 })
141 })
142
143 // authorized requests below this point
144 r.Group(func(r chi.Router) {
145 r.Use(AuthMiddleware(s))
146 r.Route("/resubmit", func(r chi.Router) {
147 r.Get("/", s.ResubmitPull)
148 r.Post("/", s.ResubmitPull)
149 })
150 r.Route("/comment", func(r chi.Router) {
151 r.Get("/", s.PullComment)
152 r.Post("/", s.PullComment)
153 })
154 r.Post("/close", s.ClosePull)
155 r.Post("/reopen", s.ReopenPull)
156 // collaborators only
157 r.Group(func(r chi.Router) {
158 r.Use(RepoPermissionMiddleware(s, "repo:push"))
159 r.Post("/merge", s.MergePull)
160 // maybe lock, etc.
161 })
162 })
163 })
164 })
165
166 // These routes get proxied to the knot
167 r.Get("/info/refs", s.InfoRefs)
168 r.Post("/git-upload-pack", s.UploadPack)
169
170 // settings routes, needs auth
171 r.Group(func(r chi.Router) {
172 r.Use(AuthMiddleware(s))
173 // repo description can only be edited by owner
174 r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
175 r.Put("/", s.RepoDescription)
176 r.Get("/", s.RepoDescription)
177 r.Get("/edit", s.RepoDescriptionEdit)
178 })
179 r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
180 r.Get("/", s.RepoSettings)
181 r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
182 })
183 })
184 })
185 })
186
187 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
188 s.pages.Error404(w)
189 })
190
191 return r
192}
193
194func (s *State) StandardRouter() http.Handler {
195 r := chi.NewRouter()
196
197 r.Handle("/static/*", s.pages.Static())
198
199 r.Get("/", s.Timeline)
200
201 r.With(AuthMiddleware(s)).Get("/logout", s.Logout)
202
203 r.Route("/login", func(r chi.Router) {
204 r.Get("/", s.Login)
205 r.Post("/", s.Login)
206 })
207
208 r.Route("/knots", func(r chi.Router) {
209 r.Use(AuthMiddleware(s))
210 r.Get("/", s.Knots)
211 r.Post("/key", s.RegistrationKey)
212
213 r.Route("/{domain}", func(r chi.Router) {
214 r.Post("/init", s.InitKnotServer)
215 r.Get("/", s.KnotServerInfo)
216 r.Route("/member", func(r chi.Router) {
217 r.Use(KnotOwner(s))
218 r.Get("/", s.ListMembers)
219 r.Put("/", s.AddMember)
220 r.Delete("/", s.RemoveMember)
221 })
222 })
223 })
224
225 r.Route("/repo", func(r chi.Router) {
226 r.Route("/new", func(r chi.Router) {
227 r.Use(AuthMiddleware(s))
228 r.Get("/", s.NewRepo)
229 r.Post("/", s.NewRepo)
230 })
231 // r.Post("/import", s.ImportRepo)
232 })
233
234 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
235 r.Post("/", s.Follow)
236 r.Delete("/", s.Follow)
237 })
238
239 r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) {
240 r.Post("/", s.Star)
241 r.Delete("/", s.Star)
242 })
243
244 r.Route("/settings", func(r chi.Router) {
245 r.Use(AuthMiddleware(s))
246 r.Get("/", s.Settings)
247 r.Put("/keys", s.SettingsKeys)
248 r.Put("/emails", s.SettingsEmails)
249 r.Delete("/emails", s.SettingsEmails)
250 r.Get("/emails/verify", s.SettingsEmailsVerify)
251 r.Post("/emails/verify/resend", s.SettingsEmailsVerifyResend)
252 r.Post("/emails/primary", s.SettingsEmailsPrimary)
253 })
254
255 r.Get("/keys/{user}", s.Keys)
256
257 r.NotFound(func(w http.ResponseWriter, r *http.Request) {
258 s.pages.Error404(w)
259 })
260 return r
261}