this repo has no description
1package state 2 3import ( 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 21func (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 73func (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 114func (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 155func (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 212func (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 252func (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 294func (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 345func (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 406func (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 444type FullyResolvedRepo struct { 445 Knot string 446 OwnerId identity.Identity 447 RepoName string 448} 449 450func (f *FullyResolvedRepo) OwnerDid() string { 451 return f.OwnerId.DID.String() 452} 453 454func (f *FullyResolvedRepo) OwnerHandle() string { 455 return f.OwnerId.Handle.String() 456} 457 458func (f *FullyResolvedRepo) OwnerSlashRepo() string { 459 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 460 return p 461} 462 463func (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 502func 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 522func 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}