Skip to content

Commit

Permalink
Merge pull request #889 from hpcc-systems/yadhap/register-login
Browse files Browse the repository at this point in the history
Added registration and login routes, middlewares, controlles and util…
  • Loading branch information
FancMa01 authored Sep 20, 2024
2 parents 0caa54e + fd8b6f4 commit 07639f8
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 21 deletions.
15 changes: 4 additions & 11 deletions Tombolo/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,6 @@ DB_PORT=3306
DB_NAME=tombolo
DB_HOSTNAME=localhost

# Authentication Configuration
APP_AUTH_METHOD=

# if using auth service for authentication
AUTH_SERVICE_URL=
AUTHSERVICE_TOMBOLO_CLIENT_ID=

# If using Azure AD for authentication
TENENT_ID=
CLIENT_ID=

# Email Configuration
EMAIL_SMTP_HOST=
EMAIL_PORT=
Expand All @@ -49,6 +38,10 @@ EMAIL_SENDER=donotreply@tombolo.com
ENCRYPTION_KEY=
API_KEY_DURATION=

# Authentication and Authorization Configuration
JWT_SECRET=
JWT_REFRESH_SECRET=

# Logging Configuration
NODE_LOG_LEVEL=http

Expand Down
152 changes: 152 additions & 0 deletions Tombolo/server/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const {v4: uuidv4} = require("uuid");

const logger = require("../config/logger");
const models = require("../models");
const {
generateAccessToken,
generateRefreshToken,
} = require("../utils/authUtil");

const User = models.user;
const UserRoles = models.UserRoles;
const RoleTypes = models.RoleTypes;
const RefreshTokens = models.RefreshTokens;

// Register basic user
const createBasicUser = async (req, res) => {
try {
const { deviceInfo = {} } = req.body;
const payload = req.body;

// Hash password
const salt = bcrypt.genSaltSync(10);
payload.hash = bcrypt.hashSync(req.body.password, salt);

// Save user to DB
const user = await User.create(payload);

// remove hash from user object
const userObj = user.toJSON();
delete userObj.hash;

// Create token id
const tokenId = uuidv4();

// Create access jwt
userObj.token = generateAccessToken({ ...userObj, tokenId });

// Generate refresh token
const refreshToken = generateRefreshToken({ tokenId });

// Save refresh token to DB
const { iat, exp } = jwt.decode(refreshToken);

// Save refresh token in DB
await RefreshTokens.create({
id: tokenId,
userId: user.id,
token: refreshToken,
deviceInfo,
metaData: {},
iat: new Date(iat * 1000),
exp: new Date(exp * 1000),
});

// Send response
res.status(201).json({
success: true,
message: "User created successfully",
data: {...userObj, "UserRoles": [],},
});
} catch (err) {
logger.error(`Create user: ${err.message}`);
res
.status(err.status || 500)
.json({ success: false, message: err.message });
}
};

//Login Basic user
const loginBasicUser = async (req, res) => {
try {
const { email, password, deviceInfo = {} } = req.body;

// find user - include user roles from UserRoles table
const user = await User.findOne({
where: { email },
include: [
{
model: UserRoles,
attributes: ["id"],
as: "roles",
include: [
{
model: RoleTypes,
as: "role_details",
attributes: ["id", "roleName"],
},
],
},
],
});

// User with the given email does not exist
if (!user) {
logger.error(`Login : User with email ${email} does not exist`);
return res
.status(404)
.json({ success: false, message: "User not found" });
}

//Compare password
if (!bcrypt.compareSync(password, user.hash)) {
logger.error(`Login : Invalid password for user with email ${email}`);
return res
.status(401)
.json({ success: false, message: "Invalid password" });
}

// Remove hash from use object
const userObj = user.toJSON();
delete userObj.hash;

// Create token id
const tokenId = uuidv4();

// Create access jwt
userObj.token = generateAccessToken({ ...userObj, tokenId });

// Generate refresh token
const refreshToken = generateRefreshToken({ tokenId });

// Save refresh token to DB
const { iat, exp } = jwt.decode(refreshToken);

// Save refresh token in DB
await RefreshTokens.create({id: tokenId,
userId: user.id,
token: refreshToken,
deviceInfo,
metaData: {},
iat : new Date(iat * 1000),
exp : new Date(exp * 1000)});

// Success response
res.status(200).json({ success: true, message: "User logged in successfully", data: userObj });
} catch (err) {
logger.error(`Login user: ${err.message}`);
res.status(err.status || 500).json({ success: false, message: err.message });
}
};

