mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Merge branch 'dev' into this-line-platform
This commit is contained in:
commit
8401b01586
42
apps/backend/src/app/api/latest/internal/emails/crud.tsx
Normal file
42
apps/backend/src/app/api/latest/internal/emails/crud.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { prismaClient } from "@/prisma-client";
|
||||
import { createCrudHandlers } from "@/route-handlers/crud-handler";
|
||||
import { SentEmail } from "@prisma/client";
|
||||
import { InternalEmailsCrud, internalEmailsCrud } from "@stackframe/stack-shared/dist/interface/crud/emails";
|
||||
import { projectIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
|
||||
|
||||
function prismaModelToCrud(prismaModel: SentEmail): InternalEmailsCrud["Admin"]["Read"] {
|
||||
|
||||
return {
|
||||
id: prismaModel.id,
|
||||
subject: prismaModel.subject,
|
||||
sent_at_millis: prismaModel.createdAt.getTime(),
|
||||
to: prismaModel.to,
|
||||
sender_config: prismaModel.senderConfig,
|
||||
error: prismaModel.error,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const internalEmailsCrudHandlers = createLazyProxy(() => createCrudHandlers(internalEmailsCrud, {
|
||||
paramsSchema: yupObject({
|
||||
projectId: projectIdSchema.defined(),
|
||||
}),
|
||||
onList: async ({ params }) => {
|
||||
const emails = await prismaClient.sentEmail.findMany({
|
||||
where: {
|
||||
tenancy: {
|
||||
projectId: params.projectId,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
items: emails.map(x => prismaModelToCrud(x)),
|
||||
is_paginated: false,
|
||||
};
|
||||
}
|
||||
}));
|
||||
@ -0,0 +1,3 @@
|
||||
import { internalEmailsCrudHandlers } from "./crud";
|
||||
|
||||
export const GET = internalEmailsCrudHandlers.listHandler;
|
||||
@ -5,16 +5,18 @@ import { InputField, SelectField } from "@/components/form-fields";
|
||||
import { useRouter } from "@/components/router";
|
||||
import { SettingCard, SettingText } from "@/components/settings";
|
||||
import { getPublicEnvVar } from "@/lib/env";
|
||||
import { AdminEmailConfig, AdminProject } from "@stackframe/stack";
|
||||
import { AdminEmailConfig, AdminProject, AdminSentEmail } from "@stackframe/stack";
|
||||
import { Reader } from "@stackframe/stack-emails/dist/editor/email-builder/index";
|
||||
import { EMAIL_TEMPLATES_METADATA, convertEmailSubjectVariables, convertEmailTemplateMetadataExampleValues, convertEmailTemplateVariables, validateEmailTemplateContent } from "@stackframe/stack-emails/dist/utils";
|
||||
import { EmailTemplateType } from "@stackframe/stack-shared/dist/interface/crud/email-templates";
|
||||
import { strictEmailSchema } from "@stackframe/stack-shared/dist/schema-fields";
|
||||
import { throwErr } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
import { deepPlainEquals } from "@stackframe/stack-shared/dist/utils/objects";
|
||||
import { ActionCell, ActionDialog, Alert, AlertDescription, AlertTitle, Button, Card, SimpleTooltip, Typography, useToast } from "@stackframe/stack-ui";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { ActionCell, ActionDialog, Alert, AlertDescription, AlertTitle, Button, Card, DataTable, SimpleTooltip, Typography, useToast } from "@stackframe/stack-ui";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import * as yup from "yup";
|
||||
import { PageLayout } from "../page-layout";
|
||||
import { useAdminApp } from "../use-admin-app";
|
||||
@ -110,6 +112,9 @@ export default function PageClient() {
|
||||
</Card>
|
||||
))}
|
||||
</SettingCard>
|
||||
<SettingCard title="Email Log" description="Manage email sending history">
|
||||
<EmailSendDataTable />
|
||||
</SettingCard>
|
||||
|
||||
<ActionDialog
|
||||
open={sharedSmtpWarningDialogOpen !== null}
|
||||
@ -133,6 +138,60 @@ export default function PageClient() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const emailTableColumns: ColumnDef<AdminSentEmail>[] = [
|
||||
{ accessorKey: 'recipient', header: 'Recipient' },
|
||||
{ accessorKey: 'subject', header: 'Subject' },
|
||||
{ accessorKey: 'sentAt', header: 'Sent At', cell: ({ row }) => {
|
||||
const date = row.original.sentAt;
|
||||
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||
}
|
||||
},
|
||||
{ accessorKey: 'status', header: 'Status', cell: ({ row }) => {
|
||||
return row.original.error ? (
|
||||
<div className="text-red-500">Failed</div>
|
||||
) : (
|
||||
<div className="text-green-500">Sent</div>
|
||||
);
|
||||
} },
|
||||
];
|
||||
|
||||
function EmailSendDataTable() {
|
||||
const stackAdminApp = useAdminApp();
|
||||
const [emailLogs, setEmailLogs] = useState<AdminSentEmail[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Fetch email logs when component mounts
|
||||
useEffect(() => {
|
||||
runAsynchronously(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const emails = await stackAdminApp.listSentEmails();
|
||||
setEmailLogs(emails);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch email logs:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, [stackAdminApp]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center py-4">
|
||||
<Typography>Loading email logs...</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <DataTable
|
||||
data={emailLogs}
|
||||
defaultColumnFilters={[]}
|
||||
columns={emailTableColumns}
|
||||
defaultSorting={[{ id: 'sentAt', desc: true }]}
|
||||
/>;
|
||||
}
|
||||
|
||||
function EmailPreview(props: { content: any, type: EmailTemplateType }) {
|
||||
const project = useAdminApp().useProject();
|
||||
const [valid, document] = useMemo(() => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { InternalSession } from "../sessions";
|
||||
import { ApiKeysCrud } from "./crud/api-keys";
|
||||
import { EmailTemplateCrud, EmailTemplateType } from "./crud/email-templates";
|
||||
import { InternalEmailsCrud } from "./crud/emails";
|
||||
import { ProjectsCrud } from "./crud/projects";
|
||||
import { SvixTokenCrud } from "./crud/svix-token";
|
||||
import { TeamPermissionDefinitionsCrud } from "./crud/team-permissions";
|
||||
@ -247,4 +248,11 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
}, null);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async listSentEmails(): Promise<InternalEmailsCrud["Admin"]["List"]> {
|
||||
const response = await this.sendAdminRequest("/internal/emails", {
|
||||
method: "GET",
|
||||
}, null);
|
||||
return await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/stack-shared/src/interface/crud/emails.ts
Normal file
17
packages/stack-shared/src/interface/crud/emails.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { createCrud, CrudTypeOf } from "../../crud";
|
||||
import * as fieldSchema from "../../schema-fields";
|
||||
|
||||
export const sentEmailReadSchema = fieldSchema.yupObject({
|
||||
id: fieldSchema.yupString().defined(),
|
||||
subject: fieldSchema.yupString().defined(),
|
||||
sent_at_millis: fieldSchema.yupNumber().defined(),
|
||||
to: fieldSchema.yupArray(fieldSchema.yupString().defined()),
|
||||
sender_config: fieldSchema.yupMixed().nullable().defined(),
|
||||
error: fieldSchema.yupMixed().nullable().optional(),
|
||||
}).defined();
|
||||
|
||||
export const internalEmailsCrud = createCrud({
|
||||
clientReadSchema: sentEmailReadSchema,
|
||||
});
|
||||
|
||||
export type InternalEmailsCrud = CrudTypeOf<typeof internalEmailsCrud>;
|
||||
@ -17,6 +17,7 @@ import { StackAdminApp, StackAdminAppConstructorOptions } from "../interfaces/ad
|
||||
import { clientVersion, createCache, getBaseUrl, getDefaultProjectId, getDefaultPublishableClientKey, getDefaultSecretServerKey, getDefaultSuperSecretAdminKey } from "./common";
|
||||
import { _StackServerAppImplIncomplete } from "./server-app-impl";
|
||||
|
||||
import { AdminSentEmail } from "../..";
|
||||
import { useAsyncCache } from "./common"; // THIS_LINE_PLATFORM react-like
|
||||
|
||||
|
||||
@ -336,4 +337,16 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
return Result.error({ errorMessage: response.error_message ?? throwErr("Email test error not specified") });
|
||||
}
|
||||
}
|
||||
|
||||
async listSentEmails(): Promise<AdminSentEmail[]> {
|
||||
const response = await this._interface.listSentEmails();
|
||||
return response.items.map((email) => ({
|
||||
id: email.id,
|
||||
to: email.to ?? [],
|
||||
subject: email.subject,
|
||||
recipient: email.to?.[0] ?? "",
|
||||
sentAt: new Date(email.sent_at_millis),
|
||||
error: email.error,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { InternalSession } from "@stackframe/stack-shared/dist/sessions";
|
||||
import { Result } from "@stackframe/stack-shared/dist/utils/results";
|
||||
import { ApiKey, ApiKeyCreateOptions, ApiKeyFirstView } from "../../api-keys";
|
||||
import { AsyncStoreProperty, EmailConfig } from "../../common";
|
||||
import { AdminSentEmail } from "../../email";
|
||||
import { AdminEmailTemplate, AdminEmailTemplateUpdateOptions } from "../../email-templates";
|
||||
import { AdminTeamPermission, AdminTeamPermissionDefinition, AdminTeamPermissionDefinitionCreateOptions, AdminTeamPermissionDefinitionUpdateOptions } from "../../permissions";
|
||||
import { AdminProject } from "../../projects";
|
||||
@ -48,6 +49,8 @@ export type StackAdminApp<HasTokenStore extends boolean = boolean, ProjectId ext
|
||||
recipientEmail: string,
|
||||
emailConfig: EmailConfig,
|
||||
}): Promise<Result<undefined, { errorMessage: string }>>,
|
||||
|
||||
listSentEmails(): Promise<AdminSentEmail[]>,
|
||||
}
|
||||
& StackServerApp<HasTokenStore, ProjectId>
|
||||
);
|
||||
|
||||
8
packages/template/src/lib/stack-app/email/index.ts
Normal file
8
packages/template/src/lib/stack-app/email/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export type AdminSentEmail = {
|
||||
id: string,
|
||||
to: string[],
|
||||
subject: string,
|
||||
recipient: string, // We'll derive this from to[0] for display
|
||||
sentAt: Date, // We'll derive this from sent_at_millis for display
|
||||
error?: unknown,
|
||||
}
|
||||
@ -42,6 +42,10 @@ export type {
|
||||
ContactChannel
|
||||
} from "./contact-channels";
|
||||
|
||||
export type {
|
||||
AdminSentEmail
|
||||
} from "./email";
|
||||
|
||||
export type {
|
||||
AdminTeamPermission,
|
||||
AdminTeamPermissionDefinition,
|
||||
|
||||
567
pnpm-lock.yaml
567
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user