Skip to content

Commit

Permalink
- add multitenacy routes
Browse files Browse the repository at this point in the history
khevin2 committed Oct 31, 2024
1 parent 444a94a commit 05a648d
Showing 14 changed files with 314 additions and 22 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Default
HOST=http://localhost:2023
HOST=http://lvh.me:2023
CALLBACK_HOST=http://localhost:2023
NODE_ENV="development"
PORT=2023
API_VERSION=v1
5 changes: 5 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -36,6 +36,11 @@ app.use(
);
app.use((req, res, next) => {
const tenantId = req.subdomains[0];
const path = req.path
if (path.startsWith("/api/v1/users") || path.startsWith("/api/v1/auth/microsoft")) {
return next();
}

if (!tenantId) {
return res.status(400).send('No tenant found');
}
2 changes: 2 additions & 0 deletions src/database/index.js
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import reviewerModel from "./models/reviewer";
import RatingCategoryModel from "./models/ratingCategory";
import RatingFieldModel from "./models/ratingField";
import FieldReviewModel from "./models/fieldReview";
import TenantsModel from "./models/tenants"

const DB = {
sequelize, // connection instance (RAW queries)
@@ -24,6 +25,7 @@ const DB = {
RatingCategory: RatingCategoryModel(sequelize),
RatingField: RatingFieldModel(sequelize),
FieldReview: FieldReviewModel(sequelize),
Tenants: TenantsModel(sequelize)
};

export default DB;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
/*
Add altering commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
return queryInterface.addColumn('UserProjects', 'tenantId', {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'Tenants',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
})
},

down: (queryInterface, Sequelize) => {
/*
Add reverting commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.dropTable('users');
*/
return queryInterface.removeColumn('UserProjects', 'tenantId');
}
};
48 changes: 34 additions & 14 deletions src/database/models/tenants.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
'use strict';
module.exports = (sequelize, DataTypes) => {
const Tenants = sequelize.define('Tenants', {
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
import { Model, DataTypes } from "sequelize";
import sequelize from "../config/sequelize";

class Tenants extends Model { }

const TenantsModel = () => {
Tenants.init(
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
unique: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
subdomain: {
type: DataTypes.STRING,
allowNull: false,
}
},
name: DataTypes.STRING,
subdomain: DataTypes.STRING
}, {});
Tenants.associate = function(models) {
// associations can be defined here
};
{
sequelize,
modelName: "Tenants",
tableName: "Tenants",
timestamps: true,
}
);

return Tenants;
};
};

export default TenantsModel;
89 changes: 89 additions & 0 deletions src/documentation/Tenants/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import responses from "../responses";

const tenants = {
"/tenants": {
post: {
tags: ["Tenants"],
security: [{ JWT: [] }],
summary: "Create a tenant",
parameters: [
{
in: "body",
name: "body",
required: true,
schema: {
example: {
name: "test"
},
},
},
],
consumes: ["application/json"],
responses,
},
get: {
tags: ["Tenants"],
security: [{ JWT: [] }],
summary: "get all Tenants",
parameters: [],
consumes: ["application/json"],
responses,
},
},
"/tenants/{id}": {
get: {
tags: ["Tenants"],
security: [{ JWT: [] }],
summary: "Get tenant by ID",
parameters: [
{
in: "path",
name: "id",
required: true,
type: "string",
description: "Tenant ID"
}
],
consumes: ["application/json"],
responses,
},
put: {
tags: ["Tenants"],
security: [{ JWT: [] }],
summary: "Update a tenant",
parameters: [
{
in: "body",
name: "body",
required: true,
schema: {
example: {
name: "test"
},
},
}
],
consumes: ["application/json"],
responses,
},
delete: {
tags: ["Tenants"],
security: [{ JWT: [] }],
summary: "Delete a tenant",
parameters: [
{
in: "path",
name: "id",
required: true,
type: "string",
description: "Tenant ID"
}
],
consumes: ["application/json"],
responses,
},
},

};

export default tenants;
2 changes: 2 additions & 0 deletions src/documentation/index.js
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ import search from "./search";
import dashboard from "./auth/dashboard";
import ratingCategories from "./ratingCategory";
import ratingFields from "./ratingField";
import tenants from "./Tenants";

const defaults = swaggerDoc.paths;

@@ -32,6 +33,7 @@ const paths = {
...search,
...ratingCategories,
...ratingFields,
...tenants
};

