Skip to content

Commit

Permalink
[delivers #187584915] Added user login
Browse files Browse the repository at this point in the history
  • Loading branch information
ProgrammerDATCH committed May 28, 2024
1 parent 15533e4 commit 213e89c
Show file tree
Hide file tree
Showing 12 changed files with 321 additions and 25 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"start": "ts-node-dev src/index.ts",
"dev": "ts-node-dev src/index.ts",
"test": "cross-env NODE_ENV=test npm run deleteAllTables && cross-env NODE_ENV=test npm run createAllTables && cross-env NODE_ENV=test npm run createAllSeeders && nyc cross-env NODE_ENV=test mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit",
"test-dev": "cross-env NODE_ENV=development npm run deleteAllTables && cross-env NODE_ENV=development npm run createAllTables && cross-env NODE_ENV=development npm run createAllSeeders && nyc cross-env NODE_ENV=development mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit",
"coveralls": "nyc --reporter=lcov --reporter=text-lcov npm test | coveralls",
"coverage": "cross-env NODE_ENV=test nyc mocha --require ts-node/register 'src/**/*.spec.ts' --timeout 600000 --exit",
"lint": "eslint . --ext .ts",
Expand Down
44 changes: 44 additions & 0 deletions src/databases/migrations/20240523180022-create-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { QueryInterface, DataTypes } from "sequelize";
export default {
up: async (queryInterface: QueryInterface) => {
await queryInterface.createTable("tokens", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: new DataTypes.INTEGER,
allowNull: false
},
device: {
type: new DataTypes.STRING(280),
allowNull: true
},
accessToken: {
type: new DataTypes.STRING(280),
allowNull: true
},
verifyToken: {
type: new DataTypes.STRING(280),
allowNull: true
},
createdAt: {
field: "createdAt",
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updatedAt: {
field: "updatedAt",
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
});
},

down: async (queryInterface: QueryInterface) => {
await queryInterface.dropTable("tokens");
}
};
73 changes: 73 additions & 0 deletions src/databases/models/tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable require-jsdoc */
import { Model, DataTypes } from "sequelize";
import sequelizeConnection from "../config/db.config";
export interface TokenAttributes {
id: number;
userId: number;
device: string;
accessToken: string;
verifyToken: string;
createdAt: Date;
updatedAt: Date;
}

class Tokens extends Model<TokenAttributes> implements TokenAttributes {
declare id: number;
declare userId: number;
declare device: string;
declare accessToken: string;
declare verifyToken:string;
declare createdAt: Date;
declare updatedAt: Date;

static associate(models: any) {
Tokens.belongsTo(models.Users, { foreignKey: "userId",as: "user" });
}
}

Tokens.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
userId: {
type: new DataTypes.INTEGER,
allowNull: false
},
device: {
type: new DataTypes.STRING(280),
allowNull: true
},
accessToken: {
type: new DataTypes.STRING(280),
allowNull: true
},
verifyToken: {
type: new DataTypes.STRING(280),
allowNull: true
},
createdAt: {
field: "createdAt",
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
},
updatedAt: {
field: "updatedAt",
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW
}
},
{
sequelize: sequelizeConnection,
tableName: "tokens",
timestamps: true,
modelName:"Tokens"
}
);

export default Tokens;
3 changes: 2 additions & 1 deletion src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ dotenv.config
const generateToken = (id: number) => {
return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1h" });
};
export { generateToken}

export { generateToken }
34 changes: 26 additions & 8 deletions src/middlewares/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import authRepositories from "../modules/auth/repository/authRepositories";
import { UsersAttributes } from "../databases/models/users";
import Joi from "joi";
import httpStatus from "http-status";
import { IRequest } from "../types";

const validation = (schema: Joi.ObjectSchema | Joi.ArraySchema) => async (req: Request, res: Response, next: NextFunction) => {
try {
Expand All @@ -22,16 +23,33 @@ const validation = (schema: Joi.ObjectSchema | Joi.ArraySchema) => async (req: R

const isUserExist = async (req: Request, res: Response, next: NextFunction) => {
try {
const email:string = req.body.email
const userExists:UsersAttributes = await authRepositories.findUserByEmail(email);
if (userExists) {
return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "User already exists." });
const email: string = req.body.email
const userExists: UsersAttributes = await authRepositories.findUserByEmail(email);
if (userExists) {
return res.status(httpStatus.BAD_REQUEST).json({ status: httpStatus.BAD_REQUEST, message: "User already exists." });
}
return next();
} catch (error) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message })
}
return next();

}

const checkLoginUser = async (req: Request, res: Response, next: NextFunction) => {
try {
const email: string = req.body.email
const user: UsersAttributes = await authRepositories.findUserByEmail(email);
if (user) {
const passwordMatches = (req.body.password === user.password)
if (!passwordMatches) return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password", data: null });
(req as IRequest).loginUserId = user.id;
return next();
}
return res.status(httpStatus.BAD_REQUEST).json({ message: "Invalid Email or Password", data: null });
} catch (error) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR , message: error.message})
// res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: "Server error", data: error.message })
}

}

export {validation,isUserExist};
export { validation, isUserExist, checkLoginUser };
34 changes: 27 additions & 7 deletions src/modules/auth/controller/authControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@ import userRepositories from "../repository/authRepositories";
import { generateToken } from "../../../helpers";
import httpStatus from "http-status";
import { UsersAttributes } from "../../../databases/models/users";
import { IRequest, IToken } from "../../../types";


