Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding PasswordLess login to sign in options #785

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Comment on lines +107 to +115
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instructions for setting up Passwordless authentication locally are clear and helpful. Including a tutorial link for obtaining ACCESS_KEY and SECRET_KEY values is a thoughtful addition. However, the note about the hardcoded AWS region ("eu-west-1") is crucial. It's recommended to make this configuration more flexible by allowing it to be set via environment variables, enhancing the application's adaptability to different deployment environments.

- **Note: Currently 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**
+ **Note: Ensure to set the AWS region in your environment variables to match the region of your SNS service.**

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
### 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**
### 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: Ensure to set the AWS region in your environment variables to match the region of your SNS service.**

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because we dont set the AWS region in env variables. Dont want to add it as an env var as that would make this PR a breaking change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JohnAllenTech, understood. To ensure clarity for future contributors and users, it might be beneficial to document the hardcoded AWS region limitation in the README or relevant documentation. This can serve as a temporary measure until a more flexible solution is implemented. Appreciate your attention to maintaining the stability of the PR.


### 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.
Expand Down
34 changes: 34 additions & 0 deletions app/(app)/auth/error/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
72 changes: 72 additions & 0 deletions app/(app)/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="flex w-full flex-grow flex-col justify-center bg-neutral-100 px-4 py-20 dark:bg-black sm:px-6 lg:py-40">
<div className="flex flex-shrink-0 justify-center">
<Link href="/">
<span className="sr-only">Codú</span>
<Image
// Uses black codu logo if in light mode and white codu logo if in dark mode
src={
resolvedTheme === THEME_MODES.LIGHT
? "/images/codu-black.png"
: "/images/codu.png"
}
alt="Codú logo"
height={60}
width={189}
/>
</Link>
</div>
<div className="py-16">
<div className="text-center">
<p className="bg-gradient-to-r from-orange-400 to-pink-600 bg-clip-text text-xl font-semibold uppercase leading-6 tracking-wide text-transparent">
{content.heading}{" "}
</p>
<h1 className="mt-2 text-4xl font-extrabold tracking-tight text-black dark:text-white sm:text-5xl">
{content.subHeading}{" "}
</h1>
<div className="mt-6">
<Link
className="bg-gradient-to-r from-orange-400 to-pink-600 bg-clip-text text-base font-semibold tracking-wide text-transparent"
href="/"
>
Return home<span aria-hidden="true"> &rarr;</span>
</Link>
</div>
</div>
</div>
</main>
);
};

const Auth: NextPage = () => {
return PostAuthPage({
heading: "Sign in email has been sent",
subHeading: "See you soon 🚀",
});
};

export default Auth;
81 changes: 49 additions & 32 deletions app/(app)/get-started/_client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("");

const redirectTo =
typeof callbackUrl === "string" ? callbackUrl : "/articles";

Expand Down Expand Up @@ -42,39 +43,55 @@ const GetStarted: NextPage = () => {
</Link>
</p>
</div>

<div>
<button
type="button"
onClick={async () => {
await signIn("github", { callbackUrl: redirectTo });
}}
className="group relative inline-flex w-full justify-center rounded-md border border-transparent bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2"
>
<span className="absolute inset-y-0 left-0 flex items-center pl-3">
<LockClosedIcon
className="h-5 w-5 text-orange-600 group-hover:text-white"
aria-hidden="true"
{!!process?.env.NEXT_PUBLIC_ALPHA && (
<>
<div>
<input
className="w-full flex-auto appearance-none rounded-md border-neutral-200 bg-white pl-6 font-medium text-neutral-950 ring-offset-0 placeholder:text-xl placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-transparent focus:ring-offset-2 dark:border-none dark:bg-neutral-900 dark:bg-neutral-900 dark:text-white dark:text-white [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden"
placeholder="Enter your email"
onChange={(event) => {
setUserEmail(event.target.value);
}}
value={userEmail}
/>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-3">
<span className="sr-only">Sign in with GitHub</span>
<svg
className="h-5 w-5 text-pink-800 group-hover:text-white"
<button
type="button"
disabled={!userEmail}
onClick={async () => {
await signIn("email", {
callbackUrl: redirectTo,
email: userEmail,
});
}}
className="group relative mt-6 inline-flex w-full justify-center rounded-md border border-transparent bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-xl font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2"
>
Sign In / Sign Up
</button>
</div>
<div className="relative">
<div
className="absolute inset-0 flex items-center"
aria-hidden="true"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fillRule="evenodd"
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
clipRule="evenodd"
/>
</svg>
</span>
Login with GitHub
</button>
</div>
<div className="w-full border-t border-black dark:border-gray-200" />
</div>
<div className="relative flex justify-center text-sm font-medium leading-6">
<span className="bg-neutral-100 px-6 text-xl text-neutral-900 dark:bg-black dark:text-white">
Or continue with
</span>
</div>
</div>
</>
)}
<button
type="button"
onClick={async () => {
await signIn("github", { callbackUrl: redirectTo });
}}
className="group relative inline-flex w-full justify-center rounded-md border border-transparent bg-gradient-to-r from-orange-400 to-pink-600 px-4 py-2 text-xl font-medium text-white shadow-sm hover:from-orange-300 hover:to-pink-500 focus:outline-none focus:ring-2 focus:ring-pink-300 focus:ring-offset-2"
>
GitHub
</button>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion components/Theme/ThemeToggle/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed this exported so I could use it to determine the coco log that should be displayed. ie black or white

DARK: "dark",
LIGHT: "light",
};
Expand Down
2 changes: 1 addition & 1 deletion sample.env
Original file line number Diff line number Diff line change
@@ -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:[email protected]:5432/postgres
DATABASE_URL=postgresql://postgres:[email protected]:5432/postgres
31 changes: 29 additions & 2 deletions server/auth.ts
Original file line number Diff line number Diff line change
@@ -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),
Expand All @@ -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 }) {
Expand Down
Loading