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