Auth Actions (Mongoose)
Depending on the authentication method you are using, you can use the following actions to authenticate users.
Register User
This function is used to register a new user. It takes in the user details and validates them using the RegisterSchema. Here's what it does:
- Validate Inputs: It checks if the provided email and password are valid.
- Hash Password: It hashes the password using bcrypt for security.
- Check Existing User: It checks if the user already exists in the database based on the provided email or username.
- Create User: If the user does not exist, it creates a new user with the provided details.
- Send Verification Email: It sends a verification email to the user for email confirmation.
Depending on the user's choice, the function can be customized to include or exclude the username field. Below are two versions of the function based on whether the user chooses to include a username or not.
Register User (With Username)
import { connectToDatabase } from "@/lib/database";
import User from "@/models/user.model";
import { RegisterSchema } from "@/validations";
import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
import { getProfileUrl } from "@/lib/constant";
import * as z from "zod";
import bcrypt from "bcryptjs";
import { sendVerificationEmail } from "@/lib/mail";
export async function RegisterUser(values: z.infer<typeof RegisterSchema>) {
try {
connectToDatabase();
const validatedFields = RegisterSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid email and password" };
}
const { name, email, password, username } = validatedFields.data;
const hashedPassword = await bcrypt.hash(password, 10);
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
if (existingUser) {
return { error: "User already exists" };
}
const image = getProfileUrl();
const newUser = new User({
name,
image,
email,
password: hashedPassword,
username,
});
await newUser.save();
const Verificationtoken = await generateVerificationToken(email);
await sendVerificationEmail(email, Verificationtoken.token);
return { success: "Verification Email Sent!" };
} catch (error: any) {
return error.message;
}
}
Register User (Without Username)
export async function RegisterUser(values: z.infer<typeof RegisterSchema>) {
try {
connectToDatabase();
const validatedFields = RegisterSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid email and password" };
}
const { name, email, password } = validatedFields.data;
const hashedPassword = await bcrypt.hash(password, 10);
const existingUser = await User.findOne({ email });
if (existingUser) {
return { error: "User already exists" };
}
const image = getProfileUrl();
const newUser = new User({
name,
image,
email,
//[RegisterUsername]
password: hashedPassword,
});
await newUser.save();
const Verificationtoken = await generateVerificationToken(email);
await sendVerificationEmail(name, email, Verificationtoken.token);
return { success: "Verification Email Sent!" };
} catch (error: any) {
return error.message;
}
}
Login User
LoginUser function has three different cases.most of the code remains the same but still the different code in each is hilighed.
Login User (Without Username and Two-Factor)
This version of the function is suitable for users who do not choose to have a username and do not opt for two-factor authentication. Here's what it does:
- Validate Inputs: It checks if the provided email and password are valid.
- Check Existing User: It retrieves the user from the database based on the provided email.
- Verify Email: If the user exists, it checks if their email is verified. If not, it sends a confirmation email.
- Sign In: Finally, if the credentials are valid and the email is verified, it signs in the user.
import { LoginSchema } from "@/validations";
import { sendVerificationEmail } from "@/lib/mail";
import * as z from "zod";
import bcrypt from "bcryptjs";
import EmailVerification from "@/models/email_verify.model";
import { signIn } from "@/auth";
import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
import { sendTwoFactorTokenEmail } from "@/lib/mail";
import { connectToDatabase } from "@/lib/database";
import { AuthError } from "next-auth";
import User from "@/models/user.model";
export async function LoginUser(values: z.infer<typeof LoginSchema>) {
try {
connectToDatabase();
const validatedFields = LoginSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid email and password" };
}
const { email, password } = validatedFields.data;
const callbackUrl = "/";
const existingLoginUser = await User.findOne({ email });
if (
!existingLoginUser ||
!existingLoginUser.email ||
!existingLoginUser.password
) {
return { error: "User not found" };
}
if (!existingLoginUser.emailVerified) {
const Verificationtoken = await generateVerificationToken(email);
await sendVerificationEmail(
Verificationtoken.email,
Verificationtoken.token,
);
return { success: "Confirmation email sent" };
}
//[LoginTwoFactor]
await signIn("credentials", {
email: existingLoginUser.email,
password,
redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT,
});
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid credentials!" };
default:
return { error: "Something went wrong!" };
}
}
throw error;
}
}
Login User (With Username Only)
This version of the function includes support for a username field but does not require two-factor authentication. The only difference is that now we are looking for both the username and email in our database.
Note: You can see that I am using the field name email for both email and username. This was done to reduce too much change in the code.
export async function LoginUser(values: z.infer<typeof LoginSchema>) {
try {
connectToDatabase();
const validatedFields = LoginSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid email and password" };
}
const { email, password } = validatedFields.data;
const callbackUrl = "/";
const existingLoginUser = await User.findOne({
$or: [{ email }, { username: email }],
});
if (
!existingLoginUser ||
!existingLoginUser.email ||
!existingLoginUser.password
) {
return { error: "User not found" };
}
if (!existingLoginUser.emailVerified) {
const Verificationtoken = await generateVerificationToken(email);
await sendVerificationEmail(
Verificationtoken.email,
Verificationtoken.token,
);
return { success: "Confirmation email sent" };
}
//[LoginTwoFactor]
await signIn("credentials", {
email: existingLoginUser.email,
password,
redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT,
});
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid credentials!" };
default:
return { error: "Something went wrong!" };
}
}
throw error;
}
}
Login User (With Two-Factor Authentication Only)
export async function LoginUser(values: z.infer<typeof LoginSchema>) {
try {
connectToDatabase();
const validatedFields = LoginSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid email and password" };
}
const { email, password, code } = validatedFields.data;
const callbackUrl = "/";
const existingLoginUser = await User.findOne({ email });
if (
!existingLoginUser ||
!existingLoginUser.email ||
!existingLoginUser.password
) {
return { error: "User not found" };
}
if (!existingLoginUser.emailVerified) {
const Verificationtoken = await generateVerificationToken(email);
await sendVerificationEmail(
Verificationtoken.email,
Verificationtoken.token,
);
return { success: "Confirmation email sent" };
}
if (existingLoginUser.twoFactorEnabled && existingLoginUser.email) {
if (code) {
const twoFactorToken = await TwoFactorToken.findOne({ token: code });
if (!twoFactorToken || twoFactorToken.token !== code) {
return { error: "Invalid code" };
}
if (twoFactorToken.expiresAt.getTime() < new Date().getTime()) {
return { error: "Code expired" };
}
await twoFactorToken.deleteOne();
const existingConfirmation = await TwoFactorConfirmation.findOne({
_id: existingLoginUser._id,
});
if (existingConfirmation) {
await existingConfirmation.deleteOne();
}
await TwoFactorConfirmation.create({
user: existingLoginUser._id,
expiresAt: new Date(new Date().getTime() + 60 * 60 * 1000),
});
} else {
const twoFactorToken = await generateTwoFactorToken(
existingLoginUser.email,
);
await sendTwoFactorTokenEmail(
existingLoginUser.email,
twoFactorToken.token,
);
return { twoFactor: true };
}
}
await signIn("credentials", {
email: existingLoginUser.email,
password,
redirectTo: callbackUrl || DEFAULT_LOGIN_REDIRECT,
});
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return { error: "Invalid credentials!" };
default:
return { error: "Something went wrong!" };
}
}
throw error;
}
}
At line 21, we first check if the user has enabled two-factor or not, and if yes, then we go into the code block.If the user has two-factor authentication enabled, we proceed to validate the provided authentication code. This code is generated and sent to the user's email during the two-factor authentication setup process. We compare the provided code with the one stored in the database. If the codes match and the code has not expired, the user is considered authenticated.
However, if the provided code is invalid or has expired, appropriate error messages are returned to the user. Invalid codes result in an "Invalid code" error, while expired codes prompt an "Code expired" error.
Upon successful validation of the authentication code, we delete the used token from the database to ensure it cannot be reused. Additionally, any existing two-factor confirmation records associated with the user are deleted to maintain a clean record of authentication attempts.
Subsequently, a new two-factor confirmation record is created for the user, specifying the expiration time. This record serves as a marker indicating that the user has successfully passed the two-factor authentication process within the given timeframe.
Overall, this version of the login function provides support for two-factor authentication, enhancing the security of user authentication processes.
Generate Two-Factor Token
This function generates a six-digit two-factor authentication token for the given email and stores it in the TwoFactorToken
collection. It creates a new document containing the email, token, and expiration time (set to 15 minutes from the current time).
export const generateTwoFactorToken = async (email: string) => {
try {
connectToDatabase();
const token = crypto.randomInt(100_100, 999_999).toString();
const expiresAt = new Date(new Date().getTime() + 15 * 60 * 1000);
const existingToken = await TwoFactorToken.findOneAndDelete({ email });
const newTwoFactorToken = new TwoFactorToken({
email,
token,
expiresAt,
});
await newTwoFactorToken.save();
return newTwoFactorToken;
} catch (error) {
console.log(error);
}
};
Get Two-Factor Confirmation By User Id
This function retrieves the two-factor confirmation associated with the given user ID from the TwoFactorConfirmation
collection.
export async function getTwoFactorConfirmationByUserId(userId: string) {
try {
const twoFactorConfimation = await TwoFactorConfirmation.findOne({
user: userId,
});
if (!twoFactorConfimation) {
return { error: "Token not found" };
}
return twoFactorConfimation;
} catch (error) {
console.log(error);
}
}
Login With OAuth
Login With OAuth (Without Username)
This function allows users to log in using OAuth authentication. If the user already exists in the database, their account details are updated with the OAuth provider's information. If the user is new, a new account is created.
import User from "@/models/user.model";
import { connectToDatabase } from "@/lib/database";
import { AuthError } from "@/lib/errors/auth.error";
export async function LoginWithOAuth({ user, account }: any) {
try {
connectToDatabase();
const { email, name } = user;
const existingUser = await User.findOne({ email });
if (existingUser) {
existingUser.accounts = [
{
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
expires_at: account.expires_at,
expires_in: account.expires_in,
token_type: account.token_type,
scope: account.scope,
id_token: account.id_token,
},
];
await existingUser.save();
return true;
} else {
//[DefUsername]
const newUser = new User({
name,
email,
//[OauthUsername]
emailVerified: Date.now(),
accounts: [
{
provider: account.provider,
providerAccountId: account.id,
access_token: account.accessToken,
expires_at: account.expiresAt,
token_type: account.tokenType,
scope: account.scope,
id_token: account.idToken,
providerId: account.id,
},
],
});
await newUser.save();
}
return true;
} catch (error) {
// Handle authentication errors
if (error instanceof AuthError) {
switch (error.type) {
case "OAuthSignInError":
return { error: "Please Try Again" };
default:
return { error: "Something went wrong!" };
}
}
throw error;
}
}
This function takes two parameters: user
, which contains information about the user obtained from the OAuth provider, and account
, which contains details about the authentication account.
First, it checks if the user already exists in the database by searching for their email address. If the user exists, their account details are updated with the information provided by the OAuth provider.
If the user is new (i.e., not found in the database), a new user account is created using the provided user information and OAuth account details.
In case of any authentication errors, such as OAuth sign-in errors, appropriate error messages are returned.
Login With OAuth (With Username)
This is the same function as above but with the username field included in the user object.
export async function LoginWithOAuth({ user, account }: any) {
try {
connectToDatabase();
const { email, name } = user;
const existingUser = await User.findOne({ email });
if (existingUser) {
existingUser.accounts = [
{
provider: account.provider,
providerAccountId: account.providerAccountId,
access_token: account.access_token,
expires_at: account.expires_at,
expires_in: account.expires_in,
token_type: account.token_type,
scope: account.scope,
id_token: account.id_token,
},
];
await existingUser.save();
return true;
} else {
const username = email.split("@")[0];
const newUser = new User({
name,
email,
username
emailVerified: Date.now(),
accounts: [
{
provider: account.provider,
providerAccountId: account.id,
access_token: account.accessToken,
expires_at: account.expiresAt,
token_type: account.tokenType,
scope: account.scope,
id_token: account.idToken,
providerId: account.id,
},
],
});
await newUser.save();
}
return true;
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "OAuthSignInError":
return { error: "Please Try Again" };
default:
return { error: "Something went wrong!" };
}
}
throw error;
}
}
Get Verification Token By Email
This function retrieves the verification token associated with the given email from the database.
export async function getVerificationTokenByEmail(email: string) {
try {
connectToDatabase();
const Verificationtoken = await EmailVerification.findOne({ email });
if (!Verificationtoken) {
return { error: "Token not found" };
}
return Verificationtoken;
} catch (error) {
console.log(error);
}
}
Get Verification Token By Token
This function retrieves the verification token associated with the given token from the database.
export async function getVerificationTokenByToken(token: string) {
try {
connectToDatabase();
const Verificationtoken = await EmailVerification.findOne({ token });
if (!Verificationtoken) {
return { error: "Token not found" };
}
return Verificationtoken;
} catch (error) {
console.log(error);
}
}
Generate Verification Token
This function generates a verification token for the given email and stores it in the database.
import { connectToDatabase } from "@/lib/database";
import EmailVerification from "@/models/email_verify.model";
import { v4 as uuidv4 } from "uuid";
export const generateVerificationToken = async (email: string) => {
try {
connectToDatabase();
const token = uuidv4();
const expiresAt = new Date(new Date().getTime() + 1 * 60 * 60 * 1000);
const existingToken = await EmailVerification.findOneAndDelete({ email });
const newVerificationToken = new EmailVerification({
email,
token,
expiresAt,
});
await newVerificationToken.save();
return newVerificationToken;
} catch (error) {
console.log(error);
}
};
This function generates a unique verification token for the given email address and stores it in a database collection called EmailVerification
.
The function uses the uuidv4()
function from the uuid
library to create a universally unique identifier (UUID) string, which serves as the verification token. It then calculates an expiration time for the token, which is set to one hour from the current time.
Before creating a new token, the function checks if an existing token is already associated with the provided email address. If found, it deletes the existing token from the database.
Next, it creates a new document in the EmailVerification
collection, containing the email address, the generated token, and the expiration time.
Finally, the function saves the new document to the database and returns it.
In case of any errors during the process, the error is logged to the console.
Verify Token
This function verifies the provided token and marks the associated user's email as verified in the database. It first checks if the token exists and hasn't expired. If valid, it retrieves the user associated with the email, sets their emailVerified
property, and saves the changes. Finally, it deletes the token from the database.
import EmailVerification from "@/models/email_verify.model";
import User from "@/models/user.model";
export const verifyToken = async (token: string) => {
try {
connectToDatabase();
const existingToken = await EmailVerification.findOne({ token });
if (!existingToken) {
return { error: "Token not found" };
}
const hasExpired = new Date().getTime() > existingToken.expiresAt.getTime();
if (hasExpired) {
await existingToken.deleteOne();
return { error: "Token expired" };
}
const { email } = existingToken;
const user = await User.findOne({ email });
if (!user) {
return { error: "User not found" };
}
user.emailVerified = new Date();
user.email = email;
await user.save();
await existingToken.deleteOne();
return { success: "Email Verified!" };
} catch (error) {
console.log(error);
}
};
Generate Password Reset Token
This function generates a unique password reset token for the given email and stores it in the ForgotPassword
collection. It creates a new document containing the email, token, and expiration time (set to one hour from the current time).
import ForgotPassword from "@/models/forgot_pass.model";
export const generatePasswordResetToken = async (email: string) => {
try {
connectToDatabase();
const token = uuidv4();
const expiresAt = new Date(new Date().getTime() + 60 * 60 * 1000);
const existingToken = await ForgotPassword.findOneAndDelete({ email });
const newPasswordResetToken = new ForgotPassword({
email,
token,
expiresAt,
});
await newPasswordResetToken.save();
return newPasswordResetToken;
} catch (error) {
console.log(error);
}
};
Reset Password
This function initiates the password reset process. It first validates the provided email, then checks if the user exists. If found, it generates a password reset token and sends a reset email to the user containing the token.
import ForgotPassword from "@/models/forgot_pass.model";
export async function resetPassword(values: z.infer<typeof ResetSchema>) {
try {
const validatedFields = ResetSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid email" };
}
const { email } = validatedFields.data;
connectToDatabase();
const user = await User.findOne({ email });
if (!user) {
return { error: "User not found" };
}
const Verificationtoken = await generatePasswordResetToken(email);
await sendResetEmail(Verificationtoken.email, Verificationtoken.token);
return { success: "Password reset email sent" };
} catch (error) {
return { error: "Something went wrong" };
}
}
Reset Password With Token
This function resets the user's password using the provided reset token. It first validates the token and checks if it hasn't expired. If valid, it retrieves the associated user, hashes the new password, updates the user's password, saves the changes, and deletes the token from the database.
import ForgotPassword from "@/models/forgot_pass.model";
export async function resetPasswordWithToken(
values: z.infer<typeof NewPasswordSchema>,
token: string,
) {
try {
const validatedFields = NewPasswordSchema.safeParse(values);
if (!validatedFields.success) {
return { error: "Please provide a valid
email" };
}
const { password } = validatedFields.data;
connectToDatabase();
const existingToken = await ForgotPassword.findOne({ token });
if (!existingToken) {
return { error: "Token not found" };
}
const user = await User.findOne({ email: existingToken.email });
const hasExpired = new Date().getTime() > existingToken.expiresAt.getTime();
if (hasExpired) {
await existingToken.deleteOne();
return { error: "Token expired" };
}
const hashedPassword = await bcrypt.hash(values.password, 10);
user.password = hashedPassword;
await user.save();
await existingToken.deleteOne();
return { success: "Password reset" };
} catch (error) {
return { error: "Something went wrong" };
}
}
Get User By Email
This function retrieves a user from the database based on their email.
import User from "@/models/user.model";
export async function GetUserByEmail(email: string) {
try {
connectToDatabase();
const user = await User.findOne({ email });
return user;
} catch (error) {
console.log(error);
}
}
Get User By Id
This function retrieves a user from the database based on their ID.
import User from "@/models/user.model";
export async function GetUserById(id: string) {
try {
connectToDatabase();
const user = await User.findOne({ _id: id });
if (!user) {
return { error: "User not found" };
}
return user;
} catch (error) {
console.log(error);
}
}
Get Database User
This function retrieves the current session user from the database by first retrieving the user's email from the session and then finding the associated user in the database.
import User from "@/models/user.model";
import { currentUser } from "@/lib/utils/currentUser";
export async function getdbUser() {
try {
connectToDatabase();
const sessionUser = await currentUser();
if (!sessionUser) {
return { error: "User not found" };
}
const user = await User.findOne({
email: sessionUser.email,
});
return user;
} catch (error) {
console.log(error);
}
}
"use server";
//[imports]
import {
LoginSchema,
NewPasswordSchema,
RegisterSchema,
ResetSchema,
} from "@/validations";
import * as z from "zod";
import bcrypt from "bcryptjs";
import EmailVerification from "@/models/email_verify.model";
import { v4 as uuidv4 } from "uuid";
import { sendResetEmail, sendVerificationEmail } from "@/lib/mail";
import ForgotPassword from "@/models/forgot_pass.model";
import { signIn } from "@/auth";
import crypto from "crypto";
import { DEFAULT_LOGIN_REDIRECT } from "@/routes";
import { getProfileUrl } from "@/lib/constant";
import { sendTwoFactorTokenEmail } from "@/lib/mail";
import TwoFactorToken, {
TwoFactorConfirmation,
} from "@/models/two_factor_token.model";
import User from "@/models/user.model";
import { connectToDatabase } from "@/lib/database";
import { currentUser } from "@/lib/utils/currentUser";
import { AuthError } from "next-auth";