const registerUser = async (req: Request, res: Response): Promise<void> => {
try {
const register:UsersAttributes = await userRepositories.registerUser(req.body);
const register: UsersAttributes = await userRepositories.registerUser(req.body);
const token: string = generateToken(register.id);
const data = {register, token};
res.status(httpStatus.OK).json({message:"Account created successfully. Please check email to verify account.", data})
}catch(error) {
const data = { register, token };

res.status(httpStatus.OK).json({ message: "Account created successfully. Please check email to verify account.", data })
} catch (error) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ status: httpStatus.INTERNAL_SERVER_ERROR, message: error.message });
}

}

export default { registerUser }

const loginUser = async (req: Request, res: Response) => {
try {
const userId = (req as IRequest).loginUserId;
const token = generateToken(userId);
const newToken: IToken = {
userId,
device: req.headers["user-agent"] || "TEST DEVICE",
accessToken: token
}
await userRepositories.addToken(newToken);
res.status(httpStatus.OK).json({ message: "Logged in successfully", data: { token } });
}
catch (err) {
// return res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ message: "Server error", data: err.message });
}
}


export default { registerUser, loginUser }
8 changes: 7 additions & 1 deletion src/modules/auth/repository/authRepositories.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Users from "../../../databases/models/users"
import Tokens from "../../../databases/models/tokens"
import { IToken } from "../../../types"

const registerUser = async (body:any) =>{
return await Users.create(body)
Expand All @@ -9,5 +11,9 @@ const findUserByEmail = async (email:string) =>{
return await Users.findOne({ where: { email: email} })
}

const addToken = async (body: IToken) =>{
return await Tokens.create(body);
}


export default {registerUser, findUserByEmail}
export default { registerUser, findUserByEmail, addToken }
66 changes: 64 additions & 2 deletions src/modules/auth/test/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import { isUserExist } from "../../../middlewares/validation";
import authRepositories from "../repository/authRepositories";
import Users from "../../../databases/models/users";



chai.use(chaiHttp);
const router = () => chai.request(app);

describe("Authentication Test Cases", () => {

it("Should be able to register new user", (done) => {
router()
.post("/api/auth/register")
Expand All @@ -29,6 +29,66 @@ describe("Authentication Test Cases", () => {
});
});

it("Should be able to login a registered user", (done) => {
router()
.post("/api/auth/login")
.send({
email: "[email protected]",
password: "password123"
})
.end((error, response) => {
expect(response.status).to.equal(httpStatus.OK);
expect(response.body).to.be.a("object");
expect(response.body).to.have.property("data");
expect(response.body.message).to.be.a("string");
expect(response.body.data).to.have.property("token");
done(error);
});
});

it("Should return validation error when no email or password given", (done) => {
router()
.post("/api/auth/login")
.send({
email: "[email protected]"
})
.end((error, response) => {
expect(response).to.have.status(httpStatus.BAD_REQUEST);
expect(response.body).to.be.a("object");
done(error);
});
});

it("Should not be able to login user with invalid Email", (done) => {
router()
.post("/api/auth/login")
.send({
email: "[email protected]",
password: "fakepassword"
})
.end((error, response) => {
expect(response).to.have.status(httpStatus.BAD_REQUEST);
expect(response.body).to.be.a("object");
expect(response.body).to.have.property("message", "Invalid Email or Password");
done(error);
});
});

it("Should not be able to login user with invalid Password", (done) => {
router()
.post("/api/auth/login")
.send({
email: "[email protected]",
password: "fakepassword"
})
.end((error, response) => {
expect(response).to.have.status(httpStatus.BAD_REQUEST);
expect(response.body).to.be.a("object");
expect(response.body).to.have.property("message", "Invalid Email or Password");
done(error);
});
});

it("should return validation return message error and 400", (done) => {
router()
.post("/api/auth/register")
Expand All @@ -43,6 +103,7 @@ describe("Authentication Test Cases", () => {
done(error);
});
});

})

describe("isUserExist Middleware", () => {
Expand Down Expand Up @@ -102,6 +163,7 @@ describe("isUserExist Middleware", () => {
});
});
});

describe("POST /auth/register - Error Handling", () => {
let registerUserStub: sinon.SinonStub;

Expand Down
7 changes: 6 additions & 1 deletion src/modules/auth/validation/authValidations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ const authSchema = Joi.object<User>({
})
});

export {authSchema};
const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(6).required()
});

export { authSchema, loginSchema };
5 changes: 3 additions & 2 deletions src/routes/authRouter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {validation,isUserExist} from "../middlewares/validation";
import { validation, isUserExist, checkLoginUser } from "../middlewares/validation";
import authControllers from "../modules/auth/controller/authControllers";
import { Router } from "express";
import {authSchema} from "../modules/auth/validation/authValidations";
import { authSchema, loginSchema } from "../modules/auth/validation/authValidations";


const router: Router = Router();

router.post("/register", validation(authSchema), isUserExist, authControllers.registerUser);
router.post("/login", validation(loginSchema), checkLoginUser, authControllers.loginUser);


export default router;
Loading

0 comments on commit 213e89c

Please sign in to comment.