[WIP] music platform user data scraper
teal-fm atproto

oauth changes

+2
.env.template
··· 16 16 ATPROTO_CLIENT_ID= 17 17 ATPROTO_METADATA_URL= 18 18 ATPROTO_CALLBACK_URL= 19 + ATPROTO_CLIENT_SECRET_KEY={goat key generate -t P-256} 20 + ATPROTO_CLIENT_SECRET_KEY_ID={can be whatever usually a timestamp} 19 21 20 22 # Last.fm 21 23 LASTFM_API_KEY=
-1
Dockerfile
··· 29 29 #Overwrite the main.css with the one from the builder 30 30 COPY --from=node_builder /app/static/main.css /app/pages/static/main.css 31 31 #generate the jwks 32 - RUN go run github.com/haileyok/atproto-oauth-golang/cmd/helper generate-jwks 33 32 RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags='-w -s -extldflags "-static"' -o main ./cmd 34 33 ARG TARGETOS=${TARGETPLATFORM%%/*} 35 34 ARG TARGETARCH=${TARGETPLATFORM##*/}
-4
Makefile
··· 25 25 --build-file ./lexcfg.json \ 26 26 ../atproto/lexicons \ 27 27 ./lexicons/teal 28 - 29 - .PHONY: jwtgen 30 - jwtgen: 31 - go run github.com/haileyok/atproto-oauth-golang/cmd/helper generate-jwks
+8 -1
README.md
··· 23 23 24 24 This is a break down of what each env variable is and what it may look like 25 25 26 + **_breaking piper/v0.0.2 changes env_** 27 + 28 + You now have to bring your own private key to run piper. Can do this via goat `goat key generate -t P-256`. You want the one that is labeled under "Secret Key (Multibase Syntax): save this securely (eg, add to password manager)" 29 + 30 + - `ATPROTO_CLIENT_SECRET_KEY` - Private key for oauth confidential client. This can be generated via goat `goat key generate -t P-256` 31 + - `ATPROTO_CLIENT_SECRET_KEY_ID` - Key ID for oauth confidential client. This needs to be persistent and unique, can use a timestamp. Here's one for you: `1758199756` 32 + 33 + 26 34 - `SERVER_PORT` - The port piper is hosted on 27 35 - `SERVER_HOST` - The server host. `localhost` is fine here, or `0.0.0.0` for docker 28 36 - `SERVER_ROOT_URL` - This needs to be the pubically accessible url created in [Setup](#setup). Like `https://piper.teal.fm` ··· 53 61 run some make scripts: 54 62 55 63 ``` 56 - make jwtgen 57 64 58 65 make dev-setup 59 66 ```
+16 -11
cmd/main.go
··· 5 5 "fmt" 6 6 "log" 7 7 "net/http" 8 - "os" 9 8 "time" 10 9 11 10 "github.com/teal-fm/piper/service/lastfm" ··· 57 56 log.Fatalf("Error initializing database: %v", err) 58 57 } 59 58 59 + sessionManager := session.NewSessionManager(database) 60 + 60 61 // --- Service Initializations --- 61 - jwksBytes, err := os.ReadFile("./jwks.json") 62 - if err != nil { 63 - // run `make jwtgen` 64 - log.Fatalf("Error reading JWK file: %v", err) 62 + 63 + var newJwkPrivateKey = viper.GetString("atproto.client_secret_key") 64 + if newJwkPrivateKey == "" { 65 + fmt.Printf("You now have to set the ATPROTO_CLIENT_SECRET_KEY env var to a private key. This can be done via goat key generate -t P-256") 66 + return 65 67 } 66 - jwks, err := atproto.LoadJwks(jwksBytes) 67 - if err != nil { 68 - log.Fatalf("Error loading JWK: %v", err) 68 + var clientSecretKeyId = viper.GetString("atproto.client_secret_key_id") 69 + if clientSecretKeyId == "" { 70 + fmt.Printf("You also now have to set the ATPROTO_CLIENT_SECRET_KEY_ID env var to a key ID. This needs to be persistent and unique. Here's one for you: %d", time.Now().Unix()) 71 + return 69 72 } 73 + 70 74 atprotoService, err := atproto.NewATprotoAuthService( 71 75 database, 72 - jwks, 76 + sessionManager, 77 + newJwkPrivateKey, 73 78 viper.GetString("atproto.client_id"), 74 79 viper.GetString("atproto.callback_url"), 80 + clientSecretKeyId, 75 81 ) 76 82 if err != nil { 77 83 log.Fatalf("Error creating ATproto auth service: %v", err) ··· 82 88 spotifyService := spotify.NewSpotifyService(database, atprotoService, mbService, playingNowService) 83 89 lastfmService := lastfm.NewLastFMService(database, viper.GetString("lastfm.api_key"), mbService, atprotoService, playingNowService) 84 90 85 - sessionManager := session.NewSessionManager(database) 86 - oauthManager := oauth.NewOAuthServiceManager(sessionManager) 91 + oauthManager := oauth.NewOAuthServiceManager() 87 92 88 93 spotifyOAuth := oauth.NewOAuth2Service( 89 94 viper.GetString("spotify.client_id"),
+2 -2
cmd/routes.go
··· 28 28 mux.HandleFunc("/api-keys", session.WithAuth(app.apiKeyService.HandleAPIKeyManagement(app.database, app.pages), app.sessionManager)) 29 29 mux.HandleFunc("/link-lastfm", session.WithAuth(handleLinkLastfmForm(app.database, app.pages), app.sessionManager)) // GET form 30 30 mux.HandleFunc("/link-lastfm/submit", session.WithAuth(handleLinkLastfmSubmit(app.database), app.sessionManager)) // POST submit - Changed route slightly 31 - mux.HandleFunc("/logout", app.sessionManager.HandleLogout) 31 + mux.HandleFunc("/logout", app.oauthManager.HandleLogout("atproto")) 32 32 mux.HandleFunc("/debug/", session.WithAuth(app.sessionManager.HandleDebug, app.sessionManager)) 33 33 34 34 mux.HandleFunc("/api/v1/me", session.WithAPIAuth(apiMeHandler(app.database), app.sessionManager)) ··· 45 45 serverUrlRoot := viper.GetString("server.root_url") 46 46 atpClientId := viper.GetString("atproto.client_id") 47 47 atpCallbackUrl := viper.GetString("atproto.callback_url") 48 - mux.HandleFunc("/.well-known/client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 48 + mux.HandleFunc("/oauth-client-metadata.json", func(w http.ResponseWriter, r *http.Request) { 49 49 app.atprotoService.HandleClientMetadata(w, r, serverUrlRoot, atpClientId, atpCallbackUrl) 50 50 }) 51 51 mux.HandleFunc("/oauth/jwks.json", app.atprotoService.HandleJwks)
+261 -170
db/atproto.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 - "encoding/json" 7 6 "fmt" 7 + "strings" 8 8 "time" 9 9 10 - oauth "github.com/haileyok/atproto-oauth-golang" 11 - "github.com/haileyok/atproto-oauth-golang/helpers" 12 - "github.com/lestrrat-go/jwx/v2/jwk" 10 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 13 12 "github.com/teal-fm/piper/models" 14 13 ) 15 14 16 - type ATprotoAuthData struct { 17 - State string `json:"state"` 18 - DID string `json:"did"` 19 - PDSUrl string `json:"pds_url"` 20 - AuthServerIssuer string `json:"authserver_issuer"` 21 - PKCEVerifier string `json:"pkce_verifier"` 22 - DPoPAuthServerNonce string `json:"dpop_authserver_nonce"` 23 - DPoPPrivateJWK jwk.Key `json:"dpop_private_jwk"` 24 - CreatedAt time.Time `json:"created_at"` 25 - } 26 - 27 - func (db *DB) SaveATprotoAuthData(data *models.ATprotoAuthData) error { 28 - dpopPrivateJWKBytes, err := json.Marshal(data.DPoPPrivateJWK) 29 - if err != nil { 30 - return err 31 - } 32 - 33 - _, err = db.Exec(` 34 - INSERT INTO atproto_auth_data (state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk) 35 - VALUES (?, ?, ?, ?, ?, ?, ?)`, 36 - data.State, data.DID, data.PDSUrl, data.AuthServerIssuer, data.PKCEVerifier, data.DPoPAuthServerNonce, string(dpopPrivateJWKBytes)) 37 - 38 - return err 39 - } 40 - 41 - func (db *DB) GetATprotoAuthData(state string) (*models.ATprotoAuthData, error) { 42 - var data models.ATprotoAuthData 43 - var dpopPrivateJWKString string 44 - 45 - err := db.QueryRow(` 46 - SELECT state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk 47 - FROM atproto_auth_data 48 - WHERE state = ?`, 49 - state).Scan( 50 - &data.State, 51 - &data.DID, 52 - &data.PDSUrl, 53 - &data.AuthServerIssuer, 54 - &data.PKCEVerifier, 55 - &data.DPoPAuthServerNonce, 56 - &dpopPrivateJWKString, 57 - ) 58 - if err != nil { 59 - if err == sql.ErrNoRows { 60 - return nil, fmt.Errorf("no auth data found for state %s: %w", state, err) 61 - } 62 - return nil, fmt.Errorf("failed to scan auth data for state %s: %w", state, err) 63 - } 64 - 65 - key, err := helpers.ParseJWKFromBytes([]byte(dpopPrivateJWKString)) 66 - if err != nil { 67 - return nil, fmt.Errorf("failed to parse DPoPPrivateJWK for state %s: %w", state, err) 68 - } 69 - data.DPoPPrivateJWK = key 70 - 71 - return &data, nil 72 - } 73 - 74 15 func (db *DB) FindOrCreateUserByDID(did string) (*models.User, error) { 75 16 var user models.User 76 17 err := db.QueryRow(` ··· 108 49 return &user, err 109 50 } 110 51 111 - // create or update the current user's ATproto session data. 112 - func (db *DB) SaveATprotoSession(tokenResp *oauth.TokenResponse, authserverIss string, dpopPrivateJWK jwk.Key, pdsUrl string) error { 113 - db.logger.Printf("Saving session with PDS url %s", pdsUrl) 114 - expiryTime := time.Now().UTC().Add(time.Second * time.Duration(tokenResp.ExpiresIn)) 52 + func (db *DB) SetLatestATProtoSessionId(did string, atProtoSessionID string) error { 53 + db.logger.Printf("Setting latest atproto session id for did %s to %s", did, atProtoSessionID) 115 54 now := time.Now().UTC() 116 55 117 - dpopPrivateJWKBytes, err := json.Marshal(dpopPrivateJWK) 118 - if err != nil { 119 - return err 120 - } 121 - 122 56 result, err := db.Exec(` 123 57 UPDATE users 124 - SET atproto_access_token = ?, 125 - atproto_refresh_token = ?, 126 - atproto_token_expiry = ?, 127 - atproto_scope = ?, 128 - atproto_sub = ?, 129 - atproto_authserver_issuer = ?, 130 - atproto_token_type = ?, 131 - atproto_authserver_nonce = ?, 132 - atproto_dpop_private_jwk = ?, 133 - atproto_pds_url = ?, 134 - atproto_pds_nonce = ?, 58 + SET 59 + most_recent_at_session_id = ?, 135 60 updated_at = ? 136 61 WHERE atproto_did = ?`, 137 - tokenResp.AccessToken, 138 - tokenResp.RefreshToken, 139 - expiryTime, 140 - tokenResp.Scope, 141 - tokenResp.Sub, 142 - authserverIss, 143 - tokenResp.TokenType, 144 - tokenResp.DpopAuthserverNonce, 145 - string(dpopPrivateJWKBytes), 146 - pdsUrl, 147 - // will get set later 148 - "", 62 + atProtoSessionID, 149 63 now, 150 - tokenResp.Sub, 64 + did, 151 65 ) 152 - 153 66 if err != nil { 154 - return fmt.Errorf("failed to update atproto session for did %s: %w", tokenResp.Sub, err) 67 + db.logger.Printf("%v", err) 68 + return fmt.Errorf("failed to update atproto session for did %s: %w", did, atProtoSessionID) 155 69 } 156 70 157 71 rowsAffected, err := result.RowsAffected() 158 72 if err != nil { 159 73 // it's possible the update succeeded here? 160 - return fmt.Errorf("failed to check rows affected after updating atproto session for did %s: %w", tokenResp.Sub, err) 74 + return fmt.Errorf("failed to check rows affected after updating atproto session for did %s: %w", did, atProtoSessionID) 161 75 } 162 76 163 77 if rowsAffected == 0 { 164 - return fmt.Errorf("no user found with did %s to update session, creating new session", tokenResp.Sub) 78 + return fmt.Errorf("no user found with did %s to update session, creating new session", did) 165 79 } 166 80 167 81 return nil 168 82 } 169 83 170 - func (db *DB) GetAtprotoSession(did string, ctx context.Context, oauthClient oauth.Client) (*models.ATprotoAuthSession, error) { 171 - var oauthSession models.ATprotoAuthSession 172 - var authserverIss string 173 - var jwkBytes string 84 + type SqliteATProtoStore struct { 85 + db *sql.DB 86 + } 174 87 175 - err := db.QueryRow( 176 - ` 177 - SELECT id, 178 - atproto_did, 179 - atproto_pds_url, 180 - atproto_authserver_issuer, 181 - atproto_access_token, 182 - atproto_refresh_token, 183 - atproto_pds_nonce, 184 - atproto_authserver_nonce, 185 - atproto_dpop_private_jwk, 186 - atproto_token_expiry 187 - FROM users 188 - WHERE atproto_did = ?`, 189 - did, 190 - ).Scan( 191 - &oauthSession.ID, 192 - &oauthSession.DID, 193 - &oauthSession.PDSUrl, 194 - &authserverIss, 195 - &oauthSession.AccessToken, 196 - &oauthSession.RefreshToken, 197 - &oauthSession.DpopPdsNonce, 198 - &oauthSession.DpopAuthServerNonce, 199 - &jwkBytes, 200 - &oauthSession.TokenExpiry, 88 + var _ oauth.ClientAuthStore = (*SqliteATProtoStore)(nil) 89 + 90 + func NewSqliteATProtoStore(db *sql.DB) *SqliteATProtoStore { 91 + return &SqliteATProtoStore{ 92 + db: db, 93 + } 94 + } 95 + 96 + func sessionKey(did syntax.DID, sessionID string) string { 97 + return fmt.Sprintf("%s/%s", did, sessionID) 98 + } 99 + 100 + func splitScopes(s string) []string { 101 + if s == "" { 102 + return nil 103 + } 104 + return strings.Fields(s) 105 + } 106 + 107 + func joinScopes(scopes []string) string { 108 + if len(scopes) == 0 { 109 + return "" 110 + } 111 + return strings.Join(scopes, " ") 112 + } 113 + 114 + func (s *SqliteATProtoStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) { 115 + lookUpKey := sessionKey(did, sessionID) 116 + 117 + var ( 118 + accountDIDStr string 119 + lookUpKeyStr string 120 + sessionIDStr string 121 + hostURL string 122 + authServerURL string 123 + authServerTokenEndpoint string 124 + authServerRevocationEndpoint string 125 + scopesStr string 126 + accessToken string 127 + refreshToken string 128 + dpopAuthServerNonce string 129 + dpopHostNonce string 130 + dpopPrivateKeyMultibase string 131 + ) 132 + 133 + err := s.db.QueryRow(` 134 + SELECT account_did, 135 + look_up_key, 136 + session_id, 137 + host_url, 138 + authserver_url, 139 + authserver_token_endpoint, 140 + authserver_revocation_endpoint, 141 + scopes, 142 + access_token, 143 + refresh_token, 144 + dpop_authserver_nonce, 145 + dpop_host_nonce, 146 + dpop_privatekey_multibase 147 + FROM atproto_sessions 148 + WHERE look_up_key = ? 149 + `, lookUpKey).Scan( 150 + &accountDIDStr, 151 + &lookUpKeyStr, 152 + &sessionIDStr, 153 + &hostURL, 154 + &authServerURL, 155 + &authServerTokenEndpoint, 156 + &authServerRevocationEndpoint, 157 + &scopesStr, 158 + &accessToken, 159 + &refreshToken, 160 + &dpopAuthServerNonce, 161 + &dpopHostNonce, 162 + &dpopPrivateKeyMultibase, 201 163 ) 202 164 165 + if err == sql.ErrNoRows { 166 + return nil, fmt.Errorf("session not found: %s", lookUpKey) 167 + } 203 168 if err != nil { 204 - return nil, fmt.Errorf("failed to get atproto session for did %s: %w", did, err) 169 + return nil, err 205 170 } 206 171 207 - privateJwk, err := helpers.ParseJWKFromBytes([]byte(jwkBytes)) 172 + accDID, err := syntax.ParseDID(accountDIDStr) 208 173 if err != nil { 209 - return nil, fmt.Errorf("failed to parse DPoPPrivateJWK: %w", err) 210 - } else { 211 - // add jwk to the struct 212 - oauthSession.DpopPrivateJWK = privateJwk 174 + return nil, fmt.Errorf("invalid account DID in session: %w", err) 213 175 } 214 176 215 - // printout the session details 216 - db.logger.Printf("Getting session details for the did: %+v\n", oauthSession.DID) 177 + sess := oauth.ClientSessionData{ 178 + AccountDID: accDID, 179 + SessionID: sessionIDStr, 180 + HostURL: hostURL, 181 + AuthServerURL: authServerURL, 182 + AuthServerTokenEndpoint: authServerTokenEndpoint, 183 + AuthServerRevocationEndpoint: authServerRevocationEndpoint, 184 + Scopes: splitScopes(scopesStr), 185 + AccessToken: accessToken, 186 + RefreshToken: refreshToken, 187 + DPoPAuthServerNonce: dpopAuthServerNonce, 188 + DPoPHostNonce: dpopHostNonce, 189 + DPoPPrivateKeyMultibase: dpopPrivateKeyMultibase, 190 + } 217 191 218 - // if token is expired, refresh it 219 - if time.Now().UTC().After(oauthSession.TokenExpiry) { 192 + return &sess, nil 193 + } 220 194 221 - resp, err := oauthClient.RefreshTokenRequest(ctx, oauthSession.RefreshToken, authserverIss, oauthSession.DpopAuthServerNonce, privateJwk) 222 - if err != nil { 223 - return nil, err 224 - } 195 + func (s *SqliteATProtoStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error { 196 + lookUpKey := sessionKey(sess.AccountDID, sess.SessionID) 197 + // simple upsert: delete then insert 198 + _, _ = s.db.Exec(`DELETE FROM atproto_sessions WHERE look_up_key = ?`, lookUpKey) 199 + _, err := s.db.Exec(` 200 + INSERT INTO atproto_sessions ( 201 + look_up_key, 202 + account_did, 203 + session_id, 204 + host_url, 205 + authserver_url, 206 + authserver_token_endpoint, 207 + authserver_revocation_endpoint, 208 + scopes, 209 + access_token, 210 + refresh_token, 211 + dpop_authserver_nonce, 212 + dpop_host_nonce, 213 + dpop_privatekey_multibase 214 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 215 + `, 216 + lookUpKey, 217 + sess.AccountDID.String(), 218 + sess.SessionID, 219 + sess.HostURL, 220 + sess.AuthServerURL, 221 + sess.AuthServerTokenEndpoint, 222 + sess.AuthServerRevocationEndpoint, 223 + joinScopes(sess.Scopes), 224 + sess.AccessToken, 225 + sess.RefreshToken, 226 + sess.DPoPAuthServerNonce, 227 + sess.DPoPHostNonce, 228 + sess.DPoPPrivateKeyMultibase, 229 + ) 230 + return err 231 + } 225 232 226 - if err := db.SaveATprotoSession(resp, authserverIss, privateJwk, oauthSession.PDSUrl); err != nil { 227 - return nil, fmt.Errorf("failed to save refreshed token: %w", err) 228 - } 233 + func (s *SqliteATProtoStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error { 234 + lookUpKey := sessionKey(did, sessionID) 235 + _, err := s.db.Exec(`DELETE FROM atproto_sessions WHERE look_up_key = ?`, lookUpKey) 236 + return err 237 + } 229 238 230 - oauthSession = models.ATprotoAuthSession{ 231 - ID: oauthSession.ID, 232 - DID: oauthSession.DID, 233 - PDSUrl: oauthSession.PDSUrl, 234 - AuthServerIssuer: authserverIss, 235 - AccessToken: resp.AccessToken, 236 - RefreshToken: resp.RefreshToken, 237 - DpopPdsNonce: oauthSession.DpopPdsNonce, 238 - DpopAuthServerNonce: resp.DpopAuthserverNonce, 239 - DpopPrivateJWK: privateJwk, 240 - TokenExpiry: time.Now().UTC().Add(time.Duration(resp.ExpiresIn) * time.Second), 239 + func (s *SqliteATProtoStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) { 240 + var ( 241 + authServerURL string 242 + accountDIDStr sql.NullString 243 + scopesStr string 244 + requestURI string 245 + authServerTokenEndpoint string 246 + authServerRevocationEndpoint string 247 + pkceVerifier string 248 + dpopAuthServerNonce string 249 + dpopPrivateKeyMultibase string 250 + ) 251 + err := s.db.QueryRow(` 252 + SELECT authserver_url, 253 + account_did, 254 + scopes, 255 + request_uri, 256 + authserver_token_endpoint, 257 + authserver_revocation_endpoint, 258 + pkce_verifier, 259 + dpop_authserver_nonce, 260 + dpop_privatekey_multibase 261 + FROM atproto_state 262 + WHERE state = ? 263 + `, state).Scan( 264 + &authServerURL, 265 + &accountDIDStr, 266 + &scopesStr, 267 + &requestURI, 268 + &authServerTokenEndpoint, 269 + &authServerRevocationEndpoint, 270 + &pkceVerifier, 271 + &dpopAuthServerNonce, 272 + &dpopPrivateKeyMultibase, 273 + ) 274 + if err == sql.ErrNoRows { 275 + return nil, fmt.Errorf("request info not found: %s", state) 276 + } 277 + if err != nil { 278 + return nil, err 279 + } 280 + var accountDIDPtr *syntax.DID 281 + if accountDIDStr.Valid && accountDIDStr.String != "" { 282 + acc, err := syntax.ParseDID(accountDIDStr.String) 283 + if err != nil { 284 + return nil, fmt.Errorf("invalid account DID in auth request: %w", err) 241 285 } 286 + accountDIDPtr = &acc 287 + } 288 + info := oauth.AuthRequestData{ 289 + State: state, 290 + AuthServerURL: authServerURL, 291 + AccountDID: accountDIDPtr, 292 + Scopes: splitScopes(scopesStr), 293 + RequestURI: requestURI, 294 + AuthServerTokenEndpoint: authServerTokenEndpoint, 295 + AuthServerRevocationEndpoint: authServerRevocationEndpoint, 296 + PKCEVerifier: pkceVerifier, 297 + DPoPAuthServerNonce: dpopAuthServerNonce, 298 + DPoPPrivateKeyMultibase: dpopPrivateKeyMultibase, 299 + } 300 + return &info, nil 301 + } 242 302 303 + func (s *SqliteATProtoStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error { 304 + // ensure not already exists 305 + var exists int 306 + err := s.db.QueryRow(`SELECT 1 FROM atproto_state WHERE state = ?`, info.State).Scan(&exists) 307 + if err == nil { 308 + return fmt.Errorf("auth request already saved for state %s", info.State) 243 309 } 244 - 245 - return &oauthSession, nil 310 + if err != nil && err != sql.ErrNoRows { 311 + return err 312 + } 313 + var accountDIDStr interface{} 314 + if info.AccountDID != nil { 315 + accountDIDStr = info.AccountDID.String() 316 + } else { 317 + accountDIDStr = nil 318 + } 319 + _, err = s.db.Exec(` 320 + INSERT INTO atproto_state ( 321 + state, 322 + authserver_url, 323 + account_did, 324 + scopes, 325 + request_uri, 326 + authserver_token_endpoint, 327 + authserver_revocation_endpoint, 328 + pkce_verifier, 329 + dpop_authserver_nonce, 330 + dpop_privatekey_multibase 331 + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) 332 + `, 333 + info.State, 334 + info.AuthServerURL, 335 + accountDIDStr, 336 + joinScopes(info.Scopes), 337 + info.RequestURI, 338 + info.AuthServerTokenEndpoint, 339 + info.AuthServerRevocationEndpoint, 340 + info.PKCEVerifier, 341 + info.DPoPAuthServerNonce, 342 + info.DPoPPrivateKeyMultibase, 343 + ) 344 + return err 246 345 } 247 346 248 - func AtpSessionToAuthArgs(sess *models.ATprotoAuthSession) *oauth.XrpcAuthedRequestArgs { 249 - //Commenting out so jwts and tokens are not in logs 250 - //fmt.Printf("DID: %s\nPDS URL: %s\nISS: %s\nAccess Token: %s\nNonce: %s\nPrivate JWK: %s\n", sess.DID, sess.PDSUrl, sess.AuthServerIssuer, sess.AccessToken, sess.DpopPdsNonce, sess.DpopPrivateJWK) 251 - return &oauth.XrpcAuthedRequestArgs{ 252 - Did: sess.DID, 253 - PdsUrl: sess.PDSUrl, 254 - Issuer: sess.AuthServerIssuer, 255 - AccessToken: sess.AccessToken, 256 - DpopPdsNonce: sess.DpopPdsNonce, 257 - DpopPrivateJwk: sess.DpopPrivateJWK, 258 - } 347 + func (s *SqliteATProtoStore) DeleteAuthRequestInfo(ctx context.Context, state string) error { 348 + _, err := s.db.Exec(`DELETE FROM atproto_state WHERE state = ?`, state) 349 + return err 259 350 }
+57 -24
db/db.go
··· 45 45 username TEXT, -- Made nullable, might not have username initially 46 46 email TEXT UNIQUE, -- Made nullable 47 47 atproto_did TEXT UNIQUE, -- Atproto DID (identifier) 48 - atproto_pds_url TEXT, 49 - atproto_authserver_issuer TEXT, 50 - atproto_access_token TEXT, -- Atproto access token 51 - atproto_refresh_token TEXT, -- Atproto refresh token 52 - atproto_token_expiry TIMESTAMP, -- Atproto token expiry 53 - atproto_sub TEXT, 54 - atproto_scope TEXT, -- Atproto token scope 55 - atproto_token_type TEXT, -- Atproto token type 56 - atproto_authserver_nonce TEXT, 57 - atproto_pds_nonce TEXT, 58 - atproto_dpop_private_jwk TEXT, 48 + most_recent_at_session_id TEXT, -- Most recent oAuth session id 59 49 spotify_id TEXT UNIQUE, -- Spotify specific ID 60 50 access_token TEXT, -- Spotify access token 61 51 refresh_token TEXT, -- Spotify refresh token ··· 91 81 } 92 82 93 83 _, err = db.Exec(` 94 - CREATE TABLE IF NOT EXISTS atproto_auth_data ( 95 - id INTEGER PRIMARY KEY AUTOINCREMENT, 96 - state TEXT NOT NULL, 97 - did TEXT, 98 - pds_url TEXT NOT NULL, 99 - authserver_issuer TEXT NOT NULL, 100 - pkce_verifier TEXT NOT NULL, 101 - dpop_authserver_nonce TEXT NOT NULL, 102 - dpop_private_jwk TEXT NOT NULL, 103 - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 104 - )`) 84 + CREATE TABLE IF NOT EXISTS atproto_state ( 85 + id INTEGER PRIMARY KEY AUTOINCREMENT, 86 + state TEXT NOT NULL, 87 + authserver_url TEXT, 88 + account_did TEXT, 89 + scopes TEXT, 90 + request_uri TEXT, 91 + authserver_token_endpoint TEXT, 92 + authserver_revocation_endpoint TEXT, 93 + pkce_verifier TEXT, 94 + dpop_authserver_nonce TEXT, 95 + dpop_privatekey_multibase TEXT, 96 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 97 + ); 98 + CREATE INDEX IF NOT EXISTS atproto_state_state ON atproto_state(state); 99 + 100 + `) 101 + if err != nil { 102 + return err 103 + } 104 + 105 + _, err = db.Exec(` 106 + CREATE TABLE IF NOT EXISTS atproto_sessions ( 107 + id INTEGER PRIMARY KEY AUTOINCREMENT, 108 + look_up_key TEXT NOT NULL, 109 + account_did TEXT, 110 + session_id TEXT, 111 + host_url TEXT, 112 + authserver_url TEXT, 113 + authserver_token_endpoint TEXT, 114 + authserver_revocation_endpoint TEXT, 115 + scopes TEXT, 116 + access_token TEXT, 117 + refresh_token TEXT, 118 + dpop_authserver_nonce TEXT, 119 + dpop_host_nonce TEXT, 120 + dpop_privatekey_multibase TEXT, 121 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 122 + ); 123 + CREATE INDEX IF NOT EXISTS idx_atproto_sessions_look_up_key ON atproto_sessions(look_up_key); 124 + `) 105 125 if err != nil { 106 126 return err 107 127 } ··· 162 182 user := &models.User{} 163 183 164 184 err := db.QueryRow(` 165 - SELECT id, username, email, atproto_did, spotify_id, access_token, refresh_token, token_expiry, lastfm_username, created_at, updated_at 185 + SELECT id, 186 + username, 187 + email, 188 + atproto_did, 189 + most_recent_at_session_id, 190 + spotify_id, 191 + access_token, 192 + refresh_token, 193 + token_expiry, 194 + lastfm_username, 195 + created_at, 196 + updated_at 166 197 FROM users WHERE id = ?`, ID).Scan( 167 - &user.ID, &user.Username, &user.Email, &user.ATProtoDID, &user.SpotifyID, 198 + &user.ID, &user.Username, &user.Email, &user.ATProtoDID, &user.MostRecentAtProtoSessionID, &user.SpotifyID, 168 199 &user.AccessToken, &user.RefreshToken, &user.TokenExpiry, 169 200 &user.LastFMUsername, 170 201 &user.CreatedAt, &user.UpdatedAt) ··· 472 503 473 504 return &lastTimestamp, nil 474 505 } 506 + 507 + //
+2 -2
db/lfm.go
··· 42 42 43 43 func (db *DB) GetUserByLastFM(lastfmUsername string) (*models.User, error) { 44 44 row := db.QueryRow(` 45 - SELECT id, username, email, atproto_did, created_at, updated_at, lastfm_username 45 + SELECT id, username, email, atproto_did, most_recent_at_session_id, created_at, updated_at, lastfm_username 46 46 FROM users 47 47 WHERE lastfm_username = ?`, lastfmUsername) 48 48 49 49 user := &models.User{} 50 50 err := row.Scan( 51 - &user.ID, &user.Username, &user.Email, &user.ATProtoDID, 51 + &user.ID, &user.MostRecentAtProtoSessionID, &user.Username, &user.Email, &user.ATProtoDID, 52 52 &user.CreatedAt, &user.UpdatedAt, &user.LastFMUsername) 53 53 if err != nil { 54 54 return nil, err
+29 -3
go.mod
··· 3 3 go 1.24.0 4 4 5 5 require ( 6 - github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e 6 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 7 7 github.com/dlclark/regexp2 v1.11.5 8 - github.com/haileyok/atproto-oauth-golang v0.0.2 9 8 github.com/ipfs/go-cid v0.4.1 10 9 github.com/joho/godotenv v1.5.1 11 10 github.com/justinas/alice v1.2.0 ··· 21 20 require ( 22 21 dario.cat/mergo v1.0.1 // indirect 23 22 github.com/air-verse/air v1.61.7 // indirect 23 + github.com/beorn7/perks v1.0.1 // indirect 24 24 github.com/bep/godartsass v1.2.0 // indirect 25 25 github.com/bep/godartsass/v2 v2.1.0 // indirect 26 26 github.com/bep/golibsass v1.2.0 // indirect 27 27 github.com/carlmjohnson/versioninfo v0.22.5 // indirect 28 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 28 29 github.com/cli/safeexec v1.0.1 // indirect 29 30 github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect 30 31 github.com/creack/pty v1.1.23 // indirect ··· 39 40 github.com/goccy/go-json v0.10.2 // indirect 40 41 github.com/gogo/protobuf v1.3.2 // indirect 41 42 github.com/gohugoio/hugo v0.134.3 // indirect 42 - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 43 + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 44 + github.com/google/go-querystring v1.1.0 // indirect 43 45 github.com/google/uuid v1.6.0 // indirect 46 + github.com/gorilla/context v1.1.2 // indirect 47 + github.com/gorilla/securecookie v1.1.2 // indirect 48 + github.com/gorilla/sessions v1.4.0 // indirect 44 49 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 45 50 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 46 51 github.com/hashicorp/golang-lru v1.0.2 // indirect 52 + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 47 53 github.com/ipfs/bbloom v0.0.4 // indirect 48 54 github.com/ipfs/go-block-format v0.2.0 // indirect 49 55 github.com/ipfs/go-datastore v0.6.0 // indirect ··· 56 62 github.com/ipfs/go-log/v2 v2.5.1 // indirect 57 63 github.com/ipfs/go-metrics-interface v0.0.1 // indirect 58 64 github.com/jbenet/goprocess v0.1.4 // indirect 65 + github.com/jinzhu/inflection v1.0.0 // indirect 66 + github.com/jinzhu/now v1.1.5 // indirect 59 67 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 68 + github.com/labstack/echo-contrib v0.17.2 // indirect 69 + github.com/labstack/echo/v4 v4.13.3 // indirect 70 + github.com/labstack/gommon v0.4.2 // indirect 60 71 github.com/lestrrat-go/blackmagic v1.0.2 // indirect 61 72 github.com/lestrrat-go/httpcc v1.0.1 // indirect 62 73 github.com/lestrrat-go/httprc v1.0.4 // indirect ··· 64 75 github.com/lestrrat-go/option v1.0.1 // indirect 65 76 github.com/mattn/go-colorable v0.1.13 // indirect 66 77 github.com/mattn/go-isatty v0.0.20 // indirect 78 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 67 79 github.com/minio/sha256-simd v1.0.1 // indirect 68 80 github.com/mr-tron/base58 v1.2.0 // indirect 69 81 github.com/multiformats/go-base32 v0.1.0 // indirect ··· 71 83 github.com/multiformats/go-multibase v0.2.0 // indirect 72 84 github.com/multiformats/go-multihash v0.2.3 // indirect 73 85 github.com/multiformats/go-varint v0.0.7 // indirect 86 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 74 87 github.com/opentracing/opentracing-go v1.2.0 // indirect 75 88 github.com/pelletier/go-toml v1.9.5 // indirect 76 89 github.com/pelletier/go-toml/v2 v2.2.3 // indirect 77 90 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 91 + github.com/prometheus/client_golang v1.20.5 // indirect 92 + github.com/prometheus/client_model v0.6.1 // indirect 93 + github.com/prometheus/common v0.61.0 // indirect 94 + github.com/prometheus/procfs v0.15.1 // indirect 78 95 github.com/russross/blackfriday/v2 v2.1.0 // indirect 79 96 github.com/sagikazarmark/locafero v0.7.0 // indirect 97 + github.com/samber/lo v1.47.0 // indirect 98 + github.com/samber/slog-echo v1.15.1 // indirect 80 99 github.com/segmentio/asm v1.2.0 // indirect 81 100 github.com/sourcegraph/conc v0.3.0 // indirect 82 101 github.com/spaolacci/murmur3 v1.1.0 // indirect ··· 86 105 github.com/subosito/gotenv v1.6.0 // indirect 87 106 github.com/tdewolff/parse/v2 v2.7.15 // indirect 88 107 github.com/urfave/cli/v2 v2.27.5 // indirect 108 + github.com/valyala/bytebufferpool v1.0.0 // indirect 109 + github.com/valyala/fasttemplate v1.2.2 // indirect 89 110 github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 111 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 112 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 90 113 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 91 114 go.opentelemetry.io/otel v1.29.0 // indirect 92 115 go.opentelemetry.io/otel/metric v1.29.0 // indirect ··· 96 119 go.uber.org/zap v1.26.0 // indirect 97 120 golang.org/x/crypto v0.32.0 // indirect 98 121 golang.org/x/mod v0.21.0 // indirect 122 + golang.org/x/net v0.33.0 // indirect 99 123 golang.org/x/sys v0.29.0 // indirect 100 124 golang.org/x/text v0.21.0 // indirect 101 125 google.golang.org/protobuf v1.36.1 // indirect 102 126 gopkg.in/yaml.v3 v3.0.1 // indirect 127 + gorm.io/driver/sqlite v1.5.7 // indirect 128 + gorm.io/gorm v1.25.9 // indirect 103 129 lukechampine.com/blake3 v1.2.1 // indirect 104 130 ) 105 131
+61 -4
go.sum
··· 11 11 github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= 12 12 github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 13 13 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 14 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 15 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 14 16 github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= 15 17 github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= 16 18 github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= ··· 37 39 github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= 38 40 github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= 39 41 github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= 40 - github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e h1:yEW1njmALj7i1AjLhq6Lsxts48JUCTT+wpM9m7GNLVY= 41 - github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng= 42 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU= 43 + github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= 42 44 github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 43 45 github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 44 46 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= ··· 121 123 github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= 122 124 github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= 123 125 github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= 124 - github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 125 - github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 126 + github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 127 + github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 126 128 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 127 129 github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 128 130 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= ··· 138 140 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 139 141 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 140 142 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 143 + github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 141 144 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 142 145 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 143 146 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 147 + github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 148 + github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 144 149 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 145 150 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 146 151 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 147 152 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 148 153 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 154 + github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= 155 + github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= 156 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 157 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 158 + github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 159 + github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 149 160 github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= 150 161 github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 151 162 github.com/hairyhenderson/go-codeowners v0.5.0 h1:dpQB+hVHiRc2VVvc2BHxkuM+tmu9Qej/as3apqUbsWc= ··· 199 210 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 200 211 github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= 201 212 github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= 213 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 214 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 215 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 216 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 202 217 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 203 218 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 204 219 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= ··· 221 236 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 222 237 github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= 223 238 github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= 239 + github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w= 240 + github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E= 241 + github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= 242 + github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= 243 + github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 244 + github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 224 245 github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 225 246 github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 226 247 github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= ··· 251 272 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 252 273 github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= 253 274 github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 275 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 276 + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 254 277 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 255 278 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 256 279 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= ··· 273 296 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 274 297 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 275 298 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 299 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 300 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 276 301 github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= 277 302 github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= 278 303 github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= ··· 297 322 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 298 323 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 299 324 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 325 + github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= 326 + github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= 327 + github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 328 + github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 300 329 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 330 + github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 331 + github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 332 + github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 333 + github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 334 + github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 335 + github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 336 + github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= 337 + github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 338 + github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 339 + github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 340 + github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 341 + github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 301 342 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 302 343 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 303 344 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= ··· 309 350 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 310 351 github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= 311 352 github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= 353 + github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= 354 + github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= 355 + github.com/samber/slog-echo v1.15.1 h1:mzeQNPYPxmpehIRtgQJRgJMVvrRbZHp5D2maxSljTBw= 356 + github.com/samber/slog-echo v1.15.1/go.mod h1:K21nbusPmai/MYm8PFactmZoFctkMmkeaTdXXyvhY1c= 312 357 github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 313 358 github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 314 359 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= ··· 356 401 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 357 402 github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= 358 403 github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= 404 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 405 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 406 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 407 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 359 408 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 360 409 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 361 410 github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= ··· 372 421 github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 373 422 github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= 374 423 github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 424 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 425 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 426 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= 427 + gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 375 428 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 376 429 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 377 430 go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= ··· 537 590 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 538 591 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 539 592 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 593 + gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= 594 + gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= 595 + gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= 596 + gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 540 597 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 541 598 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 542 599 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+7 -4
models/user.go
··· 18 18 LastFMUsername *string 19 19 20 20 // atp info 21 - ATProtoDID *string 22 - ATProtoAccessToken *string 23 - ATProtoRefreshToken *string 24 - ATProtoTokenExpiry *time.Time 21 + ATProtoDID *string 22 + //This is meant to only be used by the automated music stamping service. If the user ever does an 23 + //atproto action from the web ui use the atproto session id for the logged-in session 24 + MostRecentAtProtoSessionID *string 25 + //ATProtoAccessToken *string 26 + //ATProtoRefreshToken *string 27 + //ATProtoTokenExpiry *time.Time 25 28 26 29 CreatedAt time.Time 27 30 UpdatedAt time.Time
+104 -134
oauth/atproto/atproto.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + 7 + "github.com/bluesky-social/indigo/atproto/auth/oauth" 8 + _ "github.com/bluesky-social/indigo/atproto/auth/oauth" 9 + "github.com/bluesky-social/indigo/atproto/client" 10 + "github.com/bluesky-social/indigo/atproto/crypto" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + "github.com/teal-fm/piper/db" 13 + 14 + "github.com/teal-fm/piper/session" 15 + 6 16 "log" 7 17 "net/http" 8 18 "net/url" 9 - 10 - oauth "github.com/haileyok/atproto-oauth-golang" 11 - "github.com/haileyok/atproto-oauth-golang/helpers" 12 - "github.com/lestrrat-go/jwx/v2/jwk" 13 - "github.com/teal-fm/piper/db" 14 - "github.com/teal-fm/piper/models" 19 + "os" 20 + "slices" 15 21 ) 16 22 17 23 type ATprotoAuthService struct { 18 - client *oauth.Client 19 - jwks jwk.Key 20 - DB *db.DB 21 - clientId string 22 - callbackUrl string 23 - xrpc *oauth.XrpcClient 24 + clientApp *oauth.ClientApp 25 + DB *db.DB 26 + sessionManager *session.SessionManager 27 + clientId string 28 + callbackUrl string 29 + logger *log.Logger 24 30 } 25 31 26 - func NewATprotoAuthService(db *db.DB, jwks jwk.Key, clientId string, callbackUrl string) (*ATprotoAuthService, error) { 32 + func NewATprotoAuthService(database *db.DB, sessionManager *session.SessionManager, clientSecretKey string, clientId string, callbackUrl string, clientSecretId string) (*ATprotoAuthService, error) { 27 33 fmt.Println(clientId, callbackUrl) 28 - cli, err := oauth.NewClient(oauth.ClientArgs{ 29 - ClientJwk: jwks, 30 - ClientId: clientId, 31 - RedirectUri: callbackUrl, 32 - }) 34 + 35 + scopes := []string{"atproto", "repo:fm.teal.alpha.feed.play", "repo:fm.teal.alpha.actor.status"} 36 + 37 + var config oauth.ClientConfig 38 + config = oauth.NewPublicConfig(clientId, callbackUrl, scopes) 39 + 40 + priv, err := crypto.ParsePrivateMultibase(clientSecretKey) 33 41 if err != nil { 34 - return nil, fmt.Errorf("failed to create atproto oauth client: %w", err) 42 + return nil, err 35 43 } 44 + if err := config.SetClientSecret(priv, clientSecretId); err != nil { 45 + return nil, err 46 + } 47 + 48 + oauthClient := oauth.NewClientApp(&config, db.NewSqliteATProtoStore(database.DB)) 49 + 50 + logger := log.New(os.Stdout, "ATProto oauth: ", log.LstdFlags|log.Lmsgprefix) 51 + 36 52 svc := &ATprotoAuthService{ 37 - client: cli, 38 - jwks: jwks, 39 - callbackUrl: callbackUrl, 40 - DB: db, 41 - clientId: clientId, 53 + clientApp: oauthClient, 54 + callbackUrl: callbackUrl, 55 + DB: database, 56 + sessionManager: sessionManager, 57 + clientId: clientId, 58 + logger: logger, 42 59 } 43 - svc.NewXrpcClient() 44 60 return svc, nil 45 61 } 46 62 47 - func (a *ATprotoAuthService) GetATProtoClient() (*oauth.Client, error) { 48 - if a.client != nil { 49 - return a.client, nil 63 + func (a *ATprotoAuthService) GetATProtoClient(accountDID string, sessionID string, ctx context.Context) (*client.APIClient, error) { 64 + did, err := syntax.ParseDID(accountDID) 65 + if err != nil { 66 + return nil, err 50 67 } 51 68 52 - if a.client == nil { 53 - cli, err := oauth.NewClient(oauth.ClientArgs{ 54 - ClientJwk: a.jwks, 55 - ClientId: a.clientId, 56 - RedirectUri: a.callbackUrl, 57 - }) 58 - if err != nil { 59 - return nil, fmt.Errorf("failed to create atproto oauth client: %w", err) 60 - } 61 - a.client = cli 69 + oauthSess, err := a.clientApp.ResumeSession(ctx, did, sessionID) 70 + if err != nil { 71 + return nil, err 62 72 } 63 73 64 - return a.client, nil 65 - } 74 + return oauthSess.APIClient(), nil 66 75 67 - func LoadJwks(jwksBytes []byte) (jwk.Key, error) { 68 - key, err := helpers.ParseJWKFromBytes(jwksBytes) 69 - if err != nil { 70 - return nil, fmt.Errorf("failed to parse JWK from bytes: %w", err) 71 - } 72 - return key, nil 73 76 } 74 77 75 78 func (a *ATprotoAuthService) HandleLogin(w http.ResponseWriter, r *http.Request) { 76 79 handle := r.URL.Query().Get("handle") 77 80 if handle == "" { 78 - log.Printf("ATProto Login Error: handle is required") 81 + a.logger.Printf("ATProto Login Error: handle is required") 79 82 http.Error(w, "handle query parameter is required", http.StatusBadRequest) 80 83 return 81 84 } 82 - 83 - authUrl, err := a.getLoginUrlAndSaveState(r.Context(), handle) 85 + ctx := r.Context() 86 + redirectURL, err := a.clientApp.StartAuthFlow(ctx, handle) 87 + if err != nil { 88 + http.Error(w, fmt.Sprintf("Error initiating login: %v", err), http.StatusInternalServerError) 89 + } 90 + authUrl, err := url.Parse(redirectURL) 84 91 if err != nil { 85 - log.Printf("ATProto Login Error: Failed to get login URL for handle %s: %v", handle, err) 86 92 http.Error(w, fmt.Sprintf("Error initiating login: %v", err), http.StatusInternalServerError) 87 - return 88 93 } 89 94 90 - log.Printf("ATProto Login: Redirecting user %s to %s", handle, authUrl.String()) 95 + a.logger.Printf("ATProto Login: Redirecting user %s to %s", handle, authUrl.String()) 91 96 http.Redirect(w, r, authUrl.String(), http.StatusFound) 92 97 } 93 98 94 - func (a *ATprotoAuthService) getLoginUrlAndSaveState(ctx context.Context, handle string) (*url.URL, error) { 95 - scope := "atproto transition:generic" 96 - // resolve 97 - ui, err := a.getUserInformation(ctx, handle) 98 - if err != nil { 99 - return nil, fmt.Errorf("failed to get user information for %s: %w", handle, err) 100 - } 101 - 102 - fmt.Println("user info: ", ui.AuthServer, ui.AuthService) 103 - 104 - // create a dpop jwk for this session 105 - k, err := helpers.GenerateKey(nil) // Generate ephemeral DPoP key for this flow 106 - if err != nil { 107 - return nil, fmt.Errorf("failed to generate DPoP key: %w", err) 108 - } 99 + func (a *ATprotoAuthService) HandleLogout(w http.ResponseWriter, r *http.Request) { 100 + cookie, err := r.Cookie("session") 109 101 110 - // Send PAR auth req 111 - parResp, err := a.client.SendParAuthRequest(ctx, ui.AuthServer, ui.AuthMeta, ui.Handle, scope, k) 112 - if err != nil { 113 - return nil, fmt.Errorf("failed PAR request to %s: %w", ui.AuthServer, err) 114 - } 102 + if err == nil { 103 + session, exists := a.sessionManager.GetSession(cookie.Value) 104 + if !exists { 105 + http.Redirect(w, r, "/", http.StatusSeeOther) 106 + return 107 + } 115 108 116 - // Save state 117 - data := &models.ATprotoAuthData{ 118 - State: parResp.State, 119 - DID: ui.DID, 120 - PDSUrl: ui.AuthService, 121 - AuthServerIssuer: ui.AuthMeta.Issuer, 122 - PKCEVerifier: parResp.PkceVerifier, 123 - DPoPAuthServerNonce: parResp.DpopAuthserverNonce, 124 - DPoPPrivateJWK: k, 125 - } 109 + dbUser, err := a.DB.GetUserByID(session.UserID) 110 + if err != nil { 111 + http.Redirect(w, r, "/", http.StatusSeeOther) 112 + return 113 + } 114 + did, err := syntax.ParseDID(*dbUser.ATProtoDID) 126 115 127 - // print data 128 - fmt.Println(data) 116 + if err != nil { 117 + a.logger.Printf("Should not happen: %s", err) 118 + a.sessionManager.ClearSessionCookie(w) 119 + http.Redirect(w, r, "/", http.StatusSeeOther) 120 + } 129 121 130 - err = a.DB.SaveATprotoAuthData(data) 131 - if err != nil { 132 - return nil, fmt.Errorf("failed to save ATProto auth data for state %s: %w", parResp.State, err) 122 + ctx := r.Context() 123 + err = a.clientApp.Logout(ctx, did, session.ATProtoSessionID) 124 + if err != nil { 125 + a.logger.Printf("Error logging the user: %s out: %s", did, err) 126 + } 127 + a.sessionManager.DeleteSession(cookie.Value) 133 128 } 134 129 135 - // Construct authorization URL using the request_uri from PAR response 136 - authEndpointURL, err := url.Parse(ui.AuthMeta.AuthorizationEndpoint) 137 - if err != nil { 138 - return nil, fmt.Errorf("invalid authorization endpoint URL %s: %w", ui.AuthMeta.AuthorizationEndpoint, err) 139 - } 140 - q := authEndpointURL.Query() 141 - q.Set("client_id", a.clientId) 142 - q.Set("request_uri", parResp.RequestUri) 143 - q.Set("state", parResp.State) 144 - authEndpointURL.RawQuery = q.Encode() 130 + a.sessionManager.ClearSessionCookie(w) 145 131 146 - return authEndpointURL, nil 132 + http.Redirect(w, r, "/", http.StatusSeeOther) 147 133 } 148 134 149 135 func (a *ATprotoAuthService) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) { 150 - state := r.URL.Query().Get("state") 151 - code := r.URL.Query().Get("code") 152 - issuer := r.URL.Query().Get("iss") // Issuer (auth base URL) is needed for token request 153 - 154 - if state == "" || code == "" || issuer == "" { 155 - errMsg := r.URL.Query().Get("error") 156 - errDesc := r.URL.Query().Get("error_description") 157 - log.Printf("ATProto Callback Error: Missing parameters. State: '%s', Code: '%s', Issuer: '%s'. Error: '%s', Description: '%s'", state, code, issuer, errMsg, errDesc) 158 - http.Error(w, fmt.Sprintf("Authorization callback failed: %s (%s). Missing state, code, or issuer.", errMsg, errDesc), http.StatusBadRequest) 159 - return 0, fmt.Errorf("missing state, code, or issuer") 160 - } 136 + ctx := r.Context() 161 137 162 - // Retrieve saved data using state 163 - data, err := a.DB.GetATprotoAuthData(state) 138 + sessData, err := a.clientApp.ProcessCallback(ctx, r.URL.Query()) 164 139 if err != nil { 165 - log.Printf("ATProto Callback Error: Failed to retrieve auth data for state '%s': %v", state, err) 166 - http.Error(w, "Invalid or expired state.", http.StatusBadRequest) 167 - return 0, fmt.Errorf("invalid or expired state") 140 + errMsg := fmt.Errorf("processing OAuth callback: %w", err) 141 + http.Error(w, errMsg.Error(), http.StatusBadRequest) 142 + return 0, errMsg 168 143 } 169 144 170 - // Clean up the temporary auth data now that we've retrieved it 171 - // defer a.DB.DeleteATprotoAuthData(state) // Consider adding deletion logic 172 - // if issuers don't match, return an error 173 - if data.AuthServerIssuer != issuer { 174 - log.Printf("ATProto Callback Error: Issuer mismatch for state '%s', expected '%s', got '%s'", state, data.AuthServerIssuer, issuer) 175 - http.Error(w, "Invalid or expired state.", http.StatusBadRequest) 176 - return 0, fmt.Errorf("issuer mismatch") 145 + // It's in the example repo and leaving for some debugging cause i've seen different scopes cause issues before 146 + // so may be some nice debugging info to have 147 + if !slices.Equal(sessData.Scopes, a.clientApp.Config.Scopes) { 148 + a.logger.Printf("session auth scopes did not match those requested") 177 149 } 178 150 179 - resp, err := a.client.InitialTokenRequest(r.Context(), code, issuer, data.PKCEVerifier, data.DPoPAuthServerNonce, data.DPoPPrivateJWK) 151 + user, err := a.DB.FindOrCreateUserByDID(sessData.AccountDID.String()) 180 152 if err != nil { 181 - log.Printf("ATProto Callback Error: Failed initial token request for state '%s', issuer '%s': %v", state, issuer, err) 182 - http.Error(w, fmt.Sprintf("Error exchanging code for token: %v", err), http.StatusInternalServerError) 183 - return 0, fmt.Errorf("failed initial token request") 184 - } 185 - 186 - userID, err := a.DB.FindOrCreateUserByDID(data.DID) 187 - if err != nil { 188 - log.Printf("ATProto Callback Error: Failed to find or create user for DID %s: %v", data.DID, err) 153 + a.logger.Printf("ATProto Callback Error: Failed to find or create user for DID %s: %v", sessData.AccountDID.String(), err) 189 154 http.Error(w, "Failed to process user information.", http.StatusInternalServerError) 190 155 return 0, fmt.Errorf("failed to find or create user") 191 156 } 192 157 193 - err = a.DB.SaveATprotoSession(resp, data.AuthServerIssuer, data.DPoPPrivateJWK, data.PDSUrl) 158 + //This is piper's session for manging piper, not atproto sessions 159 + createdSession := a.sessionManager.CreateSession(user.ID, sessData.SessionID) 160 + a.sessionManager.SetSessionCookie(w, createdSession) 161 + a.logger.Printf("Created session for user %d via service atproto", user.ATProtoDID) 162 + 163 + err = a.DB.SetLatestATProtoSessionId(sessData.AccountDID.String(), sessData.SessionID) 194 164 if err != nil { 195 - log.Printf("ATProto Callback Error: Failed to save ATProto tokens for user %d (DID %s): %v", userID.ID, data.DID, err) 165 + a.logger.Printf("Failed to set latest atproto session id for user %d: %v", user.ID, err) 196 166 } 197 167 198 - log.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", userID.ID, data.DID) 199 - return userID.ID, nil 168 + a.logger.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", user.ID, user.ATProtoDID) 169 + return user.ID, nil 200 170 }
+26 -30
oauth/atproto/http.go
··· 4 4 import ( 5 5 "encoding/json" 6 6 "fmt" 7 - "log" 8 7 "net/http" 9 - 10 - "github.com/haileyok/atproto-oauth-golang/helpers" 11 8 ) 12 9 13 - func (a *ATprotoAuthService) HandleJwks(w http.ResponseWriter, r *http.Request) { 14 - pubKey, err := a.jwks.PublicKey() 15 - if err != nil { 16 - http.Error(w, fmt.Sprintf("Error getting public key from JWK: %v", err), http.StatusInternalServerError) 17 - log.Printf("Error getting public key from JWK: %v", err) 18 - return 19 - } 10 + func strPtr(raw string) *string { 11 + return &raw 12 + } 20 13 14 + func (a *ATprotoAuthService) HandleJwks(w http.ResponseWriter, r *http.Request) { 21 15 w.Header().Set("Content-Type", "application/json") 22 - if err := json.NewEncoder(w).Encode(helpers.CreateJwksResponseObject(pubKey)); err != nil { 23 - log.Printf("Error encoding JWKS response: %v", err) 16 + body := a.clientApp.Config.PublicJWKS() 17 + if err := json.NewEncoder(w).Encode(body); err != nil { 18 + http.Error(w, err.Error(), http.StatusInternalServerError) 19 + return 24 20 } 25 21 } 26 22 27 23 func (a *ATprotoAuthService) HandleClientMetadata(w http.ResponseWriter, r *http.Request, serverUrlRoot, serverMetadataUrl, serverCallbackUrl string) { 28 - metadata := map[string]any{ 29 - "client_id": serverMetadataUrl, 30 - "client_name": "Piper Telekinesis", 31 - "client_uri": serverUrlRoot, 32 - "logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot), 33 - "tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot), 34 - "policy_url": fmt.Sprintf("%s/policy", serverUrlRoot), 35 - "redirect_uris": []string{serverCallbackUrl}, 36 - "grant_types": []string{"authorization_code", "refresh_token"}, 37 - "response_types": []string{"code"}, 38 - "application_type": "web", 39 - "dpop_bound_access_tokens": true, 40 - "jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot), 41 - "scope": "atproto transition:generic", 42 - "token_endpoint_auth_method": "private_key_jwt", 43 - "token_endpoint_auth_signing_alg": "ES256", 24 + 25 + meta := a.clientApp.Config.ClientMetadata() 26 + if a.clientApp.Config.IsConfidential() { 27 + meta.JWKSURI = strPtr(fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot)) 44 28 } 29 + meta.ClientName = strPtr("Piper Telekinesis") 30 + meta.ClientURI = strPtr(serverUrlRoot) 31 + 32 + // internal consistency check 33 + if err := meta.Validate(a.clientApp.Config.ClientID); err != nil { 34 + a.logger.Printf("validating client metadata", "err", err) 35 + http.Error(w, err.Error(), http.StatusInternalServerError) 36 + return 37 + } 38 + 45 39 w.Header().Set("Content-Type", "application/json") 46 - if err := json.NewEncoder(w).Encode(metadata); err != nil { 47 - log.Printf("Error encoding client metadata: %v", err) 40 + if err := json.NewEncoder(w).Encode(meta); err != nil { 41 + http.Error(w, err.Error(), http.StatusInternalServerError) 42 + return 48 43 } 44 + 49 45 }
+2 -55
oauth/atproto/resolve.go
··· 12 12 "strings" 13 13 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 - oauth "github.com/haileyok/atproto-oauth-golang" 16 15 ) 17 16 18 17 // user information struct 19 18 type UserInformation struct { 20 - AuthService string `json:"authService"` 21 - AuthServer string `json:"authServer"` 22 - AuthMeta *oauth.OauthAuthorizationMetadata `json:"authMeta"` 19 + AuthService string `json:"authService"` 20 + AuthServer string `json:"authServer"` 23 21 // do NOT save the current handle permanently! 24 22 Handle string `json:"handle"` 25 23 DID string `json:"did"` ··· 32 30 Type string `json:"type"` 33 31 ServiceEndpoint string `json:"serviceEndpoint"` 34 32 } `json:"service"` 35 - } 36 - 37 - func (a *ATprotoAuthService) getUserInformation(ctx context.Context, handleOrDid string) (*UserInformation, error) { 38 - cli := a.client 39 - 40 - // if we have a did skip this 41 - did := handleOrDid 42 - err := error(nil) 43 - // technically checking SHOULD be more rigorous. 44 - if !strings.HasPrefix(handleOrDid, "did:") { 45 - did, err = resolveHandle(ctx, did) 46 - if err != nil { 47 - return nil, err 48 - } 49 - } else { 50 - did = handleOrDid 51 - } 52 - 53 - doc, err := getIdentityDocument(ctx, did) 54 - if err != nil { 55 - return nil, err 56 - } 57 - 58 - service, err := getAtprotoPdsService(doc) 59 - if err != nil { 60 - return nil, err 61 - } 62 - 63 - authserver, err := cli.ResolvePdsAuthServer(ctx, service) 64 - if err != nil { 65 - return nil, err 66 - } 67 - 68 - authmeta, err := cli.FetchAuthServerMetadata(ctx, authserver) 69 - if err != nil { 70 - return nil, err 71 - } 72 - 73 - if len(doc.AlsoKnownAs) == 0 { 74 - return nil, fmt.Errorf("alsoKnownAs is empty, couldn't acquire handle: %w", err) 75 - 76 - } 77 - handle := strings.Replace(doc.AlsoKnownAs[0], "at://", "", 1) 78 - 79 - return &UserInformation{ 80 - AuthService: service, 81 - AuthServer: authserver, 82 - AuthMeta: authmeta, 83 - Handle: handle, 84 - DID: did, 85 - }, nil 86 33 } 87 34 88 35 func resolveHandle(ctx context.Context, handle string) (string, error) {
-25
oauth/atproto/xrpc.go
··· 1 - package atproto 2 - 3 - import ( 4 - "log/slog" 5 - 6 - oauth "github.com/haileyok/atproto-oauth-golang" 7 - ) 8 - 9 - func (atp *ATprotoAuthService) NewXrpcClient() { 10 - atp.xrpc = &oauth.XrpcClient{ 11 - OnDpopPdsNonceChanged: func(did, newNonce string) { 12 - _, err := atp.DB.Exec("UPDATE users SET atproto_pds_nonce = ? WHERE atproto_did = ?", newNonce, did) 13 - if err != nil { 14 - slog.Default().Error("error updating pds nonce", "err", err) 15 - } 16 - }, 17 - } 18 - } 19 - 20 - func (atp *ATprotoAuthService) GetXrpcClient() *oauth.XrpcClient { 21 - if atp.xrpc == nil { 22 - atp.NewXrpcClient() 23 - } 24 - return atp.xrpc 25 - }
+5
oauth/oauth2.go
··· 86 86 http.Redirect(w, r, authURL, http.StatusSeeOther) 87 87 } 88 88 89 + func (o *OAuth2Service) HandleLogout(w http.ResponseWriter, r *http.Request) { 90 + //TODO not implemented yet. not sure what the api call is for this package 91 + http.Redirect(w, r, "/", http.StatusSeeOther) 92 + } 93 + 89 94 func (o *OAuth2Service) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) { 90 95 state := r.URL.Query().Get("state") 91 96 if state != o.state {
+22 -15
oauth/oauth_manager.go
··· 6 6 "log" 7 7 "net/http" 8 8 "sync" 9 - 10 - "github.com/teal-fm/piper/session" 11 9 ) 12 10 13 11 // manages multiple oauth client services 14 12 type OAuthServiceManager struct { 15 - services map[string]AuthService 16 - sessionManager *session.SessionManager 17 - mu sync.RWMutex 18 - logger *log.Logger 13 + services map[string]AuthService 14 + mu sync.RWMutex 15 + logger *log.Logger 19 16 } 20 17 21 - func NewOAuthServiceManager(sessionManager *session.SessionManager) *OAuthServiceManager { 18 + func NewOAuthServiceManager() *OAuthServiceManager { 22 19 return &OAuthServiceManager{ 23 - services: make(map[string]AuthService), 24 - sessionManager: sessionManager, 25 - logger: log.New(log.Writer(), "oauth: ", log.LstdFlags|log.Lmsgprefix), 20 + services: make(map[string]AuthService), 21 + logger: log.New(log.Writer(), "oauth: ", log.LstdFlags|log.Lmsgprefix), 26 22 } 27 23 } 28 24 ··· 58 54 } 59 55 } 60 56 57 + func (m *OAuthServiceManager) HandleLogout(serviceName string) http.HandlerFunc { 58 + return func(w http.ResponseWriter, r *http.Request) { 59 + m.mu.RLock() 60 + service, exists := m.services[serviceName] 61 + m.mu.RUnlock() 62 + 63 + if exists { 64 + service.HandleLogout(w, r) 65 + return 66 + } 67 + 68 + m.logger.Printf("Auth service '%s' not found for login request", serviceName) 69 + http.Error(w, fmt.Sprintf("Auth service '%s' not found", serviceName), http.StatusNotFound) 70 + } 71 + } 72 + 61 73 func (m *OAuthServiceManager) HandleCallback(serviceName string) http.HandlerFunc { 62 74 return func(w http.ResponseWriter, r *http.Request) { 63 75 m.mu.RLock() ··· 81 93 } 82 94 83 95 if userID > 0 { 84 - session := m.sessionManager.CreateSession(userID) 85 - 86 - m.sessionManager.SetSessionCookie(w, session) 87 - 88 - m.logger.Printf("Created session for user %d via service %s", userID, serviceName) 89 96 90 97 http.Redirect(w, r, "/", http.StatusSeeOther) 91 98 } else {
+2
oauth/service.go
··· 10 10 // handles the callback for the provider. is responsible for inserting 11 11 // sessions in the db 12 12 HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) 13 + 14 + HandleLogout(w http.ResponseWriter, r *http.Request) 13 15 } 14 16 15 17 // optional but recommended
+1 -1
service/lastfm/lastfm.go
··· 390 390 } 391 391 l.db.SaveTrack(user.ID, hydratedTrack) 392 392 l.logger.Printf("Submitting track") 393 - err = l.SubmitTrackToPDS(*user.ATProtoDID, hydratedTrack, ctx) 393 + err = l.SubmitTrackToPDS(*user.ATProtoDID, *user.MostRecentAtProtoSessionID, hydratedTrack, ctx) 394 394 if err != nil { 395 395 l.logger.Printf("error submitting track for user %s: %s - %s: %v", username, track.Artist.Text, track.Name, err) 396 396 }
+31 -58
service/playingnow/playingnow.go
··· 8 8 "strconv" 9 9 "time" 10 10 11 - "github.com/bluesky-social/indigo/api/atproto" 11 + "github.com/bluesky-social/indigo/atproto/client" 12 12 lexutil "github.com/bluesky-social/indigo/lex/util" 13 - "github.com/bluesky-social/indigo/xrpc" 14 - oauth "github.com/haileyok/atproto-oauth-golang" 15 13 "github.com/spf13/viper" 14 + 15 + comatproto "github.com/bluesky-social/indigo/api/atproto" 16 16 "github.com/teal-fm/piper/api/teal" 17 17 "github.com/teal-fm/piper/db" 18 18 "github.com/teal-fm/piper/models" ··· 52 52 53 53 did := *user.ATProtoDID 54 54 55 - // Get ATProto client 56 - client, err := p.atprotoService.GetATProtoClient() 57 - if err != nil || client == nil { 58 - return fmt.Errorf("failed to get ATProto client: %w", err) 59 - } 60 - 61 - xrpcClient := p.atprotoService.GetXrpcClient() 62 - if xrpcClient == nil { 63 - return fmt.Errorf("xrpc client is not available") 64 - } 65 - 66 - // Get user session 67 - sess, err := p.db.GetAtprotoSession(did, ctx, *client) 68 - if err != nil { 69 - return fmt.Errorf("couldn't get Atproto session for DID %s: %w", did, err) 55 + // Get ATProto atProtoClient 56 + atProtoClient, err := p.atprotoService.GetATProtoClient(did, *user.MostRecentAtProtoSessionID, ctx) 57 + if err != nil || atProtoClient == nil { 58 + return fmt.Errorf("failed to get ATProto atProtoClient: %w", err) 70 59 } 71 60 72 61 // Convert track to PlayView format ··· 86 75 Item: playView, 87 76 } 88 77 89 - authArgs := db.AtpSessionToAuthArgs(sess) 90 78 var swapRecord *string 91 - swapRecord, err = p.getStatusSwapRecord(ctx, xrpcClient, sess, authArgs) 79 + swapRecord, err = p.getStatusSwapRecord(ctx, atProtoClient) 92 80 if err != nil { 93 81 return err 94 82 } 95 83 96 84 // Create the record input 97 - input := atproto.RepoPutRecord_Input{ 85 + input := comatproto.RepoPutRecord_Input{ 98 86 Collection: "fm.teal.alpha.actor.status", 99 - Repo: sess.DID, 87 + Repo: atProtoClient.AccountDID.String(), 100 88 Rkey: "self", // Use "self" as the record key for current status 101 89 Record: &lexutil.LexiconTypeDecoder{Val: status}, 102 90 SwapRecord: swapRecord, 103 91 } 104 92 105 93 // Submit to PDS 106 - var out atproto.RepoPutRecord_Output 107 - if err := xrpcClient.Do(ctx, authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil { 94 + if _, err := comatproto.RepoPutRecord(ctx, atProtoClient, &input); err != nil { 108 95 p.logger.Printf("Error creating playing now status for DID %s: %v", did, err) 109 96 return fmt.Errorf("failed to create playing now status for DID %s: %w", did, err) 110 97 } ··· 131 118 did := *user.ATProtoDID 132 119 133 120 // Get ATProto clients 134 - client, err := p.atprotoService.GetATProtoClient() 135 - if err != nil || client == nil { 136 - return fmt.Errorf("failed to get ATProto client: %w", err) 137 - } 138 - 139 - xrpcClient := p.atprotoService.GetXrpcClient() 140 - if xrpcClient == nil { 141 - return fmt.Errorf("xrpc client is not available") 142 - } 143 - 144 - // Get user session 145 - sess, err := p.db.GetAtprotoSession(did, ctx, *client) 146 - if err != nil { 147 - return fmt.Errorf("couldn't get Atproto session for DID %s: %w", did, err) 121 + atProtoClient, err := p.atprotoService.GetATProtoClient(did, *user.MostRecentAtProtoSessionID, ctx) 122 + if err != nil || atProtoClient == nil { 123 + return fmt.Errorf("failed to get ATProto atProtoClient: %w", err) 148 124 } 149 125 150 126 // Create an expired status (essentially clearing it) ··· 164 140 Item: emptyPlayView, 165 141 } 166 142 167 - authArgs := db.AtpSessionToAuthArgs(sess) 168 143 var swapRecord *string 169 - swapRecord, err = p.getStatusSwapRecord(ctx, xrpcClient, sess, authArgs) 144 + swapRecord, err = p.getStatusSwapRecord(ctx, atProtoClient) 170 145 if err != nil { 171 146 return err 172 147 } 173 148 174 149 // Update the record 175 - input := atproto.RepoPutRecord_Input{ 150 + input := comatproto.RepoPutRecord_Input{ 176 151 Collection: "fm.teal.alpha.actor.status", 177 - Repo: sess.DID, 152 + Repo: atProtoClient.AccountDID.String(), 178 153 Rkey: "self", 179 154 Record: &lexutil.LexiconTypeDecoder{Val: status}, 180 155 SwapRecord: swapRecord, 181 156 } 182 157 183 - var out atproto.RepoPutRecord_Output 184 - if err := xrpcClient.Do(ctx, authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil { 158 + if _, err := comatproto.RepoPutRecord(ctx, atProtoClient, &input); err != nil { 185 159 p.logger.Printf("Error clearing playing now status for DID %s: %v", did, err) 186 160 return fmt.Errorf("failed to clear playing now status for DID %s: %w", did, err) 187 161 } ··· 242 216 // Get submission client agent 243 217 submissionAgent := viper.GetString("app.submission_agent") 244 218 if submissionAgent == "" { 245 - submissionAgent = "piper/v0.0.1" 219 + submissionAgent = "piper/v0.0.2" 246 220 } 247 221 248 222 playView := &teal.AlphaFeedDefs_PlayView{ ··· 264 238 265 239 // getStatusSwapRecord retrieves the current swap record (CID) for the actor status record. 266 240 // Returns (nil, nil) if the record does not exist yet. 267 - func (p *PlayingNowService) getStatusSwapRecord(ctx context.Context, xrpcClient *oauth.XrpcClient, sess *models.ATprotoAuthSession, authArgs *oauth.XrpcAuthedRequestArgs) (*string, error) { 268 - getOutput := atproto.RepoGetRecord_Output{} 269 - if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.repo.getRecord", map[string]any{ 270 - "repo": sess.DID, 271 - "collection": "fm.teal.alpha.actor.status", 272 - "rkey": "self", 273 - }, nil, &getOutput); err != nil { 274 - xErr, ok := err.(*xrpc.Error) 241 + func (p *PlayingNowService) getStatusSwapRecord(ctx context.Context, atApiClient *client.APIClient) (*string, error) { 242 + result, err := comatproto.RepoGetRecord(ctx, atApiClient, "", "fm.teal.alpha.actor.status", atApiClient.AccountDID.String(), "self") 243 + 244 + if err != nil { 245 + xErr, ok := err.(*client.APIError) 275 246 if !ok { 276 - return nil, fmt.Errorf("could not get record: %w", err) 247 + return nil, fmt.Errorf("error getting the record: %w", err) 277 248 } 278 - if xErr.StatusCode != 400 { // 400 means not found in this API 279 - return nil, fmt.Errorf("could not get record: %w", err) 249 + if xErr.StatusCode == 400 { // 400 means not found in this API, which would be the case if the record does not exist yet 250 + return nil, nil 280 251 } 281 - return nil, nil 252 + 253 + return nil, fmt.Errorf("error getting the record: %w", err) 254 + 282 255 } 283 - return getOutput.Cid, nil 256 + return result.Cid, nil 284 257 }
+1 -1
service/spotify/spotify.go
··· 657 657 658 658 s.logger.Printf("User %d (%d): Attempting to submit track '%s' by %s to PDS (DID: %s)", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name, artistName, *dbUser.ATProtoDID) 659 659 // Use context.Background() for now, or pass down a context if available 660 - if errPDS := s.SubmitTrackToPDS(*dbUser.ATProtoDID, trackToSubmitToPDS, context.Background()); errPDS != nil { 660 + if errPDS := s.SubmitTrackToPDS(*dbUser.ATProtoDID, *dbUser.MostRecentAtProtoSessionID, trackToSubmitToPDS, context.Background()); errPDS != nil { 661 661 s.logger.Printf("User %d (%d): Error submitting track '%s' to PDS: %v", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name, errPDS) 662 662 } else { 663 663 s.logger.Printf("User %d (%d): Successfully submitted track '%s' to PDS.", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name)
+22 -29
session/session.go
··· 17 17 18 18 // session/session.go 19 19 type Session struct { 20 - ID string 21 - UserID int64 22 - ATprotoDID string 23 - ATprotoAccessToken string 24 - ATprotoRefreshToken string 25 - CreatedAt time.Time 26 - ExpiresAt time.Time 20 + 21 + //need to re work this. May add onto it for atproto oauth. But need to be careful about that expiresd 22 + //Maybe a speerate oauth session store table and it has a created date? yeah do that then can look it up by session id from this table for user actions 23 + 24 + ID string 25 + UserID int64 26 + ATProtoSessionID string 27 + CreatedAt time.Time 28 + ExpiresAt time.Time 27 29 } 28 30 29 31 type SessionManager struct { ··· 38 40 _, err := database.Exec(` 39 41 CREATE TABLE IF NOT EXISTS sessions ( 40 42 id TEXT PRIMARY KEY, 41 - user_id INTEGER NOT NULL, 43 + user_id INTEGER NOT NULL, 44 + at_proto_session_id TEXT NOT NULL, 42 45 created_at TIMESTAMP, 43 46 expires_at TIMESTAMP, 44 47 FOREIGN KEY (user_id) REFERENCES users(id) ··· 58 61 } 59 62 60 63 // create a new session for a user 61 - func (sm *SessionManager) CreateSession(userID int64) *Session { 64 + func (sm *SessionManager) CreateSession(userID int64, atProtoSessionId string) *Session { 62 65 sm.mu.Lock() 63 66 defer sm.mu.Unlock() 64 67 ··· 71 74 expiresAt := now.Add(24 * time.Hour) // 24-hour session 72 75 73 76 session := &Session{ 74 - ID: sessionID, 75 - UserID: userID, 76 - CreatedAt: now, 77 - ExpiresAt: expiresAt, 77 + ID: sessionID, 78 + UserID: userID, 79 + ATProtoSessionID: atProtoSessionId, 80 + CreatedAt: now, 81 + ExpiresAt: expiresAt, 78 82 } 79 83 80 84 // store session in memory ··· 83 87 // store session in database if available 84 88 if sm.db != nil { 85 89 _, err := sm.db.Exec(` 86 - INSERT INTO sessions (id, user_id, created_at, expires_at) 87 - VALUES (?, ?, ?, ?)`, 88 - sessionID, userID, now, expiresAt) 90 + INSERT INTO sessions (id, user_id, at_proto_session_id, created_at, expires_at) 91 + VALUES (?, ?, ?, ?, ?)`, 92 + sessionID, userID, atProtoSessionId, now, expiresAt) 89 93 90 94 if err != nil { 91 95 log.Printf("Error storing session in database: %v", err) ··· 116 120 session = &Session{ID: sessionID} 117 121 118 122 err := sm.db.QueryRow(` 119 - SELECT user_id, created_at, expires_at 123 + SELECT user_id, at_proto_session_id, created_at, expires_at 120 124 FROM sessions WHERE id = ?`, sessionID).Scan( 121 - &session.UserID, &session.CreatedAt, &session.ExpiresAt) 125 + &session.UserID, &session.ATProtoSessionID, &session.CreatedAt, &session.ExpiresAt) 122 126 123 127 if err != nil { 124 128 return nil, false ··· 178 182 MaxAge: -1, 179 183 } 180 184 http.SetCookie(w, cookie) 181 - } 182 - 183 - func (sm *SessionManager) HandleLogout(w http.ResponseWriter, r *http.Request) { 184 - cookie, err := r.Cookie("session") 185 - if err == nil { 186 - sm.DeleteSession(cookie.Value) 187 - } 188 - 189 - sm.ClearSessionCookie(w) 190 - 191 - http.Redirect(w, r, "/", http.StatusSeeOther) 192 185 } 193 186 194 187 func (sm *SessionManager) GetAPIKeyManager() *apikey.ApiKeyManager {