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