const config = {
77 changes: 77 additions & 0 deletions src/restful/controllers/tenantsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import DB from "../../database"
import Response from "../../system/helpers/Response"
import createSubdomain from "../../system/utils/createSubdomain"

const { Tenants } = DB



export default class TenantController {
static async createTenant(req, res) {
const { name } = req?.body
const subdomain = await createSubdomain(name)
console.log({ subdomain })
try {
const tenant = await Tenants.create({ name, subdomain })
Response.success(res, 201, {
message: "Tenant created",
tenant
})
} catch (error) {
Response.error(res, 500, error)

}
}

static async getTenants(req, res) {
try {

const tenants = await Tenants.findAll()
Response.success(res, 200, {
tenants
})
} catch (error) {
Response.error(res, 500, error)
}

}

static async getTenantById(req, res) {
const { id } = req?.params
try {
const tenant = await Tenants.findByPk(id)
Response.success(res, 200, {
tenant
})
} catch (error) {
Response.error(res, 500, error)
}
}

static async updateTenant(req, res) {
const { id } = req?.params
const { name } = req?.body
try {
const tenant = await Tenants.update({ name }, { where: { id } })
Response.success(res, 200, {
message: "Tenant updated",
tenant
})
} catch (error) {
Response.error(res, 500, error)
}
}

static async deleteTenant(req, res) {
const { id } = req?.params
try {
const tenant = await Tenants.destroy({ where: { id } })
Response.success(res, 200, {
message: "Tenant deleted",
tenant
})
} catch (error) {
Response.error(res, 500, error)
}
}
}
6 changes: 2 additions & 4 deletions src/restful/routes/authRouters.js
Original file line number Diff line number Diff line change
@@ -6,19 +6,17 @@ import protect from "../middlewares";

const router = express.Router();
const { loginCallback } = AuthController;
const { HOST, CLIENTID, CLIENTSECRET } = process.env;
const { CALLBACK_HOST, CLIENTID, CLIENTSECRET } = process.env;

passport.use(
new MicrosoftStrategy(
{
clientID: `${CLIENTID}`,
clientSecret: `${CLIENTSECRET}`,
callbackURL: `${HOST}/api/v1/auth/microsoft/callback`,
callbackURL: `${CALLBACK_HOST}/api/v1/auth/microsoft/callback`,
scope: ["user.read"],
// tenant: '',
},
async (req, accessToken, refreshToken, profile, done) => {
// Your code here
const user = {
microsoftId: profile.id,
firstName: profile.name.givenName,
2 changes: 2 additions & 0 deletions src/restful/routes/index.js
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import searchRouter from "./searchRouter";
import dashboardRouter from "./dashboard";
import ratingCategoryRouter from "./ratingCategoryRoutes";
import ratingFieldRouter from "./ratingFieldRoutes";
import tenantRouter from "./tenants";

const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
@@ -39,6 +40,7 @@ router.use(`${url}/search`, searchRouter);
router.use(`${url}/dashboard`, dashboardRouter);
router.use(`${url}/ratingCategories`, ratingCategoryRouter);
router.use(`${url}/ratingFields`, ratingFieldRouter);
router.use(`${url}/tenants`, tenantRouter);

router.all(`${url}/`, (req, res) => {
return res.status(200).json({ message: "Welcome to codehills backend!" });
15 changes: 15 additions & 0 deletions src/restful/routes/tenants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Router } from "express";
import protect from "../middlewares";
import TenantController from "../controllers/tenantsController";
import allowedRole from "../middlewares/allowedRoles";

const router = Router();

router.post("/",protect, TenantController.createTenant);
router.get("/", protect, allowedRole(["admin"]), TenantController.getTenants);
router.get("/:id", protect, TenantController.getTenantById);
router.put("/:id", protect, allowedRole(["admin"]), TenantController.updateTenant);
router.delete("/:id", protect, allowedRole(["admin"]), TenantController.deleteTenant);


export default router;
6 changes: 3 additions & 3 deletions src/services/DashboardService.js
Original file line number Diff line number Diff line change
@@ -65,11 +65,11 @@ export default class DashboardService {
where: { role: "developer" },
});

dashboard.totalReceivedReviews = await DB.Review.count({
where: userRole === "admin" ? {} : { revieweeId: userId },
dashboard.totalReceivedReviews = await DB.Reviewer.count({
where: userRole === "admin" ? {} : { developerId: userId },
});

dashboard.totalReviews = await DB.Review.count({
dashboard.totalReviews = await DB.Reviewer.count({
where: userRole === "admin" ? {} : { reviewerId: userId },
});

15 changes: 15 additions & 0 deletions src/services/TenantService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Op } from "sequelize";
import db from "../database";
const { Tenants } = db;

export default class TenantService {
static async findAllTenants(param) {
return await Tenants.findAll()
}

static async findOneTenant(param) {
return await Tenants.findOne({
where: param
})
}
}
32 changes: 32 additions & 0 deletions src/system/utils/createSubdomain.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import DB from "../../database";
import TenantService from "../../services/TenantService";
const { Tenant } = DB;

export default async (name) => {

let subdomain = name.toLowerCase().replace(/[^a-z0-9]/g, '');


let isUnique = false;
let counter = 1;

try {
while (!isUnique) {
const existingTenant = await TenantService.findOneTenant({ subdomain });

if (!existingTenant) {
isUnique = true;
} else {

subdomain = `${subdomain}${counter}`;
counter++;
}
}

} catch (error) {
throw new Error(error);

}
console.log({ subdomain });
return subdomain;
};

0 comments on commit 05a648d

Please sign in to comment.