Skip to content

Commit aaf49db

Browse files
authored
Merge branch 'dev' into dario-likes-mcps
2 parents 5078747 + cfa6204 commit aaf49db

11 files changed

Lines changed: 467 additions & 120 deletions

File tree

apps/backend/.env.development

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only
9292

9393
STACK_EMAILABLE_API_KEY=
9494

95+
STACK_INTERNAL_FEEDBACK_RECIPIENTS=team@stack-auth.com
96+
9597
# S3 Configuration for local development using s3mock
9698
STACK_S3_ENDPOINT=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21
9799
STACK_S3_REGION=us-east-1

apps/backend/src/app/api/latest/internal/feature-requests/[featureRequestId]/upvote/route.tsx

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
2+
import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase";
23
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
3-
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
44
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
5-
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";
6-
7-
const STACK_FEATUREBASE_API_KEY = getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
85

96
// POST /api/latest/internal/feature-requests/[featureRequestId]/upvote
107
export const POST = createSmartRouteHandler({
@@ -36,23 +33,14 @@ export const POST = createSmartRouteHandler({
3633
}).defined(),
3734
}),
3835
handler: async ({ auth, params }) => {
39-
if (!STACK_FEATUREBASE_API_KEY) {
40-
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
41-
}
42-
43-
// Get or create Featurebase user for consistent email handling
44-
const featurebaseUser = await getOrCreateFeaturebaseUser({
45-
id: auth.user.id,
46-
primaryEmail: auth.user.primary_email,
47-
displayName: auth.user.display_name,
48-
profileImageUrl: auth.user.profile_image_url,
49-
});
36+
const featurebaseApiKey = requireFeaturebaseApiKey();
37+
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);
5038

