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