forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+521 -987
api
tangled
appview
config
db
knots
oauth
handler
pages
templates
knots
layouts
repo
settings
spindles
pulls
repo
state
xrpcclient
docs
knotclient
knotserver
lexicons
nix
modules
rbac
xrpc
errors
+2
api/tangled/repodelete.go
··· 20 20 Did string `json:"did" cborgen:"did"` 21 21 // name: Name of the repository to delete 22 22 Name string `json:"name" cborgen:"name"` 23 + // rkey: Rkey of the repository record 24 + Rkey string `json:"rkey" cborgen:"rkey"` 23 25 } 24 26 25 27 // RepoDelete calls the XRPC method "sh.tangled.repo.delete".
+1 -1
appview/config/config.go
··· 17 17 Dev bool `env:"DEV, default=false"` 18 18 DisallowedNicknamesFile string `env:"DISALLOWED_NICKNAMES_FILE"` 19 19 20 - // temporarily, to add users to default spindle 20 + // temporarily, to add users to default knot and spindle 21 21 AppPassword string `env:"APP_PASSWORD"` 22 22 } 23 23
+67 -133
appview/db/registration.go
··· 1 1 package db 2 2 3 3 import ( 4 - "crypto/rand" 5 4 "database/sql" 6 - "encoding/hex" 7 5 "fmt" 8 - "log" 9 6 "strings" 10 7 "time" 11 8 ) ··· 18 15 ByDid string 19 16 Created *time.Time 20 17 Registered *time.Time 18 + ReadOnly bool 21 19 } 22 20 23 21 func (r *Registration) Status() Status { 24 - if r.Registered != nil { 22 + if r.ReadOnly { 23 + return ReadOnly 24 + } else if r.Registered != nil { 25 25 return Registered 26 26 } else { 27 27 return Pending 28 28 } 29 29 } 30 30 31 + func (r *Registration) IsRegistered() bool { 32 + return r.Status() == Registered 33 + } 34 + 35 + func (r *Registration) IsReadOnly() bool { 36 + return r.Status() == ReadOnly 37 + } 38 + 39 + func (r *Registration) IsPending() bool { 40 + return r.Status() == Pending 41 + } 42 + 31 43 type Status uint32 32 44 33 45 const ( 34 46 Registered Status = iota 35 47 Pending 48 + ReadOnly 36 49 ) 37 50 38 - // returns registered status, did of owner, error 39 - func RegistrationsByDid(e Execer, did string) ([]Registration, error) { 51 + func GetRegistrations(e Execer, filters ...filter) ([]Registration, error) { 40 52 var registrations []Registration 41 53 42 - rows, err := e.Query(` 43 - select id, domain, did, created, registered from registrations 44 - where did = ? 45 - `, did) 46 - if err != nil { 47 - return nil, err 54 + var conditions []string 55 + var args []any 56 + for _, filter := range filters { 57 + conditions = append(conditions, filter.Condition()) 58 + args = append(args, filter.Arg()...) 48 59 } 49 60 50 - for rows.Next() { 51 - var createdAt *string 52 - var registeredAt *string 53 - var registration Registration 54 - err = rows.Scan(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 55 - 56 - if err != nil { 57 - log.Println(err) 58 - } else { 59 - createdAtTime, _ := time.Parse(time.RFC3339, *createdAt) 60 - var registeredAtTime *time.Time 61 - if registeredAt != nil { 62 - x, _ := time.Parse(time.RFC3339, *registeredAt) 63 - registeredAtTime = &x 64 - } 65 - 66 - registration.Created = &createdAtTime 67 - registration.Registered = registeredAtTime 68 - registrations = append(registrations, registration) 69 - } 61 + whereClause := "" 62 + if conditions != nil { 63 + whereClause = " where " + strings.Join(conditions, " and ") 70 64 } 71 65 72 - return registrations, nil 73 - } 74 - 75 - // returns registered status, did of owner, error 76 - func RegistrationByDomain(e Execer, domain string) (*Registration, error) { 77 - var createdAt *string 78 - var registeredAt *string 79 - var registration Registration 80 - 81 - err := e.QueryRow(` 82 - select id, domain, did, created, registered from registrations 83 - where domain = ? 84 - `, domain).Scan(&registration.Id, &registration.Domain, &registration.ByDid, &createdAt, &registeredAt) 66 + query := fmt.Sprintf(` 67 + select id, domain, did, created, registered, read_only 68 + from registrations 69 + %s 70 + order by created 71 + `, 72 + whereClause, 73 + ) 85 74 75 + rows, err := e.Query(query, args...) 86 76 if err != nil { 87 - if err == sql.ErrNoRows { 88 - return nil, nil 89 - } else { 90 - return nil, err 91 - } 77 + return nil, err 92 78 } 93 79 94 - createdAtTime, _ := time.Parse(time.RFC3339, *createdAt) 95 - var registeredAtTime *time.Time 96 - if registeredAt != nil { 97 - x, _ := time.Parse(time.RFC3339, *registeredAt) 98 - registeredAtTime = &x 99 - } 100 - 101 - registration.Created = &createdAtTime 102 - registration.Registered = registeredAtTime 103 - 104 - return &registration, nil 105 - } 106 - 107 - func genSecret() string { 108 - key := make([]byte, 32) 109 - rand.Read(key) 110 - return hex.EncodeToString(key) 111 - } 80 + for rows.Next() { 81 + var createdAt string 82 + var registeredAt sql.Null[string] 83 + var readOnly int 84 + var reg Registration 112 85 113 - func GenerateRegistrationKey(e Execer, domain, did string) (string, error) { 114 - // sanity check: does this domain already have a registration? 115 - reg, err := RegistrationByDomain(e, domain) 116 - if err != nil { 117 - return "", err 118 - } 119 - 120 - // registration is open 121 - if reg != nil { 122 - switch reg.Status() { 123 - case Registered: 124 - // already registered by `owner` 125 - return "", fmt.Errorf("%s already registered by %s", domain, reg.ByDid) 126 - case Pending: 127 - // TODO: be loud about this 128 - log.Printf("%s registered by %s, status pending", domain, reg.ByDid) 86 + err = rows.Scan(&reg.Id, &reg.Domain, &reg.ByDid, &createdAt, &registeredAt, &readOnly) 87 + if err != nil { 88 + return nil, err 129 89 } 130 - } 131 90 132 - secret := genSecret() 91 + if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 92 + reg.Created = &t 93 + } 133 94 134 - _, err = e.Exec(` 135 - insert into registrations (domain, did, secret) 136 - values (?, ?, ?) 137 - on conflict(domain) do update set did = excluded.did, secret = excluded.secret, created = excluded.created 138 - `, domain, did, secret) 139 - 140 - if err != nil { 141 - return "", err 142 - } 143 - 144 - return secret, nil 145 - } 95 + if registeredAt.Valid { 96 + if t, err := time.Parse(time.RFC3339, registeredAt.V); err == nil { 97 + reg.Registered = &t 98 + } 99 + } 146 100 147 - func GetRegistrationKey(e Execer, domain string) (string, error) { 148 - res := e.QueryRow(`select secret from registrations where domain = ?`, domain) 101 + if readOnly != 0 { 102 + reg.ReadOnly = true 103 + } 149 104 150 - var secret string 151 - err := res.Scan(&secret) 152 - if err != nil || secret == "" { 153 - return "", err 105 + registrations = append(registrations, reg) 154 106 } 155 107 156 - return secret, nil 108 + return registrations, nil 157 109 } 158 110 159 - func GetCompletedRegistrations(e Execer) ([]string, error) { 160 - rows, err := e.Query(`select domain from registrations where registered not null`) 161 - if err != nil { 162 - return nil, err 163 - } 164 - 165 - var domains []string 166 - for rows.Next() { 167 - var domain string 168 - err = rows.Scan(&domain) 169 - 170 - if err != nil { 171 - log.Println(err) 172 - } else { 173 - domains = append(domains, domain) 174 - } 111 + func MarkRegistered(e Execer, filters ...filter) error { 112 + var conditions []string 113 + var args []any 114 + for _, filter := range filters { 115 + conditions = append(conditions, filter.Condition()) 116 + args = append(args, filter.Arg()...) 175 117 } 176 118 177 - if err = rows.Err(); err != nil { 178 - return nil, err 119 + query := "update registrations set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), read_only = 0" 120 + if len(conditions) > 0 { 121 + query += " where " + strings.Join(conditions, " and ") 179 122 } 180 123 181 - return domains, nil 182 - } 183 - 184 - func Register(e Execer, domain string) error { 185 - _, err := e.Exec(` 186 - update registrations 187 - set registered = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') 188 - where domain = ?; 189 - `, domain) 190 - 124 + _, err := e.Exec(query, args...) 191 125 return err 192 126 } 193 127
+40 -16
appview/knots/knots.go
··· 3 3 import ( 4 4 "errors" 5 5 "fmt" 6 + "log" 6 7 "log/slog" 7 8 "net/http" 8 9 "slices" ··· 49 50 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/add", k.addMember) 50 51 r.With(middleware.AuthMiddleware(k.OAuth)).Post("/{domain}/remove", k.removeMember) 51 52 53 + r.With(middleware.AuthMiddleware(k.OAuth)).Get("/upgradeBanner", k.banner) 54 + 52 55 return r 53 56 } 54 57 55 58 func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 56 59 user := k.OAuth.GetUser(r) 57 - registrations, err := db.RegistrationsByDid(k.Db, user.Did) 60 + registrations, err := db.GetRegistrations( 61 + k.Db, 62 + db.FilterEq("did", user.Did), 63 + ) 58 64 if err != nil { 59 65 k.Logger.Error("failed to fetch knot registrations", "err", err) 60 66 w.WriteHeader(http.StatusInternalServerError) ··· 89 95 http.Error(w, "Not found", http.StatusNotFound) 90 96 return 91 97 } 92 - 93 - // Find the specific registration for this domain 94 - var registration *db.Registration 95 - for _, reg := range registrations { 96 - if reg.Domain == domain && reg.ByDid == user.Did && reg.Registered != nil { 97 - registration = &reg 98 - break 99 - } 100 - } 101 - 102 - if registration == nil { 103 - l.Error("registration not found or not verified") 104 - http.Error(w, "Not found", http.StatusNotFound) 98 + if len(registrations) != 1 { 99 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 105 100 return 106 101 } 107 102 registration := registrations[0] ··· 518 513 db.FilterIsNot("registered", "null"), 519 514 ) 520 515 if err != nil { 521 - l.Error("failed to retrieve domain registration", "err", err) 522 - http.Error(w, "Not found", http.StatusNotFound) 516 + l.Error("failed to get registration", "err", err) 523 517 return 524 518 } 519 + if len(registrations) != 1 { 520 + l.Error("got incorret number of registrations", "got", len(registrations), "expected", 1) 521 + return 522 + } 523 + registration := registrations[0] 525 524 526 525 noticeId := fmt.Sprintf("add-member-error-%d", registration.Id) 527 526 defaultErr := "Failed to add member. Try again later." ··· 679 678 // ok 680 679 k.Pages.HxRefresh(w) 681 680 } 681 + 682 + func (k *Knots) banner(w http.ResponseWriter, r *http.Request) { 683 + user := k.OAuth.GetUser(r) 684 + l := k.Logger.With("handler", "removeMember") 685 + l = l.With("did", user.Did) 686 + l = l.With("handle", user.Handle) 687 + 688 + registrations, err := db.GetRegistrations( 689 + k.Db, 690 + db.FilterEq("did", user.Did), 691 + db.FilterEq("read_only", 1), 692 + ) 693 + if err != nil { 694 + l.Error("non-fatal: failed to get registrations") 695 + return 696 + } 697 + 698 + if registrations == nil { 699 + return 700 + } 701 + 702 + k.Pages.KnotBanner(w, pages.KnotBannerParams{ 703 + Registrations: registrations, 704 + }) 705 + }
+98 -82
appview/oauth/handler/handler.go
··· 8 8 "log" 9 9 "net/http" 10 10 "net/url" 11 + "slices" 11 12 "strings" 12 13 "time" 13 14 ··· 25 26 "tangled.sh/tangled.sh/core/appview/oauth/client" 26 27 "tangled.sh/tangled.sh/core/appview/pages" 27 28 "tangled.sh/tangled.sh/core/idresolver" 28 - "tangled.sh/tangled.sh/core/knotclient" 29 29 "tangled.sh/tangled.sh/core/rbac" 30 30 "tangled.sh/tangled.sh/core/tid" 31 31 ) ··· 353 353 return pubKey, nil 354 354 } 355 355 356 + var ( 357 + tangledHandle = "tangled.sh" 358 + tangledDid = "did:plc:wshs7t2adsemcrrd4snkeqli" 359 + defaultSpindle = "spindle.tangled.sh" 360 + defaultKnot = "knot1.tangled.sh" 361 + ) 362 + 356 363 func (o *OAuthHandler) addToDefaultSpindle(did string) { 357 364 // use the tangled.sh app password to get an accessJwt 358 365 // and create an sh.tangled.spindle.member record with that 359 - 360 - defaultSpindle := "spindle.tangled.sh" 361 - appPassword := o.config.Core.AppPassword 362 - 363 366 spindleMembers, err := db.GetSpindleMembers( 364 367 o.db, 365 368 db.FilterEq("instance", "spindle.tangled.sh"), ··· 375 378 return 376 379 } 377 380 378 - // TODO: hardcoded tangled handle and did for now 379 - tangledHandle := "tangled.sh" 380 - tangledDid := "did:plc:wshs7t2adsemcrrd4snkeqli" 381 + log.Printf("adding %s to default spindle", did) 382 + session, err := o.createAppPasswordSession() 383 + if err != nil { 384 + log.Printf("failed to create session: %s", err) 385 + return 386 + } 387 + 388 + record := tangled.SpindleMember{ 389 + LexiconTypeID: "sh.tangled.spindle.member", 390 + Subject: did, 391 + Instance: defaultSpindle, 392 + CreatedAt: time.Now().Format(time.RFC3339), 393 + } 394 + 395 + if err := session.putRecord(record); err != nil { 396 + log.Printf("failed to add member to default knot: %s", err) 397 + return 398 + } 399 + 400 + log.Printf("successfully added %s to default spindle", did) 401 + } 402 + 403 + func (o *OAuthHandler) addToDefaultKnot(did string) { 404 + // use the tangled.sh app password to get an accessJwt 405 + // and create an sh.tangled.spindle.member record with that 406 + 407 + allKnots, err := o.enforcer.GetKnotsForUser(did) 408 + if err != nil { 409 + log.Printf("failed to get knot members for did %s: %v", did, err) 410 + return 411 + } 412 + 413 + if slices.Contains(allKnots, defaultKnot) { 414 + log.Printf("did %s is already a member of the default knot", did) 415 + return 416 + } 381 417 382 - if appPassword == "" { 383 - log.Println("no app password configured, skipping spindle member addition") 418 + log.Printf("adding %s to default knot", did) 419 + session, err := o.createAppPasswordSession() 420 + if err != nil { 421 + log.Printf("failed to create session: %s", err) 384 422 return 385 423 } 386 424 387 - log.Printf("adding %s to default spindle", did) 425 + record := tangled.KnotMember{ 426 + LexiconTypeID: "sh.tangled.knot.member", 427 + Subject: did, 428 + Domain: defaultKnot, 429 + CreatedAt: time.Now().Format(time.RFC3339), 430 + } 431 + 432 + if err := session.putRecord(record); err != nil { 433 + log.Printf("failed to add member to default knot: %s", err) 434 + return 435 + } 436 + 437 + log.Printf("successfully added %s to default Knot", did) 438 + } 439 + 440 + // create a session using apppasswords 441 + type session struct { 442 + AccessJwt string `json:"accessJwt"` 443 + PdsEndpoint string 444 + } 445 + 446 + func (o *OAuthHandler) createAppPasswordSession() (*session, error) { 447 + appPassword := o.config.Core.AppPassword 448 + if appPassword == "" { 449 + return nil, fmt.Errorf("no app password configured, skipping member addition") 450 + } 388 451 389 452 resolved, err := o.idResolver.ResolveIdent(context.Background(), tangledDid) 390 453 if err != nil { 391 - log.Printf("failed to resolve tangled.sh DID %s: %v", tangledDid, err) 392 - return 454 + return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", tangledDid, err) 393 455 } 394 456 395 457 pdsEndpoint := resolved.PDSEndpoint() 396 458 if pdsEndpoint == "" { 397 - log.Printf("no PDS endpoint found for tangled.sh DID %s", tangledDid) 398 - return 459 + return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", tangledDid) 399 460 } 400 461 401 462 sessionPayload := map[string]string{ ··· 404 465 } 405 466 sessionBytes, err := json.Marshal(sessionPayload) 406 467 if err != nil { 407 - log.Printf("failed to marshal session payload: %v", err) 408 - return 468 + return nil, fmt.Errorf("failed to marshal session payload: %v", err) 409 469 } 410 470 411 471 sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" 412 472 sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) 413 473 if err != nil { 414 - log.Printf("failed to create session request: %v", err) 415 - return 474 + return nil, fmt.Errorf("failed to create session request: %v", err) 416 475 } 417 476 sessionReq.Header.Set("Content-Type", "application/json") 418 477 419 478 client := &http.Client{Timeout: 30 * time.Second} 420 479 sessionResp, err := client.Do(sessionReq) 421 480 if err != nil { 422 - log.Printf("failed to create session: %v", err) 423 - return 481 + return nil, fmt.Errorf("failed to create session: %v", err) 424 482 } 425 483 defer sessionResp.Body.Close() 426 484 427 485 if sessionResp.StatusCode != http.StatusOK { 428 - log.Printf("failed to create session: HTTP %d", sessionResp.StatusCode) 429 - return 486 + return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode) 430 487 } 431 488 432 - var session struct { 433 - AccessJwt string `json:"accessJwt"` 434 - } 489 + var session session 435 490 if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { 436 - log.Printf("failed to decode session response: %v", err) 437 - return 491 + return nil, fmt.Errorf("failed to decode session response: %v", err) 438 492 } 439 493 440 - record := tangled.SpindleMember{ 441 - LexiconTypeID: "sh.tangled.spindle.member", 442 - Subject: did, 443 - Instance: defaultSpindle, 444 - CreatedAt: time.Now().Format(time.RFC3339), 445 - } 494 + session.PdsEndpoint = pdsEndpoint 495 + 496 + return &session, nil 497 + } 446 498 499 + func (s *session) putRecord(record any) error { 447 500 recordBytes, err := json.Marshal(record) 448 501 if err != nil { 449 - log.Printf("failed to marshal spindle member record: %v", err) 450 - return 502 + return fmt.Errorf("failed to marshal knot member record: %w", err) 451 503 } 452 504 453 - payload := map[string]interface{}{ 505 + payload := map[string]any{ 454 506 "repo": tangledDid, 455 - "collection": tangled.SpindleMemberNSID, 507 + "collection": tangled.KnotMemberNSID, 456 508 "rkey": tid.TID(), 457 509 "record": json.RawMessage(recordBytes), 458 510 } 459 511 460 512 payloadBytes, err := json.Marshal(payload) 461 513 if err != nil { 462 - log.Printf("failed to marshal request payload: %v", err) 463 - return 514 + return fmt.Errorf("failed to marshal request payload: %w", err) 464 515 } 465 516 466 - url := pdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 517 + url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 467 518 req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) 468 519 if err != nil { 469 - log.Printf("failed to create HTTP request: %v", err) 470 - return 520 + return fmt.Errorf("failed to create HTTP request: %w", err) 471 521 } 472 522 473 523 req.Header.Set("Content-Type", "application/json") 474 - req.Header.Set("Authorization", "Bearer "+session.AccessJwt) 524 + req.Header.Set("Authorization", "Bearer "+s.AccessJwt) 475 525 526 + client := &http.Client{Timeout: 30 * time.Second} 476 527 resp, err := client.Do(req) 477 528 if err != nil { 478 - log.Printf("failed to add user to default spindle: %v", err) 479 - return 529 + return fmt.Errorf("failed to add user to default Knot: %w", err) 480 530 } 481 531 defer resp.Body.Close() 482 532 483 533 if resp.StatusCode != http.StatusOK { 484 - log.Printf("failed to add user to default spindle: HTTP %d", resp.StatusCode) 485 - return 486 - } 487 - 488 - log.Printf("successfully added %s to default spindle", did) 489 - } 490 - 491 - func (o *OAuthHandler) addToDefaultKnot(did string) { 492 - defaultKnot := "knot1.tangled.sh" 493 - 494 - log.Printf("adding %s to default knot", did) 495 - err := o.enforcer.AddKnotMember(defaultKnot, did) 496 - if err != nil { 497 - log.Println("failed to add user to knot1.tangled.sh: ", err) 498 - return 499 - } 500 - err = o.enforcer.E.SavePolicy() 501 - if err != nil { 502 - log.Println("failed to add user to knot1.tangled.sh: ", err) 503 - return 504 - } 505 - 506 - secret, err := db.GetRegistrationKey(o.db, defaultKnot) 507 - if err != nil { 508 - log.Println("failed to get registration key for knot1.tangled.sh") 509 - return 510 - } 511 - signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.config.Core.Dev) 512 - resp, err := signedClient.AddMember(did) 513 - if err != nil { 514 - log.Println("failed to add user to knot1.tangled.sh: ", err) 515 - return 534 + return fmt.Errorf("failed to add user to default Knot: HTTP %d", resp.StatusCode) 516 535 } 517 536 518 - if resp.StatusCode != http.StatusNoContent { 519 - log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 520 - return 521 - } 537 + return nil 522 538 }
+9 -1
appview/pages/pages.go
··· 338 338 return p.execute("user/settings/emails", w, params) 339 339 } 340 340 341 + type KnotBannerParams struct { 342 + Registrations []db.Registration 343 + } 344 + 345 + func (p *Pages) KnotBanner(w io.Writer, params KnotBannerParams) error { 346 + return p.executePlain("knots/fragments/banner", w, params) 347 + } 348 + 341 349 type KnotsParams struct { 342 350 LoggedInUser *oauth.User 343 351 Registrations []db.Registration ··· 360 368 } 361 369 362 370 type KnotListingParams struct { 363 - db.Registration 371 + *db.Registration 364 372 } 365 373 366 374 func (p *Pages) KnotListing(w io.Writer, params KnotListingParams) error {
+9
appview/pages/templates/knots/fragments/banner.html
··· 1 + {{ define "knots/fragments/banner" }} 2 + <div class="w-full px-6 py-2 -z-15 bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800 rounded-b drop-shadow-sm"> 3 + A knot ({{range $i, $r := .Registrations}}{{if ne $i 0}}, {{end}}{{ $r.Domain }}{{ end }}) 4 + that you administer is presently read-only. Consider upgrading this knot to 5 + continue creating repositories on it. 6 + <a href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations">Click to read the upgrade guide</a>. 7 + </div> 8 + {{ end }} 9 +
+11 -2
appview/pages/templates/knots/fragments/knotListing.html
··· 30 30 {{ define "knotRightSide" }} 31 31 <div id="right-side" class="flex gap-2"> 32 32 {{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2 text-sm" }} 33 - {{ if .Registered }} 34 - <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}">{{ i "shield-check" "w-4 h-4" }} verified</span> 33 + {{ if .IsRegistered }} 34 + <span class="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 {{$style}}"> 35 + {{ i "shield-check" "w-4 h-4" }} verified 36 + </span> 35 37 {{ template "knots/fragments/addMemberModal" . }} 38 + {{ block "knotDeleteButton" . }} {{ end }} 39 + {{ else if .IsReadOnly }} 40 + <span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200 {{$style}}"> 41 + {{ i "shield-alert" "w-4 h-4" }} read-only 42 + </span> 43 + {{ block "knotRetryButton" . }} {{ end }} 44 + {{ block "knotDeleteButton" . }} {{ end }} 36 45 {{ else }} 37 46 <span class="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200 {{$style}}"> 38 47 {{ i "shield-off" "w-4 h-4" }} unverified
+1 -1
appview/pages/templates/knots/index.html
··· 77 77 </button> 78 78 </div> 79 79 80 - <div id="registration-error" class="error dark:text-red-400"></div> 80 + <div id="register-error" class="error dark:text-red-400"></div> 81 81 </form> 82 82 83 83 </section>
+7
appview/pages/templates/layouts/topbar.html
··· 21 21 </div> 22 22 </div> 23 23 </nav> 24 + {{ if .LoggedInUser }} 25 + <div id="upgrade-banner" 26 + hx-get="/knots/upgradeBanner" 27 + hx-trigger="load" 28 + hx-swap="innerHTML"> 29 + </div> 30 + {{ end }} 24 31 {{ end }} 25 32 26 33 {{ define "newButton" }}
+3 -1
appview/pages/templates/repo/settings/general.html
··· 8 8 <div class="col-span-1 md:col-span-3 flex flex-col gap-6 p-2"> 9 9 {{ template "branchSettings" . }} 10 10 {{ template "deleteRepo" . }} 11 + <div id="operation-error" class="text-red-500 dark:text-red-400"></div> 11 12 </div> 12 13 </section> 13 14 {{ end }} ··· 22 23 unless you specify a different branch. 23 24 </p> 24 25 </div> 25 - <form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch"> 26 + <form hx-put="/{{ $.RepoInfo.FullName }}/settings/branches/default" hx-swap="none" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch"> 26 27 <select id="branch" name="branch" required class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 27 28 <option value="" disabled selected > 28 29 Choose a default branch ··· 54 55 <button 55 56 class="btn group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex gap-2 items-center" 56 57 type="button" 58 + hx-swap="none" 57 59 hx-delete="/{{ $.RepoInfo.FullName }}/settings/delete" 58 60 hx-confirm="Are you sure you want to delete {{ $.RepoInfo.FullName }}?"> 59 61 {{ i "trash-2" "size-4" }}
+3 -1
appview/pages/templates/spindles/fragments/spindleListing.html
··· 9 9 {{ if .Verified }} 10 10 <a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]"> 11 11 {{ i "hard-drive" "w-4 h-4" }} 12 - {{ .Instance }} 12 + <span class="hover:underline"> 13 + {{ .Instance }} 14 + </span> 13 15 <span class="text-gray-500"> 14 16 {{ template "repo/fragments/shortTimeAgo" .Created }} 15 17 </span>
+19 -43
appview/pulls/pulls.go
··· 19 19 "tangled.sh/tangled.sh/core/appview/pages" 20 20 "tangled.sh/tangled.sh/core/appview/pages/markup" 21 21 "tangled.sh/tangled.sh/core/appview/reporesolver" 22 + "tangled.sh/tangled.sh/core/appview/xrpcclient" 22 23 "tangled.sh/tangled.sh/core/idresolver" 23 24 "tangled.sh/tangled.sh/core/knotclient" 24 25 "tangled.sh/tangled.sh/core/patchutil" ··· 28 29 "github.com/bluekeyes/go-gitdiff/gitdiff" 29 30 comatproto "github.com/bluesky-social/indigo/api/atproto" 30 31 lexutil "github.com/bluesky-social/indigo/lex/util" 32 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 31 33 "github.com/go-chi/chi/v5" 32 34 "github.com/google/uuid" 33 35 ) ··· 218 220 return types.MergeCheckResponse{} 219 221 } 220 222 221 - client, err := s.oauth.ServiceClient( 222 - r, 223 - oauth.WithService(f.Knot), 224 - oauth.WithLxm(tangled.RepoMergeCheckNSID), 225 - oauth.WithDev(s.config.Core.Dev), 226 - ) 227 - if err != nil { 228 - log.Printf("failed to connect to knot server: %v", err) 229 - return types.MergeCheckResponse{ 230 - Error: "failed to check merge status: could not connect to knot server", 231 - } 223 + scheme := "https" 224 + if s.config.Core.Dev { 225 + scheme = "http" 226 + } 227 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 228 + 229 + xrpcc := indigoxrpc.Client{ 230 + Host: host, 232 231 } 233 232 234 233 patch := pull.LatestPatch() ··· 893 892 Repo: fork.RepoAt().String(), 894 893 }, 895 894 ) 896 - if err != nil { 897 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 898 - if parseErr != nil { 899 - log.Printf("failed to create hidden ref: %v", err) 900 - s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 901 - } else { 902 - log.Printf("failed to create hidden ref: %s", xe.Error()) 903 - if xe.Tag == "AccessControl" { 904 - s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 905 - } else { 906 - s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create pull request: %s", xe.Message)) 907 - } 908 - } 895 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 896 + s.pages.Notice(w, "pull", err.Error()) 909 897 return 910 898 } 911 899 ··· 1501 1489 Repo: forkRepo.RepoAt().String(), 1502 1490 }, 1503 1491 ) 1504 - if err != nil || !resp.Success { 1505 - if err != nil { 1506 - log.Printf("failed to update tracking branch: %s", err) 1507 - } else { 1508 - log.Printf("failed to update tracking branch: success=false") 1509 - } 1510 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1492 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1493 + s.pages.Notice(w, "resubmit-error", err.Error()) 1494 + return 1495 + } 1496 + if !resp.Success { 1497 + log.Println("Failed to update tracking ref.", "err", resp.Error) 1498 + s.pages.Notice(w, "resubmit-error", "Failed to update tracking ref.") 1511 1499 return 1512 1500 } 1513 1501 ··· 1932 1920 } 1933 1921 1934 1922 patch := pullsToMerge.CombinedPatch() 1935 - 1936 - client, err := s.oauth.ServiceClient( 1937 - r, 1938 - oauth.WithService(f.Knot), 1939 - oauth.WithLxm(tangled.RepoMergeNSID), 1940 - oauth.WithDev(s.config.Core.Dev), 1941 - ) 1942 - if err != nil { 1943 - log.Printf("failed to connect to knot server: %v", err) 1944 - s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") 1945 - return 1946 - } 1947 1923 1948 1924 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid) 1949 1925 if err != nil {
-157
appview/repo/index.go
··· 101 101 user := rp.oauth.GetUser(r) 102 102 repoInfo := f.RepoInfo(user) 103 103 104 - // secret, err := db.GetRegistrationKey(rp.db, f.Knot) 105 - // if err != nil { 106 - // log.Printf("failed to get registration key for %s: %s", f.Knot, err) 107 - // rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 108 - // } 109 - 110 - // signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 111 - // if err != nil { 112 - // log.Printf("failed to create signed client for %s: %s", f.Knot, err) 113 - // return 114 - // } 115 - 116 - // var forkInfo *types.ForkInfo 117 - // if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 118 - // forkInfo, err = getForkInfo(r, repoInfo, rp, f, result.Ref, user, signedClient) 119 - // if err != nil { 120 - // log.Printf("Failed to fetch fork information: %v", err) 121 - // return 122 - // } 123 - // } 124 - 125 104 // TODO: a bit dirty 126 105 languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "") 127 106 if err != nil { ··· 227 206 228 207 return languageStats, nil 229 208 } 230 - 231 - // func getForkInfo( 232 - // r *http.Request, 233 - // repoInfo repoinfo.RepoInfo, 234 - // rp *Repo, 235 - // f *reporesolver.ResolvedRepo, 236 - // currentRef string, 237 - // user *oauth.User, 238 - // signedClient *knotclient.SignedClient, 239 - // ) (*types.ForkInfo, error) { 240 - // if user == nil { 241 - // return nil, nil 242 - // } 243 - // 244 - // forkInfo := types.ForkInfo{ 245 - // IsFork: repoInfo.Source != nil, 246 - // Status: types.UpToDate, 247 - // } 248 - // 249 - // if !forkInfo.IsFork { 250 - // forkInfo.IsFork = false 251 - // return &forkInfo, nil 252 - // } 253 - // 254 - // us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev) 255 - // if err != nil { 256 - // log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 257 - // return nil, err 258 - // } 259 - // 260 - // result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name) 261 - // if err != nil { 262 - // log.Println("failed to reach knotserver", err) 263 - // return nil, err 264 - // } 265 - // 266 - // if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool { 267 - // return branch.Name == currentRef 268 - // }) { 269 - // forkInfo.Status = types.MissingBranch 270 - // return &forkInfo, nil 271 - // } 272 - // 273 - // <<<<<<< Conflict 1 of 2 274 - // %%%%%%% Changes from base #1 to side #1 275 - // client, err := rp.oauth.ServiceClient( 276 - // r, 277 - // oauth.WithService(f.Knot), 278 - // oauth.WithLxm(tangled.RepoHiddenRefNSID), 279 - // oauth.WithDev(rp.config.Core.Dev), 280 - // ) 281 - // if err != nil { 282 - // log.Printf("failed to connect to knot server: %v", err) 283 - // %%%%%%% Changes from base #2 to side #2 284 - // - newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, currentRef, currentRef) 285 - // + newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref) 286 - // if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent { 287 - // log.Printf("failed to update tracking branch: %s", err) 288 - // +++++++ Contents of side #3 289 - // client, err := rp.oauth.ServiceClient( 290 - // r, 291 - // oauth.WithService(f.Knot), 292 - // oauth.WithLxm(tangled.RepoHiddenRefNSID), 293 - // oauth.WithDev(rp.config.Core.Dev), 294 - // ) 295 - // if err != nil { 296 - // log.Printf("failed to connect to knot server: %v", err) 297 - // >>>>>>> Conflict 1 of 2 ends 298 - // return nil, err 299 - // } 300 - // 301 - // <<<<<<< Conflict 2 of 2 302 - // %%%%%%% Changes from base #1 to side #1 303 - // resp, err := tangled.RepoHiddenRef( 304 - // r.Context(), 305 - // client, 306 - // &tangled.RepoHiddenRef_Input{ 307 - // - ForkRef: f.Ref, 308 - // - RemoteRef: f.Ref, 309 - // + ForkRef: currentRef, 310 - // + RemoteRef: currentRef, 311 - // Repo: f.RepoAt().String(), 312 - // }, 313 - // ) 314 - // if err != nil || !resp.Success { 315 - // if err != nil { 316 - // log.Printf("failed to update tracking branch: %s", err) 317 - // } else { 318 - // log.Printf("failed to update tracking branch: success=false") 319 - // } 320 - // return nil, fmt.Errorf("failed to update tracking branch") 321 - // } 322 - // 323 - // - hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 324 - // + hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef) 325 - // 326 - // %%%%%%% Changes from base #2 to side #2 327 - // - hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef) 328 - // + hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref) 329 - // 330 - // +++++++ Contents of side #3 331 - // resp, err := tangled.RepoHiddenRef( 332 - // r.Context(), 333 - // client, 334 - // &tangled.RepoHiddenRef_Input{ 335 - // ForkRef: currentRef, 336 - // RemoteRef: currentRef, 337 - // Repo: f.RepoAt().String(), 338 - // }, 339 - // ) 340 - // if err != nil || !resp.Success { 341 - // if err != nil { 342 - // log.Printf("failed to update tracking branch: %s", err) 343 - // } else { 344 - // log.Printf("failed to update tracking branch: success=false") 345 - // } 346 - // return nil, fmt.Errorf("failed to update tracking branch") 347 - // } 348 - // 349 - // hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef) 350 - // >>>>>>> Conflict 2 of 2 ends 351 - // var status types.AncestorCheckResponse 352 - // forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt()), repoInfo.Name, currentRef, hiddenRef) 353 - // if err != nil { 354 - // log.Printf("failed to check if fork is ahead/behind: %s", err) 355 - // return nil, err 356 - // } 357 - // 358 - // if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil { 359 - // log.Printf("failed to decode fork status: %s", err) 360 - // return nil, err 361 - // } 362 - // 363 - // forkInfo.Status = status.Status 364 - // return &forkInfo, nil 365 - // }
+35 -114
appview/repo/repo.go
··· 863 863 fail("Failed to write record to PDS.", err) 864 864 return 865 865 } 866 - l = l.With("at-uri", resp.Uri) 866 + 867 + aturi := resp.Uri 868 + l = l.With("at-uri", aturi) 867 869 l.Info("wrote record to PDS") 868 870 869 - l.Info("adding to knot") 870 - secret, err := db.GetRegistrationKey(rp.db, f.Knot) 871 + tx, err := rp.db.BeginTx(r.Context(), nil) 871 872 if err != nil { 872 - fail("Failed to add to knot.", err) 873 + fail("Failed to add collaborator.", err) 873 874 return 874 875 } 875 876 876 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 877 - if err != nil { 878 - fail("Failed to add to knot.", err) 879 - return 880 - } 877 + rollback := func() { 878 + err1 := tx.Rollback() 879 + err2 := rp.enforcer.E.LoadPolicy() 880 + err3 := rollbackRecord(context.Background(), aturi, client) 881 881 882 - ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String()) 883 - if err != nil { 884 - fail("Knot was unreachable.", err) 885 - return 886 - } 882 + // ignore txn complete errors, this is okay 883 + if errors.Is(err1, sql.ErrTxDone) { 884 + err1 = nil 885 + } 887 886 888 - if ksResp.StatusCode != http.StatusNoContent { 889 - fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil) 890 - return 887 + if errs := errors.Join(err1, err2, err3); errs != nil { 888 + l.Error("failed to rollback changes", "errs", errs) 889 + return 890 + } 891 891 } 892 - 893 - tx, err := rp.db.BeginTx(r.Context(), nil) 894 - if err != nil { 895 - fail("Failed to add collaborator.", err) 896 - return 897 - } 898 - defer func() { 899 - tx.Rollback() 900 - err = rp.enforcer.E.LoadPolicy() 901 - if err != nil { 902 - fail("Failed to add collaborator.", err) 903 - } 904 - }() 892 + defer rollback() 905 893 906 894 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 907 895 if err != nil { ··· 933 921 return 934 922 } 935 923 924 + // clear aturi to when everything is successful 925 + aturi = "" 926 + 936 927 rp.pages.HxRefresh(w) 937 928 } 938 929 939 930 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 940 931 user := rp.oauth.GetUser(r) 941 932 933 + noticeId := "operation-error" 942 934 f, err := rp.repoResolver.Resolve(r) 943 935 if err != nil { 944 936 log.Println("failed to get repo and knot", err) ··· 958 950 }) 959 951 if err != nil { 960 952 log.Printf("failed to delete record: %s", err) 961 - rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 953 + rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 962 954 return 963 955 } 964 956 log.Println("removed repo record ", f.RepoAt().String()) ··· 980 972 &tangled.RepoDelete_Input{ 981 973 Did: f.OwnerDid(), 982 974 Name: f.Name, 975 + Rkey: f.Rkey, 983 976 }, 984 977 ) 985 - if err != nil { 986 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 987 - if parseErr != nil { 988 - log.Printf("failed to delete repo from knot %s: %s", f.Knot, err) 989 - } else { 990 - log.Printf("failed to delete repo from knot %s: %s", f.Knot, xe.Error()) 991 - } 992 - // Continue anyway since we want to clean up local state 993 - } else { 994 - log.Println("removed repo from knot ", f.Knot) 978 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 979 + rp.pages.Notice(w, noticeId, err.Error()) 980 + return 995 981 } 982 + log.Println("deleted repo from knot") 996 983 997 984 tx, err := rp.db.BeginTx(r.Context(), nil) 998 985 if err != nil { ··· 1011 998 // remove collaborator RBAC 1012 999 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 1013 1000 if err != nil { 1014 - rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 1001 + rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 1015 1002 return 1016 1003 } 1017 1004 for _, c := range repoCollaborators { ··· 1023 1010 // remove repo RBAC 1024 1011 err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 1025 1012 if err != nil { 1026 - rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 1013 + rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 1027 1014 return 1028 1015 } 1029 1016 1030 1017 // remove repo from db 1031 1018 err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 1032 1019 if err != nil { 1033 - rp.pages.Notice(w, "settings-delete", "Failed to update appview") 1020 + rp.pages.Notice(w, noticeId, "Failed to update appview") 1034 1021 return 1035 1022 } 1036 1023 log.Println("removed repo from db") ··· 1059 1046 return 1060 1047 } 1061 1048 1049 + noticeId := "operation-error" 1062 1050 branch := r.FormValue("branch") 1063 1051 if branch == "" { 1064 1052 http.Error(w, "malformed form", http.StatusBadRequest) ··· 1091 1079 return 1092 1080 } 1093 1081 1094 - w.Write(fmt.Append(nil, "default branch set to: ", branch)) 1082 + rp.pages.HxRefresh(w) 1095 1083 } 1096 1084 1097 1085 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { ··· 1207 1195 case "pipelines": 1208 1196 rp.pipelineSettings(w, r) 1209 1197 } 1210 - 1211 - // user := rp.oauth.GetUser(r) 1212 - // repoCollaborators, err := f.Collaborators(r.Context()) 1213 - // if err != nil { 1214 - // log.Println("failed to get collaborators", err) 1215 - // } 1216 - 1217 - // isCollaboratorInviteAllowed := false 1218 - // if user != nil { 1219 - // ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 1220 - // if err == nil && ok { 1221 - // isCollaboratorInviteAllowed = true 1222 - // } 1223 - // } 1224 - 1225 - // us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1226 - // if err != nil { 1227 - // log.Println("failed to create unsigned client", err) 1228 - // return 1229 - // } 1230 - 1231 - // result, err := us.Branches(f.OwnerDid(), f.Name) 1232 - // if err != nil { 1233 - // log.Println("failed to reach knotserver", err) 1234 - // return 1235 - // } 1236 - 1237 - // // all spindles that this user is a member of 1238 - // spindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 1239 - // if err != nil { 1240 - // log.Println("failed to fetch spindles", err) 1241 - // return 1242 - // } 1243 - 1244 - // var secrets []*tangled.RepoListSecrets_Secret 1245 - // if f.Spindle != "" { 1246 - // if spindleClient, err := rp.oauth.ServiceClient( 1247 - // r, 1248 - // oauth.WithService(f.Spindle), 1249 - // oauth.WithLxm(tangled.RepoListSecretsNSID), 1250 - // oauth.WithDev(rp.config.Core.Dev), 1251 - // ); err != nil { 1252 - // log.Println("failed to create spindle client", err) 1253 - // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 1254 - // log.Println("failed to fetch secrets", err) 1255 - // } else { 1256 - // secrets = resp.Secrets 1257 - // } 1258 - // } 1259 - 1260 - // rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 1261 - // LoggedInUser: user, 1262 - // RepoInfo: f.RepoInfo(user), 1263 - // Collaborators: repoCollaborators, 1264 - // IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 1265 - // Branches: result.Branches, 1266 - // Spindles: spindles, 1267 - // CurrentSpindle: f.Spindle, 1268 - // Secrets: secrets, 1269 - // }) 1270 1198 } 1271 1199 1272 1200 func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { ··· 1413 1341 Branch: ref, 1414 1342 }, 1415 1343 ) 1416 - if err != nil { 1417 - xe, parseErr := xrpcerr.Unmarshal(err.Error()) 1418 - if parseErr != nil { 1419 - log.Printf("failed to sync repository fork: %s", err) 1420 - rp.pages.Notice(w, "repo", "Failed to sync repository fork.") 1421 - } else { 1422 - log.Printf("failed to sync repository fork: %s", xe.Error()) 1423 - rp.pages.Notice(w, "repo", fmt.Sprintf("Failed to sync repository fork: %s", xe.Message)) 1424 - } 1344 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1345 + rp.pages.Notice(w, "repo", err.Error()) 1425 1346 return 1426 1347 } 1427 1348
+5 -2
appview/state/knotstream.go
··· 24 24 ) 25 25 26 26 func Knotstream(ctx context.Context, c *config.Config, d *db.DB, enforcer *rbac.Enforcer, posthog posthog.Client) (*ec.Consumer, error) { 27 - knots, err := db.GetCompletedRegistrations(d) 27 + knots, err := db.GetRegistrations( 28 + d, 29 + db.FilterIsNot("registered", "null"), 30 + ) 28 31 if err != nil { 29 32 return nil, err 30 33 } 31 34 32 35 srcs := make(map[ec.Source]struct{}) 33 36 for _, k := range knots { 34 - s := ec.NewKnotSource(k) 37 + s := ec.NewKnotSource(k.Domain) 35 38 srcs[s] = struct{}{} 36 39 } 37 40
+3 -3
appview/state/state.go
··· 435 435 Rkey: rkey, 436 436 }, 437 437 ) 438 - if err != nil { 439 - l.Error("xrpc request failed", "err", err) 440 - s.pages.Notice(w, "repo", fmt.Sprintf("Failed to create repository on knot server: %s.", err.Error())) 438 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 439 + l.Error("xrpc error", "xe", xe) 440 + s.pages.Notice(w, "repo", err.Error()) 441 441 return 442 442 } 443 443
+25
appview/xrpcclient/xrpc.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "errors" 7 + "fmt" 6 8 "io" 9 + "net/http" 7 10 8 11 "github.com/bluesky-social/indigo/api/atproto" 9 12 "github.com/bluesky-social/indigo/xrpc" 13 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 10 14 oauth "tangled.sh/icyphox.sh/atproto-oauth" 11 15 ) 12 16 ··· 102 106 103 107 return &out, nil 104 108 } 109 + 110 + // produces a more manageable error 111 + func HandleXrpcErr(err error) error { 112 + if err == nil { 113 + return nil 114 + } 115 + 116 + var xrpcerr *indigoxrpc.Error 117 + if ok := errors.As(err, &xrpcerr); !ok { 118 + return fmt.Errorf("Recieved invalid XRPC error response.") 119 + } 120 + 121 + switch xrpcerr.StatusCode { 122 + case http.StatusNotFound: 123 + return fmt.Errorf("XRPC is unsupported on this knot, consider upgrading your knot.") 124 + case http.StatusUnauthorized: 125 + return fmt.Errorf("Unauthorized XRPC request.") 126 + default: 127 + return fmt.Errorf("Failed to perform operation. Try again later.") 128 + } 129 + }
+19 -19
docs/hacking.md
··· 55 55 quite cumbersome. So the nix flake provides a 56 56 `nixosConfiguration` to do so. 57 57 58 - To begin, head to `http://localhost:3000/knots` in the browser 59 - and create a knot with hostname `localhost:6000`. This will 60 - generate a knot secret. Set `$TANGLED_VM_KNOT_SECRET` to it, 61 - ideally in a `.envrc` with [direnv](https://direnv.net) so you 62 - don't lose it. 58 + To begin, grab your DID from http://localhost:3000/settings. 59 + Then, set `TANGLED_VM_KNOT_OWNER` and 60 + `TANGLED_VM_SPINDLE_OWNER` to your DID. 63 61 64 - You will also need to set the `$TANGLED_VM_SPINDLE_OWNER` 65 - variable to some value. If you don't want to [set up a 66 - spindle](#running-a-spindle), you can use any placeholder 67 - value. 62 + If you don't want to [set up a spindle](#running-a-spindle), 63 + you can use any placeholder value. 68 64 69 65 You can now start a lightweight NixOS VM like so: 70 66 ··· 75 71 ``` 76 72 77 73 This starts a knot on port 6000, a spindle on port 6555 78 - with `ssh` exposed on port 2222. You can push repositories 79 - to this VM with this ssh config block on your main machine: 74 + with `ssh` exposed on port 2222. 75 + 76 + Once the services are running, head to 77 + http://localhost:3000/knots and hit verify (and similarly, 78 + http://localhost:3000/spindles to verify your spindle). It 79 + should verify the ownership of the services instantly if 80 + everything went smoothly. 81 + 82 + You can push repositories to this VM with this ssh config 83 + block on your main machine: 80 84 81 85 ```bash 82 86 Host nixos-shell ··· 95 99 96 100 ## running a spindle 97 101 98 - You will need to find out your DID by entering your login handle into 99 - <https://pdsls.dev/>. Set `$TANGLED_VM_SPINDLE_OWNER` to your DID. 100 - 101 - The above VM should already be running a spindle on `localhost:6555`. 102 - You can head to the spindle dashboard on `http://localhost:3000/spindles`, 103 - and register a spindle with hostname `localhost:6555`. It should instantly 104 - be verified. You can then configure each repository to use this spindle 105 - and run CI jobs. 102 + The above VM should already be running a spindle on 103 + `localhost:6555`. Head to http://localhost:3000/spindles and 104 + hit verify. You can then configure each repository to use 105 + this spindle and run CI jobs. 106 106 107 107 Of interest when debugging spindles: 108 108
+7 -5
docs/knot-hosting.md
··· 73 73 ``` 74 74 75 75 Create `/home/git/.knot.env` with the following, updating the values as 76 - necessary. The `KNOT_SERVER_SECRET` can be obtained from the 77 - [/knots](https://tangled.sh/knots) page on Tangled. 76 + necessary. The `KNOT_SERVER_OWNER` should be set to your 77 + DID, you can find your DID in the [Settings](https://tangled.sh/settings) page. 78 78 79 79 ``` 80 80 KNOT_REPO_SCAN_PATH=/home/git 81 81 KNOT_SERVER_HOSTNAME=knot.example.com 82 82 APPVIEW_ENDPOINT=https://tangled.sh 83 - KNOT_SERVER_SECRET=secret 83 + KNOT_SERVER_OWNER=did:plc:foobar 84 84 KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 85 85 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 86 86 ``` ··· 128 128 Remember to use Let's Encrypt or similar to procure a certificate for your 129 129 knot domain. 130 130 131 - You should now have a running knot server! You can finalize your registration by hitting the 132 - `initialize` button on the [/knots](https://tangled.sh/knots) page. 131 + You should now have a running knot server! You can finalize 132 + your registration by hitting the `verify` button on the 133 + [/knots](https://tangled.sh/knots) page. This simply creates 134 + a record on your PDS to announce the existence of the knot. 133 135 134 136 ### custom paths 135 137
+39
docs/migrations/knot-1.7.0.md
··· 1 + # Upgrading from v1.7.0 2 + 3 + After v1.7.0, knot secrets have been deprecated. You no 4 + longer need a secret from the appview to run a knot. All 5 + authorized commands between services to knots are managed 6 + via [Service 7 + Auth](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 8 + Knots will be read-only until upgraded. 9 + 10 + Upgrading is quite easy, in essence: 11 + 12 + - `KNOT_SERVER_SECRET` is no more, you can remove this 13 + environment variable entirely 14 + - `KNOT_SERVER_OWNER` is now required on boot, set this to 15 + your DID. You can find your DID in the 16 + [settings](https://tangled.sh/settings) page. 17 + - Restart your knot once you have replace the environment 18 + variable 19 + - Head to the [knot dashboard](https://tangled.sh/knots) and 20 + hit the "retry" button to verify your knot. This simply 21 + writes a `sh.tangled.knot` record to your PDS. 22 + 23 + ## Nix 24 + 25 + If you use the nix module, simply bump the flake to the 26 + latest revision, and change your config block like so: 27 + 28 + ```diff 29 + services.tangled-knot = { 30 + enable = true; 31 + server = { 32 + - secretFile = /path/to/secret; 33 + + owner = "did:plc:foo"; 34 + . 35 + . 36 + . 37 + }; 38 + }; 39 + ```
-299
knotclient/signer.go
··· 1 - package knotclient 2 - 3 - import ( 4 - "bytes" 5 - "crypto/hmac" 6 - "crypto/sha256" 7 - "encoding/hex" 8 - "encoding/json" 9 - "fmt" 10 - "net/http" 11 - "net/url" 12 - "time" 13 - 14 - "tangled.sh/tangled.sh/core/types" 15 - ) 16 - 17 - type SignerTransport struct { 18 - Secret string 19 - } 20 - 21 - func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) { 22 - timestamp := time.Now().Format(time.RFC3339) 23 - mac := hmac.New(sha256.New, []byte(s.Secret)) 24 - message := req.Method + req.URL.Path + timestamp 25 - mac.Write([]byte(message)) 26 - signature := hex.EncodeToString(mac.Sum(nil)) 27 - req.Header.Set("X-Signature", signature) 28 - req.Header.Set("X-Timestamp", timestamp) 29 - return http.DefaultTransport.RoundTrip(req) 30 - } 31 - 32 - type SignedClient struct { 33 - Secret string 34 - Url *url.URL 35 - client *http.Client 36 - } 37 - 38 - func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) { 39 - client := &http.Client{ 40 - Timeout: 5 * time.Second, 41 - Transport: SignerTransport{ 42 - Secret: secret, 43 - }, 44 - } 45 - 46 - scheme := "https" 47 - if dev { 48 - scheme = "http" 49 - } 50 - url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 51 - if err != nil { 52 - return nil, err 53 - } 54 - 55 - signedClient := &SignedClient{ 56 - Secret: secret, 57 - client: client, 58 - Url: url, 59 - } 60 - 61 - return signedClient, nil 62 - } 63 - 64 - func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) { 65 - return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body)) 66 - } 67 - 68 - func (s *SignedClient) Init(did string) (*http.Response, error) { 69 - const ( 70 - Method = "POST" 71 - Endpoint = "/init" 72 - ) 73 - 74 - body, _ := json.Marshal(map[string]any{ 75 - "did": did, 76 - }) 77 - 78 - req, err := s.newRequest(Method, Endpoint, body) 79 - if err != nil { 80 - return nil, err 81 - } 82 - 83 - return s.client.Do(req) 84 - } 85 - 86 - func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) { 87 - const ( 88 - Method = "PUT" 89 - Endpoint = "/repo/new" 90 - ) 91 - 92 - body, _ := json.Marshal(map[string]any{ 93 - "did": did, 94 - "name": repoName, 95 - "default_branch": defaultBranch, 96 - }) 97 - 98 - req, err := s.newRequest(Method, Endpoint, body) 99 - if err != nil { 100 - return nil, err 101 - } 102 - 103 - return s.client.Do(req) 104 - } 105 - 106 - func (s *SignedClient) RepoForkAheadBehind(ownerDid, source, name, branch, hiddenRef string) (*http.Response, error) { 107 - const ( 108 - Method = "GET" 109 - ) 110 - endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 111 - 112 - body, _ := json.Marshal(map[string]any{ 113 - "did": ownerDid, 114 - "source": source, 115 - "name": name, 116 - "hiddenref": hiddenRef, 117 - }) 118 - 119 - req, err := s.newRequest(Method, endpoint, body) 120 - if err != nil { 121 - return nil, err 122 - } 123 - 124 - return s.client.Do(req) 125 - } 126 - 127 - func (s *SignedClient) SyncRepoFork(ownerDid, source, name, branch string) (*http.Response, error) { 128 - const ( 129 - Method = "POST" 130 - ) 131 - endpoint := fmt.Sprintf("/repo/fork/sync/%s", url.PathEscape(branch)) 132 - 133 - body, _ := json.Marshal(map[string]any{ 134 - "did": ownerDid, 135 - "source": source, 136 - "name": name, 137 - }) 138 - 139 - req, err := s.newRequest(Method, endpoint, body) 140 - if err != nil { 141 - return nil, err 142 - } 143 - 144 - return s.client.Do(req) 145 - } 146 - 147 - func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) { 148 - const ( 149 - Method = "POST" 150 - Endpoint = "/repo/fork" 151 - ) 152 - 153 - body, _ := json.Marshal(map[string]any{ 154 - "did": ownerDid, 155 - "source": source, 156 - "name": name, 157 - }) 158 - 159 - req, err := s.newRequest(Method, Endpoint, body) 160 - if err != nil { 161 - return nil, err 162 - } 163 - 164 - return s.client.Do(req) 165 - } 166 - 167 - func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 168 - const ( 169 - Method = "DELETE" 170 - Endpoint = "/repo" 171 - ) 172 - 173 - body, _ := json.Marshal(map[string]any{ 174 - "did": did, 175 - "name": repoName, 176 - }) 177 - 178 - req, err := s.newRequest(Method, Endpoint, body) 179 - if err != nil { 180 - return nil, err 181 - } 182 - 183 - return s.client.Do(req) 184 - } 185 - 186 - func (s *SignedClient) AddMember(did string) (*http.Response, error) { 187 - const ( 188 - Method = "PUT" 189 - Endpoint = "/member/add" 190 - ) 191 - 192 - body, _ := json.Marshal(map[string]any{ 193 - "did": did, 194 - }) 195 - 196 - req, err := s.newRequest(Method, Endpoint, body) 197 - if err != nil { 198 - return nil, err 199 - } 200 - 201 - return s.client.Do(req) 202 - } 203 - 204 - func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) { 205 - const ( 206 - Method = "PUT" 207 - ) 208 - endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 209 - 210 - body, _ := json.Marshal(map[string]any{ 211 - "branch": branch, 212 - }) 213 - 214 - req, err := s.newRequest(Method, endpoint, body) 215 - if err != nil { 216 - return nil, err 217 - } 218 - 219 - return s.client.Do(req) 220 - } 221 - 222 - func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) { 223 - const ( 224 - Method = "POST" 225 - ) 226 - endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 227 - 228 - body, _ := json.Marshal(map[string]any{ 229 - "did": memberDid, 230 - }) 231 - 232 - req, err := s.newRequest(Method, endpoint, body) 233 - if err != nil { 234 - return nil, err 235 - } 236 - 237 - return s.client.Do(req) 238 - } 239 - 240 - func (s *SignedClient) Merge( 241 - patch []byte, 242 - ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 243 - ) (*http.Response, error) { 244 - const ( 245 - Method = "POST" 246 - ) 247 - endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 248 - 249 - mr := types.MergeRequest{ 250 - Branch: branch, 251 - CommitMessage: commitMessage, 252 - CommitBody: commitBody, 253 - AuthorName: authorName, 254 - AuthorEmail: authorEmail, 255 - Patch: string(patch), 256 - } 257 - 258 - body, _ := json.Marshal(mr) 259 - 260 - req, err := s.newRequest(Method, endpoint, body) 261 - if err != nil { 262 - return nil, err 263 - } 264 - 265 - return s.client.Do(req) 266 - } 267 - 268 - func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 269 - const ( 270 - Method = "POST" 271 - ) 272 - endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo) 273 - 274 - body, _ := json.Marshal(map[string]any{ 275 - "patch": string(patch), 276 - "branch": branch, 277 - }) 278 - 279 - req, err := s.newRequest(Method, endpoint, body) 280 - if err != nil { 281 - return nil, err 282 - } 283 - 284 - return s.client.Do(req) 285 - } 286 - 287 - func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) { 288 - const ( 289 - Method = "POST" 290 - ) 291 - endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch)) 292 - 293 - req, err := s.newRequest(Method, endpoint, nil) 294 - if err != nil { 295 - return nil, err 296 - } 297 - 298 - return s.client.Do(req) 299 - }
-10
knotserver/http_util.go
··· 20 20 func notFound(w http.ResponseWriter) { 21 21 writeError(w, "not found", http.StatusNotFound) 22 22 } 23 - 24 - func writeMsg(w http.ResponseWriter, msg string) { 25 - writeJSON(w, map[string]string{"msg": msg}) 26 - } 27 - 28 - func writeConflict(w http.ResponseWriter, data interface{}) { 29 - w.Header().Set("Content-Type", "application/json") 30 - w.WriteHeader(http.StatusConflict) 31 - json.NewEncoder(w).Encode(data) 32 - }
+10 -6
knotserver/ingester.go
··· 73 73 } 74 74 l.Info("added member from firehose", "member", record.Subject) 75 75 76 - if err := h.db.AddDid(did); err != nil { 76 + if err := h.db.AddDid(record.Subject); err != nil { 77 77 l.Error("failed to add did", "error", err) 78 78 return fmt.Errorf("failed to add did: %w", err) 79 79 } 80 - h.jc.AddDid(did) 80 + h.jc.AddDid(record.Subject) 81 81 82 - if err := h.fetchAndAddKeys(ctx, did); err != nil { 82 + if err := h.fetchAndAddKeys(ctx, record.Subject); err != nil { 83 83 return fmt.Errorf("failed to fetch and add keys: %w", err) 84 84 } 85 85 ··· 255 255 didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 256 256 257 257 // check perms for this user 258 - if ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo); !ok || err != nil { 259 - return fmt.Errorf("insufficient permissions: %w", err) 258 + ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo) 259 + if err != nil { 260 + return fmt.Errorf("failed to check permissions: %w", err) 261 + } 262 + if !ok { 263 + return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo) 260 264 } 261 265 262 266 if err := h.db.AddDid(subjectId.DID.String()); err != nil { ··· 298 302 return fmt.Errorf("error reading response body: %w", err) 299 303 } 300 304 301 - for _, key := range strings.Split(string(plaintext), "\n") { 305 + for key := range strings.SplitSeq(string(plaintext), "\n") { 302 306 if key == "" { 303 307 continue 304 308 }
-2
knotserver/internal.go
··· 47 47 } 48 48 49 49 w.WriteHeader(http.StatusNoContent) 50 - return 51 50 } 52 51 53 52 func (h *InternalHandle) InternalKeys(w http.ResponseWriter, r *http.Request) { ··· 63 62 data = append(data, j) 64 63 } 65 64 writeJSON(w, data) 66 - return 67 65 } 68 66 69 67 type PushOptions struct {
+93 -79
knotserver/xrpc/delete_repo.go
··· 1 1 package xrpc 2 2 3 - // import ( 4 - // "encoding/json" 5 - // "fmt" 6 - // "net/http" 7 - // "os" 8 - // "path/filepath" 9 - // 10 - // "github.com/bluesky-social/indigo/atproto/syntax" 11 - // securejoin "github.com/cyphar/filepath-securejoin" 12 - // "tangled.sh/tangled.sh/core/api/tangled" 13 - // "tangled.sh/tangled.sh/core/rbac" 14 - // xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 15 - // ) 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "os" 8 + "path/filepath" 16 9 17 - // func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) { 18 - // l := x.Logger.With("handler", "DeleteRepo") 19 - // fail := func(e xrpcerr.XrpcError) { 20 - // l.Error("failed", "kind", e.Tag, "error", e.Message) 21 - // writeError(w, e, http.StatusBadRequest) 22 - // } 23 - // 24 - // actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 25 - // if !ok { 26 - // fail(xrpcerr.MissingActorDidError) 27 - // return 28 - // } 29 - // 30 - // isMember, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer) 31 - // if err != nil { 32 - // fail(xrpcerr.GenericError(err)) 33 - // return 34 - // } 35 - // if !isMember { 36 - // fail(xrpcerr.AccessControlError(actorDid.String())) 37 - // return 38 - // } 39 - // 40 - // var data tangled.RepoDelete_Input 41 - // if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 42 - // fail(xrpcerr.GenericError(err)) 43 - // return 44 - // } 45 - // 46 - // did := data.Did 47 - // name := data.Name 48 - // 49 - // if did == "" || name == "" { 50 - // fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 51 - // return 52 - // } 53 - // 54 - // relativeRepoPath := filepath.Join(did, name) 55 - // if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 56 - // l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 57 - // writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 58 - // return 59 - // } 60 - // 61 - // repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 62 - // if err != nil { 63 - // fail(xrpcerr.GenericError(err)) 64 - // return 65 - // } 66 - // 67 - // err = os.RemoveAll(repoPath) 68 - // if err != nil { 69 - // l.Error("deleting repo", "error", err.Error()) 70 - // writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 71 - // return 72 - // } 73 - // 74 - // err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 75 - // if err != nil { 76 - // l.Error("failed to delete repo from enforcer", "error", err.Error()) 77 - // writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 78 - // return 79 - // } 80 - // 81 - // w.WriteHeader(http.StatusOK) 82 - // } 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/bluesky-social/indigo/xrpc" 13 + securejoin "github.com/cyphar/filepath-securejoin" 14 + "tangled.sh/tangled.sh/core/api/tangled" 15 + "tangled.sh/tangled.sh/core/rbac" 16 + xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 17 + ) 18 + 19 + func (x *Xrpc) DeleteRepo(w http.ResponseWriter, r *http.Request) { 20 + l := x.Logger.With("handler", "DeleteRepo") 21 + fail := func(e xrpcerr.XrpcError) { 22 + l.Error("failed", "kind", e.Tag, "error", e.Message) 23 + writeError(w, e, http.StatusBadRequest) 24 + } 25 + 26 + actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 27 + if !ok { 28 + fail(xrpcerr.MissingActorDidError) 29 + return 30 + } 31 + 32 + var data tangled.RepoDelete_Input 33 + if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 34 + fail(xrpcerr.GenericError(err)) 35 + return 36 + } 37 + 38 + did := data.Did 39 + name := data.Name 40 + rkey := data.Rkey 41 + 42 + if did == "" || name == "" { 43 + fail(xrpcerr.GenericError(fmt.Errorf("did and name are required"))) 44 + return 45 + } 46 + 47 + ident, err := x.Resolver.ResolveIdent(r.Context(), actorDid.String()) 48 + if err != nil || ident.Handle.IsInvalidHandle() { 49 + fail(xrpcerr.GenericError(err)) 50 + return 51 + } 52 + 53 + xrpcc := xrpc.Client{ 54 + Host: ident.PDSEndpoint(), 55 + } 56 + 57 + // ensure that the record does not exists 58 + _, err = comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 59 + if err == nil { 60 + fail(xrpcerr.RecordExistsError(rkey)) 61 + return 62 + } 63 + 64 + relativeRepoPath := filepath.Join(did, name) 65 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 66 + if err != nil { 67 + fail(xrpcerr.GenericError(err)) 68 + return 69 + } 70 + if !isDeleteAllowed { 71 + fail(xrpcerr.AccessControlError(actorDid.String())) 72 + return 73 + } 74 + 75 + repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 76 + if err != nil { 77 + fail(xrpcerr.GenericError(err)) 78 + return 79 + } 80 + 81 + err = os.RemoveAll(repoPath) 82 + if err != nil { 83 + l.Error("deleting repo", "error", err.Error()) 84 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 85 + return 86 + } 87 + 88 + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 89 + if err != nil { 90 + l.Error("failed to delete repo from enforcer", "error", err.Error()) 91 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 92 + return 93 + } 94 + 95 + w.WriteHeader(http.StatusOK) 96 + }
+1
knotserver/xrpc/xrpc.go
··· 37 37 38 38 r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch) 39 39 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo) 40 + r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo) 40 41 r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus) 41 42 r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync) 42 43 r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
+5 -1
lexicons/repo/delete.json
··· 9 9 "encoding": "application/json", 10 10 "schema": { 11 11 "type": "object", 12 - "required": ["did", "name"], 12 + "required": ["did", "name", "rkey"], 13 13 "properties": { 14 14 "did": { 15 15 "type": "string", ··· 19 19 "name": { 20 20 "type": "string", 21 21 "description": "Name of the repository to delete" 22 + }, 23 + "rkey": { 24 + "type": "string", 25 + "description": "Rkey of the repository record" 22 26 } 23 27 } 24 28 }
-7
nix/modules/knot.nix
··· 99 99 description = "DID of owner (required)"; 100 100 }; 101 101 102 - secretFile = mkOption { 103 - type = lib.types.path; 104 - example = "KNOT_SERVER_SECRET=<hash>"; 105 - description = "File containing secret key provided by appview (required)"; 106 - }; 107 - 108 102 dbPath = mkOption { 109 103 type = types.path; 110 104 default = "${cfg.stateDir}/knotserver.db"; ··· 207 201 "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 208 202 "KNOT_SERVER_OWNER=${cfg.server.owner}" 209 203 ]; 210 - EnvironmentFile = cfg.server.secretFile; 211 204 ExecStart = "${cfg.package}/bin/knot server"; 212 205 Restart = "always"; 213 206 };
+2 -2
rbac/rbac.go
··· 281 281 return e.E.Enforce(user, domain, domain, "repo:create") 282 282 } 283 283 284 - func (e *Enforcer) IsRepoDeleteAllowed(user, domain string) (bool, error) { 285 - return e.E.Enforce(user, domain, domain, "repo:delete") 284 + func (e *Enforcer) IsRepoDeleteAllowed(user, domain, repo string) (bool, error) { 285 + return e.E.Enforce(user, domain, repo, "repo:delete") 286 286 } 287 287 288 288 func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
+7
xrpc/errors/errors.go
··· 86 86 ) 87 87 } 88 88 89 + var RecordExistsError = func(r string) XrpcError { 90 + return NewXrpcError( 91 + WithTag("RecordExists"), 92 + WithError(fmt.Errorf("repo already exists: %s", r)), 93 + ) 94 + } 95 + 89 96 func GenericError(err error) XrpcError { 90 97 return NewXrpcError( 91 98 WithTag("Generic"),