enable teams for self-hosted (#137)
* enable teams for self-hosted * remove console
This commit is contained in:
@@ -91,12 +91,10 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|||||||
Developer settings
|
Developer settings
|
||||||
</NavButton>
|
</NavButton>
|
||||||
|
|
||||||
{isCloud() ? (
|
|
||||||
<NavButton href="/settings">
|
<NavButton href="/settings">
|
||||||
<Cog className="h-4 w-4" />
|
<Cog className="h-4 w-4" />
|
||||||
Settings
|
Settings
|
||||||
</NavButton>
|
</NavButton>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{isSelfHosted() || session?.user.isAdmin ? (
|
{isSelfHosted() || session?.user.isAdmin ? (
|
||||||
<NavButton href="/admin">
|
<NavButton href="/admin">
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useTeam } from "~/providers/team-context";
|
import { useTeam } from "~/providers/team-context";
|
||||||
import { SettingsNavButton } from "../dev-settings/settings-nav-button";
|
import { SettingsNavButton } from "../dev-settings/settings-nav-button";
|
||||||
|
import { isCloud } from "~/utils/common";
|
||||||
|
|
||||||
export const dynamic = "force-static";
|
export const dynamic = "force-static";
|
||||||
|
|
||||||
@@ -16,8 +17,10 @@ export default function ApiKeysPage({
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="font-bold text-lg">Settings</h1>
|
<h1 className="font-bold text-lg">Settings</h1>
|
||||||
<div className="flex gap-4 mt-4">
|
<div className="flex gap-4 mt-4">
|
||||||
|
{isCloud() ? (
|
||||||
<SettingsNavButton href="/settings">Usage</SettingsNavButton>
|
<SettingsNavButton href="/settings">Usage</SettingsNavButton>
|
||||||
{currentIsAdmin ? (
|
) : null}
|
||||||
|
{currentIsAdmin && isCloud() ? (
|
||||||
<SettingsNavButton href="/settings/billing">
|
<SettingsNavButton href="/settings/billing">
|
||||||
Billing
|
Billing
|
||||||
</SettingsNavButton>
|
</SettingsNavButton>
|
||||||
|
@@ -1,10 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@unsend/ui/src/button";
|
import { isCloud } from "~/utils/common";
|
||||||
import { api } from "~/trpc/react";
|
|
||||||
import UsagePage from "./usage/usage";
|
import UsagePage from "./usage/usage";
|
||||||
|
import InviteTeamMember from "./team/invite-team-member";
|
||||||
|
import TeamMembersList from "./team/team-members-list";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
|
if (!isCloud()) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-end ">
|
||||||
|
<InviteTeamMember />
|
||||||
|
</div>
|
||||||
|
<TeamMembersList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UsagePage />
|
<UsagePage />
|
||||||
|
@@ -33,6 +33,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@unsend/ui/src/form";
|
} from "@unsend/ui/src/form";
|
||||||
import { useTeam } from "~/providers/team-context";
|
import { useTeam } from "~/providers/team-context";
|
||||||
|
import { isCloud, isSelfHosted } from "~/utils/common";
|
||||||
|
|
||||||
const inviteTeamMemberSchema = z.object({
|
const inviteTeamMemberSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@@ -47,6 +48,7 @@ type FormData = z.infer<typeof inviteTeamMemberSchema>;
|
|||||||
|
|
||||||
export default function InviteTeamMember() {
|
export default function InviteTeamMember() {
|
||||||
const { currentIsAdmin } = useTeam();
|
const { currentIsAdmin } = useTeam();
|
||||||
|
const { data: domains } = api.domain.domains.useQuery();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@@ -60,7 +62,16 @@ export default function InviteTeamMember() {
|
|||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const createInvite = api.team.createTeamInvite.useMutation({
|
const createInvite = api.team.createTeamInvite.useMutation();
|
||||||
|
|
||||||
|
function onSubmit(values: FormData) {
|
||||||
|
createInvite.mutate(
|
||||||
|
{
|
||||||
|
email: values.email,
|
||||||
|
role: values.role,
|
||||||
|
sendEmail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
form.reset();
|
form.reset();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -68,15 +79,36 @@ export default function InviteTeamMember() {
|
|||||||
toast.success("Invitation sent successfully");
|
toast.success("Invitation sent successfully");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
toast.error(error.message || "Failed to send invitation");
|
toast.error(error.message || "Failed to send invitation");
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function onSubmit(values: FormData) {
|
async function onCopyLink() {
|
||||||
createInvite.mutate({
|
createInvite.mutate(
|
||||||
email: values.email,
|
{
|
||||||
role: values.role,
|
email: form.getValues("email"),
|
||||||
});
|
role: form.getValues("role"),
|
||||||
|
sendEmail: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (invite) => {
|
||||||
|
void utils.team.getTeamInvites.invalidate();
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${location.origin}/join-team?inviteId=${invite.id}`
|
||||||
|
);
|
||||||
|
form.reset();
|
||||||
|
setOpen(false);
|
||||||
|
toast.success("Invitation link copied to clipboard");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(error.message || "Failed to copy invitation link");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentIsAdmin) {
|
if (!currentIsAdmin) {
|
||||||
@@ -91,7 +123,7 @@ export default function InviteTeamMember() {
|
|||||||
Invite Member
|
Invite Member
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className=" max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Invite Team Member</DialogTitle>
|
<DialogTitle>Invite Team Member</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -152,6 +184,13 @@ export default function InviteTeamMember() {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{isSelfHosted() && domains?.length ? (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Will use{" "}
|
||||||
|
<span className="font-bold">hello@{domains[0]?.name}</span> to
|
||||||
|
send invitation
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -160,15 +199,24 @@ export default function InviteTeamMember() {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={createInvite.isPending}
|
||||||
|
isLoading={createInvite.isPending}
|
||||||
|
className="w-[150px]"
|
||||||
|
onClick={form.handleSubmit(onCopyLink)}
|
||||||
|
>
|
||||||
|
Copy Invitation
|
||||||
|
</Button>
|
||||||
|
{isCloud() || domains?.length ? (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={createInvite.isPending}
|
disabled={createInvite.isPending}
|
||||||
showSpinner
|
|
||||||
isLoading={createInvite.isPending}
|
isLoading={createInvite.isPending}
|
||||||
className="w-[150px]"
|
className="w-[150px]"
|
||||||
>
|
>
|
||||||
{createInvite.isPending ? "Sending..." : "Send Invitation"}
|
Send Invitation
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@@ -3,13 +3,14 @@
|
|||||||
import { Button } from "@unsend/ui/src/button";
|
import { Button } from "@unsend/ui/src/button";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { toast } from "@unsend/ui/src/toaster";
|
import { toast } from "@unsend/ui/src/toaster";
|
||||||
import { RotateCw } from "lucide-react";
|
import { Copy, RotateCw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@unsend/ui/src/tooltip";
|
} from "@unsend/ui/src/tooltip";
|
||||||
|
import { isSelfHosted } from "~/utils/common";
|
||||||
|
|
||||||
export const ResendTeamInvite: React.FC<{
|
export const ResendTeamInvite: React.FC<{
|
||||||
invite: { id: string; email: string };
|
invite: { id: string; email: string };
|
||||||
@@ -44,6 +45,28 @@ export const ResendTeamInvite: React.FC<{
|
|||||||
<p>Resend invite</p>
|
<p>Resend invite</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
{isSelfHosted() ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${location.origin}/join-team?inviteId=${invite.id}`
|
||||||
|
);
|
||||||
|
toast.success(`Invite link copied to clipboard`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Copy invite link</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,11 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { Button } from "@unsend/ui/src/button";
|
import { Button } from "@unsend/ui/src/button";
|
||||||
import { Spinner } from "@unsend/ui/src/spinner";
|
import { Spinner } from "@unsend/ui/src/spinner";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/trpc/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { toast } from "@unsend/ui/src/toaster";
|
import { toast } from "@unsend/ui/src/toaster";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -23,19 +21,18 @@ type Invite = NonNullable<
|
|||||||
RouterOutputs["invitation"]["getUserInvites"]
|
RouterOutputs["invitation"]["getUserInvites"]
|
||||||
>[number];
|
>[number];
|
||||||
|
|
||||||
const FormSchema = z.object({
|
|
||||||
name: z.string().min(2, {
|
|
||||||
message: "Team name must be at least 2 characters.",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function JoinTeam({
|
export default function JoinTeam({
|
||||||
showCreateTeam = false,
|
showCreateTeam = false,
|
||||||
}: {
|
}: {
|
||||||
showCreateTeam?: boolean;
|
showCreateTeam?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const inviteId = searchParams.get("inviteId");
|
||||||
|
|
||||||
const { data: invites, status: invitesStatus } =
|
const { data: invites, status: invitesStatus } =
|
||||||
api.invitation.getUserInvites.useQuery();
|
api.invitation.getUserInvites.useQuery({
|
||||||
|
inviteId,
|
||||||
|
});
|
||||||
const joinTeamMutation = api.invitation.acceptTeamInvite.useMutation();
|
const joinTeamMutation = api.invitation.acceptTeamInvite.useMutation();
|
||||||
const [selectedInvite, setSelectedInvite] = useState<Invite | null>(null);
|
const [selectedInvite, setSelectedInvite] = useState<Invite | null>(null);
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
@@ -73,8 +70,11 @@ export default function JoinTeam({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!invites?.length)
|
if (!invites?.length) {
|
||||||
return <div className="text-center text-xl">No invites found</div>;
|
return !showCreateTeam ? (
|
||||||
|
<div className="text-center text-xl">No invites found</div>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@@ -5,55 +5,22 @@ import { env } from "~/env";
|
|||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
|
|
||||||
export const invitationRouter = createTRPCRouter({
|
export const invitationRouter = createTRPCRouter({
|
||||||
createTeam: protectedProcedure
|
getUserInvites: protectedProcedure
|
||||||
.input(z.object({ name: z.string() }))
|
.input(
|
||||||
.mutation(async ({ ctx, input }) => {
|
z.object({
|
||||||
const teams = await ctx.db.team.findMany({
|
inviteId: z.string().optional().nullable(),
|
||||||
where: {
|
})
|
||||||
teamUsers: {
|
)
|
||||||
some: {
|
.query(async ({ ctx, input }) => {
|
||||||
userId: ctx.session.user.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (teams.length > 0) {
|
|
||||||
console.log("User already has a team");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!env.NEXT_PUBLIC_IS_CLOUD) {
|
|
||||||
const _team = await ctx.db.team.findFirst();
|
|
||||||
if (_team) {
|
|
||||||
throw new TRPCError({
|
|
||||||
message: "Can't have multiple teams in self hosted version",
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.db.team.create({
|
|
||||||
data: {
|
|
||||||
name: input.name,
|
|
||||||
teamUsers: {
|
|
||||||
create: {
|
|
||||||
userId: ctx.session.user.id,
|
|
||||||
role: "ADMIN",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
|
|
||||||
getUserInvites: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
if (!ctx.session.user.email) {
|
if (!ctx.session.user.email) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const invites = await ctx.db.teamInvite.findMany({
|
const invites = await ctx.db.teamInvite.findMany({
|
||||||
where: {
|
where: {
|
||||||
email: ctx.session.user.email,
|
...(input.inviteId
|
||||||
|
? { id: input.inviteId }
|
||||||
|
: { email: ctx.session.user.email }),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
team: true,
|
team: true,
|
||||||
|
@@ -9,6 +9,7 @@ import {
|
|||||||
teamAdminProcedure,
|
teamAdminProcedure,
|
||||||
} from "~/server/api/trpc";
|
} from "~/server/api/trpc";
|
||||||
import { sendTeamInviteEmail } from "~/server/mailer";
|
import { sendTeamInviteEmail } from "~/server/mailer";
|
||||||
|
import send from "~/server/public-api/api/emails/send-email";
|
||||||
|
|
||||||
export const teamRouter = createTRPCRouter({
|
export const teamRouter = createTRPCRouter({
|
||||||
createTeam: protectedProcedure
|
createTeam: protectedProcedure
|
||||||
@@ -97,8 +98,21 @@ export const teamRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
createTeamInvite: teamAdminProcedure
|
createTeamInvite: teamAdminProcedure
|
||||||
.input(z.object({ email: z.string(), role: z.enum(["MEMBER", "ADMIN"]) }))
|
.input(
|
||||||
|
z.object({
|
||||||
|
email: z.string(),
|
||||||
|
role: z.enum(["MEMBER", "ADMIN"]),
|
||||||
|
sendEmail: z.boolean().default(true),
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!input.email) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Email is required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const user = await ctx.db.user.findUnique({
|
const user = await ctx.db.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email: input.email,
|
email: input.email,
|
||||||
@@ -125,7 +139,9 @@ export const teamRouter = createTRPCRouter({
|
|||||||
|
|
||||||
const teamUrl = `${env.NEXTAUTH_URL}/join-team?inviteId=${teamInvite.id}`;
|
const teamUrl = `${env.NEXTAUTH_URL}/join-team?inviteId=${teamInvite.id}`;
|
||||||
|
|
||||||
|
if (input.sendEmail) {
|
||||||
await sendTeamInviteEmail(input.email, teamUrl, ctx.team.name);
|
await sendTeamInviteEmail(input.email, teamUrl, ctx.team.name);
|
||||||
|
}
|
||||||
|
|
||||||
return teamInvite;
|
return teamInvite;
|
||||||
}),
|
}),
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import { Unsend } from "unsend";
|
import { Unsend } from "unsend";
|
||||||
|
import { isSelfHosted } from "~/utils/common";
|
||||||
|
import { db } from "./db";
|
||||||
|
import { getDomains } from "./service/domain-service";
|
||||||
|
import { sendEmail } from "./service/email-service";
|
||||||
|
|
||||||
let unsend: Unsend | undefined;
|
let unsend: Unsend | undefined;
|
||||||
|
|
||||||
@@ -54,7 +58,37 @@ async function sendMail(
|
|||||||
text: string,
|
text: string,
|
||||||
html: string
|
html: string
|
||||||
) {
|
) {
|
||||||
if (env.UNSEND_API_KEY && env.FROM_EMAIL) {
|
if (isSelfHosted()) {
|
||||||
|
console.log("Sending email using self hosted");
|
||||||
|
/*
|
||||||
|
Self hosted so checking if we can send using one of the available domain
|
||||||
|
Assuming self hosted will have only one team
|
||||||
|
TODO: fix this
|
||||||
|
*/
|
||||||
|
const team = await db.team.findFirst({});
|
||||||
|
if (!team) {
|
||||||
|
console.error("No team found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domains = await getDomains(team.id);
|
||||||
|
|
||||||
|
if (domains.length === 0 || !domains[0]) {
|
||||||
|
console.error("No domains found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const domain = domains[0];
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
teamId: team.id,
|
||||||
|
to: email,
|
||||||
|
from: `hello@${domain.name}`,
|
||||||
|
subject,
|
||||||
|
text,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
} else if (env.UNSEND_API_KEY && env.FROM_EMAIL) {
|
||||||
const resp = await getClient().emails.send({
|
const resp = await getClient().emails.send({
|
||||||
to: email,
|
to: email,
|
||||||
from: env.FROM_EMAIL,
|
from: env.FROM_EMAIL,
|
||||||
|
@@ -165,6 +165,17 @@ export async function deleteDomain(id: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getDomains(teamId: number) {
|
||||||
|
return db.domain.findMany({
|
||||||
|
where: {
|
||||||
|
teamId,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function getDmarcRecord(domain: string) {
|
async function getDmarcRecord(domain: string) {
|
||||||
try {
|
try {
|
||||||
const dmarcRecord = await dnsResolveTxt(`_dmarc.${domain}`);
|
const dmarcRecord = await dnsResolveTxt(`_dmarc.${domain}`);
|
||||||
|
@@ -201,6 +201,8 @@ async function executeEmail(
|
|||||||
? JSON.parse(email.attachments)
|
? JSON.parse(email.attachments)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
console.log(`Domain: ${JSON.stringify(domain)}`);
|
||||||
|
|
||||||
const configurationSetName = await getConfigurationSetName(
|
const configurationSetName = await getConfigurationSetName(
|
||||||
domain?.clickTracking ?? false,
|
domain?.clickTracking ?? false,
|
||||||
domain?.openTracking ?? false,
|
domain?.openTracking ?? false,
|
||||||
@@ -208,7 +210,6 @@ async function executeEmail(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!configurationSetName) {
|
if (!configurationSetName) {
|
||||||
console.log(`[EmailQueueService]: Configuration set not found, skipping`);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -27,6 +27,7 @@ export class SesSettingsService {
|
|||||||
region = env.AWS_DEFAULT_REGION
|
region = env.AWS_DEFAULT_REGION
|
||||||
): Promise<SesSetting | null> {
|
): Promise<SesSetting | null> {
|
||||||
await this.checkInitialized();
|
await this.checkInitialized();
|
||||||
|
|
||||||
if (this.cache[region]) {
|
if (this.cache[region]) {
|
||||||
return this.cache[region] as SesSetting;
|
return this.cache[region] as SesSetting;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user