···1717 Dev bool `env:"DEV, default=false"`1818 DisallowedNicknamesFile string `env:"DISALLOWED_NICKNAMES_FILE"`19192020- // temporarily, to add users to default spindle2020+ // temporarily, to add users to default knot and spindle2121 AppPassword string `env:"APP_PASSWORD"`2222}2323
+136-120
appview/oauth/handler/handler.go
···88 "log"99 "net/http"1010 "net/url"1111+ "slices"1112 "strings"1213 "time"1314···2625 "tangled.sh/tangled.sh/core/appview/oauth/client"2726 "tangled.sh/tangled.sh/core/appview/pages"2827 "tangled.sh/tangled.sh/core/idresolver"2929- "tangled.sh/tangled.sh/core/knotclient"3028 "tangled.sh/tangled.sh/core/rbac"3129 "tangled.sh/tangled.sh/core/tid"3230)···353353 return pubKey, nil354354}355355356356+var (357357+ tangledHandle = "tangled.sh"358358+ tangledDid = "did:plc:wshs7t2adsemcrrd4snkeqli"359359+ defaultSpindle = "spindle.tangled.sh"360360+ defaultKnot = "knot1.tangled.sh"361361+)362362+356363func (o *OAuthHandler) addToDefaultSpindle(did string) {357364 // use the tangled.sh app password to get an accessJwt358365 // and create an sh.tangled.spindle.member record with that359359-360360- defaultSpindle := "spindle.tangled.sh"361361- appPassword := o.config.Core.AppPassword362362-363366 spindleMembers, err := db.GetSpindleMembers(364367 o.db,365368 db.FilterEq("instance", "spindle.tangled.sh"),···378375 return379376 }380377381381- // TODO: hardcoded tangled handle and did for now382382- tangledHandle := "tangled.sh"383383- tangledDid := "did:plc:wshs7t2adsemcrrd4snkeqli"384384-385385- if appPassword == "" {386386- log.Println("no app password configured, skipping spindle member addition")387387- return388388- }389389-390378 log.Printf("adding %s to default spindle", did)391391-392392- resolved, err := o.idResolver.ResolveIdent(context.Background(), tangledDid)379379+ session, err := o.createAppPasswordSession()393380 if err != nil {394394- log.Printf("failed to resolve tangled.sh DID %s: %v", tangledDid, err)395395- return396396- }397397-398398- pdsEndpoint := resolved.PDSEndpoint()399399- if pdsEndpoint == "" {400400- log.Printf("no PDS endpoint found for tangled.sh DID %s", tangledDid)401401- return402402- }403403-404404- sessionPayload := map[string]string{405405- "identifier": tangledHandle,406406- "password": appPassword,407407- }408408- sessionBytes, err := json.Marshal(sessionPayload)409409- if err != nil {410410- log.Printf("failed to marshal session payload: %v", err)411411- return412412- }413413-414414- sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession"415415- sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes))416416- if err != nil {417417- log.Printf("failed to create session request: %v", err)418418- return419419- }420420- sessionReq.Header.Set("Content-Type", "application/json")421421-422422- client := &http.Client{Timeout: 30 * time.Second}423423- sessionResp, err := client.Do(sessionReq)424424- if err != nil {425425- log.Printf("failed to create session: %v", err)426426- return427427- }428428- defer sessionResp.Body.Close()429429-430430- if sessionResp.StatusCode != http.StatusOK {431431- log.Printf("failed to create session: HTTP %d", sessionResp.StatusCode)432432- return433433- }434434-435435- var session struct {436436- AccessJwt string `json:"accessJwt"`437437- }438438- if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil {439439- log.Printf("failed to decode session response: %v", err)381381+ log.Printf("failed to create session: %s", err)440382 return441383 }442384···392444 CreatedAt: time.Now().Format(time.RFC3339),393445 }394446395395- recordBytes, err := json.Marshal(record)396396- if err != nil {397397- log.Printf("failed to marshal spindle member record: %v", err)398398- return399399- }400400-401401- payload := map[string]interface{}{402402- "repo": tangledDid,403403- "collection": tangled.SpindleMemberNSID,404404- "rkey": tid.TID(),405405- "record": json.RawMessage(recordBytes),406406- }407407-408408- payloadBytes, err := json.Marshal(payload)409409- if err != nil {410410- log.Printf("failed to marshal request payload: %v", err)411411- return412412- }413413-414414- url := pdsEndpoint + "/xrpc/com.atproto.repo.putRecord"415415- req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes))416416- if err != nil {417417- log.Printf("failed to create HTTP request: %v", err)418418- return419419- }420420-421421- req.Header.Set("Content-Type", "application/json")422422- req.Header.Set("Authorization", "Bearer "+session.AccessJwt)423423-424424- resp, err := client.Do(req)425425- if err != nil {426426- log.Printf("failed to add user to default spindle: %v", err)427427- return428428- }429429- defer resp.Body.Close()430430-431431- if resp.StatusCode != http.StatusOK {432432- log.Printf("failed to add user to default spindle: HTTP %d", resp.StatusCode)447447+ if err := session.putRecord(record); err != nil {448448+ log.Printf("failed to add member to default knot: %s", err)433449 return434450 }435451···401489}402490403491func (o *OAuthHandler) addToDefaultKnot(did string) {404404- defaultKnot := "knot1.tangled.sh"492492+ // use the tangled.sh app password to get an accessJwt493493+ // and create an sh.tangled.spindle.member record with that405494406406- log.Printf("adding %s to default knot", did)407407- err := o.enforcer.AddKnotMember(defaultKnot, did)495495+ allKnots, err := o.enforcer.GetKnotsForUser(did)408496 if err != nil {409409- log.Println("failed to add user to knot1.tangled.sh: ", err)410410- return411411- }412412- err = o.enforcer.E.SavePolicy()413413- if err != nil {414414- log.Println("failed to add user to knot1.tangled.sh: ", err)497497+ log.Printf("failed to get knot members for did %s: %v", did, err)415498 return416499 }417500418418- secret, err := db.GetRegistrationKey(o.db, defaultKnot)419419- if err != nil {420420- log.Println("failed to get registration key for knot1.tangled.sh")421421- return422422- }423423- signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.config.Core.Dev)424424- resp, err := signedClient.AddMember(did)425425- if err != nil {426426- log.Println("failed to add user to knot1.tangled.sh: ", err)501501+ if slices.Contains(allKnots, defaultKnot) {502502+ log.Printf("did %s is already a member of the default knot", did)427503 return428504 }429505430430- if resp.StatusCode != http.StatusNoContent {431431- log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode)506506+ log.Printf("adding %s to default knot", did)507507+ session, err := o.createAppPasswordSession()508508+ if err != nil {509509+ log.Printf("failed to create session: %s", err)432510 return433511 }512512+513513+ record := tangled.KnotMember{514514+ LexiconTypeID: "sh.tangled.knot.member",515515+ Subject: did,516516+ Domain: defaultKnot,517517+ CreatedAt: time.Now().Format(time.RFC3339),518518+ }519519+520520+ if err := session.putRecord(record); err != nil {521521+ log.Printf("failed to add member to default knot: %s", err)522522+ return523523+ }524524+525525+ log.Printf("successfully added %s to default Knot", did)526526+}527527+528528+// create a session using apppasswords529529+type session struct {530530+ AccessJwt string `json:"accessJwt"`531531+ PdsEndpoint string532532+}533533+534534+func (o *OAuthHandler) createAppPasswordSession() (*session, error) {535535+ appPassword := o.config.Core.AppPassword536536+ if appPassword == "" {537537+ return nil, fmt.Errorf("no app password configured, skipping member addition")538538+ }539539+540540+ resolved, err := o.idResolver.ResolveIdent(context.Background(), tangledDid)541541+ if err != nil {542542+ return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", tangledDid, err)543543+ }544544+545545+ pdsEndpoint := resolved.PDSEndpoint()546546+ if pdsEndpoint == "" {547547+ return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", tangledDid)548548+ }549549+550550+ sessionPayload := map[string]string{551551+ "identifier": tangledHandle,552552+ "password": appPassword,553553+ }554554+ sessionBytes, err := json.Marshal(sessionPayload)555555+ if err != nil {556556+ return nil, fmt.Errorf("failed to marshal session payload: %v", err)557557+ }558558+559559+ sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession"560560+ sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes))561561+ if err != nil {562562+ return nil, fmt.Errorf("failed to create session request: %v", err)563563+ }564564+ sessionReq.Header.Set("Content-Type", "application/json")565565+566566+ client := &http.Client{Timeout: 30 * time.Second}567567+ sessionResp, err := client.Do(sessionReq)568568+ if err != nil {569569+ return nil, fmt.Errorf("failed to create session: %v", err)570570+ }571571+ defer sessionResp.Body.Close()572572+573573+ if sessionResp.StatusCode != http.StatusOK {574574+ return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode)575575+ }576576+577577+ var session session578578+ if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil {579579+ return nil, fmt.Errorf("failed to decode session response: %v", err)580580+ }581581+582582+ session.PdsEndpoint = pdsEndpoint583583+584584+ return &session, nil585585+}586586+587587+func (s *session) putRecord(record any) error {588588+ recordBytes, err := json.Marshal(record)589589+ if err != nil {590590+ return fmt.Errorf("failed to marshal knot member record: %w", err)591591+ }592592+593593+ payload := map[string]any{594594+ "repo": tangledDid,595595+ "collection": tangled.KnotMemberNSID,596596+ "rkey": tid.TID(),597597+ "record": json.RawMessage(recordBytes),598598+ }599599+600600+ payloadBytes, err := json.Marshal(payload)601601+ if err != nil {602602+ return fmt.Errorf("failed to marshal request payload: %w", err)603603+ }604604+605605+ url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord"606606+ req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes))607607+ if err != nil {608608+ return fmt.Errorf("failed to create HTTP request: %w", err)609609+ }610610+611611+ req.Header.Set("Content-Type", "application/json")612612+ req.Header.Set("Authorization", "Bearer "+s.AccessJwt)613613+614614+ client := &http.Client{Timeout: 30 * time.Second}615615+ resp, err := client.Do(req)616616+ if err != nil {617617+ return fmt.Errorf("failed to add user to default Knot: %w", err)618618+ }619619+ defer resp.Body.Close()620620+621621+ if resp.StatusCode != http.StatusOK {622622+ return fmt.Errorf("failed to add user to default Knot: HTTP %d", resp.StatusCode)623623+ }624624+625625+ return nil434626}
-157
appview/repo/index.go
···101101 user := rp.oauth.GetUser(r)102102 repoInfo := f.RepoInfo(user)103103104104- // secret, err := db.GetRegistrationKey(rp.db, f.Knot)105105- // if err != nil {106106- // log.Printf("failed to get registration key for %s: %s", f.Knot, err)107107- // rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")108108- // }109109-110110- // signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)111111- // if err != nil {112112- // log.Printf("failed to create signed client for %s: %s", f.Knot, err)113113- // return114114- // }115115-116116- // var forkInfo *types.ForkInfo117117- // if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {118118- // forkInfo, err = getForkInfo(r, repoInfo, rp, f, result.Ref, user, signedClient)119119- // if err != nil {120120- // log.Printf("Failed to fetch fork information: %v", err)121121- // return122122- // }123123- // }124124-125104 // TODO: a bit dirty126105 languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "")127106 if err != nil {···206227207228 return languageStats, nil208229}209209-210210-// func getForkInfo(211211-// r *http.Request,212212-// repoInfo repoinfo.RepoInfo,213213-// rp *Repo,214214-// f *reporesolver.ResolvedRepo,215215-// currentRef string,216216-// user *oauth.User,217217-// signedClient *knotclient.SignedClient,218218-// ) (*types.ForkInfo, error) {219219-// if user == nil {220220-// return nil, nil221221-// }222222-//223223-// forkInfo := types.ForkInfo{224224-// IsFork: repoInfo.Source != nil,225225-// Status: types.UpToDate,226226-// }227227-//228228-// if !forkInfo.IsFork {229229-// forkInfo.IsFork = false230230-// return &forkInfo, nil231231-// }232232-//233233-// us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev)234234-// if err != nil {235235-// log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)236236-// return nil, err237237-// }238238-//239239-// result, err := us.Branches(repoInfo.Source.Did, repoInfo.Source.Name)240240-// if err != nil {241241-// log.Println("failed to reach knotserver", err)242242-// return nil, err243243-// }244244-//245245-// if !slices.ContainsFunc(result.Branches, func(branch types.Branch) bool {246246-// return branch.Name == currentRef247247-// }) {248248-// forkInfo.Status = types.MissingBranch249249-// return &forkInfo, nil250250-// }251251-//252252-// <<<<<<< Conflict 1 of 2253253-// %%%%%%% Changes from base #1 to side #1254254-// client, err := rp.oauth.ServiceClient(255255-// r,256256-// oauth.WithService(f.Knot),257257-// oauth.WithLxm(tangled.RepoHiddenRefNSID),258258-// oauth.WithDev(rp.config.Core.Dev),259259-// )260260-// if err != nil {261261-// log.Printf("failed to connect to knot server: %v", err)262262-// %%%%%%% Changes from base #2 to side #2263263-// - newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, currentRef, currentRef)264264-// + newHiddenRefResp, err := signedClient.NewHiddenRef(user.Did, repoInfo.Name, f.Ref, f.Ref)265265-// if err != nil || newHiddenRefResp.StatusCode != http.StatusNoContent {266266-// log.Printf("failed to update tracking branch: %s", err)267267-// +++++++ Contents of side #3268268-// client, err := rp.oauth.ServiceClient(269269-// r,270270-// oauth.WithService(f.Knot),271271-// oauth.WithLxm(tangled.RepoHiddenRefNSID),272272-// oauth.WithDev(rp.config.Core.Dev),273273-// )274274-// if err != nil {275275-// log.Printf("failed to connect to knot server: %v", err)276276-// >>>>>>> Conflict 1 of 2 ends277277-// return nil, err278278-// }279279-//280280-// <<<<<<< Conflict 2 of 2281281-// %%%%%%% Changes from base #1 to side #1282282-// resp, err := tangled.RepoHiddenRef(283283-// r.Context(),284284-// client,285285-// &tangled.RepoHiddenRef_Input{286286-// - ForkRef: f.Ref,287287-// - RemoteRef: f.Ref,288288-// + ForkRef: currentRef,289289-// + RemoteRef: currentRef,290290-// Repo: f.RepoAt().String(),291291-// },292292-// )293293-// if err != nil || !resp.Success {294294-// if err != nil {295295-// log.Printf("failed to update tracking branch: %s", err)296296-// } else {297297-// log.Printf("failed to update tracking branch: success=false")298298-// }299299-// return nil, fmt.Errorf("failed to update tracking branch")300300-// }301301-//302302-// - hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)303303-// + hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef)304304-//305305-// %%%%%%% Changes from base #2 to side #2306306-// - hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef)307307-// + hiddenRef := fmt.Sprintf("hidden/%s/%s", f.Ref, f.Ref)308308-//309309-// +++++++ Contents of side #3310310-// resp, err := tangled.RepoHiddenRef(311311-// r.Context(),312312-// client,313313-// &tangled.RepoHiddenRef_Input{314314-// ForkRef: currentRef,315315-// RemoteRef: currentRef,316316-// Repo: f.RepoAt().String(),317317-// },318318-// )319319-// if err != nil || !resp.Success {320320-// if err != nil {321321-// log.Printf("failed to update tracking branch: %s", err)322322-// } else {323323-// log.Printf("failed to update tracking branch: success=false")324324-// }325325-// return nil, fmt.Errorf("failed to update tracking branch")326326-// }327327-//328328-// hiddenRef := fmt.Sprintf("hidden/%s/%s", currentRef, currentRef)329329-// >>>>>>> Conflict 2 of 2 ends330330-// var status types.AncestorCheckResponse331331-// forkSyncableResp, err := signedClient.RepoForkAheadBehind(user.Did, string(f.RepoAt()), repoInfo.Name, currentRef, hiddenRef)332332-// if err != nil {333333-// log.Printf("failed to check if fork is ahead/behind: %s", err)334334-// return nil, err335335-// }336336-//337337-// if err := json.NewDecoder(forkSyncableResp.Body).Decode(&status); err != nil {338338-// log.Printf("failed to decode fork status: %s", err)339339-// return nil, err340340-// }341341-//342342-// forkInfo.Status = status.Status343343-// return &forkInfo, nil344344-// }
+22-91
appview/repo/repo.go
···863863 fail("Failed to write record to PDS.", err)864864 return865865 }866866- l = l.With("at-uri", resp.Uri)866866+867867+ aturi := resp.Uri868868+ l = l.With("at-uri", aturi)867869 l.Info("wrote record to PDS")868868-869869- l.Info("adding to knot")870870- secret, err := db.GetRegistrationKey(rp.db, f.Knot)871871- if err != nil {872872- fail("Failed to add to knot.", err)873873- return874874- }875875-876876- ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)877877- if err != nil {878878- fail("Failed to add to knot.", err)879879- return880880- }881881-882882- ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.Name, collaboratorIdent.DID.String())883883- if err != nil {884884- fail("Knot was unreachable.", err)885885- return886886- }887887-888888- if ksResp.StatusCode != http.StatusNoContent {889889- fail(fmt.Sprintf("Knot returned unexpected status code: %d.", ksResp.StatusCode), nil)890890- return891891- }892870893871 tx, err := rp.db.BeginTx(r.Context(), nil)894872 if err != nil {895873 fail("Failed to add collaborator.", err)896874 return897875 }898898- defer func() {899899- tx.Rollback()900900- err = rp.enforcer.E.LoadPolicy()901901- if err != nil {902902- fail("Failed to add collaborator.", err)876876+877877+ rollback := func() {878878+ err1 := tx.Rollback()879879+ err2 := rp.enforcer.E.LoadPolicy()880880+ err3 := rollbackRecord(context.Background(), aturi, client)881881+882882+ // ignore txn complete errors, this is okay883883+ if errors.Is(err1, sql.ErrTxDone) {884884+ err1 = nil903885 }904904- }()886886+887887+ if errs := errors.Join(err1, err2, err3); errs != nil {888888+ l.Error("failed to rollback changes", "errs", errs)889889+ return890890+ }891891+ }892892+ defer rollback()905893906894 err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())907895 if err != nil {···920932 fail("Failed to update collaborator permissions.", err)921933 return922934 }935935+936936+ // clear aturi to when everything is successful937937+ aturi = ""923938924939 rp.pages.HxRefresh(w)925940}···11981207 case "pipelines":11991208 rp.pipelineSettings(w, r)12001209 }12011201-12021202- // user := rp.oauth.GetUser(r)12031203- // repoCollaborators, err := f.Collaborators(r.Context())12041204- // if err != nil {12051205- // log.Println("failed to get collaborators", err)12061206- // }12071207-12081208- // isCollaboratorInviteAllowed := false12091209- // if user != nil {12101210- // ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())12111211- // if err == nil && ok {12121212- // isCollaboratorInviteAllowed = true12131213- // }12141214- // }12151215-12161216- // us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)12171217- // if err != nil {12181218- // log.Println("failed to create unsigned client", err)12191219- // return12201220- // }12211221-12221222- // result, err := us.Branches(f.OwnerDid(), f.Name)12231223- // if err != nil {12241224- // log.Println("failed to reach knotserver", err)12251225- // return12261226- // }12271227-12281228- // // all spindles that this user is a member of12291229- // spindles, err := rp.enforcer.GetSpindlesForUser(user.Did)12301230- // if err != nil {12311231- // log.Println("failed to fetch spindles", err)12321232- // return12331233- // }12341234-12351235- // var secrets []*tangled.RepoListSecrets_Secret12361236- // if f.Spindle != "" {12371237- // if spindleClient, err := rp.oauth.ServiceClient(12381238- // r,12391239- // oauth.WithService(f.Spindle),12401240- // oauth.WithLxm(tangled.RepoListSecretsNSID),12411241- // oauth.WithDev(rp.config.Core.Dev),12421242- // ); err != nil {12431243- // log.Println("failed to create spindle client", err)12441244- // } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil {12451245- // log.Println("failed to fetch secrets", err)12461246- // } else {12471247- // secrets = resp.Secrets12481248- // }12491249- // }12501250-12511251- // rp.pages.RepoSettings(w, pages.RepoSettingsParams{12521252- // LoggedInUser: user,12531253- // RepoInfo: f.RepoInfo(user),12541254- // Collaborators: repoCollaborators,12551255- // IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,12561256- // Branches: result.Branches,12571257- // Spindles: spindles,12581258- // CurrentSpindle: f.Spindle,12591259- // Secrets: secrets,12601260- // })12611210}1262121112631212func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {