Add MVP version
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { postRouter } from "~/server/api/routers/post";
|
||||
import { domainRouter } from "~/server/api/routers/domain";
|
||||
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||
import { apiRouter } from "./routers/api";
|
||||
import { emailRouter } from "./routers/email";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -7,7 +9,9 @@ import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
|
||||
* All routers added in /api/routers should be manually added here.
|
||||
*/
|
||||
export const appRouter = createTRPCRouter({
|
||||
post: postRouter,
|
||||
domain: domainRouter,
|
||||
apiKey: apiRouter,
|
||||
email: emailRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
42
apps/web/src/server/api/routers/api.ts
Normal file
42
apps/web/src/server/api/routers/api.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
teamProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { db } from "~/server/db";
|
||||
import { addApiKey } from "~/server/service/api-service";
|
||||
import { createDomain, getDomain } from "~/server/service/domain-service";
|
||||
|
||||
export const apiRouter = createTRPCRouter({
|
||||
createToken: teamProcedure
|
||||
.input(
|
||||
z.object({ name: z.string(), permission: z.enum(["FULL", "SENDING"]) })
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return addApiKey({
|
||||
name: input.name,
|
||||
permission: input.permission,
|
||||
teamId: ctx.team.id,
|
||||
});
|
||||
}),
|
||||
|
||||
getApiKeys: teamProcedure.query(async ({ ctx }) => {
|
||||
const keys = await ctx.db.apiKey.findMany({
|
||||
where: {
|
||||
teamId: ctx.team.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
permission: true,
|
||||
partialToken: true,
|
||||
lastUsed: true,
|
||||
},
|
||||
});
|
||||
|
||||
return keys;
|
||||
}),
|
||||
});
|
34
apps/web/src/server/api/routers/domain.ts
Normal file
34
apps/web/src/server/api/routers/domain.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
teamProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { db } from "~/server/db";
|
||||
import { createDomain, getDomain } from "~/server/service/domain-service";
|
||||
|
||||
export const domainRouter = createTRPCRouter({
|
||||
createDomain: teamProcedure
|
||||
.input(z.object({ name: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return createDomain(ctx.team.id, input.name);
|
||||
}),
|
||||
|
||||
domains: teamProcedure.query(async ({ ctx }) => {
|
||||
const domains = await db.domain.findMany({
|
||||
where: {
|
||||
teamId: ctx.team.id,
|
||||
},
|
||||
});
|
||||
|
||||
return domains;
|
||||
}),
|
||||
|
||||
getDomain: teamProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return getDomain(input.id);
|
||||
}),
|
||||
});
|
41
apps/web/src/server/api/routers/email.ts
Normal file
41
apps/web/src/server/api/routers/email.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
teamProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
import { db } from "~/server/db";
|
||||
import { createDomain, getDomain } from "~/server/service/domain-service";
|
||||
|
||||
export const emailRouter = createTRPCRouter({
|
||||
emails: teamProcedure.query(async ({ ctx }) => {
|
||||
const emails = await db.email.findMany({
|
||||
where: {
|
||||
teamId: ctx.team.id,
|
||||
},
|
||||
});
|
||||
|
||||
return emails;
|
||||
}),
|
||||
|
||||
getEmail: teamProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const email = await db.email.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
include: {
|
||||
emailEvents: {
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return email;
|
||||
}),
|
||||
});
|
@@ -1,42 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
publicProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
|
||||
export const postRouter = createTRPCRouter({
|
||||
hello: publicProcedure
|
||||
.input(z.object({ text: z.string() }))
|
||||
.query(({ input }) => {
|
||||
return {
|
||||
greeting: `Hello ${input.text}`,
|
||||
};
|
||||
}),
|
||||
|
||||
create: protectedProcedure
|
||||
.input(z.object({ name: z.string().min(1) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// simulate a slow db call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
return ctx.db.post.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
createdBy: { connect: { id: ctx.session.user.id } },
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
getLatest: protectedProcedure.query(({ ctx }) => {
|
||||
return ctx.db.post.findFirst({
|
||||
orderBy: { createdAt: "desc" },
|
||||
where: { createdBy: { id: ctx.session.user.id } },
|
||||
});
|
||||
}),
|
||||
|
||||
getSecretMessage: protectedProcedure.query(() => {
|
||||
return "you can now see this secret message!";
|
||||
}),
|
||||
});
|
@@ -9,7 +9,7 @@
|
||||
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
import { getServerAuthSession } from "~/server/auth";
|
||||
import { db } from "~/server/db";
|
||||
@@ -106,3 +106,21 @@ export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export const teamProcedure = protectedProcedure.use(
|
||||
async ({ ctx, next, input }) => {
|
||||
const teamUser = await db.teamUser.findFirst({
|
||||
where: { userId: ctx.session.user.id },
|
||||
include: { team: true },
|
||||
});
|
||||
if (!teamUser) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
team: teamUser.team,
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
type NextAuthOptions,
|
||||
} from "next-auth";
|
||||
import { type Adapter } from "next-auth/adapters";
|
||||
import DiscordProvider from "next-auth/providers/discord";
|
||||
import GitHubProvider from "next-auth/providers/github";
|
||||
|
||||
import { env } from "~/env";
|
||||
import { db } from "~/server/db";
|
||||
@@ -19,16 +19,15 @@ import { db } from "~/server/db";
|
||||
declare module "next-auth" {
|
||||
interface Session extends DefaultSession {
|
||||
user: {
|
||||
id: string;
|
||||
id: number;
|
||||
// ...other properties
|
||||
// role: UserRole;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
// interface User {
|
||||
// // ...other properties
|
||||
// // role: UserRole;
|
||||
// }
|
||||
interface User {
|
||||
id: number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,6 +56,10 @@ export const authOptions: NextAuthOptions = {
|
||||
*
|
||||
* @see https://next-auth.js.org/providers/github
|
||||
*/
|
||||
GitHubProvider({
|
||||
clientId: env.GITHUB_ID,
|
||||
clientSecret: env.GITHUB_SECRET,
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -66,3 +69,15 @@ export const authOptions: NextAuthOptions = {
|
||||
* @see https://next-auth.js.org/configuration/nextjs
|
||||
*/
|
||||
export const getServerAuthSession = () => getServerSession(authOptions);
|
||||
|
||||
import { createHash } from "crypto";
|
||||
|
||||
/**
|
||||
* Hashes a token using SHA-256.
|
||||
*
|
||||
* @param {string} token - The token to be hashed.
|
||||
* @returns {string} The hashed token.
|
||||
*/
|
||||
export function hashToken(token: string) {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
|
95
apps/web/src/server/aws/setup.ts
Normal file
95
apps/web/src/server/aws/setup.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { JsonValue } from "@prisma/client/runtime/library";
|
||||
import { db } from "../db";
|
||||
import { APP_SETTINGS } from "~/utils/constants";
|
||||
import { createTopic, subscribeEndpoint } from "./sns";
|
||||
import { env } from "~/env";
|
||||
import { AppSettingsService } from "~/server/service/app-settings-service";
|
||||
import { addWebhookConfiguration } from "../ses";
|
||||
import { EventType } from "@aws-sdk/client-sesv2";
|
||||
|
||||
const GENERAL_EVENTS: EventType[] = [
|
||||
"BOUNCE",
|
||||
"COMPLAINT",
|
||||
"DELIVERY",
|
||||
"DELIVERY_DELAY",
|
||||
"REJECT",
|
||||
"RENDERING_FAILURE",
|
||||
"SEND",
|
||||
"SUBSCRIPTION",
|
||||
];
|
||||
|
||||
export async function setupAws() {
|
||||
AppSettingsService.initializeCache();
|
||||
let snsTopicArn = await AppSettingsService.getSetting(
|
||||
APP_SETTINGS.SNS_TOPIC_ARN
|
||||
);
|
||||
console.log("Setting up AWS");
|
||||
|
||||
if (!snsTopicArn) {
|
||||
console.log("SNS topic not present, creating...");
|
||||
snsTopicArn = await createUnsendSNSTopic();
|
||||
}
|
||||
|
||||
await setupSESConfiguration();
|
||||
}
|
||||
|
||||
async function createUnsendSNSTopic() {
|
||||
const topicArn = await createTopic(env.SNS_TOPIC);
|
||||
if (!topicArn) {
|
||||
console.error("Failed to create SNS topic");
|
||||
return;
|
||||
}
|
||||
|
||||
await subscribeEndpoint(
|
||||
topicArn,
|
||||
`${env.APP_URL ?? env.NEXTAUTH_URL}/api/ses_callback`
|
||||
);
|
||||
|
||||
return await AppSettingsService.setSetting(
|
||||
APP_SETTINGS.SNS_TOPIC_ARN,
|
||||
topicArn
|
||||
);
|
||||
}
|
||||
|
||||
async function setupSESConfiguration() {
|
||||
const topicArn = (
|
||||
await AppSettingsService.getSetting(APP_SETTINGS.SNS_TOPIC_ARN)
|
||||
)?.toString();
|
||||
|
||||
if (!topicArn) {
|
||||
return;
|
||||
}
|
||||
console.log("Setting up SES webhook configuration");
|
||||
|
||||
await setWebhookConfiguration(
|
||||
APP_SETTINGS.SES_CONFIGURATION_GENERAL,
|
||||
topicArn,
|
||||
GENERAL_EVENTS
|
||||
);
|
||||
|
||||
await setWebhookConfiguration(
|
||||
APP_SETTINGS.SES_CONFIGURATION_CLICK_TRACKING,
|
||||
topicArn,
|
||||
[...GENERAL_EVENTS, "CLICK"]
|
||||
);
|
||||
|
||||
await setWebhookConfiguration(
|
||||
APP_SETTINGS.SES_CONFIGURATION_OPEN_TRACKING,
|
||||
topicArn,
|
||||
[...GENERAL_EVENTS, "OPEN"]
|
||||
);
|
||||
}
|
||||
|
||||
async function setWebhookConfiguration(
|
||||
setting: string,
|
||||
topicArn: string,
|
||||
eventTypes: EventType[]
|
||||
) {
|
||||
const sesConfigurationGeneral = await AppSettingsService.getSetting(setting);
|
||||
|
||||
if (!sesConfigurationGeneral) {
|
||||
console.log(`Setting up SES webhook configuration for ${setting}`);
|
||||
const status = await addWebhookConfiguration(setting, topicArn, eventTypes);
|
||||
await AppSettingsService.setSetting(setting, status.toString());
|
||||
}
|
||||
}
|
39
apps/web/src/server/aws/sns.ts
Normal file
39
apps/web/src/server/aws/sns.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
SNSClient,
|
||||
CreateTopicCommand,
|
||||
SubscribeCommand,
|
||||
} from "@aws-sdk/client-sns";
|
||||
import { env } from "~/env";
|
||||
|
||||
function getSnsClient(region = "us-east-1") {
|
||||
return new SNSClient({
|
||||
region: region,
|
||||
credentials: {
|
||||
accessKeyId: env.AWS_ACCESS_KEY,
|
||||
secretAccessKey: env.AWS_SECRET_KEY,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createTopic(topic: string) {
|
||||
const client = getSnsClient();
|
||||
const command = new CreateTopicCommand({
|
||||
Name: topic,
|
||||
});
|
||||
|
||||
const data = await client.send(command);
|
||||
return data.TopicArn;
|
||||
}
|
||||
|
||||
export async function subscribeEndpoint(topicArn: string, endpointUrl: string) {
|
||||
const subscribeCommand = new SubscribeCommand({
|
||||
Protocol: "https",
|
||||
TopicArn: topicArn,
|
||||
Endpoint: endpointUrl,
|
||||
});
|
||||
const client = getSnsClient();
|
||||
|
||||
const data = await client.send(subscribeCommand);
|
||||
console.log(data.SubscriptionArn);
|
||||
return data.SubscriptionArn;
|
||||
}
|
59
apps/web/src/server/service/api-service.ts
Normal file
59
apps/web/src/server/service/api-service.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ApiPermission } from "@prisma/client";
|
||||
import { db } from "../db";
|
||||
import { randomBytes } from "crypto";
|
||||
import { hashToken } from "../auth";
|
||||
|
||||
export async function addApiKey({
|
||||
name,
|
||||
permission,
|
||||
teamId,
|
||||
}: {
|
||||
name: string;
|
||||
permission: ApiPermission;
|
||||
teamId: number;
|
||||
}) {
|
||||
try {
|
||||
const token = `us_${randomBytes(20).toString("hex")}`;
|
||||
const hashedToken = hashToken(token);
|
||||
|
||||
await db.apiKey.create({
|
||||
data: {
|
||||
name,
|
||||
permission: permission,
|
||||
teamId,
|
||||
tokenHash: hashedToken,
|
||||
partialToken: `${token.slice(0, 8)}...${token.slice(-5)}`,
|
||||
},
|
||||
});
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error("Error adding API key:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function retrieveApiKey(token: string) {
|
||||
const hashedToken = hashToken(token);
|
||||
|
||||
try {
|
||||
const apiKey = await db.apiKey.findUnique({
|
||||
where: {
|
||||
tokenHash: hashedToken,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
permission: true,
|
||||
teamId: true,
|
||||
partialToken: true,
|
||||
},
|
||||
});
|
||||
if (!apiKey) {
|
||||
throw new Error("API Key not found");
|
||||
}
|
||||
return apiKey;
|
||||
} catch (error) {
|
||||
console.error("Error retrieving API key:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
38
apps/web/src/server/service/app-settings-service.ts
Normal file
38
apps/web/src/server/service/app-settings-service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { db } from "../db";
|
||||
import { JsonValue } from "@prisma/client/runtime/library";
|
||||
|
||||
export class AppSettingsService {
|
||||
private static cache: Record<string, JsonValue> = {};
|
||||
|
||||
public static async getSetting(key: string) {
|
||||
if (!this.cache[key]) {
|
||||
const setting = await db.appSetting.findUnique({
|
||||
where: { key },
|
||||
});
|
||||
if (setting) {
|
||||
this.cache[key] = setting.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return this.cache[key];
|
||||
}
|
||||
|
||||
public static async setSetting(key: string, value: string) {
|
||||
await db.appSetting.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
});
|
||||
this.cache[key] = value;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static async initializeCache(): Promise<void> {
|
||||
const settings = await db.appSetting.findMany();
|
||||
settings.forEach((setting) => {
|
||||
this.cache[setting.key] = setting.value;
|
||||
});
|
||||
}
|
||||
}
|
69
apps/web/src/server/service/domain-service.ts
Normal file
69
apps/web/src/server/service/domain-service.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { addDomain, getDomainIdentity } from "~/server/ses";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
export async function createDomain(teamId: number, name: string) {
|
||||
console.log("Creating domain:", name);
|
||||
const publicKey = await addDomain(name);
|
||||
|
||||
const domain = await db.domain.create({
|
||||
data: {
|
||||
name,
|
||||
publicKey,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
export async function getDomain(id: number) {
|
||||
let domain = await db.domain.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!domain) {
|
||||
throw new Error("Domain not found");
|
||||
}
|
||||
|
||||
if (domain.status !== "SUCCESS") {
|
||||
const domainIdentity = await getDomainIdentity(domain.name, domain.region);
|
||||
|
||||
const dkimStatus = domainIdentity.DkimAttributes?.Status;
|
||||
const spfDetails = domainIdentity.MailFromAttributes?.MailFromDomainStatus;
|
||||
const verificationError = domainIdentity.VerificationInfo?.ErrorType;
|
||||
const verificationStatus = domainIdentity.VerificationStatus;
|
||||
const lastCheckedTime =
|
||||
domainIdentity.VerificationInfo?.LastCheckedTimestamp;
|
||||
|
||||
console.log(domainIdentity);
|
||||
|
||||
if (
|
||||
domain.dkimStatus !== dkimStatus ||
|
||||
domain.spfDetails !== spfDetails ||
|
||||
domain.status !== verificationStatus
|
||||
) {
|
||||
domain = await db.domain.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
dkimStatus,
|
||||
spfDetails,
|
||||
status: verificationStatus ?? "NOT_STARTED",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...domain,
|
||||
dkimStatus,
|
||||
spfDetails,
|
||||
verificationError,
|
||||
lastCheckedTime,
|
||||
};
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
49
apps/web/src/server/service/email-service.ts
Normal file
49
apps/web/src/server/service/email-service.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { EmailContent } from "~/types";
|
||||
import { db } from "../db";
|
||||
import { sendEmailThroughSes } from "../ses";
|
||||
|
||||
export async function sendEmail(
|
||||
emailContent: EmailContent & { teamId: number }
|
||||
) {
|
||||
const { to, from, subject, text, html, teamId } = emailContent;
|
||||
|
||||
const domains = await db.domain.findMany({ where: { teamId } });
|
||||
|
||||
const fromDomain = from.split("@")[1];
|
||||
if (!fromDomain) {
|
||||
throw new Error("From email is not valid");
|
||||
}
|
||||
|
||||
const domain = domains.find((domain) => domain.name === fromDomain);
|
||||
if (!domain) {
|
||||
throw new Error("Domain not found. Add domain to unsend first");
|
||||
}
|
||||
|
||||
if (domain.status !== "SUCCESS") {
|
||||
throw new Error("Domain is not verified");
|
||||
}
|
||||
|
||||
const messageId = await sendEmailThroughSes({
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
region: domain.region,
|
||||
});
|
||||
|
||||
if (messageId) {
|
||||
return await db.email.create({
|
||||
data: {
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
id: messageId,
|
||||
teamId,
|
||||
domainId: domain.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
176
apps/web/src/server/ses.ts
Normal file
176
apps/web/src/server/ses.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
SESv2Client,
|
||||
CreateEmailIdentityCommand,
|
||||
DeleteEmailIdentityCommand,
|
||||
GetEmailIdentityCommand,
|
||||
PutEmailIdentityMailFromAttributesCommand,
|
||||
SendEmailCommand,
|
||||
CreateConfigurationSetEventDestinationCommand,
|
||||
CreateConfigurationSetCommand,
|
||||
EventType,
|
||||
} from "@aws-sdk/client-sesv2";
|
||||
import { generateKeyPairSync } from "crypto";
|
||||
import { env } from "~/env";
|
||||
import { EmailContent } from "~/types";
|
||||
import { APP_SETTINGS } from "~/utils/constants";
|
||||
|
||||
function getSesClient(region = "us-east-1") {
|
||||
return new SESv2Client({
|
||||
region: region,
|
||||
credentials: {
|
||||
accessKeyId: env.AWS_ACCESS_KEY,
|
||||
secretAccessKey: env.AWS_SECRET_KEY,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function generateKeyPair() {
|
||||
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
|
||||
modulusLength: 2048, // Length of your key in bits
|
||||
publicKeyEncoding: {
|
||||
type: "spki", // Recommended to be 'spki' by the Node.js docs
|
||||
format: "pem",
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: "pkcs8", // Recommended to be 'pkcs8' by the Node.js docs
|
||||
format: "pem",
|
||||
},
|
||||
});
|
||||
|
||||
const base64PrivateKey = privateKey
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replace(/\n/g, "");
|
||||
|
||||
const base64PublicKey = publicKey
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replace(/\n/g, "");
|
||||
|
||||
return { privateKey: base64PrivateKey, publicKey: base64PublicKey };
|
||||
}
|
||||
|
||||
export async function addDomain(domain: string, region = "us-east-1") {
|
||||
const sesClient = getSesClient(region);
|
||||
|
||||
const { privateKey, publicKey } = generateKeyPair();
|
||||
const command = new CreateEmailIdentityCommand({
|
||||
EmailIdentity: domain,
|
||||
DkimSigningAttributes: {
|
||||
DomainSigningSelector: "unsend",
|
||||
DomainSigningPrivateKey: privateKey,
|
||||
},
|
||||
ConfigurationSetName: APP_SETTINGS.SES_CONFIGURATION_GENERAL,
|
||||
});
|
||||
const response = await sesClient.send(command);
|
||||
|
||||
const emailIdentityCommand = new PutEmailIdentityMailFromAttributesCommand({
|
||||
EmailIdentity: domain,
|
||||
MailFromDomain: `send.${domain}`,
|
||||
});
|
||||
|
||||
const emailIdentityResponse = await sesClient.send(emailIdentityCommand);
|
||||
|
||||
if (
|
||||
response.$metadata.httpStatusCode !== 200 ||
|
||||
emailIdentityResponse.$metadata.httpStatusCode !== 200
|
||||
) {
|
||||
throw new Error("Failed to create email identity");
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
export async function getDomainIdentity(domain: string, region = "us-east-1") {
|
||||
const sesClient = getSesClient(region);
|
||||
const command = new GetEmailIdentityCommand({
|
||||
EmailIdentity: domain,
|
||||
});
|
||||
const response = await sesClient.send(command);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function sendEmailThroughSes({
|
||||
to,
|
||||
from,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
region = "us-east-1",
|
||||
}: EmailContent & {
|
||||
region?: string;
|
||||
}) {
|
||||
const sesClient = getSesClient(region);
|
||||
const command = new SendEmailCommand({
|
||||
FromEmailAddress: from,
|
||||
Destination: {
|
||||
ToAddresses: [to],
|
||||
},
|
||||
Content: {
|
||||
// EmailContent
|
||||
Simple: {
|
||||
// Message
|
||||
Subject: {
|
||||
// Content
|
||||
Data: subject, // required
|
||||
Charset: "UTF-8",
|
||||
},
|
||||
Body: {
|
||||
// Body
|
||||
Text: {
|
||||
Data: text, // required
|
||||
Charset: "UTF-8",
|
||||
},
|
||||
Html: {
|
||||
Data: html, // required
|
||||
Charset: "UTF-8",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await sesClient.send(command);
|
||||
console.log("Email sent! Message ID:", response.MessageId);
|
||||
return response.MessageId;
|
||||
} catch (error) {
|
||||
console.error("Failed to send email", error);
|
||||
throw new Error("Failed to send email");
|
||||
}
|
||||
}
|
||||
|
||||
export async function addWebhookConfiguration(
|
||||
configName: string,
|
||||
topicArn: string,
|
||||
eventTypes: EventType[],
|
||||
region = "us-east-1"
|
||||
) {
|
||||
const sesClient = getSesClient(region);
|
||||
|
||||
const configSetCommand = new CreateConfigurationSetCommand({
|
||||
ConfigurationSetName: configName,
|
||||
});
|
||||
|
||||
const configSetResponse = await sesClient.send(configSetCommand);
|
||||
|
||||
if (configSetResponse.$metadata.httpStatusCode !== 200) {
|
||||
throw new Error("Failed to create configuration set");
|
||||
}
|
||||
|
||||
const command = new CreateConfigurationSetEventDestinationCommand({
|
||||
ConfigurationSetName: configName, // required
|
||||
EventDestinationName: "unsend_destination", // required
|
||||
EventDestination: {
|
||||
// EventDestinationDefinition
|
||||
Enabled: true,
|
||||
MatchingEventTypes: eventTypes,
|
||||
SnsDestination: {
|
||||
TopicArn: topicArn,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await sesClient.send(command);
|
||||
return response.$metadata.httpStatusCode === 200;
|
||||
}
|
Reference in New Issue
Block a user