@heymarco/next-auth
Version:
A complete authentication solution for web applications.
1,020 lines (1,003 loc) • 49.6 kB
JavaScript
// react:
import {
// react:
default as React, } from 'react';
import { NextResponse as NextResponseFix, } from 'next/server';
// auth-js:
// import {
// defaultCookies,
// } from '@auth/core/lib/utils/cookie.js'
import {
// cryptos:
encode, decode, } from '@auth/core/jwt';
// next-auth:
import {
// routers:
default as NextAuthFix, } from 'next-auth';
// import {
// defaultCookies,
// } from 'next-auth/core/lib/cookie.js'
// credentials providers:
import { default as CredentialsProviderFix, } from '@auth/core/providers/credentials';
// webs:
import { default as nodemailer, } from 'nodemailer';
// cryptos:
import { randomUUID, } from 'crypto';
// formats:
import { default as moment, } from 'moment';
// templates:
import { BusinessContextProvider, } from './templates/businessDataContext.js';
import {
// react components:
UserContextProvider, } from './templates/userContext.js';
import {
// react components:
EmailConfirmationContextProvider, } from './templates/emailConfirmationContext.js';
import {
// react components:
PasswordResetContextProvider, } from './templates/passwordResetContext.js';
import { signUpPath, passwordResetPath, usernameValidationPath, emailValidationPath, passwordValidationPath, emailConfirmationPath, } from './api-paths.js';
// utilities:
const dataKey = Symbol();
const getRequestData = async (req) => {
switch (req.method) {
case 'GET':
case 'PUT':
return Object.fromEntries(new URL(req.url, 'https://localhost/').searchParams.entries());
case 'POST':
case 'PATCH': {
if (dataKey in req)
return req[dataKey];
const data = await req.json();
req[dataKey] = data;
return data;
}
} // switch
};
const NextResponse = (NextResponseFix.default
??
NextResponseFix);
const NextAuth = (NextAuthFix.default
??
NextAuthFix);
const CredentialsProvider = (CredentialsProviderFix.default
??
CredentialsProviderFix);
const createNextAuthHandler = (options) => {
// options:
const { adapter, authConfigServer, credentialsConfigServer, callbacks, } = options;
const { business: { name: businessName, url: businessUrl, }, signUp: { enabled: signUpEnabled, }, signIn: { requireVerifiedEmail: signInRequireVerifiedEmail, failureMaxAttempts: signInFailureMaxAttempts, failureLockDuration: signInFailureLockDuration, path: signInPath, }, reset: { enabled: resetEnabled, throttle: resetThrottle, maxAge: resetMaxAge, }, session: { maxAge: sessionMaxAge, updateAge: sessionUpdateAge, }, oAuthProviders, emails: { signUp: { host: emailSignUpHost, port: emailSignUpPort, secure: emailSignUpSecure, username: emailSignUpUsername, password: emailSignUpPassword, from: emailSignUpFrom, subject: emailSignUpSubject, message: emailSignUpMessage, }, reset: { host: emailResetHost, port: emailResetPort, secure: emailResetSecure, username: emailResetUsername, password: emailResetPassword, from: emailResetFrom, subject: emailResetSubject, message: emailResetMessage, }, }, } = authConfigServer;
const { name: { minLength: nameMinLength, maxLength: nameMaxLength, }, email: { minLength: emailMinLength, maxLength: emailMaxLength, format: emailFormat, }, username: { minLength: usernameMinLength, maxLength: usernameMaxLength, format: usernameFormat, prohibited: usernameProhibited, }, password: { minLength: passwordMinLength, maxLength: passwordMaxLength, hasUppercase: passwordHasUppercase, hasLowercase: passwordHasLowercase, prohibited: passwordProhibited, }, } = credentialsConfigServer;
//#region configs
const session = {
strategy: 'database',
maxAge: sessionMaxAge * 60 * 60 /* convert hours to seconds */,
updateAge: sessionUpdateAge * 60 * 60 /* convert hours to seconds */,
generateSessionToken() {
return randomUUID();
},
};
const authOptions = {
adapter: adapter,
session: session,
providers: [
// credentials providers:
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: 'Username or Email', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials, req) {
// conditions:
if (!credentials)
return null; // a credentials must be provided to be verified
// get user by valid credentials:
try {
const now = new Date();
const result = await adapter.credentialsSignIn(credentials, {
now: now,
requireEmailVerified: signInRequireVerifiedEmail,
failureMaxAttempts: signInFailureMaxAttempts,
failureLockDuration: signInFailureLockDuration,
});
if (result === null)
return null;
if (result === false) {
console.log('EMAIL UNVERIFIED', result); // TODO: remove log
throw Error(`Your email has not been verified. Please activate your account by clicking on the link sent to your email.`);
}
if (result instanceof Date) {
console.log('LOGIN LOCKED', result); // TODO: remove log
throw Error(`Your account is locked due to too many login attempts. Please try again ${moment(now).to(result)}.`);
}
return result;
}
catch {
return null; // something was wrong when reading database => unable to verify
} // try
},
}),
// OAuth providers:
...oAuthProviders,
],
callbacks: {
...callbacks,
async signIn(params) {
const result = callbacks?.signIn?.(params);
if ((result === true) || (typeof (result) === 'string'))
return result;
const { user, account, } = params;
if (!('emailVerified' in user)) {
// sign up (register a new user)
if (!signUpEnabled)
return false;
const newUser = {
id: user.id,
name: user.name ?? '',
email: user.email ?? '',
image: user.image ?? null,
};
if (!newUser.name)
return false; // the name field is required to be stored to model User => sign up failed
if (!newUser.email)
return false; // the email field is required to be stored to model User => sign up failed
}
else {
// sign in (existing user)
// const dbUser : AdapterUser = user;
} // if
if ((account?.type === 'oauth') && (!('emailVerified' in user) || (user.emailVerified === null))) {
const markEmailAsVerified = async () => {
// login with oAuth is also intrinsically verifies the email:
const now = new Date();
await adapter.markEmailAsVerified(user.id, {
now: now,
});
user.emailVerified = now; // update the data
};
if (!('emailVerified' in user)) {
// no update: the `User` record is not yet created
}
else {
// immediately update:
await markEmailAsVerified();
} // if
} // if
// all verification passed => logged in
return true;
},
async jwt(params) {
const { token, user, account, } = params;
// assigning additional data to session:
if (account) { // if `account` exist, this means that the callback is being invoked for the first time (i.e. the user is being signed in).
// add a related userId to token:
if (!('userId' in token))
token.userId = user.id;
// add a related credentials to token object:
const credentials = (!!user.id
? adapter.getCredentialsByUserId(user.id) // faster
: !!user.email
? adapter.getCredentialsByUserEmail(user.email) // slower
: null);
if (credentials)
token.credentials = credentials;
// add a related role to token object:
const role = (!!user.id
? adapter.getRoleByUserId(user.id) // faster
: !!user.email
? adapter.getRoleByUserEmail(user.email) // slower
: null);
if (role)
token.role = role;
} // if
// the token object will be attached to the client side cookie:
return callbacks?.jwt?.({ ...params, token }) ?? token;
},
async session(params) {
const { session, user: dbUser, } = params;
// assigning additional data to session:
const sessionUser = session.user;
if (sessionUser) {
// add a related userId to sessionUser:
if (!('id' in sessionUser))
sessionUser.id = dbUser.id;
// add a related credentials to session object:
const credentials = (!!dbUser.id
? await adapter.getCredentialsByUserId(dbUser.id) // faster
: !!dbUser.email
? await adapter.getCredentialsByUserEmail(dbUser.email) // slower
: null);
if (credentials)
session.credentials = credentials;
// add a related role to session object:
const role = (!!dbUser.id
? await adapter.getRoleByUserId(dbUser.id) // faster
: !!dbUser.email
? await adapter.getRoleByUserEmail(dbUser.email) // slower
: null);
if (role)
session.role = role;
} // if
// the session object will be synced to the client side:
return callbacks?.session?.({ ...params, session }) ?? session;
},
},
pages: {
signIn: signInPath,
// signOut : '/auth/signout',
error: signInPath, // Error code passed in query string as ?error=
// verifyRequest : '/auth/verify-request', // Check your email: A sign in link has been sent to your email address.
// newUser : '/auth/new-user', // New users will be directed here on first sign in (leave the property out if not of interest)
},
};
//#endregion configs
//#region custom handlers
// password resets:
const requestPasswordResetRouteHandler = async (req, context, path) => {
// conditions:
if (!resetEnabled)
return false; // ignore
// filters the request type:
if (req.method !== 'POST')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
// validate the request parameter(s):
const { username, } = await getRequestData(req);
if ((typeof (username) !== 'string') || !username) {
return NextResponse.json({
error: 'The required username or email is not provided.',
}, { status: 400 }); // handled with error
} // if
if (username.length > 50) { // prevents of DDOS attack
return NextResponse.json({
error: 'The username or email is too long.',
}, { status: 400 }); // handled with error
} // if
// create a new passwordResetToken and then send a link of passwordResetToken to the user's email:
try {
// create a new passwordResetToken:
const now = new Date();
const result = await adapter.createPasswordResetToken(username, {
now: now,
resetThrottle: resetThrottle,
resetMaxAge: resetMaxAge,
});
if (!result) {
// the user account is not found => reject:
return NextResponse.json({
error: 'There is no user with the specified username or email.',
}, { status: 404 }); // handled with error
} // if
if (result instanceof Date) {
// the reset request is too frequent => reject:
return NextResponse.json({
error: `The password reset request is too often. Please try again ${moment(now).to(result)}.`,
}, { status: 400 }); // handled with error
} // if
const { passwordResetToken, user, } = result;
// generate a link to a page for resetting password:
const resetLinkUrl = `${businessUrl ?? ''}${signInPath}?passwordResetToken=${encodeURIComponent(passwordResetToken)}`;
// send a link of passwordResetToken to the user's email:
const { renderToStaticMarkup } = await import('react-dom/server');
const businessContextProviderProps = {
// data:
model: {
name: businessName,
url: businessUrl,
},
};
const transporter = nodemailer.createTransport({
host: emailResetHost,
port: emailResetPort,
secure: emailResetSecure,
auth: {
user: emailResetUsername,
pass: emailResetPassword,
},
});
try {
await transporter.sendMail({
from: emailResetFrom, // sender address
to: user.email, // list of receivers
subject: renderToStaticMarkup(React.createElement(BusinessContextProvider, { ...businessContextProviderProps },
React.createElement(PasswordResetContextProvider, { url: resetLinkUrl },
React.createElement(UserContextProvider, { model: user }, emailResetSubject)))).replace(/[\r\n\t]+/g, ' ').trim(),
html: renderToStaticMarkup(React.createElement(BusinessContextProvider, { ...businessContextProviderProps },
React.createElement(PasswordResetContextProvider, { url: resetLinkUrl },
React.createElement(UserContextProvider, { model: user }, emailResetMessage)))),
});
}
finally {
transporter.close();
} // try
// report the success:
return NextResponse.json({
ok: true,
message: 'A password reset link sent to your email. Please check your inbox in a moment.',
}); // handled with success
}
catch (error) {
// report the failure:
return NextResponse.json({
error: `Oops, there was an error while resetting your password.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
const validatePasswordResetRouteHandler = async (req, context, path) => {
// conditions:
if (!resetEnabled)
return false; // ignore
// filters the request type:
if (req.method !== 'GET')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
// validate the request parameter(s):
const { passwordResetToken, } = await getRequestData(req);
if ((typeof (passwordResetToken) !== 'string') || !passwordResetToken) {
return NextResponse.json({
error: 'The required password reset token is not provided.',
}, { status: 400 }); // handled with error
} // if
if (passwordResetToken.length > 50) { // prevents of DDOS attack
return NextResponse.json({
error: 'The password reset token is too long.',
}, { status: 400 }); // handled with error
} // if
// find the related email & username by given passwordResetToken:
try {
const result = await adapter.validatePasswordResetToken(passwordResetToken, {
now: new Date(),
});
if (!result) {
return NextResponse.json({
error: 'The password reset token is invalid or expired.',
}, { status: 404 }); // handled with error
} // if
// report the success:
return NextResponse.json({
ok: true,
email: result.email,
username: result.username,
}); // handled with success
}
catch (error) {
// report the failure:
return NextResponse.json({
error: `Oops, there was an error while validating your token.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
const usePasswordResetRouteHandler = async (req, context, path) => {
// conditions:
if (!resetEnabled)
return false; // ignore
// filters the request type:
if (req.method !== 'PATCH')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
// validate the request parameter(s):
const { passwordResetToken, password, } = await getRequestData(req);
if ((typeof (passwordResetToken) !== 'string') || !passwordResetToken) {
return NextResponse.json({
error: 'The required password reset token is not provided.',
}, { status: 400 }); // handled with error
} // if
if (passwordResetToken.length > 50) { // prevents of DDOS attack
return NextResponse.json({
error: 'The password reset token is too long.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (password) !== 'string') || !password) {
return NextResponse.json({
error: 'The required password is not provided.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (passwordMinLength) === 'number') && Number.isFinite(passwordMinLength) && (password.length < passwordMinLength)) {
return NextResponse.json({
error: `The password is too short. Minimum is ${passwordMinLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if ((typeof (passwordMaxLength) === 'number') && Number.isFinite(passwordMaxLength) && (password.length > passwordMaxLength)) {
return NextResponse.json({
error: `The password is too long. Maximum is ${passwordMaxLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if (passwordHasUppercase && !password.match(/[A-Z]/)) {
return NextResponse.json({
error: `The password must have at least one capital letter.`,
}, { status: 400 }); // handled with error
} // if
if (passwordHasLowercase && !password.match(/[a-z]/)) {
return NextResponse.json({
error: `The password must have at least one non-capital letter.`,
}, { status: 400 }); // handled with error
} // if
const validationPasswordNotProhibited = await checkPasswordNotProhibitedRouteHandler(req, context, '');
if (validationPasswordNotProhibited && !validationPasswordNotProhibited.ok)
return validationPasswordNotProhibited;
try {
const result = await adapter.usePasswordResetToken(passwordResetToken, password, {
now: new Date(),
});
if (!result) {
return NextResponse.json({
error: 'The password reset token is invalid or expired.',
}, { status: 404 }); // handled with error
} // if
return NextResponse.json({
ok: true,
message: 'The password has been successfully changed. Now you can sign in with the new password.',
}); // handled with success
}
catch (error) {
return NextResponse.json({
error: `Oops, there was an error while resetting your password.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
// registrations:
const checkUsernameAvailabilityRouteHandler = async (req, context, path) => {
// conditions:
if (!signUpEnabled)
return false; // ignore
// filters the request type:
if (path) {
if (req.method !== 'GET')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
} // if
// validate the request parameter(s):
const { username, } = await getRequestData(req);
if ((typeof (username) !== 'string') || !username) {
return NextResponse.json({
error: 'The required username is not provided.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (usernameMinLength) === 'number') && Number.isFinite(usernameMinLength) && (username.length < usernameMinLength)) {
return NextResponse.json({
error: `The username is too short. Minimum is ${usernameMinLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if ((typeof (usernameMaxLength) === 'number') && Number.isFinite(usernameMaxLength) && (username.length > usernameMaxLength)) {
return NextResponse.json({
error: `The username is too long. Maximum is ${usernameMaxLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if (!username.match(usernameFormat)) {
return NextResponse.json({
error: `The username is not well formatted.`,
}, { status: 400 }); // handled with error
} // if
try {
const result = await adapter.checkUsernameAvailability(username);
if (!result) {
return NextResponse.json({
error: `The username "${username}" is already taken.`,
}, { status: 409 }); // handled with error
} // if
return NextResponse.json({
ok: true,
message: `The username "${username}" can be used.`,
}); // handled with success
}
catch (error) {
return NextResponse.json({
error: `Oops, there was an error while checking username availability.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
const checkEmailAvailabilityRouteHandler = async (req, context, path) => {
// conditions:
if (!signUpEnabled)
return false; // ignore
// filters the request type:
if (path) {
if (req.method !== 'GET')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
} // if
// validate the request parameter(s):
const { email, } = await getRequestData(req);
if ((typeof (email) !== 'string') || !email) {
return NextResponse.json({
error: 'The required email is not provided.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (emailMinLength) === 'number') && Number.isFinite(emailMinLength) && (email.length < emailMinLength)) {
return NextResponse.json({
error: `The email is too short. Minimum is ${emailMinLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if ((typeof (emailMaxLength) === 'number') && Number.isFinite(emailMaxLength) && (email.length > emailMaxLength)) {
return NextResponse.json({
error: `The email is too long. Maximum is ${emailMaxLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if (!email.match(emailFormat)) {
return NextResponse.json({
error: `The email is not well formatted.`,
}, { status: 400 }); // handled with error
} // if
try {
const result = await adapter.checkEmailAvailability(email);
if (!result) {
return NextResponse.json({
error: `The email "${email}" is already taken.`,
}, { status: 409 }); // handled with error
} // if
return NextResponse.json({
ok: true,
message: `The email "${email}" can be used.`,
}); // handled with success
}
catch (error) {
return NextResponse.json({
error: `Oops, there was an error while checking email availability.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
const checkUsernameNotProhibitedRouteHandler = async (req, context, path) => {
// conditions:
if (!signUpEnabled)
return false; // ignore
// filters the request type:
if (path) {
if (req.method !== 'PUT')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
} // if
// validate the request parameter(s):
const { username, } = await getRequestData(req);
if ((typeof (username) !== 'string') || !username) {
return NextResponse.json({
error: 'The required username is not provided.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (usernameMinLength) === 'number') && Number.isFinite(usernameMinLength) && (username.length < usernameMinLength)) {
return NextResponse.json({
error: `The username is too short. Minimum is ${usernameMinLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if ((typeof (usernameMaxLength) === 'number') && Number.isFinite(usernameMaxLength) && (username.length > usernameMaxLength)) {
return NextResponse.json({
error: `The username is too long. Maximum is ${usernameMaxLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if (!username.match(usernameFormat)) {
return NextResponse.json({
error: `The username is not well formatted.`,
}, { status: 400 }); // handled with error
} // if
if ((() => {
for (const prohibited of usernameProhibited) {
if (prohibited instanceof RegExp) {
if (prohibited.test(username))
return true; // prohibited word found
}
else {
if (prohibited === username)
return true; // prohibited word found
} // if
} // for
return false; // all checks passed, no prohibited word was found
})()) {
return NextResponse.json({
error: `The username "${username}" is prohibited.`,
}, { status: 409 }); // handled with error
} // if
return NextResponse.json({
ok: true,
message: `The username "${username}" can be used.`,
}); // handled with success
};
const checkPasswordNotProhibitedRouteHandler = async (req, context, path) => {
// conditions:
if (!signUpEnabled && !resetEnabled)
return false; // ignore
// filters the request type:
if (path) {
if (req.method !== 'PUT')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
} // if
// validate the request parameter(s):
const { password, } = await getRequestData(req);
if ((typeof (password) !== 'string') || !password) {
return NextResponse.json({
error: 'The required password is not provided.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (passwordMinLength) === 'number') && Number.isFinite(passwordMinLength) && (password.length < passwordMinLength)) {
return NextResponse.json({
error: `The password is too short. Minimum is ${passwordMinLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if ((typeof (passwordMaxLength) === 'number') && Number.isFinite(passwordMaxLength) && (password.length > passwordMaxLength)) {
return NextResponse.json({
error: `The password is too long. Maximum is ${passwordMaxLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if (passwordHasUppercase && !password.match(/[A-Z]/)) {
return NextResponse.json({
error: `The password must have at least one capital letter.`,
}, { status: 400 }); // handled with error
} // if
if (passwordHasLowercase && !password.match(/[a-z]/)) {
return NextResponse.json({
error: `The password must have at least one non-capital letter.`,
}, { status: 400 }); // handled with error
} // if
if ((() => {
for (const prohibited of passwordProhibited) {
if (prohibited instanceof RegExp) {
if (prohibited.test(password))
return true; // prohibited word found
}
else {
if (prohibited === password)
return true; // prohibited word found
} // if
} // for
return false; // all checks passed, no prohibited word was found
})()) {
return NextResponse.json({
error: `The password "${password}" is prohibited.`,
}, { status: 409 }); // handled with error
} // if
return NextResponse.json({
ok: true,
message: `The password "${password}" can be used.`,
}); // handled with success
};
const signUpRouteHandler = async (req, context, path) => {
// conditions:
if (!signUpEnabled)
return false; // ignore
// filters the request type:
if (req.method !== 'POST')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
// validate the request parameter(s):
const { name, email, username, password, } = await getRequestData(req);
if ((typeof (name) !== 'string') || !name) {
return NextResponse.json({
error: 'The required name is not provided.',
}, { status: 400 }); // handled with error
} // if
if ((typeof (nameMinLength) === 'number') && Number.isFinite(nameMinLength) && (name.length < nameMinLength)) {
return NextResponse.json({
error: `The name is too short. Minimum is ${nameMinLength} characters.`,
}, { status: 400 }); // handled with error
} // if
if ((typeof (nameMaxLength) === 'number') && Number.isFinite(nameMaxLength) && (name.length > nameMaxLength)) {
return NextResponse.json({
error: `The name is too long. Maximum is ${nameMaxLength} characters.`,
}, { status: 400 }); // handled with error
} // if
const validationEmailAvailability = await checkEmailAvailabilityRouteHandler(req, context, '');
if (validationEmailAvailability && !validationEmailAvailability.ok)
return validationEmailAvailability;
const validationUsernameAvailability = await checkUsernameAvailabilityRouteHandler(req, context, '');
if (validationUsernameAvailability && !validationUsernameAvailability.ok)
return validationUsernameAvailability;
const validationUsernameNotProhibited = await checkUsernameNotProhibitedRouteHandler(req, context, '');
if (validationUsernameNotProhibited && !validationUsernameNotProhibited.ok)
return validationUsernameNotProhibited;
const validationPasswordNotProhibited = await checkPasswordNotProhibitedRouteHandler(req, context, '');
if (validationPasswordNotProhibited && !validationPasswordNotProhibited.ok)
return validationPasswordNotProhibited;
try {
const { emailConfirmationToken, } = await adapter.registerUser(name, email, username, password, {
requireEmailVerified: signInRequireVerifiedEmail,
});
if (emailConfirmationToken) {
// generate a link to a page for confirming email:
const emailConfirmationLinkUrl = `${businessUrl ?? ''}${signInPath}?emailConfirmationToken=${encodeURIComponent(emailConfirmationToken)}`;
// send a link of emailConfirmationToken to the user's email:
const { renderToStaticMarkup } = await import('react-dom/server');
const businessContextProviderProps = {
// data:
model: {
name: businessName,
url: businessUrl,
},
};
const transporter = nodemailer.createTransport({
host: emailSignUpHost,
port: emailSignUpPort,
secure: emailSignUpSecure,
auth: {
user: emailSignUpUsername,
pass: emailSignUpPassword,
},
});
try {
await transporter.sendMail({
from: emailSignUpFrom, // sender address
to: email, // list of receivers
subject: renderToStaticMarkup(React.createElement(BusinessContextProvider, { ...businessContextProviderProps },
React.createElement(EmailConfirmationContextProvider, { url: emailConfirmationLinkUrl },
React.createElement(UserContextProvider, { model: {
name: name,
email: email,
} }, emailSignUpSubject)))).replace(/[\r\n\t]+/g, ' ').trim(),
html: renderToStaticMarkup(React.createElement(BusinessContextProvider, { ...businessContextProviderProps },
React.createElement(EmailConfirmationContextProvider, { url: emailConfirmationLinkUrl },
React.createElement(UserContextProvider, { model: {
name: name,
email: email,
} }, emailSignUpMessage)))),
});
}
finally {
transporter.close();
} // try
} // if
return NextResponse.json({
ok: true,
message: !emailConfirmationToken
? 'Your account has been successfully created.\n\nNow you can sign in with the new username and password.'
: 'Your account has been successfully created.\n\nWe have sent a confirmation link to your email to activate your account. Please check your inbox in a moment.',
}, { status: !emailConfirmationToken ? 200 : 201 }); // handled with success
}
catch (error) {
return NextResponse.json({
error: `Oops, there was an error while registering your account.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
// email verifications:
const useEmailConfirmationRouteHandler = async (req, context, path) => {
// conditions:
if (!signUpEnabled)
return false; // ignore
// filters the request type:
if (req.method !== 'PATCH')
return false; // ignore
if (context.params.nextauth?.[0] !== path)
return false; // ignore
// validate the request parameter(s):
const { emailConfirmationToken, } = await getRequestData(req);
if ((typeof (emailConfirmationToken) !== 'string') || !emailConfirmationToken) {
return NextResponse.json({
error: 'The required email confirmation token is not provided.',
}, { status: 400 }); // handled with error
} // if
if (emailConfirmationToken.length > 50) { // prevents of DDOS attack
return NextResponse.json({
error: 'The email confirmation token is too long.',
}, { status: 400 }); // handled with error
} // if
try {
const result = await adapter.useEmailConfirmationToken(emailConfirmationToken, {
now: new Date(),
});
if (!result) {
return NextResponse.json({
error: 'The email confirmation token is invalid or expired.',
}, { status: 404 }); // handled with error
} // if
return NextResponse.json({
ok: true,
message: 'Your email has been successfully confirmed. Now you can sign in with your username (or email) and password.',
}); // handled with success
}
catch (error) {
return NextResponse.json({
error: `Oops, there was an error while confirming your token.
There was a problem on our server.
The server may be busy or currently under maintenance.
Please try again in a few minutes.
If the problem still persists, please contact our technical support.`,
}, { status: 500 }); // handled with error
} // try
};
//#endregion custom handlers
// built in handlers:
const nextAuthHandler = async (req, context) => {
/*
The next-auth design limitation:
The `CredentialsProvider` is NOT COMPATIBLE with `Database Sessions`.
In order to the `CredentialsProvider` to WORK with `Database Sessions`,
a `session` + `cookie` HACK is used to force next-auth to treat the authenticated user as loggedIn.
Check if this sign in callback is being called in the credentials authentication flow.
If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
Credit:
https://nneko.branche.online/next-auth-credentials-provider-with-the-database-session-strategy/
*/
const isUpdatingCookie = (session.strategy === 'database') && ((req.method === 'POST')
&&
context.params.nextauth.includes('callback')
&&
context.params.nextauth.includes('credentials'));
let sessionCookie = null;
const response = await NextAuth(req, context, {
...authOptions,
callbacks: {
...authOptions.callbacks,
async signIn(params) {
if (isUpdatingCookie) {
// extract the user detail:
const { user: userDetail } = params;
// generate the sessionToken data:
const sessionToken = await session.generateSessionToken();
const sessionMaxAge = session.maxAge /* relative time from now in seconds */ * 1000 /* convert seconds to milliseconds */;
const sessionExpiry = new Date(Date.now() + sessionMaxAge);
// create the sessionToken record into database:
await adapter.createSession?.({
sessionToken: sessionToken,
expires: sessionExpiry,
userId: userDetail.id,
});
const useSecureCookies = (process.env.AUTH_URL ?? process.env.NEXTAUTH_URL)?.startsWith?.('https://') ?? !!process.env.VERCEL;
const cookiePrefix = useSecureCookies ? '__Secure-' : '';
const { sessionToken: { name: cookieName, options: { httpOnly: cookieHttpOnly, sameSite: cookieSameSite, path: cookiePath, secure: cookieSecure, }, }, } = {
sessionToken: {
// name: `${cookiePrefix}authjs.session-token`,
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies,
},
},
};
const cookieSameSiteValue = (() => {
switch (cookieSameSite) {
case 'lax':
return 'Lax';
case true:
case 'strict':
return 'Strict';
case 'none':
return 'None';
case false:
default:
return '';
} // switch
})();
// const cookieName = `${isSecureCookie ? '__Secure-' : ''}next-auth.session-token`;
// create the sessionToken record into cookie:
// const cookies = new Cookies(req, context);
// cookies.set(cookieName, sessionToken, {
// path : '/',
// expires : sessionExpiry,
// httpOnly : true,
// secure : true,
// sameSite : 'lax',
// });
sessionCookie = `${cookieName}=${sessionToken}; Path=${cookiePath}; Expires=${sessionExpiry.toUTCString()};${cookieHttpOnly ? ' HttpOnly;' : ''}${cookieSecure ? ' Secure;' : ''}${cookieSameSiteValue ? ` SameSite=${cookieSameSiteValue}` : ''}`;
} // if
// config's origin signIn handler:
return await authOptions.callbacks?.signIn?.(params) ?? true;
},
},
jwt: {
async encode(params) {
if (isUpdatingCookie)
return ''; // force not to use jwt token => fallback to database token
// jwt's built in encode handler:
return encode(params);
},
async decode(params) {
if (isUpdatingCookie)
return null; // force not to use jwt token => fallback to database token
// jwt's built in decode handler:
return decode(params);
},
},
});
if (!!sessionCookie) {
response.headers.append('Set-Cookie', sessionCookie);
} // if
return response;
};
// merged handlers:
const mergedRouteHandler = async (req, context) => {
return (
// password resets:
await requestPasswordResetRouteHandler(req, context, passwordResetPath)
||
await validatePasswordResetRouteHandler(req, context, passwordResetPath)
||
await usePasswordResetRouteHandler(req, context, passwordResetPath)
||
// registrations:
await checkEmailAvailabilityRouteHandler(req, context, emailValidationPath)
||
await checkUsernameAvailabilityRouteHandler(req, context, usernameValidationPath)
||
await checkUsernameNotProhibitedRouteHandler(req, context, usernameValidationPath)
||
await checkPasswordNotProhibitedRouteHandler(req, context, passwordValidationPath)
||
await signUpRouteHandler(req, context, signUpPath)
||
// email verifications:
await useEmailConfirmationRouteHandler(req, context, emailConfirmationPath)
||
// built in handlers:
await nextAuthHandler(req, context));
};
return {
authHandler: mergedRouteHandler,
authOptions,
};
};
// specific next-js /app auth handlers:
export const createAuthRouteHandler = (options) => {
const { authHandler, authOptions, } = createNextAuthHandler(options);
const authRouteHandler = async (req, context) => {
// responses HEAD request as success:
if (req.method === 'HEAD')
return new Response(null, { status: 200 });
return await authHandler(req, context);
};
authRouteHandler.authOptions = authOptions;
return authRouteHandler;
};
// specific next-js /pages auth handlers:
export const createAuthApiHandler = (options) => {
const { authHandler, authOptions, } = createNextAuthHandler(options);
const nextApiWrapperHandler = async (req, res, handler) => {
const response = await handler(
/* request: */ new Request(new URL(req.url ?? '/', 'https://localhost').href, {
method: req.method,
body: /^(POST|PUT|PATCH)$/i.test(req.method ?? '') ? JSON.stringify(req.body) : null,
}),
/* context: */ {
params: {
nextauth: req.query.nextauth,
},
});
if (!response)
return false;
for (const [headerKey, headerValue] of response.headers.entries()) {
res.setHeader(headerKey, headerValue);
} // for
res.status(response.status).send(await response.text());
return true;
};
const authApiHandler = async (req, res) => {
// responses HEAD request as success:
if (req.method === 'HEAD')
return res.status(200).send(null);
await nextApiWrapperHandler(req, res, (request, context) => authHandler(request, context));
};
authApiHandler.authOptions = authOptions;
return authApiHandler;
};