Skip to content

Commit

Permalink
feat: passwordless auth proof of concept
Browse files Browse the repository at this point in the history
  • Loading branch information
bamorim committed Mar 14, 2021
1 parent 2eb945a commit 604f30c
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ PAGARME_ENC_KEY=PAGARME_TEST_ENVIRONMENT_ECRYPTION_KEY
ROLLOUT_API_KEY=ROLLOUT_API_KEY
HEROKU_APP_NAME=reditus-local-SOME_RANDOM_NUMBER
[email protected]
MAILER_PASS=anything
MAILER_PASS=anything
MAILER_SERVICE=mailhog
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ services:
environment:
DATABASE_URL: 'postgres://postgres:postgres@postgres:5432/postgres?sslmode=disable'

email:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"

start_dependencies:
image: dadarek/wait-for-dependencies
depends_on:
Expand Down
53 changes: 32 additions & 21 deletions helpers/mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { serverRuntimeConfig } = getConfig();

const email: string = process.env.MAILER_EMAIL || "";
const pass: string = process.env.MAILER_PASS || "";
const service: string = process.env.MAILER_SERVICE || "mailhog";

/**
* Validates current environment and configs.
Expand All @@ -20,13 +21,6 @@ function hasValidConfigs(): boolean {
return false;
}

if (["reditus-next-production", "reditus-next-staging"].indexOf(env) == -1) {
console.log(
`Emails are only sent in production and staging. Currently in ${process.env.HEROKU_APP_NAME}`
);
return false;
}

if (!email || !pass) {
console.log("Mailing information not set. Credentials were not provided.");
return false;
Expand Down Expand Up @@ -60,13 +54,7 @@ export async function mailError(userEmail: string, error: any) {
return;
}

const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: email,
pass: pass,
},
});
const transporter = buildMailTransport();

const mailOptions = {
from: email,
Expand Down Expand Up @@ -107,13 +95,7 @@ export default async function mail(to: string, userName: string) {
{ name: userName },
(err: any, html: string) => {
if (err) throw err;
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: email,
pass: pass,
},
});
const transporter = buildMailTransport();

const mailOptions = {
from: email,
Expand All @@ -137,3 +119,32 @@ export default async function mail(to: string, userName: string) {
console.log(`Error while sending error email: ${err}`);
}
}

function buildMailTransport() {
const auth = {
user: email,
pass: pass,
};

return nodemailer.createTransport({
...getServiceOptions(service),
auth,
});
}

/**
* Get the nodemailer options based on configured service.
*
* This is used to switch between gmail for production and SMTP (to mailhog) in local development.
*
* @param {string} service The service being used currently.
* @return {object} The options for nodemailer
*/
function getServiceOptions(service: string) {
if (service !== "mailhog") return { service };

return {
host: "localhost",
port: 1025,
};
}
52 changes: 52 additions & 0 deletions pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth, { InitOptions } from "next-auth";
import Providers from "next-auth/providers";
import Adapters from "next-auth/adapters";
import { PrismaClient } from "@prisma/client";

// TODO: Use dotenv to setup test config.
const site = process.env.SITE || "http://localhost:3000";
const server =
process.env.EMAIL_SERVER || "smtp://mailhog:password@localhost:1025";
const from = process.env.EMAIL_FROM || "[email protected]";

const email: string = process.env.MAILER_EMAIL || "";
const pass: string = process.env.MAILER_PASS || "";
const prisma = new PrismaClient();

if (!site || !server || !from) throw new Error("Invalid auth configuration.");

// Ignore some eslint rules due to the way next-auth is setup (functions that have capitalized names)
/* eslint new-cap: ["error", { "capIsNewExceptions": ["Adapters.Prisma.Adapter", "Providers.Email", "NextAuth"] }] */

const baseAdapter = Adapters.Prisma.Adapter({ prisma });
async function notImplemented() {
throw new Error("Not implemented");
}
const adapter = {
...baseAdapter,
createUser: notImplemented,
deleteUser: notImplemented,
unlinkAccount: notImplemented,
};

const options: InitOptions = {
// Configure one or more authentication providers
providers: [
Providers.Email({
server: {
host: "localhost",
port: 1025,
auth: {
user: email,
pass,
},
},
from,
}),
],
adapter,
};

export default (req: NextApiRequest, res: NextApiResponse) =>
NextAuth(req, res, options);
24 changes: 24 additions & 0 deletions pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import Head from "next/head";
import { GridRow, GridCell } from "@rmwc/grid";
import { List, SimpleListItem } from "@rmwc/list";
import styles from "./index.module.css";
import { signIn, signOut, useSession } from "next-auth/client";

// Components
import { Form } from "../components/Form";

export default function Home() {
const [session, loading] = useSession();

return (
<div>
<Head>
Expand Down Expand Up @@ -60,6 +63,27 @@ export default function Home() {
>
<img src="./logoReditusWhite.png" />
<Form />
{/* Demo on how to use next-auth hooks. */}
{loading}
{!session && (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
<button
onClick={() =>
signIn("email", { email: "[email protected]" })
}
>
Sign in with specific Email
</button>
</>
)}
{session && (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)}
</GridCell>
</GridRow>
</main>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "email_verified" TIMESTAMP(3),
ADD COLUMN "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

-- CreateTable
CREATE TABLE "verification_requests" (
"id" SERIAL NOT NULL,
"identifier" TEXT NOT NULL,
"token" TEXT NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "sessions" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"expires" TIMESTAMP(3) NOT NULL,
"session_token" TEXT NOT NULL,
"access_token" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "verification_requests.token_unique" ON "verification_requests"("token");

-- CreateIndex
CREATE UNIQUE INDEX "sessions.session_token_unique" ON "sessions"("session_token");

-- CreateIndex
CREATE UNIQUE INDEX "sessions.access_token_unique" ON "sessions"("access_token");
25 changes: 25 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ model Contribution {
model User {
id Int @default(autoincrement()) @id
email String @unique
emailVerified DateTime? @map(name: "email_verified")
firstName String @map("first_name")
lastName String @map("last_name")
university String
Expand All @@ -66,6 +67,30 @@ model User {
contributions Contribution[]
subscriptions ContributionSubscription[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map("users")
}

model VerificationRequest {
id Int @default(autoincrement()) @id
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "verification_requests")
}

model Session {
id Int @default(autoincrement()) @id
userId Int @map(name: "user_id")
expires DateTime
sessionToken String @unique @map(name: "session_token")
accessToken String @unique @map(name: "access_token")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "sessions")
}

0 comments on commit 604f30c

Please sign in to comment.