mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-06 21:02:52 +08:00
https://www.loom.com/share/cc379c5372244a169f3ae1d2cc91eae5?sid=ec5bc438-56d8-4cca-9bbc-6cf6c6d313ad
<!-- ELLIPSIS_HIDDEN -->
----
> [!IMPORTANT]
> Introduce email draft functionality with AI assistance, including
creation, editing, and sending capabilities, and update APIs and UI
components accordingly.
>
> - **Features**:
> - Add email draft functionality: create, list, edit, preview, and send
drafts.
> - Integrate AI-assisted draft generation with `emailDraftAdapter` in
`email-draft-adapter.ts`.
> - Update `send-email` and `render-email` APIs to support drafts.
> - **Backend**:
> - Add `EmailDraft` model in `schema.prisma`.
> - Implement draft CRUD operations in `email-drafts/route.tsx` and
`email-drafts/[id]/route.tsx`.
> - Update `render-email/route.tsx` and `send-email/route.tsx` to handle
draft inputs.
> - **Frontend**:
> - Add email draft UI in `email-drafts/page-client.tsx` and
`email-drafts/[draftId]/page-client.tsx`.
> - Implement theme selection with `EmailThemeSelector` in
`email-theme-selector.tsx`.
> - Update sidebar navigation in `sidebar-layout.tsx` to include drafts.
> - **Tests**:
> - Add E2E tests for draft lifecycle and sending in
`email-drafts.test.ts` and `send-email.test.ts`.
> - **Misc**:
> - Update `admin-interface.ts` and `server-interface.ts` to support
draft operations.
> - Add `XOR` type utility in `types.tsx` for exclusive option handling.
>
> <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 c7ebb00ac5. 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**
* Email Drafts: create, list, edit, preview, theme-select, AI-assisted
draft generation, send (marks draft as sent) and dashboard UI for
drafting + recipient selection.
* **Improvements**
* Send/render APIs accept html, template, or draft inputs and an
all-users option; per-recipient delivery/reporting, unified theme
selector, expanded chat context for drafts, clearer schema validation
errors, breadcrumb updates.
* **Tests**
* E2E coverage for draft lifecycle, draft-based send, all-users flow,
and updated schema-validation snapshots.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
172 lines
5.0 KiB
TypeScript
172 lines
5.0 KiB
TypeScript
import { KnownErrors } from "@stackframe/stack-shared";
|
|
import { DEFAULT_EMAIL_THEME_ID, DEFAULT_TEMPLATE_IDS } from "@stackframe/stack-shared/dist/helpers/emails";
|
|
import { it } from "../helpers";
|
|
import { createApp } from "./js-helpers";
|
|
|
|
async function setupEmailServer(adminApp: any) {
|
|
const project = await adminApp.getProject();
|
|
await project.updateConfig({
|
|
emails: {
|
|
server: {
|
|
isShared: false,
|
|
host: "localhost",
|
|
port: 1025,
|
|
username: "test",
|
|
password: "password",
|
|
senderEmail: "test@example.com",
|
|
senderName: "Test User",
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
it("should successfully send email with HTML content", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
html: "<h1>Test Email</h1><p>This is a test email with HTML content.</p>",
|
|
subject: "Test Subject",
|
|
})).resolves.not.toThrow();
|
|
});
|
|
|
|
it("should successfully send email with template", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
templateId: DEFAULT_TEMPLATE_IDS.sign_in_invitation,
|
|
variables: {
|
|
teamDisplayName: "Test Team",
|
|
signInInvitationLink: "https://example.com",
|
|
},
|
|
subject: "Welcome!",
|
|
})).resolves.not.toThrow();
|
|
});
|
|
|
|
it("should successfully send email to multiple users", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user1 = await serverApp.createUser({
|
|
primaryEmail: "test1@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
const user2 = await serverApp.createUser({
|
|
primaryEmail: "test2@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user1.id, user2.id],
|
|
html: "<p>Bulk email test</p>",
|
|
subject: "Bulk Email Test",
|
|
})).resolves.not.toThrow();
|
|
});
|
|
|
|
it("should send email with theme customization", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
html: "<p>Themed email test</p>",
|
|
subject: "Themed Email",
|
|
themeId: DEFAULT_EMAIL_THEME_ID,
|
|
})).resolves.not.toThrow();
|
|
});
|
|
|
|
it("should send email with notification category", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
html: "<p>Notification email test</p>",
|
|
subject: "Notification Email",
|
|
notificationCategoryName: "Transactional",
|
|
})).resolves.not.toThrow();
|
|
});
|
|
|
|
it("should throw RequiresCustomEmailServer error when email server is not configured", async ({ expect }) => {
|
|
const { serverApp } = await createApp();
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
html: "<p>This should fail</p>",
|
|
subject: "Test Email",
|
|
})).rejects.toThrow(KnownErrors.RequiresCustomEmailServer);
|
|
});
|
|
|
|
it("should handle non-existent user IDs", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
// Use a properly formatted UUID that doesn't exist
|
|
await expect(serverApp.sendEmail({
|
|
userIds: ["123e4567-e89b-12d3-a456-426614174000"],
|
|
html: "<p>Non-existent user test</p>",
|
|
subject: "Test Email",
|
|
})).rejects.toThrow(KnownErrors.UserIdDoesNotExist);
|
|
});
|
|
|
|
it("should handle missing required email content", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
subject: "Test Email",
|
|
} as any)).rejects.toThrow(KnownErrors.SchemaError);
|
|
});
|
|
|
|
it("should handle html and templateId at the same time", async ({ expect }) => {
|
|
const { adminApp, serverApp } = await createApp();
|
|
await setupEmailServer(adminApp);
|
|
|
|
const user = await serverApp.createUser({
|
|
primaryEmail: "test@example.com",
|
|
primaryEmailVerified: true,
|
|
});
|
|
|
|
await expect(serverApp.sendEmail({
|
|
userIds: [user.id],
|
|
html: "<p>Test Email</p>",
|
|
templateId: DEFAULT_TEMPLATE_IDS.sign_in_invitation,
|
|
subject: "Test Email",
|
|
} as any)).rejects.toThrow(KnownErrors.SchemaError);
|
|
});
|