payload
Version:
Node, React, Headless CMS and Application Framework built on Next.js
351 lines (350 loc) ⢠13.5 kB
JavaScript
import { v4 as uuid } from 'uuid';
import { buildAfterOperation } from '../../collections/operations/utils.js';
import { AuthenticationError, LockedAuth, UnverifiedEmail, ValidationError } from '../../errors/index.js';
import { afterRead } from '../../fields/hooks/afterRead/index.js';
import { Forbidden } from '../../index.js';
import { appendNonTrashedFilter } from '../../utilities/appendNonTrashedFilter.js';
import { killTransaction } from '../../utilities/killTransaction.js';
import { sanitizeInternalFields } from '../../utilities/sanitizeInternalFields.js';
import { getFieldsToSign } from '../getFieldsToSign.js';
import { getLoginOptions } from '../getLoginOptions.js';
import { isUserLocked } from '../isUserLocked.js';
import { jwtSign } from '../jwt.js';
import { removeExpiredSessions } from '../removeExpiredSessions.js';
import { authenticateLocalStrategy } from '../strategies/local/authenticate.js';
import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts.js';
import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js';
/**
* Throws an error if the user is locked or does not exist.
* This does not check the login attempts, only the lock status. Whoever increments login attempts
* is responsible for locking the user properly, not whoever checks the login permission.
*/ export const checkLoginPermission = ({ loggingInWithUsername, req, user })=>{
if (!user) {
throw new AuthenticationError(req.t, Boolean(loggingInWithUsername));
}
if (isUserLocked(new Date(user.lockUntil))) {
throw new LockedAuth(req.t);
}
};
export const loginOperation = async (incomingArgs)=>{
let args = incomingArgs;
if (args.collection.config.auth.disableLocalStrategy) {
throw new Forbidden(args.req.t);
}
try {
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
if (args.collection.config.hooks?.beforeOperation?.length) {
for (const hook of args.collection.config.hooks.beforeOperation){
args = await hook({
args,
collection: args.collection?.config,
context: args.req.context,
operation: 'login',
req: args.req
}) || args;
}
}
const { collection: { config: collectionConfig }, data, depth, overrideAccess, req, req: { fallbackLocale, locale, payload, payload: { secret } }, showHiddenFields } = args;
// /////////////////////////////////////
// Login
// /////////////////////////////////////
const { email: unsanitizedEmail, password } = data;
const loginWithUsername = collectionConfig.auth.loginWithUsername;
const sanitizedEmail = typeof unsanitizedEmail === 'string' ? unsanitizedEmail.toLowerCase().trim() : null;
const sanitizedUsername = 'username' in data && typeof data?.username === 'string' ? data.username.toLowerCase().trim() : null;
const { canLoginWithEmail, canLoginWithUsername } = getLoginOptions(loginWithUsername);
// cannot login with email, did not provide username
if (!canLoginWithEmail && !sanitizedUsername) {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [
{
message: req.i18n.t('validation:required'),
path: 'username'
}
]
});
}
// cannot login with username, did not provide email
if (!canLoginWithUsername && !sanitizedEmail) {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [
{
message: req.i18n.t('validation:required'),
path: 'email'
}
]
});
}
// can login with either email or username, did not provide either
if (!sanitizedUsername && !sanitizedEmail) {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [
{
message: req.i18n.t('validation:required'),
path: 'email'
},
{
message: req.i18n.t('validation:required'),
path: 'username'
}
]
});
}
// did not provide password for login
if (typeof password !== 'string' || password.trim() === '') {
throw new ValidationError({
collection: collectionConfig.slug,
errors: [
{
message: req.i18n.t('validation:required'),
path: 'password'
}
]
});
}
let whereConstraint = {};
const emailConstraint = {
email: {
equals: sanitizedEmail
}
};
const usernameConstraint = {
username: {
equals: sanitizedUsername
}
};
if (canLoginWithEmail && canLoginWithUsername && (sanitizedUsername || sanitizedEmail)) {
if (sanitizedUsername) {
whereConstraint = {
or: [
usernameConstraint,
{
email: {
equals: sanitizedUsername
}
}
]
};
} else {
whereConstraint = {
or: [
emailConstraint,
{
username: {
equals: sanitizedEmail
}
}
]
};
}
} else if (canLoginWithEmail && sanitizedEmail) {
whereConstraint = emailConstraint;
} else if (canLoginWithUsername && sanitizedUsername) {
whereConstraint = usernameConstraint;
}
// Exclude trashed users
whereConstraint = appendNonTrashedFilter({
enableTrash: collectionConfig.trash,
trash: false,
where: whereConstraint
});
let user = await payload.db.findOne({
collection: collectionConfig.slug,
req,
where: whereConstraint
});
checkLoginPermission({
loggingInWithUsername: Boolean(canLoginWithUsername && sanitizedUsername),
req,
user
});
user.collection = collectionConfig.slug;
user._strategy = 'local-jwt';
const authResult = await authenticateLocalStrategy({
doc: user,
password
});
user = sanitizeInternalFields(user);
const maxLoginAttemptsEnabled = args.collection.config.auth.maxLoginAttempts > 0;
if (!authResult) {
if (maxLoginAttemptsEnabled) {
await incrementLoginAttempts({
collection: collectionConfig,
payload: req.payload,
req,
user
});
// Re-check login permissions and max attempts after incrementing attempts, in case parallel updates occurred
checkLoginPermission({
loggingInWithUsername: Boolean(canLoginWithUsername && sanitizedUsername),
req,
user
});
}
throw new AuthenticationError(req.t);
}
if (collectionConfig.auth.verify && user._verified === false) {
throw new UnverifiedEmail({
t: req.t
});
}
/*
* Correct password accepted - reācheck that the account didn't
* get locked by parallel bad attempts in the meantime.
*/ if (maxLoginAttemptsEnabled) {
const { lockUntil, loginAttempts } = await payload.db.findOne({
collection: collectionConfig.slug,
req,
select: {
lockUntil: true,
loginAttempts: true
},
where: {
id: {
equals: user.id
}
}
});
user.lockUntil = lockUntil;
user.loginAttempts = loginAttempts;
checkLoginPermission({
req,
user
});
}
const fieldsToSignArgs = {
collectionConfig,
email: sanitizedEmail,
user
};
if (collectionConfig.auth.useSessions) {
// Add session to user
const newSessionID = uuid();
const now = new Date();
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000;
const expiresAt = new Date(now.getTime() + tokenExpInMs);
const session = {
id: newSessionID,
createdAt: now,
expiresAt
};
if (!user.sessions?.length) {
user.sessions = [
session
];
} else {
user.sessions = removeExpiredSessions(user.sessions);
user.sessions.push(session);
}
await payload.db.updateOne({
id: user.id,
collection: collectionConfig.slug,
data: user,
req,
returning: false
});
user.collection = collectionConfig.slug;
user._strategy = 'local-jwt';
fieldsToSignArgs.sid = newSessionID;
}
const fieldsToSign = getFieldsToSign(fieldsToSignArgs);
if (maxLoginAttemptsEnabled) {
await resetLoginAttempts({
collection: collectionConfig,
doc: user,
payload: req.payload,
req
});
}
// /////////////////////////////////////
// beforeLogin - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.beforeLogin?.length) {
for (const hook of collectionConfig.hooks.beforeLogin){
user = await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
user
}) || user;
}
}
const { exp, token } = await jwtSign({
fieldsToSign,
secret,
tokenExpiration: collectionConfig.auth.tokenExpiration
});
req.user = user;
// /////////////////////////////////////
// afterLogin - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterLogin?.length) {
for (const hook of collectionConfig.hooks.afterLogin){
user = await hook({
collection: args.collection?.config,
context: args.req.context,
req: args.req,
token,
user
}) || user;
}
}
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
user = await afterRead({
collection: collectionConfig,
context: req.context,
depth: depth,
doc: user,
// @ts-expect-error - vestiges of when tsconfig was not strict. Feel free to improve
draft: undefined,
fallbackLocale: fallbackLocale,
global: null,
locale: locale,
overrideAccess: overrideAccess,
req,
showHiddenFields: showHiddenFields
});
// /////////////////////////////////////
// afterRead - Collection
// /////////////////////////////////////
if (collectionConfig.hooks?.afterRead?.length) {
for (const hook of collectionConfig.hooks.afterRead){
user = await hook({
collection: args.collection?.config,
context: req.context,
doc: user,
req
}) || user;
}
}
let result = {
exp,
token,
user
};
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation({
args,
collection: args.collection?.config,
operation: 'login',
result
});
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
return result;
} catch (error) {
await killTransaction(args.req);
throw error;
}
};
//# sourceMappingURL=login.js.map