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 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 71func (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 112func (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 153func (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 210func (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 250func (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 290func (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 341func (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 402func (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 440type FullyResolvedRepo struct { 441 Knot string 442 OwnerId identity.Identity 443 RepoName string 444} 445 446func (f *FullyResolvedRepo) OwnerDid() string { 447 return f.OwnerId.DID.String() 448} 449 450func (f *FullyResolvedRepo) OwnerHandle() string { 451 return f.OwnerId.Handle.String() 452} 453 454func (f *FullyResolvedRepo) OwnerSlashRepo() string { 455 p, _ := securejoin.SecureJoin(f.OwnerDid(), f.RepoName) 456 return p 457} 458 459func (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 503func 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 523func 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}