From 02c8e8dae0f79a2dcab0a949924b276d09b43485 Mon Sep 17 00:00:00 2001 From: John Allen Date: Mon, 26 Feb 2024 23:25:28 +0000 Subject: [PATCH 01/11] chore: added passwordless login and altered signup page --- app/(app)/get-started/_client.tsx | 66 ++- server/auth.ts | 40 +- utils/createPasswordLessEmailTemplate.ts | 684 +++++++++++++++++++++++ 3 files changed, 762 insertions(+), 28 deletions(-) create mode 100644 utils/createPasswordLessEmailTemplate.ts diff --git a/app/(app)/get-started/_client.tsx b/app/(app)/get-started/_client.tsx index 50c02ec2..3ed56b1b 100644 --- a/app/(app)/get-started/_client.tsx +++ b/app/(app)/get-started/_client.tsx @@ -5,13 +5,14 @@ import Image from "next/image"; import Link from "next/link"; import { signIn } from "next-auth/react"; import { useSearchParams } from "next/navigation"; - -import { LockClosedIcon } from "@heroicons/react/20/solid"; +import { useState } from "react"; const GetStarted: NextPage = () => { const searchParams = useSearchParams(); const callbackUrl = searchParams?.get("callbackUrl"); + const [userEmail, setUserEmail] = useState(""); + const redirectTo = typeof callbackUrl === "string" ? callbackUrl : "/articles"; @@ -42,39 +43,50 @@ const GetStarted: NextPage = () => {

-
+ { + setUserEmail(event.target.value); + }} + value={userEmail} + />
+
+ +
); diff --git a/server/auth.ts b/server/auth.ts index 7ce3dad2..aafd549a 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -1,5 +1,8 @@ -import { type NextAuthOptions, getServerSession } from "next-auth"; +import { type NextAuthOptions, getServerSession, Theme } from "next-auth"; import GitHubProvider from "next-auth/providers/github"; +import EmailProvider, { + SendVerificationRequestParams, +} from "next-auth/providers/email"; import { PrismaAdapter } from "@next-auth/prisma-adapter"; import { createWelcomeEmailTemplate } from "@/utils/createEmailTemplate"; import * as Sentry from "@sentry/nextjs"; @@ -7,6 +10,28 @@ import * as Sentry from "@sentry/nextjs"; import prisma from "@/server/db/client"; import sendEmail from "@/utils/sendEmail"; import { manageNewsletterSubscription } from "./lib/newsletter"; +import { createTransport } from "nodemailer"; +import { createPasswordLessEmailTemplate } from "@/utils/createPasswordLessEmailTemplate"; + +const sendPassworldLessEmail = async ( + params: SendVerificationRequestParams, +) => { + const { identifier, url, provider } = params; + const transport = createTransport(provider.server); + + const result = await transport.sendMail({ + to: identifier, + from: provider.from, + subject: `Sign in to Codú 🚀`, + /** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */ + text: `Sign in to Codú 🚀\n\n`, + html: createPasswordLessEmailTemplate(url), + }); + const failed = result.rejected.concat(result.pending).filter(Boolean); + if (failed.length) { + throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`); + } +}; export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), @@ -15,7 +40,20 @@ export const authOptions: NextAuthOptions = { clientId: process.env.GITHUB_ID || "", clientSecret: process.env.GITHUB_SECRET || "", }), + EmailProvider({ + server: { + host: process.env.EMAIL_SERVER_HOST, + port: process.env.EMAIL_SERVER_PORT, + auth: { + user: process.env.EMAIL_SERVER_USER, + pass: process.env.EMAIL_SERVER_PASSWORD, + }, + }, + sendVerificationRequest: sendPassworldLessEmail, + from: process.env.EMAIL_FROM, + }), ], + secret: "testsecret", pages: { signIn: "/get-started", newUser: "/settings", diff --git a/utils/createPasswordLessEmailTemplate.ts b/utils/createPasswordLessEmailTemplate.ts new file mode 100644 index 00000000..6e31c49f --- /dev/null +++ b/utils/createPasswordLessEmailTemplate.ts @@ -0,0 +1,684 @@ +export const createPasswordLessEmailTemplate = (url: string | null) => + ` + + + + + + + + + New message + + + + + + +
+ + + + + +
+ + + + +
+ + + + +
+ + + + +
+ + + + + + + + + + + + + + + + +
+

+ Dia dhuit! (Hello in Irish)

+

+
+ Click here to sign into Codú +
+

+

Happy coding! 😊

Niall & the Codú community
+

+
+ + + + +
+
+ + + + + + + +
+
+
+
+
+
+ + + `; From 370c1d2ef0a8d2c0150dda08bc60a7d2189cd0d5 Mon Sep 17 00:00:00 2001 From: John Allen Date: Wed, 28 Feb 2024 12:27:29 +0000 Subject: [PATCH 02/11] chore: fixed darkmode issue with error post passwordLess signin --- app/(app)/auth/error/page.tsx | 32 ++++++++ app/(app)/auth/page.tsx | 67 ++++++++++++++++ app/(app)/get-started/_client.tsx | 82 +++++++++++--------- components/Theme/ThemeToggle/ThemeToggle.tsx | 2 +- sample.env | 6 ++ server/auth.ts | 3 + 6 files changed, 155 insertions(+), 37 deletions(-) create mode 100644 app/(app)/auth/error/page.tsx create mode 100644 app/(app)/auth/page.tsx diff --git a/app/(app)/auth/error/page.tsx b/app/(app)/auth/error/page.tsx new file mode 100644 index 00000000..1fba502d --- /dev/null +++ b/app/(app)/auth/error/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import type { NextPage } from "next"; +import { useSearchParams } from "next/navigation"; +import { PostAuthPage } from "../page"; + +const Auth: NextPage = () => { + const errorHeadings = { + // sign in link already used or expired + verification: { + heading: "The sign in link is no longer valid", + subHeading: "It may have been used already or it may have expired", + }, + // issue with email provider. SNS may be down or something similiar + emailsignin: { + heading: "Well this is embarrassing", + subHeading: "Something unexpected happened, we will look into it", + }, + // if specific error param is not found give the generic + unknown: { + heading: "Oops... Not sure what happened there", + subHeading: "Please try again later", + }, + }; + // Checking does the error query param exist and if it doesnt replacing with unknown + const error = (useSearchParams().get("error")?.toLowerCase() ?? + "unknown") as keyof typeof errorHeadings; + // Checking does the error query param has been covered in errorHeading if not falling back to unknown + return PostAuthPage(errorHeadings[error] ?? errorHeadings[error]); +}; + +export default Auth; diff --git a/app/(app)/auth/page.tsx b/app/(app)/auth/page.tsx new file mode 100644 index 00000000..34778240 --- /dev/null +++ b/app/(app)/auth/page.tsx @@ -0,0 +1,67 @@ +"use client"; + +import type { NextPage } from "next"; +import Link from "next/link"; +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { THEME_MODES } from "@/components/Theme/ThemeToggle/ThemeToggle"; +import { useEffect } from "react"; + +export const PostAuthPage = (content: { + heading: string; + subHeading: string; +}) => { + const { resolvedTheme, setTheme } = useTheme(); + + useEffect(() => { + if (resolvedTheme) setTheme(resolvedTheme); + }, [resolvedTheme, setTheme]); + + return ( +
+
+ + Codú + Codú logo + +
+
+
+

+ {content.heading}{" "} +

+

+ {content.subHeading}{" "} +

+
+ + Return home + +
+
+
+
+ ); +}; + +const Auth: NextPage = () => { + return PostAuthPage({ + heading: "Sign in email has been sent", + subHeading: "See you soon 🚀", + }); +}; + +export default Auth; diff --git a/app/(app)/get-started/_client.tsx b/app/(app)/get-started/_client.tsx index 3ed56b1b..971c4151 100644 --- a/app/(app)/get-started/_client.tsx +++ b/app/(app)/get-started/_client.tsx @@ -5,13 +5,19 @@ import Image from "next/image"; import Link from "next/link"; import { signIn } from "next-auth/react"; import { useSearchParams } from "next/navigation"; -import { useState } from "react"; +import { useEffect, useState } from "react"; const GetStarted: NextPage = () => { const searchParams = useSearchParams(); const callbackUrl = searchParams?.get("callbackUrl"); const [userEmail, setUserEmail] = useState(""); + const [isAlpha, setIsAlpha] = useState(); + + useEffect(() => { + // toLowerCase can maybe be removed. Just want to make sure its not TRUE in Prod + setIsAlpha(process.env.ALPHA?.toLowerCase() === "true"); + }, []); const redirectTo = typeof callbackUrl === "string" ? callbackUrl : "/articles"; @@ -43,41 +49,45 @@ const GetStarted: NextPage = () => {

-
- { - setUserEmail(event.target.value); - }} - value={userEmail} - /> - -
-
- + {isAlpha && ( + <> +
+ { + setUserEmail(event.target.value); + }} + value={userEmail} + /> + +
+
+ + + )}
- {isAlpha && ( + {true && ( <>
{ setUserEmail(event.target.value); @@ -78,10 +78,10 @@ const GetStarted: NextPage = () => { className="absolute inset-0 flex items-center" aria-hidden="true" > -
+
- + Or continue with
From e5e93442f38af67778eeeec6066898a1217a9e56 Mon Sep 17 00:00:00 2001 From: John Allen Date: Wed, 28 Feb 2024 20:21:32 +0000 Subject: [PATCH 04/11] chore: readding alpha flags --- app/(app)/get-started/_client.tsx | 3 +-- server/auth.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/(app)/get-started/_client.tsx b/app/(app)/get-started/_client.tsx index 158da572..14d2c68c 100644 --- a/app/(app)/get-started/_client.tsx +++ b/app/(app)/get-started/_client.tsx @@ -18,7 +18,6 @@ const GetStarted: NextPage = () => { // toLowerCase can maybe be removed. Just want to make sure its not TRUE in Prod setIsAlpha(process.env.ALPHA?.toLowerCase() === "true"); }, []); - const redirectTo = typeof callbackUrl === "string" ? callbackUrl : "/articles"; @@ -49,7 +48,7 @@ const GetStarted: NextPage = () => {

- {true && ( + {isAlpha && ( <>
Date: Wed, 28 Feb 2024 22:29:17 +0000 Subject: [PATCH 05/11] Update app/(app)/auth/error/page.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- app/(app)/auth/error/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/(app)/auth/error/page.tsx b/app/(app)/auth/error/page.tsx index 1fba502d..a23f7761 100644 --- a/app/(app)/auth/error/page.tsx +++ b/app/(app)/auth/error/page.tsx @@ -26,7 +26,7 @@ const Auth: NextPage = () => { const error = (useSearchParams().get("error")?.toLowerCase() ?? "unknown") as keyof typeof errorHeadings; // Checking does the error query param has been covered in errorHeading if not falling back to unknown - return PostAuthPage(errorHeadings[error] ?? errorHeadings[error]); + return PostAuthPage(errorHeadings[error] ? errorHeadings[error] : errorHeadings.unknown); }; export default Auth; From 25289b9795eff2b355ffd465520de3cdf6a97705 Mon Sep 17 00:00:00 2001 From: John Allen Date: Wed, 28 Feb 2024 22:39:02 +0000 Subject: [PATCH 06/11] chore: added isMounted check --- app/(app)/auth/error/page.tsx | 8 +++++--- app/(app)/auth/page.tsx | 13 +++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/(app)/auth/error/page.tsx b/app/(app)/auth/error/page.tsx index a23f7761..1694171e 100644 --- a/app/(app)/auth/error/page.tsx +++ b/app/(app)/auth/error/page.tsx @@ -4,7 +4,7 @@ import type { NextPage } from "next"; import { useSearchParams } from "next/navigation"; import { PostAuthPage } from "../page"; -const Auth: NextPage = () => { +const Page: NextPage = () => { const errorHeadings = { // sign in link already used or expired verification: { @@ -26,7 +26,9 @@ const Auth: NextPage = () => { const error = (useSearchParams().get("error")?.toLowerCase() ?? "unknown") as keyof typeof errorHeadings; // Checking does the error query param has been covered in errorHeading if not falling back to unknown - return PostAuthPage(errorHeadings[error] ? errorHeadings[error] : errorHeadings.unknown); + return PostAuthPage( + errorHeadings[error] ? errorHeadings[error] : errorHeadings.unknown, + ); }; -export default Auth; +export default Page; diff --git a/app/(app)/auth/page.tsx b/app/(app)/auth/page.tsx index 34778240..7500bd51 100644 --- a/app/(app)/auth/page.tsx +++ b/app/(app)/auth/page.tsx @@ -5,17 +5,22 @@ import Link from "next/link"; import Image from "next/image"; import { useTheme } from "next-themes"; import { THEME_MODES } from "@/components/Theme/ThemeToggle/ThemeToggle"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; export const PostAuthPage = (content: { heading: string; subHeading: string; }) => { - const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const { resolvedTheme } = useTheme(); + // useEffect only happens on client not server useEffect(() => { - if (resolvedTheme) setTheme(resolvedTheme); - }, [resolvedTheme, setTheme]); + setMounted(true); + }, []); + + // if on server dont render. needed to prevent a hydration mismatch error + if (!mounted) return null; return (
From 5623b4b5ecb42126c7d940f0bd601dca5f315dc3 Mon Sep 17 00:00:00 2001 From: John Allen Date: Wed, 28 Feb 2024 22:40:30 +0000 Subject: [PATCH 07/11] chore: addressing pr comments --- app/(app)/get-started/_client.tsx | 1 + server/auth.ts | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/(app)/get-started/_client.tsx b/app/(app)/get-started/_client.tsx index 14d2c68c..935071a2 100644 --- a/app/(app)/get-started/_client.tsx +++ b/app/(app)/get-started/_client.tsx @@ -61,6 +61,7 @@ const GetStarted: NextPage = () => { />
- {isAlpha && ( + {!!process?.env.NEXT_PUBLIC_ALPHA && ( <>
{ const { identifier, url, provider } = params; - const transport = createTransport(provider.server); - const result = await transport.sendMail({ - to: identifier, - from: provider.from, - subject: `Sign in to Codú 🚀`, - /** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */ - text: `Sign in to Codú 🚀\n\n`, - html: createPasswordLessEmailTemplate(url), - }); - const failed = result.rejected.concat(result.pending).filter(Boolean); - if (failed.length) { - throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`); + try { + await nodemailerSesTransporter.sendMail({ + to: identifier, + from: provider.from, + subject: `Sign in to Codú 🚀`, + /** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */ + text: `Sign in to Codú 🚀\n\n`, + html: createPasswordLessEmailTemplate(url), + }); + } catch (error) { + throw new Error(`Sign in email could not be sent`); } }; @@ -39,14 +37,7 @@ export const authOptions: NextAuthOptions = { clientSecret: process.env.GITHUB_SECRET || "", }), EmailProvider({ - server: { - host: process.env.EMAIL_SERVER_HOST, - port: process.env.EMAIL_SERVER_PORT, - auth: { - user: process.env.EMAIL_SERVER_USER, - pass: process.env.EMAIL_SERVER_PASSWORD, - }, - }, + server: {}, sendVerificationRequest: sendPasswordLessEmail, from: process.env.EMAIL_FROM, }), diff --git a/utils/sendEmail.ts b/utils/sendEmail.ts index 623d6c81..f8715dcd 100644 --- a/utils/sendEmail.ts +++ b/utils/sendEmail.ts @@ -18,7 +18,7 @@ const ses = new aws.SES({ }); // create Nodemailer SES transporter -const transporter = nodemailer.createTransport({ +export const nodemailerSesTransporter = nodemailer.createTransport({ SES: { ses, aws }, }); @@ -36,7 +36,7 @@ const sendEmail = async (config: MailConfig) => { const to = emailSchema.parse(recipient); // send some mail return new Promise((resolve, reject) => { - transporter.sendMail( + nodemailerSesTransporter.sendMail( { from: "hi@codu.co", to, From 8d2bca531f92afd7e6a833b7f5e12afe6c39d3c4 Mon Sep 17 00:00:00 2001 From: John Allen Date: Fri, 8 Mar 2024 11:32:34 +0000 Subject: [PATCH 10/11] chore: removing unused sample.env vars --- sample.env | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/sample.env b/sample.env index 3f974cd4..a8f52624 100644 --- a/sample.env +++ b/sample.env @@ -1,10 +1,4 @@ GITHUB_ID= ### Replace with GitHub OAuth ID (https://github.com/settings/applications/new) GITHUB_SECRET= ### Replace with GitHub OAuth Secret (https://github.com/settings/applications/new) NEXTAUTH_URL=http://localhost:3000/api/auth -DATABASE_URL=postgresql://postgres:secret@127.0.0.1:5432/postgres - -EMAIL_SERVER_USER= ### Replace with SNS username -EMAIL_SERVER_PASSWORD= ### Replace with SNS password -EMAIL_SERVER_HOST= ### Replace with AWS host -EMAIL_SERVER_PORT=587 -EMAIL_FROM= ### Replace with email address to deliver passwordless email from +DATABASE_URL=postgresql://postgres:secret@127.0.0.1:5432/postgres \ No newline at end of file From 19379a8b771118eb454d887c690df6e7cd377d72 Mon Sep 17 00:00:00 2001 From: John Allen Date: Sat, 9 Mar 2024 12:41:41 +0000 Subject: [PATCH 11/11] removed newly introduced env variable --- server/auth.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/auth.ts b/server/auth.ts index fd5543ed..95bd770d 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -18,7 +18,7 @@ const sendPasswordLessEmail = async (params: SendVerificationRequestParams) => { try { await nodemailerSesTransporter.sendMail({ to: identifier, - from: provider.from, + from: process.env.ADMIN_EMAIL, subject: `Sign in to Codú 🚀`, /** Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) */ text: `Sign in to Codú 🚀\n\n`, @@ -39,7 +39,6 @@ export const authOptions: NextAuthOptions = { EmailProvider({ server: {}, sendVerificationRequest: sendPasswordLessEmail, - from: process.env.EMAIL_FROM, }), ], pages: {