backend for xcvr appview
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor oauth types and create xrpc client with beep to bsky functionality

+158 -58
+4 -7
server/cmd/main.go
··· 7 7 "xcvr-backend/internal/db" 8 8 "xcvr-backend/internal/handler" 9 9 "xcvr-backend/internal/log" 10 - "xcvr-backend/internal/oauth" 11 10 "xcvr-backend/internal/model" 11 + "xcvr-backend/internal/oauth" 12 12 13 13 "github.com/joho/godotenv" 14 14 ) 15 - 16 15 17 16 func main() { 18 17 logger := log.New(os.Stdout, true) ··· 20 19 gdeerr := godotenv.Load("../.env") 21 20 if gdeerr != nil { 22 21 logger.Println("i think you should make a .env file in the xcvr-backend directory !\n\nExample contents:\n-------------------------------------------------------------------\nPOSTGRES_USER=xcvr\nPOSTGRES_PASSWORD=secret\nPOSTGRES_DB=xcvrdb\nPOSTGRES_PORT=15432\n-------------------------------------------------------------------\n\nGood luck !\n\n") 23 - panic(gdeerr) 22 + panic(gdeerr) 24 23 } 25 24 store, err := db.Init() 26 25 defer store.Close() ··· 40 39 logger.Println(err.Error()) 41 40 panic(err) 42 41 } 43 - h := handler.New(store, logger, oauthclient) 42 + h := handler.New(store, &logger, oauthclient) 44 43 http.ListenAndServe(":8080", h.WithCORSAll()) 45 - 44 + 46 45 } 47 46 48 47 // func initChannel(w http.ResponseWriter, r *http.Request) { ··· 105 104 // } 106 105 // return ieOK 107 106 // } 108 - 109 -
+18 -7
server/internal/db/oauth.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 - "xcvr-backend/internal/oauth" 6 + "fmt" 7 + "xcvr-backend/internal/types" 7 8 ) 8 9 9 - func (s *Store) StoreOAuthRequest(req *oauth.OAuthRequest, ctx context.Context) error { 10 + func (s *Store) StoreOAuthRequest(req *types.OAuthRequest, ctx context.Context) error { 10 11 _, err := s.pool.Exec(ctx, ` 11 12 INSERT INTO oauthrequests ( 12 13 authserver_iss, ··· 27 28 return err 28 29 } 29 30 30 - func (s *Store) StoreOAuthSession(session *oauth.Session, ctx context.Context) error { 31 + func (s *Store) StoreOAuthSession(session *types.Session, ctx context.Context) error { 31 32 _, err := s.pool.Exec(ctx, ` 32 33 INSERT INTO oauthsessions ( 33 34 authserver_iss, ··· 59 60 return nil 60 61 } 61 62 62 - func (s *Store) GetOauthRequest(state string, ctx context.Context) (*oauth.OAuthRequest, error) { 63 + func (s *Store) GetOauthRequest(state string, ctx context.Context) (*types.OAuthRequest, error) { 63 64 rows, err := s.pool.Query(ctx, ` 64 65 SELECT 65 66 r.authserver_iss, ··· 76 77 return nil, errors.New("error querying for oauth request:" + err.Error()) 77 78 } 78 79 defer rows.Close() 79 - var req oauth.OAuthRequest 80 + var req types.OAuthRequest 80 81 ok := rows.Next() 81 82 if !ok { 82 83 return nil, errors.New("no rows") ··· 88 89 return &req, nil 89 90 } 90 91 91 - func (s *Store) GetOauthSesson(did string, ctx context.Context) (*oauth.Session, error) { 92 + func (s *Store) GetOauthSesson(did string, ctx context.Context) (*types.Session, error) { 92 93 rows, err := s.pool.Query(ctx, ` 93 94 SELECT 94 95 r.authserver_iss, ··· 109 110 return nil, errors.New("error querying oauthsessions:" + err.Error()) 110 111 } 111 112 defer rows.Close() 112 - var session oauth.Session 113 + var session types.Session 113 114 ok := rows.Next() 114 115 if !ok { 115 116 return nil, errors.New("no rows") ··· 140 141 } 141 142 return nil 142 143 } 144 + 145 + func (s *Store) SetDpopPdsNonce(did, dpopnonce string) error { 146 + _, err := s.pool.Exec(context.Background(), ` 147 + UPDATE oauthsessions SET dpop_pds_nonce = $1 WHERE did = $2 148 + `, dpopnonce, did) 149 + if err != nil { 150 + return errors.New(fmt.Sprintf("error updating dpop nonce for did %s: %s", did, err.Error())) 151 + } 152 + return nil 153 + }
+10 -7
server/internal/handler/handler.go
··· 1 1 package handler 2 2 3 3 import ( 4 + "github.com/gorilla/sessions" 4 5 "net/http" 5 6 "os" 6 - "github.com/gorilla/sessions" 7 7 "xcvr-backend/internal/db" 8 8 "xcvr-backend/internal/log" 9 9 "xcvr-backend/internal/oauth" 10 10 ) 11 11 12 12 type Handler struct { 13 - db *db.Store 13 + db *db.Store 14 14 sessionStore *sessions.CookieStore 15 - router *http.ServeMux 16 - logger log.Logger 17 - oauth *oauth.Service 15 + router *http.ServeMux 16 + logger *log.Logger 17 + oauth *oauth.Service 18 + xrpc *oauth.Client 18 19 } 19 20 20 - func New(db *db.Store, logger log.Logger, oauth *oauth.Service) *Handler { 21 + func New(db *db.Store, logger *log.Logger, oauthserv *oauth.Service) *Handler { 21 22 mux := http.NewServeMux() 22 23 sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) 23 - h := &Handler{db, sessionStore, mux, logger, oauth} 24 + xrpc := oauth.NewXRPCClient(db, logger) 25 + h := &Handler{db, sessionStore, mux, logger, oauthserv, xrpc} 24 26 // lrc handlers 25 27 mux.HandleFunc("GET /lrc/{user}/{rkey}/ws", h.acceptWebsocket) 26 28 mux.HandleFunc("POST /lrc/channel", postChannel) 27 29 mux.HandleFunc("POST /lrc/message", postMessage) 28 30 // beep handlers 29 31 mux.HandleFunc("POST /xcvr/profile", h.postProfile) 32 + mux.HandleFunc("POST /xcvr/beep", h.beep) 30 33 // lexicon handlers 31 34 mux.HandleFunc("GET /xrpc/org.xcvr.feed.getChannels", h.getChannels) 32 35 mux.HandleFunc("GET /xrpc/org.xcvr.lrc.getMessages", h.getMessages)
+4 -4
server/internal/handler/oauthHandlers.go
··· 154 154 } 155 155 156 156 func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) { 157 - did, handle, err := h.findDidAndHandle(w, r) 157 + did, handle, err := h.findDidAndHandle(r) 158 158 if err != nil { 159 - h.handleFindDidAndHandleError(w, r, err) 159 + h.handleFindDidAndHandleError(w, err) 160 160 return 161 161 } 162 162 w.Header().Set("Content-Type", "application/json") ··· 166 166 }) 167 167 } 168 168 169 - func (h *Handler) findDidAndHandle(w http.ResponseWriter, r *http.Request) (string, string, error) { 169 + func (h *Handler) findDidAndHandle(r *http.Request) (string, string, error) { 170 170 session, _ := h.sessionStore.Get(r, "oauthsession") 171 171 did, ok := session.Values["did"].(string) 172 172 if !ok || did == "" { ··· 187 187 return did, handle, nil 188 188 } 189 189 190 - func (h *Handler) handleFindDidAndHandleError(w http.ResponseWriter, r *http.Request, err error) { 190 + func (h *Handler) handleFindDidAndHandleError(w http.ResponseWriter, err error) { 191 191 if err != nil { 192 192 if err.Error() == "not authenticated" { 193 193 http.Error(w, "not authenticated", http.StatusUnauthorized)
+16 -2
server/internal/handler/xcvrHandlers.go
··· 11 11 ) 12 12 13 13 func (h *Handler) postProfile(w http.ResponseWriter, r *http.Request) { 14 - did, handle, err := h.findDidAndHandle(w, r) 14 + did, handle, err := h.findDidAndHandle(r) 15 15 if err != nil { 16 - h.handleFindDidAndHandleError(w, r, err) 16 + h.handleFindDidAndHandleError(w, err) 17 17 return 18 18 } 19 19 var p types.PostProfileRequest ··· 80 80 h.serverError(w, errors.New("error updating profile: "+err.Error())) 81 81 return 82 82 } 83 + 83 84 h.serveProfileView(did, handle, w, r) 84 85 } 86 + 87 + func (h *Handler) beep(w http.ResponseWriter, r *http.Request) { 88 + session, _ := h.sessionStore.Get(r, "oauthsession") 89 + did, ok := session.Values["did"].(string) 90 + if !ok || did == "" { 91 + h.badRequest(w, errors.New("cannot beep, not authenticated")) 92 + } 93 + s, err := h.db.GetOauthSesson(did, r.Context()) 94 + if err != nil { 95 + h.serverError(w, errors.New("error finding session: "+err.Error())) 96 + } 97 + h.xrpc.MakeBskyPost("beep_", s, r.Context()) 98 + }
+6 -31
server/internal/oauth/service.go
··· 10 10 "strings" 11 11 "time" 12 12 "xcvr-backend/internal/atputils" 13 + "xcvr-backend/internal/types" 13 14 14 15 atoauth "github.com/haileyok/atproto-oauth-golang" 15 16 "github.com/haileyok/atproto-oauth-golang/helpers" ··· 44 45 }, nil 45 46 } 46 47 47 - type OauthFlowResult struct { 48 - AuthzEndpoint string 49 - State string 50 - DID string 51 - RequestUri string 52 - } 53 - 54 - type OAuthRequest struct { 55 - ID uint 56 - AuthserverIss string 57 - State string 58 - Did string 59 - PdsUrl string 60 - PkceVerifier string 61 - DpopAuthServerNonce string 62 - DpopPrivKey string 63 - } 64 - 65 48 type CallbackParams struct { 66 49 Iss string 67 50 State string 68 51 Code string 69 52 } 70 53 71 - type Session struct { 72 - OAuthRequest 73 - DpopPdsNonce string 74 - AccessToken string 75 - RefreshToken string 76 - Expiration time.Time 77 - } 78 - 79 - func (s *Service) StartAuthFlow(ctx context.Context, handle string) (*OAuthRequest, *OauthFlowResult, error) { 54 + func (s *Service) StartAuthFlow(ctx context.Context, handle string) (*types.OAuthRequest, *types.OauthFlowResult, error) { 80 55 did, err := atputils.GetDidFromHandle(ctx, handle) 81 56 if err != nil { 82 57 return nil, nil, errors.New("error resolving handle:" + err.Error()) ··· 93 68 if err != nil { 94 69 return nil, nil, errors.New("error making oauth request:" + err.Error()) 95 70 } 96 - oauthReq := OAuthRequest{ 71 + oauthReq := types.OAuthRequest{ 97 72 AuthserverIss: metadata.Issuer, 98 73 State: parResp.State, 99 74 Did: did, ··· 102 77 DpopPrivKey: string(dpopPrivKeyJson), 103 78 PdsUrl: service, 104 79 } 105 - oauthFlowResult := OauthFlowResult{ 80 + oauthFlowResult := types.OauthFlowResult{ 106 81 AuthzEndpoint: metadata.AuthorizationEndpoint, 107 82 State: parResp.State, 108 83 DID: did, ··· 210 185 // return resDid.Did, nil 211 186 // } 212 187 213 - func (s *Service) OauthCallback(ctx context.Context, oauthRequest *OAuthRequest, params CallbackParams) (*Session, error) { 188 + func (s *Service) OauthCallback(ctx context.Context, oauthRequest *types.OAuthRequest, params CallbackParams) (*types.Session, error) { 214 189 jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivKey)) 215 190 if err != nil { 216 191 return nil, errors.New("error parsing jwk:" + err.Error()) ··· 222 197 if initialTokenResp.Scope != "atproto transition:generic" { 223 198 return nil, errors.New(fmt.Sprintf("incorrect scope: %s", initialTokenResp.Scope)) 224 199 } 225 - oauthSession := Session{ 200 + oauthSession := types.Session{ 226 201 OAuthRequest: *oauthRequest, 227 202 AccessToken: initialTokenResp.AccessToken, 228 203 RefreshToken: initialTokenResp.RefreshToken,
+69
server/internal/oauth/xrpcclient.go
··· 1 + package oauth 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "github.com/bluesky-social/indigo/api/atproto" 7 + "github.com/bluesky-social/indigo/api/bsky" 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/bluesky-social/indigo/lex/util" 10 + "github.com/haileyok/atproto-oauth-golang" 11 + "github.com/haileyok/atproto-oauth-golang/helpers" 12 + "xcvr-backend/internal/db" 13 + "xcvr-backend/internal/log" 14 + "xcvr-backend/internal/types" 15 + ) 16 + 17 + type Client struct { 18 + xrpccli *oauth.XrpcClient 19 + } 20 + 21 + func NewXRPCClient(s *db.Store, l *log.Logger) *Client { 22 + return &Client{ 23 + xrpccli: &oauth.XrpcClient{ 24 + OnDpopPdsNonceChanged: func(did, newNonce string) { 25 + err := s.SetDpopPdsNonce(did, newNonce) 26 + if err != nil { 27 + l.Deprintln(err.Error()) 28 + } 29 + }, 30 + }, 31 + } 32 + } 33 + 34 + func getOauthSessionAuthArgs(s *types.Session) (*oauth.XrpcAuthedRequestArgs, error) { 35 + privateJwk, err := helpers.ParseJWKFromBytes([]byte(s.DpopPrivKey)) 36 + if err != nil { 37 + return nil, errors.New("failed to parse jwk in getoauthsessionauthargs: " + err.Error()) 38 + } 39 + return &oauth.XrpcAuthedRequestArgs{ 40 + Did: s.Did, 41 + AccessToken: s.AccessToken, 42 + PdsUrl: s.PdsUrl, 43 + Issuer: s.AuthserverIss, 44 + DpopPdsNonce: s.DpopPdsNonce, 45 + DpopPrivateJwk: privateJwk, 46 + }, nil 47 + } 48 + 49 + func (c *Client) MakeBskyPost(text string, s *types.Session, ctx context.Context) error { 50 + authargs, err := getOauthSessionAuthArgs(s) 51 + if err != nil { 52 + return errors.New("failed to get oauthsessionauthargs while making post: " + err.Error()) 53 + } 54 + post := bsky.FeedPost{ 55 + Text: text, 56 + CreatedAt: syntax.DatetimeNow().String(), 57 + } 58 + input := atproto.RepoCreateRecord_Input{ 59 + Collection: "app.bsky.feed.post", 60 + Repo: authargs.Did, 61 + Record: &util.LexiconTypeDecoder{Val: &post}, 62 + } 63 + var out atproto.RepoCreateRecord_Output 64 + err = c.xrpccli.Do(ctx, authargs, "POST", "application/json", "com.atproto.repo.createRecord", nil, input, &out) 65 + if err != nil { 66 + return errors.New("oops! failed to make post: " + err.Error()) 67 + } 68 + return nil 69 + }
+31
server/internal/types/oauth.go
··· 1 + package types 2 + 3 + import ( 4 + "time" 5 + ) 6 + 7 + type OauthFlowResult struct { 8 + AuthzEndpoint string 9 + State string 10 + DID string 11 + RequestUri string 12 + } 13 + 14 + type OAuthRequest struct { 15 + ID uint 16 + AuthserverIss string 17 + State string 18 + Did string 19 + PdsUrl string 20 + PkceVerifier string 21 + DpopAuthServerNonce string 22 + DpopPrivKey string 23 + } 24 + 25 + type Session struct { 26 + OAuthRequest 27 + DpopPdsNonce string 28 + AccessToken string 29 + RefreshToken string 30 + Expiration time.Time 31 + }