diff --git a/README.md b/README.md index 9698c105..7c2a035c 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ After you click the "Register application" button you should see the `GITHUB_ID` After generating the secret, make sure you copy this value to your `.env` file as this value can not be seen again once you refresh the page. 👇 ![Screenshot 2022-10-25 at 08 26 04](https://user-images.githubusercontent.com/12615742/197710697-ef791d9e-b205-4667-a97c-477148917897.png) +### Setting up Passwordless auth locally + +In order to use Passwordless login locally you need to have a `ACCESS_KEY` and `SECRET_KEY` value. + +Niall has written a [tutorial](https://www.codu.co/articles/sending-emails-with-aws-ses-and-nodemailer-in-node-js-xfuucrri) on how to send emails with AWS SES and shows how to get these values. + +Check out the example .env file [here](./sample.env) to see how to populate these values + +**Note: Currenly the AWS region of the SNS service is hardcoded to "eu-west-1" it may be necessary to change this if your SNS service is in a different region** + ### NEXTAUTH_URL You shouldn't need to change the default value here. This is a variable used by Next Auth as the authentication URL to your site. diff --git a/app/(app)/auth/error/page.tsx b/app/(app)/auth/error/page.tsx new file mode 100644 index 00000000..1694171e --- /dev/null +++ b/app/(app)/auth/error/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import type { NextPage } from "next"; +import { useSearchParams } from "next/navigation"; +import { PostAuthPage } from "../page"; + +const Page: 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] : errorHeadings.unknown, + ); +}; + +export default Page; diff --git a/app/(app)/auth/page.tsx b/app/(app)/auth/page.tsx new file mode 100644 index 00000000..7500bd51 --- /dev/null +++ b/app/(app)/auth/page.tsx @@ -0,0 +1,72 @@ +"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, useState } from "react"; + +export const PostAuthPage = (content: { + heading: string; + subHeading: string; +}) => { + const [mounted, setMounted] = useState(false); + const { resolvedTheme } = useTheme(); + + // useEffect only happens on client not server + useEffect(() => { + setMounted(true); + }, []); + + // if on server dont render. needed to prevent a hydration mismatch error + if (!mounted) return null; + + 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 50c02ec2..07b115ef 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 { useEffect, 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,55 @@ const GetStarted: NextPage = () => {

- -
- +
+
+ +
+
+
+ + Or continue with + +
+
+ + )} + ); diff --git a/components/Theme/ThemeToggle/ThemeToggle.tsx b/components/Theme/ThemeToggle/ThemeToggle.tsx index 917e252a..53b62c19 100644 --- a/components/Theme/ThemeToggle/ThemeToggle.tsx +++ b/components/Theme/ThemeToggle/ThemeToggle.tsx @@ -4,7 +4,7 @@ import { MoonIcon, SunIcon } from "@heroicons/react/20/solid"; import { useTheme } from "next-themes"; import { useEffect, useState } from "react"; -const THEME_MODES = { +export const THEME_MODES = { DARK: "dark", LIGHT: "light", }; diff --git a/sample.env b/sample.env index 042dae50..a8f52624 100644 --- a/sample.env +++ b/sample.env @@ -1,4 +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 +DATABASE_URL=postgresql://postgres:secret@127.0.0.1:5432/postgres \ No newline at end of file diff --git a/server/auth.ts b/server/auth.ts index 7ce3dad2..95bd770d 100644 --- a/server/auth.ts +++ b/server/auth.ts @@ -1,12 +1,33 @@ -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"; import prisma from "@/server/db/client"; -import sendEmail from "@/utils/sendEmail"; +import sendEmail, { nodemailerSesTransporter } from "@/utils/sendEmail"; import { manageNewsletterSubscription } from "./lib/newsletter"; +import { createPasswordLessEmailTemplate } from "@/utils/createPasswordLessEmailTemplate"; + +const sendPasswordLessEmail = async (params: SendVerificationRequestParams) => { + const { identifier, url, provider } = params; + + try { + await nodemailerSesTransporter.sendMail({ + to: identifier, + 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`, + html: createPasswordLessEmailTemplate(url), + }); + } catch (error) { + throw new Error(`Sign in email could not be sent`); + } +}; export const authOptions: NextAuthOptions = { adapter: PrismaAdapter(prisma), @@ -15,10 +36,16 @@ export const authOptions: NextAuthOptions = { clientId: process.env.GITHUB_ID || "", clientSecret: process.env.GITHUB_SECRET || "", }), + EmailProvider({ + server: {}, + sendVerificationRequest: sendPasswordLessEmail, + }), ], pages: { signIn: "/get-started", newUser: "/settings", + verifyRequest: "/auth", + error: "/auth/error", // (used for any errors which occur during auth }, callbacks: { async session({ session, user }) { 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
+

+
+ + + + +
+
+ + + + + + + +
+
+
+
+
+
+ + + `; 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,