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