forked from tangled.org/core
Monorepo for Tangled

appview: switch to indigo oauth library

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li d76a775d 5ecd54b3

verified
Changed files
+539 -1037
appview
issues
knots
labels
middleware
notifications
oauth
pages
templates
layouts
fragments
repo
pulls
user
settings
pipelines
pulls
repo
settings
signup
spindles
state
strings
xrpcclient
+11 -11
appview/issues/issues.go
··· 12 12 "time" 13 13 14 14 comatproto "github.com/bluesky-social/indigo/api/atproto" 15 + atpclient "github.com/bluesky-social/indigo/atproto/client" 15 16 "github.com/bluesky-social/indigo/atproto/syntax" 16 17 lexutil "github.com/bluesky-social/indigo/lex/util" 17 18 "github.com/go-chi/chi/v5" ··· 26 27 "tangled.org/core/appview/pagination" 27 28 "tangled.org/core/appview/reporesolver" 28 29 "tangled.org/core/appview/validator" 29 - "tangled.org/core/appview/xrpcclient" 30 30 "tangled.org/core/idresolver" 31 31 tlog "tangled.org/core/log" 32 32 "tangled.org/core/tid" ··· 166 166 return 167 167 } 168 168 169 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey) 169 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey) 170 170 if err != nil { 171 171 l.Error("failed to get record", "err", err) 172 172 rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.") 173 173 return 174 174 } 175 175 176 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 176 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 177 177 Collection: tangled.RepoIssueNSID, 178 178 Repo: user.Did, 179 179 Rkey: newIssue.Rkey, ··· 241 241 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 242 242 return 243 243 } 244 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 244 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 245 245 Collection: tangled.RepoIssueNSID, 246 246 Repo: issue.Did, 247 247 Rkey: issue.Rkey, ··· 408 408 } 409 409 410 410 // create a record first 411 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 411 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 412 412 Collection: tangled.RepoIssueCommentNSID, 413 413 Repo: comment.Did, 414 414 Rkey: comment.Rkey, ··· 559 559 // rkey is optional, it was introduced later 560 560 if newComment.Rkey != "" { 561 561 // update the record on pds 562 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 562 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 563 563 if err != nil { 564 564 log.Println("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 565 565 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 566 566 return 567 567 } 568 568 569 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 569 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 570 570 Collection: tangled.RepoIssueCommentNSID, 571 571 Repo: user.Did, 572 572 Rkey: newComment.Rkey, ··· 733 733 rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 734 734 return 735 735 } 736 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 736 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 737 737 Collection: tangled.RepoIssueCommentNSID, 738 738 Repo: user.Did, 739 739 Rkey: comment.Rkey, ··· 865 865 rp.pages.Notice(w, "issues", "Failed to create issue.") 866 866 return 867 867 } 868 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 868 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 869 869 Collection: tangled.RepoIssueNSID, 870 870 Repo: user.Did, 871 871 Rkey: issue.Rkey, ··· 923 923 // this is used to rollback changes made to the PDS 924 924 // 925 925 // it is a no-op if the provided ATURI is empty 926 - func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 926 + func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 927 927 if aturi == "" { 928 928 return nil 929 929 } ··· 934 934 repo := parsed.Authority().String() 935 935 rkey := parsed.RecordKey().String() 936 936 937 - _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 937 + _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 938 938 Collection: collection, 939 939 Repo: repo, 940 940 Rkey: rkey,
+6 -6
appview/knots/knots.go
··· 185 185 return 186 186 } 187 187 188 - ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain) 188 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 189 189 var exCid *string 190 190 if ex != nil { 191 191 exCid = ex.Cid 192 192 } 193 193 194 194 // re-announce by registering under same rkey 195 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 195 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 196 196 Collection: tangled.KnotNSID, 197 197 Repo: user.Did, 198 198 Rkey: domain, ··· 323 323 return 324 324 } 325 325 326 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 326 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 327 327 Collection: tangled.KnotNSID, 328 328 Repo: user.Did, 329 329 Rkey: domain, ··· 431 431 return 432 432 } 433 433 434 - ex, _ := client.RepoGetRecord(r.Context(), "", tangled.KnotNSID, user.Did, domain) 434 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 435 435 var exCid *string 436 436 if ex != nil { 437 437 exCid = ex.Cid 438 438 } 439 439 440 440 // ignore the error here 441 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 441 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 442 442 Collection: tangled.KnotNSID, 443 443 Repo: user.Did, 444 444 Rkey: domain, ··· 555 555 556 556 rkey := tid.TID() 557 557 558 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 558 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 559 559 Collection: tangled.KnotMemberNSID, 560 560 Repo: user.Did, 561 561 Rkey: rkey,
+9 -9
appview/labels/labels.go
··· 9 9 "net/http" 10 10 "time" 11 11 12 - comatproto "github.com/bluesky-social/indigo/api/atproto" 13 - "github.com/bluesky-social/indigo/atproto/syntax" 14 - lexutil "github.com/bluesky-social/indigo/lex/util" 15 - "github.com/go-chi/chi/v5" 16 - 17 12 "tangled.org/core/api/tangled" 18 13 "tangled.org/core/appview/db" 19 14 "tangled.org/core/appview/middleware" ··· 21 16 "tangled.org/core/appview/oauth" 22 17 "tangled.org/core/appview/pages" 23 18 "tangled.org/core/appview/validator" 24 - "tangled.org/core/appview/xrpcclient" 25 19 "tangled.org/core/log" 26 20 "tangled.org/core/rbac" 27 21 "tangled.org/core/tid" 22 + 23 + comatproto "github.com/bluesky-social/indigo/api/atproto" 24 + atpclient "github.com/bluesky-social/indigo/atproto/client" 25 + "github.com/bluesky-social/indigo/atproto/syntax" 26 + lexutil "github.com/bluesky-social/indigo/lex/util" 27 + "github.com/go-chi/chi/v5" 28 28 ) 29 29 30 30 type Labels struct { ··· 196 196 return 197 197 } 198 198 199 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 199 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 200 200 Collection: tangled.LabelOpNSID, 201 201 Repo: did, 202 202 Rkey: rkey, ··· 252 252 // this is used to rollback changes made to the PDS 253 253 // 254 254 // it is a no-op if the provided ATURI is empty 255 - func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 255 + func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 256 256 if aturi == "" { 257 257 return nil 258 258 } ··· 263 263 repo := parsed.Authority().String() 264 264 rkey := parsed.RecordKey().String() 265 265 266 - _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 266 + _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 267 267 Collection: collection, 268 268 Repo: repo, 269 269 Rkey: rkey,
+5 -14
appview/middleware/middleware.go
··· 43 43 44 44 type middlewareFunc func(http.Handler) http.Handler 45 45 46 - func (mw *Middleware) TryRefreshSession() middlewareFunc { 47 - return func(next http.Handler) http.Handler { 48 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 - _, _, _ = mw.oauth.GetSession(r) 50 - next.ServeHTTP(w, r) 51 - }) 52 - } 53 - } 54 - 55 - func AuthMiddleware(a *oauth.OAuth) middlewareFunc { 46 + func AuthMiddleware(o *oauth.OAuth) middlewareFunc { 56 47 return func(next http.Handler) http.Handler { 57 48 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 58 49 returnURL := "/" ··· 72 63 } 73 64 } 74 65 75 - _, auth, err := a.GetSession(r) 66 + sess, err := o.ResumeSession(r) 76 67 if err != nil { 77 - log.Println("not logged in, redirecting", "err", err) 68 + log.Println("failed to resume session, redirecting...", "err", err, "url", r.URL.String()) 78 69 redirectFunc(w, r) 79 70 return 80 71 } 81 72 82 - if !auth { 83 - log.Printf("not logged in, redirecting") 73 + if sess == nil { 74 + log.Printf("session is nil, redirecting...") 84 75 redirectFunc(w, r) 85 76 return 86 77 }
+18 -20
appview/notifications/notifications.go
··· 1 1 package notifications 2 2 3 3 import ( 4 - "fmt" 5 4 "log" 6 5 "net/http" 7 6 "strconv" ··· 31 30 func (n *Notifications) Router(mw *middleware.Middleware) http.Handler { 32 31 r := chi.NewRouter() 33 32 34 - r.Use(middleware.AuthMiddleware(n.oauth)) 35 - 36 - r.With(middleware.Paginate).Get("/", n.notificationsPage) 37 - 38 33 r.Get("/count", n.getUnreadCount) 39 - r.Post("/{id}/read", n.markRead) 40 - r.Post("/read-all", n.markAllRead) 41 - r.Delete("/{id}", n.deleteNotification) 34 + 35 + r.Group(func(r chi.Router) { 36 + r.Use(middleware.AuthMiddleware(n.oauth)) 37 + r.With(middleware.Paginate).Get("/", n.notificationsPage) 38 + r.Post("/{id}/read", n.markRead) 39 + r.Post("/read-all", n.markAllRead) 40 + r.Delete("/{id}", n.deleteNotification) 41 + }) 42 42 43 43 return r 44 44 } 45 45 46 46 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 47 - userDid := n.oauth.GetDid(r) 47 + user := n.oauth.GetUser(r) 48 48 49 49 page, ok := r.Context().Value("page").(pagination.Page) 50 50 if !ok { ··· 54 54 55 55 total, err := db.CountNotifications( 56 56 n.db, 57 - db.FilterEq("recipient_did", userDid), 57 + db.FilterEq("recipient_did", user.Did), 58 58 ) 59 59 if err != nil { 60 60 log.Println("failed to get total notifications:", err) ··· 65 65 notifications, err := db.GetNotificationsWithEntities( 66 66 n.db, 67 67 page, 68 - db.FilterEq("recipient_did", userDid), 68 + db.FilterEq("recipient_did", user.Did), 69 69 ) 70 70 if err != nil { 71 71 log.Println("failed to get notifications:", err) ··· 73 73 return 74 74 } 75 75 76 - err = n.db.MarkAllNotificationsRead(r.Context(), userDid) 76 + err = n.db.MarkAllNotificationsRead(r.Context(), user.Did) 77 77 if err != nil { 78 78 log.Println("failed to mark notifications as read:", err) 79 79 } 80 80 81 81 unreadCount := 0 82 82 83 - user := n.oauth.GetUser(r) 84 - if user == nil { 85 - http.Error(w, "Failed to get user", http.StatusInternalServerError) 86 - return 87 - } 88 - 89 - fmt.Println(n.pages.Notifications(w, pages.NotificationsParams{ 83 + n.pages.Notifications(w, pages.NotificationsParams{ 90 84 LoggedInUser: user, 91 85 Notifications: notifications, 92 86 UnreadCount: unreadCount, 93 87 Page: page, 94 88 Total: total, 95 - })) 89 + }) 96 90 } 97 91 98 92 func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 99 93 user := n.oauth.GetUser(r) 94 + if user == nil { 95 + return 96 + } 97 + 100 98 count, err := db.CountNotifications( 101 99 n.db, 102 100 db.FilterEq("recipient_did", user.Did),
-24
appview/oauth/client/oauth_client.go
··· 1 - package client 2 - 3 - import ( 4 - oauth "tangled.org/anirudh.fi/atproto-oauth" 5 - "tangled.org/anirudh.fi/atproto-oauth/helpers" 6 - ) 7 - 8 - type OAuthClient struct { 9 - *oauth.Client 10 - } 11 - 12 - func NewClient(clientId, clientJwk, redirectUri string) (*OAuthClient, error) { 13 - k, err := helpers.ParseJWKFromBytes([]byte(clientJwk)) 14 - if err != nil { 15 - return nil, err 16 - } 17 - 18 - cli, err := oauth.NewClient(oauth.ClientArgs{ 19 - ClientId: clientId, 20 - ClientJwk: k, 21 - RedirectUri: redirectUri, 22 - }) 23 - return &OAuthClient{cli}, err 24 - }
+2 -1
appview/oauth/consts.go
··· 1 1 package oauth 2 2 3 3 const ( 4 - SessionName = "appview-session" 4 + SessionName = "appview-session-v2" 5 5 SessionHandle = "handle" 6 6 SessionDid = "did" 7 + SessionId = "id" 7 8 SessionPds = "pds" 8 9 SessionAccessJwt = "accessJwt" 9 10 SessionRefreshJwt = "refreshJwt"
+65
appview/oauth/handler.go
··· 1 + package oauth 2 + 3 + import ( 4 + "encoding/json" 5 + "log" 6 + "net/http" 7 + 8 + "github.com/go-chi/chi/v5" 9 + "github.com/lestrrat-go/jwx/v2/jwk" 10 + ) 11 + 12 + func (o *OAuth) Router() http.Handler { 13 + r := chi.NewRouter() 14 + 15 + r.Get("/oauth/client-metadata.json", o.clientMetadata) 16 + r.Get("/oauth/jwks.json", o.jwks) 17 + r.Get("/oauth/callback", o.callback) 18 + return r 19 + } 20 + 21 + func (o *OAuth) clientMetadata(w http.ResponseWriter, r *http.Request) { 22 + doc := o.ClientApp.Config.ClientMetadata() 23 + doc.JWKSURI = &o.JwksUri 24 + 25 + w.Header().Set("Content-Type", "application/json") 26 + if err := json.NewEncoder(w).Encode(doc); err != nil { 27 + http.Error(w, err.Error(), http.StatusInternalServerError) 28 + return 29 + } 30 + } 31 + 32 + func (o *OAuth) jwks(w http.ResponseWriter, r *http.Request) { 33 + jwks := o.Config.OAuth.Jwks 34 + pubKey, err := pubKeyFromJwk(jwks) 35 + if err != nil { 36 + log.Printf("error parsing public key: %v", err) 37 + http.Error(w, err.Error(), http.StatusInternalServerError) 38 + return 39 + } 40 + 41 + response := map[string]any{ 42 + "keys": []jwk.Key{pubKey}, 43 + } 44 + 45 + w.Header().Set("Content-Type", "application/json") 46 + w.WriteHeader(http.StatusOK) 47 + json.NewEncoder(w).Encode(response) 48 + } 49 + 50 + func (o *OAuth) callback(w http.ResponseWriter, r *http.Request) { 51 + ctx := r.Context() 52 + 53 + sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 54 + if err != nil { 55 + http.Error(w, err.Error(), http.StatusInternalServerError) 56 + return 57 + } 58 + 59 + if err := o.SaveSession(w, r, sessData); err != nil { 60 + http.Error(w, err.Error(), http.StatusInternalServerError) 61 + return 62 + } 63 + 64 + http.Redirect(w, r, "/", http.StatusFound) 65 + }
-538
appview/oauth/handler/handler.go
··· 1 - package oauth 2 - 3 - import ( 4 - "bytes" 5 - "context" 6 - "encoding/json" 7 - "fmt" 8 - "log" 9 - "net/http" 10 - "net/url" 11 - "slices" 12 - "strings" 13 - "time" 14 - 15 - "github.com/go-chi/chi/v5" 16 - "github.com/gorilla/sessions" 17 - "github.com/lestrrat-go/jwx/v2/jwk" 18 - "github.com/posthog/posthog-go" 19 - "tangled.org/anirudh.fi/atproto-oauth/helpers" 20 - tangled "tangled.org/core/api/tangled" 21 - sessioncache "tangled.org/core/appview/cache/session" 22 - "tangled.org/core/appview/config" 23 - "tangled.org/core/appview/db" 24 - "tangled.org/core/appview/middleware" 25 - "tangled.org/core/appview/oauth" 26 - "tangled.org/core/appview/oauth/client" 27 - "tangled.org/core/appview/pages" 28 - "tangled.org/core/consts" 29 - "tangled.org/core/idresolver" 30 - "tangled.org/core/rbac" 31 - "tangled.org/core/tid" 32 - ) 33 - 34 - const ( 35 - oauthScope = "atproto transition:generic" 36 - ) 37 - 38 - type OAuthHandler struct { 39 - config *config.Config 40 - pages *pages.Pages 41 - idResolver *idresolver.Resolver 42 - sess *sessioncache.SessionStore 43 - db *db.DB 44 - store *sessions.CookieStore 45 - oauth *oauth.OAuth 46 - enforcer *rbac.Enforcer 47 - posthog posthog.Client 48 - } 49 - 50 - func New( 51 - config *config.Config, 52 - pages *pages.Pages, 53 - idResolver *idresolver.Resolver, 54 - db *db.DB, 55 - sess *sessioncache.SessionStore, 56 - store *sessions.CookieStore, 57 - oauth *oauth.OAuth, 58 - enforcer *rbac.Enforcer, 59 - posthog posthog.Client, 60 - ) *OAuthHandler { 61 - return &OAuthHandler{ 62 - config: config, 63 - pages: pages, 64 - idResolver: idResolver, 65 - db: db, 66 - sess: sess, 67 - store: store, 68 - oauth: oauth, 69 - enforcer: enforcer, 70 - posthog: posthog, 71 - } 72 - } 73 - 74 - func (o *OAuthHandler) Router() http.Handler { 75 - r := chi.NewRouter() 76 - 77 - r.Get("/login", o.login) 78 - r.Post("/login", o.login) 79 - 80 - r.With(middleware.AuthMiddleware(o.oauth)).Post("/logout", o.logout) 81 - 82 - r.Get("/oauth/client-metadata.json", o.clientMetadata) 83 - r.Get("/oauth/jwks.json", o.jwks) 84 - r.Get("/oauth/callback", o.callback) 85 - return r 86 - } 87 - 88 - func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) { 89 - w.Header().Set("Content-Type", "application/json") 90 - w.WriteHeader(http.StatusOK) 91 - json.NewEncoder(w).Encode(o.oauth.ClientMetadata()) 92 - } 93 - 94 - func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) { 95 - jwks := o.config.OAuth.Jwks 96 - pubKey, err := pubKeyFromJwk(jwks) 97 - if err != nil { 98 - log.Printf("error parsing public key: %v", err) 99 - http.Error(w, err.Error(), http.StatusInternalServerError) 100 - return 101 - } 102 - 103 - response := helpers.CreateJwksResponseObject(pubKey) 104 - 105 - w.Header().Set("Content-Type", "application/json") 106 - w.WriteHeader(http.StatusOK) 107 - json.NewEncoder(w).Encode(response) 108 - } 109 - 110 - func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { 111 - switch r.Method { 112 - case http.MethodGet: 113 - returnURL := r.URL.Query().Get("return_url") 114 - o.pages.Login(w, pages.LoginParams{ 115 - ReturnUrl: returnURL, 116 - }) 117 - case http.MethodPost: 118 - handle := r.FormValue("handle") 119 - 120 - // when users copy their handle from bsky.app, it tends to have these characters around it: 121 - // 122 - // @nelind.dk: 123 - // \u202a ensures that the handle is always rendered left to right and 124 - // \u202c reverts that so the rest of the page renders however it should 125 - handle = strings.TrimPrefix(handle, "\u202a") 126 - handle = strings.TrimSuffix(handle, "\u202c") 127 - 128 - // `@` is harmless 129 - handle = strings.TrimPrefix(handle, "@") 130 - 131 - // basic handle validation 132 - if !strings.Contains(handle, ".") { 133 - log.Println("invalid handle format", "raw", handle) 134 - o.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle. Did you mean %s.bsky.social?", handle, handle)) 135 - return 136 - } 137 - 138 - resolved, err := o.idResolver.ResolveIdent(r.Context(), handle) 139 - if err != nil { 140 - log.Println("failed to resolve handle:", err) 141 - o.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 142 - return 143 - } 144 - self := o.oauth.ClientMetadata() 145 - oauthClient, err := client.NewClient( 146 - self.ClientID, 147 - o.config.OAuth.Jwks, 148 - self.RedirectURIs[0], 149 - ) 150 - 151 - if err != nil { 152 - log.Println("failed to create oauth client:", err) 153 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 154 - return 155 - } 156 - 157 - authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint()) 158 - if err != nil { 159 - log.Println("failed to resolve auth server:", err) 160 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 161 - return 162 - } 163 - 164 - authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer) 165 - if err != nil { 166 - log.Println("failed to fetch auth server metadata:", err) 167 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 168 - return 169 - } 170 - 171 - dpopKey, err := helpers.GenerateKey(nil) 172 - if err != nil { 173 - log.Println("failed to generate dpop key:", err) 174 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 175 - return 176 - } 177 - 178 - dpopKeyJson, err := json.Marshal(dpopKey) 179 - if err != nil { 180 - log.Println("failed to marshal dpop key:", err) 181 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 182 - return 183 - } 184 - 185 - parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey) 186 - if err != nil { 187 - log.Println("failed to send par auth request:", err) 188 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 189 - return 190 - } 191 - 192 - err = o.sess.SaveRequest(r.Context(), sessioncache.OAuthRequest{ 193 - Did: resolved.DID.String(), 194 - PdsUrl: resolved.PDSEndpoint(), 195 - Handle: handle, 196 - AuthserverIss: authMeta.Issuer, 197 - PkceVerifier: parResp.PkceVerifier, 198 - DpopAuthserverNonce: parResp.DpopAuthserverNonce, 199 - DpopPrivateJwk: string(dpopKeyJson), 200 - State: parResp.State, 201 - ReturnUrl: r.FormValue("return_url"), 202 - }) 203 - if err != nil { 204 - log.Println("failed to save oauth request:", err) 205 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 206 - return 207 - } 208 - 209 - u, _ := url.Parse(authMeta.AuthorizationEndpoint) 210 - query := url.Values{} 211 - query.Add("client_id", self.ClientID) 212 - query.Add("request_uri", parResp.RequestUri) 213 - u.RawQuery = query.Encode() 214 - o.pages.HxRedirect(w, u.String()) 215 - } 216 - } 217 - 218 - func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { 219 - state := r.FormValue("state") 220 - 221 - oauthRequest, err := o.sess.GetRequestByState(r.Context(), state) 222 - if err != nil { 223 - log.Println("failed to get oauth request:", err) 224 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 225 - return 226 - } 227 - 228 - defer func() { 229 - err := o.sess.DeleteRequestByState(r.Context(), state) 230 - if err != nil { 231 - log.Println("failed to delete oauth request for state:", state, err) 232 - } 233 - }() 234 - 235 - error := r.FormValue("error") 236 - errorDescription := r.FormValue("error_description") 237 - if error != "" || errorDescription != "" { 238 - log.Printf("error: %s, %s", error, errorDescription) 239 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 240 - return 241 - } 242 - 243 - code := r.FormValue("code") 244 - if code == "" { 245 - log.Println("missing code for state: ", state) 246 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 247 - return 248 - } 249 - 250 - iss := r.FormValue("iss") 251 - if iss == "" { 252 - log.Println("missing iss for state: ", state) 253 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 254 - return 255 - } 256 - 257 - if iss != oauthRequest.AuthserverIss { 258 - log.Println("mismatched iss:", iss, "!=", oauthRequest.AuthserverIss, "for state:", state) 259 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 260 - return 261 - } 262 - 263 - self := o.oauth.ClientMetadata() 264 - 265 - oauthClient, err := client.NewClient( 266 - self.ClientID, 267 - o.config.OAuth.Jwks, 268 - self.RedirectURIs[0], 269 - ) 270 - 271 - if err != nil { 272 - log.Println("failed to create oauth client:", err) 273 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 274 - return 275 - } 276 - 277 - jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk)) 278 - if err != nil { 279 - log.Println("failed to parse jwk:", err) 280 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 281 - return 282 - } 283 - 284 - tokenResp, err := oauthClient.InitialTokenRequest( 285 - r.Context(), 286 - code, 287 - oauthRequest.AuthserverIss, 288 - oauthRequest.PkceVerifier, 289 - oauthRequest.DpopAuthserverNonce, 290 - jwk, 291 - ) 292 - if err != nil { 293 - log.Println("failed to get token:", err) 294 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 295 - return 296 - } 297 - 298 - if tokenResp.Scope != oauthScope { 299 - log.Println("scope doesn't match:", tokenResp.Scope) 300 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 301 - return 302 - } 303 - 304 - err = o.oauth.SaveSession(w, r, *oauthRequest, tokenResp) 305 - if err != nil { 306 - log.Println("failed to save session:", err) 307 - o.pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 308 - return 309 - } 310 - 311 - log.Println("session saved successfully") 312 - go o.addToDefaultKnot(oauthRequest.Did) 313 - go o.addToDefaultSpindle(oauthRequest.Did) 314 - 315 - if !o.config.Core.Dev { 316 - err = o.posthog.Enqueue(posthog.Capture{ 317 - DistinctId: oauthRequest.Did, 318 - Event: "signin", 319 - }) 320 - if err != nil { 321 - log.Println("failed to enqueue posthog event:", err) 322 - } 323 - } 324 - 325 - returnUrl := oauthRequest.ReturnUrl 326 - if returnUrl == "" { 327 - returnUrl = "/" 328 - } 329 - 330 - http.Redirect(w, r, returnUrl, http.StatusFound) 331 - } 332 - 333 - func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) { 334 - err := o.oauth.ClearSession(r, w) 335 - if err != nil { 336 - log.Println("failed to clear session:", err) 337 - http.Redirect(w, r, "/", http.StatusFound) 338 - return 339 - } 340 - 341 - log.Println("session cleared successfully") 342 - o.pages.HxRedirect(w, "/login") 343 - } 344 - 345 - func pubKeyFromJwk(jwks string) (jwk.Key, error) { 346 - k, err := helpers.ParseJWKFromBytes([]byte(jwks)) 347 - if err != nil { 348 - return nil, err 349 - } 350 - pubKey, err := k.PublicKey() 351 - if err != nil { 352 - return nil, err 353 - } 354 - return pubKey, nil 355 - } 356 - 357 - func (o *OAuthHandler) addToDefaultSpindle(did string) { 358 - // use the tangled.sh app password to get an accessJwt 359 - // and create an sh.tangled.spindle.member record with that 360 - spindleMembers, err := db.GetSpindleMembers( 361 - o.db, 362 - db.FilterEq("instance", "spindle.tangled.sh"), 363 - db.FilterEq("subject", did), 364 - ) 365 - if err != nil { 366 - log.Printf("failed to get spindle members for did %s: %v", did, err) 367 - return 368 - } 369 - 370 - if len(spindleMembers) != 0 { 371 - log.Printf("did %s is already a member of the default spindle", did) 372 - return 373 - } 374 - 375 - log.Printf("adding %s to default spindle", did) 376 - session, err := o.createAppPasswordSession(o.config.Core.AppPassword, consts.TangledDid) 377 - if err != nil { 378 - log.Printf("failed to create session: %s", err) 379 - return 380 - } 381 - 382 - record := tangled.SpindleMember{ 383 - LexiconTypeID: "sh.tangled.spindle.member", 384 - Subject: did, 385 - Instance: consts.DefaultSpindle, 386 - CreatedAt: time.Now().Format(time.RFC3339), 387 - } 388 - 389 - if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { 390 - log.Printf("failed to add member to default spindle: %s", err) 391 - return 392 - } 393 - 394 - log.Printf("successfully added %s to default spindle", did) 395 - } 396 - 397 - func (o *OAuthHandler) addToDefaultKnot(did string) { 398 - // use the tangled.sh app password to get an accessJwt 399 - // and create an sh.tangled.spindle.member record with that 400 - 401 - allKnots, err := o.enforcer.GetKnotsForUser(did) 402 - if err != nil { 403 - log.Printf("failed to get knot members for did %s: %v", did, err) 404 - return 405 - } 406 - 407 - if slices.Contains(allKnots, consts.DefaultKnot) { 408 - log.Printf("did %s is already a member of the default knot", did) 409 - return 410 - } 411 - 412 - log.Printf("adding %s to default knot", did) 413 - session, err := o.createAppPasswordSession(o.config.Core.TmpAltAppPassword, consts.IcyDid) 414 - if err != nil { 415 - log.Printf("failed to create session: %s", err) 416 - return 417 - } 418 - 419 - record := tangled.KnotMember{ 420 - LexiconTypeID: "sh.tangled.knot.member", 421 - Subject: did, 422 - Domain: consts.DefaultKnot, 423 - CreatedAt: time.Now().Format(time.RFC3339), 424 - } 425 - 426 - if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { 427 - log.Printf("failed to add member to default knot: %s", err) 428 - return 429 - } 430 - 431 - if err := o.enforcer.AddKnotMember(consts.DefaultKnot, did); err != nil { 432 - log.Printf("failed to set up enforcer rules: %s", err) 433 - return 434 - } 435 - 436 - log.Printf("successfully added %s to default Knot", did) 437 - } 438 - 439 - // create a session using apppasswords 440 - type session struct { 441 - AccessJwt string `json:"accessJwt"` 442 - PdsEndpoint string 443 - Did string 444 - } 445 - 446 - func (o *OAuthHandler) createAppPasswordSession(appPassword, did string) (*session, error) { 447 - if appPassword == "" { 448 - return nil, fmt.Errorf("no app password configured, skipping member addition") 449 - } 450 - 451 - resolved, err := o.idResolver.ResolveIdent(context.Background(), did) 452 - if err != nil { 453 - return nil, fmt.Errorf("failed to resolve tangled.sh DID %s: %v", did, err) 454 - } 455 - 456 - pdsEndpoint := resolved.PDSEndpoint() 457 - if pdsEndpoint == "" { 458 - return nil, fmt.Errorf("no PDS endpoint found for tangled.sh DID %s", did) 459 - } 460 - 461 - sessionPayload := map[string]string{ 462 - "identifier": did, 463 - "password": appPassword, 464 - } 465 - sessionBytes, err := json.Marshal(sessionPayload) 466 - if err != nil { 467 - return nil, fmt.Errorf("failed to marshal session payload: %v", err) 468 - } 469 - 470 - sessionURL := pdsEndpoint + "/xrpc/com.atproto.server.createSession" 471 - sessionReq, err := http.NewRequestWithContext(context.Background(), "POST", sessionURL, bytes.NewBuffer(sessionBytes)) 472 - if err != nil { 473 - return nil, fmt.Errorf("failed to create session request: %v", err) 474 - } 475 - sessionReq.Header.Set("Content-Type", "application/json") 476 - 477 - client := &http.Client{Timeout: 30 * time.Second} 478 - sessionResp, err := client.Do(sessionReq) 479 - if err != nil { 480 - return nil, fmt.Errorf("failed to create session: %v", err) 481 - } 482 - defer sessionResp.Body.Close() 483 - 484 - if sessionResp.StatusCode != http.StatusOK { 485 - return nil, fmt.Errorf("failed to create session: HTTP %d", sessionResp.StatusCode) 486 - } 487 - 488 - var session session 489 - if err := json.NewDecoder(sessionResp.Body).Decode(&session); err != nil { 490 - return nil, fmt.Errorf("failed to decode session response: %v", err) 491 - } 492 - 493 - session.PdsEndpoint = pdsEndpoint 494 - session.Did = did 495 - 496 - return &session, nil 497 - } 498 - 499 - func (s *session) putRecord(record any, collection string) error { 500 - recordBytes, err := json.Marshal(record) 501 - if err != nil { 502 - return fmt.Errorf("failed to marshal knot member record: %w", err) 503 - } 504 - 505 - payload := map[string]any{ 506 - "repo": s.Did, 507 - "collection": collection, 508 - "rkey": tid.TID(), 509 - "record": json.RawMessage(recordBytes), 510 - } 511 - 512 - payloadBytes, err := json.Marshal(payload) 513 - if err != nil { 514 - return fmt.Errorf("failed to marshal request payload: %w", err) 515 - } 516 - 517 - url := s.PdsEndpoint + "/xrpc/com.atproto.repo.putRecord" 518 - req, err := http.NewRequestWithContext(context.Background(), "POST", url, bytes.NewBuffer(payloadBytes)) 519 - if err != nil { 520 - return fmt.Errorf("failed to create HTTP request: %w", err) 521 - } 522 - 523 - req.Header.Set("Content-Type", "application/json") 524 - req.Header.Set("Authorization", "Bearer "+s.AccessJwt) 525 - 526 - client := &http.Client{Timeout: 30 * time.Second} 527 - resp, err := client.Do(req) 528 - if err != nil { 529 - return fmt.Errorf("failed to add user to default service: %w", err) 530 - } 531 - defer resp.Body.Close() 532 - 533 - if resp.StatusCode != http.StatusOK { 534 - return fmt.Errorf("failed to add user to default service: HTTP %d", resp.StatusCode) 535 - } 536 - 537 - return nil 538 - }
+107 -202
appview/oauth/oauth.go
··· 1 1 package oauth 2 2 3 3 import ( 4 + "errors" 4 5 "fmt" 5 - "log" 6 6 "net/http" 7 - "net/url" 8 7 "time" 9 8 10 - indigo_xrpc "github.com/bluesky-social/indigo/xrpc" 9 + comatproto "github.com/bluesky-social/indigo/api/atproto" 10 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 + atpclient "github.com/bluesky-social/indigo/atproto/client" 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + xrpc "github.com/bluesky-social/indigo/xrpc" 11 14 "github.com/gorilla/sessions" 12 - oauth "tangled.org/anirudh.fi/atproto-oauth" 13 - "tangled.org/anirudh.fi/atproto-oauth/helpers" 14 - sessioncache "tangled.org/core/appview/cache/session" 15 + "github.com/lestrrat-go/jwx/v2/jwk" 15 16 "tangled.org/core/appview/config" 16 - "tangled.org/core/appview/oauth/client" 17 - xrpc "tangled.org/core/appview/xrpcclient" 18 17 ) 19 18 20 - type OAuth struct { 21 - store *sessions.CookieStore 22 - config *config.Config 23 - sess *sessioncache.SessionStore 24 - } 19 + func New(config *config.Config) (*OAuth, error) { 20 + 21 + var oauthConfig oauth.ClientConfig 22 + var clientUri string 25 23 26 - func NewOAuth(config *config.Config, sess *sessioncache.SessionStore) *OAuth { 27 - return &OAuth{ 28 - store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)), 29 - config: config, 30 - sess: sess, 24 + if config.Core.Dev { 25 + clientUri = "http://127.0.0.1:3000" 26 + callbackUri := clientUri + "/oauth/callback" 27 + oauthConfig = oauth.NewLocalhostConfig(callbackUri, []string{"atproto", "transition:generic"}) 28 + } else { 29 + clientUri = config.Core.AppviewHost 30 + clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri) 31 + callbackUri := clientUri + "/oauth/callback" 32 + oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"}) 31 33 } 34 + 35 + jwksUri := clientUri + "/oauth/jwks.json" 36 + 37 + authStore, err := NewRedisStore(config.Redis.ToURL()) 38 + if err != nil { 39 + return nil, err 40 + } 41 + 42 + sessStore := sessions.NewCookieStore([]byte(config.Core.CookieSecret)) 43 + 44 + return &OAuth{ 45 + ClientApp: oauth.NewClientApp(&oauthConfig, authStore), 46 + Config: config, 47 + SessStore: sessStore, 48 + JwksUri: jwksUri, 49 + }, nil 32 50 } 33 51 34 - func (o *OAuth) Stores() *sessions.CookieStore { 35 - return o.store 52 + type OAuth struct { 53 + ClientApp *oauth.ClientApp 54 + SessStore *sessions.CookieStore 55 + Config *config.Config 56 + JwksUri string 36 57 } 37 58 38 - func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq sessioncache.OAuthRequest, oresp *oauth.TokenResponse) error { 59 + func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error { 39 60 // first we save the did in the user session 40 - userSession, err := o.store.Get(r, SessionName) 61 + userSession, err := o.SessStore.Get(r, SessionName) 41 62 if err != nil { 42 63 return err 43 64 } 44 65 45 - userSession.Values[SessionDid] = oreq.Did 46 - userSession.Values[SessionHandle] = oreq.Handle 47 - userSession.Values[SessionPds] = oreq.PdsUrl 66 + userSession.Values[SessionDid] = sessData.AccountDID.String() 67 + userSession.Values[SessionPds] = sessData.HostURL 68 + userSession.Values[SessionId] = sessData.SessionID 48 69 userSession.Values[SessionAuthenticated] = true 49 - err = userSession.Save(r, w) 70 + return userSession.Save(r, w) 71 + } 72 + 73 + func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) { 74 + userSession, err := o.SessStore.Get(r, SessionName) 50 75 if err != nil { 51 - return fmt.Errorf("error saving user session: %w", err) 76 + return nil, fmt.Errorf("error getting user session: %w", err) 52 77 } 53 - 54 - // then save the whole thing in the db 55 - session := sessioncache.OAuthSession{ 56 - Did: oreq.Did, 57 - Handle: oreq.Handle, 58 - PdsUrl: oreq.PdsUrl, 59 - DpopAuthserverNonce: oreq.DpopAuthserverNonce, 60 - AuthServerIss: oreq.AuthserverIss, 61 - DpopPrivateJwk: oreq.DpopPrivateJwk, 62 - AccessJwt: oresp.AccessToken, 63 - RefreshJwt: oresp.RefreshToken, 64 - Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339), 78 + if userSession.IsNew { 79 + return nil, fmt.Errorf("no session available for user") 65 80 } 66 81 67 - return o.sess.SaveSession(r.Context(), session) 68 - } 69 - 70 - func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error { 71 - userSession, err := o.store.Get(r, SessionName) 72 - if err != nil || userSession.IsNew { 73 - return fmt.Errorf("error getting user session (or new session?): %w", err) 82 + d := userSession.Values[SessionDid].(string) 83 + sessDid, err := syntax.ParseDID(d) 84 + if err != nil { 85 + return nil, fmt.Errorf("malformed DID in session cookie '%s': %w", d, err) 74 86 } 75 87 76 - did := userSession.Values[SessionDid].(string) 88 + sessId := userSession.Values[SessionId].(string) 77 89 78 - err = o.sess.DeleteSession(r.Context(), did) 90 + clientSess, err := o.ClientApp.ResumeSession(r.Context(), sessDid, sessId) 79 91 if err != nil { 80 - return fmt.Errorf("error deleting oauth session: %w", err) 92 + return nil, fmt.Errorf("failed to resume session: %w", err) 81 93 } 82 94 83 - userSession.Options.MaxAge = -1 84 - 85 - return userSession.Save(r, w) 95 + return clientSess, nil 86 96 } 87 97 88 - func (o *OAuth) GetSession(r *http.Request) (*sessioncache.OAuthSession, bool, error) { 89 - userSession, err := o.store.Get(r, SessionName) 90 - if err != nil || userSession.IsNew { 91 - return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err) 98 + func (o *OAuth) DeleteSession(w http.ResponseWriter, r *http.Request) error { 99 + userSession, err := o.SessStore.Get(r, SessionName) 100 + if err != nil { 101 + return fmt.Errorf("error getting user session: %w", err) 92 102 } 93 - 94 - did := userSession.Values[SessionDid].(string) 95 - auth := userSession.Values[SessionAuthenticated].(bool) 96 - 97 - session, err := o.sess.GetSession(r.Context(), did) 98 - if err != nil { 99 - return nil, false, fmt.Errorf("error getting oauth session: %w", err) 103 + if userSession.IsNew { 104 + return fmt.Errorf("no session available for user") 100 105 } 101 106 102 - expiry, err := time.Parse(time.RFC3339, session.Expiry) 107 + d := userSession.Values[SessionDid].(string) 108 + sessDid, err := syntax.ParseDID(d) 103 109 if err != nil { 104 - return nil, false, fmt.Errorf("error parsing expiry time: %w", err) 110 + return fmt.Errorf("malformed DID in session cookie '%s': %w", d, err) 105 111 } 106 - if time.Until(expiry) <= 5*time.Minute { 107 - privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) 108 - if err != nil { 109 - return nil, false, err 110 - } 111 112 112 - self := o.ClientMetadata() 113 + sessId := userSession.Values[SessionId].(string) 113 114 114 - oauthClient, err := client.NewClient( 115 - self.ClientID, 116 - o.config.OAuth.Jwks, 117 - self.RedirectURIs[0], 118 - ) 115 + // delete the session 116 + err1 := o.ClientApp.Logout(r.Context(), sessDid, sessId) 119 117 120 - if err != nil { 121 - return nil, false, err 122 - } 118 + // remove the cookie 119 + userSession.Options.MaxAge = -1 120 + err2 := o.SessStore.Save(r, w, userSession) 123 121 124 - resp, err := oauthClient.RefreshTokenRequest(r.Context(), session.RefreshJwt, session.AuthServerIss, session.DpopAuthserverNonce, privateJwk) 125 - if err != nil { 126 - return nil, false, err 127 - } 122 + return errors.Join(err1, err2) 123 + } 128 124 129 - newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339) 130 - err = o.sess.RefreshSession(r.Context(), did, resp.AccessToken, resp.RefreshToken, newExpiry) 131 - if err != nil { 132 - return nil, false, fmt.Errorf("error refreshing oauth session: %w", err) 133 - } 134 - 135 - // update the current session 136 - session.AccessJwt = resp.AccessToken 137 - session.RefreshJwt = resp.RefreshToken 138 - session.DpopAuthserverNonce = resp.DpopAuthserverNonce 139 - session.Expiry = newExpiry 125 + func pubKeyFromJwk(jwks string) (jwk.Key, error) { 126 + k, err := jwk.ParseKey([]byte(jwks)) 127 + if err != nil { 128 + return nil, err 129 + } 130 + pubKey, err := k.PublicKey() 131 + if err != nil { 132 + return nil, err 140 133 } 141 - 142 - return session, auth, nil 134 + return pubKey, nil 143 135 } 144 136 145 137 type User struct { 146 - Handle string 147 - Did string 148 - Pds string 138 + Did string 139 + Pds string 149 140 } 150 141 151 - func (a *OAuth) GetUser(r *http.Request) *User { 152 - clientSession, err := a.store.Get(r, SessionName) 142 + func (o *OAuth) GetUser(r *http.Request) *User { 143 + sess, err := o.SessStore.Get(r, SessionName) 153 144 154 - if err != nil || clientSession.IsNew { 145 + if err != nil || sess.IsNew { 155 146 return nil 156 147 } 157 148 158 149 return &User{ 159 - Handle: clientSession.Values[SessionHandle].(string), 160 - Did: clientSession.Values[SessionDid].(string), 161 - Pds: clientSession.Values[SessionPds].(string), 150 + Did: sess.Values[SessionDid].(string), 151 + Pds: sess.Values[SessionPds].(string), 162 152 } 163 153 } 164 154 165 - func (a *OAuth) GetDid(r *http.Request) string { 166 - clientSession, err := a.store.Get(r, SessionName) 167 - 168 - if err != nil || clientSession.IsNew { 169 - return "" 155 + func (o *OAuth) GetDid(r *http.Request) string { 156 + if u := o.GetUser(r); u != nil { 157 + return u.Did 170 158 } 171 159 172 - return clientSession.Values[SessionDid].(string) 160 + return "" 173 161 } 174 162 175 - func (o *OAuth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) { 176 - session, auth, err := o.GetSession(r) 163 + func (o *OAuth) AuthorizedClient(r *http.Request) (*atpclient.APIClient, error) { 164 + session, err := o.ResumeSession(r) 177 165 if err != nil { 178 166 return nil, fmt.Errorf("error getting session: %w", err) 179 167 } 180 - if !auth { 181 - return nil, fmt.Errorf("not authorized") 182 - } 183 - 184 - client := &oauth.XrpcClient{ 185 - OnDpopPdsNonceChanged: func(did, newNonce string) { 186 - err := o.sess.UpdateNonce(r.Context(), did, newNonce) 187 - if err != nil { 188 - log.Printf("error updating dpop pds nonce: %v", err) 189 - } 190 - }, 191 - } 192 - 193 - privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) 194 - if err != nil { 195 - return nil, fmt.Errorf("error parsing private jwk: %w", err) 196 - } 197 - 198 - xrpcClient := xrpc.NewClient(client, &oauth.XrpcAuthedRequestArgs{ 199 - Did: session.Did, 200 - PdsUrl: session.PdsUrl, 201 - DpopPdsNonce: session.PdsUrl, 202 - AccessToken: session.AccessJwt, 203 - Issuer: session.AuthServerIss, 204 - DpopPrivateJwk: privateJwk, 205 - }) 206 - 207 - return xrpcClient, nil 168 + return session.APIClient(), nil 208 169 } 209 170 210 - // use this to create a client to communicate with knots or spindles 211 - // 212 171 // this is a higher level abstraction on ServerGetServiceAuth 213 172 type ServiceClientOpts struct { 214 173 service string ··· 259 218 return scheme + s.service 260 219 } 261 220 262 - func (o *OAuth) ServiceClient(r *http.Request, os ...ServiceClientOpt) (*indigo_xrpc.Client, error) { 221 + func (o *OAuth) ServiceClient(r *http.Request, os ...ServiceClientOpt) (*xrpc.Client, error) { 263 222 opts := ServiceClientOpts{} 264 223 for _, o := range os { 265 224 o(&opts) 266 225 } 267 226 268 - authorizedClient, err := o.AuthorizedClient(r) 227 + client, err := o.AuthorizedClient(r) 269 228 if err != nil { 270 229 return nil, err 271 230 } ··· 276 235 opts.exp = sixty 277 236 } 278 237 279 - resp, err := authorizedClient.ServerGetServiceAuth(r.Context(), opts.Audience(), opts.exp, opts.lxm) 238 + resp, err := comatproto.ServerGetServiceAuth(r.Context(), client, opts.Audience(), opts.exp, opts.lxm) 280 239 if err != nil { 281 240 return nil, err 282 241 } 283 242 284 - return &indigo_xrpc.Client{ 285 - Auth: &indigo_xrpc.AuthInfo{ 243 + return &xrpc.Client{ 244 + Auth: &xrpc.AuthInfo{ 286 245 AccessJwt: resp.Token, 287 246 }, 288 247 Host: opts.Host(), ··· 291 250 }, 292 251 }, nil 293 252 } 294 - 295 - type ClientMetadata struct { 296 - ClientID string `json:"client_id"` 297 - ClientName string `json:"client_name"` 298 - SubjectType string `json:"subject_type"` 299 - ClientURI string `json:"client_uri"` 300 - RedirectURIs []string `json:"redirect_uris"` 301 - GrantTypes []string `json:"grant_types"` 302 - ResponseTypes []string `json:"response_types"` 303 - ApplicationType string `json:"application_type"` 304 - DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"` 305 - JwksURI string `json:"jwks_uri"` 306 - Scope string `json:"scope"` 307 - TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` 308 - TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` 309 - } 310 - 311 - func (o *OAuth) ClientMetadata() ClientMetadata { 312 - makeRedirectURIs := func(c string) []string { 313 - return []string{fmt.Sprintf("%s/oauth/callback", c)} 314 - } 315 - 316 - clientURI := o.config.Core.AppviewHost 317 - clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI) 318 - redirectURIs := makeRedirectURIs(clientURI) 319 - 320 - if o.config.Core.Dev { 321 - clientURI = "http://127.0.0.1:3000" 322 - redirectURIs = makeRedirectURIs(clientURI) 323 - 324 - query := url.Values{} 325 - query.Add("redirect_uri", redirectURIs[0]) 326 - query.Add("scope", "atproto transition:generic") 327 - clientID = fmt.Sprintf("http://localhost?%s", query.Encode()) 328 - } 329 - 330 - jwksURI := fmt.Sprintf("%s/oauth/jwks.json", clientURI) 331 - 332 - return ClientMetadata{ 333 - ClientID: clientID, 334 - ClientName: "Tangled", 335 - SubjectType: "public", 336 - ClientURI: clientURI, 337 - RedirectURIs: redirectURIs, 338 - GrantTypes: []string{"authorization_code", "refresh_token"}, 339 - ResponseTypes: []string{"code"}, 340 - ApplicationType: "web", 341 - DpopBoundAccessTokens: true, 342 - JwksURI: jwksURI, 343 - Scope: "atproto transition:generic", 344 - TokenEndpointAuthMethod: "private_key_jwt", 345 - TokenEndpointAuthSigningAlg: "ES256", 346 - } 347 - }
+147
appview/oauth/store.go
··· 1 + package oauth 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "time" 8 + 9 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/redis/go-redis/v9" 12 + ) 13 + 14 + // redis-backed implementation of ClientAuthStore. 15 + type RedisStore struct { 16 + client *redis.Client 17 + SessionTTL time.Duration 18 + AuthRequestTTL time.Duration 19 + } 20 + 21 + var _ oauth.ClientAuthStore = &RedisStore{} 22 + 23 + func NewRedisStore(redisURL string) (*RedisStore, error) { 24 + opts, err := redis.ParseURL(redisURL) 25 + if err != nil { 26 + return nil, fmt.Errorf("failed to parse redis URL: %w", err) 27 + } 28 + 29 + client := redis.NewClient(opts) 30 + 31 + // test the connection 32 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 33 + defer cancel() 34 + 35 + if err := client.Ping(ctx).Err(); err != nil { 36 + return nil, fmt.Errorf("failed to connect to redis: %w", err) 37 + } 38 + 39 + return &RedisStore{ 40 + client: client, 41 + SessionTTL: 30 * 24 * time.Hour, // 30 days 42 + AuthRequestTTL: 10 * time.Minute, // 10 minutes 43 + }, nil 44 + } 45 + 46 + func (r *RedisStore) Close() error { 47 + return r.client.Close() 48 + } 49 + 50 + func sessionKey(did syntax.DID, sessionID string) string { 51 + return fmt.Sprintf("oauth:session:%s:%s", did, sessionID) 52 + } 53 + 54 + func authRequestKey(state string) string { 55 + return fmt.Sprintf("oauth:auth_request:%s", state) 56 + } 57 + 58 + func (r *RedisStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { 59 + key := sessionKey(did, sessionID) 60 + data, err := r.client.Get(ctx, key).Bytes() 61 + if err == redis.Nil { 62 + return nil, fmt.Errorf("session not found: %s", did) 63 + } 64 + if err != nil { 65 + return nil, fmt.Errorf("failed to get session: %w", err) 66 + } 67 + 68 + var sess oauth.ClientSessionData 69 + if err := json.Unmarshal(data, &sess); err != nil { 70 + return nil, fmt.Errorf("failed to unmarshal session: %w", err) 71 + } 72 + 73 + return &sess, nil 74 + } 75 + 76 + func (r *RedisStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error { 77 + key := sessionKey(sess.AccountDID, sess.SessionID) 78 + 79 + data, err := json.Marshal(sess) 80 + if err != nil { 81 + return fmt.Errorf("failed to marshal session: %w", err) 82 + } 83 + 84 + if err := r.client.Set(ctx, key, data, r.SessionTTL).Err(); err != nil { 85 + return fmt.Errorf("failed to save session: %w", err) 86 + } 87 + 88 + return nil 89 + } 90 + 91 + func (r *RedisStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { 92 + key := sessionKey(did, sessionID) 93 + if err := r.client.Del(ctx, key).Err(); err != nil { 94 + return fmt.Errorf("failed to delete session: %w", err) 95 + } 96 + return nil 97 + } 98 + 99 + func (r *RedisStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { 100 + key := authRequestKey(state) 101 + data, err := r.client.Get(ctx, key).Bytes() 102 + if err == redis.Nil { 103 + return nil, fmt.Errorf("request info not found: %s", state) 104 + } 105 + if err != nil { 106 + return nil, fmt.Errorf("failed to get auth request: %w", err) 107 + } 108 + 109 + var req oauth.AuthRequestData 110 + if err := json.Unmarshal(data, &req); err != nil { 111 + return nil, fmt.Errorf("failed to unmarshal auth request: %w", err) 112 + } 113 + 114 + return &req, nil 115 + } 116 + 117 + func (r *RedisStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error { 118 + key := authRequestKey(info.State) 119 + 120 + // check if already exists (to match MemStore behavior) 121 + exists, err := r.client.Exists(ctx, key).Result() 122 + if err != nil { 123 + return fmt.Errorf("failed to check auth request existence: %w", err) 124 + } 125 + if exists > 0 { 126 + return fmt.Errorf("auth request already saved for state %s", info.State) 127 + } 128 + 129 + data, err := json.Marshal(info) 130 + if err != nil { 131 + return fmt.Errorf("failed to marshal auth request: %w", err) 132 + } 133 + 134 + if err := r.client.Set(ctx, key, data, r.AuthRequestTTL).Err(); err != nil { 135 + return fmt.Errorf("failed to save auth request: %w", err) 136 + } 137 + 138 + return nil 139 + } 140 + 141 + func (r *RedisStore) DeleteAuthRequestInfo(ctx context.Context, state string) error { 142 + key := authRequestKey(state) 143 + if err := r.client.Del(ctx, key).Err(); err != nil { 144 + return fmt.Errorf("failed to delete auth request: %w", err) 145 + } 146 + return nil 147 + }
+1 -1
appview/pages/templates/layouts/fragments/topbar.html
··· 51 51 <summary 52 52 class="cursor-pointer list-none flex items-center gap-1" 53 53 > 54 - {{ $user := didOrHandle .Did .Handle }} 54 + {{ $user := .Did }} 55 55 <img 56 56 src="{{ tinyAvatar $user }}" 57 57 alt=""
+1 -1
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 3 3 id="pull-comment-card-{{ .RoundNumber }}" 4 4 class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 5 <div class="text-sm text-gray-500 dark:text-gray-400"> 6 - {{ didOrHandle .LoggedInUser.Did .LoggedInUser.Handle }} 6 + {{ resolve .LoggedInUser.Did }} 7 7 </div> 8 8 <form 9 9 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment"
+1 -3
appview/pages/templates/user/settings/profile.html
··· 33 33 <div class="flex flex-wrap text-sm items-center gap-1 text-gray-500 dark:text-gray-400"> 34 34 <span>Handle</span> 35 35 </div> 36 - {{ if .LoggedInUser.Handle }} 37 36 <span class="font-bold"> 38 - @{{ .LoggedInUser.Handle }} 37 + {{ resolve .LoggedInUser.Did }} 39 38 </span> 40 - {{ end }} 41 39 </div> 42 40 </div> 43 41 <div class="flex items-center justify-between p-4">
+2 -1
appview/pipelines/pipelines.go
··· 48 48 ) *Pipelines { 49 49 logger := log.New("pipelines") 50 50 51 - return &Pipelines{oauth: oauth, 51 + return &Pipelines{ 52 + oauth: oauth, 52 53 repoResolver: repoResolver, 53 54 pages: pages, 54 55 idResolver: idResolver,
+6 -6
appview/pulls/pulls.go
··· 665 665 s.pages.Notice(w, "pull-comment", "Failed to create comment.") 666 666 return 667 667 } 668 - atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 668 + atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 669 669 Collection: tangled.RepoPullCommentNSID, 670 670 Repo: user.Did, 671 671 Rkey: tid.TID(), ··· 1142 1142 return 1143 1143 } 1144 1144 1145 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1145 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1146 1146 Collection: tangled.RepoPullNSID, 1147 1147 Repo: user.Did, 1148 1148 Rkey: rkey, ··· 1239 1239 } 1240 1240 writes = append(writes, &write) 1241 1241 } 1242 - _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 1242 + _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1243 1243 Repo: user.Did, 1244 1244 Writes: writes, 1245 1245 }) ··· 1770 1770 return 1771 1771 } 1772 1772 1773 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1773 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1774 1774 if err != nil { 1775 1775 // failed to get record 1776 1776 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1793 1793 } 1794 1794 } 1795 1795 1796 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1796 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1797 1797 Collection: tangled.RepoPullNSID, 1798 1798 Repo: user.Did, 1799 1799 Rkey: pull.Rkey, ··· 2065 2065 return 2066 2066 } 2067 2067 2068 - _, err = client.RepoApplyWrites(r.Context(), &comatproto.RepoApplyWrites_Input{ 2068 + _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 2069 2069 Repo: user.Did, 2070 2070 Writes: writes, 2071 2071 })
+11 -10
appview/repo/artifact.go
··· 10 10 "net/url" 11 11 "time" 12 12 13 - comatproto "github.com/bluesky-social/indigo/api/atproto" 14 - lexutil "github.com/bluesky-social/indigo/lex/util" 15 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 16 - "github.com/dustin/go-humanize" 17 - "github.com/go-chi/chi/v5" 18 - "github.com/go-git/go-git/v5/plumbing" 19 - "github.com/ipfs/go-cid" 20 13 "tangled.org/core/api/tangled" 21 14 "tangled.org/core/appview/db" 22 15 "tangled.org/core/appview/models" ··· 25 18 "tangled.org/core/appview/xrpcclient" 26 19 "tangled.org/core/tid" 27 20 "tangled.org/core/types" 21 + 22 + comatproto "github.com/bluesky-social/indigo/api/atproto" 23 + lexutil "github.com/bluesky-social/indigo/lex/util" 24 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 25 + "github.com/dustin/go-humanize" 26 + "github.com/go-chi/chi/v5" 27 + "github.com/go-git/go-git/v5/plumbing" 28 + "github.com/ipfs/go-cid" 28 29 ) 29 30 30 31 // TODO: proper statuses here on early exit ··· 60 61 return 61 62 } 62 63 63 - uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file) 64 + uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 64 65 if err != nil { 65 66 log.Println("failed to upload blob", err) 66 67 rp.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") ··· 72 73 rkey := tid.TID() 73 74 createdAt := time.Now() 74 75 75 - putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 76 + putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 76 77 Collection: tangled.RepoArtifactNSID, 77 78 Repo: user.Did, 78 79 Rkey: rkey, ··· 249 250 return 250 251 } 251 252 252 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 253 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 253 254 Collection: tangled.RepoArtifactNSID, 254 255 Repo: user.Did, 255 256 Rkey: artifact.Rkey,
+28 -35
appview/repo/repo.go
··· 17 17 "strings" 18 18 "time" 19 19 20 - comatproto "github.com/bluesky-social/indigo/api/atproto" 21 - lexutil "github.com/bluesky-social/indigo/lex/util" 22 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 23 20 "tangled.org/core/api/tangled" 24 21 "tangled.org/core/appview/commitverify" 25 22 "tangled.org/core/appview/config" ··· 40 37 "tangled.org/core/types" 41 38 "tangled.org/core/xrpc/serviceauth" 42 39 40 + comatproto "github.com/bluesky-social/indigo/api/atproto" 41 + atpclient "github.com/bluesky-social/indigo/atproto/client" 42 + "github.com/bluesky-social/indigo/atproto/syntax" 43 + lexutil "github.com/bluesky-social/indigo/lex/util" 44 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 43 45 securejoin "github.com/cyphar/filepath-securejoin" 44 46 "github.com/go-chi/chi/v5" 45 47 "github.com/go-git/go-git/v5/plumbing" 46 - 47 - "github.com/bluesky-social/indigo/atproto/syntax" 48 48 ) 49 49 50 50 type Repo struct { ··· 307 307 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 308 308 // 309 309 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 310 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 310 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 311 311 if err != nil { 312 312 // failed to get record 313 313 rp.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 314 314 return 315 315 } 316 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 316 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 317 317 Collection: tangled.RepoNSID, 318 318 Repo: newRepo.Did, 319 319 Rkey: newRepo.Rkey, ··· 863 863 user := rp.oauth.GetUser(r) 864 864 l := rp.logger.With("handler", "EditSpindle") 865 865 l = l.With("did", user.Did) 866 - l = l.With("handle", user.Handle) 867 866 868 867 errorId := "operation-error" 869 868 fail := func(msg string, err error) { ··· 916 915 return 917 916 } 918 917 919 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 918 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 920 919 if err != nil { 921 920 fail("Failed to update spindle, no record found on PDS.", err) 922 921 return 923 922 } 924 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 923 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 925 924 Collection: tangled.RepoNSID, 926 925 Repo: newRepo.Did, 927 926 Rkey: newRepo.Rkey, ··· 951 950 user := rp.oauth.GetUser(r) 952 951 l := rp.logger.With("handler", "AddLabel") 953 952 l = l.With("did", user.Did) 954 - l = l.With("handle", user.Handle) 955 953 956 954 f, err := rp.repoResolver.Resolve(r) 957 955 if err != nil { ··· 1020 1018 1021 1019 // emit a labelRecord 1022 1020 labelRecord := label.AsRecord() 1023 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1021 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1024 1022 Collection: tangled.LabelDefinitionNSID, 1025 1023 Repo: label.Did, 1026 1024 Rkey: label.Rkey, ··· 1043 1041 newRepo.Labels = append(newRepo.Labels, aturi) 1044 1042 repoRecord := newRepo.AsRecord() 1045 1043 1046 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 1044 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 1047 1045 if err != nil { 1048 1046 fail("Failed to update labels, no record found on PDS.", err) 1049 1047 return 1050 1048 } 1051 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1049 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1052 1050 Collection: tangled.RepoNSID, 1053 1051 Repo: newRepo.Did, 1054 1052 Rkey: newRepo.Rkey, ··· 1111 1109 user := rp.oauth.GetUser(r) 1112 1110 l := rp.logger.With("handler", "DeleteLabel") 1113 1111 l = l.With("did", user.Did) 1114 - l = l.With("handle", user.Handle) 1115 1112 1116 1113 f, err := rp.repoResolver.Resolve(r) 1117 1114 if err != nil { ··· 1141 1138 } 1142 1139 1143 1140 // delete label record from PDS 1144 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 1141 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 1145 1142 Collection: tangled.LabelDefinitionNSID, 1146 1143 Repo: label.Did, 1147 1144 Rkey: label.Rkey, ··· 1163 1160 newRepo.Labels = updated 1164 1161 repoRecord := newRepo.AsRecord() 1165 1162 1166 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 1163 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 1167 1164 if err != nil { 1168 1165 fail("Failed to update labels, no record found on PDS.", err) 1169 1166 return 1170 1167 } 1171 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1168 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1172 1169 Collection: tangled.RepoNSID, 1173 1170 Repo: newRepo.Did, 1174 1171 Rkey: newRepo.Rkey, ··· 1220 1217 user := rp.oauth.GetUser(r) 1221 1218 l := rp.logger.With("handler", "SubscribeLabel") 1222 1219 l = l.With("did", user.Did) 1223 - l = l.With("handle", user.Handle) 1224 1220 1225 1221 f, err := rp.repoResolver.Resolve(r) 1226 1222 if err != nil { ··· 1261 1257 return 1262 1258 } 1263 1259 1264 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey) 1260 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey) 1265 1261 if err != nil { 1266 1262 fail("Failed to update labels, no record found on PDS.", err) 1267 1263 return 1268 1264 } 1269 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1265 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1270 1266 Collection: tangled.RepoNSID, 1271 1267 Repo: newRepo.Did, 1272 1268 Rkey: newRepo.Rkey, ··· 1307 1303 user := rp.oauth.GetUser(r) 1308 1304 l := rp.logger.With("handler", "UnsubscribeLabel") 1309 1305 l = l.With("did", user.Did) 1310 - l = l.With("handle", user.Handle) 1311 1306 1312 1307 f, err := rp.repoResolver.Resolve(r) 1313 1308 if err != nil { ··· 1350 1345 return 1351 1346 } 1352 1347 1353 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey) 1348 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey) 1354 1349 if err != nil { 1355 1350 fail("Failed to update labels, no record found on PDS.", err) 1356 1351 return 1357 1352 } 1358 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1353 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1359 1354 Collection: tangled.RepoNSID, 1360 1355 Repo: newRepo.Did, 1361 1356 Rkey: newRepo.Rkey, ··· 1479 1474 user := rp.oauth.GetUser(r) 1480 1475 l := rp.logger.With("handler", "AddCollaborator") 1481 1476 l = l.With("did", user.Did) 1482 - l = l.With("handle", user.Handle) 1483 1477 1484 1478 f, err := rp.repoResolver.Resolve(r) 1485 1479 if err != nil { ··· 1526 1520 currentUser := rp.oauth.GetUser(r) 1527 1521 rkey := tid.TID() 1528 1522 createdAt := time.Now() 1529 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1523 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1530 1524 Collection: tangled.RepoCollaboratorNSID, 1531 1525 Repo: currentUser.Did, 1532 1526 Rkey: rkey, ··· 1617 1611 } 1618 1612 1619 1613 // remove record from pds 1620 - xrpcClient, err := rp.oauth.AuthorizedClient(r) 1614 + atpClient, err := rp.oauth.AuthorizedClient(r) 1621 1615 if err != nil { 1622 1616 log.Println("failed to get authorized client", err) 1623 1617 return 1624 1618 } 1625 - _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 1619 + _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 1626 1620 Collection: tangled.RepoNSID, 1627 1621 Repo: user.Did, 1628 1622 Rkey: f.Rkey, ··· 1764 1758 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 1765 1759 user := rp.oauth.GetUser(r) 1766 1760 l := rp.logger.With("handler", "Secrets") 1767 - l = l.With("handle", user.Handle) 1768 1761 l = l.With("did", user.Did) 1769 1762 1770 1763 f, err := rp.repoResolver.Resolve(r) ··· 2179 2172 } 2180 2173 record := repo.AsRecord() 2181 2174 2182 - xrpcClient, err := rp.oauth.AuthorizedClient(r) 2175 + atpClient, err := rp.oauth.AuthorizedClient(r) 2183 2176 if err != nil { 2184 2177 l.Error("failed to create xrpcclient", "err", err) 2185 2178 rp.pages.Notice(w, "repo", "Failed to fork repository.") 2186 2179 return 2187 2180 } 2188 2181 2189 - atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 2182 + atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 2190 2183 Collection: tangled.RepoNSID, 2191 2184 Repo: user.Did, 2192 2185 Rkey: rkey, ··· 2218 2211 rollback := func() { 2219 2212 err1 := tx.Rollback() 2220 2213 err2 := rp.enforcer.E.LoadPolicy() 2221 - err3 := rollbackRecord(context.Background(), aturi, xrpcClient) 2214 + err3 := rollbackRecord(context.Background(), aturi, atpClient) 2222 2215 2223 2216 // ignore txn complete errors, this is okay 2224 2217 if errors.Is(err1, sql.ErrTxDone) { ··· 2291 2284 aturi = "" 2292 2285 2293 2286 rp.notifier.NewRepo(r.Context(), repo) 2294 - rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 2287 + rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Did, forkName)) 2295 2288 } 2296 2289 } 2297 2290 2298 2291 // this is used to rollback changes made to the PDS 2299 2292 // 2300 2293 // it is a no-op if the provided ATURI is empty 2301 - func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 2294 + func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 2302 2295 if aturi == "" { 2303 2296 return nil 2304 2297 } ··· 2309 2302 repo := parsed.Authority().String() 2310 2303 rkey := parsed.RecordKey().String() 2311 2304 2312 - _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 2305 + _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 2313 2306 Collection: collection, 2314 2307 Repo: repo, 2315 2308 Rkey: rkey,
+2 -2
appview/settings/settings.go
··· 470 470 } 471 471 472 472 // store in pds too 473 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 473 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 474 474 Collection: tangled.PublicKeyNSID, 475 475 Repo: did, 476 476 Rkey: rkey, ··· 527 527 528 528 if rkey != "" { 529 529 // remove from pds too 530 - _, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 530 + _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 531 531 Collection: tangled.PublicKeyNSID, 532 532 Repo: did, 533 533 Rkey: rkey,
-2
appview/signup/signup.go
··· 20 20 "tangled.org/core/appview/models" 21 21 "tangled.org/core/appview/pages" 22 22 "tangled.org/core/appview/state/userutil" 23 - "tangled.org/core/appview/xrpcclient" 24 23 "tangled.org/core/idresolver" 25 24 ) 26 25 ··· 29 28 db *db.DB 30 29 cf *dns.Cloudflare 31 30 posthog posthog.Client 32 - xrpc *xrpcclient.Client 33 31 idResolver *idresolver.Resolver 34 32 pages *pages.Pages 35 33 l *slog.Logger
+5 -5
appview/spindles/spindles.go
··· 189 189 return 190 190 } 191 191 192 - ex, _ := client.RepoGetRecord(r.Context(), "", tangled.SpindleNSID, user.Did, instance) 192 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance) 193 193 var exCid *string 194 194 if ex != nil { 195 195 exCid = ex.Cid 196 196 } 197 197 198 198 // re-announce by registering under same rkey 199 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 199 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 200 200 Collection: tangled.SpindleNSID, 201 201 Repo: user.Did, 202 202 Rkey: instance, ··· 332 332 return 333 333 } 334 334 335 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 335 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 336 336 Collection: tangled.SpindleNSID, 337 337 Repo: user.Did, 338 338 Rkey: instance, ··· 542 542 return 543 543 } 544 544 545 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 545 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 546 546 Collection: tangled.SpindleMemberNSID, 547 547 Repo: user.Did, 548 548 Rkey: rkey, ··· 683 683 } 684 684 685 685 // remove from pds 686 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 686 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 687 687 Collection: tangled.SpindleMemberNSID, 688 688 Repo: user.Did, 689 689 Rkey: members[0].Rkey,
+2 -2
appview/state/follow.go
··· 43 43 case http.MethodPost: 44 44 createdAt := time.Now().Format(time.RFC3339) 45 45 rkey := tid.TID() 46 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 46 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 47 47 Collection: tangled.GraphFollowNSID, 48 48 Repo: currentUser.Did, 49 49 Rkey: rkey, ··· 88 88 return 89 89 } 90 90 91 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 91 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 92 92 Collection: tangled.GraphFollowNSID, 93 93 Repo: currentUser.Did, 94 94 Rkey: follow.Rkey,
+63
appview/state/login.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + "strings" 8 + 9 + "tangled.org/core/appview/pages" 10 + ) 11 + 12 + func (s *State) Login(w http.ResponseWriter, r *http.Request) { 13 + switch r.Method { 14 + case http.MethodGet: 15 + returnURL := r.URL.Query().Get("return_url") 16 + s.pages.Login(w, pages.LoginParams{ 17 + ReturnUrl: returnURL, 18 + }) 19 + case http.MethodPost: 20 + handle := r.FormValue("handle") 21 + 22 + // when users copy their handle from bsky.app, it tends to have these characters around it: 23 + // 24 + // @nelind.dk: 25 + // \u202a ensures that the handle is always rendered left to right and 26 + // \u202c reverts that so the rest of the page renders however it should 27 + handle = strings.TrimPrefix(handle, "\u202a") 28 + handle = strings.TrimSuffix(handle, "\u202c") 29 + 30 + // `@` is harmless 31 + handle = strings.TrimPrefix(handle, "@") 32 + 33 + // basic handle validation 34 + if !strings.Contains(handle, ".") { 35 + log.Println("invalid handle format", "raw", handle) 36 + s.pages.Notice( 37 + w, 38 + "login-msg", 39 + fmt.Sprintf("\"%s\" is an invalid handle. Did you mean %s.bsky.social or %s.tngl.sh?", handle, handle, handle), 40 + ) 41 + return 42 + } 43 + 44 + redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 45 + if err != nil { 46 + http.Error(w, err.Error(), http.StatusInternalServerError) 47 + return 48 + } 49 + 50 + s.pages.HxRedirect(w, redirectURL) 51 + } 52 + } 53 + 54 + func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 55 + err := s.oauth.DeleteSession(w, r) 56 + if err != nil { 57 + log.Println("failed to logout", "err", err) 58 + } else { 59 + log.Println("logged out successfully") 60 + } 61 + 62 + s.pages.HxRedirect(w, "/login") 63 + }
+2 -2
appview/state/profile.go
··· 634 634 vanityStats = append(vanityStats, string(v.Kind)) 635 635 } 636 636 637 - ex, _ := client.RepoGetRecord(r.Context(), "", tangled.ActorProfileNSID, user.Did, "self") 637 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 638 638 var cid *string 639 639 if ex != nil { 640 640 cid = ex.Cid 641 641 } 642 642 643 - _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 643 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 644 644 Collection: tangled.ActorProfileNSID, 645 645 Repo: user.Did, 646 646 Rkey: "self",
+3 -3
appview/state/reaction.go
··· 7 7 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - 11 10 lexutil "github.com/bluesky-social/indigo/lex/util" 11 + 12 12 "tangled.org/core/api/tangled" 13 13 "tangled.org/core/appview/db" 14 14 "tangled.org/core/appview/models" ··· 47 47 case http.MethodPost: 48 48 createdAt := time.Now().Format(time.RFC3339) 49 49 rkey := tid.TID() 50 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 50 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 51 51 Collection: tangled.FeedReactionNSID, 52 52 Repo: currentUser.Did, 53 53 Rkey: rkey, ··· 92 92 return 93 93 } 94 94 95 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 95 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 96 96 Collection: tangled.FeedReactionNSID, 97 97 Repo: currentUser.Did, 98 98 Rkey: reaction.Rkey,
+5 -10
appview/state/router.go
··· 5 5 "strings" 6 6 7 7 "github.com/go-chi/chi/v5" 8 - "github.com/gorilla/sessions" 9 8 "tangled.org/core/appview/issues" 10 9 "tangled.org/core/appview/knots" 11 10 "tangled.org/core/appview/labels" 12 11 "tangled.org/core/appview/middleware" 13 12 "tangled.org/core/appview/notifications" 14 - oauthhandler "tangled.org/core/appview/oauth/handler" 15 13 "tangled.org/core/appview/pipelines" 16 14 "tangled.org/core/appview/pulls" 17 15 "tangled.org/core/appview/repo" ··· 34 32 s.pages, 35 33 ) 36 34 37 - router.Use(middleware.TryRefreshSession()) 38 35 router.Get("/favicon.svg", s.Favicon) 39 36 router.Get("/favicon.ico", s.Favicon) 40 37 router.Get("/pwa-manifest.json", s.PWAManifest) ··· 123 120 // special-case handler for serving tangled.org/core 124 121 r.Get("/core", s.Core()) 125 122 123 + r.Get("/login", s.Login) 124 + r.Post("/login", s.Login) 125 + r.Post("/logout", s.Logout) 126 + 126 127 r.Route("/repo", func(r chi.Router) { 127 128 r.Route("/new", func(r chi.Router) { 128 129 r.Use(middleware.AuthMiddleware(s.oauth)) ··· 164 165 r.Mount("/notifications", s.NotificationsRouter(mw)) 165 166 166 167 r.Mount("/signup", s.SignupRouter()) 167 - r.Mount("/", s.OAuthRouter()) 168 + r.Mount("/", s.oauth.Router()) 168 169 169 170 r.Get("/keys/{user}", s.Keys) 170 171 r.Get("/terms", s.TermsOfService) ··· 189 190 190 191 http.Redirect(w, r, "/@tangled.org/core", http.StatusFound) 191 192 } 192 - } 193 - 194 - func (s *State) OAuthRouter() http.Handler { 195 - store := sessions.NewCookieStore([]byte(s.config.Core.CookieSecret)) 196 - oauth := oauthhandler.New(s.config, s.pages, s.idResolver, s.db, s.sess, store, s.oauth, s.enforcer, s.posthog) 197 - return oauth.Router() 198 193 } 199 194 200 195 func (s *State) SettingsRouter() http.Handler {
+2 -2
appview/state/star.go
··· 40 40 case http.MethodPost: 41 41 createdAt := time.Now().Format(time.RFC3339) 42 42 rkey := tid.TID() 43 - resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 43 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 44 Collection: tangled.FeedStarNSID, 45 45 Repo: currentUser.Did, 46 46 Rkey: rkey, ··· 92 92 return 93 93 } 94 94 95 - _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 95 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 96 96 Collection: tangled.FeedStarNSID, 97 97 Repo: currentUser.Did, 98 98 Rkey: star.Rkey,
+23 -20
appview/state/state.go
··· 11 11 "strings" 12 12 "time" 13 13 14 - comatproto "github.com/bluesky-social/indigo/api/atproto" 15 - "github.com/bluesky-social/indigo/atproto/syntax" 16 - lexutil "github.com/bluesky-social/indigo/lex/util" 17 - securejoin "github.com/cyphar/filepath-securejoin" 18 - "github.com/go-chi/chi/v5" 19 - "github.com/posthog/posthog-go" 20 14 "tangled.org/core/api/tangled" 21 15 "tangled.org/core/appview" 22 16 "tangled.org/core/appview/cache" ··· 38 32 tlog "tangled.org/core/log" 39 33 "tangled.org/core/rbac" 40 34 "tangled.org/core/tid" 35 + 36 + comatproto "github.com/bluesky-social/indigo/api/atproto" 37 + atpclient "github.com/bluesky-social/indigo/atproto/client" 38 + "github.com/bluesky-social/indigo/atproto/syntax" 39 + lexutil "github.com/bluesky-social/indigo/lex/util" 40 + securejoin "github.com/cyphar/filepath-securejoin" 41 + "github.com/go-chi/chi/v5" 42 + "github.com/posthog/posthog-go" 41 43 ) 42 44 43 45 type State struct { ··· 75 77 res = idresolver.DefaultResolver() 76 78 } 77 79 78 - pgs := pages.NewPages(config, res) 80 + pages := pages.NewPages(config, res) 79 81 cache := cache.New(config.Redis.Addr) 80 82 sess := session.New(cache) 81 - oauth := oauth.NewOAuth(config, sess) 83 + oauth2, err := oauth.New(config) 84 + if err != nil { 85 + return nil, fmt.Errorf("failed to start oauth handler: %w", err) 86 + } 82 87 validator := validator.New(d, res, enforcer) 83 88 84 89 posthog, err := posthog.NewWithConfig(config.Posthog.ApiKey, posthog.Config{Endpoint: config.Posthog.Endpoint}) ··· 162 167 state := &State{ 163 168 d, 164 169 notifier, 165 - oauth, 170 + oauth2, 166 171 enforcer, 167 - pgs, 172 + pages, 168 173 sess, 169 174 res, 170 175 posthog, ··· 275 280 // non-fatal 276 281 } 277 282 278 - fmt.Println(s.pages.Timeline(w, pages.TimelineParams{ 283 + s.pages.Timeline(w, pages.TimelineParams{ 279 284 LoggedInUser: user, 280 285 Timeline: timeline, 281 286 Repos: repos, 282 287 GfiLabel: gfiLabel, 283 - })) 288 + }) 284 289 } 285 290 286 291 func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) { ··· 291 296 292 297 l := s.logger.With("handler", "UpgradeBanner") 293 298 l = l.With("did", user.Did) 294 - l = l.With("handle", user.Handle) 295 299 296 300 regs, err := db.GetRegistrations( 297 301 s.db, ··· 431 435 432 436 user := s.oauth.GetUser(r) 433 437 l = l.With("did", user.Did) 434 - l = l.With("handle", user.Handle) 435 438 436 439 // form validation 437 440 domain := r.FormValue("domain") ··· 495 498 } 496 499 record := repo.AsRecord() 497 500 498 - xrpcClient, err := s.oauth.AuthorizedClient(r) 501 + atpClient, err := s.oauth.AuthorizedClient(r) 499 502 if err != nil { 500 503 l.Info("PDS write failed", "err", err) 501 504 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 502 505 return 503 506 } 504 507 505 - atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 508 + atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 506 509 Collection: tangled.RepoNSID, 507 510 Repo: user.Did, 508 511 Rkey: rkey, ··· 534 537 rollback := func() { 535 538 err1 := tx.Rollback() 536 539 err2 := s.enforcer.E.LoadPolicy() 537 - err3 := rollbackRecord(context.Background(), aturi, xrpcClient) 540 + err3 := rollbackRecord(context.Background(), aturi, atpClient) 538 541 539 542 // ignore txn complete errors, this is okay 540 543 if errors.Is(err1, sql.ErrTxDone) { ··· 607 610 aturi = "" 608 611 609 612 s.notifier.NewRepo(r.Context(), repo) 610 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, repoName)) 613 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Did, repoName)) 611 614 } 612 615 } 613 616 614 617 // this is used to rollback changes made to the PDS 615 618 // 616 619 // it is a no-op if the provided ATURI is empty 617 - func rollbackRecord(ctx context.Context, aturi string, xrpcc *xrpcclient.Client) error { 620 + func rollbackRecord(ctx context.Context, aturi string, client *atpclient.APIClient) error { 618 621 if aturi == "" { 619 622 return nil 620 623 } ··· 625 628 repo := parsed.Authority().String() 626 629 rkey := parsed.RecordKey().String() 627 630 628 - _, err := xrpcc.RepoDeleteRecord(ctx, &comatproto.RepoDeleteRecord_Input{ 631 + _, err := comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{ 629 632 Collection: collection, 630 633 Repo: repo, 631 634 Rkey: rkey,
+9 -7
appview/strings/strings.go
··· 22 22 "github.com/bluesky-social/indigo/api/atproto" 23 23 "github.com/bluesky-social/indigo/atproto/identity" 24 24 "github.com/bluesky-social/indigo/atproto/syntax" 25 - lexutil "github.com/bluesky-social/indigo/lex/util" 26 25 "github.com/go-chi/chi/v5" 26 + 27 + comatproto "github.com/bluesky-social/indigo/api/atproto" 28 + lexutil "github.com/bluesky-social/indigo/lex/util" 27 29 ) 28 30 29 31 type Strings struct { ··· 254 256 } 255 257 256 258 // first replace the existing record in the PDS 257 - ex, err := client.RepoGetRecord(r.Context(), "", tangled.StringNSID, entry.Did.String(), entry.Rkey) 259 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.StringNSID, entry.Did.String(), entry.Rkey) 258 260 if err != nil { 259 261 fail("Failed to updated existing record.", err) 260 262 return 261 263 } 262 - resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{ 264 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 263 265 Collection: tangled.StringNSID, 264 266 Repo: entry.Did.String(), 265 267 Rkey: entry.Rkey, ··· 284 286 s.Notifier.EditString(r.Context(), &entry) 285 287 286 288 // if that went okay, redir to the string 287 - s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+entry.Rkey) 289 + s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey) 288 290 } 289 291 290 292 } ··· 336 338 return 337 339 } 338 340 339 - resp, err := client.RepoPutRecord(r.Context(), &atproto.RepoPutRecord_Input{ 341 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 340 342 Collection: tangled.StringNSID, 341 343 Repo: user.Did, 342 344 Rkey: string.Rkey, ··· 360 362 s.Notifier.NewString(r.Context(), &string) 361 363 362 364 // successful 363 - s.Pages.HxRedirect(w, "/strings/"+user.Handle+"/"+string.Rkey) 365 + s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey) 364 366 } 365 367 } 366 368 ··· 403 405 404 406 s.Notifier.DeleteString(r.Context(), user.Did, rkey) 405 407 406 - s.Pages.HxRedirect(w, "/strings/"+user.Handle) 408 + s.Pages.HxRedirect(w, "/strings/"+user.Did) 407 409 } 408 410 409 411 func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
-99
appview/xrpcclient/xrpc.go
··· 1 1 package xrpcclient 2 2 3 3 import ( 4 - "bytes" 5 - "context" 6 4 "errors" 7 - "io" 8 5 "net/http" 9 6 10 - "github.com/bluesky-social/indigo/api/atproto" 11 - "github.com/bluesky-social/indigo/xrpc" 12 7 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 - oauth "tangled.org/anirudh.fi/atproto-oauth" 14 8 ) 15 9 16 10 var ( ··· 19 13 ErrXrpcFailed = errors.New("xrpc request failed") 20 14 ErrXrpcInvalid = errors.New("invalid xrpc request") 21 15 ) 22 - 23 - type Client struct { 24 - *oauth.XrpcClient 25 - authArgs *oauth.XrpcAuthedRequestArgs 26 - } 27 - 28 - func NewClient(client *oauth.XrpcClient, authArgs *oauth.XrpcAuthedRequestArgs) *Client { 29 - return &Client{ 30 - XrpcClient: client, 31 - authArgs: authArgs, 32 - } 33 - } 34 - 35 - func (c *Client) RepoPutRecord(ctx context.Context, input *atproto.RepoPutRecord_Input) (*atproto.RepoPutRecord_Output, error) { 36 - var out atproto.RepoPutRecord_Output 37 - if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil { 38 - return nil, err 39 - } 40 - 41 - return &out, nil 42 - } 43 - 44 - func (c *Client) RepoApplyWrites(ctx context.Context, input *atproto.RepoApplyWrites_Input) (*atproto.RepoApplyWrites_Output, error) { 45 - var out atproto.RepoApplyWrites_Output 46 - if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.applyWrites", nil, input, &out); err != nil { 47 - return nil, err 48 - } 49 - 50 - return &out, nil 51 - } 52 - 53 - func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) { 54 - var out atproto.RepoGetRecord_Output 55 - 56 - params := map[string]interface{}{ 57 - "cid": cid, 58 - "collection": collection, 59 - "repo": repo, 60 - "rkey": rkey, 61 - } 62 - if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.repo.getRecord", params, nil, &out); err != nil { 63 - return nil, err 64 - } 65 - 66 - return &out, nil 67 - } 68 - 69 - func (c *Client) RepoUploadBlob(ctx context.Context, input io.Reader) (*atproto.RepoUploadBlob_Output, error) { 70 - var out atproto.RepoUploadBlob_Output 71 - if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "*/*", "com.atproto.repo.uploadBlob", nil, input, &out); err != nil { 72 - return nil, err 73 - } 74 - 75 - return &out, nil 76 - } 77 - 78 - func (c *Client) SyncGetBlob(ctx context.Context, cid string, did string) ([]byte, error) { 79 - buf := new(bytes.Buffer) 80 - 81 - params := map[string]interface{}{ 82 - "cid": cid, 83 - "did": did, 84 - } 85 - if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.sync.getBlob", params, nil, buf); err != nil { 86 - return nil, err 87 - } 88 - 89 - return buf.Bytes(), nil 90 - } 91 - 92 - func (c *Client) RepoDeleteRecord(ctx context.Context, input *atproto.RepoDeleteRecord_Input) (*atproto.RepoDeleteRecord_Output, error) { 93 - var out atproto.RepoDeleteRecord_Output 94 - if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, &out); err != nil { 95 - return nil, err 96 - } 97 - 98 - return &out, nil 99 - } 100 - 101 - func (c *Client) ServerGetServiceAuth(ctx context.Context, aud string, exp int64, lxm string) (*atproto.ServerGetServiceAuth_Output, error) { 102 - var out atproto.ServerGetServiceAuth_Output 103 - 104 - params := map[string]interface{}{ 105 - "aud": aud, 106 - "exp": exp, 107 - "lxm": lxm, 108 - } 109 - if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.server.getServiceAuth", params, nil, &out); err != nil { 110 - return nil, err 111 - } 112 - 113 - return &out, nil 114 - } 115 16 116 17 // produces a more manageable error 117 18 func HandleXrpcErr(err error) error {
+1 -1
go.mod
··· 8 8 github.com/alecthomas/chroma/v2 v2.15.0 9 9 github.com/avast/retry-go/v4 v4.6.1 10 10 github.com/bluekeyes/go-gitdiff v0.8.1 11 - github.com/bluesky-social/indigo v0.0.0-20250724221105-5827c8fb61bb 11 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 12 12 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 13 13 github.com/carlmjohnson/versioninfo v0.22.5 14 14 github.com/casbin/casbin/v2 v2.103.0
+2
go.sum
··· 25 25 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 26 26 github.com/bluesky-social/indigo v0.0.0-20250724221105-5827c8fb61bb h1:BqMNDZMfXwiRTJ6NvQotJ0qInn37JH5U8E+TF01CFHQ= 27 27 github.com/bluesky-social/indigo v0.0.0-20250724221105-5827c8fb61bb/go.mod h1:0XUyOCRtL4/OiyeqMTmr6RlVHQMDgw3LS7CfibuZR5Q= 28 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU= 29 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= 28 30 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= 29 31 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 30 32 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=