Improve self host support (#28)

* Add docker setup for self hosting

* Add ses settings tables
This commit is contained in:
KM Koushik
2024-06-10 17:40:42 +10:00
committed by GitHub
parent 6128f26a78
commit 18b523912d
24 changed files with 708 additions and 169 deletions

View File

@@ -40,6 +40,12 @@ export default function ApiList() {
/>
</TableCell>
</TableRow>
) : apiKeysQuery.data?.length === 0 ? (
<TableRow className="h-32">
<TableCell colSpan={6} className="text-center py-4">
<p>No API keys added</p>
</TableCell>
</TableRow>
) : (
apiKeysQuery.data?.map((apiKey) => (
<TableRow key={apiKey.id}>

View File

@@ -8,6 +8,7 @@ import {
Home,
LayoutDashboard,
LineChart,
LogOut,
Mail,
Menu,
Package,
@@ -28,7 +29,7 @@ import {
} from "@unsend/ui/src/dropdown-menu";
import { Sheet, SheetContent, SheetTrigger } from "@unsend/ui/src/sheet";
import { NavButton } from "./nav-button";
import { LogoutButton, NavButton } from "./nav-button";
import { DashboardProvider } from "~/providers/dashboard-provider";
import { NextAuthProvider } from "~/providers/next-auth";
@@ -89,15 +90,16 @@ export default function AuthenticatedDashboardLayout({
Developer settings
</NavButton>
</div>
<div className=" absolute bottom-10 p-4">
<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"
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>

View File

@@ -1,5 +1,7 @@
"use client";
import { LogOut } from "lucide-react";
import { signOut } from "next-auth/react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import React from "react";
@@ -37,3 +39,15 @@ export const NavButton: React.FC<{
</Link>
);
};
export const LogoutButton: React.FC = () => {
return (
<button
className={` w-full justify-start flex items-center gap-2 rounded-lg py-2 transition-all hover:text-primary text-muted-foreground`}
onClick={() => signOut()}
>
<LogOut className="h-4 w-4" />
Logout
</button>
);
};

View File

@@ -1,3 +1,4 @@
import { db } from "~/server/db";
import { AppSettingsService } from "~/server/service/app-settings-service";
import { parseSesHook } from "~/server/service/ses-hook-parser";
import { SnsNotificationMessage } from "~/types/aws-types";
@@ -13,6 +14,10 @@ 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);
@@ -47,9 +52,38 @@ async function handleSubscription(message: any) {
method: "GET",
});
const topicArn = message.TopicArn as string;
const setting = await db.sesSetting.findFirst({
where: {
topicArn,
},
});
if (!setting) {
return Response.json({ data: "Setting not found" });
}
await db.sesSetting.update({
where: {
id: setting?.id,
},
data: {
callbackSuccess: true,
},
});
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;

View File

@@ -6,6 +6,7 @@ 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"],
@@ -18,11 +19,17 @@ export const metadata: Metadata = {
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
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}`}>

View File

@@ -22,6 +22,7 @@ import {
REGEXP_ONLY_DIGITS_AND_CHARS,
} from "@unsend/ui/src/input-otp";
import { Input } from "@unsend/ui/src/input";
import { env } from "~/env";
const emailSchema = z.object({
email: z
@@ -93,108 +94,129 @@ export default function LoginPage() {
</svg>
Continue with Github
</Button>
<Button
className="w-[350px]"
size="lg"
onClick={() => signIn("google")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
className="h-6 w-6 stroke-black fill-black mr-4"
{env.NEXT_PUBLIC_IS_CLOUD ? (
<Button
className="w-[350px]"
size="lg"
onClick={() => signIn("google")}
>
<path d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z" />
</svg>
Continue with Google
</Button>
<div className=" flex w-[350px] items-center justify-between gap-2">
<p className=" z-10 ml-[175px] -translate-x-1/2 bg-background px-4 text-sm">
or
</p>
<div className="absolute h-[1px] w-[350px] bg-gradient-to-r from-zinc-800 via-zinc-300 to-zinc-800"></div>
</div>
{emailStatus === "success" ? (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 488 512"
className="h-6 w-6 stroke-black fill-black mr-4"
>
<path d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z" />
</svg>
Continue with Google
</Button>
) : null}
{env.NEXT_PUBLIC_IS_CLOUD ? (
<>
<p className=" w-[350px] text-center text-sm">
We have sent an email with the OTP. Please check your inbox
</p>
<Form {...otpForm}>
<form
onSubmit={otpForm.handleSubmit(onOTPSubmit)}
className="space-y-4"
>
<FormField
control={otpForm.control}
name="otp"
render={({ field }) => (
<FormItem>
<FormControl>
<InputOTP
className="w-[350px]"
maxLength={5}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
inputMode="text"
{...field}
>
<InputOTPGroup>
<InputOTPSlot className="w-[70px]" index={0} />
<InputOTPSlot className="w-[70px]" index={1} />
<InputOTPSlot className="w-[70px]" index={2} />
<InputOTPSlot className="w-[70px]" index={3} />
<InputOTPSlot className="w-[70px]" index={4} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<div className=" flex w-[350px] items-center justify-between gap-2">
<p className=" z-10 ml-[175px] -translate-x-1/2 bg-background px-4 text-sm">
or
</p>
<div className="absolute h-[1px] w-[350px] bg-gradient-to-r from-zinc-800 via-zinc-300 to-zinc-800"></div>
</div>
{emailStatus === "success" ? (
<>
<p className=" w-[350px] text-center text-sm">
We have sent an email with the OTP. Please check your inbox
</p>
<Form {...otpForm}>
<form
onSubmit={otpForm.handleSubmit(onOTPSubmit)}
className="space-y-4"
>
<FormField
control={otpForm.control}
name="otp"
render={({ field }) => (
<FormItem>
<FormControl>
<InputOTP
className="w-[350px]"
maxLength={5}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
inputMode="text"
{...field}
>
<InputOTPGroup>
<InputOTPSlot
className="w-[70px]"
index={0}
/>
<InputOTPSlot
className="w-[70px]"
index={1}
/>
<InputOTPSlot
className="w-[70px]"
index={2}
/>
<InputOTPSlot
className="w-[70px]"
index={3}
/>
<InputOTPSlot
className="w-[70px]"
index={4}
/>
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
<Button className="mt-6 w-[350px] bg-white hover:bg-gray-100 focus:bg-gray-100">
Submit
</Button>
</form>
</Form>
<Button className="mt-6 w-[350px] bg-white hover:bg-gray-100 focus:bg-gray-100">
Submit
</Button>
</form>
</Form>
</>
) : (
<>
<Form {...emailForm}>
<form
onSubmit={emailForm.handleSubmit(onEmailSubmit)}
className="space-y-4"
>
<FormField
control={emailForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your email"
className=" w-[350px]"
type="email"
{...field}
/>
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
)}
/>
<Button
className=" w-[350px] bg-white hover:bg-gray-100 focus:bg-gray-100"
type="submit"
disabled={emailStatus === "sending"}
>
{emailStatus === "sending"
? "Sending..."
: "Send magic link"}
</Button>
</form>
</Form>
</>
)}
</>
) : (
<>
<Form {...emailForm}>
<form
onSubmit={emailForm.handleSubmit(onEmailSubmit)}
className="space-y-4"
>
<FormField
control={emailForm.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Enter your email"
className=" w-[350px]"
type="email"
{...field}
/>
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
)}
/>
<Button
className=" w-[350px] bg-white hover:bg-gray-100 focus:bg-gray-100"
type="submit"
disabled={emailStatus === "sending"}
>
{emailStatus === "sending"
? "Sending..."
: "Send magic link"}
</Button>
</form>
</Form>
</>
)}
) : null}
</div>
</div>
</main>