forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package state 2 3import ( 4 "context" 5 "crypto/hmac" 6 "crypto/sha256" 7 "encoding/hex" 8 "fmt" 9 "log" 10 "log/slog" 11 "net/http" 12 "runtime/debug" 13 "strings" 14 "time" 15 16 comatproto "github.com/bluesky-social/indigo/api/atproto" 17 "github.com/bluesky-social/indigo/atproto/syntax" 18 lexutil "github.com/bluesky-social/indigo/lex/util" 19 securejoin "github.com/cyphar/filepath-securejoin" 20 "github.com/go-chi/chi/v5" 21 "go.opentelemetry.io/otel/attribute" 22 "tangled.sh/tangled.sh/core/api/tangled" 23 "tangled.sh/tangled.sh/core/appview" 24 "tangled.sh/tangled.sh/core/appview/auth" 25 "tangled.sh/tangled.sh/core/appview/db" 26 "tangled.sh/tangled.sh/core/appview/pages" 27 "tangled.sh/tangled.sh/core/jetstream" 28 "tangled.sh/tangled.sh/core/rbac" 29 "tangled.sh/tangled.sh/core/telemetry" 30) 31 32type State struct { 33 db *db.DB 34 auth *auth.Auth 35 enforcer *rbac.Enforcer 36 tidClock *syntax.TIDClock 37 pages *pages.Pages 38 resolver *appview.Resolver 39 jc *jetstream.JetstreamClient 40 t *telemetry.Telemetry 41 config *appview.Config 42} 43 44func Make(ctx context.Context, config *appview.Config) (*State, error) { 45 d, err := db.Make(config.DbPath) 46 if err != nil { 47 return nil, err 48 } 49 50 auth, err := auth.Make(config.CookieSecret) 51 if err != nil { 52 return nil, err 53 } 54 55 enforcer, err := rbac.NewEnforcer(config.DbPath) 56 if err != nil { 57 return nil, err 58 } 59 60 clock := syntax.NewTIDClock(0) 61 62 pgs := pages.NewPages(config) 63 64 resolver := appview.NewResolver() 65 66 bi, ok := debug.ReadBuildInfo() 67 var version string 68 if ok { 69 version = bi.Main.Version 70 } else { 71 version = "v0.0.0-unknown" 72 } 73 74 wrapper := db.DbWrapper{d} 75 jc, err := jetstream.NewJetstreamClient( 76 config.JetstreamEndpoint, 77 "appview", 78 []string{tangled.GraphFollowNSID, tangled.FeedStarNSID, tangled.PublicKeyNSID, tangled.RepoArtifactNSID}, 79 nil, 80 slog.Default(), 81 wrapper, 82 false, 83 ) 84 if err != nil { 85 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 86 } 87 err = jc.StartJetstream(ctx, appview.Ingest(wrapper)) 88 if err != nil { 89 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 90 } 91 92 var tele *telemetry.Telemetry 93 if config.EnableTelemetry { 94 tele, err = telemetry.NewTelemetry(ctx, "appview", version, config.Dev) 95 if err != nil { 96 return nil, fmt.Errorf("failed to setup telemetry: %w", err) 97 } 98 } 99 100 state := &State{ 101 d, 102 auth, 103 enforcer, 104 clock, 105 pgs, 106 resolver, 107 jc, 108 tele, 109 config, 110 } 111 112 return state, nil 113} 114 115func TID(c *syntax.TIDClock) string { 116 return c.Next().String() 117} 118 119func (s *State) Login(w http.ResponseWriter, r *http.Request) { 120 ctx := r.Context() 121 122 switch r.Method { 123 case http.MethodGet: 124 err := s.pages.Login(w, pages.LoginParams{}) 125 if err != nil { 126 log.Printf("rendering login page: %s", err) 127 } 128 129 return 130 case http.MethodPost: 131 handle := strings.TrimPrefix(r.FormValue("handle"), "@") 132 appPassword := r.FormValue("app_password") 133 134 resolved, err := s.resolver.ResolveIdent(ctx, handle) 135 if err != nil { 136 log.Println("failed to resolve handle:", err) 137 s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 138 return 139 } 140 141 atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 142 if err != nil { 143 s.pages.Notice(w, "login-msg", "Invalid handle or password.") 144 return 145 } 146 sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 147 148 err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 149 if err != nil { 150 s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 151 return 152 } 153 154 log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 155 156 did := resolved.DID.String() 157 defaultKnot := "knot1.tangled.sh" 158 159 go func() { 160 log.Printf("adding %s to default knot", did) 161 err = s.enforcer.AddMember(defaultKnot, did) 162 if err != nil { 163 log.Println("failed to add user to knot1.tangled.sh: ", err) 164 return 165 } 166 err = s.enforcer.E.SavePolicy() 167 if err != nil { 168 log.Println("failed to add user to knot1.tangled.sh: ", err) 169 return 170 } 171 172 secret, err := db.GetRegistrationKey(s.db, defaultKnot) 173 if err != nil { 174 log.Println("failed to get registration key for knot1.tangled.sh") 175 return 176 } 177 signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev) 178 resp, err := signedClient.AddMember(did) 179 if err != nil { 180 log.Println("failed to add user to knot1.tangled.sh: ", err) 181 return 182 } 183 184 if resp.StatusCode != http.StatusNoContent { 185 log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 186 return 187 } 188 }() 189 190 s.pages.HxRedirect(w, "/") 191 return 192 } 193} 194 195func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 196 s.auth.ClearSession(r, w) 197 w.Header().Set("HX-Redirect", "/login") 198 w.WriteHeader(http.StatusSeeOther) 199} 200 201func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 202 ctx, span := s.t.TraceStart(r.Context(), "Timeline") 203 defer span.End() 204 205 user := s.auth.GetUser(r) 206 span.SetAttributes(attribute.String("user.did", user.Did)) 207 208 timeline, err := db.MakeTimeline(ctx, s.db) 209 if err != nil { 210 log.Println(err) 211 span.RecordError(err) 212 s.pages.Notice(w, "timeline", "Uh oh! Failed to load timeline.") 213 } 214 215 var didsToResolve []string 216 for _, ev := range timeline { 217 if ev.Repo != nil { 218 didsToResolve = append(didsToResolve, ev.Repo.Did) 219 if ev.Source != nil { 220 didsToResolve = append(didsToResolve, ev.Source.Did) 221 } 222 } 223 if ev.Follow != nil { 224 didsToResolve = append(didsToResolve, ev.Follow.UserDid, ev.Follow.SubjectDid) 225 } 226 if ev.Star != nil { 227 didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did) 228 } 229 } 230 span.SetAttributes(attribute.Int("dids.to_resolve.count", len(didsToResolve))) 231 232 resolvedIds := s.resolver.ResolveIdents(ctx, didsToResolve) 233 didHandleMap := make(map[string]string) 234 for _, identity := range resolvedIds { 235 if !identity.Handle.IsInvalidHandle() { 236 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 237 } else { 238 didHandleMap[identity.DID.String()] = identity.DID.String() 239 } 240 } 241 span.SetAttributes(attribute.Int("dids.resolved.count", len(resolvedIds))) 242 243 s.pages.Timeline(w, pages.TimelineParams{ 244 LoggedInUser: user, 245 Timeline: timeline, 246 DidHandleMap: didHandleMap, 247 }) 248 249 return 250} 251 252// requires auth 253func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { 254 switch r.Method { 255 case http.MethodGet: 256 // list open registrations under this did 257 258 return 259 case http.MethodPost: 260 session, err := s.auth.Store.Get(r, appview.SessionName) 261 if err != nil || session.IsNew { 262 log.Println("unauthorized attempt to generate registration key") 263 http.Error(w, "Forbidden", http.StatusUnauthorized) 264 return 265 } 266 267 did := session.Values[appview.SessionDid].(string) 268 269 // check if domain is valid url, and strip extra bits down to just host 270 domain := r.FormValue("domain") 271 if domain == "" { 272 http.Error(w, "Invalid form", http.StatusBadRequest) 273 return 274 } 275 276 key, err := db.GenerateRegistrationKey(s.db, domain, did) 277 278 if err != nil { 279 log.Println(err) 280 http.Error(w, "unable to register this domain", http.StatusNotAcceptable) 281 return 282 } 283 284 w.Write([]byte(key)) 285 } 286} 287 288func (s *State) Keys(w http.ResponseWriter, r *http.Request) { 289 user := chi.URLParam(r, "user") 290 user = strings.TrimPrefix(user, "@") 291 292 if user == "" { 293 w.WriteHeader(http.StatusBadRequest) 294 return 295 } 296 297 id, err := s.resolver.ResolveIdent(r.Context(), user) 298 if err != nil { 299 w.WriteHeader(http.StatusInternalServerError) 300 return 301 } 302 303 pubKeys, err := db.GetPublicKeys(s.db, id.DID.String()) 304 if err != nil { 305 w.WriteHeader(http.StatusNotFound) 306 return 307 } 308 309 if len(pubKeys) == 0 { 310 w.WriteHeader(http.StatusNotFound) 311 return 312 } 313 314 for _, k := range pubKeys { 315 key := strings.TrimRight(k.Key, "\n") 316 w.Write([]byte(fmt.Sprintln(key))) 317 } 318} 319 320// create a signed request and check if a node responds to that 321func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 322 user := s.auth.GetUser(r) 323 324 domain := chi.URLParam(r, "domain") 325 if domain == "" { 326 http.Error(w, "malformed url", http.StatusBadRequest) 327 return 328 } 329 log.Println("checking ", domain) 330 331 secret, err := db.GetRegistrationKey(s.db, domain) 332 if err != nil { 333 log.Printf("no key found for domain %s: %s\n", domain, err) 334 return 335 } 336 337 client, err := NewSignedClient(domain, secret, s.config.Dev) 338 if err != nil { 339 log.Println("failed to create client to ", domain) 340 } 341 342 resp, err := client.Init(user.Did) 343 if err != nil { 344 w.Write([]byte("no dice")) 345 log.Println("domain was unreachable after 5 seconds") 346 return 347 } 348 349 if resp.StatusCode == http.StatusConflict { 350 log.Println("status conflict", resp.StatusCode) 351 w.Write([]byte("already registered, sorry!")) 352 return 353 } 354 355 if resp.StatusCode != http.StatusNoContent { 356 log.Println("status nok", resp.StatusCode) 357 w.Write([]byte("no dice")) 358 return 359 } 360 361 // verify response mac 362 signature := resp.Header.Get("X-Signature") 363 signatureBytes, err := hex.DecodeString(signature) 364 if err != nil { 365 return 366 } 367 368 expectedMac := hmac.New(sha256.New, []byte(secret)) 369 expectedMac.Write([]byte("ok")) 370 371 if !hmac.Equal(expectedMac.Sum(nil), signatureBytes) { 372 log.Printf("response body signature mismatch: %x\n", signatureBytes) 373 return 374 } 375 376 tx, err := s.db.BeginTx(r.Context(), nil) 377 if err != nil { 378 log.Println("failed to start tx", err) 379 http.Error(w, err.Error(), http.StatusInternalServerError) 380 return 381 } 382 defer func() { 383 tx.Rollback() 384 err = s.enforcer.E.LoadPolicy() 385 if err != nil { 386 log.Println("failed to rollback policies") 387 } 388 }() 389 390 // mark as registered 391 err = db.Register(tx, domain) 392 if err != nil { 393 log.Println("failed to register domain", err) 394 http.Error(w, err.Error(), http.StatusInternalServerError) 395 return 396 } 397 398 // set permissions for this did as owner 399 reg, err := db.RegistrationByDomain(tx, domain) 400 if err != nil { 401 log.Println("failed to register domain", err) 402 http.Error(w, err.Error(), http.StatusInternalServerError) 403 return 404 } 405 406 // add basic acls for this domain 407 err = s.enforcer.AddDomain(domain) 408 if err != nil { 409 log.Println("failed to setup owner of domain", err) 410 http.Error(w, err.Error(), http.StatusInternalServerError) 411 return 412 } 413 414 // add this did as owner of this domain 415 err = s.enforcer.AddOwner(domain, reg.ByDid) 416 if err != nil { 417 log.Println("failed to setup owner of domain", err) 418 http.Error(w, err.Error(), http.StatusInternalServerError) 419 return 420 } 421 422 err = tx.Commit() 423 if err != nil { 424 log.Println("failed to commit changes", err) 425 http.Error(w, err.Error(), http.StatusInternalServerError) 426 return 427 } 428 429 err = s.enforcer.E.SavePolicy() 430 if err != nil { 431 log.Println("failed to update ACLs", err) 432 http.Error(w, err.Error(), http.StatusInternalServerError) 433 return 434 } 435 436 w.Write([]byte("check success")) 437} 438 439func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { 440 domain := chi.URLParam(r, "domain") 441 if domain == "" { 442 http.Error(w, "malformed url", http.StatusBadRequest) 443 return 444 } 445 446 user := s.auth.GetUser(r) 447 reg, err := db.RegistrationByDomain(s.db, domain) 448 if err != nil { 449 w.Write([]byte("failed to pull up registration info")) 450 return 451 } 452 453 var members []string 454 if reg.Registered != nil { 455 members, err = s.enforcer.GetUserByRole("server:member", domain) 456 if err != nil { 457 w.Write([]byte("failed to fetch member list")) 458 return 459 } 460 } 461 462 var didsToResolve []string 463 for _, m := range members { 464 didsToResolve = append(didsToResolve, m) 465 } 466 didsToResolve = append(didsToResolve, reg.ByDid) 467 resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 468 didHandleMap := make(map[string]string) 469 for _, identity := range resolvedIds { 470 if !identity.Handle.IsInvalidHandle() { 471 didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 472 } else { 473 didHandleMap[identity.DID.String()] = identity.DID.String() 474 } 475 } 476 477 ok, err := s.enforcer.IsServerOwner(user.Did, domain) 478 isOwner := err == nil && ok 479 480 p := pages.KnotParams{ 481 LoggedInUser: user, 482 DidHandleMap: didHandleMap, 483 Registration: reg, 484 Members: members, 485 IsOwner: isOwner, 486 } 487 488 s.pages.Knot(w, p) 489} 490 491// get knots registered by this user 492func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 493 // for now, this is just pubkeys 494 user := s.auth.GetUser(r) 495 registrations, err := db.RegistrationsByDid(s.db, user.Did) 496 if err != nil { 497 log.Println(err) 498 } 499 500 s.pages.Knots(w, pages.KnotsParams{ 501 LoggedInUser: user, 502 Registrations: registrations, 503 }) 504} 505 506// list members of domain, requires auth and requires owner status 507func (s *State) ListMembers(w http.ResponseWriter, r *http.Request) { 508 domain := chi.URLParam(r, "domain") 509 if domain == "" { 510 http.Error(w, "malformed url", http.StatusBadRequest) 511 return 512 } 513 514 // list all members for this domain 515 memberDids, err := s.enforcer.GetUserByRole("server:member", domain) 516 if err != nil { 517 w.Write([]byte("failed to fetch member list")) 518 return 519 } 520 521 w.Write([]byte(strings.Join(memberDids, "\n"))) 522 return 523} 524 525// add member to domain, requires auth and requires invite access 526func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { 527 domain := chi.URLParam(r, "domain") 528 if domain == "" { 529 http.Error(w, "malformed url", http.StatusBadRequest) 530 return 531 } 532 533 subjectIdentifier := r.FormValue("subject") 534 if subjectIdentifier == "" { 535 http.Error(w, "malformed form", http.StatusBadRequest) 536 return 537 } 538 539 subjectIdentity, err := s.resolver.ResolveIdent(r.Context(), subjectIdentifier) 540 if err != nil { 541 w.Write([]byte("failed to resolve member did to a handle")) 542 return 543 } 544 log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) 545 546 // announce this relation into the firehose, store into owners' pds 547 client, _ := s.auth.AuthorizedClient(r) 548 currentUser := s.auth.GetUser(r) 549 createdAt := time.Now().Format(time.RFC3339) 550 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 551 Collection: tangled.KnotMemberNSID, 552 Repo: currentUser.Did, 553 Rkey: appview.TID(), 554 Record: &lexutil.LexiconTypeDecoder{ 555 Val: &tangled.KnotMember{ 556 Subject: subjectIdentity.DID.String(), 557 Domain: domain, 558 CreatedAt: createdAt, 559 }}, 560 }) 561 562 // invalid record 563 if err != nil { 564 log.Printf("failed to create record: %s", err) 565 return 566 } 567 log.Println("created atproto record: ", resp.Uri) 568 569 secret, err := db.GetRegistrationKey(s.db, domain) 570 if err != nil { 571 log.Printf("no key found for domain %s: %s\n", domain, err) 572 return 573 } 574 575 ksClient, err := NewSignedClient(domain, secret, s.config.Dev) 576 if err != nil { 577 log.Println("failed to create client to ", domain) 578 return 579 } 580 581 ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) 582 if err != nil { 583 log.Printf("failed to make request to %s: %s", domain, err) 584 return 585 } 586 587 if ksResp.StatusCode != http.StatusNoContent { 588 w.Write([]byte(fmt.Sprint("knotserver failed to add member: ", err))) 589 return 590 } 591 592 err = s.enforcer.AddMember(domain, subjectIdentity.DID.String()) 593 if err != nil { 594 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 595 return 596 } 597 598 w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String()))) 599} 600 601func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 602} 603 604func validateRepoName(name string) error { 605 // check for path traversal attempts 606 if name == "." || name == ".." || 607 strings.Contains(name, "/") || strings.Contains(name, "\\") { 608 return fmt.Errorf("Repository name contains invalid path characters") 609 } 610 611 // check for sequences that could be used for traversal when normalized 612 if strings.Contains(name, "./") || strings.Contains(name, "../") || 613 strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 614 return fmt.Errorf("Repository name contains invalid path sequence") 615 } 616 617 // then continue with character validation 618 for _, char := range name { 619 if !((char >= 'a' && char <= 'z') || 620 (char >= 'A' && char <= 'Z') || 621 (char >= '0' && char <= '9') || 622 char == '-' || char == '_' || char == '.') { 623 return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 624 } 625 } 626 627 // additional check to prevent multiple sequential dots 628 if strings.Contains(name, "..") { 629 return fmt.Errorf("Repository name cannot contain sequential dots") 630 } 631 632 // if all checks pass 633 return nil 634} 635 636func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 637 ctx, span := s.t.TraceStart(r.Context(), "NewRepo") 638 defer span.End() 639 640 switch r.Method { 641 case http.MethodGet: 642 user := s.auth.GetUser(r) 643 span.SetAttributes(attribute.String("user.did", user.Did)) 644 span.SetAttributes(attribute.String("request.method", "GET")) 645 646 knots, err := s.enforcer.GetDomainsForUser(user.Did) 647 if err != nil { 648 span.RecordError(err) 649 s.pages.Notice(w, "repo", "Invalid user account.") 650 return 651 } 652 span.SetAttributes(attribute.Int("knots.count", len(knots))) 653 654 s.pages.NewRepo(w, pages.NewRepoParams{ 655 LoggedInUser: user, 656 Knots: knots, 657 }) 658 659 case http.MethodPost: 660 user := s.auth.GetUser(r) 661 span.SetAttributes(attribute.String("user.did", user.Did)) 662 span.SetAttributes(attribute.String("request.method", "POST")) 663 664 domain := r.FormValue("domain") 665 if domain == "" { 666 s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 667 return 668 } 669 span.SetAttributes(attribute.String("domain", domain)) 670 671 repoName := r.FormValue("name") 672 if repoName == "" { 673 s.pages.Notice(w, "repo", "Repository name cannot be empty.") 674 return 675 } 676 span.SetAttributes(attribute.String("repo.name", repoName)) 677 678 if err := validateRepoName(repoName); err != nil { 679 s.pages.Notice(w, "repo", err.Error()) 680 return 681 } 682 683 defaultBranch := r.FormValue("branch") 684 if defaultBranch == "" { 685 defaultBranch = "main" 686 } 687 span.SetAttributes(attribute.String("repo.default_branch", defaultBranch)) 688 689 description := r.FormValue("description") 690 691 ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 692 if err != nil || !ok { 693 if err != nil { 694 span.RecordError(err) 695 } 696 span.SetAttributes(attribute.Bool("permission.granted", false)) 697 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 698 return 699 } 700 span.SetAttributes(attribute.Bool("permission.granted", true)) 701 702 existingRepo, err := db.GetRepo(ctx, s.db, user.Did, repoName) 703 if err == nil && existingRepo != nil { 704 span.SetAttributes(attribute.Bool("repo.exists", true)) 705 s.pages.Notice(w, "repo", fmt.Sprintf("A repo by this name already exists on %s", existingRepo.Knot)) 706 return 707 } 708 span.SetAttributes(attribute.Bool("repo.exists", false)) 709 710 secret, err := db.GetRegistrationKey(s.db, domain) 711 if err != nil { 712 span.RecordError(err) 713 s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", domain)) 714 return 715 } 716 717 client, err := NewSignedClient(domain, secret, s.config.Dev) 718 if err != nil { 719 span.RecordError(err) 720 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 721 return 722 } 723 724 rkey := appview.TID() 725 repo := &db.Repo{ 726 Did: user.Did, 727 Name: repoName, 728 Knot: domain, 729 Rkey: rkey, 730 Description: description, 731 } 732 733 rWithCtx := r.WithContext(ctx) 734 xrpcClient, _ := s.auth.AuthorizedClient(rWithCtx) 735 736 createdAt := time.Now().Format(time.RFC3339) 737 atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{ 738 Collection: tangled.RepoNSID, 739 Repo: user.Did, 740 Rkey: rkey, 741 Record: &lexutil.LexiconTypeDecoder{ 742 Val: &tangled.Repo{ 743 Knot: repo.Knot, 744 Name: repoName, 745 CreatedAt: createdAt, 746 Owner: user.Did, 747 }}, 748 }) 749 if err != nil { 750 span.RecordError(err) 751 log.Printf("failed to create record: %s", err) 752 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 753 return 754 } 755 log.Println("created repo record: ", atresp.Uri) 756 span.SetAttributes(attribute.String("repo.uri", atresp.Uri)) 757 758 tx, err := s.db.BeginTx(ctx, nil) 759 if err != nil { 760 span.RecordError(err) 761 log.Println(err) 762 s.pages.Notice(w, "repo", "Failed to save repository information.") 763 return 764 } 765 defer func() { 766 tx.Rollback() 767 err = s.enforcer.E.LoadPolicy() 768 if err != nil { 769 log.Println("failed to rollback policies") 770 } 771 }() 772 773 resp, err := client.NewRepo(user.Did, repoName, defaultBranch) 774 if err != nil { 775 span.RecordError(err) 776 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 777 return 778 } 779 span.SetAttributes(attribute.Int("knot_response.status", resp.StatusCode)) 780 781 switch resp.StatusCode { 782 case http.StatusConflict: 783 s.pages.Notice(w, "repo", "A repository with that name already exists.") 784 return 785 case http.StatusInternalServerError: 786 s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 787 case http.StatusNoContent: 788 // continue 789 } 790 791 repo.AtUri = atresp.Uri 792 err = db.AddRepo(ctx, tx, repo) 793 if err != nil { 794 span.RecordError(err) 795 log.Println(err) 796 s.pages.Notice(w, "repo", "Failed to save repository information.") 797 return 798 } 799 800 // acls 801 p, _ := securejoin.SecureJoin(user.Did, repoName) 802 err = s.enforcer.AddRepo(user.Did, domain, p) 803 if err != nil { 804 span.RecordError(err) 805 log.Println(err) 806 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 807 return 808 } 809 810 err = tx.Commit() 811 if err != nil { 812 span.RecordError(err) 813 log.Println("failed to commit changes", err) 814 http.Error(w, err.Error(), http.StatusInternalServerError) 815 return 816 } 817 818 err = s.enforcer.E.SavePolicy() 819 if err != nil { 820 span.RecordError(err) 821 log.Println("failed to update ACLs", err) 822 http.Error(w, err.Error(), http.StatusInternalServerError) 823 return 824 } 825 826 s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 827 return 828 } 829}