Skip to content
Open
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
3 changes: 3 additions & 0 deletions apps/backend/src/lib/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,9 @@ export const renderedOrganizationConfigToProjectCrud = (renderedConfig: Complete

email_config: renderedConfig.emails.server.isShared ? {
type: 'shared',
host: getEnvVariable("STACK_EMAIL_HOST"),
port: parseInt(getEnvVariable("STACK_EMAIL_PORT")),
sender_email: getEnvVariable("STACK_EMAIL_SENDER"),
} : renderedConfig.emails.server.provider === "managed" ? {
type: 'standard',
host: "smtp.resend.com",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type ServerFieldConfig = {
};

const SERVER_TYPE_LABELS: Record<ServerType, string> = {
shared: "Shared (noreply@stackframe.co)",
shared: "Shared (environment-configured)",
managed: "Managed (via managed domain setup)",
resend: "Resend",
standard: "Custom SMTP",
Expand Down Expand Up @@ -87,9 +87,16 @@ function getServerTypeFromConfig(config: CompleteConfig["emails"]["server"]): Se
return "standard";
}

function getFormValuesFromConfig(config: CompleteConfig["emails"]["server"], projectName: string): Record<string, string> {
function getFormValuesFromConfig(
config: CompleteConfig["emails"]["server"],
projectName: string,
sharedSenderEmail: string | null,
): Record<string, string> {
if (config.isShared) {
return { senderEmail: "noreply@stackframe.co", senderName: projectName };
return {
senderEmail: sharedSenderEmail ?? "Configured via STACK_EMAIL_SENDER",
senderName: projectName,
};
}
if (config.provider === "managed") {
const senderEmail = config.managedSubdomain && config.managedSenderLocalPart
Expand Down Expand Up @@ -357,12 +364,21 @@ export function DomainSettings() {
const stackAdminApp = useAdminApp();
const project = stackAdminApp.useProject();
const emailConfig = project.useConfig().emails.server;
const sharedSenderEmail = project.config.emailConfig?.type === "shared"
&& "senderEmail" in project.config.emailConfig
&& typeof project.config.emailConfig.senderEmail === "string"
? project.config.emailConfig.senderEmail
: null;
const updateConfig = useUpdateConfig();
const { toast } = useToast();
const isEmulator = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true";

const savedServerType = getServerTypeFromConfig(emailConfig);
const savedValues = getFormValuesFromConfig(emailConfig, project.displayName);
const savedValues = getFormValuesFromConfig(
emailConfig,
project.displayName,
sharedSenderEmail,
);

const [serverType, setServerType] = useState<ServerType>(savedServerType);
const [formValues, setFormValues] = useState<Record<string, string>>(savedValues);
Expand Down Expand Up @@ -541,8 +557,10 @@ export function DomainSettings() {
<div className="space-y-1.5">
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Sender Email</Label>
{isShared ? (
<SimpleTooltip tooltip="Sender email is fixed on the shared server">
<Typography className="text-sm font-medium text-foreground/60 cursor-default py-1">noreply@stackframe.co</Typography>
<SimpleTooltip tooltip="Sender email is read from STACK_EMAIL_SENDER on the server.">
<Typography className="text-sm font-medium text-foreground/60 cursor-default py-1">
{sharedSenderEmail ?? "Configured via STACK_EMAIL_SENDER"}
</Typography>
</SimpleTooltip>
) : serverType === "managed" ? (
<SimpleTooltip tooltip="Sender email is configured through the managed domain setup">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { TeamMemberSearchTable } from "@/components/data-table/team-member-search-table";
import { DesignAnalyticsCard } from "@/components/design-components";
import { FormDialog } from "@/components/form-dialog";
import { InputField, SelectField, TextAreaField } from "@/components/form-fields";
import { ActionDialog, Alert, AlertDescription, AlertTitle, Button, DataTable, DataTableColumnHeader, DataTableViewOptions, SimpleTooltip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Typography, useToast } from "@/components/ui";
Expand All @@ -19,7 +20,6 @@ import * as yup from "yup";
import { AppEnabledGuard } from "../app-enabled-guard";
import { PageLayout } from "../page-layout";
import { useAdminApp } from "../use-admin-app";
import { DesignAnalyticsCard } from "@/components/design-components";

// Section header with icon following design guide
function SectionHeader({ icon: Icon, title }: { icon: ElementType, title: string }) {
Expand Down Expand Up @@ -59,6 +59,11 @@ export default function PageClient() {
const stackAdminApp = useAdminApp();
const project = stackAdminApp.useProject();
const emailConfig = project.useConfig().emails.server;
const sharedSenderEmail = project.config.emailConfig?.type === "shared"
&& "senderEmail" in project.config.emailConfig
&& typeof project.config.emailConfig.senderEmail === "string"
? project.config.emailConfig.senderEmail
: null;
const isLocalEmulator = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true";

return (
Expand All @@ -82,7 +87,7 @@ export default function PageClient() {
{isLocalEmulator && <EmulatorModeCard />}

{/* Email Server Card */}
<EmailServerCard emailConfig={emailConfig} />
<EmailServerCard emailConfig={emailConfig} sharedSenderEmail={sharedSenderEmail} />

{/* Email Log Card */}
<EmailLogCard />
Expand Down Expand Up @@ -130,16 +135,22 @@ function EmulatorModeCard() {
);
}

function EmailServerCard({ emailConfig }: { emailConfig: CompleteConfig['emails']['server'] }) {
function EmailServerCard({
emailConfig,
sharedSenderEmail,
}: {
emailConfig: CompleteConfig['emails']['server'],
sharedSenderEmail: string | null,
}) {
const isLocalEmulator = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true";
const serverType = emailConfig.isShared
? 'Shared'
? 'Shared (environment-configured)'
: emailConfig.provider === 'managed'
? 'Managed By Stack Auth'
: (emailConfig.provider === 'resend' ? 'Resend' : 'Custom SMTP');

const senderEmail = emailConfig.isShared
? 'noreply@stackframe.co'
? (sharedSenderEmail ?? 'Configured via STACK_EMAIL_SENDER')
: emailConfig.provider === 'managed' && emailConfig.managedSubdomain && emailConfig.managedSenderLocalPart
? `${emailConfig.managedSenderLocalPart}@${emailConfig.managedSubdomain}`
: emailConfig.senderEmail;
Expand Down Expand Up @@ -207,7 +218,7 @@ function EmailServerCard({ emailConfig }: { emailConfig: CompleteConfig['emails'
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">{serverType}</span>
{emailConfig.isShared && (
<SimpleTooltip tooltip="When you use the shared email server, all the emails are sent from Stack's email address" type='info' />
<SimpleTooltip tooltip="Shared email settings are read from STACK_EMAIL_* environment variables on the server." type='info' />
)}
</div>
</div>
Expand Down Expand Up @@ -820,7 +831,7 @@ function EditEmailServerDialog(props: {
name="type"
control={form.control}
options={[
{ label: "Shared (noreply@stackframe.co)", value: 'shared' },
{ label: "Shared (environment-configured)", value: 'shared' },
{ label: "Managed (via managed domain setup)", value: 'managed' },
{ label: "Resend (your own email address)", value: 'resend' },
{ label: "Custom SMTP server (your own email address)", value: 'standard' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { InternalApiKey, InternalApiKeyBase, InternalApiKeyBaseCrudRead, Interna
import { AdminProjectPermission, AdminProjectPermissionDefinition, AdminProjectPermissionDefinitionCreateOptions, AdminProjectPermissionDefinitionUpdateOptions, AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions, adminProjectPermissionDefinitionCreateOptionsToCrud, adminProjectPermissionDefinitionUpdateOptionsToCrud, adminTeamPermissionDefinitionCreateOptionsToCrud, adminTeamPermissionDefinitionUpdateOptionsToCrud } from "../../permissions";
import { AdminOwnedProject, AdminProject, AdminProjectUpdateOptions, PushConfigOptions, adminProjectUpdateOptionsToCrud } from "../../projects";
import type { AdminSessionReplay, AdminSessionReplayChunk, ListSessionReplayChunksOptions, ListSessionReplayChunksResult, ListSessionReplaysOptions, ListSessionReplaysResult, SessionReplayAllEventsResult } from "../../session-replays";
import { ManagedEmailProviderListItem, ManagedEmailProviderSetupResult, ManagedEmailProviderStatus, EmailOutboxUpdateOptions, StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
import { EmailOutboxUpdateOptions, ManagedEmailProviderListItem, ManagedEmailProviderSetupResult, ManagedEmailProviderStatus, StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/admin-app";
import { clientVersion, createCache, getDefaultExtraRequestHeaders, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey, resolveApiUrls, resolveConstructorOptions } from "./common";
import { _StackServerAppImplIncomplete } from "./server-app-impl";

Expand Down Expand Up @@ -208,7 +208,10 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
appleBundleIds: p.apple_bundle_ids,
} as const))),
emailConfig: data.config.email_config.type === 'shared' ? {
type: 'shared'
type: 'shared',
senderEmail: data.config.email_config.sender_email,
host: data.config.email_config.host,
port: data.config.email_config.port,
} : {
type: 'standard',
host: data.config.email_config.host ?? throwErr("Email host is missing"),
Expand Down
3 changes: 3 additions & 0 deletions packages/template/src/lib/stack-app/project-configs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export type AdminEmailConfig = (
}
| {
type: "shared",
senderEmail?: string,
host?: string,
port?: number,
}
);

Expand Down
Loading