Skip to content
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
1 change: 1 addition & 0 deletions src/constants/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ module.exports = {
refreshTokenLimit: 3,
otpExpirationTime: process.env.OTP_EXP_TIME, // In Seconds,
ADMIN_ROLE: 'admin',
TENANT_ADMIN_ROLE: 'tenant_admin',
ORG_ADMIN_ROLE: 'org_admin',
USER_ROLE: 'user',
SESSION_MANAGER_ROLE: 'session_manager',
Expand Down
43 changes: 43 additions & 0 deletions src/controllers/v1/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,49 @@ module.exports = class Admin {
}
}

/**
* Assigns a role to a user.
*
* Extracts `organization_code` and `tenant_code` from `req.decodedToken`
* instead of the request body and delegates the actual role assignment
* to the service layer. Performs only an admin-access check.
*
* @async
* @function assignRole
* @param {import('express').Request} req - Express request object
* @param {Object} req.decodedToken - Decoded JWT payload
* @param {string} req.decodedToken.organization_code - Organization code from token
* @param {string} req.decodedToken.tenant_code - Tenant code from token
* @param {Object} req.body - Request body containing assignment data
* @param {number|string} req.body.user_id - Target user ID
* @param {number|string} req.body.role_id - Role ID to assign
* @param {number|string} [req.body.organization_id] - Optional organization ID
* @returns {Promise<Object>} Service response object
*/

async assignRole(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ADMIN_ROLE)) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
responseCode: 'CLIENT_ERROR',
})
}

const params = {
organization_code: req.decodedToken.organization_code,
tenant_code: req.decodedToken.tenant_code,
}

// Pass token-derived params separately per new service pattern
const user = await adminService.assignRole(params, req.body)
return user
} catch (error) {
return error
}
}

