aegis-auth
Version:
A credentials-based auth solution for Next.js (and other Node projects) with IP rate-limiting, account lockouts, and sessions in DB.
165 lines • 6.14 kB
JavaScript
import { verifyPassword } from "../security";
import { ErrorCodes, } from "../types";
import { clientSafeUser, createTime, limitIpAttempts, } from "../utils";
import { loginSchema } from "../validations";
import { createSession } from "./createSession";
export async function loginUser(context, input) {
var _a;
const { prisma, config, limiters, parsedRequest } = context;
const { ipAddress } = parsedRequest !== null && parsedRequest !== void 0 ? parsedRequest : {};
try {
// Validate input using Zod schema.
const credentials = loginSchema.safeParse(input);
if (!credentials.success) {
config.logger.securityEvent("INVALID_INPUT", Object.assign({ route: "login", ipAddress }, input));
return {
success: false,
status: 400,
message: "Invalid input provided",
code: ErrorCodes.INVALID_INPUT,
};
}
const { usernameOrEmail, password } = credentials.data;
if (config.rateLimiting.login.enabled && ipAddress) {
const limiter = limiters.login;
const limit = await limitIpAttempts({ ipAddress, limiter });
if (!limit.success) {
config.logger.securityEvent("RATE_LIMIT_EXCEEDED", {
route: "login",
ipAddress,
});
return {
success: false,
status: 429,
message: "Too many attempts. Try again later.",
code: ErrorCodes.RATE_LIMIT_EXCEEDED,
};
}
}
const user = (await prisma.user.findFirst({
where: {
OR: [{ email: usernameOrEmail }, { username: usernameOrEmail }],
},
}));
if (!user) {
config.logger.securityEvent("INVALID_CREDENTIALS", Object.assign({ route: "login", ipAddress }, input));
return {
success: false,
status: 401,
message: "Invalid credentials",
code: ErrorCodes.INVALID_CREDENTIALS,
};
}
if (user.isBanned) {
config.logger.securityEvent("ACCOUNT_BANNED", {
route: "login",
ipAddress,
userId: user.id,
});
return {
success: false,
status: 403,
message: "Account is banned",
code: ErrorCodes.ACCOUNT_BANNED,
};
}
if (user.lockedUntil && user.lockedUntil > new Date()) {
config.logger.securityEvent("ACCOUNT_LOCKED", {
route: "login",
ipAddress,
userId: user.id,
});
const lockedUntil = createTime(user.lockedUntil.getTime(), "ms");
return {
success: false,
status: 403,
message: `Account locked. Try again ${lockedUntil.fromNow}.`,
code: ErrorCodes.ACCOUNT_LOCKED,
};
}
const passwordMatch = await verifyPassword({
hash: user.password,
password: password,
config,
});
if (!passwordMatch) {
config.logger.securityEvent("INVALID_CREDENTIALS", {
route: "login",
ipAddress,
userId: user.id,
});
let failedLoginAttempts = user.failedLoginAttempts + 1;
let lockedUntil = null;
if (failedLoginAttempts >= config.accountSecurity.maxFailedLogins) {
lockedUntil = createTime(config.accountSecurity.lockoutDurationSeconds, "s").getDate();
failedLoginAttempts = 0;
config.logger.securityEvent("ACCOUNT_LOCKED", {
route: "login",
ipAddress,
userId: user.id,
});
}
await prisma.user.update({
where: { id: user.id },
data: { failedLoginAttempts, lockedUntil },
});
return {
success: false,
status: 401,
message: "Invalid credentials",
code: ErrorCodes.INVALID_CREDENTIALS,
};
}
if (!user.isEmailVerified &&
config.accountSecurity.requireEmailVerification) {
config.logger.securityEvent("EMAIL_NOT_VERIFIED", {
route: "login",
ipAddress,
userId: user.id,
});
return {
success: false,
status: 403,
message: "Email not verified",
code: ErrorCodes.EMAIL_NOT_VERIFIED,
};
}
await prisma.user.update({
where: { id: user.id },
data: { failedLoginAttempts: 0, lockedUntil: null },
});
const newSession = await createSession(context, {
userId: user.id,
});
if (!newSession.success || !((_a = newSession.data) === null || _a === void 0 ? void 0 : _a.session)) {
return {
success: false,
status: 500,
message: "Failed to create session",
code: ErrorCodes.INTERNAL_SERVER_ERROR,
};
}
config.logger.securityEvent("LOGIN_SUCCESS", {
route: "login",
ipAddress,
userId: user.id,
});
const clientUser = clientSafeUser({ user });
return {
success: true,
status: 200,
message: "Login successful",
data: { user: clientUser, session: newSession.data.session },
};
}
catch (err) {
config.logger.error("Unexpected error in loginUser", { error: err });
return {
success: false,
status: 500,
message: "Internal Server Error",
code: ErrorCodes.INTERNAL_SERVER_ERROR,
};
}
}
//# sourceMappingURL=loginUser.js.map