Pull up downstream changes

This commit is contained in:
Owen
2025-07-13 21:57:24 -07:00
parent c679875273
commit 98a261e38c
108 changed files with 9799 additions and 2038 deletions

View File

@@ -2,6 +2,7 @@ import { render } from "@react-email/render";
import { ReactElement } from "react";
import emailClient from "@server/emails";
import logger from "@server/logger";
import config from "@server/lib/config";
export async function sendEmail(
template: ReactElement,
@@ -24,9 +25,11 @@ export async function sendEmail(
const emailHtml = await render(template);
const appName = "Fossorial - Pangolin";
await emailClient.sendMail({
from: {
name: opts.name || "Pangolin",
name: opts.name || appName,
address: opts.from,
},
to: opts.to,

View File

@@ -1,11 +1,5 @@
import {
Body,
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
@@ -22,29 +16,29 @@ interface Props {
}
export const ConfirmPasswordReset = ({ email }: Props) => {
const previewText = `Your password has been reset`;
const previewText = `Your password has been successfully reset.`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans relative">
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailHeading>Password Reset Confirmation</EmailHeading>
{/* <EmailHeading>Password Successfully Reset</EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
This email confirms that your password has just been
reset. If you made this change, no further action is
required.
Your password has been successfully reset. You can
now sign in to your account using your new password.
</EmailText>
<EmailText>
Thank you for keeping your account secure.
If you didn't make this change, please contact our
support team immediately to secure your account.
</EmailText>
<EmailFooter>

View File

@@ -1,11 +1,5 @@
import {
Body,
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
@@ -18,6 +12,7 @@ import {
EmailText
} from "./components/Email";
import CopyCodeBox from "./components/CopyCodeBox";
import ButtonLink from "./components/ButtonLink";
interface Props {
email: string;
@@ -26,37 +21,39 @@ interface Props {
}
export const ResetPasswordCode = ({ email, code, link }: Props) => {
const previewText = `Your password reset code is ${code}`;
const previewText = `Reset your password with code: ${code}`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans">
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailHeading>Password Reset Request</EmailHeading>
{/* <EmailHeading>Reset Your Password</EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
Youve requested to reset your password. Please{" "}
<a href={link} className="text-primary">
click here
</a>{" "}
and follow the instructions to reset your password,
or manually enter the following code:
You've requested to reset your password. Click the
button below to reset your password, or use the
verification code provided if prompted.
</EmailText>
<EmailSection>
<ButtonLink href={link}>Reset Password</ButtonLink>
</EmailSection>
<EmailSection>
<CopyCodeBox text={code} />
</EmailSection>
<EmailText>
If you didnt request this, you can safely ignore
this email.
This reset code will expire in 2 hours. If you
didn't request a password reset, you can safely
ignore this email.
</EmailText>
<EmailFooter>

View File

@@ -1,11 +1,5 @@
import {
Body,
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import {
EmailContainer,
EmailLetterHead,
@@ -32,34 +26,40 @@ export const ResourceOTPCode = ({
orgName: organizationName,
otp
}: ResourceOTPCodeProps) => {
const previewText = `Your one-time password for ${resourceName} is ${otp}`;
const previewText = `Your access code for ${resourceName}: ${otp}`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans">
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailHeading>
Your One-Time Code for {resourceName}
</EmailHeading>
{/* <EmailHeading> */}
{/* Access Code for {resourceName} */}
{/* </EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
Youve requested a one-time password to access{" "}
You've requested access to{" "}
<strong>{resourceName}</strong> in{" "}
<strong>{organizationName}</strong>. Use the code
below to complete your authentication:
<strong>{organizationName}</strong>. Use the
verification code below to complete your
authentication.
</EmailText>
<EmailSection>
<CopyCodeBox text={otp} />
</EmailSection>
<EmailText>
This code will expire in 15 minutes. If you didn't
request this code, please ignore this email.
</EmailText>
<EmailFooter>
<EmailSignature />
</EmailFooter>

View File

@@ -1,11 +1,5 @@
import {
Body,
Head,
Html,
Preview,
Tailwind,
} from "@react-email/components";
import * as React from "react";
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
@@ -41,35 +35,44 @@ export const SendInviteLink = ({
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans">
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailHeading>Invited to Join {orgName}</EmailHeading>
{/* <EmailHeading> */}
{/* You're Invited to Join {orgName} */}
{/* </EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
Youve been invited to join the organization{" "}
You've been invited to join{" "}
<strong>{orgName}</strong>
{inviterName ? ` by ${inviterName}.` : "."} Please
access the link below to accept the invite.
</EmailText>
<EmailText>
This invite will expire in{" "}
<strong>
{expiresInDays}{" "}
{expiresInDays === "1" ? "day" : "days"}.
</strong>
{inviterName ? ` by ${inviterName}` : ""}. Click the
button below to accept your invitation and get
started.
</EmailText>
<EmailSection>
<ButtonLink href={inviteLink}>
Accept Invite to {orgName}
Accept Invitation
</ButtonLink>
</EmailSection>
{/* <EmailText> */}
{/* If you're having trouble clicking the button, copy */}
{/* and paste the URL below into your web browser: */}
{/* <br /> */}
{/* <span className="break-all">{inviteLink}</span> */}
{/* </EmailText> */}
<EmailText>
This invite expires in {expiresInDays}{" "}
{expiresInDays === "1" ? "day" : "days"}. If the
link has expired, please contact the owner of the
organization to request a new invitation.
</EmailText>
<EmailFooter>
<EmailSignature />
</EmailFooter>

View File

@@ -1,11 +1,5 @@
import {
Body,
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
@@ -23,44 +17,52 @@ interface Props {
}
export const TwoFactorAuthNotification = ({ email, enabled }: Props) => {
const previewText = `Two-Factor Authentication has been ${enabled ? "enabled" : "disabled"}`;
const previewText = `Two-Factor Authentication ${enabled ? "enabled" : "disabled"} for your account`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans">
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailHeading>
Two-Factor Authentication{" "}
{enabled ? "Enabled" : "Disabled"}
</EmailHeading>
{/* <EmailHeading> */}
{/* Security Update: 2FA{" "} */}
{/* {enabled ? "Enabled" : "Disabled"} */}
{/* </EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
This email confirms that Two-Factor Authentication
has been successfully{" "}
{enabled ? "enabled" : "disabled"} on your account.
Two-factor authentication has been successfully{" "}
<strong>{enabled ? "enabled" : "disabled"}</strong>{" "}
on your account.
</EmailText>
{enabled ? (
<EmailText>
With Two-Factor Authentication enabled, your
account is now more secure. Please ensure you
keep your authentication method safe.
</EmailText>
<>
<EmailText>
Your account is now protected with an
additional layer of security. Keep your
authentication method safe and accessible.
</EmailText>
</>
) : (
<EmailText>
With Two-Factor Authentication disabled, your
account may be less secure. We recommend
enabling it to protect your account.
</EmailText>
<>
<EmailText>
We recommend re-enabling two-factor
authentication to keep your account secure.
</EmailText>
</>
)}
<EmailText>
If you didn't make this change, please contact our
support team immediately.
</EmailText>
<EmailFooter>
<EmailSignature />
</EmailFooter>

View File

@@ -1,5 +1,5 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import * as React from "react";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
@@ -24,25 +24,24 @@ export const VerifyEmail = ({
verificationCode,
verifyLink
}: VerifyEmailProps) => {
const previewText = `Your verification code is ${verificationCode}`;
const previewText = `Verify your email with code: ${verificationCode}`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans">
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailHeading>Please Verify Your Email</EmailHeading>
{/* <EmailHeading>Verify Your Email Address</EmailHeading> */}
<EmailGreeting>Hi {username || "there"},</EmailGreeting>
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
Youve requested to verify your email. Please use
the code below to complete the verification process
upon logging in.
Welcome! To complete your account setup, please
verify your email address using the code below.
</EmailText>
<EmailSection>
@@ -50,7 +49,8 @@ export const VerifyEmail = ({
</EmailSection>
<EmailText>
If you didnt request this, you can safely ignore
This verification code will expire in 15 minutes. If
you didn't create an account, you can safely ignore
this email.
</EmailText>

View File

@@ -0,0 +1,131 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
EmailFooter,
EmailGreeting,
EmailHeading,
EmailLetterHead,
EmailSection,
EmailSignature,
EmailText,
EmailInfoSection
} from "./components/Email";
import ButtonLink from "./components/ButtonLink";
import CopyCodeBox from "./components/CopyCodeBox";
interface WelcomeQuickStartProps {
username?: string;
link: string;
fallbackLink: string;
resourceMethod: string;
resourceHostname: string;
resourcePort: string | number;
resourceUrl: string;
cliCommand: string;
}
export const WelcomeQuickStart = ({
username,
link,
fallbackLink,
resourceMethod,
resourceHostname,
resourcePort,
resourceUrl,
cliCommand
}: WelcomeQuickStartProps) => {
const previewText = "Welcome! Here's what to do next";
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
Thank you for trying out Pangolin! We're excited to
have you on board.
</EmailText>
<EmailText>
To continue to configure your site, resources, and
other features, complete your account setup to
access the full dashboard.
</EmailText>
<EmailSection>
<ButtonLink href={link}>
View Your Dashboard
</ButtonLink>
{/* <p className="text-sm text-gray-300 mt-2"> */}
{/* If the button above doesn't work, you can also */}
{/* use this{" "} */}
{/* <a href={fallbackLink} className="underline"> */}
{/* link */}
{/* </a> */}
{/* . */}
{/* </p> */}
</EmailSection>
<EmailSection>
<div className="mb-2 font-semibold text-gray-900 text-base text-left">
Connect your site using Newt
</div>
<div className="inline-block w-full">
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto text-left">
<span className="text-sm font-mono text-gray-900 tracking-wider">
{cliCommand}
</span>
</div>
<p className="text-xs text-gray-500 mt-2">
To learn how to use Newt, including more
installation methods, visit the{" "}
<a
href="https://docs.fossorial.io"
className="underline"
>
docs
</a>
.
</p>
</div>
</EmailSection>
<EmailInfoSection
title="Your Demo Resource"
items={[
{ label: "Method", value: resourceMethod },
{ label: "Hostname", value: resourceHostname },
{ label: "Port", value: resourcePort },
{
label: "Resource URL",
value: (
<a
href={resourceUrl}
className="underline text-blue-600"
>
{resourceUrl}
</a>
)
}
]}
/>
<EmailFooter>
<EmailSignature />
</EmailFooter>
</EmailContainer>
</Body>
</Tailwind>
</Html>
);
};
export default WelcomeQuickStart;

View File

@@ -12,7 +12,11 @@ export default function ButtonLink({
return (
<a
href={href}
className={`rounded-full bg-primary px-4 py-2 text-center font-semibold text-white text-xl no-underline inline-block ${className}`}
className={`inline-block bg-primary hover:bg-primary/90 text-white font-semibold px-8 py-3 rounded-lg text-center no-underline transition-colors ${className}`}
style={{
backgroundColor: "#F97316",
textDecoration: "none"
}}
>
{children}
</a>

View File

@@ -2,10 +2,15 @@ import React from "react";
export default function CopyCodeBox({ text }: { text: string }) {
return (
<div className="text-center rounded-lg bg-neutral-100 p-2">
<span className="text-2xl font-mono text-neutral-600 tracking-wide">
{text}
</span>
<div className="inline-block">
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto">
<span className="text-2xl font-mono text-gray-900 tracking-wider font-semibold">
{text}
</span>
</div>
<p className="text-xs text-gray-500 mt-2">
Copy and paste this code when prompted
</p>
</div>
);
}

View File

@@ -1,47 +1,26 @@
import { Container } from "@react-email/components";
import React from "react";
import { Container, Img } from "@react-email/components";
// EmailContainer: Wraps the entire email layout
export function EmailContainer({ children }: { children: React.ReactNode }) {
return (
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
<Container className="bg-white border border-solid border-gray-200 max-w-lg mx-auto my-8 rounded-lg overflow-hidden shadow-sm">
{children}
</Container>
);
}
// EmailLetterHead: For branding or logo at the top
// EmailLetterHead: For branding with logo on dark background
export function EmailLetterHead() {
return (
<div className="mb-4">
<table
role="presentation"
width="100%"
style={{
marginBottom: "24px"
}}
>
<tr>
<td
style={{
fontSize: "14px",
fontWeight: "bold",
color: "#F97317"
}}
>
Pangolin
</td>
<td
style={{
fontSize: "14px",
textAlign: "right",
color: "#6B7280"
}}
>
{new Date().getFullYear()}
</td>
</tr>
</table>
<div className="px-6 pt-8 pb-2 text-center">
<Img
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
alt="Fossorial"
width="120"
height="auto"
className="mx-auto"
/>
</div>
);
}
@@ -49,14 +28,22 @@ export function EmailLetterHead() {
// EmailHeading: For the primary message or headline
export function EmailHeading({ children }: { children: React.ReactNode }) {
return (
<h1 className="text-2xl font-semibold text-gray-800 text-center">
{children}
</h1>
<div className="px-6 pt-4 pb-1">
<h1 className="text-2xl font-semibold text-gray-900 text-center leading-tight">
{children}
</h1>
</div>
);
}
export function EmailGreeting({ children }: { children: React.ReactNode }) {
return <p className="text-base text-gray-700 my-4">{children}</p>;
return (
<div className="px-6">
<p className="text-base text-gray-700 leading-relaxed">
{children}
</p>
</div>
);
}
// EmailText: For general text content
@@ -68,9 +55,13 @@ export function EmailText({
className?: string;
}) {
return (
<p className={`my-2 text-base text-gray-700 ${className}`}>
{children}
</p>
<div className="px-6">
<p
className={`text-base text-gray-700 leading-relaxed ${className}`}
>
{children}
</p>
</div>
);
}
@@ -82,20 +73,70 @@ export function EmailSection({
children: React.ReactNode;
className?: string;
}) {
return <div className={`text-center my-6 ${className}`}>{children}</div>;
return (
<div className={`px-6 py-6 text-center ${className}`}>{children}</div>
);
}
// EmailFooter: For closing or signature
export function EmailFooter({ children }: { children: React.ReactNode }) {
return <div className="text-sm text-gray-500 mt-6">{children}</div>;
return (
<div className="px-6 py-6 border-t border-gray-100 bg-gray-50">
{children}
<p className="text-xs text-gray-400 mt-4">
For any questions or support, please contact us at:
<br />
support@fossorial.io
</p>
<p className="text-xs text-gray-300 text-center mt-4">
&copy; {new Date().getFullYear()} Fossorial, Inc. All rights
reserved.
</p>
</div>
);
}
export function EmailSignature() {
return (
<p>
Best regards,
<br />
Fossorial
</p>
<div className="text-sm text-gray-600">
<p className="mb-2">
Best regards,
<br />
<strong>The Fossorial Team</strong>
</p>
</div>
);
}
// EmailInfoSection: For structured key-value info (like resource details)
export function EmailInfoSection({
title,
items
}: {
title?: string;
items: { label: string; value: React.ReactNode }[];
}) {
return (
<div className="px-6 py-4">
{title && (
<div className="mb-2 font-semibold text-gray-900 text-base">
{title}
</div>
)}
<table className="w-full text-sm text-left">
<tbody>
{items.map((item, idx) => (
<tr key={idx}>
<td className="pr-4 py-1 text-gray-600 align-top whitespace-nowrap">
{item.label}
</td>
<td className="py-1 text-gray-900 break-all">
{item.value}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -1,3 +1,5 @@
import React from "react";
export const themeColors = {
theme: {
extend: {