From 3c800aa38c85676839d9d9eeaf47d6e59e512158 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:37:48 -0700 Subject: [PATCH 1/4] feat(oauth): return standard User from /oauth/me, document in swagger Replaces the hand-rolled JWT-style response (userId, name, handle, verified, sub, iat) with the standard User struct via the existing v1UserResponse helper, matching every other user endpoint. Adds /oauth/me as a documented path in swagger-v1.yaml under a new oauth tag, referencing the existing user_response_single schema. Co-Authored-By: Claude Sonnet 4.6 --- api/swagger/swagger-v1.yaml | 28 ++++++++++++++++++++++++++++ api/v1_oauth.go | 16 +--------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 7791f0c0..192910a2 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -63,6 +63,8 @@ tags: description: Rewards related operations - name: prizes description: Prize claiming related operations + - name: oauth + description: OAuth 2.0 authorization paths: /challenges/undisbursed: get: @@ -9748,6 +9750,32 @@ paths: application/json: schema: $ref: "#/components/schemas/transaction_history_count_response" + /oauth/me: + get: + tags: + - oauth + summary: Get authenticated user + description: Returns the full profile of the currently authenticated user based on the Bearer access token obtained via OAuth. + operationId: Get Authenticated User + security: + - OAuth2: + - read + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/user_response_single" + "401": + description: Missing or invalid access token + content: {} + "404": + description: User not found + content: {} + "500": + description: Server error + content: {} components: schemas: create_access_key_response: diff --git a/api/v1_oauth.go b/api/v1_oauth.go index bafdbe58..87d4550f 100644 --- a/api/v1_oauth.go +++ b/api/v1_oauth.go @@ -570,21 +570,7 @@ func (app *ApiServer) v1OAuthMe(c *fiber.Ctx) error { return oauthError(c, fiber.StatusNotFound, "invalid_token", "User not found") } - user := users[0] - - response := fiber.Map{ - "userId": user.ID, - "name": user.Name.String, - "handle": user.Handle.String, - "verified": user.IsVerified, - "sub": user.ID, - "iat": time.Now().Unix(), - } - if user.ProfilePicture != nil { - response["profilePicture"] = user.ProfilePicture - } - - return c.JSON(response) + return c.JSON(fiber.Map{"data": users[0]}) } // --- Helper methods --- From 0c4dd5b115947375a374f02de127dea89d9e287b Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:50:49 -0700 Subject: [PATCH 2/4] test(oauth): update /oauth/me tests for new { data: User } response shape Co-Authored-By: Claude Sonnet 4.6 --- api/v1_oauth_test.go | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/api/v1_oauth_test.go b/api/v1_oauth_test.go index 022ec96a..86d8e987 100644 --- a/api/v1_oauth_test.go +++ b/api/v1_oauth_test.go @@ -913,16 +913,12 @@ func TestOAuthMe(t *testing.T) { status, body := oauthGetWithBearer(t, app, "/v1/oauth/me", accessToken) assert.Equal(t, 200, status) - assert.True(t, gjson.GetBytes(body, "userId").Exists()) + assert.True(t, gjson.GetBytes(body, "data.id").Exists()) jsonAssert(t, body, map[string]any{ - "handle": "oauthuser", - "name": "OAuth User", - "verified": false, + "data.handle": "oauthuser", + "data.name": "OAuth User", + "data.is_verified": false, }) - assert.Equal(t, - gjson.GetBytes(body, "userId").String(), - gjson.GetBytes(body, "sub").String(), - ) } func TestOAuthMe_InvalidToken(t *testing.T) { @@ -1017,7 +1013,7 @@ func TestOAuthFullFlow(t *testing.T) { // Step 2: Use access token to get user profile status, body = oauthGetWithBearer(t, app, "/v1/oauth/me", accessToken) assert.Equal(t, 200, status) - jsonAssert(t, body, map[string]any{"handle": "oauthuser"}) + jsonAssert(t, body, map[string]any{"data.handle": "oauthuser"}) // Step 3: Refresh the token status, body = oauthPostJSON(t, app, "/v1/oauth/token", map[string]string{ @@ -1034,7 +1030,7 @@ func TestOAuthFullFlow(t *testing.T) { // Step 4: New access token works status, body = oauthGetWithBearer(t, app, "/v1/oauth/me", newAccessToken) assert.Equal(t, 200, status) - jsonAssert(t, body, map[string]any{"handle": "oauthuser"}) + jsonAssert(t, body, map[string]any{"data.handle": "oauthuser"}) // Step 5: Revoke status, _ = oauthPostJSON(t, app, "/v1/oauth/revoke", map[string]string{ From 2b0b626551bedf0baff4de1ab90a6f8490a33974 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:58:37 -0700 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- api/v1_oauth.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1_oauth.go b/api/v1_oauth.go index 87d4550f..11c12279 100644 --- a/api/v1_oauth.go +++ b/api/v1_oauth.go @@ -560,7 +560,8 @@ func (app *ApiServer) v1OAuthMe(c *fiber.Ctx) error { // Fetch user via the standard query helper (includes rendezvous-based image URLs) users, err := app.queries.Users(c.Context(), dbv1.GetUsersParams{ - Ids: []int32{entry.UserID}, + Ids: []int32{entry.UserID}, + MyID: entry.UserID, }) if err != nil { app.logger.Error("Failed to query user for /oauth/me", zap.Error(err)) From d65f2a82f68d0256ae5cecc94fcb968e0c94058e Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:14:19 -0700 Subject: [PATCH 4/4] feat(me): move /oauth/me to /me, support any auth method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the endpoint to /v1/me and extracts it into its own file (v1_me.go). The new handler uses the auth context already resolved by authMiddleware (PKCE token, wallet signature, JWT, or API key) rather than requiring a Bearer OAuth token specifically. Resolves userId from myId locals (set for PKCE) or falls back to wallet→userId lookup for other auth methods. Co-Authored-By: Claude Sonnet 4.6 --- api/server.go | 2 +- api/swagger/swagger-v1.yaml | 2 +- api/v1_me.go | 36 ++++++++++++++++++++++++++++++++++++ api/v1_oauth.go | 35 ----------------------------------- api/v1_oauth_test.go | 18 +++++++++--------- 5 files changed, 47 insertions(+), 46 deletions(-) create mode 100644 api/v1_me.go diff --git a/api/server.go b/api/server.go index 5029e2d4..7c429ed8 100644 --- a/api/server.go +++ b/api/server.go @@ -556,7 +556,7 @@ func NewApiServer(config config.Config) *ApiServer { g.Post("/oauth/authorize", app.v1OAuthAuthorize) g.Post("/oauth/token", app.v1OAuthToken) g.Post("/oauth/revoke", app.v1OAuthRevoke) - g.Get("/oauth/me", app.requireAuthMiddleware, app.v1OAuthMe) + g.Get("/me", app.requireAuthMiddleware, app.v1Me) // Rewards g.Post("/rewards/claim", app.v1ClaimRewards) diff --git a/api/swagger/swagger-v1.yaml b/api/swagger/swagger-v1.yaml index 192910a2..67baf9aa 100644 --- a/api/swagger/swagger-v1.yaml +++ b/api/swagger/swagger-v1.yaml @@ -9750,7 +9750,7 @@ paths: application/json: schema: $ref: "#/components/schemas/transaction_history_count_response" - /oauth/me: + /me: get: tags: - oauth diff --git a/api/v1_me.go b/api/v1_me.go new file mode 100644 index 00000000..5a680ebc --- /dev/null +++ b/api/v1_me.go @@ -0,0 +1,36 @@ +package api + +import ( + "api.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +// v1Me handles GET /v1/me +// Returns the authenticated user's profile. Supports any auth method that +// resolves to a user (OAuth PKCE token, wallet signature, JWT, or API key). +func (app *ApiServer) v1Me(c *fiber.Ctx) error { + userId := app.getMyId(c) + if userId == 0 { + wallet := app.getAuthedWallet(c) + id, err := app.getUserIDFromWallet(c.Context(), wallet) + if err != nil { + return fiber.NewError(fiber.StatusUnauthorized, "Could not resolve authenticated user") + } + userId = int32(id) + } + + users, err := app.queries.Users(c.Context(), dbv1.GetUsersParams{ + Ids: []int32{userId}, + MyID: userId, + }) + if err != nil { + app.logger.Error("Failed to query user for /me", zap.Error(err)) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get user info") + } + if len(users) == 0 { + return fiber.NewError(fiber.StatusNotFound, "User not found") + } + + return c.JSON(fiber.Map{"data": users[0]}) +} diff --git a/api/v1_oauth.go b/api/v1_oauth.go index 11c12279..334be08e 100644 --- a/api/v1_oauth.go +++ b/api/v1_oauth.go @@ -12,7 +12,6 @@ import ( "strings" "time" - "api.audius.co/api/dbv1" "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5" "go.uber.org/zap" @@ -539,40 +538,6 @@ func (app *ApiServer) v1OAuthRevoke(c *fiber.Ctx) error { return c.JSON(fiber.Map{}) } -// v1OAuthMe handles GET /v1/oauth/me -// Returns the authenticated user's profile based on Bearer access token. -func (app *ApiServer) v1OAuthMe(c *fiber.Ctx) error { - // Extract Bearer token - authHeader := c.Get("Authorization") - if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { - return oauthError(c, fiber.StatusUnauthorized, "invalid_token", "Missing or invalid Authorization header") - } - token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer ")) - if token == "" { - return oauthError(c, fiber.StatusUnauthorized, "invalid_token", "Bearer token is empty") - } - - // Look up the access token (try cache first) - entry, ok := app.lookupOAuthAccessToken(c, token) - if !ok { - return oauthError(c, fiber.StatusUnauthorized, "invalid_token", "Invalid or expired access token") - } - - // Fetch user via the standard query helper (includes rendezvous-based image URLs) - users, err := app.queries.Users(c.Context(), dbv1.GetUsersParams{ - Ids: []int32{entry.UserID}, - MyID: entry.UserID, - }) - if err != nil { - app.logger.Error("Failed to query user for /oauth/me", zap.Error(err)) - return oauthError(c, fiber.StatusInternalServerError, "server_error", "Failed to get user info") - } - if len(users) == 0 { - return oauthError(c, fiber.StatusNotFound, "invalid_token", "User not found") - } - - return c.JSON(fiber.Map{"data": users[0]}) -} // --- Helper methods --- diff --git a/api/v1_oauth_test.go b/api/v1_oauth_test.go index 86d8e987..7ceb84df 100644 --- a/api/v1_oauth_test.go +++ b/api/v1_oauth_test.go @@ -901,7 +901,7 @@ func TestOAuthRevoke_MissingToken(t *testing.T) { jsonAssert(t, body, map[string]any{"error": "invalid_request"}) } -// --- /oauth/me --- +// --- /me --- func TestOAuthMe(t *testing.T) { app := emptyTestApp(t) @@ -910,7 +910,7 @@ func TestOAuthMe(t *testing.T) { familyID := "test-family-me" accessToken, _ := insertTestTokens(t, app, clientID, 100, "read", familyID, time.Hour, 30*24*time.Hour) - status, body := oauthGetWithBearer(t, app, "/v1/oauth/me", accessToken) + status, body := oauthGetWithBearer(t, app, "/v1/me", accessToken) assert.Equal(t, 200, status) assert.True(t, gjson.GetBytes(body, "data.id").Exists()) @@ -924,7 +924,7 @@ func TestOAuthMe(t *testing.T) { func TestOAuthMe_InvalidToken(t *testing.T) { app := emptyTestApp(t) - status, _ := oauthGetWithBearer(t, app, "/v1/oauth/me", "invalid-token") + status, _ := oauthGetWithBearer(t, app, "/v1/me", "invalid-token") // requireAuthMiddleware intercepts before the handler runs assert.Equal(t, 401, status) @@ -948,7 +948,7 @@ func TestOAuthMe_ExpiredToken(t *testing.T) { }, }) - status, _ := oauthGetWithBearer(t, app, "/v1/oauth/me", expiredToken) + status, _ := oauthGetWithBearer(t, app, "/v1/me", expiredToken) // requireAuthMiddleware intercepts before the handler runs assert.Equal(t, 401, status) @@ -973,7 +973,7 @@ func TestOAuthMe_RevokedToken(t *testing.T) { }, }) - status, _ := oauthGetWithBearer(t, app, "/v1/oauth/me", revokedToken) + status, _ := oauthGetWithBearer(t, app, "/v1/me", revokedToken) // requireAuthMiddleware intercepts before the handler runs assert.Equal(t, 401, status) @@ -982,7 +982,7 @@ func TestOAuthMe_RevokedToken(t *testing.T) { func TestOAuthMe_MissingAuthHeader(t *testing.T) { app := emptyTestApp(t) - req := httptest.NewRequest("GET", "/v1/oauth/me", nil) + req := httptest.NewRequest("GET", "/v1/me", nil) res, err := app.Test(req, -1) require.NoError(t, err) @@ -1011,7 +1011,7 @@ func TestOAuthFullFlow(t *testing.T) { refreshToken := gjson.GetBytes(body, "refresh_token").String() // Step 2: Use access token to get user profile - status, body = oauthGetWithBearer(t, app, "/v1/oauth/me", accessToken) + status, body = oauthGetWithBearer(t, app, "/v1/me", accessToken) assert.Equal(t, 200, status) jsonAssert(t, body, map[string]any{"data.handle": "oauthuser"}) @@ -1028,7 +1028,7 @@ func TestOAuthFullFlow(t *testing.T) { assert.NotEqual(t, refreshToken, newRefreshToken) // Step 4: New access token works - status, body = oauthGetWithBearer(t, app, "/v1/oauth/me", newAccessToken) + status, body = oauthGetWithBearer(t, app, "/v1/me", newAccessToken) assert.Equal(t, 200, status) jsonAssert(t, body, map[string]any{"data.handle": "oauthuser"}) @@ -1040,7 +1040,7 @@ func TestOAuthFullFlow(t *testing.T) { assert.Equal(t, 200, status) // Step 6: Revoked tokens no longer work - status, _ = oauthGetWithBearer(t, app, "/v1/oauth/me", newAccessToken) + status, _ = oauthGetWithBearer(t, app, "/v1/me", newAccessToken) assert.Equal(t, 401, status) // Refreshing with the new refresh token also fails (family revoked)