Skip to content

Commit

Permalink
refactor: decouple Next.js from core (#2857)
Browse files Browse the repository at this point in the history
* refactor: decouple Next.js from core (WIP)

* refactor: use `base` instead of `baseUrl`+`basePath`

* fix: signout route

* refactor(ts): convert files to TS

* fix: imports

* refactor: convert callback route

* fix: add `next` files to package

* chore(dev): alias npm email

* refactor: do not merge req with user options

* refactor: rename userOptions to options

* refactor: use native `URL` in `parseUrl`

* refactor: move Next.js specific code to `next` module

* refactor(ts): return `OutgoingResponse` on all routes

* fix: change `base` to `url`

* feat: introduce `getServerSession`

* refactor: move main logic to `handler` file

* chore(dev): showcase `getServerSession`

* feat: extract `sessionToken` from Authorization header

* fix: pass headers to getServerSession

* refactor: rename `server` to `core`

* refactor: re-export `next-auth/next` in `next-auth`

* fix: add `core` to npm package

* fix: re-export default method

* feat: return `body`+`header` instead of `json`,`text`

* feat: pass `NEXTAUTH_URL` as a variable to core

* refactor: simplify Next.js wrapper

* feat: export `client/_utils`

* fix(ts): suppress TS errors
  • Loading branch information
balazsorban44 committed Oct 27, 2021
1 parent 58a98b6 commit eb33c9d
Show file tree
Hide file tree
Showing 56 changed files with 1,429 additions and 974 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ node_modules
/client
/css
/lib
/server
/core
/jwt
/react
/adapters.d.ts
/index.d.ts
/index.js
/next

# Development app
app/src/css
Expand Down
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"copy:css": "cpx \"../css/**/*\" src/css --watch",
"watch:css": "cd .. && npm run watch:css",
"start": "next start",
"start:email": "npx fake-smtp-server"
"email": "npx fake-smtp-server",
"start:email": "email"
},
"license": "ISC",
"dependencies": {
Expand Down
7 changes: 2 additions & 5 deletions app/pages/_app.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { SessionProvider } from "next-auth/react"
import "./styles.css"

export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={session}>
<SessionProvider session={pageProps.session}>
<Component {...pageProps} />
</SessionProvider>
)
Expand Down
13 changes: 7 additions & 6 deletions app/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import NextAuth from "next-auth"
import NextAuth, { NextAuthOptions } from "next-auth"
import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
Expand Down Expand Up @@ -37,8 +37,7 @@ import AzureB2C from "next-auth/providers/azure-ad-b2c"
// domain: process.env.FAUNA_DOMAIN,
// })
// const adapter = FaunaAdapter(client)

