diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js index 6696436b..65fc0419 100644 --- a/pages/api/auth/[...nextauth].js +++ b/pages/api/auth/[...nextauth].js @@ -1,8 +1,84 @@ import NextAuth from "next-auth"; -import { TypeORMLegacyAdapter } from "@next-auth/typeorm-legacy-adapter"; import EmailProvider from "next-auth/providers/email"; -import * as entities from "../../../lib/entities"; +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import { PrismaClient } from "@prisma/client"; +import nodemailer from "nodemailer"; +const prisma = new PrismaClient(); + +// async function sendVerificationRequest({ +// identifier: email, +// url, +// provider: { server, from }, +// }) { +// const { host } = new URL(url); +// const transport = nodemailer.createTransport(server); +// await transport.sendMail({ +// to: email, +// from, +// subject: `Sign in to ${host}`, +// text: text({ url, host }), +// html: html({ url, host, email }), +// }); +// } +// Email HTML body +function html({ url, host, email }) { + // Insert invisible space into domains and email address to prevent both the + // email address and the domain from being turned into a hyperlink by email + // clients like Outlook and Apple mail, as this is confusing because it seems + // like they are supposed to click on their email address to sign in. + const escapedEmail = `${email.replace(/\./g, "​.")}`; + const escapedHost = `${host.replace(/\./g, "​.")}`; + + // Some simple styling options + const backgroundColor = "#ffffff"; + const textColor = "#444444"; + const mainBackgroundColor = "#ffffff"; + const buttonBackgroundColor = "#aa1111"; + const buttonBorderColor = "#aa1111"; + const buttonTextColor = "#ffffff"; + const headerBackgroundColor = "#000000"; + + return ` + + + + + + + + + + + + + +
+
+ Sign in as ${escapedEmail} +
+ + + + +
Sign in
+
+ If you did not request this email you can safely ignore it. +
+ + `; +} + +// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones) +function text({ url, host }) { + return `Sign in to PEDW - Planning Casework\n${url}\n\n`; +} // For more information on each option (and a full list of options) go to // https://next-auth.js.org/configuration/options export default NextAuth({ @@ -19,19 +95,35 @@ export default NextAuth({ }, from: process.env.EMAIL_FROM, maxAge: 10 * 60, // Magic links are valid for 10 min only + async sendVerificationRequest({ + identifier: email, + url, + provider: { server, from }, + }) { + const { host } = new URL(url); + const transport = nodemailer.createTransport(server); + await transport.sendMail({ + to: email, + from, + subject: `Sign in to PEDW - Planning Casework`, + text: text({ url, host }), + html: html({ url, host, email }), + }); + }, }), ], - adapter: TypeORMLegacyAdapter({ - type: "mysql", - database: - "mysql://nextAuthAdmin:!Sky9fish1972@77.68.17.157:3306/nextauthDB?synchronise=true", - synchronize: false, - }), - secret: "SuperSecret", + adapter: PrismaAdapter(prisma), session: { jwt: true, pages: {}, theme: "dark", debug: true, }, + pages: { + signIn: "/auth/signin", + //signOut: "/auth/signout", + error: "/auth/error", // Error code passed in query string as ?error= + verifyRequest: "/auth/verify-request", // (used for check email message) + // newUser: "/auth/new-user", // New users will be directed here on first sign in (leave the property out if not of interest) + }, }); diff --git a/pages/auth/error.js b/pages/auth/error.js new file mode 100644 index 00000000..2d0113b0 --- /dev/null +++ b/pages/auth/error.js @@ -0,0 +1,80 @@ +import useTranslation from "next-translate/useTranslation"; +import Head from "next/head"; +import Link from "next/link"; +import CookieBanner from "../../components/cookieBanner"; +import Footer from "../../components/footer"; +import Header from "../../components/header"; +import Breadcrumbs from "../../components/breadcrumbs"; +import { useRouter } from "next/router"; + +import { getCsrfToken } from "next-auth/react"; + +const Error = (props) => { + let { t, lang } = useTranslation(); + const { footerLinks, pages, csrfToken } = props; + + const { error } = useRouter().query; + + const errors = { + Signin: "Try signing with a different account.", + OAuthSignin: "Try signing with a different account.", + OAuthCallback: "Try signing with a different account.", + OAuthCreateAccount: "Try signing with a different account.", + EmailCreateAccount: "Try signing with a different account.", + Callback: "Try signing with a different account.", + OAuthAccountNotLinked: + "To confirm your identity, sign in with the same account you used originally.", + EmailSignin: "Check your email address.", + CredentialsSignin: + "Sign in failed. Check the details you provided are correct.", + Verification: + "Unable to sign in. The sign in link is no longer valid.It may have been used already or it may have expired. ", + default: "Unable to sign in.", + }; + + const errorMessage = error && (errors[error] ?? errors.default); + + return ( +
+ + Sign in Error - {t("common:service-name")} + +
+ +
+
+ + +
+
+
+

There has been an error

+
+
+
+
+ {errorMessage} +
+
+
+
+ {" "} + Return to Home +
+
+
+
+
+
+ ); +}; + +export async function getServerSideProps(context) { + const csrfToken = await getCsrfToken(context); + return { + props: { csrfToken }, + }; +} + +export default Error; diff --git a/pages/auth/signin.js b/pages/auth/signin.js new file mode 100644 index 00000000..e389cf3b --- /dev/null +++ b/pages/auth/signin.js @@ -0,0 +1,82 @@ +import useTranslation from "next-translate/useTranslation"; +import Head from "next/head"; +import Link from "next/link"; +import CookieBanner from "../../components/cookieBanner"; +import Footer from "../../components/footer"; +import Header from "../../components/header"; +import Breadcrumbs from "../../components/breadcrumbs"; +import { getCsrfToken } from "next-auth/react"; + +const SignIn = (props) => { + let { t, lang } = useTranslation(); + const { footerLinks, pages, csrfToken } = props; + + return ( +
+ + Sign in - {t("common:service-name")} + +
+ +
+
+ +
+
+
+

Sign in

+
+
+
+
+
+
+ + + +
+
+ +
+
+
+
+
+
+
+
+ ); +}; + +export async function getServerSideProps(context) { + const csrfToken = await getCsrfToken(context); + return { + props: { csrfToken }, + }; +} + +export default SignIn; diff --git a/pages/auth/verify-request.js b/pages/auth/verify-request.js new file mode 100644 index 00000000..53708db1 --- /dev/null +++ b/pages/auth/verify-request.js @@ -0,0 +1,59 @@ +import useTranslation from "next-translate/useTranslation"; +import Head from "next/head"; +import Link from "next/link"; +import CookieBanner from "../../components/cookieBanner"; +import Footer from "../../components/footer"; +import Header from "../../components/header"; +import Breadcrumbs from "../../components/breadcrumbs"; + +import { getCsrfToken } from "next-auth/react"; + +const SignIn = (props) => { + let { t, lang } = useTranslation(); + const { footerLinks, pages, csrfToken } = props; + + return ( +
+ + Sign in - {t("common:service-name")} + +
+ +
+
+ + +
+
+
+

Check your email

+
+
+
+
+ A link has been sent to your email so you can + continue +
+
+
+
+ {" "} + Return to Home +
+
+
+
+
+
+ ); +}; + +export async function getServerSideProps(context) { + const csrfToken = await getCsrfToken(context); + return { + props: { csrfToken }, + }; +} + +export default SignIn;