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