Skip to content

Commit

Permalink
Ft authorization 187584917 (#34)
Browse files Browse the repository at this point in the history
* Added authorization middleware

* Added authentication middleware

* Added authorization middleware

* Added authorization middleware

* Added authorization middleware
  • Loading branch information
Fabrice-Dush authored May 30, 2024
1 parent 7582873 commit b0606a1
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 61 deletions.
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion 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-local": "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 Expand Up @@ -82,6 +83,7 @@
"sequelize-cli": "^6.6.2",
"sequelize-typescript": "^2.1.6",
"sinon": "^18.0.0",
"sinon-chai": "^3.7.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"ts-node": "^10.9.2",
Expand All @@ -107,4 +109,4 @@
"eslint-plugin-import": "^2.29.1",
"lint-staged": "^15.2.2"
}
}
}
21 changes: 11 additions & 10 deletions src/databases/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
/* eslint-disable comma-dangle */
import dotenv from "dotenv";

dotenv.config();

const commonDatabaseConfig = {
dialect: "postgres"
dialect: "postgres",
};

const sequelizeConfig = {
development: {
...commonDatabaseConfig,
url: process.env.DATABASE_URL_DEV
url: process.env.DATABASE_URL_DEV,
},
test: {
...commonDatabaseConfig,
url: process.env.DATABASE_URL_TEST,
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false
}
}
rejectUnauthorized: false,
},
},
},
production: {
...commonDatabaseConfig,
url: process.env.DATABASE_URL_PRO,
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: false
}
}
}
rejectUnauthorized: false,
},
},
},
};

module.exports = sequelizeConfig;
module.exports = sequelizeConfig;
29 changes: 15 additions & 14 deletions src/databases/config/db.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { config } from "dotenv"
import { Sequelize } from "sequelize"
/* eslint-disable comma-dangle */
import { config } from "dotenv";
import { Sequelize } from "sequelize";

config()
const NODE_ENV: string = process.env.NODE_ENV || "development"
const DB_HOST_MODE: string = process.env.DB_HOST_TYPE || "remote"
config();
const NODE_ENV: string = process.env.NODE_ENV || "development";
const DB_HOST_MODE: string = process.env.DB_HOST_TYPE || "remote";

/**
* Get the URI for the database connection.
Expand All @@ -12,11 +13,11 @@ const DB_HOST_MODE: string = process.env.DB_HOST_TYPE || "remote"
function getDbUri(): string {
switch (NODE_ENV) {
case "development":
return process.env.DATABASE_URL_DEV as string
return process.env.DATABASE_URL_DEV as string;
case "test":
return process.env.DATABASE_URL_TEST as string
return process.env.DATABASE_URL_TEST as string;
default:
return process.env.DATABASE_URL_PRO as string
return process.env.DATABASE_URL_PRO as string;
}
}

Expand All @@ -30,15 +31,15 @@ function getDialectOptions() {
: {
ssl: {
require: true,
rejectUnauthorized: false
}
}
rejectUnauthorized: false,
},
};
}

const sequelizeConnection: Sequelize = new Sequelize(getDbUri(), {
dialect: "postgres",
dialectOptions: getDialectOptions(),
logging: false
})
logging: false,
});

export default sequelizeConnection
export default sequelizeConnection;
151 changes: 139 additions & 12 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,147 @@
import chai, { expect } from "chai";
import chaiHttp from "chai-http";
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable comma-dangle */
import app from "./index";
import chai from "chai";
import chaiHttp from "chai-http";
const { expect } = require("chai");
const sinon = require("sinon");
const sinonChai = require("sinon-chai");
const { userAuthorization } = require("./middlewares/authorization");
const httpStatus = require("http-status");
import * as helpers from "./helpers/index";
import authRepositories from "./modules/auth/repository/authRepositories";

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

