mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +08:00
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md
-->
<!-- ELLIPSIS_HIDDEN -->
----
> [!IMPORTANT]
> Introduces Stack Companion with a right-side panel for docs, feature
requests, changelog, and support, along with a new feedback form and
improved feature request handling.
>
> - **New Features**:
> - Adds `StackCompanion` component for right-side panel with Docs,
Feature Requests, Changelog, and Support in `sidebar-layout.tsx` and
`stack-companion.tsx`.
> - Introduces `FeedbackForm` component in `feedback-form.tsx` with
success/error states and contact links.
> - **Feature Requests**:
> - Implements `GET`, `POST`, and `upvote` routes in `route.tsx` and
`[featureRequestId]/upvote/route.tsx` for feature requests with SSO and
upvote syncing.
> - Adds `FeatureRequestBoard` component in `feature-request-board.tsx`
for managing feature requests.
> - **Changelog**:
> - Adds `ChangelogWidget` component in `changelog-widget.tsx` to
display recent updates.
> - **Version Checking**:
> - Refactors version checking logic into `version-check.ts` and updates
`VersionAlerter` in `version-alerter.tsx`.
> - **Miscellaneous**:
> - Allows remote images from `featurebase-attachments.com` in
`next.config.mjs`.
> - Removes old `FeedbackDialog` and `docs/middleware.ts`.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 8baf5e1a0f. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>
----
<!-- ELLIPSIS_HIDDEN -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- New Features
- Right-side Stack Companion panel: Docs, Feature Requests (browse,
submit, upvote), Changelog, and Support.
- In-app Feedback form with success/error states and contact links.
- Improvements
- Feature Requests: SSO integration and upvote syncing with backend.
- Changelog viewer: loads and formats recent entries.
- Remote images allowed from featurebase-attachments.com.
- Consolidated version-checking for streamlined alerts.
- Removals
- Old Feedback dialog and docs middleware removed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: GitButler <gitbutler@gitbutler.com>
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
248 lines
7.7 KiB
TypeScript
248 lines
7.7 KiB
TypeScript
import { getEnvVariable } from "./env";
|
|
import { StackAssertionError } from "./errors";
|
|
|
|
export type FeaturebaseUser = {
|
|
userId: string,
|
|
email: string,
|
|
name?: string,
|
|
profilePicture?: string,
|
|
};
|
|
|
|
export type StackAuthUser = {
|
|
id: string,
|
|
primaryEmail: string | null,
|
|
displayName?: string | null,
|
|
profileImageUrl?: string | null,
|
|
};
|
|
|
|
/**
|
|
* Find a Featurebase user by their Stack Auth user ID
|
|
*/
|
|
async function findFeaturebaseUserById(stackAuthUserId: string, apiKey: string): Promise<FeaturebaseUser | null> {
|
|
try {
|
|
const response = await fetch(`https://do.featurebase.app/v2/organization/identifyUser?id=${stackAuthUserId}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-API-Key': apiKey,
|
|
},
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new StackAssertionError(`Failed to find Featurebase user by ID: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const user = data.user;
|
|
|
|
if (!user) {
|
|
throw new StackAssertionError(`Featurebase API returned success but no user data for ID: ${stackAuthUserId}`, { data });
|
|
}
|
|
|
|
return {
|
|
userId: user.externalUserId || user.userId || stackAuthUserId,
|
|
email: user.email,
|
|
name: user.name,
|
|
profilePicture: user.profilePicture,
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof StackAssertionError) {
|
|
throw error;
|
|
}
|
|
throw new StackAssertionError("Failed to find Featurebase user by ID", { cause: error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find a Featurebase user by their email address
|
|
*/
|
|
async function findFeaturebaseUserByEmail(email: string, apiKey: string): Promise<FeaturebaseUser | null> {
|
|
try {
|
|
const response = await fetch(`https://do.featurebase.app/v2/organization/identifyUser?email=${encodeURIComponent(email)}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-API-Key': apiKey,
|
|
},
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new StackAssertionError(`Failed to find Featurebase user by email: ${response.statusText}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const user = data.user;
|
|
|
|
if (!user) {
|
|
throw new StackAssertionError(`Featurebase API returned success but no user data for email: ${email}`, { data });
|
|
}
|
|
|
|
return {
|
|
userId: user.externalUserId || user.userId,
|
|
email: user.email,
|
|
name: user.name,
|
|
profilePicture: user.profilePicture,
|
|
};
|
|
} catch (error) {
|
|
console.error('Error finding Featurebase user by email:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a new Featurebase user using the identifyUser endpoint
|
|
*/
|
|
async function createFeaturebaseUser(user: FeaturebaseUser, apiKey: string): Promise<FeaturebaseUser> {
|
|
try {
|
|
const response = await fetch('https://do.featurebase.app/v2/organization/identifyUser', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': apiKey,
|
|
},
|
|
body: JSON.stringify({
|
|
userId: user.userId,
|
|
email: user.email,
|
|
name: user.name,
|
|
profilePicture: user.profilePicture,
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new StackAssertionError(`Failed to create Featurebase user: ${errorData.error || response.statusText}`, { errorData });
|
|
}
|
|
|
|
// The identifyUser endpoint just returns { "success": true }, so we return the input data
|
|
return user;
|
|
} catch (error) {
|
|
if (error instanceof StackAssertionError) {
|
|
throw error;
|
|
}
|
|
throw new StackAssertionError("Failed to create Featurebase user", { cause: error });
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update an existing Featurebase user (excluding email)
|
|
*/
|
|
async function updateFeaturebaseUser(userId: string, updates: Partial<Omit<FeaturebaseUser, 'userId' | 'email'>>, apiKey: string): Promise<FeaturebaseUser> {
|
|
try {
|
|
const response = await fetch(`https://do.featurebase.app/v2/users/${userId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-API-Key': apiKey,
|
|
},
|
|
body: JSON.stringify(updates),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new StackAssertionError(`Failed to update Featurebase user: ${errorData.error || response.statusText}`, { errorData });
|
|
}
|
|
|
|
const data = await response.json();
|
|
return {
|
|
userId: data.userId || userId,
|
|
email: data.email,
|
|
name: data.name,
|
|
profilePicture: data.profilePicture,
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof StackAssertionError) {
|
|
throw error;
|
|
}
|
|
throw new StackAssertionError("Failed to update Featurebase user", { cause: error });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or create a Featurebase user based on Stack Auth user data.
|
|
* This function ensures that:
|
|
* 1. We never change a user's email address on Featurebase
|
|
* 2. We use Stack Auth user ID as the primary identifier
|
|
* 3. We handle email conflicts by using fallback emails
|
|
* 4. We update profile information when needed
|
|
*/
|
|
export async function getOrCreateFeaturebaseUser(
|
|
stackAuthUser: StackAuthUser,
|
|
options?: { apiKey?: string }
|
|
): Promise<{ userId: string, email: string }> {
|
|
const apiKey = options?.apiKey || getEnvVariable("STACK_FEATUREBASE_API_KEY");
|
|
const fallbackEmail = `${stackAuthUser.id}@featurebase-user.stack-auth-app.com`;
|
|
|
|
// First, try to find existing user by Stack Auth user ID
|
|
const existingById = await findFeaturebaseUserById(stackAuthUser.id, apiKey);
|
|
if (existingById) {
|
|
// Ensure the user has an email on Featurebase.
|
|
let ensuredEmail = existingById.email;
|
|
if (!ensuredEmail) {
|
|
try {
|
|
await createFeaturebaseUser({
|
|
userId: existingById.userId,
|
|
email: fallbackEmail,
|
|
name: stackAuthUser.displayName || undefined,
|
|
profilePicture: stackAuthUser.profileImageUrl || undefined,
|
|
}, apiKey);
|
|
ensuredEmail = fallbackEmail;
|
|
} catch (e) {
|
|
// If setting fallback email failed, keep ensuredEmail as-is (undefined) and let callers handle
|
|
throw new StackAssertionError(`Failed to set fallback email for existing Featurebase user ${existingById.userId}`, { cause: e });
|
|
}
|
|
}
|
|
|
|
// Update profile information if needed (but not email)
|
|
try {
|
|
const updates: Partial<Omit<FeaturebaseUser, 'userId' | 'email'>> = {};
|
|
|
|
if (stackAuthUser.displayName && stackAuthUser.displayName !== existingById.name) {
|
|
updates.name = stackAuthUser.displayName;
|
|
}
|
|
|
|
if (stackAuthUser.profileImageUrl && stackAuthUser.profileImageUrl !== existingById.profilePicture) {
|
|
updates.profilePicture = stackAuthUser.profileImageUrl;
|
|
}
|
|
|
|
if (Object.keys(updates).length > 0) {
|
|
await updateFeaturebaseUser(existingById.userId, updates, apiKey);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to update existing Featurebase user profile:', error);
|
|
// Continue with existing user data even if update fails
|
|
}
|
|
|
|
return {
|
|
userId: existingById.userId,
|
|
email: ensuredEmail,
|
|
};
|
|
}
|
|
|
|
// No existing user found by ID, need to create one
|
|
const candidateEmail = stackAuthUser.primaryEmail ?? fallbackEmail;
|
|
|
|
// Check if someone already has this email on Featurebase
|
|
const existingByEmail = await findFeaturebaseUserByEmail(candidateEmail, apiKey);
|
|
const safeEmail = existingByEmail ? fallbackEmail : candidateEmail;
|
|
|
|
// Create new user
|
|
const created = await createFeaturebaseUser({
|
|
userId: stackAuthUser.id,
|
|
email: safeEmail,
|
|
name: stackAuthUser.displayName || stackAuthUser.primaryEmail?.split('@')[0] || 'User',
|
|
profilePicture: stackAuthUser.profileImageUrl || undefined,
|
|
}, apiKey);
|
|
|
|
return {
|
|
userId: created.userId,
|
|
email: created.email,
|
|
};
|
|
}
|