Improve Self host setup (#30)

* Add self host setup

* Improve blunders

* Move to bull mq

* More changes

* Add example code for sending test emails
This commit is contained in:
KM Koushik
2024-06-24 08:21:37 +10:00
committed by GitHub
parent 8a2769621c
commit f77a8829be
67 changed files with 1771 additions and 688 deletions

View File

@@ -0,0 +1,40 @@
"use client";
import { Button } from "@unsend/ui/src/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@unsend/ui/src/dialog";
import { Plus } from "lucide-react";
import { useState } from "react";
import { AddSesSettingsForm } from "~/components/settings/AddSesSettings";
export default function AddSesConfiguration() {
const [open, setOpen] = useState(false);
return (
<Dialog
open={open}
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-1" />
Add SES configuration
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a new SES configuration</DialogTitle>
</DialogHeader>
<div className="py-2">
<AddSesSettingsForm onSuccess={() => setOpen(false)} />
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,19 @@
"use client";
import AddSesConfiguration from "./add-ses-configuration";
import SesConfigurations from "./ses-configurations";
export default function ApiKeysPage() {
return (
<div>
<div className="flex justify-between items-center">
<h1 className="font-bold text-lg">Admin</h1>
<AddSesConfiguration />
</div>
<div className="mt-10">
<p className="font-semibold mb-4">SES Configurations</p>
<SesConfigurations />
</div>
</div>
);
}

View File

@@ -0,0 +1,65 @@
"use client";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@unsend/ui/src/table";
import { formatDistanceToNow } from "date-fns";
import { api } from "~/trpc/react";
import Spinner from "@unsend/ui/src/spinner";
export default function SesConfigurations() {
const sesSettingsQuery = api.admin.getSesSettings.useQuery();
return (
<div className="">
<div className="border rounded-xl">
<Table className="">
<TableHeader className="">
<TableRow className=" bg-muted/30">
<TableHead className="rounded-tl-xl">Region</TableHead>
<TableHead>Callback URL</TableHead>
<TableHead>Callback status</TableHead>
<TableHead>Created at</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sesSettingsQuery.isLoading ? (
<TableRow className="h-32">
<TableCell colSpan={6} className="text-center py-4">
<Spinner
className="w-6 h-6 mx-auto"
innerSvgClass="stroke-primary"
/>
</TableCell>
</TableRow>
) : sesSettingsQuery.data?.length === 0 ? (
<TableRow className="h-32">
<TableCell colSpan={6} className="text-center py-4">
<p>No SES configurations added</p>
</TableCell>
</TableRow>
) : (
sesSettingsQuery.data?.map((sesSetting) => (
<TableRow key={sesSetting.id}>
<TableCell>{sesSetting.region}</TableCell>
<TableCell>{sesSetting.callbackUrl}</TableCell>
<TableCell>
{sesSetting.callbackSuccess ? "Success" : "Failed"}
</TableCell>
<TableCell>
{formatDistanceToNow(sesSetting.createdAt)} ago
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,192 @@
"use client";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { LogoutButton, NavButton } from "./nav-button";
import {
BookOpenText,
BookUser,
CircleUser,
Code,
Globe,
Home,
LayoutDashboard,
LineChart,
Mail,
Menu,
Package,
Package2,
Server,
ShoppingCart,
Users,
Volume2,
} from "lucide-react";
import { env } from "~/env";
import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
import { Button } from "@unsend/ui/src/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@unsend/ui/src/dropdown-menu";
export function DashboardLayout({ children }: { children: React.ReactNode }) {
const { data: session } = useSession();
return (
<div className="flex min-h-screen w-full h-full">
<div className="hidden bg-muted/20 md:block md:w-[280px]">
<div className="flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 gap-4 items-center px-4 lg:h-[60px] lg:px-6">
<Link href="/" className="flex items-center gap-2 font-semibold">
<span className=" text-lg">Unsend</span>
</Link>
<span className="text-[10px] text-muted-foreground bg-muted p-0.5 px-2 rounded-full">
Early access
</span>
</div>
<div className="flex-1 h-full">
<nav className=" flex-1 h-full flex-col justify-between items-center px-2 text-sm font-medium lg:px-4">
<div>
<NavButton href="/dashboard">
<LayoutDashboard className="h-4 w-4" />
Dashboard
</NavButton>
<NavButton href="/emails">
<Mail className="h-4 w-4" />
Emails
</NavButton>
<NavButton href="/domains">
<Globe className="h-4 w-4" />
Domains
</NavButton>
<NavButton href="/contacts" comingSoon>
<BookUser className="h-4 w-4" />
Contacts
</NavButton>
<NavButton href="/contacts" comingSoon>
<Volume2 className="h-4 w-4" />
Marketing
</NavButton>
<NavButton href="/api-keys">
<Code className="h-4 w-4" />
Developer settings
</NavButton>
{!env.NEXT_PUBLIC_IS_CLOUD || session?.user.isAdmin ? (
<NavButton href="/admin">
<Server className="h-4 w-4" />
Admin
</NavButton>
) : null}
</div>
<div className=" absolute bottom-10 p-4 flex flex-col gap-2">
<Link
href="https://docs.unsend.dev"
target="_blank"
className="flex gap-2 items-center hover:text-primary text-muted-foreground"
>
<BookOpenText className="h-4 w-4" />
<span className="">Docs</span>
</Link>
<LogoutButton />
</div>
</nav>
</div>
<div className="mt-auto p-4"></div>
</div>
</div>
<div className="flex flex-1 flex-col">
<header className="flex h-14 items-center gap-4 md:hidden bg-muted/20 px-4 lg:h-[60px] lg:px-6">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="flex flex-col">
<nav className="grid gap-2 text-lg font-medium">
<Link
href="#"
className="flex items-center gap-2 text-lg font-semibold"
>
<Package2 className="h-6 w-6" />
<span className="sr-only">Acme Inc</span>
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<Home className="h-5 w-5" />
Dashboard
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl bg-muted px-3 py-2 text-foreground hover:text-foreground"
>
<ShoppingCart className="h-5 w-5" />
Orders
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<Package className="h-5 w-5" />
Products
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<Users className="h-5 w-5" />
Customers
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<LineChart className="h-5 w-5" />
Analytics
</Link>
</nav>
<div className="mt-auto"></div>
</SheetContent>
</Sheet>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" className="rounded-full">
<CircleUser className="h-5 w-5" />
<span className="sr-only">Toggle user menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
<main className="flex-1 overflow-y-auto h-full">
<div className="flex flex-col gap-4 p-4 w-full lg:max-w-6xl mx-auto lg:gap-6 lg:p-6">
{children}
</div>
</main>
</div>
</div>
);
}

View File

@@ -26,6 +26,7 @@ import DeleteDomain from "./delete-domain";
import SendTestMail from "./send-test-mail";
import { Button } from "@unsend/ui/src/button";
import Link from "next/link";
import { toast } from "@unsend/ui/src/toaster";
export default function DomainItemPage({
params,
@@ -245,7 +246,8 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
{ id: domain.id, clickTracking: !clickTracking },
{
onSuccess: () => {
utils.domain.domains.invalidate();
utils.domain.invalidate();
toast.success("Click tracking updated");
},
}
);
@@ -257,7 +259,8 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
{ id: domain.id, openTracking: !openTracking },
{
onSuccess: () => {
utils.domain.domains.invalidate();
utils.domain.invalidate();
toast.success("Open tracking updated");
},
}
);

View File

@@ -14,6 +14,8 @@ import { Domain } from "@prisma/client";
import { toast } from "@unsend/ui/src/toaster";
import { SendHorizonal } from "lucide-react";
import { Code } from "@unsend/ui/src/code";
import { useSession } from "next-auth/react";
import { getSendTestEmailCode } from "~/lib/constants/example-codes";
const jsCode = `const requestOptions = {
method: "POST",
@@ -112,6 +114,8 @@ export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
const sendTestEmailFromDomainMutation =
api.domain.sendTestEmailFromDomain.useMutation();
const { data: session } = useSession();
const utils = api.useUtils();
function handleSendTestEmail() {
@@ -145,12 +149,14 @@ export const SendTestMail: React.FC<{ domain: Domain }> = ({ domain }) => {
<DialogTitle>Send test email</DialogTitle>
</DialogHeader>
<Code
codeBlocks={[
{ language: "js", code: jsCode },
{ language: "ruby", code: rubyCode },
{ language: "php", code: phpCode },
{ language: "python", code: pythonCode },
]}
codeBlocks={getSendTestEmailCode({
from: `hello@${domain.name}`,
to: session?.user?.email || "",
subject: "Unsend test email",
body: "hello,\\n\\nUnsend is the best open source sending platform",
bodyHtml:
"<p>hello,</p><p>Unsend is the best open source sending platform<p><p>check out <a href='https://unsend.dev'>unsend.dev</a>",
})}
codeClassName="max-w-[38rem] h-[20rem]"
/>
<div className="flex justify-end w-full">

View File

@@ -27,17 +27,37 @@ import * as tldts from "tldts";
import { z } from "zod";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@unsend/ui/src/select";
import { toast } from "@unsend/ui/src/toaster";
const domainSchema = z.object({
domain: z.string({ required_error: "Domain is required" }),
region: z.string({ required_error: "Region is required" }).min(1, {
message: "Region is required",
}),
domain: z.string({ required_error: "Domain is required" }).min(1, {
message: "Domain is required",
}),
});
export default function AddDomain() {
const [open, setOpen] = useState(false);
const regionQuery = api.domain.getAvailableRegions.useQuery();
const addDomainMutation = api.domain.createDomain.useMutation();
const domainForm = useForm<z.infer<typeof domainSchema>>({
resolver: zodResolver(domainSchema),
defaultValues: {
region: "",
domain: "",
},
});
const utils = api.useUtils();
@@ -56,6 +76,7 @@ export default function AddDomain() {
addDomainMutation.mutate(
{
name: values.domain,
region: values.region,
},
{
onSuccess: async (data) => {
@@ -63,6 +84,9 @@ export default function AddDomain() {
await router.push(`/domains/${data.id}`);
setOpen(false);
},
onError: async (error) => {
toast.error(error.message);
},
}
);
}
@@ -108,6 +132,41 @@ export default function AddDomain() {
</FormItem>
)}
/>
<FormField
control={domainForm.control}
name="region"
render={({ field, formState }) => (
<FormItem>
<FormLabel>Region</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
disabled={regionQuery.isLoading}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select region" />
</SelectTrigger>
</FormControl>
<SelectContent>
{regionQuery.data?.map((region) => (
<SelectItem value={region} key={region}>
{region}
</SelectItem>
))}
</SelectContent>
</Select>
{formState.errors.region ? (
<FormMessage />
) : (
<FormDescription>
Select the region from where the email is sent{" "}
</FormDescription>
)}
</FormItem>
)}
/>
<div className="flex justify-end">
<Button
className=" w-[100px] bg-white hover:bg-gray-100 focus:bg-gray-100"

View File

@@ -95,9 +95,7 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => {
<div>
<p className="text-sm text-muted-foreground">Region</p>
<p className="text-sm flex items-center gap-2">
<span className="text-2xl">🇺🇸</span> {domain.region}
</p>
<p className="text-sm flex items-center gap-2">{domain.region}</p>
</div>
</div>
<div className="flex flex-col gap-6">

View File

@@ -1,14 +1,22 @@
"use client";
import { UAParser } from "ua-parser-js";
import { api } from "~/trpc/react";
import { Separator } from "@unsend/ui/src/separator";
import { EmailStatusBadge, EmailStatusIcon } from "./email-status-badge";
import { formatDate } from "date-fns";
import { EmailStatus } from "@prisma/client";
import { JsonValue } from "@prisma/client/runtime/library";
import { SesBounce, SesDeliveryDelay } from "~/types/aws-types";
import {
SesBounce,
SesClick,
SesComplaint,
SesDeliveryDelay,
SesOpen,
} from "~/types/aws-types";
import {
BOUNCE_ERROR_MESSAGES,
COMPLAINT_ERROR_MESSAGES,
DELIVERY_DELAY_ERRORS,
} from "~/lib/constants/ses-errors";
@@ -39,7 +47,7 @@ export default function EmailDetails({ emailId }: { emailId: string }) {
<span className="w-[65px] text-muted-foreground ">Subject</span>
<span>{emailQuery.data?.subject}</span>
</div>
<div className=" dark:bg-slate-200 h-[300px] overflow-auto text-black rounded">
<div className=" dark:bg-slate-200 h-[250px] overflow-auto text-black rounded">
<div
className="px-4 py-4 overflow-auto"
dangerouslySetInnerHTML={{ __html: emailQuery.data?.html ?? "" }}
@@ -47,17 +55,20 @@ export default function EmailDetails({ emailId }: { emailId: string }) {
</div>
</div>
<div className=" border rounded-lg w-full ">
<div className=" p-4 flex flex-col gap-8">
<div className=" p-4 flex flex-col gap-8 w-full">
<div className="font-medium">Events History</div>
<div className="flex items-stretch px-4">
<div className="flex items-stretch px-4 w-full">
<div className="border-r border-dashed" />
<div className="flex flex-col gap-12">
<div className="flex flex-col gap-12 w-full">
{emailQuery.data?.emailEvents.map((evt) => (
<div key={evt.status} className="flex gap-5 items-start">
<div
key={evt.status}
className="flex gap-5 items-start w-full"
>
<div className=" -ml-2.5">
<EmailStatusIcon status={evt.status} />
</div>
<div className="-mt-[0.125rem]">
<div className="-mt-[0.125rem] w-full">
<div className=" capitalize font-medium">
<EmailStatusBadge status={evt.status} />
</div>
@@ -104,26 +115,88 @@ const EmailStatusText = ({
_errorData.bounceType;
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-4 w-full">
<p>{getErrorMessage(_errorData)}</p>
<div className="flex gap-2 ">
<div className="w-1/2">
<p className="text-sm text-muted-foreground">Type </p>
<p>{_errorData.bounceType}</p>
<div className="rounded-xl p-4 bg-muted/20 flex flex-col gap-4">
<div className="flex gap-2 w-full">
<div className="w-1/2">
<p className="text-sm text-muted-foreground">Type</p>
<p>{_errorData.bounceType}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Sub Type</p>
<p>{_errorData.bounceSubType}</p>
</div>
</div>
<div>
<p className="text-sm text-muted-foreground">Sub Type </p>
<p>{_errorData.bounceSubType}</p>
<p className="text-sm text-muted-foreground">SMTP response</p>
<p>{_errorData.bouncedRecipients[0]?.diagnosticCode}</p>
</div>
</div>
<div>
<p className="text-sm text-muted-foreground">SMTP response</p>
<p>{_errorData.bouncedRecipients[0]?.diagnosticCode}</p>
</div>
</div>
);
} else if (status === "FAILED") {
const _errorData = data as unknown as { error: string };
return <div>{_errorData.error}</div>;
} else if (status === "OPENED") {
const _data = data as unknown as SesOpen;
const userAgent = getUserAgent(_data.userAgent);
return (
<div className="w-full rounded-xl p-4 bg-muted/20 mt-4">
<div className="flex w-full ">
{userAgent.os.name ? (
<div className="w-1/2">
<p className="text-sm text-muted-foreground">OS</p>
<p>{userAgent.os.name}</p>
</div>
) : null}
{userAgent.browser.name ? (
<div>
<p className="text-sm text-muted-foreground">Browser</p>
<p>{userAgent.browser.name}</p>
</div>
) : null}
</div>
</div>
);
} else if (status === "CLICKED") {
const _data = data as unknown as SesClick;
const userAgent = getUserAgent(_data.userAgent);
return (
<div className="w-full mt-4 flex flex-col gap-4 rounded-xl p-4 bg-muted/20">
<div className="flex w-full ">
{userAgent.os.name ? (
<div className="w-1/2">
<p className="text-sm text-muted-foreground">OS </p>
<p>{userAgent.os.name}</p>
</div>
) : null}
{userAgent.browser.name ? (
<div>
<p className="text-sm text-muted-foreground">Browser </p>
<p>{userAgent.browser.name}</p>
</div>
) : null}
</div>
<div className="w-full">
<p className="text-sm text-muted-foreground">URL</p>
<p>{_data.link}</p>
</div>
</div>
);
} else if (status === "COMPLAINED") {
const _errorData = data as unknown as SesComplaint;
return (
<div className="flex flex-col gap-4 w-full">
<p>{getComplaintMessage(_errorData.complaintFeedbackType)}</p>
</div>
);
}
return <div>{status}</div>;
return <div className="w-full">{status}</div>;
};
const getErrorMessage = (data: SesBounce) => {
@@ -148,3 +221,18 @@ const getErrorMessage = (data: SesBounce) => {
return BOUNCE_ERROR_MESSAGES.Undetermined;
}
};
const getComplaintMessage = (errorType: string) => {
return COMPLAINT_ERROR_MESSAGES[
errorType as keyof typeof COMPLAINT_ERROR_MESSAGES
];
};
const getUserAgent = (userAgent: string) => {
const parser = new UAParser(userAgent);
return {
browser: parser.getBrowser(),
os: parser.getOS(),
device: parser.getDevice(),
};
};

View File

@@ -190,6 +190,7 @@ const EmailIcon: React.FC<{ status: EmailStatus }> = ({ status }) => {
// </div>
);
case "BOUNCED":
case "FAILED":
return (
// <div className="border border-red-600/60 p-2 rounded-lg bg-red-500/10">
<MailX className="w-6 h-6 text-red-900" />

View File

@@ -12,6 +12,7 @@ export const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({
badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
break;
case "BOUNCED":
case "FAILED":
badgeColor = "bg-red-500/10 text-red-600 border-red-600/10";
break;
case "CLICKED":
@@ -51,6 +52,7 @@ export const EmailStatusIcon: React.FC<{ status: EmailStatus }> = ({
insideColor = "bg-emerald-500";
break;
case "BOUNCED":
case "FAILED":
outsideColor = "bg-red-500/30";
insideColor = "bg-red-500";
break;

View File

@@ -1,37 +1,6 @@
import Link from "next/link";
import {
BookOpenText,
BookUser,
CircleUser,
Code,
Globe,
Home,
LayoutDashboard,
LineChart,
LogOut,
Mail,
Menu,
Package,
Package2,
ShoppingCart,
Users,
Volume2,
} from "lucide-react";
import { Button } from "@unsend/ui/src/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@unsend/ui/src/dropdown-menu";
import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
import { LogoutButton, NavButton } from "./nav-button";
import { DashboardProvider } from "~/providers/dashboard-provider";
import { NextAuthProvider } from "~/providers/next-auth";
import { DashboardLayout } from "./dasboard-layout";
export const dynamic = "force-static";
@@ -43,158 +12,7 @@ export default function AuthenticatedDashboardLayout({
return (
<NextAuthProvider>
<DashboardProvider>
<div className="flex min-h-screen w-full h-full">
<div className="hidden bg-muted/20 md:block md:w-[280px]">
<div className="flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 gap-4 items-center px-4 lg:h-[60px] lg:px-6">
<Link
href="/"
className="flex items-center gap-2 font-semibold"
>
<span className=" text-lg">Unsend</span>
</Link>
<span className="text-[10px] text-muted-foreground bg-muted p-0.5 px-2 rounded-full">
Early access
</span>
</div>
<div className="flex-1 h-full">
<nav className=" flex-1 h-full flex-col justify-between items-center px-2 text-sm font-medium lg:px-4">
<div>
<NavButton href="/dashboard">
<LayoutDashboard className="h-4 w-4" />
Dashboard
</NavButton>
<NavButton href="/emails">
<Mail className="h-4 w-4" />
Emails
</NavButton>
<NavButton href="/domains">
<Globe className="h-4 w-4" />
Domains
</NavButton>
<NavButton href="/contacts" comingSoon>
<BookUser className="h-4 w-4" />
Contacts
</NavButton>
<NavButton href="/contacts" comingSoon>
<Volume2 className="h-4 w-4" />
Marketing
</NavButton>
<NavButton href="/api-keys">
<Code className="h-4 w-4" />
Developer settings
</NavButton>
</div>
<div className=" absolute bottom-10 p-4 flex flex-col gap-2">
<Link
href="https://docs.unsend.dev"
target="_blank"
className="flex gap-2 items-center hover:text-primary text-muted-foreground"
>
<BookOpenText className="h-4 w-4" />
<span className="">Docs</span>
</Link>
<LogoutButton />
</div>
</nav>
</div>
<div className="mt-auto p-4"></div>
</div>
</div>
<div className="flex flex-1 flex-col">
<header className="flex h-14 items-center gap-4 md:hidden bg-muted/20 px-4 lg:h-[60px] lg:px-6">
<Sheet>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="shrink-0 md:hidden"
>
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side="left" className="flex flex-col">
<nav className="grid gap-2 text-lg font-medium">
<Link
href="#"
className="flex items-center gap-2 text-lg font-semibold"
>
<Package2 className="h-6 w-6" />
<span className="sr-only">Acme Inc</span>
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<Home className="h-5 w-5" />
Dashboard
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl bg-muted px-3 py-2 text-foreground hover:text-foreground"
>
<ShoppingCart className="h-5 w-5" />
Orders
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<Package className="h-5 w-5" />
Products
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<Users className="h-5 w-5" />
Customers
</Link>
<Link
href="#"
className="mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 text-muted-foreground hover:text-foreground"
>
<LineChart className="h-5 w-5" />
Analytics
</Link>
</nav>
<div className="mt-auto"></div>
</SheetContent>
</Sheet>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="secondary"
size="icon"
className="rounded-full"
>
<CircleUser className="h-5 w-5" />
<span className="sr-only">Toggle user menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
<main className="flex-1 overflow-y-auto h-full">
<div className="flex flex-col gap-4 p-4 w-full lg:max-w-6xl mx-auto lg:gap-6 lg:p-6">
{children}
</div>
</main>
</div>
</div>
<DashboardLayout>{children}</DashboardLayout>
</DashboardProvider>
</NextAuthProvider>
);

View File

@@ -1,8 +1,5 @@
import { setupAws } from "~/server/aws/setup";
export const dynamic = "force-dynamic";
export async function GET() {
await setupAws();
return Response.json({ data: "Healthy" });
}

View File

@@ -1,11 +1,11 @@
import { db } from "~/server/db";
import { AppSettingsService } from "~/server/service/app-settings-service";
import { parseSesHook } from "~/server/service/ses-hook-parser";
import { SesSettingsService } from "~/server/service/ses-settings-service";
import { SnsNotificationMessage } from "~/types/aws-types";
import { APP_SETTINGS } from "~/utils/constants";
export async function GET(req: Request) {
console.log("GET", req);
export const dynamic = "force-dynamic";
export async function GET() {
return Response.json({ data: "Hello" });
}
@@ -14,10 +14,6 @@ export async function POST(req: Request) {
console.log(data, data.Message);
if (isFromUnsend(data)) {
return Response.json({ data: "success" });
}
const isEventValid = await checkEventValidity(data);
console.log("isEventValid: ", isEventValid);
@@ -72,26 +68,17 @@ async function handleSubscription(message: any) {
},
});
SesSettingsService.invalidateCache();
return Response.json({ data: "Success" });
}
// A simple check to ensure that the event is from the correct topic
function isFromUnsend({ fromUnsend }: { fromUnsend: boolean }) {
if (fromUnsend) {
return true;
}
return false;
}
// A simple check to ensure that the event is from the correct topic
async function checkEventValidity(message: SnsNotificationMessage) {
const { TopicArn } = message;
const configuredTopicArn = await AppSettingsService.getSetting(
APP_SETTINGS.SNS_TOPIC_ARN
);
const configuredTopicArn = await SesSettingsService.getTopicArns();
if (TopicArn !== configuredTopicArn) {
if (!configuredTopicArn.includes(TopicArn)) {
return false;
}

View File

@@ -6,7 +6,6 @@ import { Toaster } from "@unsend/ui/src/toaster";
import { TRPCReactProvider } from "~/trpc/react";
import { Metadata } from "next";
import { getBoss } from "~/server/service/job-service";
const inter = Inter({
subsets: ["latin"],
@@ -24,12 +23,6 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
/**
* Because I don't know a better way to call this during server startup.
* This is a temporary fix to ensure that the boss is running.
*/
// await getBoss();
return (
<html lang="en">
<body className={`font-sans ${inter.variable}`}>