From 32a4ec447b362b2038b1718111048e6a54e54f20 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Thu, 8 May 2025 11:00:59 +0300 Subject: [PATCH 1/5] appview: oauth: setup handlers and service --- .gitignore | 2 + appview/oauth/client/oauth_client.go | 24 +++ appview/oauth/handler/handler.go | 260 +++++++++++++++++++++++++++ appview/oauth/oauth.go | 206 +++++++++++++++++++++ go.mod | 30 ++-- go.sum | 77 ++++++-- 6 files changed, 571 insertions(+), 28 deletions(-) create mode 100644 appview/oauth/client/oauth_client.go create mode 100644 appview/oauth/handler/handler.go create mode 100644 appview/oauth/oauth.go diff --git a/.gitignore b/.gitignore index b00e2ee..6651f18 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ out/ ./avatar/node_modules/* patches *.qcow2 +.DS_Store +.env diff --git a/appview/oauth/client/oauth_client.go b/appview/oauth/client/oauth_client.go new file mode 100644 index 0000000..a1914d8 --- /dev/null +++ b/appview/oauth/client/oauth_client.go @@ -0,0 +1,24 @@ +package client + +import ( + oauth "github.com/haileyok/atproto-oauth-golang" + "github.com/haileyok/atproto-oauth-golang/helpers" +) + +type OAuthClient struct { + *oauth.Client +} + +func NewClient(clientId, clientJwk, redirectUri string) (*OAuthClient, error) { + k, err := helpers.ParseJWKFromBytes([]byte(clientJwk)) + if err != nil { + return nil, err + } + + cli, err := oauth.NewClient(oauth.ClientArgs{ + ClientId: clientId, + ClientJwk: k, + RedirectUri: redirectUri, + }) + return &OAuthClient{cli}, err +} diff --git a/appview/oauth/handler/handler.go b/appview/oauth/handler/handler.go new file mode 100644 index 0000000..f342b99 --- /dev/null +++ b/appview/oauth/handler/handler.go @@ -0,0 +1,260 @@ +package oauth + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/gorilla/sessions" + "github.com/haileyok/atproto-oauth-golang/helpers" + "github.com/lestrrat-go/jwx/v2/jwk" + "tangled.sh/tangled.sh/core/appview" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth" + "tangled.sh/tangled.sh/core/appview/oauth/client" + "tangled.sh/tangled.sh/core/appview/pages" +) + +const ( + oauthScope = "atproto transition:generic" +) + +type OAuthHandler struct { + Config *appview.Config + Pages *pages.Pages + Resolver *appview.Resolver + Db *db.DB + Store *sessions.CookieStore + OAuth *oauth.OAuth +} + +func (o *OAuthHandler) Router() http.Handler { + r := chi.NewRouter() + + // gets mounted on /oauth + r.Get("/client-metadata.json", o.clientMetadata) + r.Get("/jwks.json", o.jwks) + r.Get("/login", o.login) + r.Post("/login", o.login) + r.Get("/callback", o.callback) + return r +} + +func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) { + metadata := map[string]any{ + "client_id": o.Config.OAuth.ServerMetadataUrl, + "client_name": "Tangled", + "subject_type": "public", + "client_uri": o.Config.Core.AppviewHost, + "redirect_uris": []string{fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)}, + "grant_types": []string{"authorization_code", "refresh_token"}, + "response_types": []string{"code"}, + "application_type": "web", + "dpop_bound_access_tokens": true, + "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", o.Config.Core.AppviewHost), + "scope": "atproto transition:generic", + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "ES256", + } + + fmt.Println("clientMetadata", metadata) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(metadata) +} + +func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) { + jwks := o.Config.OAuth.Jwks + pubKey, err := pubKeyFromJwk(jwks) + if err != nil { + log.Printf("error parsing public key: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + response := helpers.CreateJwksResponseObject(pubKey) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +// temporary until we swap out the main login page +func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + o.Pages.OAuthLogin(w, pages.LoginParams{}) + case http.MethodPost: + handle := strings.TrimPrefix(r.FormValue("handle"), "@") + + resolved, err := o.Resolver.ResolveIdent(r.Context(), handle) + if err != nil { + log.Println("failed to resolve handle:", err) + o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) + return + } + oauthClient, err := client.NewClient( + o.Config.OAuth.ServerMetadataUrl, + o.Config.OAuth.Jwks, + fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) + + if err != nil { + log.Println("failed to create oauth client:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint()) + if err != nil { + log.Println("failed to resolve auth server:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer) + if err != nil { + log.Println("failed to fetch auth server metadata:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + dpopKey, err := helpers.GenerateKey(nil) + if err != nil { + log.Println("failed to generate dpop key:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + dpopKeyJson, err := json.Marshal(dpopKey) + if err != nil { + log.Println("failed to marshal dpop key:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey) + if err != nil { + log.Println("failed to send par auth request:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + err = db.SaveOAuthRequest(o.Db, db.OAuthRequest{ + Did: resolved.DID.String(), + PdsUrl: resolved.PDSEndpoint(), + Handle: handle, + AuthserverIss: authMeta.Issuer, + PkceVerifier: parResp.PkceVerifier, + DpopAuthserverNonce: parResp.DpopAuthserverNonce, + DpopPrivateJwk: string(dpopKeyJson), + State: parResp.State, + }) + if err != nil { + log.Println("failed to save oauth request:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + u, _ := url.Parse(authMeta.AuthorizationEndpoint) + u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(o.Config.OAuth.ServerMetadataUrl), parResp.RequestUri) + o.Pages.HxRedirect(w, u.String()) + } +} + +func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { + state := r.FormValue("state") + + oauthRequest, err := db.GetOAuthRequestByState(o.Db, state) + if err != nil { + log.Println("failed to get oauth request:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + defer func() { + err := db.DeleteOAuthRequestByState(o.Db, state) + if err != nil { + log.Println("failed to delete oauth request for state:", state, err) + } + }() + + code := r.FormValue("code") + if code == "" { + log.Println("missing code for state: ", state) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + iss := r.FormValue("iss") + if iss == "" { + log.Println("missing iss for state: ", state) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + oauthClient, err := client.NewClient( + o.Config.OAuth.ServerMetadataUrl, + o.Config.OAuth.Jwks, + fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) + + if err != nil { + log.Println("failed to create oauth client:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk)) + if err != nil { + log.Println("failed to parse jwk:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + tokenResp, err := oauthClient.InitialTokenRequest( + r.Context(), + code, + oauthRequest.AuthserverIss, + oauthRequest.PkceVerifier, + oauthRequest.DpopAuthserverNonce, + jwk, + ) + if err != nil { + log.Println("failed to get token:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + if tokenResp.Scope != oauthScope { + log.Println("scope doesn't match:", tokenResp.Scope) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + err = o.OAuth.SaveSession(w, r, oauthRequest, tokenResp) + if err != nil { + log.Println("failed to save session:", err) + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") + return + } + + log.Println("session saved successfully") + + http.Redirect(w, r, "/", http.StatusFound) +} + +func pubKeyFromJwk(jwks string) (jwk.Key, error) { + k, err := helpers.ParseJWKFromBytes([]byte(jwks)) + if err != nil { + return nil, err + } + pubKey, err := k.PublicKey() + if err != nil { + return nil, err + } + return pubKey, nil +} diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go new file mode 100644 index 0000000..07c9062 --- /dev/null +++ b/appview/oauth/oauth.go @@ -0,0 +1,206 @@ +package oauth + +import ( + "fmt" + "log" + "net/http" + "time" + + "github.com/gorilla/sessions" + oauth "github.com/haileyok/atproto-oauth-golang" + "github.com/haileyok/atproto-oauth-golang/helpers" + "tangled.sh/tangled.sh/core/appview" + "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth/client" + xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient" +) + +type OAuthRequest struct { + ID uint + AuthserverIss string + State string + Did string + PdsUrl string + PkceVerifier string + DpopAuthserverNonce string + DpopPrivateJwk string +} + +type OAuth struct { + Store *sessions.CookieStore + Db *db.DB + Config *appview.Config +} + +func NewOAuth(db *db.DB, config *appview.Config) *OAuth { + return &OAuth{ + Store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)), + Db: db, + Config: config, + } +} + +func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAuthRequest, oresp *oauth.TokenResponse) error { + // first we save the did in the user session + userSession, err := o.Store.Get(r, appview.SessionName) + if err != nil { + return err + } + + userSession.Values[appview.SessionDid] = oreq.Did + userSession.Values[appview.SessionAuthenticated] = true + err = userSession.Save(r, w) + if err != nil { + return fmt.Errorf("error saving user session: %w", err) + } + + // then save the whole thing in the db + session := db.OAuthSession{ + Did: oreq.Did, + Handle: oreq.Handle, + PdsUrl: oreq.PdsUrl, + DpopAuthserverNonce: oreq.DpopAuthserverNonce, + AuthServerIss: oreq.AuthserverIss, + DpopPrivateJwk: oreq.DpopPrivateJwk, + AccessJwt: oresp.AccessToken, + RefreshJwt: oresp.RefreshToken, + Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339), + } + + return db.SaveOAuthSession(o.Db, session) +} + +func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error { + userSession, err := o.Store.Get(r, appview.SessionName) + if err != nil || userSession.IsNew { + return fmt.Errorf("error getting user session (or new session?): %w", err) + } + + did := userSession.Values[appview.SessionDid].(string) + + err = db.DeleteOAuthSessionByDid(o.Db, did) + if err != nil { + return fmt.Errorf("error deleting oauth session: %w", err) + } + + userSession.Options.MaxAge = -1 + + return userSession.Save(r, w) +} + +func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) { + userSession, err := o.Store.Get(r, appview.SessionName) + if err != nil || userSession.IsNew { + return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err) + } + + did := userSession.Values[appview.SessionDid].(string) + auth := userSession.Values[appview.SessionAuthenticated].(bool) + + session, err := db.GetOAuthSessionByDid(o.Db, did) + if err != nil { + return nil, false, fmt.Errorf("error getting oauth session: %w", err) + } + + expiry, err := time.Parse(time.RFC3339, session.Expiry) + if err != nil { + return nil, false, fmt.Errorf("error parsing expiry time: %w", err) + } + if expiry.Sub(time.Now()) <= 5*time.Minute { + privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) + if err != nil { + return nil, false, err + } + oauthClient, err := client.NewClient(o.Config.OAuth.ServerMetadataUrl, + o.Config.OAuth.Jwks, + fmt.Sprintf("%s/oauth/callback", o.Config.Core.AppviewHost)) + + if err != nil { + return nil, false, err + } + + resp, err := oauthClient.RefreshTokenRequest(r.Context(), session.RefreshJwt, session.AuthServerIss, session.DpopAuthserverNonce, privateJwk) + if err != nil { + return nil, false, err + } + + newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339) + err = db.RefreshOAuthSession(o.Db, did, resp.AccessToken, resp.RefreshToken, newExpiry) + if err != nil { + return nil, false, fmt.Errorf("error refreshing oauth session: %w", err) + } + + // update the current session + session.AccessJwt = resp.AccessToken + session.RefreshJwt = resp.RefreshToken + session.DpopAuthserverNonce = resp.DpopAuthserverNonce + session.Expiry = newExpiry + } + + return session, auth, nil +} + +type User struct { + Handle string + Did string + Pds string +} + +func (a *OAuth) GetUser(r *http.Request) *User { + clientSession, err := a.Store.Get(r, appview.SessionName) + + if err != nil || clientSession.IsNew { + return nil + } + + return &User{ + Handle: clientSession.Values[appview.SessionHandle].(string), + Did: clientSession.Values[appview.SessionDid].(string), + Pds: clientSession.Values[appview.SessionPds].(string), + } +} + +func (a *OAuth) GetDid(r *http.Request) string { + clientSession, err := a.Store.Get(r, appview.SessionName) + + if err != nil || clientSession.IsNew { + return "" + } + + return clientSession.Values[appview.SessionDid].(string) +} + +func (o *OAuth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) { + session, auth, err := o.GetSession(r) + if err != nil { + return nil, fmt.Errorf("error getting session: %w", err) + } + if !auth { + return nil, fmt.Errorf("not authorized") + } + + client := &oauth.XrpcClient{ + OnDpopPdsNonceChanged: func(did, newNonce string) { + err := db.UpdateDpopPdsNonce(o.Db, did, newNonce) + if err != nil { + log.Printf("error updating dpop pds nonce: %v", err) + } + }, + } + + privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) + if err != nil { + return nil, fmt.Errorf("error parsing private jwk: %w", err) + } + + xrpcClient := xrpc.NewClient(client, &oauth.XrpcAuthedRequestArgs{ + Did: session.Did, + PdsUrl: session.PdsUrl, + DpopPdsNonce: session.PdsUrl, + AccessToken: session.AccessJwt, + Issuer: session.AuthServerIss, + DpopPrivateJwk: privateJwk, + }) + + return xrpcClient, nil +} diff --git a/go.mod b/go.mod index 8b7dc42..f94c4b2 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module tangled.sh/tangled.sh/core -go 1.23.0 +go 1.24.0 -toolchain go1.23.6 +toolchain go1.24.3 require ( github.com/Blank-Xu/sql-adapter v1.1.1 github.com/alecthomas/chroma/v2 v2.15.0 github.com/bluekeyes/go-gitdiff v0.8.1 - github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 github.com/casbin/casbin/v2 v2.103.0 github.com/cyphar/filepath-securejoin v0.4.1 @@ -19,7 +19,9 @@ require ( github.com/go-git/go-git/v5 v5.14.0 github.com/google/uuid v1.6.0 github.com/gorilla/sessions v1.4.0 + github.com/haileyok/atproto-oauth-golang v0.0.2 github.com/ipfs/go-cid v0.5.0 + github.com/lestrrat-go/jwx/v2 v2.0.12 github.com/mattn/go-sqlite3 v1.14.24 github.com/microcosm-cc/bluemonday v1.0.27 github.com/resend/resend-go/v2 v2.15.0 @@ -41,16 +43,17 @@ require ( github.com/casbin/govaluate v1.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/websocket v1.5.1 // indirect @@ -75,6 +78,11 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/lestrrat-go/blackmagic v1.0.2 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect @@ -86,33 +94,31 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.54.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect + github.com/segmentio/asm v1.2.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/stretchr/testify v1.10.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/otel v1.29.0 // indirect + go.opentelemetry.io/otel/metric v1.29.0 // indirect + go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/time v0.8.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect ) diff --git a/go.sum b/go.sum index 9e5b733..8a861d6 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= -github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 h1:yHusfYYi8odoCcsI6AurU+dRWb7itHAQNwt3/Rl9Vfs= -github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20/go.mod h1:Qp4YqWf+AQ3TwQCxV5Ls8O2tXE55zVTGVs3zTmn7BOg= +github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= +github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -52,8 +52,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= @@ -82,8 +86,8 @@ github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6 github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= @@ -91,6 +95,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -111,6 +117,8 @@ github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzq github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= +github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= @@ -159,6 +167,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -177,6 +187,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= +github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= +github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -212,8 +236,9 @@ github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxu github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= @@ -227,9 +252,11 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= @@ -246,10 +273,16 @@ github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -270,12 +303,12 @@ gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAF gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -303,6 +336,7 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= @@ -314,6 +348,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -327,6 +362,7 @@ golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfS golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -334,6 +370,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -348,6 +385,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -357,6 +395,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -364,6 +404,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -372,10 +414,12 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -389,6 +433,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -- 2.43.0 From ed9740a137deb7c2cd25cf0b2de762a3e0374f8f Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Thu, 8 May 2025 11:00:59 +0300 Subject: [PATCH 2/5] appview: xrpcclient: init wrapper xrpc client --- appview/xrpcclient/xrpc.go | 80 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 appview/xrpcclient/xrpc.go diff --git a/appview/xrpcclient/xrpc.go b/appview/xrpcclient/xrpc.go new file mode 100644 index 0000000..812a08e --- /dev/null +++ b/appview/xrpcclient/xrpc.go @@ -0,0 +1,80 @@ +package xrpcclient + +import ( + "bytes" + "context" + "io" + + "github.com/bluesky-social/indigo/api/atproto" + "github.com/bluesky-social/indigo/xrpc" + oauth "github.com/haileyok/atproto-oauth-golang" +) + +type Client struct { + *oauth.XrpcClient + authArgs *oauth.XrpcAuthedRequestArgs +} + +func NewClient(client *oauth.XrpcClient, authArgs *oauth.XrpcAuthedRequestArgs) *Client { + return &Client{ + XrpcClient: client, + authArgs: authArgs, + } +} + +func (c *Client) RepoPutRecord(ctx context.Context, input *atproto.RepoPutRecord_Input) (*atproto.RepoPutRecord_Output, error) { + var out atproto.RepoPutRecord_Output + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} + +func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) { + var out atproto.RepoGetRecord_Output + + params := map[string]interface{}{ + "cid": cid, + "collection": collection, + "repo": repo, + "rkey": rkey, + } + if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.repo.getRecord", params, nil, &out); err != nil { + return nil, err + } + + return &out, nil +} + +func (c *Client) RepoUploadBlob(ctx context.Context, input io.Reader) (*atproto.RepoUploadBlob_Output, error) { + var out atproto.RepoUploadBlob_Output + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "*/*", "com.atproto.repo.uploadBlob", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} + +func (c *Client) SyncGetBlob(ctx context.Context, cid string, did string) ([]byte, error) { + buf := new(bytes.Buffer) + + params := map[string]interface{}{ + "cid": cid, + "did": did, + } + if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.sync.getBlob", params, nil, buf); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func (c *Client) RepoDeleteRecord(ctx context.Context, input *atproto.RepoDeleteRecord_Input) (*atproto.RepoDeleteRecord_Output, error) { + var out atproto.RepoDeleteRecord_Output + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, &out); err != nil { + return nil, err + } + + return &out, nil +} -- 2.43.0 From 492f7060ba54516a37428581d23c24b5039d3975 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Thu, 8 May 2025 11:00:59 +0300 Subject: [PATCH 3/5] appview: swap out old auth service for oauth Also does some driveby config refactoring. --- .air/appview.toml | 2 +- appview/config.go | 47 ++++- appview/consts.go | 3 + appview/db/db.go | 26 +++ appview/db/oauth.go | 173 ++++++++++++++++ appview/middleware/middleware.go | 63 +----- appview/oauth/oauth.go | 2 + appview/pages/pages.go | 78 +++---- appview/pages/templates/user/oauthlogin.html | 71 +++++++ appview/settings/settings.go | 45 ++-- appview/state/artifact.go | 29 ++- appview/state/follow.go | 12 +- appview/state/git_http.go | 4 +- appview/state/middleware.go | 4 +- appview/state/profile.go | 29 +-- appview/state/pull.go | 127 +++++++----- appview/state/repo.go | 158 ++++++++------ appview/state/repo_util.go | 6 +- appview/state/router.go | 53 +++-- appview/state/star.go | 12 +- appview/state/state.go | 205 ++++++++++--------- appview/tid.go | 2 +- cmd/appview/main.go | 4 +- 23 files changed, 765 insertions(+), 390 deletions(-) create mode 100644 appview/db/oauth.go create mode 100644 appview/pages/templates/user/oauthlogin.html diff --git a/.air/appview.toml b/.air/appview.toml index 01efc93..877f17c 100644 --- a/.air/appview.toml +++ b/.air/appview.toml @@ -1,6 +1,6 @@ [build] cmd = "tailwindcss -i input.css -o ./appview/pages/static/tw.css && go build -o .bin/app ./cmd/appview/main.go" -bin = ".bin/app" +bin = ";set -o allexport && source .env && set +o allexport; .bin/app" root = "." exclude_regex = [".*_templ.go"] diff --git a/appview/config.go b/appview/config.go index c7d321c..7d82b28 100644 --- a/appview/config.go +++ b/appview/config.go @@ -6,17 +6,44 @@ import ( "github.com/sethvargo/go-envconfig" ) +type CoreConfig struct { + CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"` + DbPath string `env:"DB_PATH, default=appview.db"` + ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"` + AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"` + Dev bool `env:"DEV, default=false"` +} + +type OAuthConfig struct { + Jwks string `env:"JWKS"` + ServerMetadataUrl string `env:"SERVER_METADATA_URL"` +} + +type JetstreamConfig struct { + Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"` +} + +type ResendConfig struct { + ApiKey string `env:"API_KEY"` +} + +type CamoConfig struct { + Host string `env:"HOST, default=https://camo.tangled.sh"` + SharedSecret string `env:"SHARED_SECRET"` +} + +type AvatarConfig struct { + Host string `env:"HOST, default=https://avatar.tangled.sh"` + SharedSecret string `env:"SHARED_SECRET"` +} + type Config struct { - CookieSecret string `env:"TANGLED_COOKIE_SECRET, default=00000000000000000000000000000000"` - DbPath string `env:"TANGLED_DB_PATH, default=appview.db"` - ListenAddr string `env:"TANGLED_LISTEN_ADDR, default=0.0.0.0:3000"` - Dev bool `env:"TANGLED_DEV, default=false"` - JetstreamEndpoint string `env:"TANGLED_JETSTREAM_ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"` - ResendApiKey string `env:"TANGLED_RESEND_API_KEY"` - CamoHost string `env:"TANGLED_CAMO_HOST, default=https://camo.tangled.sh"` - CamoSharedSecret string `env:"TANGLED_CAMO_SHARED_SECRET"` - AvatarSharedSecret string `env:"TANGLED_AVATAR_SHARED_SECRET"` - AvatarHost string `env:"TANGLED_AVATAR_HOST, default=https://avatar.tangled.sh"` + Core CoreConfig `env:",prefix=TANGLED_"` + Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"` + Resend ResendConfig `env:",prefix=TANGLED_RESEND_"` + Camo CamoConfig `env:",prefix=TANGLED_CAMO_"` + Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"` + OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"` } func LoadConfig(ctx context.Context) (*Config, error) { diff --git a/appview/consts.go b/appview/consts.go index 3a15eb1..ea86179 100644 --- a/appview/consts.go +++ b/appview/consts.go @@ -9,4 +9,7 @@ const ( SessionRefreshJwt = "refreshJwt" SessionExpiry = "expiry" SessionAuthenticated = "authenticated" + + SessionDpopPrivateJwk = "dpopPrivateJwk" + SessionDpopAuthServerNonce = "dpopAuthServerNonce" ) diff --git a/appview/db/db.go b/appview/db/db.go index eb19053..b5339da 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -288,6 +288,32 @@ func Make(dbPath string) (*DB, error) { foreign key (at_uri) references repos(at_uri) on delete cascade ); + create table if not exists oauth_requests ( + id integer primary key autoincrement, + auth_server_iss text not null, + state text not null, + did text not null, + handle text not null, + pds_url text not null, + pkce_verifier text not null, + dpop_auth_server_nonce text not null, + dpop_private_jwk text not null + ); + + create table if not exists oauth_sessions ( + id integer primary key autoincrement, + did text not null, + handle text not null, + pds_url text not null, + auth_server_iss text not null, + access_jwt text not null, + refresh_jwt text not null, + dpop_pds_nonce text, + dpop_auth_server_nonce text not null, + dpop_private_jwk text not null, + expiry text not null + ); + create table if not exists migrations ( id integer primary key autoincrement, name text unique diff --git a/appview/db/oauth.go b/appview/db/oauth.go new file mode 100644 index 0000000..684f909 --- /dev/null +++ b/appview/db/oauth.go @@ -0,0 +1,173 @@ +package db + +type OAuthRequest struct { + ID uint + AuthserverIss string + Handle string + State string + Did string + PdsUrl string + PkceVerifier string + DpopAuthserverNonce string + DpopPrivateJwk string +} + +func SaveOAuthRequest(e Execer, oauthRequest OAuthRequest) error { + _, err := e.Exec(` + insert into oauth_requests ( + auth_server_iss, + state, + handle, + did, + pds_url, + pkce_verifier, + dpop_auth_server_nonce, + dpop_private_jwk + ) values (?, ?, ?, ?, ?, ?, ?, ?)`, + oauthRequest.AuthserverIss, + oauthRequest.State, + oauthRequest.Handle, + oauthRequest.Did, + oauthRequest.PdsUrl, + oauthRequest.PkceVerifier, + oauthRequest.DpopAuthserverNonce, + oauthRequest.DpopPrivateJwk, + ) + return err +} + +func GetOAuthRequestByState(e Execer, state string) (OAuthRequest, error) { + var req OAuthRequest + err := e.QueryRow(` + select + id, + auth_server_iss, + handle, + state, + did, + pds_url, + pkce_verifier, + dpop_auth_server_nonce, + dpop_private_jwk + from oauth_requests + where state = ?`, state).Scan( + &req.ID, + &req.AuthserverIss, + &req.Handle, + &req.State, + &req.Did, + &req.PdsUrl, + &req.PkceVerifier, + &req.DpopAuthserverNonce, + &req.DpopPrivateJwk, + ) + return req, err +} + +func DeleteOAuthRequestByState(e Execer, state string) error { + _, err := e.Exec(` + delete from oauth_requests + where state = ?`, state) + return err +} + +type OAuthSession struct { + ID uint + Handle string + Did string + PdsUrl string + AccessJwt string + RefreshJwt string + AuthServerIss string + DpopPdsNonce string + DpopAuthserverNonce string + DpopPrivateJwk string + Expiry string +} + +func SaveOAuthSession(e Execer, session OAuthSession) error { + _, err := e.Exec(` + insert into oauth_sessions ( + did, + handle, + pds_url, + access_jwt, + refresh_jwt, + auth_server_iss, + dpop_auth_server_nonce, + dpop_private_jwk, + expiry + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + session.Did, + session.Handle, + session.PdsUrl, + session.AccessJwt, + session.RefreshJwt, + session.AuthServerIss, + session.DpopAuthserverNonce, + session.DpopPrivateJwk, + session.Expiry, + ) + return err +} + +func RefreshOAuthSession(e Execer, did string, accessJwt, refreshJwt, expiry string) error { + _, err := e.Exec(` + update oauth_sessions + set access_jwt = ?, refresh_jwt = ?, expiry = ? + where did = ?`, + accessJwt, + refreshJwt, + expiry, + did, + ) + return err +} + +func GetOAuthSessionByDid(e Execer, did string) (*OAuthSession, error) { + var session OAuthSession + err := e.QueryRow(` + select + id, + did, + handle, + pds_url, + access_jwt, + refresh_jwt, + auth_server_iss, + dpop_auth_server_nonce, + dpop_private_jwk, + expiry + from oauth_sessions + where did = ?`, did).Scan( + &session.ID, + &session.Did, + &session.Handle, + &session.PdsUrl, + &session.AccessJwt, + &session.RefreshJwt, + &session.AuthServerIss, + &session.DpopAuthserverNonce, + &session.DpopPrivateJwk, + &session.Expiry, + ) + return &session, err +} + +func DeleteOAuthSessionByDid(e Execer, did string) error { + _, err := e.Exec(` + delete from oauth_sessions + where did = ?`, did) + return err +} + +func UpdateDpopPdsNonce(e Execer, did string, dpopPdsNonce string) error { + _, err := e.Exec(` + update oauth_sessions + set dpop_pds_nonce = ? + where did = ?`, + dpopPdsNonce, + did, + ) + return err +} diff --git a/appview/middleware/middleware.go b/appview/middleware/middleware.go index 51aa75f..60d90b5 100644 --- a/appview/middleware/middleware.go +++ b/appview/middleware/middleware.go @@ -5,18 +5,14 @@ import ( "log" "net/http" "strconv" - "time" - comatproto "github.com/bluesky-social/indigo/api/atproto" - "github.com/bluesky-social/indigo/xrpc" - "tangled.sh/tangled.sh/core/appview" - "tangled.sh/tangled.sh/core/appview/auth" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pagination" ) type Middleware func(http.Handler) http.Handler -func AuthMiddleware(a *auth.Auth) Middleware { +func AuthMiddleware(a *oauth.OAuth) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { redirectFunc := func(w http.ResponseWriter, r *http.Request) { @@ -29,68 +25,19 @@ func AuthMiddleware(a *auth.Auth) Middleware { } } - session, err := a.GetSession(r) - if session.IsNew || err != nil { + _, auth, err := a.GetSession(r) + if err != nil { log.Printf("not logged in, redirecting") redirectFunc(w, r) return } - authorized, ok := session.Values[appview.SessionAuthenticated].(bool) - if !ok || !authorized { + if !auth { log.Printf("not logged in, redirecting") redirectFunc(w, r) return } - // refresh if nearing expiry - // TODO: dedup with /login - expiryStr := session.Values[appview.SessionExpiry].(string) - expiry, err := time.Parse(time.RFC3339, expiryStr) - if err != nil { - log.Println("invalid expiry time", err) - redirectFunc(w, r) - return - } - pdsUrl, ok1 := session.Values[appview.SessionPds].(string) - did, ok2 := session.Values[appview.SessionDid].(string) - refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string) - - if !ok1 || !ok2 || !ok3 { - log.Println("invalid expiry time", err) - redirectFunc(w, r) - return - } - - if time.Now().After(expiry) { - log.Println("token expired, refreshing ...") - - client := xrpc.Client{ - Host: pdsUrl, - Auth: &xrpc.AuthInfo{ - Did: did, - AccessJwt: refreshJwt, - RefreshJwt: refreshJwt, - }, - } - atSession, err := comatproto.ServerRefreshSession(r.Context(), &client) - if err != nil { - log.Println("failed to refresh session", err) - redirectFunc(w, r) - return - } - - sessionish := auth.RefreshSessionWrapper{atSession} - - err = a.StoreSession(r, w, &sessionish, pdsUrl) - if err != nil { - log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err) - return - } - - log.Println("successfully refreshed token") - } - next.ServeHTTP(w, r) }) } diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go index 07c9062..1cdd190 100644 --- a/appview/oauth/oauth.go +++ b/appview/oauth/oauth.go @@ -48,6 +48,8 @@ func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAut } userSession.Values[appview.SessionDid] = oreq.Did + userSession.Values[appview.SessionHandle] = oreq.Handle + userSession.Values[appview.SessionPds] = oreq.PdsUrl userSession.Values[appview.SessionAuthenticated] = true err = userSession.Save(r, w) if err != nil { diff --git a/appview/pages/pages.go b/appview/pages/pages.go index ae18d02..58af5e7 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -16,8 +16,8 @@ import ( "strings" "tangled.sh/tangled.sh/core/appview" - "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages/markup" "tangled.sh/tangled.sh/core/appview/pages/repoinfo" "tangled.sh/tangled.sh/core/appview/pagination" @@ -48,14 +48,14 @@ type Pages struct { func NewPages(config *appview.Config) *Pages { // initialized with safe defaults, can be overriden per use rctx := &markup.RenderContext{ - IsDev: config.Dev, - CamoUrl: config.CamoHost, - CamoSecret: config.CamoSharedSecret, + IsDev: config.Core.Dev, + CamoUrl: config.Camo.Host, + CamoSecret: config.Camo.SharedSecret, } p := &Pages{ t: make(map[string]*template.Template), - dev: config.Dev, + dev: config.Core.Dev, embedFS: Files, rctx: rctx, templateDir: "appview/pages", @@ -249,8 +249,12 @@ func (p *Pages) Login(w io.Writer, params LoginParams) error { return p.executePlain("user/login", w, params) } +func (p *Pages) OAuthLogin(w io.Writer, params LoginParams) error { + return p.executePlain("user/oauthlogin", w, params) +} + type TimelineParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Timeline []db.TimelineEvent DidHandleMap map[string]string } @@ -260,7 +264,7 @@ func (p *Pages) Timeline(w io.Writer, params TimelineParams) error { } type SettingsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User PubKeys []db.PublicKey Emails []db.Email } @@ -270,7 +274,7 @@ func (p *Pages) Settings(w io.Writer, params SettingsParams) error { } type KnotsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Registrations []db.Registration } @@ -279,7 +283,7 @@ func (p *Pages) Knots(w io.Writer, params KnotsParams) error { } type KnotParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User DidHandleMap map[string]string Registration *db.Registration Members []string @@ -291,7 +295,7 @@ func (p *Pages) Knot(w io.Writer, params KnotParams) error { } type NewRepoParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Knots []string } @@ -300,7 +304,7 @@ func (p *Pages) NewRepo(w io.Writer, params NewRepoParams) error { } type ForkRepoParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Knots []string RepoInfo repoinfo.RepoInfo } @@ -310,7 +314,7 @@ func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { } type ProfilePageParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Repos []db.Repo CollaboratingRepos []db.Repo ProfileTimeline *db.ProfileTimeline @@ -335,7 +339,7 @@ func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { } type ReposPageParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Repos []db.Repo Card ProfileCard @@ -356,7 +360,7 @@ func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { } type EditBioParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Profile *db.Profile } @@ -365,7 +369,7 @@ func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { } type EditPinsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User Profile *db.Profile AllRepos []PinnedRepo DidHandleMap map[string]string @@ -403,7 +407,7 @@ func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParam } type RepoIndexParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string TagMap map[string][]string @@ -444,7 +448,7 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { } type RepoLogParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo TagMap map[string][]string types.RepoLogResponse @@ -458,7 +462,7 @@ func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { } type RepoCommitParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string EmailToDidOrHandle map[string]string @@ -472,7 +476,7 @@ func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { } type RepoTreeParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string BreadCrumbs [][]string @@ -508,7 +512,7 @@ func (p *Pages) RepoTree(w io.Writer, params RepoTreeParams) error { } type RepoBranchesParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string types.RepoBranchesResponse @@ -520,7 +524,7 @@ func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { } type RepoTagsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string types.RepoTagsResponse @@ -534,7 +538,7 @@ func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { } type RepoArtifactParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Artifact db.Artifact } @@ -544,7 +548,7 @@ func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) err } type RepoBlobParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string BreadCrumbs [][]string @@ -606,7 +610,7 @@ type Collaborator struct { } type RepoSettingsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Collaborators []Collaborator Active string @@ -622,7 +626,7 @@ func (p *Pages) RepoSettings(w io.Writer, params RepoSettingsParams) error { } type RepoIssuesParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string Issues []db.Issue @@ -637,7 +641,7 @@ func (p *Pages) RepoIssues(w io.Writer, params RepoIssuesParams) error { } type RepoSingleIssueParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string Issue db.Issue @@ -659,7 +663,7 @@ func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error } type RepoNewIssueParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string } @@ -670,7 +674,7 @@ func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error { } type EditIssueCommentParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Issue *db.Issue Comment *db.Comment @@ -681,7 +685,7 @@ func (p *Pages) EditIssueCommentFragment(w io.Writer, params EditIssueCommentPar } type SingleIssueCommentParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User DidHandleMap map[string]string RepoInfo repoinfo.RepoInfo Issue *db.Issue @@ -693,7 +697,7 @@ func (p *Pages) SingleIssueCommentFragment(w io.Writer, params SingleIssueCommen } type RepoNewPullParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Branches []types.Branch Active string @@ -705,7 +709,7 @@ func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error { } type RepoPullsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Pulls []*db.Pull Active string @@ -737,7 +741,7 @@ func (r ResubmitResult) Unknown() bool { } type RepoSinglePullParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Active string DidHandleMap map[string]string @@ -752,7 +756,7 @@ func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { } type RepoPullPatchParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User DidHandleMap map[string]string RepoInfo repoinfo.RepoInfo Pull *db.Pull @@ -767,7 +771,7 @@ func (p *Pages) RepoPullPatchPage(w io.Writer, params RepoPullPatchParams) error } type RepoPullInterdiffParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User DidHandleMap map[string]string RepoInfo repoinfo.RepoInfo Pull *db.Pull @@ -817,7 +821,7 @@ func (p *Pages) PullCompareForkBranchesFragment(w io.Writer, params PullCompareF } type PullResubmitParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Pull *db.Pull SubmissionId int @@ -828,7 +832,7 @@ func (p *Pages) PullResubmitFragment(w io.Writer, params PullResubmitParams) err } type PullActionsParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Pull *db.Pull RoundNumber int @@ -841,7 +845,7 @@ func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error } type PullNewCommentParams struct { - LoggedInUser *auth.User + LoggedInUser *oauth.User RepoInfo repoinfo.RepoInfo Pull *db.Pull RoundNumber int diff --git a/appview/pages/templates/user/oauthlogin.html b/appview/pages/templates/user/oauthlogin.html new file mode 100644 index 0000000..1eaaead --- /dev/null +++ b/appview/pages/templates/user/oauthlogin.html @@ -0,0 +1,71 @@ +{{ define "user/oauthlogin" }} + + + + + + + + login + + +
+

+ tangled +

+

+ tightly-knit social coding. +

+
+
+ + + + Use your + Bluesky handle to log + in. You will then be redirected to your PDS to + complete authentication. + +
+ + +
+

+ Join our Discord or + IRC channel: + #tangled on Libera Chat. +

+

+
+ + +{{ end }} diff --git a/appview/settings/settings.go b/appview/settings/settings.go index 1fafa21..a2b57a2 100644 --- a/appview/settings/settings.go +++ b/appview/settings/settings.go @@ -13,10 +13,10 @@ import ( "github.com/go-chi/chi/v5" "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview" - "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" "tangled.sh/tangled.sh/core/appview/email" "tangled.sh/tangled.sh/core/appview/middleware" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -27,7 +27,7 @@ import ( type Settings struct { Db *db.DB - Auth *auth.Auth + OAuth *oauth.OAuth Pages *pages.Pages Config *appview.Config } @@ -35,7 +35,7 @@ type Settings struct { func (s *Settings) Router() http.Handler { r := chi.NewRouter() - r.Use(middleware.AuthMiddleware(s.Auth)) + r.Use(middleware.AuthMiddleware(s.OAuth)) r.Get("/", s.settings) @@ -56,7 +56,7 @@ func (s *Settings) Router() http.Handler { } func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { - user := s.Auth.GetUser(r) + user := s.OAuth.GetUser(r) pubKeys, err := db.GetPublicKeys(s.Db, user.Did) if err != nil { log.Println(err) @@ -79,7 +79,7 @@ func (s *Settings) buildVerificationEmail(emailAddr, did, code string) email.Ema verifyURL := s.verifyUrl(did, emailAddr, code) return email.Email{ - APIKey: s.Config.ResendApiKey, + APIKey: s.Config.Resend.ApiKey, From: "noreply@notifs.tangled.sh", To: emailAddr, Subject: "Verify your Tangled email", @@ -111,7 +111,7 @@ func (s *Settings) emails(w http.ResponseWriter, r *http.Request) { log.Println("unimplemented") return case http.MethodPut: - did := s.Auth.GetDid(r) + did := s.OAuth.GetDid(r) emAddr := r.FormValue("email") emAddr = strings.TrimSpace(emAddr) @@ -174,7 +174,7 @@ func (s *Settings) emails(w http.ResponseWriter, r *http.Request) { s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") return case http.MethodDelete: - did := s.Auth.GetDid(r) + did := s.OAuth.GetDid(r) emailAddr := r.FormValue("email") emailAddr = strings.TrimSpace(emailAddr) @@ -207,8 +207,8 @@ func (s *Settings) emails(w http.ResponseWriter, r *http.Request) { func (s *Settings) verifyUrl(did string, email string, code string) string { var appUrl string - if s.Config.Dev { - appUrl = "http://" + s.Config.ListenAddr + if s.Config.Core.Dev { + appUrl = "http://" + s.Config.Core.ListenAddr } else { appUrl = "https://tangled.sh" } @@ -252,7 +252,7 @@ func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) { return } - did := s.Auth.GetDid(r) + did := s.OAuth.GetDid(r) emAddr := r.FormValue("email") emAddr = strings.TrimSpace(emAddr) @@ -323,7 +323,7 @@ func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) { } func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) { - did := s.Auth.GetDid(r) + did := s.OAuth.GetDid(r) emailAddr := r.FormValue("email") emailAddr = strings.TrimSpace(emailAddr) @@ -348,13 +348,17 @@ func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { log.Println("unimplemented") return case http.MethodPut: - did := s.Auth.GetDid(r) + did := s.OAuth.GetDid(r) key := r.FormValue("key") key = strings.TrimSpace(key) name := r.FormValue("name") - client, _ := s.Auth.AuthorizedClient(r) + client, err := s.OAuth.AuthorizedClient(r) + if err != nil { + s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.") + return + } - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) + _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key)) if err != nil { log.Printf("parsing public key: %s", err) s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a public key.") @@ -378,7 +382,7 @@ func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { } // store in pds too - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.PublicKeyNSID, Repo: did, Rkey: rkey, @@ -409,7 +413,7 @@ func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { return case http.MethodDelete: - did := s.Auth.GetDid(r) + did := s.OAuth.GetDid(r) q := r.URL.Query() name := q.Get("name") @@ -420,7 +424,12 @@ func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { log.Println(rkey) log.Println(key) - client, _ := s.Auth.AuthorizedClient(r) + client, err := s.OAuth.AuthorizedClient(r) + if err != nil { + log.Printf("failed to authorize client: %s", err) + s.Pages.Notice(w, "settings-keys", "Failed to authorize client.") + return + } if err := db.DeletePublicKey(s.Db, did, name, key); err != nil { log.Printf("removing public key: %s", err) @@ -430,7 +439,7 @@ func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { if rkey != "" { // remove from pds too - _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ + _, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.PublicKeyNSID, Repo: did, Rkey: rkey, diff --git a/appview/state/artifact.go b/appview/state/artifact.go index 020cba7..8cb629c 100644 --- a/appview/state/artifact.go +++ b/appview/state/artifact.go @@ -22,7 +22,7 @@ import ( // TODO: proper statuses here on early exit func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) tagParam := chi.URLParam(r, "tag") f, err := s.fullyResolvedRepo(r) if err != nil { @@ -46,9 +46,14 @@ func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { } defer file.Close() - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "upload", "failed to get authorized client") + return + } - uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) + uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file) if err != nil { log.Println("failed to upload blob", err) s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") @@ -60,7 +65,7 @@ func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { rkey := appview.TID() createdAt := time.Now() - putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoArtifactNSID, Repo: user.Did, Rkey: rkey, @@ -140,7 +145,11 @@ func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) { return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + return + } artifacts, err := db.GetArtifact( s.db, @@ -159,7 +168,7 @@ func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) { artifact := artifacts[0] - getBlobResp, err := comatproto.SyncGetBlob(r.Context(), client, artifact.BlobCid.String(), artifact.Did) + getBlobResp, err := client.SyncGetBlob(r.Context(), artifact.BlobCid.String(), artifact.Did) if err != nil { log.Println("failed to get blob from pds", err) return @@ -171,7 +180,7 @@ func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) { // TODO: proper statuses here on early exit func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) tagParam := chi.URLParam(r, "tag") filename := chi.URLParam(r, "file") f, err := s.fullyResolvedRepo(r) @@ -180,7 +189,7 @@ func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { return } - client, _ := s.auth.AuthorizedClient(r) + client, _ := s.oauth.AuthorizedClient(r) tag := plumbing.NewHash(tagParam) @@ -208,7 +217,7 @@ func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { return } - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.RepoArtifactNSID, Repo: user.Did, Rkey: artifact.Rkey, @@ -254,7 +263,7 @@ func (s *State) resolveTag(f *FullyResolvedRepo, tagParam string) (*types.TagRef return nil, err } - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { return nil, err } diff --git a/appview/state/follow.go b/appview/state/follow.go index 74b5f5a..4a99f8b 100644 --- a/appview/state/follow.go +++ b/appview/state/follow.go @@ -14,7 +14,7 @@ import ( ) func (s *State) Follow(w http.ResponseWriter, r *http.Request) { - currentUser := s.auth.GetUser(r) + currentUser := s.oauth.GetUser(r) subject := r.URL.Query().Get("subject") if subject == "" { @@ -32,13 +32,17 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to authorize client") + return + } switch r.Method { case http.MethodPost: createdAt := time.Now().Format(time.RFC3339) rkey := appview.TID() - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.GraphFollowNSID, Repo: currentUser.Did, Rkey: rkey, @@ -75,7 +79,7 @@ func (s *State) Follow(w http.ResponseWriter, r *http.Request) { return } - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.GraphFollowNSID, Repo: currentUser.Did, Rkey: follow.Rkey, diff --git a/appview/state/git_http.go b/appview/state/git_http.go index d9296fb..8f0a023 100644 --- a/appview/state/git_http.go +++ b/appview/state/git_http.go @@ -15,7 +15,7 @@ func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { repo := chi.URLParam(r, "repo") scheme := "https" - if s.config.Dev { + if s.config.Core.Dev { scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) @@ -52,7 +52,7 @@ func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { repo := chi.URLParam(r, "repo") scheme := "https" - if s.config.Dev { + if s.config.Core.Dev { scheme = "http" } targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) diff --git a/appview/state/middleware.go b/appview/state/middleware.go index c3c5cdb..1baabcf 100644 --- a/appview/state/middleware.go +++ b/appview/state/middleware.go @@ -20,7 +20,7 @@ func knotRoleMiddleware(s *State, group string) middleware.Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // requires auth also - actor := s.auth.GetUser(r) + actor := s.oauth.GetUser(r) if actor == nil { // we need a logged in user log.Printf("not logged in, redirecting") @@ -54,7 +54,7 @@ func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middlewa return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // requires auth also - actor := s.auth.GetUser(r) + actor := s.oauth.GetUser(r) if actor == nil { // we need a logged in user log.Printf("not logged in, redirecting") diff --git a/appview/state/profile.go b/appview/state/profile.go index a5c3fc6..ebec453 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -119,7 +119,7 @@ func (s *State) profilePage(w http.ResponseWriter, r *http.Request) { log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) } - loggedInUser := s.auth.GetUser(r) + loggedInUser := s.oauth.GetUser(r) followStatus := db.IsNotFollowing if loggedInUser != nil { followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) @@ -161,7 +161,7 @@ func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { log.Printf("getting repos for %s: %s", ident.DID.String(), err) } - loggedInUser := s.auth.GetUser(r) + loggedInUser := s.oauth.GetUser(r) followStatus := db.IsNotFollowing if loggedInUser != nil { followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) @@ -190,15 +190,15 @@ func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { } func (s *State) GetAvatarUri(handle string) string { - secret := s.config.AvatarSharedSecret + secret := s.config.Avatar.SharedSecret h := hmac.New(sha256.New, []byte(secret)) h.Write([]byte(handle)) signature := hex.EncodeToString(h.Sum(nil)) - return fmt.Sprintf("%s/%s/%s", s.config.AvatarHost, signature, handle) + return fmt.Sprintf("%s/%s/%s", s.config.Avatar.Host, signature, handle) } func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) err := r.ParseForm() if err != nil { @@ -246,7 +246,7 @@ func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { } func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) err := r.ParseForm() if err != nil { @@ -286,7 +286,7 @@ func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { } func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) tx, err := s.db.BeginTx(r.Context(), nil) if err != nil { log.Println("failed to start transaction", err) @@ -294,7 +294,12 @@ func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *htt return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") + return + } // yeah... lexgen dose not support syntax.ATURI in the record for some reason, // nor does it support exact size arrays @@ -308,13 +313,13 @@ func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *htt vanityStats = append(vanityStats, string(v.Kind)) } - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") + ex, _ := client.RepoGetRecord(r.Context(), "", tangled.ActorProfileNSID, user.Did, "self") var cid *string if ex != nil { cid = ex.Cid } - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.ActorProfileNSID, Repo: user.Did, Rkey: "self", @@ -347,7 +352,7 @@ func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *htt } func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) profile, err := db.GetProfile(s.db, user.Did) if err != nil { @@ -361,7 +366,7 @@ func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { } func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) profile, err := db.GetProfile(s.db, user.Did) if err != nil { diff --git a/appview/state/pull.go b/appview/state/pull.go index db6667d..7a08b69 100644 --- a/appview/state/pull.go +++ b/appview/state/pull.go @@ -13,8 +13,8 @@ import ( "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview" - "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" "tangled.sh/tangled.sh/core/patchutil" "tangled.sh/tangled.sh/core/types" @@ -29,7 +29,7 @@ import ( func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -73,7 +73,7 @@ func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { } func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -143,7 +143,7 @@ func (s *State) mergeCheck(f *FullyResolvedRepo, pull *db.Pull) types.MergeCheck } } - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Core.Dev) if err != nil { log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) return types.MergeCheckResponse{ @@ -215,7 +215,7 @@ func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.Resubmi repoName = f.RepoName } - us, err := NewUnsignedClient(knot, s.config.Dev) + us, err := NewUnsignedClient(knot, s.config.Core.Dev) if err != nil { log.Printf("failed to setup client for %s; ignoring: %v", knot, err) return pages.Unknown @@ -250,7 +250,7 @@ func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.Resubmi } func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -298,7 +298,7 @@ func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { } func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { @@ -355,7 +355,7 @@ func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { interdiff := patchutil.Interdiff(previousPatch, currentPatch) s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ - LoggedInUser: s.auth.GetUser(r), + LoggedInUser: s.oauth.GetUser(r), RepoInfo: f.RepoInfo(s, user), Pull: pull, Round: roundIdInt, @@ -397,7 +397,7 @@ func (s *State) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { } func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) params := r.URL.Query() state := db.PullOpen @@ -451,7 +451,7 @@ func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { } s.pages.RepoPulls(w, pages.RepoPullsParams{ - LoggedInUser: s.auth.GetUser(r), + LoggedInUser: s.oauth.GetUser(r), RepoInfo: f.RepoInfo(s, user), Pulls: pulls, DidHandleMap: didHandleMap, @@ -461,7 +461,7 @@ func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { } func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -519,8 +519,13 @@ func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { } atUri := f.RepoAt.String() - client, _ := s.auth.AuthorizedClient(r) - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "pull-comment", "Failed to create comment.") + return + } + atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoPullCommentNSID, Repo: user.Did, Rkey: appview.TID(), @@ -568,7 +573,7 @@ func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { } func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -577,7 +582,7 @@ func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client for %s", f.Knot) s.pages.Error503(w) @@ -646,7 +651,7 @@ func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { return } - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") @@ -689,7 +694,7 @@ func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { } } -func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { +func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, sourceBranch string) { pullSource := &db.PullSource{ Branch: sourceBranch, } @@ -698,7 +703,7 @@ func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f } // Generate a patch using /compare - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) + ksClient, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create signed client for %s: %s", f.Knot, err) s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") @@ -723,7 +728,7 @@ func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) } -func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { +func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, patch string) { if !patchutil.IsPatchValid(patch) { s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") return @@ -732,7 +737,7 @@ func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f * s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) } -func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { +func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) if errors.Is(err, sql.ErrNoRows) { s.pages.Notice(w, "pull", "No such fork.") @@ -750,14 +755,14 @@ func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *F return } - sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) + sc, err := NewSignedClient(fork.Knot, secret, s.config.Core.Dev) if err != nil { log.Println("failed to create signed client:", err) s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") return } - us, err := NewUnsignedClient(fork.Knot, s.config.Dev) + us, err := NewUnsignedClient(fork.Knot, s.config.Core.Dev) if err != nil { log.Println("failed to create unsigned client:", err) s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") @@ -816,7 +821,7 @@ func (s *State) createPullRequest( w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, - user *auth.User, + user *oauth.User, title, body, targetBranch string, patch string, sourceRev string, @@ -870,7 +875,12 @@ func (s *State) createPullRequest( s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") + return + } pullId, err := db.NextPullId(s.db, f.RepoAt) if err != nil { log.Println("failed to get pull id", err) @@ -878,7 +888,7 @@ func (s *State) createPullRequest( return } - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoPullNSID, Repo: user.Did, Rkey: rkey, @@ -929,7 +939,7 @@ func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { } func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -942,14 +952,14 @@ func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { } func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) return } - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client for %s", f.Knot) s.pages.Error503(w) @@ -982,7 +992,7 @@ func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) } func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1002,7 +1012,7 @@ func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { } func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { @@ -1019,7 +1029,7 @@ func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Requ return } - sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) + sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client for %s", repo.Knot) s.pages.Error503(w) @@ -1046,7 +1056,7 @@ func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Requ return } - targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) + targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client for target knot %s", f.Knot) s.pages.Error503(w) @@ -1081,7 +1091,7 @@ func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Requ } func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1117,7 +1127,7 @@ func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { } func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) pull, ok := r.Context().Value("pull").(*db.Pull) if !ok { @@ -1159,16 +1169,21 @@ func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") + return + } - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) if err != nil { // failed to get record s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") return } - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoPullNSID, Repo: user.Did, Rkey: pull.Rkey, @@ -1200,7 +1215,7 @@ func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { } func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) pull, ok := r.Context().Value("pull").(*db.Pull) if !ok { @@ -1227,7 +1242,7 @@ func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { return } - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) + ksClient, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create client for %s: %s", f.Knot, err) s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") @@ -1268,9 +1283,14 @@ func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to authorize client") + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") + return + } - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) if err != nil { // failed to get record s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") @@ -1280,7 +1300,7 @@ func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { recordPullSource := &tangled.RepoPull_Source{ Branch: pull.PullSource.Branch, } - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoPullNSID, Repo: user.Did, Rkey: pull.Rkey, @@ -1313,7 +1333,7 @@ func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { } func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) pull, ok := r.Context().Value("pull").(*db.Pull) if !ok { @@ -1342,7 +1362,7 @@ func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { } // extract patch by performing compare - ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) + ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") @@ -1357,7 +1377,7 @@ func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { } // update the hidden tracking branch to latest - signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) + signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) if err != nil { log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") @@ -1406,9 +1426,14 @@ func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get client") + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") + return + } - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) if err != nil { // failed to get record s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") @@ -1420,7 +1445,7 @@ func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { Branch: pull.PullSource.Branch, Repo: &repoAt, } - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoPullNSID, Repo: user.Did, Rkey: pull.Rkey, @@ -1503,7 +1528,7 @@ func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { log.Printf("failed to get primary email: %s", err) } - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Core.Dev) if err != nil { log.Printf("failed to create signed client for %s: %s", f.Knot, err) s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") @@ -1533,7 +1558,7 @@ func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { } func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { @@ -1587,7 +1612,7 @@ func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { } func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { diff --git a/appview/state/repo.go b/appview/state/repo.go index 6a85925..9c0fa29 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -18,8 +18,8 @@ import ( "tangled.sh/tangled.sh/core/api/tangled" "tangled.sh/tangled.sh/core/appview" - "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" "tangled.sh/tangled.sh/core/appview/pages/markup" "tangled.sh/tangled.sh/core/appview/pages/repoinfo" @@ -45,7 +45,7 @@ func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { return } - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client for %s", f.Knot) s.pages.Error503(w) @@ -119,7 +119,7 @@ func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { emails := uniqueEmails(commitsTrunc) - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.RepoIndexPage(w, pages.RepoIndexParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(s, user), @@ -150,7 +150,7 @@ func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { ref := chi.URLParam(r, "ref") - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Println("failed to create unsigned client", err) return @@ -190,7 +190,7 @@ func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { tagMap[hash] = append(tagMap[hash], tag.Name) } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.RepoLog(w, pages.RepoLogParams{ LoggedInUser: user, TagMap: tagMap, @@ -209,7 +209,7 @@ func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { return } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ RepoInfo: f.RepoInfo(s, user), }) @@ -232,7 +232,7 @@ func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { return } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) switch r.Method { case http.MethodGet: @@ -241,9 +241,14 @@ func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { }) return case http.MethodPut: - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) newDescription := r.FormValue("description") - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get client") + s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") + return + } // optimistic update err = db.UpdateDescription(s.db, string(repoAt), newDescription) @@ -256,13 +261,13 @@ func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field // // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey) + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) if err != nil { // failed to get record s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") return } - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoNSID, Repo: user.Did, Rkey: rkey, @@ -303,7 +308,7 @@ func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { } ref := chi.URLParam(r, "ref") protocol := "http" - if !s.config.Dev { + if !s.config.Core.Dev { protocol = "https" } @@ -331,7 +336,7 @@ func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { return } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.RepoCommit(w, pages.RepoCommitParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(s, user), @@ -351,7 +356,7 @@ func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { ref := chi.URLParam(r, "ref") treePath := chi.URLParam(r, "*") protocol := "http" - if !s.config.Dev { + if !s.config.Core.Dev { protocol = "https" } resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) @@ -380,7 +385,7 @@ func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { return } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) var breadcrumbs [][]string breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) @@ -411,7 +416,7 @@ func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { return } - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Println("failed to create unsigned client", err) return @@ -451,7 +456,7 @@ func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { } } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.RepoTags(w, pages.RepoTagsParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(s, user), @@ -469,7 +474,7 @@ func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { return } - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Println("failed to create unsigned client", err) return @@ -511,7 +516,7 @@ func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { return strings.Compare(a.Name, b.Name) * -1 }) - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.RepoBranches(w, pages.RepoBranchesParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(s, user), @@ -530,7 +535,7 @@ func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { ref := chi.URLParam(r, "ref") filePath := chi.URLParam(r, "*") protocol := "http" - if !s.config.Dev { + if !s.config.Core.Dev { protocol = "https" } resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) @@ -568,7 +573,7 @@ func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { showRendered = r.URL.Query().Get("code") != "true" } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) s.pages.RepoBlob(w, pages.RepoBlobParams{ LoggedInUser: user, RepoInfo: f.RepoInfo(s, user), @@ -591,7 +596,7 @@ func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { filePath := chi.URLParam(r, "*") protocol := "http" - if !s.config.Dev { + if !s.config.Core.Dev { protocol = "https" } resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) @@ -652,7 +657,7 @@ func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { return } - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Core.Dev) if err != nil { log.Println("failed to create client to ", f.Knot) return @@ -714,7 +719,7 @@ func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { } func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { @@ -723,9 +728,13 @@ func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { } // remove record from pds - xrpcClient, _ := s.auth.AuthorizedClient(r) + xrpcClient, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + return + } repoRkey := f.RepoAt.RecordKey().String() - _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ + _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.RepoNSID, Repo: user.Did, Rkey: repoRkey, @@ -743,7 +752,7 @@ func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { return } - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Core.Dev) if err != nil { log.Println("failed to create client to ", f.Knot) return @@ -838,7 +847,7 @@ func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { return } - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Core.Dev) if err != nil { log.Println("failed to create client to ", f.Knot) return @@ -868,7 +877,7 @@ func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: // for now, this is just pubkeys - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) repoCollaborators, err := f.Collaborators(r.Context(), s) if err != nil { log.Println("failed to get collaborators", err) @@ -884,7 +893,7 @@ func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { var branchNames []string var defaultBranch string - us, err := NewUnsignedClient(f.Knot, s.config.Dev) + us, err := NewUnsignedClient(f.Knot, s.config.Core.Dev) if err != nil { log.Println("failed to create unsigned client", err) } else { @@ -1008,7 +1017,7 @@ func (f *FullyResolvedRepo) Collaborators(ctx context.Context, s *State) ([]page return collaborators, nil } -func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo { +func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo { isStarred := false if u != nil { isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) @@ -1051,7 +1060,7 @@ func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo { knot := f.Knot var disableFork bool - us, err := NewUnsignedClient(knot, s.config.Dev) + us, err := NewUnsignedClient(knot, s.config.Core.Dev) if err != nil { log.Printf("failed to create unsigned client for %s: %v", knot, err) } else { @@ -1105,7 +1114,7 @@ func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo { } func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1159,7 +1168,7 @@ func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { } func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1195,8 +1204,12 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { closed := tangled.RepoIssueStateClosed - client, _ := s.auth.AuthorizedClient(r) - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + return + } + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueStateNSID, Repo: user.Did, Rkey: appview.TID(), @@ -1214,7 +1227,7 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { return } - err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) + err = db.CloseIssue(s.db, f.RepoAt, issueIdInt) if err != nil { log.Println("failed to close issue", err) s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") @@ -1231,7 +1244,7 @@ func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { } func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1279,7 +1292,7 @@ func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { } func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1330,8 +1343,13 @@ func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { } atUri := f.RepoAt.String() - client, _ := s.auth.AuthorizedClient(r) - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "issue-comment", "Failed to create comment.") + return + } + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueCommentNSID, Repo: user.Did, Rkey: rkey, @@ -1358,7 +1376,7 @@ func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { } func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1417,7 +1435,7 @@ func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { } func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1469,7 +1487,12 @@ func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { case http.MethodPost: // extract form value newBody := r.FormValue("body") - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "issue-comment", "Failed to create comment.") + return + } rkey := comment.Rkey // optimistic update @@ -1484,7 +1507,7 @@ func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { // rkey is optional, it was introduced later if comment.Rkey != "" { // update the record on pds - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey) + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) if err != nil { // failed to get record log.Println(err, rkey) @@ -1499,7 +1522,7 @@ func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { createdAt := record["createdAt"].(string) commentIdInt64 := int64(commentIdInt) - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueCommentNSID, Repo: user.Did, Rkey: rkey, @@ -1542,7 +1565,7 @@ func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { } func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1599,8 +1622,13 @@ func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { // delete from pds if comment.Rkey != "" { - client, _ := s.auth.AuthorizedClient(r) - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "issue-comment", "Failed to delete comment.") + return + } + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.GraphFollowNSID, Repo: user.Did, Rkey: comment.Rkey, @@ -1647,7 +1675,7 @@ func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { page = pagination.FirstPage() } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Println("failed to get repo and knot", err) @@ -1676,7 +1704,7 @@ func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { } s.pages.RepoIssues(w, pages.RepoIssuesParams{ - LoggedInUser: s.auth.GetUser(r), + LoggedInUser: s.oauth.GetUser(r), RepoInfo: f.RepoInfo(s, user), Issues: issues, DidHandleMap: didHandleMap, @@ -1687,7 +1715,7 @@ func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { } func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { @@ -1735,9 +1763,14 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "issues", "Failed to create issue.") + return + } atUri := f.RepoAt.String() - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoIssueNSID, Repo: user.Did, Rkey: appview.TID(), @@ -1770,7 +1803,7 @@ func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { } func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) f, err := s.fullyResolvedRepo(r) if err != nil { log.Printf("failed to resolve source repo: %v", err) @@ -1779,7 +1812,7 @@ func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) knots, err := s.enforcer.GetDomainsForUser(user.Did) if err != nil { s.pages.Notice(w, "repo", "Invalid user account.") @@ -1829,14 +1862,14 @@ func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { return } - client, err := NewSignedClient(knot, secret, s.config.Dev) + client, err := NewSignedClient(knot, secret, s.config.Core.Dev) if err != nil { s.pages.Notice(w, "repo", "Failed to reach knot server.") return } var uri string - if s.config.Dev { + if s.config.Core.Dev { uri = "http" } else { uri = "https" @@ -1883,10 +1916,15 @@ func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { // continue } - xrpcClient, _ := s.auth.AuthorizedClient(r) + xrpcClient, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to get authorized client", err) + s.pages.Notice(w, "repo", "Failed to create repository.") + return + } createdAt := time.Now().Format(time.RFC3339) - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoNSID, Repo: user.Did, Rkey: rkey, diff --git a/appview/state/repo_util.go b/appview/state/repo_util.go index 9ca3b2d..df8d867 100644 --- a/appview/state/repo_util.go +++ b/appview/state/repo_util.go @@ -12,8 +12,8 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" "github.com/go-git/go-git/v5/plumbing/object" - "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages/repoinfo" ) @@ -45,7 +45,7 @@ func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { ref := chi.URLParam(r, "ref") if ref == "" { - us, err := NewUnsignedClient(knot, s.config.Dev) + us, err := NewUnsignedClient(knot, s.config.Core.Dev) if err != nil { return nil, err } @@ -73,7 +73,7 @@ func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { }, nil } -func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { +func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { if u != nil { r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) return repoinfo.RolesInRepo{r} diff --git a/appview/state/router.go b/appview/state/router.go index 788ea62..44605b4 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -5,7 +5,9 @@ import ( "strings" "github.com/go-chi/chi/v5" + "github.com/gorilla/sessions" "tangled.sh/tangled.sh/core/appview/middleware" + oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" "tangled.sh/tangled.sh/core/appview/settings" "tangled.sh/tangled.sh/core/appview/state/userutil" ) @@ -67,7 +69,7 @@ func (s *State) UserRouter() http.Handler { r.Route("/tags", func(r chi.Router) { r.Get("/", s.RepoTags) r.Route("/{tag}", func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) // require auth to download for now r.Get("/download/{file}", s.DownloadArtifact) @@ -90,7 +92,7 @@ func (s *State) UserRouter() http.Handler { r.Get("/{issue}", s.RepoSingleIssue) r.Group(func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/new", s.NewIssue) r.Post("/new", s.NewIssue) r.Post("/{issue}/comment", s.NewIssueComment) @@ -106,14 +108,14 @@ func (s *State) UserRouter() http.Handler { }) r.Route("/fork", func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/", s.ForkRepo) r.Post("/", s.ForkRepo) }) r.Route("/pulls", func(r chi.Router) { r.Get("/", s.RepoPulls) - r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { r.Get("/", s.NewPull) r.Get("/patch-upload", s.PatchUploadFragment) r.Post("/validate-patch", s.ValidatePatch) @@ -131,7 +133,7 @@ func (s *State) UserRouter() http.Handler { r.Get("/", s.RepoPullPatch) r.Get("/interdiff", s.RepoPullInterdiff) r.Get("/actions", s.PullActions) - r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { r.Get("/", s.PullComment) r.Post("/", s.PullComment) }) @@ -142,7 +144,7 @@ func (s *State) UserRouter() http.Handler { }) r.Group(func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) r.Route("/resubmit", func(r chi.Router) { r.Get("/", s.ResubmitPull) r.Post("/", s.ResubmitPull) @@ -165,7 +167,7 @@ func (s *State) UserRouter() http.Handler { // settings routes, needs auth r.Group(func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) // repo description can only be edited by owner r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { r.Put("/", s.RepoDescription) @@ -196,15 +198,15 @@ func (s *State) StandardRouter() http.Handler { r.Get("/", s.Timeline) - r.With(middleware.AuthMiddleware(s.auth)).Post("/logout", s.Logout) + r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout) - r.Route("/login", func(r chi.Router) { - r.Get("/", s.Login) - r.Post("/", s.Login) - }) + // r.Route("/login", func(r chi.Router) { + // r.Get("/", s.Login) + // r.Post("/", s.Login) + // }) r.Route("/knots", func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/", s.Knots) r.Post("/key", s.RegistrationKey) @@ -222,25 +224,25 @@ func (s *State) StandardRouter() http.Handler { r.Route("/repo", func(r chi.Router) { r.Route("/new", func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/", s.NewRepo) r.Post("/", s.NewRepo) }) // r.Post("/import", s.ImportRepo) }) - r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { r.Post("/", s.Follow) r.Delete("/", s.Follow) }) - r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) { + r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) { r.Post("/", s.Star) r.Delete("/", s.Star) }) r.Route("/profile", func(r chi.Router) { - r.Use(middleware.AuthMiddleware(s.auth)) + r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/edit-bio", s.EditBioFragment) r.Get("/edit-pins", s.EditPinsFragment) r.Post("/bio", s.UpdateProfileBio) @@ -248,7 +250,7 @@ func (s *State) StandardRouter() http.Handler { }) r.Mount("/settings", s.SettingsRouter()) - + r.Mount("/oauth", s.OAuthRouter()) r.Get("/keys/{user}", s.Keys) r.NotFound(func(w http.ResponseWriter, r *http.Request) { @@ -257,10 +259,23 @@ func (s *State) StandardRouter() http.Handler { return r } +func (s *State) OAuthRouter() http.Handler { + oauth := &oauthhandler.OAuthHandler{ + Config: s.config, + Pages: s.pages, + Resolver: s.resolver, + Db: s.db, + Store: sessions.NewCookieStore([]byte(s.config.Core.CookieSecret)), + OAuth: s.oauth, + } + + return oauth.Router() +} + func (s *State) SettingsRouter() http.Handler { settings := &settings.Settings{ Db: s.db, - Auth: s.auth, + OAuth: s.oauth, Pages: s.pages, Config: s.config, } diff --git a/appview/state/star.go b/appview/state/star.go index 08969f2..7f4635f 100644 --- a/appview/state/star.go +++ b/appview/state/star.go @@ -15,7 +15,7 @@ import ( ) func (s *State) Star(w http.ResponseWriter, r *http.Request) { - currentUser := s.auth.GetUser(r) + currentUser := s.oauth.GetUser(r) subject := r.URL.Query().Get("subject") if subject == "" { @@ -29,13 +29,17 @@ func (s *State) Star(w http.ResponseWriter, r *http.Request) { return } - client, _ := s.auth.AuthorizedClient(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + log.Println("failed to authorize client", err) + return + } switch r.Method { case http.MethodPost: createdAt := time.Now().Format(time.RFC3339) rkey := appview.TID() - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.FeedStarNSID, Repo: currentUser.Did, Rkey: rkey, @@ -80,7 +84,7 @@ func (s *State) Star(w http.ResponseWriter, r *http.Request) { return } - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ Collection: tangled.FeedStarNSID, Repo: currentUser.Did, Rkey: star.Rkey, diff --git a/appview/state/state.go b/appview/state/state.go index 7f901f4..7bb64d7 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -21,6 +21,7 @@ import ( "tangled.sh/tangled.sh/core/appview" "tangled.sh/tangled.sh/core/appview/auth" "tangled.sh/tangled.sh/core/appview/db" + "tangled.sh/tangled.sh/core/appview/oauth" "tangled.sh/tangled.sh/core/appview/pages" "tangled.sh/tangled.sh/core/jetstream" "tangled.sh/tangled.sh/core/rbac" @@ -29,8 +30,9 @@ import ( type State struct { db *db.DB auth *auth.Auth + oauth *oauth.OAuth enforcer *rbac.Enforcer - tidClock *syntax.TIDClock + tidClock syntax.TIDClock pages *pages.Pages resolver *appview.Resolver jc *jetstream.JetstreamClient @@ -38,17 +40,17 @@ type State struct { } func Make(config *appview.Config) (*State, error) { - d, err := db.Make(config.DbPath) + d, err := db.Make(config.Core.DbPath) if err != nil { return nil, err } - auth, err := auth.Make(config.CookieSecret) + auth, err := auth.Make(config.Core.CookieSecret) if err != nil { return nil, err } - enforcer, err := rbac.NewEnforcer(config.DbPath) + enforcer, err := rbac.NewEnforcer(config.Core.DbPath) if err != nil { return nil, err } @@ -59,9 +61,11 @@ func Make(config *appview.Config) (*State, error) { resolver := appview.NewResolver() + oauth := oauth.NewOAuth(d, config) + wrapper := db.DbWrapper{d} jc, err := jetstream.NewJetstreamClient( - config.JetstreamEndpoint, + config.Jetstream.Endpoint, "appview", []string{ tangled.GraphFollowNSID, @@ -86,6 +90,7 @@ func Make(config *appview.Config) (*State, error) { state := &State{ d, auth, + oauth, enforcer, clock, pgs, @@ -101,90 +106,90 @@ func TID(c *syntax.TIDClock) string { return c.Next().String() } -func (s *State) Login(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - switch r.Method { - case http.MethodGet: - err := s.pages.Login(w, pages.LoginParams{}) - if err != nil { - log.Printf("rendering login page: %s", err) - } - - return - case http.MethodPost: - handle := strings.TrimPrefix(r.FormValue("handle"), "@") - appPassword := r.FormValue("app_password") - - resolved, err := s.resolver.ResolveIdent(ctx, handle) - if err != nil { - log.Println("failed to resolve handle:", err) - s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) - return - } - - atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) - if err != nil { - s.pages.Notice(w, "login-msg", "Invalid handle or password.") - return - } - sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} - - err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) - if err != nil { - s.pages.Notice(w, "login-msg", "Failed to login, try again later.") - return - } - - log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) - - did := resolved.DID.String() - defaultKnot := "knot1.tangled.sh" - - go func() { - log.Printf("adding %s to default knot", did) - err = s.enforcer.AddMember(defaultKnot, did) - if err != nil { - log.Println("failed to add user to knot1.tangled.sh: ", err) - return - } - err = s.enforcer.E.SavePolicy() - if err != nil { - log.Println("failed to add user to knot1.tangled.sh: ", err) - return - } - - secret, err := db.GetRegistrationKey(s.db, defaultKnot) - if err != nil { - log.Println("failed to get registration key for knot1.tangled.sh") - return - } - signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev) - resp, err := signedClient.AddMember(did) - if err != nil { - log.Println("failed to add user to knot1.tangled.sh: ", err) - return - } - - if resp.StatusCode != http.StatusNoContent { - log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) - return - } - }() - - s.pages.HxRedirect(w, "/") - return - } -} +// func (s *State) Login(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() + +// switch r.Method { +// case http.MethodGet: +// err := s.pages.Login(w, pages.LoginParams{}) +// if err != nil { +// log.Printf("rendering login page: %s", err) +// } + +// return +// case http.MethodPost: +// handle := strings.TrimPrefix(r.FormValue("handle"), "@") +// appPassword := r.FormValue("app_password") + +// resolved, err := s.resolver.ResolveIdent(ctx, handle) +// if err != nil { +// log.Println("failed to resolve handle:", err) +// s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) +// return +// } + +// atSession, err := s.oauth.CreateInitialSession(ctx, resolved, appPassword) +// if err != nil { +// s.pages.Notice(w, "login-msg", "Invalid handle or password.") +// return +// } +// sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} + +// err = s.oauth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) +// if err != nil { +// s.pages.Notice(w, "login-msg", "Failed to login, try again later.") +// return +// } + +// log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) + +// did := resolved.DID.String() +// defaultKnot := "knot1.tangled.sh" + +// go func() { +// log.Printf("adding %s to default knot", did) +// err = s.enforcer.AddMember(defaultKnot, did) +// if err != nil { +// log.Println("failed to add user to knot1.tangled.sh: ", err) +// return +// } +// err = s.enforcer.E.SavePolicy() +// if err != nil { +// log.Println("failed to add user to knot1.tangled.sh: ", err) +// return +// } + +// secret, err := db.GetRegistrationKey(s.db, defaultKnot) +// if err != nil { +// log.Println("failed to get registration key for knot1.tangled.sh") +// return +// } +// signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Core.Dev) +// resp, err := signedClient.AddMember(did) +// if err != nil { +// log.Println("failed to add user to knot1.tangled.sh: ", err) +// return +// } + +// if resp.StatusCode != http.StatusNoContent { +// log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) +// return +// } +// }() + +// s.pages.HxRedirect(w, "/") +// return +// } +// } func (s *State) Logout(w http.ResponseWriter, r *http.Request) { - s.auth.ClearSession(r, w) + s.oauth.ClearSession(r, w) w.Header().Set("HX-Redirect", "/login") w.WriteHeader(http.StatusSeeOther) } func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) timeline, err := db.MakeTimeline(s.db) if err != nil { @@ -235,7 +240,7 @@ func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { return case http.MethodPost: - session, err := s.auth.Store.Get(r, appview.SessionName) + session, err := s.oauth.Store.Get(r, appview.SessionName) if err != nil || session.IsNew { log.Println("unauthorized attempt to generate registration key") http.Error(w, "Forbidden", http.StatusUnauthorized) @@ -297,7 +302,7 @@ func (s *State) Keys(w http.ResponseWriter, r *http.Request) { // create a signed request and check if a node responds to that func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) domain := chi.URLParam(r, "domain") if domain == "" { @@ -312,7 +317,7 @@ func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { return } - client, err := NewSignedClient(domain, secret, s.config.Dev) + client, err := NewSignedClient(domain, secret, s.config.Core.Dev) if err != nil { log.Println("failed to create client to ", domain) } @@ -421,7 +426,7 @@ func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { return } - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) reg, err := db.RegistrationByDomain(s.db, domain) if err != nil { w.Write([]byte("failed to pull up registration info")) @@ -469,7 +474,7 @@ func (s *State) KnotServerInfo(w http.ResponseWriter, r *http.Request) { // get knots registered by this user func (s *State) Knots(w http.ResponseWriter, r *http.Request) { // for now, this is just pubkeys - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) registrations, err := db.RegistrationsByDid(s.db, user.Did) if err != nil { log.Println(err) @@ -522,10 +527,14 @@ func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) // announce this relation into the firehose, store into owners' pds - client, _ := s.auth.AuthorizedClient(r) - currentUser := s.auth.GetUser(r) + client, err := s.oauth.AuthorizedClient(r) + if err != nil { + http.Error(w, "failed to authorize client", http.StatusInternalServerError) + return + } + currentUser := s.oauth.GetUser(r) createdAt := time.Now().Format(time.RFC3339) - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.KnotMemberNSID, Repo: currentUser.Did, Rkey: appview.TID(), @@ -550,7 +559,7 @@ func (s *State) AddMember(w http.ResponseWriter, r *http.Request) { return } - ksClient, err := NewSignedClient(domain, secret, s.config.Dev) + ksClient, err := NewSignedClient(domain, secret, s.config.Core.Dev) if err != nil { log.Println("failed to create client to ", domain) return @@ -614,7 +623,7 @@ func validateRepoName(name string) error { func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) knots, err := s.enforcer.GetDomainsForUser(user.Did) if err != nil { s.pages.Notice(w, "repo", "Invalid user account.") @@ -627,7 +636,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { }) case http.MethodPost: - user := s.auth.GetUser(r) + user := s.oauth.GetUser(r) domain := r.FormValue("domain") if domain == "" { @@ -671,7 +680,7 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { return } - client, err := NewSignedClient(domain, secret, s.config.Dev) + client, err := NewSignedClient(domain, secret, s.config.Core.Dev) if err != nil { s.pages.Notice(w, "repo", "Failed to connect to knot server.") return @@ -686,10 +695,14 @@ func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { Description: description, } - xrpcClient, _ := s.auth.AuthorizedClient(r) + xrpcClient, err := s.oauth.AuthorizedClient(r) + if err != nil { + s.pages.Notice(w, "repo", "Failed to write record to PDS.") + return + } createdAt := time.Now().Format(time.RFC3339) - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ Collection: tangled.RepoNSID, Repo: user.Did, Rkey: rkey, diff --git a/appview/tid.go b/appview/tid.go index 46a4da6..7d0f026 100644 --- a/appview/tid.go +++ b/appview/tid.go @@ -4,7 +4,7 @@ import ( "github.com/bluesky-social/indigo/atproto/syntax" ) -var c *syntax.TIDClock = syntax.NewTIDClock(0) +var c syntax.TIDClock = syntax.NewTIDClock(0) func TID() string { return c.Next().String() diff --git a/cmd/appview/main.go b/cmd/appview/main.go index 2cc33c2..98b7a55 100644 --- a/cmd/appview/main.go +++ b/cmd/appview/main.go @@ -26,6 +26,6 @@ func main() { log.Fatal(err) } - log.Println("starting server on", c.ListenAddr) - log.Println(http.ListenAndServe(c.ListenAddr, state.Router())) + log.Println("starting server on", c.Core.ListenAddr) + log.Println(http.ListenAndServe(c.Core.ListenAddr, state.Router())) } -- 2.43.0 From ed80e5a577d40cbc2183e7dbc88df55bf48733a2 Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Sat, 10 May 2025 15:43:51 +0300 Subject: [PATCH 4/5] appview: user/login: improve login form --- appview/oauth/handler/handler.go | 11 ++- appview/pages/pages.go | 4 -- appview/pages/templates/user/login.html | 55 ++++++--------- appview/pages/templates/user/oauthlogin.html | 71 -------------------- appview/state/router.go | 7 +- 5 files changed, 28 insertions(+), 120 deletions(-) delete mode 100644 appview/pages/templates/user/oauthlogin.html diff --git a/appview/oauth/handler/handler.go b/appview/oauth/handler/handler.go index f342b99..62aff88 100644 --- a/appview/oauth/handler/handler.go +++ b/appview/oauth/handler/handler.go @@ -35,12 +35,12 @@ type OAuthHandler struct { func (o *OAuthHandler) Router() http.Handler { r := chi.NewRouter() - // gets mounted on /oauth - r.Get("/client-metadata.json", o.clientMetadata) - r.Get("/jwks.json", o.jwks) r.Get("/login", o.login) r.Post("/login", o.login) - r.Get("/callback", o.callback) + + r.Get("/oauth/client-metadata.json", o.clientMetadata) + r.Get("/oauth/jwks.json", o.jwks) + r.Get("/oauth/callback", o.callback) return r } @@ -84,11 +84,10 @@ func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -// temporary until we swap out the main login page func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - o.Pages.OAuthLogin(w, pages.LoginParams{}) + o.Pages.Login(w, pages.LoginParams{}) case http.MethodPost: handle := strings.TrimPrefix(r.FormValue("handle"), "@") diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 58af5e7..8a74a3c 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -249,10 +249,6 @@ func (p *Pages) Login(w io.Writer, params LoginParams) error { return p.executePlain("user/login", w, params) } -func (p *Pages) OAuthLogin(w io.Writer, params LoginParams) error { - return p.executePlain("user/oauthlogin", w, params) -} - type TimelineParams struct { LoggedInUser *oauth.User Timeline []db.TimelineEvent diff --git a/appview/pages/templates/user/login.html b/appview/pages/templates/user/login.html index b0a0785..b251f43 100644 --- a/appview/pages/templates/user/login.html +++ b/appview/pages/templates/user/login.html @@ -8,55 +8,43 @@ content="width=device-width, initial-scale=1.0" /> - + login -
-

+
+

tangled

tightly-knit social coding.

- - - You need to use your - Bluesky handle to log - in. - -
- -
- - - Generate an app password - here. + + Use your + Bluesky handle to log + in. You will then be redirected to your PDS to + complete authentication.
@@ -70,7 +58,8 @@

- Join our Discord or IRC channel: + Join our Discord or + IRC channel: #tangled on Libera Chat. diff --git a/appview/pages/templates/user/oauthlogin.html b/appview/pages/templates/user/oauthlogin.html deleted file mode 100644 index 1eaaead..0000000 --- a/appview/pages/templates/user/oauthlogin.html +++ /dev/null @@ -1,71 +0,0 @@ -{{ define "user/oauthlogin" }} - - - - - - - - login - - -

-

- tangled -

-

- tightly-knit social coding. -

-
-
- - - - Use your - Bluesky handle to log - in. You will then be redirected to your PDS to - complete authentication. - -
- - -
-

- Join our Discord or - IRC channel: - #tangled on Libera Chat. -

-

-
- - -{{ end }} diff --git a/appview/state/router.go b/appview/state/router.go index 44605b4..d8a5ad3 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -200,11 +200,6 @@ func (s *State) StandardRouter() http.Handler { r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout) - // r.Route("/login", func(r chi.Router) { - // r.Get("/", s.Login) - // r.Post("/", s.Login) - // }) - r.Route("/knots", func(r chi.Router) { r.Use(middleware.AuthMiddleware(s.oauth)) r.Get("/", s.Knots) @@ -250,7 +245,7 @@ func (s *State) StandardRouter() http.Handler { }) r.Mount("/settings", s.SettingsRouter()) - r.Mount("/oauth", s.OAuthRouter()) + r.Mount("/", s.OAuthRouter()) r.Get("/keys/{user}", s.Keys) r.NotFound(func(w http.ResponseWriter, r *http.Request) { -- 2.43.0 From d3581a3471401a666d8fa221c1992d7122fae76b Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Sat, 10 May 2025 15:55:54 +0300 Subject: [PATCH 5/5] appview: oauth/handler: implement logout --- appview/oauth/handler/handler.go | 14 ++++++++++++-- appview/pages/templates/user/login.html | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/appview/oauth/handler/handler.go b/appview/oauth/handler/handler.go index 62aff88..26f01fc 100644 --- a/appview/oauth/handler/handler.go +++ b/appview/oauth/handler/handler.go @@ -61,8 +61,6 @@ func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) { "token_endpoint_auth_signing_alg": "ES256", } - fmt.Println("clientMetadata", metadata) - w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(metadata) @@ -246,6 +244,18 @@ func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusFound) } +func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) { + err := o.OAuth.ClearSession(r, w) + if err != nil { + log.Println("failed to clear session:", err) + http.Redirect(w, r, "/", http.StatusFound) + return + } + + log.Println("session cleared successfully") + http.Redirect(w, r, "/", http.StatusFound) +} + func pubKeyFromJwk(jwks string) (jwk.Key, error) { k, err := helpers.ParseJWKFromBytes([]byte(jwks)) if err != nil { diff --git a/appview/pages/templates/user/login.html b/appview/pages/templates/user/login.html index b251f43..1c157fc 100644 --- a/appview/pages/templates/user/login.html +++ b/appview/pages/templates/user/login.html @@ -27,9 +27,9 @@

-- 2.43.0