528 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
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
397
398
}
399
400
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
401
f, err := fullyResolvedRepo(r)
402
if err != nil {
403
log.Println("failed to get repo and knot", err)
404
return
405
}
406
407
switch r.Method {
408
case http.MethodGet:
409
// for now, this is just pubkeys
410
user := s.auth.GetUser(r)
411
repoCollaborators, err := f.Collaborators(r.Context(), s)
412
if err != nil {
413
log.Println("failed to get collaborators", err)
414
}
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
438
type FullyResolvedRepo struct {
439
Knot string
440
OwnerId identity.Identity
441
RepoName string
442
}
443
444
func (f *FullyResolvedRepo) OwnerDid() string {
445
return f.OwnerId.DID.String()
446
}
447
448
func (f *FullyResolvedRepo) OwnerHandle() string {
449
return f.OwnerId.Handle.String()
450
}
451
452
func (f *FullyResolvedRepo) OwnerSlashRepo() string {
453
p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName)
454
return p
455
}
456
457
func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]pages.Collaborator, error) {
458
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.OwnerSlashRepo(), f.Knot)
459
if err != nil {
460
return nil, err
461
}
462
463
var collaborators []pages.Collaborator
464
for _, item := range repoCollaborators {
465
// currently only two roles: owner and member
466
var role string
467
if item[3] == "repo:owner" {
468
role = "owner"
469
} else if item[3] == "repo:collaborator" {
470
role = "collaborator"
471
} else {
472
continue
473
}
474
475
did := item[0]
476
477
var handle string
478
id, err := s.resolver.ResolveIdent(ctx, did)
479
if err != nil {
480
handle = ""
481
} else {
482
handle = string(id.Handle)
483
}
484
485
c := pages.Collaborator{
486
Did: did,
487
Handle: handle,
488
Role: role,
489
}
490
collaborators = append(collaborators, c)
491
}
492
493
return collaborators, nil
494
}
495
496
func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
497
repoName := chi.URLParam(r, "repo")
498
knot, ok := r.Context().Value("knot").(string)
499
if !ok {
500
log.Println("malformed middleware")
501
return nil, fmt.Errorf("malformed middleware")
502
}
503
id, ok := r.Context().Value("resolvedId").(identity.Identity)
504
if !ok {
505
log.Println("malformed middleware")
506
return nil, fmt.Errorf("malformed middleware")
507
}
508
509
return &FullyResolvedRepo{
510
Knot: knot,
511
OwnerId: id,
512
RepoName: repoName,
513
}, nil
514
}
515
516
func settingsAllowed(s *State, u *auth.User, f *FullyResolvedRepo) bool {
517
settingsAllowed := false
518
if u != nil {
519
ok, err := s.enforcer.IsSettingsAllowed(u.Did, f.Knot, f.OwnerSlashRepo())
520
if err == nil && ok {
521
settingsAllowed = true
522
} else {
523
log.Println(err, ok)
524
}
525
}
526
527
return settingsAllowed
528
}
529