Add ses hook parser to capture all the events
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
"prisma": "^5.11.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"recharts": "^2.12.5",
|
||||
"server-only": "^0.0.1",
|
||||
"superjson": "^2.2.1",
|
||||
"tldts": "^6.1.16",
|
||||
|
@@ -137,14 +137,27 @@ model ApiKey {
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum EmailStatus {
|
||||
SENT
|
||||
OPENED
|
||||
CLICKED
|
||||
BOUNCED
|
||||
COMPLAINED
|
||||
DELIVERED
|
||||
REJECTED
|
||||
RENDERING_FAILURE
|
||||
DELIVERY_DELAYED
|
||||
}
|
||||
|
||||
model Email {
|
||||
id String @id
|
||||
id String @id @default(cuid())
|
||||
sesEmailId String? @unique
|
||||
to String
|
||||
from String
|
||||
subject String
|
||||
text String?
|
||||
html String?
|
||||
latestStatus String?
|
||||
latestStatus EmailStatus @default(SENT)
|
||||
teamId Int
|
||||
domainId Int?
|
||||
createdAt DateTime @default(now())
|
||||
@@ -155,10 +168,10 @@ model Email {
|
||||
|
||||
model EmailEvent {
|
||||
emailId String
|
||||
status String
|
||||
status EmailStatus
|
||||
data Json?
|
||||
createdAt DateTime @default(now())
|
||||
email Email @relation(fields: [emailId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
email Email @relation(fields: [emailId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([emailId, status])
|
||||
}
|
||||
|
@@ -15,12 +15,15 @@ import {
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { CheckIcon, ClipboardCopy, Plus } from "lucide-react";
|
||||
import { toast } from "@unsend/ui/src/toaster";
|
||||
|
||||
export default function AddApiKey() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [name, setName] = useState("");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const addDomainMutation = api.apiKey.createToken.useMutation();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
@@ -41,9 +44,18 @@ export default function AddApiKey() {
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function copyAndClose() {
|
||||
handleCopy();
|
||||
setApiKey("");
|
||||
setName("");
|
||||
setOpen(false);
|
||||
toast.success("API key copied to clipboard");
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -52,18 +64,34 @@ export default function AddApiKey() {
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Add API Key</Button>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add API Key
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{apiKey ? (
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Copy API key</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-2 bg-gray-200 rounded-lg">{apiKey}</div>
|
||||
<div className="py-2 bg-muted rounded-lg px-2 flex items-center justify-between">
|
||||
<p>{apiKey}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="hover:bg-transparent p-0 cursor-pointer group-hover:opacity-100"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<ClipboardCopy className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={handleCopy}
|
||||
onClick={copyAndClose}
|
||||
disabled={addDomainMutation.isPending}
|
||||
>
|
||||
Close
|
||||
|
@@ -1,5 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@unsend/ui/src/table";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import Link from "next/link";
|
||||
import { api } from "~/trpc/react";
|
||||
|
||||
@@ -8,18 +17,35 @@ export default function ApiList() {
|
||||
|
||||
return (
|
||||
<div className="mt-10">
|
||||
<div className="flex flex-col gap-2">
|
||||
{!apiKeysQuery.isLoading && apiKeysQuery.data?.length ? (
|
||||
apiKeysQuery.data?.map((apiKey) => (
|
||||
<div className="p-2 px-4 border rounded-lg flex justify-between">
|
||||
<p>{apiKey.name}</p>
|
||||
<p>{apiKey.permission}</p>
|
||||
<p>{apiKey.partialToken}</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div>No API keys added</div>
|
||||
)}
|
||||
<div className="border rounded-xl">
|
||||
<Table className="">
|
||||
<TableHeader className="">
|
||||
<TableRow className=" bg-muted/30">
|
||||
<TableHead className="rounded-tl-xl">Name</TableHead>
|
||||
<TableHead>Token</TableHead>
|
||||
<TableHead>Permission</TableHead>
|
||||
<TableHead>Last used</TableHead>
|
||||
<TableHead className="rounded-tr-xl">Created at</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apiKeysQuery.data?.map((apiKey) => (
|
||||
<TableRow key={apiKey.id}>
|
||||
<TableCell>{apiKey.name}</TableCell>
|
||||
<TableCell>{apiKey.partialToken}</TableCell>
|
||||
<TableCell>{apiKey.permission}</TableCell>
|
||||
<TableCell>
|
||||
{apiKey.lastUsed
|
||||
? formatDistanceToNow(apiKey.lastUsed)
|
||||
: "Never"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDistanceToNow(apiKey.createdAt, { addSuffix: true })}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
92
apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
Normal file
92
apps/web/src/app/(dashboard)/dashboard/dashboard-chart.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
AreaChart,
|
||||
Area,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
} from "recharts";
|
||||
|
||||
const data = [
|
||||
{
|
||||
name: "Page A",
|
||||
uv: 4000,
|
||||
pv: 2400,
|
||||
amt: 2400,
|
||||
},
|
||||
{
|
||||
name: "Page B",
|
||||
uv: 3000,
|
||||
pv: 1398,
|
||||
amt: 2210,
|
||||
},
|
||||
{
|
||||
name: "Page C",
|
||||
uv: 2000,
|
||||
pv: 9800,
|
||||
amt: 2290,
|
||||
},
|
||||
{
|
||||
name: "Page D",
|
||||
uv: 2780,
|
||||
pv: 3908,
|
||||
amt: 2000,
|
||||
},
|
||||
{
|
||||
name: "Page E",
|
||||
uv: 1890,
|
||||
pv: 4800,
|
||||
amt: 2181,
|
||||
},
|
||||
{
|
||||
name: "Page F",
|
||||
uv: 2390,
|
||||
pv: 3800,
|
||||
amt: 2500,
|
||||
},
|
||||
{
|
||||
name: "Page G",
|
||||
uv: 3490,
|
||||
pv: 4300,
|
||||
amt: 2100,
|
||||
},
|
||||
];
|
||||
|
||||
export default function DashboardChart() {
|
||||
return (
|
||||
<AreaChart
|
||||
width={900}
|
||||
height={250}
|
||||
data={data}
|
||||
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#8884d8" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#8884d8" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="colorPv" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#82ca9d" stopOpacity={0.8} />
|
||||
<stop offset="95%" stopColor="#82ca9d" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="uv"
|
||||
stroke="#8884d8"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorUv)"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="pv"
|
||||
stroke="#82ca9d"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorPv)"
|
||||
/>
|
||||
</AreaChart>
|
||||
);
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Bell,
|
||||
@@ -23,7 +25,15 @@ import {
|
||||
} from "@unsend/ui/src/dropdown-menu";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
|
||||
import DashboardChart from "./dashboard-chart";
|
||||
|
||||
export default function Dashboard() {
|
||||
return <div>Hello</div>;
|
||||
return (
|
||||
<div>
|
||||
Dashboard
|
||||
<div className="mx-auto flex justify-center item-center mt-[30vh]">
|
||||
<DashboardChart />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-[200px]">
|
||||
<Button variant="destructive" className="w-[150px]" size="sm">
|
||||
Delete domain
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
@@ -287,7 +287,7 @@ const DomainSettings: React.FC<{ domain: Domain }> = ({ domain }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-semibold text-xl mt-2 text-destructive">Danger</p>
|
||||
<p className="font-semibold text-lg text-destructive">Danger</p>
|
||||
|
||||
<p className="text-destructive text-sm font-semibold">
|
||||
Deleting a domain will stop sending emails with this domain.
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
|
||||
import { api } from "~/trpc/react";
|
||||
import { useState } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
export default function AddDomain() {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -43,7 +44,10 @@ export default function AddDomain() {
|
||||
onOpenChange={(_open) => (_open !== open ? setOpen(_open) : null)}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Add domain</Button>
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add domain
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
|
@@ -20,7 +20,7 @@ export default function DomainsList() {
|
||||
<DomainItem key={domain.id} domain={domain} />
|
||||
))
|
||||
) : (
|
||||
<div>No domains</div>
|
||||
<div className="text-center mt-20">No domains Added</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -21,6 +21,7 @@ import {
|
||||
MailX,
|
||||
} from "lucide-react";
|
||||
import { formatDistance, formatDistanceToNow } from "date-fns";
|
||||
import { EmailStatus } from "@prisma/client";
|
||||
|
||||
export default function EmailsList() {
|
||||
const emailsQuery = api.email.emails.useQuery();
|
||||
@@ -65,40 +66,40 @@ export default function EmailsList() {
|
||||
);
|
||||
}
|
||||
|
||||
const EmailIcon: React.FC<{ status: string }> = ({ status }) => {
|
||||
const EmailIcon: React.FC<{ status: EmailStatus }> = ({ status }) => {
|
||||
switch (status) {
|
||||
case "Send":
|
||||
case "SENT":
|
||||
return (
|
||||
// <div className="border border-gray-400/60 p-2 rounded-lg bg-gray-400/10">
|
||||
<Mail className="w-6 h-6 text-gray-500 " />
|
||||
// </div>
|
||||
);
|
||||
case "Delivery":
|
||||
case "Delayed":
|
||||
case "DELIVERED":
|
||||
return (
|
||||
// <div className="border border-emerald-600/60 p-2 rounded-lg bg-emerald-500/10">
|
||||
<MailCheck className="w-6 h-6 text-emerald-800" />
|
||||
// </div>
|
||||
);
|
||||
case "Bounced":
|
||||
case "BOUNCED":
|
||||
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" />
|
||||
// </div>
|
||||
);
|
||||
case "Clicked":
|
||||
case "CLICKED":
|
||||
return (
|
||||
// <div className="border border-cyan-600/60 p-2 rounded-lg bg-cyan-500/10">
|
||||
<MailSearch className="w-6 h-6 text-cyan-700" />
|
||||
// </div>
|
||||
);
|
||||
case "Opened":
|
||||
case "OPENED":
|
||||
return (
|
||||
// <div className="border border-indigo-600/60 p-2 rounded-lg bg-indigo-500/10">
|
||||
<MailOpen className="w-6 h-6 text-indigo-700" />
|
||||
// </div>
|
||||
);
|
||||
case "Complained":
|
||||
case "DELIVERY_DELAYED":
|
||||
case "COMPLAINED":
|
||||
return (
|
||||
// <div className="border border-yellow-600/60 p-2 rounded-lg bg-yellow-500/10">
|
||||
<MailWarning className="w-6 h-6 text-yellow-700" />
|
||||
@@ -113,26 +114,27 @@ const EmailIcon: React.FC<{ status: string }> = ({ status }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const EmailStatusBadge: React.FC<{ status: string }> = ({ status }) => {
|
||||
const EmailStatusBadge: React.FC<{ status: EmailStatus }> = ({ status }) => {
|
||||
let badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10"; // Default color
|
||||
switch (status) {
|
||||
case "Send":
|
||||
case "SENT":
|
||||
badgeColor = "bg-gray-400/10 text-gray-500 border-gray-400/10";
|
||||
break;
|
||||
case "Delivery":
|
||||
case "Delayed":
|
||||
case "DELIVERED":
|
||||
badgeColor = "bg-emerald-500/10 text-emerald-500 border-emerald-600/10";
|
||||
break;
|
||||
case "Bounced":
|
||||
case "BOUNCED":
|
||||
badgeColor = "bg-red-500/10 text-red-800 border-red-600/10";
|
||||
break;
|
||||
case "Clicked":
|
||||
case "CLICKED":
|
||||
badgeColor = "bg-cyan-500/10 text-cyan-600 border-cyan-600/10";
|
||||
break;
|
||||
case "Opened":
|
||||
case "OPENED":
|
||||
badgeColor = "bg-indigo-500/10 text-indigo-600 border-indigo-600/10";
|
||||
break;
|
||||
case "Complained":
|
||||
case "DELIVERY_DELAYED":
|
||||
badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10";
|
||||
case "COMPLAINED":
|
||||
badgeColor = "bg-yellow-500/10 text-yellow-600 border-yellow-600/10";
|
||||
break;
|
||||
default:
|
||||
@@ -141,9 +143,9 @@ const EmailStatusBadge: React.FC<{ status: string }> = ({ status }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border text-center w-[120px] rounded-full py-0.5 ${badgeColor}`}
|
||||
className={` text-center w-[130px] rounded capitalize py-0.5 text-xs ${badgeColor}`}
|
||||
>
|
||||
{status}
|
||||
{status.toLowerCase().split("_").join(" ")}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
Bell,
|
||||
BellRing,
|
||||
CircleUser,
|
||||
Globe,
|
||||
Home,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
LineChart,
|
||||
Mail,
|
||||
Menu,
|
||||
MessageSquareMore,
|
||||
Package,
|
||||
Package2,
|
||||
Search,
|
||||
@@ -85,6 +87,16 @@ export default async function AuthenticatedDashboardLayout({
|
||||
Domains
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/sms" comingSoon>
|
||||
<MessageSquareMore className="h-4 w-4" />
|
||||
SMS
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/sms" comingSoon>
|
||||
<BellRing className="h-4 w-4" />
|
||||
Push notification
|
||||
</NavButton>
|
||||
|
||||
<NavButton href="/api-keys">
|
||||
<KeyRound className="h-4 w-4" />
|
||||
API keys
|
||||
|
@@ -7,15 +7,31 @@ import React from "react";
|
||||
export const NavButton: React.FC<{
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}> = ({ href, children }) => {
|
||||
comingSoon?: boolean;
|
||||
}> = ({ href, children, comingSoon }) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
const isActive = pathname?.startsWith(href);
|
||||
|
||||
if (comingSoon) {
|
||||
return (
|
||||
<div className="flex items-center justify-between hover:text-primary cursor-not-allowed mt-1">
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary cursor-not-allowed ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<div className="text-muted-foreground px-4 py-0.5 text-xs bg-muted rounded-full">
|
||||
soon
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={`flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
className={`flex items-center mt-1 gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary ${isActive ? " bg-secondary" : "text-muted-foreground"}`}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { headers } from "next/headers";
|
||||
import { hashToken } from "~/server/auth";
|
||||
import { db } from "~/server/db";
|
||||
import { parseSesHook } from "~/server/service/ses-hook-parser";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
console.log("GET", req);
|
||||
@@ -10,67 +11,26 @@ export async function GET(req: Request) {
|
||||
export async function POST(req: Request) {
|
||||
const data = await req.json();
|
||||
|
||||
console.log(data, data.Message);
|
||||
|
||||
if (data.Type === "SubscriptionConfirmation") {
|
||||
return handleSubscription(data);
|
||||
}
|
||||
|
||||
console.log(data, data.Message);
|
||||
|
||||
let message = null;
|
||||
|
||||
try {
|
||||
message = JSON.parse(data.Message || "{}");
|
||||
const status = await parseSesHook(message);
|
||||
if (!status) {
|
||||
return Response.json({ data: "Error is parsing hook" }, { status: 400 });
|
||||
}
|
||||
|
||||
return Response.json({ data: "Success" });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error(e);
|
||||
return Response.json({ data: "Error is parsing hook" }, { status: 400 });
|
||||
}
|
||||
|
||||
const emailId = message?.mail.messageId;
|
||||
|
||||
console.log(emailId, message);
|
||||
|
||||
if (!emailId) {
|
||||
return Response.json({ data: "Email not found" });
|
||||
}
|
||||
|
||||
const email = await db.email.findUnique({
|
||||
where: {
|
||||
id: emailId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email || !message.mail) {
|
||||
return Response.json({ data: "Email not found" });
|
||||
}
|
||||
|
||||
console.log("FOund email", email);
|
||||
|
||||
await db.email.update({
|
||||
where: {
|
||||
id: email.id,
|
||||
},
|
||||
data: {
|
||||
latestStatus: message.eventType,
|
||||
},
|
||||
});
|
||||
|
||||
await db.emailEvent.upsert({
|
||||
where: {
|
||||
emailId_status: {
|
||||
emailId,
|
||||
status: message.eventType,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
data: message[message.eventType.toLowerCase()],
|
||||
},
|
||||
create: {
|
||||
emailId,
|
||||
status: message.eventType,
|
||||
data: message[message.eventType.toLowerCase()],
|
||||
},
|
||||
});
|
||||
|
||||
return Response.json({ data: "Hello" });
|
||||
}
|
||||
|
||||
async function handleSubscription(message: any) {
|
||||
@@ -78,5 +38,5 @@ async function handleSubscription(message: any) {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return Response.json({ data: "Hello" });
|
||||
return Response.json({ data: "Success" });
|
||||
}
|
||||
|
@@ -34,6 +34,7 @@ export const apiRouter = createTRPCRouter({
|
||||
permission: true,
|
||||
partialToken: true,
|
||||
lastUsed: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -4,7 +4,7 @@ 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 { addWebhookConfiguration } from "./ses";
|
||||
import { EventType } from "@aws-sdk/client-sesv2";
|
||||
|
||||
const GENERAL_EVENTS: EventType[] = [
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import dns from "dns";
|
||||
import util from "util";
|
||||
import * as tldts from "tldts";
|
||||
import * as ses from "~/server/ses";
|
||||
import * as ses from "~/server/aws/ses";
|
||||
import { db } from "~/server/db";
|
||||
|
||||
const dnsResolveTxt = util.promisify(dns.resolveTxt);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { EmailContent } from "~/types";
|
||||
import { db } from "../db";
|
||||
import { sendEmailThroughSes } from "../ses";
|
||||
import { sendEmailThroughSes } from "../aws/ses";
|
||||
import { APP_SETTINGS } from "~/utils/constants";
|
||||
|
||||
export async function sendEmail(
|
||||
@@ -45,7 +45,7 @@ export async function sendEmail(
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
id: messageId,
|
||||
sesEmailId: messageId,
|
||||
teamId,
|
||||
domainId: domain.id,
|
||||
},
|
||||
|
91
apps/web/src/server/service/ses-hook-parser.ts
Normal file
91
apps/web/src/server/service/ses-hook-parser.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { EmailStatus } from "@prisma/client";
|
||||
import { SesEvent, SesEventDataKey, SesEventType } from "~/types/aws-types";
|
||||
import { db } from "../db";
|
||||
|
||||
export async function parseSesHook(data: SesEvent) {
|
||||
const mailStatus = getEmailStatus(data);
|
||||
|
||||
if (!mailStatus) {
|
||||
console.error("Unknown email status", data);
|
||||
return false;
|
||||
}
|
||||
|
||||
const sesEmailId = data.mail.messageId;
|
||||
|
||||
const mailData = getEmailData(data);
|
||||
|
||||
const email = await db.email.findUnique({
|
||||
where: {
|
||||
sesEmailId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!email) {
|
||||
console.error("Email not found", data);
|
||||
return false;
|
||||
}
|
||||
|
||||
await db.email.update({
|
||||
where: {
|
||||
id: email.id,
|
||||
},
|
||||
data: {
|
||||
latestStatus: mailStatus,
|
||||
},
|
||||
});
|
||||
|
||||
await db.emailEvent.upsert({
|
||||
where: {
|
||||
emailId_status: {
|
||||
emailId: email.id,
|
||||
status: mailStatus,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
data: mailData as any,
|
||||
},
|
||||
create: {
|
||||
emailId: email.id,
|
||||
status: mailStatus,
|
||||
data: mailData as any,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getEmailStatus(data: SesEvent) {
|
||||
const { eventType } = data;
|
||||
|
||||
if (eventType === "Send") {
|
||||
return EmailStatus.SENT;
|
||||
} else if (eventType === "Delivery") {
|
||||
return EmailStatus.DELIVERED;
|
||||
} else if (eventType === "Bounce") {
|
||||
return EmailStatus.BOUNCED;
|
||||
} else if (eventType === "Complaint") {
|
||||
return EmailStatus.COMPLAINED;
|
||||
} else if (eventType === "Reject") {
|
||||
return EmailStatus.REJECTED;
|
||||
} else if (eventType === "Open") {
|
||||
return EmailStatus.OPENED;
|
||||
} else if (eventType === "Click") {
|
||||
return EmailStatus.CLICKED;
|
||||
} else if (eventType === "Rendering Failure") {
|
||||
return EmailStatus.RENDERING_FAILURE;
|
||||
} else if (eventType === "DeliveryDelay") {
|
||||
return EmailStatus.DELIVERY_DELAYED;
|
||||
}
|
||||
}
|
||||
|
||||
function getEmailData(data: SesEvent) {
|
||||
const { eventType } = data;
|
||||
|
||||
if (eventType === "Rendering Failure") {
|
||||
return data.renderingFailure;
|
||||
} else if (eventType === "DeliveryDelay") {
|
||||
return data.deliveryDelay;
|
||||
} else {
|
||||
return data[eventType.toLowerCase() as SesEventDataKey];
|
||||
}
|
||||
}
|
138
apps/web/src/types/aws-types.ts
Normal file
138
apps/web/src/types/aws-types.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
export interface SnsNotificationMessage {
|
||||
Type: string;
|
||||
MessageId: string;
|
||||
TopicArn: string;
|
||||
Subject?: string;
|
||||
Message: string; // This is a JSON string that needs to be parsed into one of the SES event types below
|
||||
Timestamp: string;
|
||||
SignatureVersion: string;
|
||||
Signature: string;
|
||||
SigningCertURL: string;
|
||||
UnsubscribeURL: string;
|
||||
}
|
||||
|
||||
export interface SesMail {
|
||||
timestamp: string;
|
||||
source: string;
|
||||
messageId: string;
|
||||
destination: string[];
|
||||
headersTruncated: boolean;
|
||||
headers: Array<{ name: string; value: string }>;
|
||||
commonHeaders: {
|
||||
from: string[];
|
||||
to: string[];
|
||||
messageId: string;
|
||||
subject?: string;
|
||||
};
|
||||
tags: { [key: string]: string[] };
|
||||
}
|
||||
|
||||
export interface SesBounce {
|
||||
bounceType: string;
|
||||
bounceSubType: string;
|
||||
bouncedRecipients: Array<{
|
||||
emailAddress: string;
|
||||
action: string;
|
||||
status: string;
|
||||
diagnosticCode?: string;
|
||||
}>;
|
||||
timestamp: string;
|
||||
feedbackId: string;
|
||||
reportingMTA: string;
|
||||
}
|
||||
|
||||
export interface SesComplaint {
|
||||
complainedRecipients: Array<{
|
||||
emailAddress: string;
|
||||
}>;
|
||||
timestamp: string;
|
||||
feedbackId: string;
|
||||
complaintFeedbackType: string;
|
||||
userAgent: string;
|
||||
complaintSubType?: string;
|
||||
arrivedDate?: string;
|
||||
}
|
||||
|
||||
export interface SesDelivery {
|
||||
timestamp: string;
|
||||
processingTimeMillis: number;
|
||||
recipients: string[];
|
||||
smtpResponse: string;
|
||||
reportingMTA: string;
|
||||
remoteMtaIp?: string;
|
||||
}
|
||||
|
||||
export interface SesSend {
|
||||
timestamp: string;
|
||||
smtpResponse: string;
|
||||
reportingMTA: string;
|
||||
recipients: string[];
|
||||
}
|
||||
|
||||
export interface SesReject {
|
||||
reason: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface SesOpen {
|
||||
ipAddress: string;
|
||||
timestamp: string;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
export interface SesClick {
|
||||
ipAddress: string;
|
||||
timestamp: string;
|
||||
userAgent: string;
|
||||
link: string;
|
||||
linkTags: { [key: string]: string };
|
||||
}
|
||||
|
||||
export interface SesRenderingFailure {
|
||||
errorMessage: string;
|
||||
templateName: string;
|
||||
}
|
||||
|
||||
export interface SesDeliveryDelay {
|
||||
delayType: string;
|
||||
expirationTime: string;
|
||||
delayedRecipients: string[];
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export type SesEventType =
|
||||
| "Bounce"
|
||||
| "Complaint"
|
||||
| "Delivery"
|
||||
| "Send"
|
||||
| "Reject"
|
||||
| "Open"
|
||||
| "Click"
|
||||
| "Rendering Failure"
|
||||
| "DeliveryDelay";
|
||||
|
||||
export type SesEventDataKey =
|
||||
| "bounce"
|
||||
| "complaint"
|
||||
| "delivery"
|
||||
| "send"
|
||||
| "reject"
|
||||
| "open"
|
||||
| "click"
|
||||
| "renderingFailure"
|
||||
| "deliveryDelay";
|
||||
|
||||
export interface SesEvent {
|
||||
eventType: SesEventType;
|
||||
mail: SesMail;
|
||||
bounce?: SesBounce;
|
||||
complaint?: SesComplaint;
|
||||
delivery?: SesDelivery;
|
||||
send?: SesSend;
|
||||
reject?: SesReject;
|
||||
open?: SesOpen;
|
||||
click?: SesClick;
|
||||
renderingFailure?: SesRenderingFailure;
|
||||
deliveryDelay?: SesDeliveryDelay;
|
||||
// Additional fields for other event types can be added here
|
||||
}
|
221
pnpm-lock.yaml
generated
221
pnpm-lock.yaml
generated
@@ -154,6 +154,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: 18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
recharts:
|
||||
specifier: ^2.12.5
|
||||
version: 2.12.5(react-dom@18.2.0)(react@18.2.0)
|
||||
server-only:
|
||||
specifier: ^0.0.1
|
||||
version: 0.0.1
|
||||
@@ -2922,6 +2925,48 @@ packages:
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-array@3.2.1:
|
||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-color@3.1.3:
|
||||
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-ease@3.0.2:
|
||||
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-interpolate@3.0.4:
|
||||
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
|
||||
dependencies:
|
||||
'@types/d3-color': 3.1.3
|
||||
dev: false
|
||||
|
||||
/@types/d3-path@3.1.0:
|
||||
resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-scale@4.0.8:
|
||||
resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
|
||||
dependencies:
|
||||
'@types/d3-time': 3.0.3
|
||||
dev: false
|
||||
|
||||
/@types/d3-shape@3.1.6:
|
||||
resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
|
||||
dependencies:
|
||||
'@types/d3-path': 3.1.0
|
||||
dev: false
|
||||
|
||||
/@types/d3-time@3.0.3:
|
||||
resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-timer@3.0.2:
|
||||
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
|
||||
dev: false
|
||||
|
||||
/@types/eslint@8.56.5:
|
||||
resolution: {integrity: sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==}
|
||||
dependencies:
|
||||
@@ -3836,6 +3881,77 @@ packages:
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
/d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
dev: false
|
||||
|
||||
/d3-color@3.1.0:
|
||||
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-ease@3.0.1:
|
||||
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-format@3.1.0:
|
||||
resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-interpolate@3.0.1:
|
||||
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-color: 3.1.0
|
||||
dev: false
|
||||
|
||||
/d3-path@3.1.0:
|
||||
resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/d3-scale@4.0.2:
|
||||
resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
d3-format: 3.1.0
|
||||
d3-interpolate: 3.0.1
|
||||
d3-time: 3.1.0
|
||||
d3-time-format: 4.1.0
|
||||
dev: false
|
||||
|
||||
/d3-shape@3.2.0:
|
||||
resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-path: 3.1.0
|
||||
dev: false
|
||||
|
||||
/d3-time-format@4.1.0:
|
||||
resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-time: 3.1.0
|
||||
dev: false
|
||||
|
||||
/d3-time@3.1.0:
|
||||
resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
d3-array: 3.2.4
|
||||
dev: false
|
||||
|
||||
/d3-timer@3.0.1:
|
||||
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
dev: true
|
||||
@@ -3867,6 +3983,10 @@ packages:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/decimal.js-light@2.5.1:
|
||||
resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
|
||||
dev: false
|
||||
|
||||
/deep-is@0.1.4:
|
||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
dev: true
|
||||
@@ -3935,6 +4055,13 @@ packages:
|
||||
esutils: 2.0.3
|
||||
dev: true
|
||||
|
||||
/dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.0
|
||||
csstype: 3.1.3
|
||||
dev: false
|
||||
|
||||
/dotenv-cli@7.4.1:
|
||||
resolution: {integrity: sha512-fE1aywjRrWGxV3miaiUr3d2zC/VAiuzEGghi+QzgIA9fEf/M5hLMaRSXb4IxbUAwGmaLi0IozdZddnVU96acag==}
|
||||
hasBin: true
|
||||
@@ -4208,7 +4335,7 @@ packages:
|
||||
enhanced-resolve: 5.16.0
|
||||
eslint: 8.57.0
|
||||
eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(eslint@8.57.0)
|
||||
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.3
|
||||
is-core-module: 2.13.1
|
||||
@@ -4703,10 +4830,19 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
dev: false
|
||||
|
||||
/fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
dev: true
|
||||
|
||||
/fast-equals@5.0.1:
|
||||
resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/fast-glob@3.3.2:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -5106,6 +5242,11 @@ packages:
|
||||
side-channel: 1.0.6
|
||||
dev: true
|
||||
|
||||
/internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
dependencies:
|
||||
@@ -5480,7 +5621,6 @@ packages:
|
||||
|
||||
/lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
dev: true
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
@@ -6172,7 +6312,6 @@ packages:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
dev: true
|
||||
|
||||
/property-information@5.6.0:
|
||||
resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==}
|
||||
@@ -6200,7 +6339,6 @@ packages:
|
||||
|
||||
/react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: true
|
||||
|
||||
/react-remove-scroll-bar@2.3.6(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
||||
@@ -6237,6 +6375,19 @@ packages:
|
||||
use-sidecar: 1.1.2(@types/react@18.2.66)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-smooth@4.0.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
fast-equals: 5.0.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-style-singleton@2.2.1(@types/react@18.2.66)(react@18.2.0):
|
||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6267,6 +6418,20 @@ packages:
|
||||
refractor: 3.6.0
|
||||
dev: false
|
||||
|
||||
/react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
react: '>=16.6.0'
|
||||
react-dom: '>=16.6.0'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.24.0
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react@18.2.0:
|
||||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6303,6 +6468,31 @@ packages:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
/recharts-scale@0.4.5:
|
||||
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
|
||||
dependencies:
|
||||
decimal.js-light: 2.5.1
|
||||
dev: false
|
||||
|
||||
/recharts@2.12.5(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Cy+BkqrFIYTHJCyKHJEPvbHE2kVQEP6PKbOHJ8ztRGTAhvHuUnCwDaKVb13OwRFZ0QNUk1QvGTDdgWSMbuMtKw==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
clsx: 2.1.0
|
||||
eventemitter3: 4.0.7
|
||||
lodash: 4.17.21
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-is: 16.13.1
|
||||
react-smooth: 4.0.1(react-dom@18.2.0)(react@18.2.0)
|
||||
recharts-scale: 0.4.5
|
||||
tiny-invariant: 1.3.3
|
||||
victory-vendor: 36.9.2
|
||||
dev: false
|
||||
|
||||
/reflect.getprototypeof@1.0.5:
|
||||
resolution: {integrity: sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6783,6 +6973,10 @@ packages:
|
||||
dependencies:
|
||||
any-promise: 1.3.0
|
||||
|
||||
/tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
dev: false
|
||||
|
||||
/tldts-core@6.1.16:
|
||||
resolution: {integrity: sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==}
|
||||
dev: false
|
||||
@@ -7049,6 +7243,25 @@ packages:
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/victory-vendor@36.9.2:
|
||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||
dependencies:
|
||||
'@types/d3-array': 3.2.1
|
||||
'@types/d3-ease': 3.0.2
|
||||
'@types/d3-interpolate': 3.0.4
|
||||
'@types/d3-scale': 4.0.8
|
||||
'@types/d3-shape': 3.1.6
|
||||
'@types/d3-time': 3.0.3
|
||||
'@types/d3-timer': 3.0.2
|
||||
d3-array: 3.2.4
|
||||
d3-ease: 3.0.1
|
||||
d3-interpolate: 3.0.1
|
||||
d3-scale: 4.0.2
|
||||
d3-shape: 3.2.0
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
dev: false
|
||||
|
||||
/which-boxed-primitive@1.0.2:
|
||||
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||
dependencies:
|
||||
|
Reference in New Issue
Block a user