export default NextAuth({
export const authOptions: NextAuthOptions = {
// adapter,
providers: [
// E-mail
Expand All @@ -58,8 +57,8 @@ export default NextAuth({
credentials: {
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
if (credentials.password === "password") {
async authorize(credentials) {
if (credentials.password === "pw") {
return {
name: "Fill Murray",
email: "bill@fillmurray.com",
Expand Down Expand Up @@ -179,4 +178,6 @@ export default NextAuth({
logo: "https://next-auth.js.org/img/logo/logo-sm.png",
brandColor: "#1786fb",
},
})
}

export default NextAuth(authOptions)
5 changes: 3 additions & 2 deletions app/pages/protected-ssr.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// This is an example of how to protect content using server rendering
import { getSession } from "next-auth/react"
import { getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"

Expand All @@ -25,7 +26,7 @@ export default function Page({ content, session }) {
}

export async function getServerSideProps(context) {
const session = await getSession(context)
const session = await getServerSession(context, authOptions)
let content = null

if (session) {
Expand Down
2 changes: 1 addition & 1 deletion config/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ module.exports = (api) => {
],
},
{
test: ["../src/server/pages/*.tsx"],
test: ["../src/core/pages/*.tsx"],
presets: ["preact"],
plugins: [
[
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
".": "./index.js",
"./jwt": "./jwt/index.js",
"./react": "./react/index.js",
"./core": "./core/index.js",
"./next": "./next/index.js",
"./client/_utils": "./client/_utils.js",
"./providers/*": "./providers/*.js"
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"clean": "rm -rf client css lib providers server jwt react index.d.ts index.js adapters.d.ts",
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts",
"build:js": "npm run clean && npm run generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
"dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i",
Expand All @@ -47,8 +50,10 @@
"css",
"jwt",
"react",
"next",
"client",
"providers",
"server",
"core",
"index.d.ts",
"index.js",
"adapters.d.ts"
Expand Down Expand Up @@ -137,7 +142,7 @@
"types",
".next",
"dist",
"/server",
"/core",
"/react.js"
],
"globals": {
Expand Down
File renamed without changes.
File renamed without changes.
210 changes: 210 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import logger from "../lib/logger"
import * as routes from "./routes"
import renderPage from "./pages"
import type { NextAuthOptions } from "./types"
import { init } from "./init"
import { Cookie } from "./lib/cookie"

import { NextAuthAction } from "../lib/types"

export interface IncomingRequest {
/** @default "http://localhost:3000" */
host?: string
method: string
cookies?: Record<string, any>
headers?: Record<string, any>
query?: Record<string, any>
body?: Record<string, any>
action: NextAuthAction
providerId?: string
error?: string
}

export interface NextAuthHeader {
key: string
value: string
}

export interface OutgoingResponse<
Body extends string | Record<string, any> | any[] = any
> {
status?: number
headers?: NextAuthHeader[]
body?: Body
redirect?: string
cookies?: Cookie[]
}

interface NextAuthHandlerParams {
req: IncomingRequest
options: NextAuthOptions
}

export async function NextAuthHandler<
Body extends string | Record<string, any> | any[]
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
const { options: userOptions, req } = params
const { action, providerId, error } = req

const { options, cookies } = await init({
userOptions,
action,
providerId,
host: req.host,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
isPost: req.method === "POST",
})

const sessionToken =
req.cookies?.[options.cookies.sessionToken.name] ||
req.headers?.Authorization?.replace("Bearer ", "")

const codeVerifier = req.cookies?.[options.cookies.pkceCodeVerifier.name]

if (req.method === "GET") {
const render = renderPage({ options, query: req.query, cookies })
const { pages } = options
switch (action) {
case "providers":
return (await routes.providers(options.providers)) as any
case "session":
return (await routes.session({ options, sessionToken })) as any
case "csrf":
return {
headers: [{ key: "Content-Type", value: "application/json" }],
body: { csrfToken: options.csrfToken } as any,
cookies,
}
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${options.callbackUrl}`
if (error) signinUrl = `${signinUrl}&error=${error}`
return { redirect: signinUrl, cookies }
}

return render.signin()
case "signout":
if (pages.signOut) return { redirect: pages.signOut, cookies }

return render.signout()
case "callback":
if (options.provider) {
const callback = await routes.callback({
body: req.body,
query: req.query,
method: req.method,
headers: req.headers,
options,
sessionToken,
codeVerifier,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "verify-request":
if (pages.verifyRequest) {
return { redirect: pages.verifyRequest, cookies }
}
return render.verifyRequest()
case "error":
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}

// These error messages are displayed in line on the sign in page
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}

return render.error({ error })
default:
}
} else if (req.method === "POST") {
switch (action) {
case "signin":
// Verified CSRF Token required for all sign in routes
if (options.csrfTokenVerified && options.provider) {
const signin = await routes.signin({
query: req.query,
body: req.body,
options,
})
if (signin.cookies) cookies.push(...signin.cookies)
return { ...signin, cookies }
}

return { redirect: `${options.url}/signin?csrf=true`, cookies }
case "signout":
// Verified CSRF Token required for signout
if (options.csrfTokenVerified) {
const signout = await routes.signout({ options, sessionToken })
if (signout.cookies) cookies.push(...signout.cookies)
return { ...signout, cookies }
}
return { redirect: `${options.url}/signout?csrf=true`, cookies }
case "callback":
if (options.provider) {
// Verified CSRF Token required for credentials providers only
if (
options.provider.type === "credentials" &&
!options.csrfTokenVerified
) {
return { redirect: `${options.url}/signin?csrf=true`, cookies }
}

const callback = await routes.callback({
body: req.body,
query: req.query,
method: req.method,
headers: req.headers,
options,
sessionToken,
codeVerifier,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "_log":
if (userOptions.logger) {
try {
const { code, level, ...metadata } = req.body ?? {}
logger[level](code, metadata)
} catch (error) {
// If logging itself failed...
logger.error("LOGGER_ERROR", error)
}
}
return {}
default:
}
}

return {
status: 400,
body: `Error: Action ${action} with HTTP ${req.method} is not supported by NextAuth.js` as any,
}
}
Loading

0 comments on commit eb33c9d

Please sign in to comment.