this repo has no description
1package state
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9 "path"
10 "strings"
11
12 "github.com/bluesky-social/indigo/atproto/identity"
13 securejoin "github.com/cyphar/filepath-securejoin"
14 "github.com/go-chi/chi/v5"
15 "github.com/sotangled/tangled/appview/auth"
16 "github.com/sotangled/tangled/appview/pages"
17 "github.com/sotangled/tangled/types"
18)
19
20func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
21 ref := chi.URLParam(r, "ref")
22 f, err := fullyResolvedRepo(r)
23 if err != nil {
24 log.Println("failed to fully resolve repo", err)
25 return
26 }
27 var reqUrl string
28 if ref != "" {
29 reqUrl = fmt.Sprintf("http://%s/%s/%s/tree/%s", f.Knot, f.OwnerDid(), f.RepoName, ref)
30 } else {
31 reqUrl = fmt.Sprintf("http://%s/%s/%s", f.Knot, f.OwnerDid(), f.RepoName)
32 }
33
34 resp, err := http.Get(reqUrl)
35 if err != nil {
36 s.pages.Error503(w)
37 log.Println("failed to reach knotserver", err)
38 return
39 }
40 defer resp.Body.Close()
41
42 body, err := io.ReadAll(resp.Body)
43 if err != nil {
44 log.Fatalf("Error reading response body: %v", err)
45 return
46 }
47
48 var result types.RepoIndexResponse
49 err = json.Unmarshal(body, &result)
50 if err != nil {
51 log.Fatalf("Error unmarshalling response body: %v", err)
52 return
53 }
54
55 log.Println(resp.Status, result)
56
57 user := s.auth.GetUser(r)
58 s.pages.RepoIndexPage(w, pages.RepoIndexParams{
59 LoggedInUser: user,
60 RepoInfo: pages.RepoInfo{
61 OwnerDid: f.OwnerDid(),
62 OwnerHandle: f.OwnerHandle(),
63 Name: f.RepoName,
64 SettingsAllowed: settingsAllowed(s, user, f),
65 },
66 RepoIndexResponse: result,
67 })
68
69 return
70}
71
72func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
73 f, err := fullyResolvedRepo(r)
74 if err != nil {
75 log.Println("failed to fully resolve repo", err)
76 return
77 }
78
79 ref := chi.URLParam(r, "ref")
80 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/log/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
81 if err != nil {
82 log.Println("failed to reach knotserver", err)
83 return
84 }
85
86 body, err := io.ReadAll(resp.Body)
87 if err != nil {
88 log.Fatalf("Error reading response body: %v", err)
89 return
90 }
91
92 var result types.RepoLogResponse
93 err = json.Unmarshal(body, &result)
94 if err != nil {
95 log.Println("failed to parse json response", err)
96 return
97 }
98
99 user := s.auth.GetUser(r)
100 s.pages.RepoLog(w, pages.RepoLogParams{
101 LoggedInUser: user,
102 RepoInfo: pages.RepoInfo{
103 OwnerDid: f.OwnerDid(),
104 OwnerHandle: f.OwnerHandle(),
105 Name: f.RepoName,
106 SettingsAllowed: settingsAllowed(s, user, f),
107 },
108 RepoLogResponse: result,
109 })
110 return
111}
112
113func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
114 f, err := fullyResolvedRepo(r)
115 if err != nil {
116 log.Println("failed to fully resolve repo", err)
117 return
118 }
119
120 ref := chi.URLParam(r, "ref")
121 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/commit/%s", f.Knot, f.OwnerDid(), f.RepoName, ref))
122 if err != nil {
123 log.Println("failed to reach knotserver", err)
124 return
125 }
126
127 body, err := io.ReadAll(resp.Body)
128 if err != nil {
129 log.Fatalf("Error reading response body: %v", err)
130 return
131 }
132
133 var result types.RepoCommitResponse
134 err = json.Unmarshal(body, &result)
135 if err != nil {
136 log.Println("failed to parse response:", err)
137 return
138 }
139
140 user := s.auth.GetUser(r)
141 s.pages.RepoCommit(w, pages.RepoCommitParams{
142 LoggedInUser: user,
143 RepoInfo: pages.RepoInfo{
144 OwnerDid: f.OwnerDid(),
145 OwnerHandle: f.OwnerHandle(),
146 Name: f.RepoName,
147 SettingsAllowed: settingsAllowed(s, user, f),
148 },
149 RepoCommitResponse: result,
150 })
151 return
152}
153
154func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
155 f, err := fullyResolvedRepo(r)
156 if err != nil {
157 log.Println("failed to fully resolve repo", err)
158 return
159 }
160
161 ref := chi.URLParam(r, "ref")
162 treePath := chi.URLParam(r, "*")
163 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tree/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
164 if err != nil {
165 log.Println("failed to reach knotserver", err)
166 return
167 }
168
169 body, err := io.ReadAll(resp.Body)
170 if err != nil {
171 log.Fatalf("Error reading response body: %v", err)
172 return
173 }
174
175 var result types.RepoTreeResponse
176 err = json.Unmarshal(body, &result)
177 if err != nil {
178 log.Println("failed to parse response:", err)
179 return
180 }
181
182 user := s.auth.GetUser(r)
183
184 var breadcrumbs [][]string
185 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
186 if treePath != "" {
187 for idx, elem := range strings.Split(treePath, "/") {
188 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
189 }
190 }
191
192 baseTreeLink := path.Join(f.OwnerDid(), f.RepoName, "tree", ref, treePath)
193 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath)
194
195 s.pages.RepoTree(w, pages.RepoTreeParams{
196 LoggedInUser: user,
197 BreadCrumbs: breadcrumbs,
198 BaseTreeLink: baseTreeLink,
199 BaseBlobLink: baseBlobLink,
200 RepoInfo: pages.RepoInfo{
201 OwnerDid: f.OwnerDid(),
202 OwnerHandle: f.OwnerHandle(),
203 Name: f.RepoName,
204 SettingsAllowed: settingsAllowed(s, user, f),
205 },
206 RepoTreeResponse: result,
207 })
208 return
209}
210
211func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
212 f, err := fullyResolvedRepo(r)
213 if err != nil {
214 log.Println("failed to get repo and knot", err)
215 return
216 }
217
218 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/tags", f.Knot, f.OwnerDid(), f.RepoName))
219 if err != nil {
220 log.Println("failed to reach knotserver", err)
221 return
222 }
223
224 body, err := io.ReadAll(resp.Body)
225 if err != nil {
226 log.Fatalf("Error reading response body: %v", err)
227 return
228 }
229
230 var result types.RepoTagsResponse
231 err = json.Unmarshal(body, &result)
232 if err != nil {
233 log.Println("failed to parse response:", err)
234 return
235 }
236
237 user := s.auth.GetUser(r)
238 s.pages.RepoTags(w, pages.RepoTagsParams{
239 LoggedInUser: user,
240 RepoInfo: pages.RepoInfo{
241 OwnerDid: f.OwnerDid(),
242 OwnerHandle: f.OwnerHandle(),
243 Name: f.RepoName,
244 SettingsAllowed: settingsAllowed(s, user, f),
245 },
246 RepoTagsResponse: result,
247 })
248 return
249}
250
251func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
252 f, err := fullyResolvedRepo(r)
253 if err != nil {
254 log.Println("failed to get repo and knot", err)
255 return
256 }
257
258 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/branches", f.Knot, f.OwnerDid(), f.RepoName))
259 if err != nil {
260 log.Println("failed to reach knotserver", err)
261 return
262 }
263
264 body, err := io.ReadAll(resp.Body)
265 if err != nil {
266 log.Fatalf("Error reading response body: %v", err)
267 return
268 }
269
270 var result types.RepoBranchesResponse
271 err = json.Unmarshal(body, &result)
272 if err != nil {
273 log.Println("failed to parse response:", err)
274 return
275 }
276
277 log.Println(result)
278
279 user := s.auth.GetUser(r)
280 s.pages.RepoBranches(w, pages.RepoBranchesParams{
281 LoggedInUser: user,
282 RepoInfo: pages.RepoInfo{
283 OwnerDid: f.OwnerDid(),
284 OwnerHandle: f.OwnerHandle(),
285 Name: f.RepoName,
286 SettingsAllowed: settingsAllowed(s, user, f),
287 },
288 RepoBranchesResponse: result,
289 })
290 return
291}
292
293func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
294 f, err := fullyResolvedRepo(r)
295 if err != nil {
296 log.Println("failed to get repo and knot", err)
297 return
298 }
299
300 ref := chi.URLParam(r, "ref")
301 filePath := chi.URLParam(r, "*")
302 resp, err := http.Get(fmt.Sprintf("http://%s/%s/%s/blob/%s/%s", f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
303 if err != nil {
304 log.Println("failed to reach knotserver", err)
305 return
306 }
307
308 body, err := io.ReadAll(resp.Body)
309 if err != nil {
310 log.Fatalf("Error reading response body: %v", err)
311 return
312 }
313
314 var result types.RepoBlobResponse
315 err = json.Unmarshal(body, &result)
316 if err != nil {
317 log.Println("failed to parse response:", err)
318 return
319 }
320
321 var breadcrumbs [][]string
322 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/%s/tree/%s", f.OwnerDid(), f.RepoName, ref)})
323 if filePath != "" {
324 for idx, elem := range strings.Split(filePath, "/") {
325 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
326 }
327 }
328
329 user := s.auth.GetUser(r)
330 s.pages.RepoBlob(w, pages.RepoBlobParams{
331 LoggedInUser: user,
332 RepoInfo: pages.RepoInfo{
333 OwnerDid: f.OwnerDid(),
334 OwnerHandle: f.OwnerHandle(),
335 Name: f.RepoName,
336 SettingsAllowed: settingsAllowed(s, user, f),
337 },
338 RepoBlobResponse: result,
339 BreadCrumbs: breadcrumbs,
340 })
341 return
342}
343
344func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
345 f, err := fullyResolvedRepo(r)
346 if err != nil {
347 log.Println("failed to get repo and knot", err)
348 return
349 }
350
351 collaborator := r.FormValue("collaborator")
352 if collaborator == "" {
353 http.Error(w, "malformed form", http.StatusBadRequest)
354 return
355 }
356
357 collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
358 if err != nil {
359 w.Write([]byte("failed to resolve collaborator did to a handle"))
360 return
361 }
362 log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
363
364 // TODO: create an atproto record for this
365
366 secret, err := s.db.GetRegistrationKey(f.Knot)
367 if err != nil {
368 log.Printf("no key found for domain %s: %s\n", f.Knot, err)
369 return
370 }
371
372 ksClient, err := NewSignedClient(f.Knot, secret)
373 if err != nil {
374 log.Println("failed to create client to ", f.Knot)
375 return
376 }
377
378 ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
379 if err != nil {
380 log.Printf("failed to make request to %s: %s", f.Knot, err)
381 return
382 }
383
384 if ksResp.StatusCode != http.StatusNoContent {
385 w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
386 return
387 }
388
389 err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.OwnerSlashRepo())
390 if err != nil {
391 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
392 return
393 }
394
395 w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
396
397}
398
399func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
400 f, err := fullyResolvedRepo(r)
401 if err != nil {
402 log.Println("failed to get repo and knot", err)
403 return
404 }
405
406 switch r.Method {
407 case http.MethodGet:
408 // for now, this is just pubkeys
409 user := s.auth.GetUser(r)
410 repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
411 if err != nil {
412 log.Println("failed to get collaborators", err)
413 }
414 log.Println(repoCollaborators)
415
416 isCollaboratorInviteAllowed := false
417 if user != nil {
418 ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.OwnerSlashRepo())
419 if err == nil && ok {
420 isCollaboratorInviteAllowed = true
421 }
422 }
423
424 s.pages.RepoSettings(w, pages.RepoSettingsParams{
425 LoggedInUser: user,
426 RepoInfo: pages.RepoInfo{
427 OwnerDid: f.OwnerDid(),
428 OwnerHandle: f.OwnerHandle(),
429 Name: f.RepoName,
430 SettingsAllowed: settingsAllowed(s, user, f),
431 },
432 Collaborators: repoCollaborators,
433 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
434 })
435 }
436}
437
438type FullyResolvedRepo struct {
439 Knot string
440 OwnerId identity.Identity
441 RepoName string
442}
443
444func (f *FullyResolvedRepo) OwnerDid() string {
445 return f.OwnerId.DID.String()
446}
447
448func (f *FullyResolvedRepo) OwnerHandle() string {
449 return f.OwnerId.Handle.String()
450}
451
452func (f *FullyResolvedRepo) OwnerSlashRepo() string {
453 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
454 return p
455}
456
457func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
458 repoName := chi.URLParam(r, "repo")
459 knot, ok := r.Context().Value("knot").(string)
460 if !ok {
461 log.Println("malformed middleware")
462 return nil, fmt.Errorf("malformed middleware")
463 }
464 id, ok := r.Context().Value("resolvedId").(identity.Identity)
465 if !ok {
466 log.Println("malformed middleware")
467 return nil, fmt.Errorf("malformed middleware")
468 }
469
470 return &FullyResolvedRepo{
471 Knot: knot,
472 OwnerId: id,
473 RepoName: repoName,
474 }, nil
475}
476
477func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
478 settingsAllowed := false
479 if u != nil {
480 ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
481 if err == nil && ok {
482 settingsAllowed = true
483 } else {
484 log.Println(err, ok)
485 }
486 }
487
488 return settingsAllowed
489}