Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ft authorization 187584917 #34

Merged
merged 5 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading