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