// Register OAuth user

// Login OAuth user

//Exports
module.exports = {
createBasicUser,
loginBasicUser,
};
95 changes: 95 additions & 0 deletions Tombolo/server/middlewares/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Validate add user inputs using express validator
const { body, param, validationResult } = require("express-validator");
const logger = require("../config/logger");

// Validate registration payload
const validateNewUserPayload = [
body("registrationMethod")
.isString()
.notEmpty()
.withMessage("Registration method is required")
.isIn(["traditional", "microsoft"])
.withMessage(
"Registration method must be either 'traditional' or 'microsoft'"
),
body("firstName")
.isString()
.notEmpty()
.withMessage("First name is required")
.isLength({ min: 2, max: 50 })
.withMessage("First name must be between 2 and 50 characters"),
body("lastName")
.isString()
.notEmpty()
.withMessage("Last name is required")
.isLength({ min: 2, max: 50 })
.withMessage("Last name must be between 2 and 50 characters"),
body("email")
.isEmail()
.withMessage("Email address is not valid")
.notEmpty()
.withMessage("Email is required")
.isLength({ max: 100 })
.withMessage("Email must be less than 100 characters"),
body("password")
.if(body("registrationMethod").equals("traditional"))
.isString()
.notEmpty()
.withMessage("Password is required")
.isLength({ min: 8 })
.withMessage("Password must be at least 8 characters long")
.matches(/[A-Z]/)
.withMessage("Password must contain at least one uppercase letter")
.matches(/[a-z]/)
.withMessage("Password must contain at least one lowercase letter")
.matches(/[0-9]/)
.withMessage("Password must contain at least one number")
.matches(/[\W_]/)
.withMessage("Password must contain at least one special character"),
body("metaData")
.isObject()
.optional()
.withMessage("Meta data must be an object if provided"),
(req, res, next) => {
const errors = validationResult(req).array();
const errorString = errors.map((e) => e.msg).join(", ");
if (errors.length > 0) {
logger.error(`Update user: ${errorString}`);
return res.status(400).json({ success: false, message: errorString });
}
next();
},
];


// Validate login payload
const validateLoginPayload = [
body("email")
.isEmail()
.withMessage("Email address is not valid")
.notEmpty()
.withMessage("Email is required")
.isLength({ max: 100 })
.withMessage("Email must be less than 100 characters"),
body("password")
.isString()
.notEmpty()
.withMessage("Password is required"),
(req, res, next) => {
const errors = validationResult(req).array();
const errorString = errors.map((e) => e.msg).join(", ");
if (errors.length > 0) {
logger.error(`Login: ${errorString}`);
return res.status(400).json({ success: false, message: errorString });
}
next();
},
];



// Exports
module.exports = {
validateNewUserPayload,
validateLoginPayload,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use strict";

module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable("refresh_tokens", {
id: {
primaryKey: true,
type: Sequelize.UUID,
allowNull: false,
},
userId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: "users", // Name of the users model
key: "id",
},
onUpdate: "CASCADE",
onDelete: "CASCADE",
},
token: {
type: Sequelize.STRING,
allowNull: false,
},
deviceInfo: {
type: Sequelize.JSON,
allowNull: false,
},
iat: {
type: Sequelize.DATE,
allowNull: false,
},
exp:{
type: Sequelize.DATE,
allowNull: false,
},
revoked: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
revokedAt: {
type: Sequelize.DATE,
allowNull: true,
},
metaData: {
type: Sequelize.JSON,
allowNull: true,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"),
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
defaultValue: Sequelize.literal("CURRENT_TIMESTAMP"),
},
deletedAt: {
type: Sequelize.DATE,
allowNull: true,
},
});
},

down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable("refresh_tokens");
},
};
Loading

0 comments on commit 07639f8

Please sign in to comment.