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: 2 additions & 0 deletions apps/backend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only

STACK_EMAILABLE_API_KEY=

STACK_INTERNAL_FEEDBACK_RECIPIENTS=team@stack-auth.com

# S3 Configuration for local development using s3mock
STACK_S3_ENDPOINT=http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}21
STACK_S3_REGION=us-east-1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase";
import { adaptSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";

const STACK_FEATUREBASE_API_KEY = getEnvVariable("STACK_FEATUREBASE_API_KEY", "");

// POST /api/latest/internal/feature-requests/[featureRequestId]/upvote
export const POST = createSmartRouteHandler({
Expand Down Expand Up @@ -36,23 +33,14 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
handler: async ({ auth, params }) => {
if (!STACK_FEATUREBASE_API_KEY) {
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
}

// Get or create Featurebase user for consistent email handling
const featurebaseUser = await getOrCreateFeaturebaseUser({
id: auth.user.id,
primaryEmail: auth.user.primary_email,
displayName: auth.user.display_name,
profileImageUrl: auth.user.profile_image_url,
});
const featurebaseApiKey = requireFeaturebaseApiKey();
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);

const response = await fetch('https://do.featurebase.app/v2/posts/upvoters', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': STACK_FEATUREBASE_API_KEY,
'X-API-Key': featurebaseApiKey,
},
body: JSON.stringify({
id: params.featureRequestId,
Expand Down
72 changes: 35 additions & 37 deletions apps/backend/src/app/api/latest/internal/feature-requests/route.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase";
import { adaptSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";

const STACK_FEATUREBASE_API_KEY = getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
// Typed subset of the Featurebase v2 API responses; fields we don't use are omitted.
// The response schema validated by yup on output acts as the runtime safety net.
type FeaturebasePost = {
id: string,
title: string,
content: string | null,
upvotes: number,
date: string,
mergedToSubmissionId: string | null,
postStatus: { name: string, color: string, type: string } | null,
};

type FeaturebaseUpvoter = {
userId: string,
};

type FeaturebaseListResponse<T> = {
results?: T[],
error?: string,
};

// GET /api/latest/internal/feature-requests
export const GET = createSmartRouteHandler({
Expand Down Expand Up @@ -43,27 +61,18 @@ export const GET = createSmartRouteHandler({
}).defined(),
}),
handler: async ({ auth }) => {
if (!STACK_FEATUREBASE_API_KEY) {
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
}

// Get or create Featurebase user for consistent email handling
const featurebaseUser = await getOrCreateFeaturebaseUser({
id: auth.user.id,
primaryEmail: auth.user.primary_email,
displayName: auth.user.display_name,
profileImageUrl: auth.user.profile_image_url,
});
const featurebaseApiKey = requireFeaturebaseApiKey();
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);

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

const data = await response.json();
const data: FeaturebaseListResponse<FeaturebasePost> = await response.json();

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

const posts = data.results || [];
const posts = data.results ?? [];

// Filter out posts that have been merged into other posts or are completed
const activePosts = posts.filter((post: any) =>
const activePosts = posts.filter((post) =>
!post.mergedToSubmissionId &&
post.postStatus?.type !== 'completed'
);

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

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

if (upvoteResponse.ok) {
const upvoteData = await upvoteResponse.json();
const upvoters = upvoteData.results || [];
userHasUpvoted = upvoters.some((upvoter: any) =>
const upvoteData: FeaturebaseListResponse<FeaturebaseUpvoter> = await upvoteResponse.json();
const upvoters = upvoteData.results ?? [];
userHasUpvoted = upvoters.some((upvoter) =>
upvoter.userId === featurebaseUser.userId
);
}
Expand Down Expand Up @@ -156,17 +163,8 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
handler: async ({ auth, body }) => {
if (!STACK_FEATUREBASE_API_KEY) {
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
}

// Get or create Featurebase user for consistent email handling
const featurebaseUser = await getOrCreateFeaturebaseUser({
id: auth.user.id,
primaryEmail: auth.user.primary_email,
displayName: auth.user.display_name,
profileImageUrl: auth.user.profile_image_url,
});
const featurebaseApiKey = requireFeaturebaseApiKey();
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);

const featurebaseRequestBody = {
title: body.title,
Expand All @@ -189,7 +187,7 @@ export const POST = createSmartRouteHandler({
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': STACK_FEATUREBASE_API_KEY,
'X-API-Key': featurebaseApiKey,
},
body: JSON.stringify(featurebaseRequestBody),
});
Expand Down
51 changes: 51 additions & 0 deletions apps/backend/src/app/api/latest/internal/feedback/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { sendSupportFeedbackEmail } from "@/lib/internal-feedback-emails";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { adaptSchema, clientOrHigherAuthTypeSchema, emailSchema, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";

export const POST = createSmartRouteHandler({
metadata: {
summary: "Submit support feedback",
description: "Send a support feedback message to the internal Stack Auth inbox",
tags: ["Internal"],
},
request: yupObject({
auth: yupObject({
type: clientOrHigherAuthTypeSchema,
tenancy: adaptSchema.defined(),
user: adaptSchema.defined(),
project: yupObject({
id: yupString().oneOf(["internal"]).defined(),
}).defined(),
}).defined(),
body: yupObject({
name: yupString().optional().max(100),
email: emailSchema.defined().nonEmpty(),
message: yupString().defined().nonEmpty().max(5000),
}).defined(),
Comment thread
mantrakp04 marked this conversation as resolved.
Comment thread
mantrakp04 marked this conversation as resolved.
method: yupString().oneOf(["POST"]).defined(),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
success: yupBoolean().oneOf([true]).defined(),
}).defined(),
}),
async handler({ auth, body }) {
await sendSupportFeedbackEmail({
tenancy: auth.tenancy,
user: auth.user,
name: body.name ?? null,
email: body.email,
message: body.message,
});

return {
statusCode: 200,
bodyType: "json",
body: {
success: true,
},
};
},
});
29 changes: 29 additions & 0 deletions apps/backend/src/lib/featurebase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
import { getOrCreateFeaturebaseUser as getOrCreateFeaturebaseUserShared, StackAuthUser } from "@stackframe/stack-shared/dist/utils/featurebase";

export function getFeaturebaseApiKey(): string {
return getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
}

export function requireFeaturebaseApiKey(): string {
const key = getFeaturebaseApiKey();
if (!key) {
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
}
return key;
}

export function toFeaturebaseUserArgs(user: UsersCrud["Admin"]["Read"]): StackAuthUser {
return {
id: user.id,
primaryEmail: user.primary_email,
displayName: user.display_name,
profileImageUrl: user.profile_image_url,
};
}

export async function getOrCreateFeaturebaseUserFromAuth(user: UsersCrud["Admin"]["Read"]) {
return await getOrCreateFeaturebaseUserShared(toFeaturebaseUserArgs(user));
}
Loading
Loading