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