describe("Initial configuration", () => {
it("Should return `Welcome to the e-Commerce-Ninja BackEnd` when GET on /", (done) => {
router()
.get("/")
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.be.a("object");
expect(res.body).to.have.property(
"message",
"Welcome to the e-Commerce Ninjas BackEnd."
);
done(err);
});
});
});

describe("userAuthorization middleware", () => {
let req, res, next, roles;

beforeEach(() => {
roles = ["admin", "user"];
req = {
headers: {},
user: null,
session: null,
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub().returnsThis(),
};
next = sinon.spy();
});

afterEach(() => {
sinon.restore();
});

it("should respond with 401 if no authorization header", async () => {
const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should respond with 401 if no session found", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves(null);

const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should respond with 401 if no user found", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves({});
sinon.stub(authRepositories, "findUserByAttributes").resolves(null);

const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should respond with 401 if user role is not authorized", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves({});
sinon
.stub(authRepositories, "findUserByAttributes")
.resolves({ role: "guest" });

const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(res.status).to.have.been.calledWith(httpStatus.UNAUTHORIZED);
expect(res.json).to.have.been.calledWith({
status: httpStatus.UNAUTHORIZED,
message: "Not authorized",
});
});

it("should call next if user is authorized", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").resolves({ id: "userId" });
sinon.stub(authRepositories, "findSessionByUserIdAndToken").resolves({});
sinon
.stub(authRepositories, "findUserByAttributes")
.resolves({ role: "admin" });

const middleware = userAuthorization(roles);
await middleware(req, res, next);

expect(next).to.have.been.calledOnce;
expect(req.user).to.deep.equal({ role: "admin" });
expect(req.session).to.deep.equal({});
});

it("should respond with 500 if an unexpected error occurs", async () => {
req.headers.authorization = "Bearer validToken";
sinon.stub(helpers, "decodeToken").rejects(new Error("Unexpected error"));

const middleware = userAuthorization(roles);
await middleware(req, res, next);

it("Should return `Welcome to the e-Commerce-Ninja BackEnd` when GET on /", (done) => {
router()
.get("/")
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body).to.be.a("object");
expect(res.body).to.have.property("message", "Welcome to the e-Commerce Ninjas BackEnd.");
done(err);
});
expect(res.status).to.have.been.calledWith(
httpStatus.INTERNAL_SERVER_ERROR
);
expect(res.json).to.have.been.calledWith({
status: httpStatus.INTERNAL_SERVER_ERROR,
message: "Unexpected error",
});
});
});
64 changes: 64 additions & 0 deletions src/middlewares/authorization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* eslint-disable comma-dangle */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Request, Response, NextFunction } from "express";
import { UsersAttributes } from "../databases/models/users";
import authRepository from "../modules/auth/repository/authRepositories";
import httpStatus from "http-status";
import { decodeToken } from "../helpers";
import Session from "../databases/models/session";

interface ExtendedRequest extends Request {
user: UsersAttributes;
session: Session;
}

export const userAuthorization = function (roles: string[]) {
return async (req: ExtendedRequest, res: Response, next: NextFunction) => {
try {
let token: string;
if (req.headers.authorization?.startsWith("Bearer")) {
token = req.headers.authorization.split(" ").at(-1);
}

if (!token) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

const decoded: any = await decodeToken(token);

const session: Session = await authRepository.findSessionByUserIdAndToken(
decoded.id, token
);
if (!session) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

const user = await authRepository.findUserByAttributes("id", decoded.id);

if (!user) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

if (!roles.includes(user.role)) {
res
.status(httpStatus.UNAUTHORIZED)
.json({ status: httpStatus.UNAUTHORIZED, message: "Not authorized" });
}

req.user = user;
req.session = session;
next();
} catch (error: any) {
res.status(httpStatus.INTERNAL_SERVER_ERROR).json({
status: httpStatus.INTERNAL_SERVER_ERROR,
message: error.message,
});
}
};
};
Loading

0 comments on commit b0606a1

Please sign in to comment.