diff --git a/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx b/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx index 19e6e2a7c..2e20deec8 100644 --- a/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx +++ b/apps/backend/src/app/api/latest/internal/feature-requests/route.tsx @@ -72,9 +72,15 @@ export const GET = createSmartRouteHandler({ const posts = data.results || []; + // Filter out posts that have been merged into other posts or are completed + const activePosts = posts.filter((post: any) => + !post.mergedToSubmissionId && + post.postStatus?.type !== 'completed' + ); + // Check upvote status for each post for the current user using Featurebase email const postsWithUpvoteStatus = await Promise.all( - posts.map(async (post: any) => { + activePosts.map(async (post: any) => { let userHasUpvoted = false; const upvoteResponse = await fetch(`https://do.featurebase.app/v2/posts/upvoters?submissionId=${post.id}`, { diff --git a/apps/backend/src/lib/tokens.tsx b/apps/backend/src/lib/tokens.tsx index 88efb5b20..ae909d636 100644 --- a/apps/backend/src/lib/tokens.tsx +++ b/apps/backend/src/lib/tokens.tsx @@ -5,7 +5,7 @@ import { KnownErrors } from '@stackframe/stack-shared'; import { yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { generateSecureRandomString } from '@stackframe/stack-shared/dist/utils/crypto'; import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env'; -import { throwErr } from '@stackframe/stack-shared/dist/utils/errors'; +import { StackAssertionError, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { getPrivateJwks, getPublicJwkSet, signJWT, verifyJWT } from '@stackframe/stack-shared/dist/utils/jwt'; import { Result } from '@stackframe/stack-shared/dist/utils/results'; import { traceSpan } from '@stackframe/stack-shared/dist/utils/telemetry'; @@ -20,8 +20,6 @@ const accessTokenSchema = yupObject({ projectId: yupString().defined(), userId: yupString().defined(), branchId: yupString().defined(), - // we make it optional to keep backwards compatibility with old tokens for a while - // TODO next-release refreshTokenId: yupString().optional(), exp: yupNumber().defined(), isAnonymous: yupBoolean().defined(), @@ -98,11 +96,17 @@ export async function decodeAccessToken(accessToken: string, { allowAnonymous }: return Result.error(new KnownErrors.UnparsableAccessToken()); } + const branchId = payload.branch_id ?? payload.branchId; + if (branchId !== "main") { + // TODO instead, we should check here that the aud is `projectId#branch` instead + throw new StackAssertionError("Branch ID !== main not currently supported."); + } + const result = await accessTokenSchema.validate({ projectId: aud.split(":")[0], userId: payload.sub, - branchId: payload.branchId, - refreshTokenId: payload.refreshTokenId, + branchId: branchId, + refreshTokenId: payload.refresh_token_id ?? payload.refreshTokenId, exp: payload.exp, isAnonymous: payload.role === 'anon', }); @@ -137,13 +141,13 @@ export async function generateAccessToken(options: { audience: getAudience(options.tenancy.project.id, user.is_anonymous), payload: { sub: options.userId, - branchId: options.tenancy.branchId, - refreshTokenId: options.refreshTokenId, + branch_id: options.tenancy.branchId, + refresh_token_id: options.refreshTokenId, role: user.is_anonymous ? 'anon' : 'authenticated', - displayName: user.display_name, - primaryEmail: user.primary_email, - primaryEmailVerified: user.primary_email_verified, - selectedTeamId: user.selected_team_id, + name: user.display_name, + email: user.primary_email, + email_verified: user.primary_email_verified, + selected_team_id: user.selected_team_id, }, expirationTime: getEnvVariable("STACK_ACCESS_TOKEN_EXPIRATION_TIME", "10min"), }); diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index d091c4750..9d9e73f56 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -191,7 +191,7 @@ export namespace Auth { const aud = jose.decodeJwt(accessToken).aud; const jwks = jose.createRemoteJWKSet( new URL(`api/v1/projects/${aud}/.well-known/jwks.json`, STACK_BACKEND_BASE_URL), - { timeoutDuration: 10_000 }, + { timeoutDuration: 20_000 }, ); const expectedIssuer = new URL(`/api/v1/projects/${aud}`, STACK_BACKEND_BASE_URL).toString(); const { payload } = await jose.jwtVerify(accessToken, jwks); @@ -199,15 +199,15 @@ export namespace Auth { "exp": expect.any(Number), "iat": expect.any(Number), "iss": expectedIssuer, - "refreshTokenId": expect.any(String), + "branch_id": "main", + "refresh_token_id": expect.any(String), "aud": backendContext.value.projectKeys === "no-project" ? expect.any(String) : backendContext.value.projectKeys.projectId, "sub": expect.any(String), "role": "authenticated", - "branchId": "main", - "displayName": expect.toSatisfy(() => true), - "primaryEmail": expect.toSatisfy(() => true), - "primaryEmailVerified": expect.any(Boolean), - "selectedTeamId": expect.toSatisfy(() => true), + "name": expect.toSatisfy(() => true), + "email": expect.toSatisfy(() => true), + "email_verified": expect.any(Boolean), + "selected_team_id": expect.toSatisfy(() => true), }); } } diff --git a/apps/e2e/tests/snapshot-serializer.ts b/apps/e2e/tests/snapshot-serializer.ts index bb7f23b4e..ab91ae02c 100644 --- a/apps/e2e/tests/snapshot-serializer.ts +++ b/apps/e2e/tests/snapshot-serializer.ts @@ -36,6 +36,7 @@ const stripFields = [ "access_token", "refresh_token", "refreshTokenId", + "refresh_token_id", "exp", "iat", "date", diff --git a/docs/src/components/layouts/api/api-sidebar.tsx b/docs/src/components/layouts/api/api-sidebar.tsx index 576e01d25..6f0cb11c7 100644 --- a/docs/src/components/layouts/api/api-sidebar.tsx +++ b/docs/src/components/layouts/api/api-sidebar.tsx @@ -16,27 +16,27 @@ import { useSidebar } from '../sidebar-context'; const API_COLOR = 'rgb(71, 85, 105)'; // Neutral dark gray (good for light mode) const API_COLOR_LIGHT = 'rgb(148, 163, 184)'; // Lighter neutral gray -// HTTP Method color scheme - matches the HttpMethodBadge colors exactly +// HTTP Method color scheme - matches the enhanced-api-page.tsx colors exactly const METHOD_COLORS = { GET: { - main: 'rgb(22, 101, 52)', // green-800 (matches badge text color) - light: 'rgb(134, 239, 172)', // green-300 (matches dark mode badge text) - }, - POST: { - main: 'rgb(30, 64, 175)', // blue-800 (matches badge text color) + main: 'rgb(59, 130, 246)', // blue-500 (matches enhanced API page) light: 'rgb(147, 197, 253)', // blue-300 (matches dark mode badge text) }, + POST: { + main: 'rgb(34, 197, 94)', // green-500 (matches enhanced API page) + light: 'rgb(134, 239, 172)', // green-300 (matches dark mode badge text) + }, DELETE: { - main: 'rgb(153, 27, 27)', // red-800 (matches badge text color) + main: 'rgb(239, 68, 68)', // red-500 (matches enhanced API page) light: 'rgb(252, 165, 165)', // red-300 (matches dark mode badge text) }, PATCH: { - main: 'rgb(154, 52, 18)', // orange-800 (matches badge text color) - light: 'rgb(253, 186, 116)', // orange-300 (matches dark mode badge text) + main: 'rgb(234, 179, 8)', // yellow-500 (matches enhanced API page) + light: 'rgb(253, 224, 71)', // yellow-300 (matches dark mode badge text) }, PUT: { - main: 'rgb(154, 52, 18)', // orange-800 (same as PATCH) - light: 'rgb(253, 186, 116)', // orange-300 (same as PATCH) + main: 'rgb(249, 115, 22)', // orange-500 (matches enhanced API page) + light: 'rgb(253, 186, 116)', // orange-300 (matches dark mode badge text) }, } as const; @@ -118,31 +118,33 @@ function useAccordionState(key: string, defaultValue: boolean) { return [isOpen, setIsOpen] as const; } -// HTTP Method Badge Component +// HTTP Method Badge Component - matches enhanced-api-page.tsx styling function HttpMethodBadge({ method }: { method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT' }) { const getBadgeStyles = (method: string) => { switch (method) { case 'GET': { - return 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700'; + return 'from-blue-500 to-blue-600 text-white shadow-blue-500/25'; } case 'POST': { - return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'; + return 'from-green-500 to-green-600 text-white shadow-green-500/25'; } - case 'PATCH': case 'PUT': { - return 'bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-700'; + return 'from-orange-500 to-orange-600 text-white shadow-orange-500/25'; + } + case 'PATCH': { + return 'from-yellow-500 to-yellow-600 text-white shadow-yellow-500/25'; } case 'DELETE': { - return 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-700'; + return 'from-red-500 to-red-600 text-white shadow-red-500/25'; } default: { - return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/30 dark:text-gray-300 dark:border-gray-700'; + return 'from-gray-500 to-gray-600 text-white shadow-gray-500/25'; } } }; return ( - + {method} ); @@ -904,7 +906,7 @@ export function ApiSidebarContent({ pages = [] }: { pages?: PageData[] }) { {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} {!isMainSidebarCollapsed ? (
- + EVENT {title}