5139
const response = await fetch('https://do.featurebase.app/v2/posts/upvoters', {
5240
method: 'POST',
5341
headers: {
5442
'Content-Type': 'application/json',
55-
'X-API-Key': STACK_FEATUREBASE_API_KEY,
43+
'X-API-Key': featurebaseApiKey,
5644
},
5745
body: JSON.stringify({
5846
id: params.featureRequestId,

apps/backend/src/app/api/latest/internal/feature-requests/route.tsx

Lines changed: 35 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
2+
import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase";
23
import { adaptSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
3-
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
44
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
5-
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";
65

7-
const STACK_FEATUREBASE_API_KEY = getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
6+
// Typed subset of the Featurebase v2 API responses; fields we don't use are omitted.
7+
// The response schema validated by yup on output acts as the runtime safety net.
8+
type FeaturebasePost = {
9+
id: string,
10+
title: string,
11+
content: string | null,
12+
upvotes: number,
13+
date: string,
14+
mergedToSubmissionId: string | null,
15+
postStatus: { name: string, color: string, type: string } | null,
16+
};
17+
18+
type FeaturebaseUpvoter = {
19+
userId: string,
20+
};
21+
22+
type FeaturebaseListResponse<T> = {
23+
results?: T[],
24+
error?: string,
25+
};
826

927
// GET /api/latest/internal/feature-requests
1028
export const GET = createSmartRouteHandler({
@@ -43,27 +61,18 @@ export const GET = createSmartRouteHandler({
4361
}).defined(),
4462
}),
4563
handler: async ({ auth }) => {
46-
if (!STACK_FEATUREBASE_API_KEY) {
47-
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
48-
}
49-
50-
// Get or create Featurebase user for consistent email handling
51-
const featurebaseUser = await getOrCreateFeaturebaseUser({
52-
id: auth.user.id,
53-
primaryEmail: auth.user.primary_email,
54-
displayName: auth.user.display_name,
55-
profileImageUrl: auth.user.profile_image_url,
56-
});
64+
const featurebaseApiKey = requireFeaturebaseApiKey();
65+
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);
5766

5867
// Fetch all posts with sorting
5968
const response = await fetch('https://do.featurebase.app/v2/posts?limit=50&sortBy=upvotes:desc', {
6069
method: 'GET',
6170
headers: {
62-
'X-API-Key': STACK_FEATUREBASE_API_KEY,
71+
'X-API-Key': featurebaseApiKey,
6372
},
6473
});
6574

66-
const data = await response.json();
75+
const data: FeaturebaseListResponse<FeaturebasePost> = await response.json();
6776

6877
if (!response.ok) {
6978
throw new StackAssertionError(`Featurebase API error: ${data.error || 'Failed to fetch feature requests'}`, {
@@ -74,30 +83,28 @@ export const GET = createSmartRouteHandler({
7483
});
7584
}
7685

77-
const posts = data.results || [];
86+
const posts = data.results ?? [];
7887

79-
// Filter out posts that have been merged into other posts or are completed
80-
const activePosts = posts.filter((post: any) =>
88+
const activePosts = posts.filter((post) =>
8189
!post.mergedToSubmissionId &&
8290
post.postStatus?.type !== 'completed'
8391
);
8492

85-
// Check upvote status for each post for the current user using Featurebase email
8693
const postsWithUpvoteStatus = await Promise.all(
87-
activePosts.map(async (post: any) => {
94+
activePosts.map(async (post) => {
8895
let userHasUpvoted = false;
8996

9097
const upvoteResponse = await fetch(`https://do.featurebase.app/v2/posts/upvoters?submissionId=${post.id}`, {
9198
method: 'GET',
9299
headers: {
93-
'X-API-Key': STACK_FEATUREBASE_API_KEY,
100+
'X-API-Key': featurebaseApiKey,
94101
},
95102
});
96103

97104
if (upvoteResponse.ok) {
98-
const upvoteData = await upvoteResponse.json();
99-
const upvoters = upvoteData.results || [];
100-
userHasUpvoted = upvoters.some((upvoter: any) =>
105+
const upvoteData: FeaturebaseListResponse<FeaturebaseUpvoter> = await upvoteResponse.json();
106+
const upvoters = upvoteData.results ?? [];
107+
userHasUpvoted = upvoters.some((upvoter) =>
101108
upvoter.userId === featurebaseUser.userId
102109
);
103110
}
@@ -156,17 +163,8 @@ export const POST = createSmartRouteHandler({
156163
}).defined(),
157164
}),
158165
handler: async ({ auth, body }) => {
159-
if (!STACK_FEATUREBASE_API_KEY) {
160-
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
161-
}
162-
163-
// Get or create Featurebase user for consistent email handling
164-
const featurebaseUser = await getOrCreateFeaturebaseUser({
165-
id: auth.user.id,
166-
primaryEmail: auth.user.primary_email,
167-
displayName: auth.user.display_name,
168-
profileImageUrl: auth.user.profile_image_url,
169-
});
166+
const featurebaseApiKey = requireFeaturebaseApiKey();
167+
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);
170168

171169
const featurebaseRequestBody = {
172170
title: body.title,
@@ -189,7 +187,7 @@ export const POST = createSmartRouteHandler({
189187
method: 'POST',
190188
headers: {
191189
'Content-Type': 'application/json',
192-
'X-API-Key': STACK_FEATUREBASE_API_KEY,
190+
'X-API-Key': featurebaseApiKey,
193191
},
194192
body: JSON.stringify(featurebaseRequestBody),
195193
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { sendSupportFeedbackEmail } from "@/lib/internal-feedback-emails";
2+
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
3+
import { adaptSchema, clientOrHigherAuthTypeSchema, emailSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4+
5+
export const POST = createSmartRouteHandler({
6+
metadata: {
7+
summary: "Submit support feedback",
8+
description: "Send a support feedback message to the internal Stack Auth inbox",
9+
tags: ["Internal"],
10+
},
11+
request: yupObject({
12+
auth: yupObject({
13+
type: clientOrHigherAuthTypeSchema,
14+
tenancy: adaptSchema.defined(),
15+
user: adaptSchema.defined(),
16+
project: yupObject({
17+
id: yupString().oneOf(["internal"]).defined(),
18+
}).defined(),
19+
}).defined(),
20+
body: yupObject({
21+
name: yupString().optional().max(100),
22+
email: emailSchema.defined().nonEmpty(),
23+
message: yupString().defined().nonEmpty().max(5000),
24+
}).defined(),
25+
method: yupString().oneOf(["POST"]).defined(),
26+
}),
27+
response: yupObject({
28+
statusCode: yupNumber().oneOf([200]).defined(),
29+
bodyType: yupString().oneOf(["json"]).defined(),
30+
body: yupObject({
31+
success: yupBoolean().oneOf([true]).defined(),
32+
}).defined(),
33+
}),
34+
async handler({ auth, body }) {
35+
await sendSupportFeedbackEmail({
36+
tenancy: auth.tenancy,
37+
user: auth.user,
38+
name: body.name ?? null,
39+
email: body.email,
40+
message: body.message,
41+
});
42+
43+
return {
44+
statusCode: 200,
45+
bodyType: "json",
46+
body: {
47+
success: true,
48+
},
49+
};
50+
},
51+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
2+
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
3+
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
4+
import { getOrCreateFeaturebaseUser as getOrCreateFeaturebaseUserShared, StackAuthUser } from "@stackframe/stack-shared/dist/utils/featurebase";
5+
6+
export function getFeaturebaseApiKey(): string {
7+
return getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
8+
}
9+
10+
export function requireFeaturebaseApiKey(): string {
11+
const key = getFeaturebaseApiKey();
12+
if (!key) {
13+
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
14+
}
15+
return key;
16+
}
17+
18+
export function toFeaturebaseUserArgs(user: UsersCrud["Admin"]["Read"]): StackAuthUser {
19+
return {
20+
id: user.id,
21+
primaryEmail: user.primary_email,
22+
displayName: user.display_name,
23+
profileImageUrl: user.profile_image_url,
24+
};
25+
}
26+
27+
export async function getOrCreateFeaturebaseUserFromAuth(user: UsersCrud["Admin"]["Read"]) {
28+
return await getOrCreateFeaturebaseUserShared(toFeaturebaseUserArgs(user));
29+
}

0 commit comments

Comments
 (0)