Pārlūkot izejas kodu

improve email formatting and invite flow for new users

Milo Schwartz 6 mēneši atpakaļ
vecāks
revīzija
d447de9e8a

+ 2 - 2
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<typeof resourcePassword>;
 export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
 export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
 export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
-export type VersionMigration = InferSelectModel<typeof versionMigrations>;
+export type VersionMigration = InferSelectModel<typeof versionMigrations>;

+ 7 - 5
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;

+ 2 - 13
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) => {
             >
                 <Body className="font-sans relative">
                     <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <div className="flex items-center justify-between">
-                            <div className="text-sm font-bold text-orange-500">
-                                Pangolin
-                            </div>
-
-                            <div className="text-sm text-gray-500">
-                                {new Date().toLocaleDateString()}
-                            </div>
-                        </div>
+                        <LetterHead />
 
                         <Heading className="text-2xl font-semibold text-gray-800 text-center">
                             Password Reset Confirmation
@@ -56,10 +49,6 @@ export const ConfirmPasswordReset = ({ email }: Props) => {
                             reset. If you made this change, no further action is
                             required.
                         </Text>
-                        <Text className="text-base text-gray-700">
-                            If you did not request this change, please contact
-                            our support team immediately.
-                        </Text>
                         <Text className="text-base text-gray-700 mt-2">
                             Thank you for keeping your account secure.
                         </Text>

+ 3 - 10
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 (
         <Html>
@@ -37,15 +38,7 @@ export const ResetPasswordCode = ({ email, code, link }: Props) => {
             >
                 <Body className="font-sans">
                     <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <div className="flex items-center justify-between">
-                            <div className="text-sm font-bold text-orange-500">
-                                Pangolin
-                            </div>
-
-                            <div className="text-sm text-gray-500">
-                                {new Date().toLocaleDateString()}
-                            </div>
-                        </div>
+                        <LetterHead />
 
                         <Heading className="text-2xl font-semibold text-gray-800 text-center">
                             Password Reset Request

+ 6 - 14
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 (
         <Html>
@@ -43,27 +44,18 @@ export const ResourceOTPCode = ({
             >
                 <Body className="font-sans">
                     <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <div className="flex items-center justify-between">
-                            <div className="text-sm font-bold text-orange-500">
-                                Pangolin
-                            </div>
-
-                            <div className="text-sm text-gray-500">
-                                {new Date().toLocaleDateString()}
-                            </div>
-                        </div>
+                        <LetterHead />
 
                         <Heading className="text-2xl font-semibold text-gray-800 text-center">
-                            Your One-Time Password
+                            Your One-Time Password for {resourceName}
                         </Heading>
                         <Text className="text-base text-gray-700 mt-4">
                             Hi {email || "there"},
                         </Text>
                         <Text className="text-base text-gray-700 mt-2">
-                            You’ve requested a one-time password (OTP) to
-                            authenticate with the resource{" "}
+                            You’ve requested a one-time password to access{" "}
                             <strong>{resourceName}</strong> in{" "}
-                            <strong>{organizationName}</strong>. Use the OTP
+                            <strong>{organizationName}</strong>. Use the code
                             below to complete your authentication:
                         </Text>
                         <Section className="text-center">

+ 4 - 11
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 (
         <Html>
@@ -46,18 +47,10 @@ export const SendInviteLink = ({
             >
                 <Body className="font-sans">
                     <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <div className="flex items-center justify-between">
-                            <div className="text-sm font-bold text-orange-500">
-                                Pangolin
-                            </div>
-
-                            <div className="text-sm text-gray-500">
-                                {new Date().toLocaleDateString()}
-                            </div>
-                        </div>
+                        <LetterHead />
 
                         <Heading className="text-2xl font-semibold text-gray-800 text-center">
-                            You're Invite to Join {orgName}
+                            Invited to Join {orgName}
                         </Heading>
                         <Text className="text-base text-gray-700 mt-4">
                             Hi {email || "there"},

+ 2 - 13
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) => {
             >
                 <Body className="font-sans">
                     <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <div className="flex items-center justify-between">
-                            <div className="text-sm font-bold text-orange-500">
-                                Pangolin
-                            </div>
-
-                            <div className="text-sm text-gray-500">
-                                {new Date().toLocaleDateString()}
-                            </div>
-                        </div>
+                        <LetterHead />
 
                         <Heading className="text-2xl font-semibold text-gray-800 text-center">
                             Two-Factor Authentication{" "}
@@ -71,10 +64,6 @@ export const TwoFactorAuthNotification = ({ email, enabled }: Props) => {
                                 enabling it to protect your account.
                             </Text>
                         )}
-                        <Text className="text-base text-gray-700 mt-2">
-                            If you did not make this change, please contact our
-                            support team immediately.
-                        </Text>
                         <Text className="text-sm text-gray-500 mt-6">
                             Best regards,
                             <br />

+ 3 - 10
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 (
         <Html>
@@ -41,15 +42,7 @@ export const VerifyEmail = ({
             >
                 <Body className="font-sans">
                     <Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg">
-                        <div className="flex items-center justify-between">
-                            <div className="text-sm font-bold text-orange-500">
-                                Pangolin
-                            </div>
-
-                            <div className="text-sm text-gray-500">
-                                {new Date().toLocaleDateString()}
-                            </div>
-                        </div>
+                        <LetterHead />
 
                         <Heading className="text-2xl font-semibold text-gray-800 text-center">
                             Please Verify Your Email

+ 36 - 0
server/emails/templates/components/LetterHead.tsx

@@ -0,0 +1,36 @@
+import React from "react";
+
+export function LetterHead() {
+    return (
+        <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>
+    );
+}
+
+export default LetterHead;

+ 9 - 0
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 {

+ 1 - 1
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",

+ 4 - 1
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,

+ 2 - 2
src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx

@@ -304,8 +304,8 @@ export default function ReverseProxyTargets(props: {
                         {row.original.method}
                     </SelectTrigger>
                     <SelectContent>
-                        <SelectItem value="http">http</SelectItem>
-                        <SelectItem value="https">https</SelectItem>
+                        <SelectItem value="http">HTTP</SelectItem>
+                        <SelectItem value="https">HTTPS</SelectItem>
                     </SelectContent>
                 </Select>
             )

+ 17 - 2
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 (
         <Card className="w-full max-w-md">

+ 9 - 5
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 (
         <>
-            <InviteStatusCard type={cardType()} token={tokenParam} />
+            <InviteStatusCard type={type} token={tokenParam} />
         </>
     );
 }