Monorepo for Tangled
at e326cc53ad4e9123552bdf20a192aff6ca1eb135 129 lines 3.8 kB view raw
1package xrpc 2 3import ( 4 "encoding/json" 5 "log/slog" 6 "net/http" 7 "strings" 8 9 securejoin "github.com/cyphar/filepath-securejoin" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/idresolver" 12 "tangled.org/core/jetstream" 13 "tangled.org/core/knotserver/config" 14 "tangled.org/core/knotserver/db" 15 "tangled.org/core/notifier" 16 "tangled.org/core/rbac" 17 xrpcerr "tangled.org/core/xrpc/errors" 18 "tangled.org/core/xrpc/serviceauth" 19 20 "github.com/go-chi/chi/v5" 21) 22 23type Xrpc struct { 24 Config *config.Config 25 Db *db.DB 26 Ingester *jetstream.JetstreamClient 27 Enforcer *rbac.Enforcer 28 Logger *slog.Logger 29 Notifier *notifier.Notifier 30 Resolver *idresolver.Resolver 31 ServiceAuth *serviceauth.ServiceAuth 32} 33 34func (x *Xrpc) Router() http.Handler { 35 r := chi.NewRouter() 36 37 r.Group(func(r chi.Router) { 38 r.Use(x.ServiceAuth.VerifyServiceAuth) 39 40 r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) 41 r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch) 42 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) 43 r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo) 44 r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus) 45 r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync) 46 r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef) 47 r.Post("/"+tangled.RepoMergeNSID, x.Merge) 48 }) 49 50 // merge check is an open endpoint 51 // 52 // TODO: should we constrain this more? 53 // - we can calculate on PR submit/resubmit/gitRefUpdate etc. 54 // - use ETags on clients to keep requests to a minimum 55 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck) 56 57 // repo query endpoints (no auth required) 58 r.Get("/"+tangled.RepoTreeNSID, x.RepoTree) 59 r.Get("/"+tangled.RepoLogNSID, x.RepoLog) 60 r.Get("/"+tangled.RepoBranchesNSID, x.RepoBranches) 61 r.Get("/"+tangled.RepoTagsNSID, x.RepoTags) 62 r.Get("/"+tangled.RepoTagNSID, x.RepoTag) 63 r.Get("/"+tangled.RepoBlobNSID, x.RepoBlob) 64 r.Get("/"+tangled.RepoDiffNSID, x.RepoDiff) 65 r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare) 66 r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch) 67 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch) 68 r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive) 69 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages) 70 71 // knot query endpoints (no auth required) 72 r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys) 73 r.Get("/"+tangled.KnotVersionNSID, x.Version) 74 75 // service query endpoints (no auth required) 76 r.Get("/"+tangled.OwnerNSID, x.Owner) 77 78 return r 79} 80 81// parseRepoParam parses a repo parameter in 'did/repoName' format and returns 82// the full repository path on disk 83func (x *Xrpc) parseRepoParam(repo string) (string, error) { 84 if repo == "" { 85 return "", xrpcerr.NewXrpcError( 86 xrpcerr.WithTag("InvalidRequest"), 87 xrpcerr.WithMessage("missing repo parameter"), 88 ) 89 } 90 91 // Parse repo string (did/repoName format) 92 parts := strings.SplitN(repo, "/", 2) 93 if len(parts) != 2 { 94 return "", xrpcerr.NewXrpcError( 95 xrpcerr.WithTag("InvalidRequest"), 96 xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"), 97 ) 98 } 99 100 did := parts[0] 101 repoName := parts[1] 102 103 // Construct repository path using the same logic as didPath 104 didRepoPath, err := securejoin.SecureJoin(did, repoName) 105 if err != nil { 106 return "", xrpcerr.RepoNotFoundError 107 } 108 109 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 110 if err != nil { 111 return "", xrpcerr.RepoNotFoundError 112 } 113 114 return repoPath, nil 115} 116 117func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 118 w.Header().Set("Content-Type", "application/json") 119 w.WriteHeader(status) 120 json.NewEncoder(w).Encode(e) 121} 122 123func writeJson(w http.ResponseWriter, response any) { 124 w.Header().Set("Content-Type", "application/json") 125 if err := json.NewEncoder(w).Encode(response); err != nil { 126 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 127 return 128 } 129}