forked from tangled.org/core
Monorepo for Tangled — https://tangled.org
1package repo 2 3import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "slices" 8 "strings" 9 "time" 10 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/appview/db" 13 "tangled.org/core/appview/oauth" 14 "tangled.org/core/appview/pages" 15 xrpcclient "tangled.org/core/appview/xrpcclient" 16 "tangled.org/core/types" 17 18 comatproto "github.com/bluesky-social/indigo/api/atproto" 19 lexutil "github.com/bluesky-social/indigo/lex/util" 20 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 21) 22 23type tab = map[string]any 24 25var ( 26 // would be great to have ordered maps right about now 27 settingsTabs []tab = []tab{ 28 {"Name": "general", "Icon": "sliders-horizontal"}, 29 {"Name": "access", "Icon": "users"}, 30 {"Name": "pipelines", "Icon": "layers-2"}, 31 } 32) 33 34func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 35 l := rp.logger.With("handler", "SetDefaultBranch") 36 37 f, err := rp.repoResolver.Resolve(r) 38 if err != nil { 39 l.Error("failed to get repo and knot", "err", err) 40 return 41 } 42 43 noticeId := "operation-error" 44 branch := r.FormValue("branch") 45 if branch == "" { 46 http.Error(w, "malformed form", http.StatusBadRequest) 47 return 48 } 49 50 client, err := rp.oauth.ServiceClient( 51 r, 52 oauth.WithService(f.Knot), 53 oauth.WithLxm(tangled.RepoSetDefaultBranchNSID), 54 oauth.WithDev(rp.config.Core.Dev), 55 ) 56 if err != nil { 57 l.Error("failed to connect to knot server", "err", err) 58 rp.pages.Notice(w, noticeId, "Failed to connect to knot server.") 59 return 60 } 61 62 xe := tangled.RepoSetDefaultBranch( 63 r.Context(), 64 client, 65 &tangled.RepoSetDefaultBranch_Input{ 66 Repo: f.RepoAt().String(), 67 DefaultBranch: branch, 68 }, 69 ) 70 if err := xrpcclient.HandleXrpcErr(xe); err != nil { 71 l.Error("xrpc failed", "err", xe) 72 rp.pages.Notice(w, noticeId, err.Error()) 73 return 74 } 75 76 rp.pages.HxRefresh(w) 77} 78 79func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 80 user := rp.oauth.GetUser(r) 81 l := rp.logger.With("handler", "Secrets") 82 l = l.With("did", user.Did) 83 84 f, err := rp.repoResolver.Resolve(r) 85 if err != nil { 86 l.Error("failed to get repo and knot", "err", err) 87 return 88 } 89 90 if f.Spindle == "" { 91 l.Error("empty spindle cannot add/rm secret", "err", err) 92 return 93 } 94 95 lxm := tangled.RepoAddSecretNSID 96 if r.Method == http.MethodDelete { 97 lxm = tangled.RepoRemoveSecretNSID 98 } 99 100 spindleClient, err := rp.oauth.ServiceClient( 101 r, 102 oauth.WithService(f.Spindle), 103 oauth.WithLxm(lxm), 104 oauth.WithExp(60), 105 oauth.WithDev(rp.config.Core.Dev), 106 ) 107 if err != nil { 108 l.Error("failed to create spindle client", "err", err) 109 return 110 } 111 112 key := r.FormValue("key") 113 if key == "" { 114 w.WriteHeader(http.StatusBadRequest) 115 return 116 } 117 118 switch r.Method { 119 case http.MethodPut: 120 errorId := "add-secret-error" 121 122 value := r.FormValue("value") 123 if value == "" { 124 w.WriteHeader(http.StatusBadRequest) 125 return 126 } 127 128 err = tangled.RepoAddSecret( 129 r.Context(), 130 spindleClient, 131 &tangled.RepoAddSecret_Input{ 132 Repo: f.RepoAt().String(), 133 Key: key, 134 Value: value, 135 }, 136 ) 137 if err != nil { 138 l.Error("Failed to add secret.", "err", err) 139 rp.pages.Notice(w, errorId, "Failed to add secret.") 140 return 141 } 142 143 case http.MethodDelete: 144 errorId := "operation-error" 145 146 err = tangled.RepoRemoveSecret( 147 r.Context(), 148 spindleClient, 149 &tangled.RepoRemoveSecret_Input{ 150 Repo: f.RepoAt().String(), 151 Key: key, 152 }, 153 ) 154 if err != nil { 155 l.Error("Failed to delete secret.", "err", err) 156 rp.pages.Notice(w, errorId, "Failed to delete secret.") 157 return 158 } 159 } 160 161 rp.pages.HxRefresh(w) 162} 163 164func (rp *Repo) Settings(w http.ResponseWriter, r *http.Request) { 165 tabVal := r.URL.Query().Get("tab") 166 if tabVal == "" { 167 tabVal = "general" 168 } 169 170 switch tabVal { 171 case "general": 172 rp.generalSettings(w, r) 173 174 case "access": 175 rp.accessSettings(w, r) 176 177 case "pipelines": 178 rp.pipelineSettings(w, r) 179 } 180} 181 182func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { 183 l := rp.logger.With("handler", "generalSettings") 184 185 f, err := rp.repoResolver.Resolve(r) 186 user := rp.oauth.GetUser(r) 187 188 scheme := "http" 189 if !rp.config.Core.Dev { 190 scheme = "https" 191 } 192 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 193 xrpcc := &indigoxrpc.Client{ 194 Host: host, 195 } 196 197 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 198 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 199 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 200 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 201 rp.pages.Error503(w) 202 return 203 } 204 205 var result types.RepoBranchesResponse 206 if err := json.Unmarshal(xrpcBytes, &result); err != nil { 207 l.Error("failed to decode XRPC response", "err", err) 208 rp.pages.Error503(w) 209 return 210 } 211 212 defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 213 if err != nil { 214 l.Error("failed to fetch labels", "err", err) 215 rp.pages.Error503(w) 216 return 217 } 218 219 labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels)) 220 if err != nil { 221 l.Error("failed to fetch labels", "err", err) 222 rp.pages.Error503(w) 223 return 224 } 225 // remove default labels from the labels list, if present 226 defaultLabelMap := make(map[string]bool) 227 for _, dl := range defaultLabels { 228 defaultLabelMap[dl.AtUri().String()] = true 229 } 230 n := 0 231 for _, l := range labels { 232 if !defaultLabelMap[l.AtUri().String()] { 233 labels[n] = l 234 n++ 235 } 236 } 237 labels = labels[:n] 238 239 subscribedLabels := make(map[string]struct{}) 240 for _, l := range f.Repo.Labels { 241 subscribedLabels[l] = struct{}{} 242 } 243 244 // if there is atleast 1 unsubbed default label, show the "subscribe all" button, 245 // if all default labels are subbed, show the "unsubscribe all" button 246 shouldSubscribeAll := false 247 for _, dl := range defaultLabels { 248 if _, ok := subscribedLabels[dl.AtUri().String()]; !ok { 249 // one of the default labels is not subscribed to 250 shouldSubscribeAll = true 251 break 252 } 253 } 254 255 rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{ 256 LoggedInUser: user, 257 RepoInfo: f.RepoInfo(user), 258 Branches: result.Branches, 259 Labels: labels, 260 DefaultLabels: defaultLabels, 261 SubscribedLabels: subscribedLabels, 262 ShouldSubscribeAll: shouldSubscribeAll, 263 Tabs: settingsTabs, 264 Tab: "general", 265 }) 266} 267 268func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 269 l := rp.logger.With("handler", "accessSettings") 270 271 f, err := rp.repoResolver.Resolve(r) 272 user := rp.oauth.GetUser(r) 273 274 repoCollaborators, err := f.Collaborators(r.Context()) 275 if err != nil { 276 l.Error("failed to get collaborators", "err", err) 277 } 278 279 rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ 280 LoggedInUser: user, 281 RepoInfo: f.RepoInfo(user), 282 Tabs: settingsTabs, 283 Tab: "access", 284 Collaborators: repoCollaborators, 285 }) 286} 287 288func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) { 289 l := rp.logger.With("handler", "pipelineSettings") 290 291 f, err := rp.repoResolver.Resolve(r) 292 user := rp.oauth.GetUser(r) 293 294 // all spindles that the repo owner is a member of 295 spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid()) 296 if err != nil { 297 l.Error("failed to fetch spindles", "err", err) 298 return 299 } 300 301 var secrets []*tangled.RepoListSecrets_Secret 302 if f.Spindle != "" { 303 if spindleClient, err := rp.oauth.ServiceClient( 304 r, 305 oauth.WithService(f.Spindle), 306 oauth.WithLxm(tangled.RepoListSecretsNSID), 307 oauth.WithExp(60), 308 oauth.WithDev(rp.config.Core.Dev), 309 ); err != nil { 310 l.Error("failed to create spindle client", "err", err) 311 } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 312 l.Error("failed to fetch secrets", "err", err) 313 } else { 314 secrets = resp.Secrets 315 } 316 } 317 318 slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int { 319 return strings.Compare(a.Key, b.Key) 320 }) 321 322 var dids []string 323 for _, s := range secrets { 324 dids = append(dids, s.CreatedBy) 325 } 326 resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids) 327 328 // convert to a more manageable form 329 var niceSecret []map[string]any 330 for id, s := range secrets { 331 when, _ := time.Parse(time.RFC3339, s.CreatedAt) 332 niceSecret = append(niceSecret, map[string]any{ 333 "Id": id, 334 "Key": s.Key, 335 "CreatedAt": when, 336 "CreatedBy": resolvedIdents[id].Handle.String(), 337 }) 338 } 339 340 rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{ 341 LoggedInUser: user, 342 RepoInfo: f.RepoInfo(user), 343 Tabs: settingsTabs, 344 Tab: "pipelines", 345 Spindles: spindles, 346 CurrentSpindle: f.Spindle, 347 Secrets: niceSecret, 348 }) 349} 350 351func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) { 352 l := rp.logger.With("handler", "EditBaseSettings") 353 354 noticeId := "repo-base-settings-error" 355 356 f, err := rp.repoResolver.Resolve(r) 357 if err != nil { 358 l.Error("failed to get repo and knot", "err", err) 359 w.WriteHeader(http.StatusBadRequest) 360 return 361 } 362 363 client, err := rp.oauth.AuthorizedClient(r) 364 if err != nil { 365 l.Error("failed to get client") 366 rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.") 367 return 368 } 369 370 var ( 371 description = r.FormValue("description") 372 website = r.FormValue("website") 373 topicStr = r.FormValue("topics") 374 ) 375 376 err = rp.validator.ValidateURI(website) 377 if err != nil { 378 l.Error("invalid uri", "err", err) 379 rp.pages.Notice(w, noticeId, err.Error()) 380 return 381 } 382 383 topics, err := rp.validator.ValidateRepoTopicStr(topicStr) 384 if err != nil { 385 l.Error("invalid topics", "err", err) 386 rp.pages.Notice(w, noticeId, err.Error()) 387 return 388 } 389 l.Debug("got", "topicsStr", topicStr, "topics", topics) 390 391 newRepo := f.Repo 392 newRepo.Description = description 393 newRepo.Website = website 394 newRepo.Topics = topics 395 record := newRepo.AsRecord() 396 397 tx, err := rp.db.BeginTx(r.Context(), nil) 398 if err != nil { 399 l.Error("failed to begin transaction", "err", err) 400 rp.pages.Notice(w, noticeId, "Failed to save repository information.") 401 return 402 } 403 defer tx.Rollback() 404 405 err = db.PutRepo(tx, newRepo) 406 if err != nil { 407 l.Error("failed to update repository", "err", err) 408 rp.pages.Notice(w, noticeId, "Failed to save repository information.") 409 return 410 } 411 412 ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 413 if err != nil { 414 // failed to get record 415 l.Error("failed to get repo record", "err", err) 416 rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.") 417 return 418 } 419 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 420 Collection: tangled.RepoNSID, 421 Repo: newRepo.Did, 422 Rkey: newRepo.Rkey, 423 SwapRecord: ex.Cid, 424 Record: &lexutil.LexiconTypeDecoder{ 425 Val: &record, 426 }, 427 }) 428 429 if err != nil { 430 l.Error("failed to perferom update-repo query", "err", err) 431 // failed to get record 432 rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.") 433 return 434 } 435 436 err = tx.Commit() 437 if err != nil { 438 l.Error("failed to commit", "err", err) 439 } 440 441 rp.pages.HxRefresh(w) 442}