489 lines
12 kB
1
package state
2
3
import (
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
20
func (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
72
func (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
113
func (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
154
func (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
211
func (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
251
func (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
293
func (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
344
func (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
399
func (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
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 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
477
func 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
}
490