Skip to content

Commit afdb77b

Browse files
committed
Refactor internal feedback email handling and improve featurebase user management
- Introduced a new utility module for featurebase interactions, consolidating API key management and user creation logic. - Updated existing routes to utilize the new featurebase functions, enhancing code clarity and reducing redundancy. - Enhanced internal feedback email templates for better structure and readability. - Added documentation to clarify the internal feedback email flow and E2E testing strategies.
1 parent 9500d5c commit afdb77b

5 files changed

Lines changed: 94 additions & 101 deletions

File tree

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

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +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-
function getFeaturebaseApiKey() {
8-
return getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
9-
}
105

116
// POST /api/latest/internal/feature-requests/[featureRequestId]/upvote
127
export const POST = createSmartRouteHandler({
@@ -38,18 +33,8 @@ export const POST = createSmartRouteHandler({
3833
}).defined(),
3934
}),
4035
handler: async ({ auth, params }) => {
41-
const featurebaseApiKey = getFeaturebaseApiKey();
42-
if (!featurebaseApiKey) {
43-
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
44-
}
45-
46-
// Get or create Featurebase user for consistent email handling
47-
const featurebaseUser = await getOrCreateFeaturebaseUser({
48-
id: auth.user.id,
49-
primaryEmail: auth.user.primary_email,
50-
displayName: auth.user.display_name,
51-
profileImageUrl: auth.user.profile_image_url,
52-
});
36+
const featurebaseApiKey = requireFeaturebaseApiKey();
37+
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);
5338

