Skip to content

use transaction so we can rollback if creating the UserAuthMethod fails #41

@github-actions

Description

@github-actions

// TODO use transaction so we can rollback if creating the UserAuthMethod fails

const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");

const { User, UserAuthMethod } = require("../../models/index.js");

const { ErrorMessages } = require("../const.js");

const { appLogger } = require("../middleware/logging.js");

async function loginAuthPair(email, password) {
  let userAuthMethod = await UserAuthMethod.scope("withSecret").findOne({
    where: { method: "email", methodIdent: email },
  });

  if (
    !userAuthMethod ||
    !(await bcrypt.compare(password, userAuthMethod.methodSecret))
  )
    throw ErrorMessages.EmailPasswordIncorrect;

  const user = await getUser(userAuthMethod.userId);

  appLogger.info("sigining userAuthMethod", userAuthMethod.userId, user);

  // authentication successful
  const token = generateToken(user.id);
  return { ...user, token };
}

// gets the uderauthmethod dbo
async function findAuthMethodByEmail(email) {
  const userAuthMethod = await UserAuthMethod.findOne({
    where: { method: "email", methodIdent: email },
    // raw: true,
    // nest: true,
  });
  if (!userAuthMethod) return null;
  return userAuthMethod;
}
async function findEmailAuthMethodForUserId(userId) {
  const userAuthMethod = await UserAuthMethod.findOne({
    where: { method: "email", userId },
    // raw: true,
    // nest: true,
  });
  if (!userAuthMethod) return null;
  return userAuthMethod;
}

async function createEmailAuth(email, password) {
  let userAuthMethod = await findAuthMethodByEmail(email);
  appLogger.debug("userAuthMethod", userAuthMethod);

  // validate
  if (email == "" || email == null) {
    throw ErrorMessages.EmailIsRequired;
  }

  if (password == "" || password == null) {
    throw ErrorMessages.PasswordIsRequired;
  }

  if (userAuthMethod) {
    throw ErrorMessages.EmailAlreadyRegistered;
  }

  // hash password
  const passwordHash = await bcrypt.hash(password, 10);
  appLogger.info("create user", email, passwordHash);

  // TODO use transaction so we can rollback if creating the UserAuthMethod fails
  const userRecord = await User.create({});
  userAuthMethod = await UserAuthMethod.create({
    userId: userRecord.id,
    method: "email",
    methodIdent: email,
    methodSecret: passwordHash,
  });

  appLogger.debug(
    "createdUser",
    userRecord.id,
    userAuthMethod.id,
    userAuthMethod.userId
  );
  return userRecord;
}

async function updateEmail(userId, newEmail) {
  const userAuthMethod = await findEmailAuthMethodForUserId(userId);

  // validate this user has email login
  if (!userAuthMethod) {
    throw ErrorMessages.EmailMethodNotFound;
  }

  // validate
  if (newEmail == "" || newEmail == null) {
    throw ErrorMessages.EmailIsRequired;
  }

  // check email is not already set
  if (userAuthMethod.methodIdent == newEmail) {
    throw ErrorMessages.EmailNotChanged;
  }

  // validate email is not already taken
  const userByEmail = await findAuthMethodByEmail(newEmail);
  if (userByEmail) {
    throw ErrorMessages.EmailAlreadyRegistered;
  }

  appLogger.debug("updateEmail OK", userId, newEmail);

  userAuthMethod.methodIdent = newEmail;
  await userAuthMethod.save();
}

async function updatePassword(userId, newPassword) {
  const userAuthMethod = await findEmailAuthMethodForUserId(userId);

  // validate this user has email login
  if (!userAuthMethod) {
    throw ErrorMessages.EmailMethodNotFound;
  }

  // validate
  if (newPassword == "" || newPassword == null) {
    throw ErrorMessages.PasswordIsRequired;
  }

  // copy params to user and save
  userAuthMethod.methodSecret = await bcrypt.hash(newPassword, 10);

  appLogger.debug("updatePassword OK", userId);

  await userAuthMethod.save();
}

async function getUser(id, scope = "defaultScope") {
  const user = await User.scope(scope).findByPk(id, { raw: true });
  if (!user) {
    throw ErrorMessages.UserNotFound;
  }

  return user;
}

async function loginAuthMethod(method, methodIdent, methodData = {}) {
  let userAuthMethod = await UserAuthMethod.findOne({
    where: { method, methodIdent },
    raw: true,
    nest: true,
  });

  // create google account
  let userRecord, userId;
  if (!userAuthMethod) {
    // create google account
    userRecord = (await User.create({})).dataValues;
    appLogger.debug("userRecord", userRecord);
    userAuthMethod = await UserAuthMethod.create({
      userId: userRecord.id,
      method,
      methodIdent,
      methodData: JSON.stringify(methodData),
    });
    userId = userRecord.id;
  } else {
    userId = userAuthMethod.userId;
    userRecord = userAuthMethod.user;
  }

  appLogger.info("sigining userAuthMethod", userId, userRecord);

  // authentication successful
  const token = generateToken(userId);
  return { ...userRecord, token };
}

async function getUserInfoFromIdToken(idToken) {
  const oauth2Client = new google.auth.OAuth2();

  const ticket = await oauth2Client.verifyIdToken({
    idToken: idToken,
    audience: GOOGLE_CLIENT_ID_WEB,
  });

  return ticket.payload;
}

function generateToken(userId) {
  const secret = process.env.JWT_SECRET;

  return jwt.sign({ sub: userId }, secret, { expiresIn: "7d" });
}

module.exports = {
  loginAuthPair,
  createEmailAuth,
  updateEmail,
  updatePassword,
  loginAuthMethod,
  getUserInfoFromIdToken,
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions