From d447de9e8a1472069bda30d2e006baf3aeb1532a Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Tue, 31 Dec 2024 18:25:11 -0500 Subject: [PATCH] improve email formatting and invite flow for new users --- server/db/schema.ts | 4 +-- server/emails/sendEmail.ts | 12 ++++--- .../emails/templates/NotifyResetPassword.tsx | 15 ++------ server/emails/templates/ResetPasswordCode.tsx | 13 ++----- server/emails/templates/ResourceOTPCode.tsx | 20 ++++------- server/emails/templates/SendInviteLink.tsx | 15 +++----- .../templates/TwoFactorAuthNotification.tsx | 15 ++------ server/emails/templates/VerifyEmailCode.tsx | 13 ++----- .../templates/components/LetterHead.tsx | 36 +++++++++++++++++++ server/routers/auth/signup.ts | 9 +++++ server/routers/external.ts | 2 +- server/routers/user/acceptInvite.ts | 5 ++- .../[resourceId]/connectivity/page.tsx | 4 +-- src/app/auth/login/DashboardLoginForm.tsx | 19 ++++++++-- src/app/invite/page.tsx | 14 +++++--- 15 files changed, 107 insertions(+), 89 deletions(-) create mode 100644 server/emails/templates/components/LetterHead.tsx diff --git a/server/db/schema.ts b/server/db/schema.ts index 4b4145d6..fd9956b5 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -131,7 +131,7 @@ export const newtSessions = sqliteTable("newtSession", { export const userOrgs = sqliteTable("userOrgs", { userId: text("userId") .notNull() - .references(() => users.userId), + .references(() => users.userId, { onDelete: "cascade" }), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -395,4 +395,4 @@ export type ResourcePassword = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; -export type VersionMigration = InferSelectModel; \ No newline at end of file +export type VersionMigration = InferSelectModel; diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index 973f48ee..e64f0df4 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -6,6 +6,7 @@ import logger from "@server/logger"; export async function sendEmail( template: ReactElement, opts: { + name: string | undefined; from: string | undefined; to: string | undefined; subject: string; @@ -23,14 +24,15 @@ export async function sendEmail( const emailHtml = await render(template); - const options = { - from: opts.from, + await emailClient.sendMail({ + from: { + name: opts.name || "Pangolin Proxy", + address: opts.from, + }, to: opts.to, subject: opts.subject, html: emailHtml, - }; - - await emailClient.sendMail(options); + }); } export default sendEmail; diff --git a/server/emails/templates/NotifyResetPassword.tsx b/server/emails/templates/NotifyResetPassword.tsx index e789e4bd..98f2522e 100644 --- a/server/emails/templates/NotifyResetPassword.tsx +++ b/server/emails/templates/NotifyResetPassword.tsx @@ -10,6 +10,7 @@ import { Tailwind } from "@react-email/components"; import * as React from "react"; +import LetterHead from "./components/LetterHead"; interface Props { email: string; @@ -35,15 +36,7 @@ export const ConfirmPasswordReset = ({ email }: Props) => { > -
-
- Pangolin -
- -
- {new Date().toLocaleDateString()} -
-
+ Password Reset Confirmation @@ -56,10 +49,6 @@ export const ConfirmPasswordReset = ({ email }: Props) => { reset. If you made this change, no further action is required. - - If you did not request this change, please contact - our support team immediately. - Thank you for keeping your account secure. diff --git a/server/emails/templates/ResetPasswordCode.tsx b/server/emails/templates/ResetPasswordCode.tsx index e0171189..48669625 100644 --- a/server/emails/templates/ResetPasswordCode.tsx +++ b/server/emails/templates/ResetPasswordCode.tsx @@ -10,6 +10,7 @@ import { Tailwind } from "@react-email/components"; import * as React from "react"; +import LetterHead from "./components/LetterHead"; interface Props { email: string; @@ -18,7 +19,7 @@ interface Props { } export const ResetPasswordCode = ({ email, code, link }: Props) => { - const previewText = `Reset your password, ${email}`; + const previewText = `Your password reset code is ${code}`; return ( @@ -37,15 +38,7 @@ export const ResetPasswordCode = ({ email, code, link }: Props) => { > -
-
- Pangolin -
- -
- {new Date().toLocaleDateString()} -
-
+ Password Reset Request diff --git a/server/emails/templates/ResourceOTPCode.tsx b/server/emails/templates/ResourceOTPCode.tsx index 32a1fd5d..38808f32 100644 --- a/server/emails/templates/ResourceOTPCode.tsx +++ b/server/emails/templates/ResourceOTPCode.tsx @@ -10,6 +10,7 @@ import { Tailwind } from "@react-email/components"; import * as React from "react"; +import LetterHead from "./components/LetterHead"; interface ResourceOTPCodeProps { email?: string; @@ -24,7 +25,7 @@ export const ResourceOTPCode = ({ orgName: organizationName, otp }: ResourceOTPCodeProps) => { - const previewText = `Your one-time password for ${resourceName} is ready!`; + const previewText = `Your one-time password for ${resourceName} is ${otp}`; return ( @@ -43,27 +44,18 @@ export const ResourceOTPCode = ({ > -
-
- Pangolin -
- -
- {new Date().toLocaleDateString()} -
-
+ - Your One-Time Password + Your One-Time Password for {resourceName} Hi {email || "there"}, - You’ve requested a one-time password (OTP) to - authenticate with the resource{" "} + You’ve requested a one-time password to access{" "} {resourceName} in{" "} - {organizationName}. Use the OTP + {organizationName}. Use the code below to complete your authentication:
diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx index ba454386..b41b7214 100644 --- a/server/emails/templates/SendInviteLink.tsx +++ b/server/emails/templates/SendInviteLink.tsx @@ -11,6 +11,7 @@ import { Button } from "@react-email/components"; import * as React from "react"; +import LetterHead from "./components/LetterHead"; interface SendInviteLinkProps { email: string; @@ -27,7 +28,7 @@ export const SendInviteLink = ({ inviterName, expiresInDays }: SendInviteLinkProps) => { - const previewText = `${inviterName} invited to join ${orgName}`; + const previewText = `${inviterName} invited you to join ${orgName}`; return ( @@ -46,18 +47,10 @@ export const SendInviteLink = ({ > -
-
- Pangolin -
- -
- {new Date().toLocaleDateString()} -
-
+ - You're Invite to Join {orgName} + Invited to Join {orgName} Hi {email || "there"}, diff --git a/server/emails/templates/TwoFactorAuthNotification.tsx b/server/emails/templates/TwoFactorAuthNotification.tsx index 3d1abfa9..b3ff38eb 100644 --- a/server/emails/templates/TwoFactorAuthNotification.tsx +++ b/server/emails/templates/TwoFactorAuthNotification.tsx @@ -10,6 +10,7 @@ import { Tailwind } from "@react-email/components"; import * as React from "react"; +import LetterHead from "./components/LetterHead"; interface Props { email: string; @@ -36,15 +37,7 @@ export const TwoFactorAuthNotification = ({ email, enabled }: Props) => { > -
-
- Pangolin -
- -
- {new Date().toLocaleDateString()} -
-
+ Two-Factor Authentication{" "} @@ -71,10 +64,6 @@ export const TwoFactorAuthNotification = ({ email, enabled }: Props) => { enabling it to protect your account.
)} - - If you did not make this change, please contact our - support team immediately. - Best regards,
diff --git a/server/emails/templates/VerifyEmailCode.tsx b/server/emails/templates/VerifyEmailCode.tsx index efd5acfb..d197c30e 100644 --- a/server/emails/templates/VerifyEmailCode.tsx +++ b/server/emails/templates/VerifyEmailCode.tsx @@ -10,6 +10,7 @@ import { Tailwind } from "@react-email/components"; import * as React from "react"; +import LetterHead from "./components/LetterHead"; interface VerifyEmailProps { username?: string; @@ -22,7 +23,7 @@ export const VerifyEmail = ({ verificationCode, verifyLink }: VerifyEmailProps) => { - const previewText = `Verify your email, ${username}`; + const previewText = `Your verification code is ${verificationCode}`; return ( @@ -41,15 +42,7 @@ export const VerifyEmail = ({ > -
-
- Pangolin -
- -
- {new Date().toLocaleDateString()} -
-
+ Please Verify Your Email diff --git a/server/emails/templates/components/LetterHead.tsx b/server/emails/templates/components/LetterHead.tsx new file mode 100644 index 00000000..6829b8df --- /dev/null +++ b/server/emails/templates/components/LetterHead.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +export function LetterHead() { + return ( + + + + + +
+ Pangolin + + {new Date().getFullYear()} +
+ ); +} + +export default LetterHead; diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 10815da3..11984961 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -84,6 +84,15 @@ export async function signup( createHttpError(HttpCode.BAD_REQUEST, "Invite does not exist") ); } + + if (existingInvite.email !== email) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invite is not for this user" + ) + ); + } } try { diff --git a/server/routers/external.ts b/server/routers/external.ts index ba3743f5..f9d3e62d 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -137,7 +137,7 @@ authenticated.post( verifyUserHasAction(ActionsEnum.inviteUser), user.inviteUser ); // maybe make this /invite/create instead -authenticated.post("/invite/accept", user.acceptInvite); +unauthenticated.post("/invite/accept", user.acceptInvite); // this is supposed to be unauthenticated authenticated.get( "/resource/:resourceId/roles", diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index 2c08a537..3097c2d2 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -12,6 +12,7 @@ import { fromError } from "zod-validation-error"; import { isWithinExpirationDate } from "oslo"; import { verifyPassword } from "@server/auth/password"; import { checkValidInvite } from "@server/auth/checkValidInvite"; +import { verifySession } from "@server/auth"; const acceptInviteBodySchema = z .object({ @@ -72,7 +73,9 @@ export async function acceptInvite( ); } - if (req.user && req.user.email !== existingInvite.email) { + const { user, session } = await verifySession(req); + + if (user && user.email !== existingInvite.email) { return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index c50bbe00..f33d94c7 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -304,8 +304,8 @@ export default function ReverseProxyTargets(props: { {row.original.method} - http - https + HTTP + HTTPS ) diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx index 61abe8cc..25ee47ae 100644 --- a/src/app/auth/login/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -5,19 +5,34 @@ import { CardContent, CardDescription, CardHeader, - CardTitle, + CardTitle } from "@/components/ui/card"; +import { createApiClient } from "@app/api"; import LoginForm from "@app/components/LoginForm"; +import { useEnvContext } from "@app/hooks/useEnvContext"; import { useRouter } from "next/navigation"; +import { useEffect } from "react"; type DashboardLoginFormProps = { redirect?: string; }; export default function DashboardLoginForm({ - redirect, + redirect }: DashboardLoginFormProps) { const router = useRouter(); + // const api = createApiClient(useEnvContext()); + // + // useEffect(() => { + // const logout = async () => { + // try { + // await api.post("/auth/logout"); + // console.log("user logged out"); + // } catch (e) {} + // }; + // + // logout(); + // }); return ( diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 7af05536..8d3d1f21 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -20,10 +20,6 @@ export default async function InvitePage(props: { const user = await verifySession(); - if (!user) { - redirect(`/auth/signup?redirect=/invite?token=${params.token}`); - } - const parts = tokenParam.split("-"); if (parts.length !== 2) { return ( @@ -70,9 +66,17 @@ export default async function InvitePage(props: { } } + const type = cardType(); + + console.log("card type is", type, error) + + if (!user && type === "user_does_not_exist") { + redirect(`/auth/signup?redirect=/invite?token=${params.token}`); + } + return ( <> - + ); }