5439
const response = await fetch('https://do.featurebase.app/v2/posts/upvoters', {
5540
method: 'POST',

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

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
2+
import { getOrCreateFeaturebaseUserFromAuth, requireFeaturebaseApiKey } from "@/lib/featurebase";
23
import { sendFeatureRequestNotificationEmail } from "@/lib/internal-feedback-emails";
34
import { adaptSchema, yupArray, yupBoolean, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4-
import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
55
import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
6-
import { getOrCreateFeaturebaseUser } from "@stackframe/stack-shared/dist/utils/featurebase";
7-
8-
function getFeaturebaseApiKey() {
9-
return getEnvVariable("STACK_FEATUREBASE_API_KEY", "");
10-
}
116

127
// GET /api/latest/internal/feature-requests
138
export const GET = createSmartRouteHandler({
@@ -47,18 +42,8 @@ export const GET = createSmartRouteHandler({
4742
}).defined(),
4843
}),
4944
handler: async ({ auth }) => {
50-
const featurebaseApiKey = getFeaturebaseApiKey();
51-
if (!featurebaseApiKey) {
52-
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
53-
}
54-
55-
// Get or create Featurebase user for consistent email handling
56-
const featurebaseUser = await getOrCreateFeaturebaseUser({
57-
id: auth.user.id,
58-
primaryEmail: auth.user.primary_email,
59-
displayName: auth.user.display_name,
60-
profileImageUrl: auth.user.profile_image_url,
61-
});
45+
const featurebaseApiKey = requireFeaturebaseApiKey();
46+
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);
6247

6348
// Fetch all posts with sorting
6449
const response = await fetch('https://do.featurebase.app/v2/posts?limit=50&sortBy=upvotes:desc', {
@@ -162,18 +147,8 @@ export const POST = createSmartRouteHandler({
162147
}).defined(),
163148
}),
164149
handler: async ({ auth, body }) => {
165-
const featurebaseApiKey = getFeaturebaseApiKey();
166-
if (!featurebaseApiKey) {
167-
throw new StackAssertionError("STACK_FEATUREBASE_API_KEY environment variable is not set");
168-
}
169-
170-
// Get or create Featurebase user for consistent email handling
171-
const featurebaseUser = await getOrCreateFeaturebaseUser({
172-
id: auth.user.id,
173-
primaryEmail: auth.user.primary_email,
174-
displayName: auth.user.display_name,
175-
profileImageUrl: auth.user.profile_image_url,
176-
});
150+
const featurebaseApiKey = requireFeaturebaseApiKey();
151+
const featurebaseUser = await getOrCreateFeaturebaseUserFromAuth(auth.user);
177152

178153
const featurebaseRequestBody = {
179154
title: body.title,
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+
}

apps/backend/src/lib/internal-feedback-emails.tsx

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@ function sanitizeSubject(value: string): string {
1919
return value.replace(/\s+/g, " ").trim();
2020
}
2121

22+
function buildInternalEmailHtml(options: {
23+
heading: string,
24+
fields: Array<{ label: string, value: string } | { label: string, href: string, linkText: string }>,
25+
contentLabel: string,
26+
contentBody: string,
27+
}): string {
28+
const fieldRows = options.fields.map((field) => {
29+
if ("href" in field) {
30+
return `<p><strong>${escapeHtml(field.label)}:</strong> <a href="${escapeHtml(field.href)}">${escapeHtml(field.linkText)}</a></p>`;
31+
}
32+
return `<p><strong>${escapeHtml(field.label)}:</strong> ${formatTextForHtml(field.value)}</p>`;
33+
}).join("\n ");
34+
35+
return `
36+
<div style="font-family: Arial, sans-serif; max-width: 720px; margin: 0 auto; padding: 24px; color: #1f2937;">
37+
<h2 style="margin: 0 0 20px;">${escapeHtml(options.heading)}</h2>
38+
${fieldRows}
39+
<div style="margin-top: 24px;">
40+
<p style="margin-bottom: 8px;"><strong>${escapeHtml(options.contentLabel)}</strong></p>
41+
<div style="padding: 16px; border: 1px solid #d1d5db; border-radius: 8px; background: #f9fafb; white-space: normal;">
42+
${formatTextForHtml(options.contentBody)}
43+
</div>
44+
</div>
45+
</div>
46+
`;
47+
}
48+
2249
export function getInternalFeedbackRecipients(): string[] {
2350
const rawRecipients = getEnvVariable("STACK_INTERNAL_FEEDBACK_RECIPIENTS", defaultRecipient);
2451
const recipients = rawRecipients.split(",").map((recipient) => recipient.trim());
@@ -65,26 +92,21 @@ export async function sendSupportFeedbackEmail(options: {
6592
message: string,
6693
}) {
6794
const displayName = options.name ?? options.user.display_name ?? "Not provided";
68-
const htmlContent = `
69-
<div style="font-family: Arial, sans-serif; max-width: 720px; margin: 0 auto; padding: 24px; color: #1f2937;">
70-
<h2 style="margin: 0 0 20px;">Support feedback submission</h2>
71-
<p><strong>Sender name:</strong> ${formatTextForHtml(displayName)}</p>
72-
<p><strong>Sender email:</strong> ${formatTextForHtml(options.email)}</p>
73-
<p><strong>Stack Auth user ID:</strong> ${formatTextForHtml(options.user.id)}</p>
74-
<p><strong>Stack Auth display name:</strong> ${formatTextForHtml(options.user.display_name ?? "Not provided")}</p>
75-
<div style="margin-top: 24px;">
76-
<p style="margin-bottom: 8px;"><strong>Message</strong></p>
77-
<div style="padding: 16px; border: 1px solid #d1d5db; border-radius: 8px; background: #f9fafb; white-space: normal;">
78-
${formatTextForHtml(options.message)}
79-
</div>
80-
</div>
81-
</div>
82-
`;
8395

8496
await sendInternalOperationsEmail({
8597
tenancy: options.tenancy,
8698
subject: `[Support] ${options.email}`,
87-
htmlContent,
99+
htmlContent: buildInternalEmailHtml({
100+
heading: "Support feedback submission",
101+
fields: [
102+
{ label: "Sender name", value: displayName },
103+
{ label: "Sender email", value: options.email },
104+
{ label: "Stack Auth user ID", value: options.user.id },
105+
{ label: "Stack Auth display name", value: options.user.display_name ?? "Not provided" },
106+
],
107+
contentLabel: "Message",
108+
contentBody: options.message,
109+
}),
88110
});
89111
}
90112

@@ -96,28 +118,23 @@ export async function sendFeatureRequestNotificationEmail(options: {
96118
featureRequestId: string,
97119
}) {
98120
const featureRequestUrl = new URL(urlString`/p/${options.featureRequestId}`, "https://feedback.stack-auth.com").toString();
99-
const htmlContent = `
100-
<div style="font-family: Arial, sans-serif; max-width: 720px; margin: 0 auto; padding: 24px; color: #1f2937;">
101-
<h2 style="margin: 0 0 20px;">New feature request submitted</h2>
102-
<p><strong>Title:</strong> ${formatTextForHtml(options.title)}</p>
103-
<p><strong>Featurebase post ID:</strong> ${formatTextForHtml(options.featureRequestId)}</p>
104-
<p><strong>Featurebase URL:</strong> <a href="${escapeHtml(featureRequestUrl)}">${escapeHtml(featureRequestUrl)}</a></p>
105-
<p><strong>Submitted by:</strong> ${formatTextForHtml(options.user.display_name ?? "Not provided")}</p>
106-
<p><strong>Submitted email:</strong> ${formatTextForHtml(options.user.primary_email ?? "Not provided")}</p>
107-
<p><strong>Stack Auth user ID:</strong> ${formatTextForHtml(options.user.id)}</p>
108-
<div style="margin-top: 24px;">
109-
<p style="margin-bottom: 8px;"><strong>Details</strong></p>
110-
<div style="padding: 16px; border: 1px solid #d1d5db; border-radius: 8px; background: #f9fafb; white-space: normal;">
111-
${formatTextForHtml(options.content ?? "Not provided")}
112-
</div>
113-
</div>
114-
</div>
115-
`;
116121

117122
await sendInternalOperationsEmail({
118123
tenancy: options.tenancy,
119124
subject: `[Feature Request] ${options.title}`,
120-
htmlContent,
125+
htmlContent: buildInternalEmailHtml({
126+
heading: "New feature request submitted",
127+
fields: [
128+
{ label: "Title", value: options.title },
129+
{ label: "Featurebase post ID", value: options.featureRequestId },
130+
{ label: "Featurebase URL", href: featureRequestUrl, linkText: featureRequestUrl },
131+
{ label: "Submitted by", value: options.user.display_name ?? "Not provided" },
132+
{ label: "Submitted email", value: options.user.primary_email ?? "Not provided" },
133+
{ label: "Stack Auth user ID", value: options.user.id },
134+
],
135+
contentLabel: "Details",
136+
contentBody: options.content ?? "Not provided",
137+
}),
121138
});
122139
}
123140

pnpm-lock.yaml

Lines changed: 6 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)