Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions api/swagger/swagger-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -9748,6 +9750,32 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/transaction_history_count_response"
/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:
Expand Down
36 changes: 36 additions & 0 deletions api/v1_me.go
Original file line number Diff line number Diff line change
@@ -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]})
}
48 changes: 0 additions & 48 deletions api/v1_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -539,53 +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},
})
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")
}

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)
}

// --- Helper methods ---

Expand Down
34 changes: 15 additions & 19 deletions api/v1_oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -910,25 +910,21 @@ 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, "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) {
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)
Expand All @@ -952,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)
Expand All @@ -977,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)
Expand All @@ -986,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)

Expand Down Expand Up @@ -1015,9 +1011,9 @@ 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{"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{
Expand All @@ -1032,9 +1028,9 @@ 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{"handle": "oauthuser"})
jsonAssert(t, body, map[string]any{"data.handle": "oauthuser"})

// Step 5: Revoke
status, _ = oauthPostJSON(t, app, "/v1/oauth/revoke", map[string]string{
Expand All @@ -1044,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)
Expand Down
Loading