Merge dev into update-oauth-docs

This commit is contained in:
Konsti Wohlwend 2025-09-15 04:31:38 -07:00 committed by GitHub
commit 59e40551cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 52 additions and 39 deletions

View File

@ -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}`, {

View File

@ -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"),
});

View File

@ -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),
});
}
}

View File

@ -36,6 +36,7 @@ const stripFields = [
"access_token",
"refresh_token",
"refreshTokenId",
"refresh_token_id",
"exp",
"iat",
"date",

View File

@ -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 (
<span className={`inline-flex items-center justify-center px-1 py-0.5 rounded text-[10px] font-medium border ${getBadgeStyles(method)} leading-none w-10 flex-shrink-0`}>
<span className={`inline-flex items-center justify-center px-1.5 py-0.5 rounded bg-gradient-to-r ${getBadgeStyles(method)} font-mono font-bold text-[9px] tracking-wide leading-none w-10 flex-shrink-0`}>
{method}
</span>
);
@ -904,7 +906,7 @@ export function ApiSidebarContent({ pages = [] }: { pages?: PageData[] }) {
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
{!isMainSidebarCollapsed ? (
<div className="flex items-center gap-2">
<span className="inline-flex items-center px-1 py-0.5 rounded text-xs font-medium border bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700 leading-none">
<span className="inline-flex items-center justify-center px-1.5 py-0.5 rounded bg-gradient-to-r from-purple-500 to-purple-600 text-white shadow-purple-500/25 font-mono font-bold text-[9px] tracking-wide leading-none w-10 flex-shrink-0">
EVENT
</span>
<span>{title}</span>