/**
* create admin users
* @method
Expand Down
8 changes: 7 additions & 1 deletion src/controllers/v1/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ module.exports = class NotificationTemplate {

async template(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, [common.ADMIN_ROLE, common.ORG_ADMIN_ROLE])) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ADMIN_ROLE,
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand Down
49 changes: 42 additions & 7 deletions src/controllers/v1/org-admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ module.exports = class OrgAdmin {
*/
async bulkUserCreate(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand All @@ -48,7 +53,12 @@ module.exports = class OrgAdmin {
*/
async getBulkInvitesFilesList(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand All @@ -72,7 +82,12 @@ module.exports = class OrgAdmin {
*/
async getRequestDetails(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand Down Expand Up @@ -102,7 +117,12 @@ module.exports = class OrgAdmin {
*/
async getRequests(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand All @@ -129,7 +149,12 @@ module.exports = class OrgAdmin {
*/
async updateRequestStatus(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand All @@ -153,7 +178,12 @@ module.exports = class OrgAdmin {
*/
async deactivateUser(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand All @@ -178,7 +208,12 @@ module.exports = class OrgAdmin {

async inheritEntityType(req) {
try {
if (!utilsHelper.validateRoleAccess(req.decodedToken.roles, common.ORG_ADMIN_ROLE)) {
if (
!utilsHelper.validateRoleAccess(req.decodedToken.roles, [
common.ORG_ADMIN_ROLE,
common.TENANT_ADMIN_ROLE,
])
) {
throw responses.failureResponse({
message: 'USER_IS_NOT_A_ADMIN',
statusCode: httpStatusCode.bad_request,
Expand Down
9 changes: 5 additions & 4 deletions src/controllers/v1/organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module.exports = class Organization {
let isAdmin = false
const roles = req.decodedToken.roles
if (roles && roles.length > 0) {
isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE)
isAdmin = utilsHelper.validateRoleAccess(roles, [common.ADMIN_ROLE, common.TENANT_ADMIN_ROLE])
}

if (!isAdmin) {
Expand Down Expand Up @@ -68,7 +68,7 @@ module.exports = class Organization {

if (roles && roles.length > 0) {
isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE)
isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE)
isOrgAdmin = utilsHelper.validateRoleAccess(roles, [common.ORG_ADMIN_ROLE, common.TENANT_ADMIN_ROLE])
}

if (req.params.id != req.decodedToken.organization_id && isOrgAdmin) {
Expand Down Expand Up @@ -170,6 +170,7 @@ module.exports = class Organization {
isAdmin =
utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE) ||
utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE) ||
utilsHelper.validateRoleAccess(roles, common.TENANT_ADMIN_ROLE) ||
false
}
const result = await orgService.details(
Expand Down Expand Up @@ -215,7 +216,7 @@ module.exports = class Organization {

if (roles && roles.length > 0) {
isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE)
isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE)
isOrgAdmin = utilsHelper.validateRoleAccess(roles, [common.ORG_ADMIN_ROLE, common.TENANT_ADMIN_ROLE])
}
if (!isAdmin && !isOrgAdmin) {
throw responses.failureResponse({
Expand Down Expand Up @@ -260,7 +261,7 @@ module.exports = class Organization {

if (roles && roles.length > 0) {
isAdmin = utilsHelper.validateRoleAccess(roles, common.ADMIN_ROLE)
isOrgAdmin = utilsHelper.validateRoleAccess(roles, common.ORG_ADMIN_ROLE)
isOrgAdmin = utilsHelper.validateRoleAccess(roles, [common.ORG_ADMIN_ROLE, common.TENANT_ADMIN_ROLE])
}
if (!isAdmin && !isOrgAdmin) {
throw responses.failureResponse({
Expand Down
139 changes: 139 additions & 0 deletions src/database/migrations/20251022160602-add-tenant-admin-role.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict'

module.exports = {
up: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction()

try {
// Step 1: Get all unique organization_ids per tenant from existing user_roles
const [orgsByTenant] = await queryInterface.sequelize.query(
`SELECT DISTINCT tenant_code, organization_id
FROM user_roles
WHERE deleted_at IS NULL
AND tenant_code IN (
SELECT code FROM tenants WHERE deleted_at IS NULL
)
ORDER BY tenant_code, organization_id`,
{ transaction }
)

if (orgsByTenant.length === 0) {
console.log('No active organizations found. Skipping migration.')
await transaction.commit()
return
}

console.log(`Found ${orgsByTenant.length} organization-tenant combinations`)

// Step 2: Insert tenant_admin role for each tenant-organization combination
const userRoleInserts = orgsByTenant.map((org) => ({
title: 'tenant_admin',
label: 'Tenant Admin',
user_type: 1, // Adjust this value based on your user_type convention
status: 'ACTIVE',
organization_id: org.organization_id,
visibility: 'PUBLIC',
tenant_code: org.tenant_code,
translations: null,
created_at: new Date(),
updated_at: new Date(),
}))

await queryInterface.bulkInsert('user_roles', userRoleInserts, {
transaction,
ignoreDuplicates: true, // In case role already exists
})

console.log(`Inserted tenant_admin role for ${userRoleInserts.length} organization-tenant combinations`)

// Step 3: Get all admin permissions except admin module and admin-only permissions
// Excluding:
// - module = 'admin' (permission_ids: 22, 23, 26)
// - Admin-only feature permission (40)
// - Admin-only tenant permission (35)
// Including organization permissions (8, 28, 29, 30) as per requirement
const [adminPermissions] = await queryInterface.sequelize.query(
`SELECT DISTINCT
permission_id,
module,
request_type,
api_path,
created_at,
updated_at,
created_by
FROM role_permission_mapping
WHERE role_title = 'admin'
AND module != 'admin'
AND permission_id NOT IN (35, 40)
ORDER BY permission_id`,
{ transaction }
)

console.log(`Found ${adminPermissions.length} permissions to copy for tenant_admin`)

// Step 4: Insert permissions for tenant_admin role
if (adminPermissions.length > 0) {
const permissionInserts = adminPermissions.map((perm) => ({
role_title: 'tenant_admin',
permission_id: perm.permission_id,
module: perm.module,
request_type: perm.request_type,
api_path: perm.api_path,
created_at: new Date(),
updated_at: new Date(),
created_by: perm.created_by,
}))

await queryInterface.bulkInsert('role_permission_mapping', permissionInserts, {
transaction,
ignoreDuplicates: true,
})

console.log(`Inserted ${permissionInserts.length} permissions for tenant_admin role`)
}

// Commit transaction
await transaction.commit()
console.log('Migration completed successfully')
console.log('Summary:')
console.log(`- Created tenant_admin roles: ${userRoleInserts.length}`)
console.log(`- Assigned permissions: ${adminPermissions.length}`)
console.log('- Excluded modules: admin')
console.log('- Excluded permissions: 35 (tenant), 40 (feature full CRUD)')
} catch (error) {
// Rollback transaction on error
await transaction.rollback()
console.error('Migration failed, rolled back:', error)
throw error
}
},

down: async (queryInterface, Sequelize) => {
const transaction = await queryInterface.sequelize.transaction()

try {
// Step 1: Delete all tenant_admin permissions from role_permission_mapping
const [deletePermResult] = await queryInterface.sequelize.query(
`DELETE FROM role_permission_mapping WHERE role_title = 'tenant_admin'`,
{ transaction }
)

console.log('Deleted all tenant_admin permissions')

// Step 2: Delete all tenant_admin roles from user_roles (soft delete if paranoid)
const [deleteRoleResult] = await queryInterface.sequelize.query(
`DELETE FROM user_roles WHERE title = 'tenant_admin'`,
{ transaction }
)

console.log('Deleted all tenant_admin roles')

await transaction.commit()
console.log('Rollback completed successfully')
} catch (error) {
await transaction.rollback()
console.error('Rollback failed:', error)
throw error
}
},
}
5 changes: 4 additions & 1 deletion src/database/queries/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ exports.listUsers = async (roleId, organization_id, page, limit, search, tenant_

// Final query using the updated schema
let { count, rows: users } = await database.User.findAndCountAll({
where: userWhereClause,
where: {
...userWhereClause,
tenant_code, // Ensure this is in the main where clause
},
attributes: ['id', 'name', 'about', 'image'],
offset: parseInt(offset, 10),
limit: parseInt(limit, 10),
Expand Down
8 changes: 7 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@
"QUERY_FORBIDDEN_INJECTION_PATTERNS": "Query contains forbidden injection patterns.",
"QUERY_FORBIDDEN_PATTERNS": "Query contains forbidden SQL operations.",
"ADD_ORG_HEADER": "Please provide all required organization headers: {{orgCodeHeader}}, and {{tenantCodeHeader}} for admin override.",
"ORG_CODE_REQUIRED_FOR_TENANT_ADMIN": "Please provide header {{orgCodeHeader}} for admin override.",
"INVALID_ORG_ID": "Organization ID must be a valid positive integer.",
"INVALID_ORG_OR_TENANT_CODE": "The provided organization or tenant code is invalid or does not match."
"INVALID_ORG_OR_TENANT_CODE": "The provided organization or tenant code is invalid or does not match.",
"INVALID_ORG_CODE_FOR_TENANT": "The provided organization is invalid or does not match.",
"USER_ORGANIZATION_NOT_FOUND": "The user is not associated with the specified organization.",
"USER_ROLE_ALREADY_EXISTS": "The user already has this role assigned.",
"INVALID_ROLE_ID": "The provided role ID is invalid.",
"USER_ROLE_ASSIGNED_SUCCESSFULLY": "The role was assigned to the user successfully."
}
Loading