Autosave: 20260404-172721
This commit is contained in:
parent
77b3bcf3a6
commit
716d1e45e3
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
const FileDBApi = require('./file');
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -188,7 +187,6 @@ module.exports = class PropertiesDBApi {
|
|||||||
static async update(id, data, options) {
|
static async update(id, data, options) {
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
const globalAccess = currentUser.app_role?.globalAccess;
|
|
||||||
|
|
||||||
const properties = await db.properties.findByPk(id, {}, {transaction});
|
const properties = await db.properties.findByPk(id, {}, {transaction});
|
||||||
|
|
||||||
@ -451,10 +449,6 @@ module.exports = class PropertiesDBApi {
|
|||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -764,16 +758,14 @@ module.exports = class PropertiesDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
||||||
let where = {};
|
const filters = [];
|
||||||
|
|
||||||
|
|
||||||
if (!globalAccess && organizationId) {
|
if (!globalAccess && organizationId) {
|
||||||
where.organizationId = organizationId;
|
filters.push({ organizationsId: organizationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
filters.push({
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
Utils.ilike(
|
||||||
@ -782,9 +774,13 @@ module.exports = class PropertiesDBApi {
|
|||||||
query,
|
query,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const where = filters.length > 1
|
||||||
|
? { [Op.and]: filters }
|
||||||
|
: (filters[0] || {});
|
||||||
|
|
||||||
const records = await db.properties.findAll({
|
const records = await db.properties.findAll({
|
||||||
attributes: [ 'id', 'name' ],
|
attributes: [ 'id', 'name' ],
|
||||||
where,
|
where,
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const FileDBApi = require('./file');
|
|
||||||
const crypto = require('crypto');
|
|
||||||
const Utils = require('../utils');
|
const Utils = require('../utils');
|
||||||
|
|
||||||
|
|
||||||
@ -176,7 +174,6 @@ module.exports = class Unit_typesDBApi {
|
|||||||
static async update(id, data, options) {
|
static async update(id, data, options) {
|
||||||
const currentUser = (options && options.currentUser) || {id: null};
|
const currentUser = (options && options.currentUser) || {id: null};
|
||||||
const transaction = (options && options.transaction) || undefined;
|
const transaction = (options && options.transaction) || undefined;
|
||||||
const globalAccess = currentUser.app_role?.globalAccess;
|
|
||||||
|
|
||||||
const unit_types = await db.unit_types.findByPk(id, {}, {transaction});
|
const unit_types = await db.unit_types.findByPk(id, {}, {transaction});
|
||||||
|
|
||||||
@ -404,10 +401,6 @@ module.exports = class Unit_typesDBApi {
|
|||||||
|
|
||||||
offset = currentPage * limit;
|
offset = currentPage * limit;
|
||||||
|
|
||||||
const orderBy = null;
|
|
||||||
|
|
||||||
const transaction = (options && options.transaction) || undefined;
|
|
||||||
|
|
||||||
let include = [
|
let include = [
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -771,16 +764,14 @@ module.exports = class Unit_typesDBApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
static async findAllAutocomplete(query, limit, offset, globalAccess, organizationId,) {
|
||||||
let where = {};
|
const filters = [];
|
||||||
|
|
||||||
|
|
||||||
if (!globalAccess && organizationId) {
|
if (!globalAccess && organizationId) {
|
||||||
where.organizationId = organizationId;
|
filters.push({ organizationsId: organizationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
where = {
|
filters.push({
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{ ['id']: Utils.uuid(query) },
|
{ ['id']: Utils.uuid(query) },
|
||||||
Utils.ilike(
|
Utils.ilike(
|
||||||
@ -789,9 +780,13 @@ module.exports = class Unit_typesDBApi {
|
|||||||
query,
|
query,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const where = filters.length > 1
|
||||||
|
? { [Op.and]: filters }
|
||||||
|
: (filters[0] || {});
|
||||||
|
|
||||||
const records = await db.unit_types.findAll({
|
const records = await db.unit_types.findAll({
|
||||||
attributes: [ 'id', 'name' ],
|
attributes: [ 'id', 'name' ],
|
||||||
where,
|
where,
|
||||||
|
|||||||
@ -0,0 +1,56 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const USER_MANAGEMENT_PERMISSIONS = [
|
||||||
|
'CREATE_USERS',
|
||||||
|
'READ_USERS',
|
||||||
|
'UPDATE_USERS',
|
||||||
|
'DELETE_USERS',
|
||||||
|
'READ_ROLES',
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
async up(queryInterface) {
|
||||||
|
const sequelize = queryInterface.sequelize;
|
||||||
|
const [roles] = await sequelize.query(
|
||||||
|
`SELECT "id" FROM "roles" WHERE "name" = 'Administrator' LIMIT 1;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!roles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [permissions] = await sequelize.query(
|
||||||
|
`SELECT "id", "name" FROM "permissions" WHERE "name" IN (:permissionNames);`,
|
||||||
|
{
|
||||||
|
replacements: { permissionNames: USER_MANAGEMENT_PERMISSIONS },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const permission of permissions) {
|
||||||
|
await sequelize.query(
|
||||||
|
`INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId")
|
||||||
|
SELECT :createdAt, :updatedAt, :roleId, :permissionId
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM "rolesPermissionsPermissions"
|
||||||
|
WHERE "roles_permissionsId" = :roleId
|
||||||
|
AND "permissionId" = :permissionId
|
||||||
|
);`,
|
||||||
|
{
|
||||||
|
replacements: {
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
roleId: roles[0].id,
|
||||||
|
permissionId: permission.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async down() {
|
||||||
|
// Intentionally left blank. This protects live permission assignments.
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -5,9 +5,6 @@ const PropertiesService = require('../services/properties');
|
|||||||
const PropertiesDBApi = require('../db/api/properties');
|
const PropertiesDBApi = require('../db/api/properties');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { parse } = require('json2csv');
|
const { parse } = require('json2csv');
|
||||||
@ -15,8 +12,24 @@ const { parse } = require('json2csv');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
|
checkPermissions,
|
||||||
} = require('../middlewares/check-permissions');
|
} = require('../middlewares/check-permissions');
|
||||||
|
|
||||||
|
router.get('/autocomplete', checkPermissions('READ_BOOKING_REQUESTS'), async (req, res) => {
|
||||||
|
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||||
|
const organizationId = req.currentUser.organization?.id;
|
||||||
|
|
||||||
|
const payload = await PropertiesDBApi.findAllAutocomplete(
|
||||||
|
req.query.query,
|
||||||
|
req.query.limit,
|
||||||
|
req.query.offset,
|
||||||
|
globalAccess,
|
||||||
|
organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).send(payload);
|
||||||
|
});
|
||||||
|
|
||||||
router.use(checkCrudPermissions('properties'));
|
router.use(checkCrudPermissions('properties'));
|
||||||
|
|
||||||
|
|
||||||
@ -394,22 +407,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get('/autocomplete', async (req, res) => {
|
|
||||||
|
|
||||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
|
||||||
|
|
||||||
const organizationId = req.currentUser.organization?.id
|
|
||||||
|
|
||||||
|
|
||||||
const payload = await PropertiesDBApi.findAllAutocomplete(
|
|
||||||
req.query.query,
|
|
||||||
req.query.limit,
|
|
||||||
req.query.offset,
|
|
||||||
globalAccess, organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(200).send(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
|
|||||||
@ -5,9 +5,6 @@ const Unit_typesService = require('../services/unit_types');
|
|||||||
const Unit_typesDBApi = require('../db/api/unit_types');
|
const Unit_typesDBApi = require('../db/api/unit_types');
|
||||||
const wrapAsync = require('../helpers').wrapAsync;
|
const wrapAsync = require('../helpers').wrapAsync;
|
||||||
|
|
||||||
const config = require('../config');
|
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const { parse } = require('json2csv');
|
const { parse } = require('json2csv');
|
||||||
@ -15,8 +12,24 @@ const { parse } = require('json2csv');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
checkCrudPermissions,
|
checkCrudPermissions,
|
||||||
|
checkPermissions,
|
||||||
} = require('../middlewares/check-permissions');
|
} = require('../middlewares/check-permissions');
|
||||||
|
|
||||||
|
router.get('/autocomplete', checkPermissions('READ_BOOKING_REQUESTS'), async (req, res) => {
|
||||||
|
const globalAccess = req.currentUser.app_role.globalAccess;
|
||||||
|
const organizationId = req.currentUser.organization?.id;
|
||||||
|
|
||||||
|
const payload = await Unit_typesDBApi.findAllAutocomplete(
|
||||||
|
req.query.query,
|
||||||
|
req.query.limit,
|
||||||
|
req.query.offset,
|
||||||
|
globalAccess,
|
||||||
|
organizationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).send(payload);
|
||||||
|
});
|
||||||
|
|
||||||
router.use(checkCrudPermissions('unit_types'));
|
router.use(checkCrudPermissions('unit_types'));
|
||||||
|
|
||||||
|
|
||||||
@ -403,22 +416,6 @@ router.get('/count', wrapAsync(async (req, res) => {
|
|||||||
* 500:
|
* 500:
|
||||||
* description: Some server error
|
* description: Some server error
|
||||||
*/
|
*/
|
||||||
router.get('/autocomplete', async (req, res) => {
|
|
||||||
|
|
||||||
const globalAccess = req.currentUser.app_role.globalAccess;
|
|
||||||
|
|
||||||
const organizationId = req.currentUser.organization?.id
|
|
||||||
|
|
||||||
|
|
||||||
const payload = await Unit_typesDBApi.findAllAutocomplete(
|
|
||||||
req.query.query,
|
|
||||||
req.query.limit,
|
|
||||||
req.query.offset,
|
|
||||||
globalAccess, organizationId,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.status(200).send(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const db = require('../db/models');
|
const db = require('../db/models');
|
||||||
const UsersDBApi = require('../db/api/users');
|
const UsersDBApi = require('../db/api/users');
|
||||||
const processFile = require("../middlewares/upload");
|
const processFile = require('../middlewares/upload');
|
||||||
const ValidationError = require('./notifications/errors/validation');
|
const ValidationError = require('./notifications/errors/validation');
|
||||||
const csv = require('csv-parser');
|
const csv = require('csv-parser');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
@ -35,6 +35,32 @@ async function findDefaultRole(transaction) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAdminOrSuperAdmin(currentUser) {
|
||||||
|
return [config.roles.admin, config.roles.super_admin].includes(currentUser?.app_role?.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHighTrustRole(roleName) {
|
||||||
|
return Boolean(roleName) && HIGH_TRUST_ROLE_NAMES.includes(roleName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertManageableExistingUser(user, currentUser) {
|
||||||
|
if (!user) {
|
||||||
|
throw new ValidationError('iam.errors.userNotFound');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdminOrSuperAdmin(currentUser)) {
|
||||||
|
throw new ValidationError('errors.forbidden.message');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser?.id === user.id) {
|
||||||
|
throw new ValidationError('iam.errors.deletingHimself');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUser?.app_role?.globalAccess && isHighTrustRole(user?.app_role?.name)) {
|
||||||
|
throw new ValidationError('errors.forbidden.message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function normalizeManagedUserPayload(data, currentUser, transaction, existingUser = null) {
|
async function normalizeManagedUserPayload(data, currentUser, transaction, existingUser = null) {
|
||||||
const payload = { ...data };
|
const payload = { ...data };
|
||||||
const isSuperAdmin = Boolean(currentUser?.app_role?.globalAccess);
|
const isSuperAdmin = Boolean(currentUser?.app_role?.globalAccess);
|
||||||
@ -57,11 +83,11 @@ async function normalizeManagedUserPayload(data, currentUser, transaction, exist
|
|||||||
throw new ValidationError('errors.forbidden.message');
|
throw new ValidationError('errors.forbidden.message');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser?.app_role?.name && !isSuperAdmin && HIGH_TRUST_ROLE_NAMES.includes(existingUser.app_role.name)) {
|
if (existingUser?.app_role?.name && !isSuperAdmin && isHighTrustRole(existingUser.app_role.name)) {
|
||||||
throw new ValidationError('errors.forbidden.message');
|
throw new ValidationError('errors.forbidden.message');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedRole?.name && !isSuperAdmin && HIGH_TRUST_ROLE_NAMES.includes(selectedRole.name)) {
|
if (selectedRole?.name && !isSuperAdmin && isHighTrustRole(selectedRole.name)) {
|
||||||
throw new ValidationError('errors.forbidden.message');
|
throw new ValidationError('errors.forbidden.message');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,8 +106,8 @@ async function normalizeManagedUserPayload(data, currentUser, transaction, exist
|
|||||||
module.exports = class UsersService {
|
module.exports = class UsersService {
|
||||||
static async create(data, currentUser, sendInvitationEmails = true, host) {
|
static async create(data, currentUser, sendInvitationEmails = true, host) {
|
||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
let email = data.email;
|
const email = data.email;
|
||||||
let emailsToInvite = [];
|
const emailsToInvite = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -171,6 +197,16 @@ module.exports = class UsersService {
|
|||||||
throw new ValidationError('iam.errors.userNotFound');
|
throw new ValidationError('iam.errors.userNotFound');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!currentUser?.app_role?.globalAccess) {
|
||||||
|
if (currentUser?.id === id) {
|
||||||
|
throw new ValidationError('errors.forbidden.message');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHighTrustRole(user?.app_role?.name)) {
|
||||||
|
throw new ValidationError('errors.forbidden.message');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const normalizedPayload = await normalizeManagedUserPayload(data, currentUser, transaction, user);
|
const normalizedPayload = await normalizeManagedUserPayload(data, currentUser, transaction, user);
|
||||||
|
|
||||||
const updatedUser = await UsersDBApi.update(
|
const updatedUser = await UsersDBApi.update(
|
||||||
@ -195,22 +231,9 @@ module.exports = class UsersService {
|
|||||||
const transaction = await db.sequelize.transaction();
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentUser.id === id) {
|
|
||||||
throw new ValidationError('iam.errors.deletingHimself');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentUser.app_role?.name !== config.roles.admin && currentUser.app_role?.name !== config.roles.super_admin) {
|
|
||||||
throw new ValidationError('errors.forbidden.message');
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await UsersDBApi.findBy({ id }, { transaction });
|
const user = await UsersDBApi.findBy({ id }, { transaction });
|
||||||
if (!user) {
|
|
||||||
throw new ValidationError('iam.errors.userNotFound');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentUser.app_role?.globalAccess && HIGH_TRUST_ROLE_NAMES.includes(user?.app_role?.name)) {
|
await assertManageableExistingUser(user, currentUser);
|
||||||
throw new ValidationError('errors.forbidden.message');
|
|
||||||
}
|
|
||||||
|
|
||||||
await UsersDBApi.remove(id, {
|
await UsersDBApi.remove(id, {
|
||||||
currentUser,
|
currentUser,
|
||||||
@ -223,4 +246,35 @@ module.exports = class UsersService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async deleteByIds(ids, currentUser) {
|
||||||
|
const transaction = await db.sequelize.transaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const normalizedIds = Array.from(new Set((Array.isArray(ids) ? ids : []).filter(Boolean)));
|
||||||
|
|
||||||
|
if (!normalizedIds.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await Promise.all(
|
||||||
|
normalizedIds.map((id) => UsersDBApi.findBy({ id }, { transaction })),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
await assertManageableExistingUser(user, currentUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedUsers = await UsersDBApi.deleteByIds(normalizedIds, {
|
||||||
|
currentUser,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
return deletedUsers;
|
||||||
|
} catch (error) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,10 +34,11 @@ const CardOrganizations = ({
|
|||||||
|
|
||||||
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
const currentUser = useAppSelector((state) => state.auth.currentUser)
|
||||||
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ORGANIZATIONS')
|
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_ORGANIZATIONS')
|
||||||
|
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS')
|
||||||
const [linkedTenantSummaries, setLinkedTenantSummaries] = useState<OrganizationTenantSummaryMap>({})
|
const [linkedTenantSummaries, setLinkedTenantSummaries] = useState<OrganizationTenantSummaryMap>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Array.isArray(organizations) || !organizations.length) {
|
if (!Array.isArray(organizations) || !organizations.length || !canViewTenants) {
|
||||||
setLinkedTenantSummaries({})
|
setLinkedTenantSummaries({})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -60,7 +61,7 @@ const CardOrganizations = ({
|
|||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
}, [organizations])
|
}, [canViewTenants, organizations])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='p-4'>
|
<div className='p-4'>
|
||||||
@ -70,6 +71,11 @@ const CardOrganizations = ({
|
|||||||
organizations.map((item) => {
|
organizations.map((item) => {
|
||||||
const linkedSummary = linkedTenantSummaries[item.id]
|
const linkedSummary = linkedTenantSummaries[item.id]
|
||||||
const linkedCount = linkedSummary?.count || 0
|
const linkedCount = linkedSummary?.count || 0
|
||||||
|
const linkedTenantLabel = !canViewTenants
|
||||||
|
? 'Tenant access restricted'
|
||||||
|
: linkedCount
|
||||||
|
? `${linkedCount} linked tenant${linkedCount === 1 ? '' : 's'}`
|
||||||
|
: 'No tenant link'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@ -88,7 +94,7 @@ const CardOrganizations = ({
|
|||||||
Organization
|
Organization
|
||||||
</span>
|
</span>
|
||||||
<span className='rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-dark-700 dark:text-gray-100'>
|
<span className='rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-dark-700 dark:text-gray-100'>
|
||||||
{linkedCount ? `${linkedCount} linked tenant${linkedCount === 1 ? '' : 's'}` : 'No tenant link'}
|
{linkedTenantLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -117,7 +123,7 @@ const CardOrganizations = ({
|
|||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<LinkedTenantsPreview
|
<LinkedTenantsPreview
|
||||||
summary={linkedSummary}
|
summary={linkedSummary}
|
||||||
emptyMessage='This organization is not linked to a tenant yet.'
|
emptyMessage={canViewTenants ? 'This organization is not linked to a tenant yet.' : 'Tenant access restricted'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP
|
|||||||
const [linkedTenantSummaries, setLinkedTenantSummaries] = useState<OrganizationTenantSummaryMap>({})
|
const [linkedTenantSummaries, setLinkedTenantSummaries] = useState<OrganizationTenantSummaryMap>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Array.isArray(organizations) || !organizations.length) {
|
if (!Array.isArray(organizations) || !organizations.length || !canViewTenants) {
|
||||||
setLinkedTenantSummaries({})
|
setLinkedTenantSummaries({})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP
|
|||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
}, [organizations])
|
}, [canViewTenants, organizations])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -61,6 +61,11 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP
|
|||||||
organizations.map((item) => {
|
organizations.map((item) => {
|
||||||
const linkedSummary = linkedTenantSummaries[item.id]
|
const linkedSummary = linkedTenantSummaries[item.id]
|
||||||
const linkedCount = linkedSummary?.count || 0
|
const linkedCount = linkedSummary?.count || 0
|
||||||
|
const linkedTenantLabel = !canViewTenants
|
||||||
|
? 'Tenant access restricted'
|
||||||
|
: linkedCount
|
||||||
|
? `${linkedCount} linked tenant${linkedCount === 1 ? '' : 's'}`
|
||||||
|
: 'No tenant link'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
@ -76,7 +81,7 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP
|
|||||||
Organization
|
Organization
|
||||||
</span>
|
</span>
|
||||||
<span className='rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-dark-700 dark:text-gray-100'>
|
<span className='rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-dark-700 dark:text-gray-100'>
|
||||||
{linkedCount ? `${linkedCount} linked tenant${linkedCount === 1 ? '' : 's'}` : 'No tenant link'}
|
{linkedTenantLabel}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -91,7 +96,7 @@ const ListOrganizations = ({ organizations, loading, onDelete, currentPage, numP
|
|||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
<LinkedTenantsPreview
|
<LinkedTenantsPreview
|
||||||
summary={linkedSummary}
|
summary={linkedSummary}
|
||||||
emptyMessage='This organization is not linked to a tenant yet.'
|
emptyMessage={canViewTenants ? 'This organization is not linked to a tenant yet.' : 'Tenant access restricted'}
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -35,6 +35,7 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
|
|||||||
const [columns, setColumns] = useState<GridColDef[]>([]);
|
const [columns, setColumns] = useState<GridColDef[]>([]);
|
||||||
const [selectedRows, setSelectedRows] = useState([]);
|
const [selectedRows, setSelectedRows] = useState([]);
|
||||||
const [linkedTenantSummaries, setLinkedTenantSummaries] = useState({});
|
const [linkedTenantSummaries, setLinkedTenantSummaries] = useState({});
|
||||||
|
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS');
|
||||||
const [sortModel, setSortModel] = useState([
|
const [sortModel, setSortModel] = useState([
|
||||||
{
|
{
|
||||||
field: '',
|
field: '',
|
||||||
@ -175,7 +176,7 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const organizationRows = Array.isArray(organizations) ? organizations : [];
|
const organizationRows = Array.isArray(organizations) ? organizations : [];
|
||||||
|
|
||||||
if (!organizationRows.length) {
|
if (!organizationRows.length || !canViewTenants) {
|
||||||
setLinkedTenantSummaries({});
|
setLinkedTenantSummaries({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -198,7 +199,7 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
|
|||||||
return () => {
|
return () => {
|
||||||
isActive = false;
|
isActive = false;
|
||||||
};
|
};
|
||||||
}, [organizations]);
|
}, [canViewTenants, organizations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentUser) return;
|
if (!currentUser) return;
|
||||||
@ -208,8 +209,9 @@ const TableSampleOrganizations = ({ filterItems, setFilterItems, filters, showGr
|
|||||||
`organizations`,
|
`organizations`,
|
||||||
currentUser,
|
currentUser,
|
||||||
linkedTenantSummaries,
|
linkedTenantSummaries,
|
||||||
|
canViewTenants,
|
||||||
).then((newCols) => setColumns(newCols));
|
).then((newCols) => setColumns(newCols));
|
||||||
}, [currentUser, linkedTenantSummaries]);
|
}, [canViewTenants, currentUser, linkedTenantSummaries]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export const loadColumns = async (
|
|||||||
_entityName: string,
|
_entityName: string,
|
||||||
user,
|
user,
|
||||||
linkedTenantSummaries: OrganizationTenantSummaryMap = {},
|
linkedTenantSummaries: OrganizationTenantSummaryMap = {},
|
||||||
|
canViewTenants = true,
|
||||||
) => {
|
) => {
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_ORGANIZATIONS')
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_ORGANIZATIONS')
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ export const loadColumns = async (
|
|||||||
<LinkedTenantsPreview
|
<LinkedTenantsPreview
|
||||||
summary={linkedTenantSummaries[params?.row?.id]}
|
summary={linkedTenantSummaries[params?.row?.id]}
|
||||||
compact
|
compact
|
||||||
emptyMessage='Not linked yet'
|
emptyMessage={canViewTenants ? 'Not linked yet' : 'Tenant access restricted'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import LoadingSpinner from "../LoadingSpinner";
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
import { canManagePlatformUserFields, canManageUserRecord } from '../../helpers/manageableUsers';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -38,7 +39,7 @@ const CardUsers = ({
|
|||||||
|
|
||||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||||
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS')
|
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS')
|
||||||
|
const canManagePlatformFields = canManagePlatformUserFields(currentUser)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'p-4'}>
|
<div className={'p-4'}>
|
||||||
@ -47,7 +48,10 @@ const CardUsers = ({
|
|||||||
role='list'
|
role='list'
|
||||||
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'
|
||||||
>
|
>
|
||||||
{!loading && users.map((item, index) => (
|
{!loading && users.map((item) => {
|
||||||
|
const canManageUser = hasUpdatePermission && canManageUserRecord(currentUser, item)
|
||||||
|
|
||||||
|
return (
|
||||||
<li
|
<li
|
||||||
key={item.id}
|
key={item.id}
|
||||||
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
className={`overflow-hidden ${corners !== 'rounded-full'? corners : 'rounded-3xl'} border ${focusRing} border-gray-200 dark:border-dark-700 ${
|
||||||
@ -78,7 +82,7 @@ const CardUsers = ({
|
|||||||
pathEdit={`/users/users-edit/?id=${item.id}`}
|
pathEdit={`/users/users-edit/?id=${item.id}`}
|
||||||
pathView={`/users/users-view/?id=${item.id}`}
|
pathView={`/users/users-view/?id=${item.id}`}
|
||||||
|
|
||||||
hasUpdatePermission={hasUpdatePermission}
|
hasUpdatePermission={canManageUser}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -190,7 +194,7 @@ const CardUsers = ({
|
|||||||
<dt className=' text-gray-500 dark:text-dark-600'>Organizations</dt>
|
<dt className=' text-gray-500 dark:text-dark-600'>Organizations</dt>
|
||||||
<dd className='flex items-start gap-x-2'>
|
<dd className='flex items-start gap-x-2'>
|
||||||
<div className='font-medium line-clamp-4'>
|
<div className='font-medium line-clamp-4'>
|
||||||
{ dataFormatter.organizationsOneListFormatter(item.organizations) }
|
{ canManagePlatformFields ? dataFormatter.organizationsOneListFormatter(item.organizations) : 'Pinned to your workspace' }
|
||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@ -199,7 +203,8 @@ const CardUsers = ({
|
|||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
{!loading && users.length === 0 && (
|
{!loading && users.length === 0 && (
|
||||||
<div className='col-span-full flex items-center justify-center h-40'>
|
<div className='col-span-full flex items-center justify-center h-40'>
|
||||||
<p className=''>No data to display</p>
|
<p className=''>No data to display</p>
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import LoadingSpinner from "../LoadingSpinner";
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
import { canManagePlatformUserFields, canManageUserRecord } from '../../helpers/manageableUsers';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -25,7 +26,8 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
|
|||||||
|
|
||||||
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
const currentUser = useAppSelector((state) => state.auth.currentUser);
|
||||||
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS')
|
const hasUpdatePermission = hasPermission(currentUser, 'UPDATE_USERS')
|
||||||
|
const canManagePlatformFields = canManagePlatformUserFields(currentUser)
|
||||||
|
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
const corners = useAppSelector((state) => state.style.corners);
|
||||||
const bgColor = useAppSelector((state) => state.style.cardsColor);
|
const bgColor = useAppSelector((state) => state.style.cardsColor);
|
||||||
|
|
||||||
@ -34,7 +36,10 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
|
|||||||
<>
|
<>
|
||||||
<div className='relative overflow-x-auto p-4 space-y-4'>
|
<div className='relative overflow-x-auto p-4 space-y-4'>
|
||||||
{loading && <LoadingSpinner />}
|
{loading && <LoadingSpinner />}
|
||||||
{!loading && users.map((item) => (
|
{!loading && users.map((item) => {
|
||||||
|
const canManageUser = hasUpdatePermission && canManageUserRecord(currentUser, item)
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<CardBox hasTable isList className={'rounded shadow-none'}>
|
<CardBox hasTable isList className={'rounded shadow-none'}>
|
||||||
<div className={`flex ${bgColor} ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 border border-gray-600 items-center overflow-hidden`}>
|
<div className={`flex ${bgColor} ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 border border-gray-600 items-center overflow-hidden`}>
|
||||||
@ -116,7 +121,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
|
|||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>Custom Permissions</p>
|
<p className={'text-xs text-gray-500 '}>Custom Permissions</p>
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.permissionsManyListFormatter(item.custom_permissions).join(', ')}</p>
|
<p className={'line-clamp-2'}>{canManagePlatformFields ? dataFormatter.permissionsManyListFormatter(item.custom_permissions).join(', ') : '—'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -124,7 +129,7 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
|
|||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
<div className={'flex-1 px-3'}>
|
||||||
<p className={'text-xs text-gray-500 '}>Organizations</p>
|
<p className={'text-xs text-gray-500 '}>Organizations</p>
|
||||||
<p className={'line-clamp-2'}>{ dataFormatter.organizationsOneListFormatter(item.organizations) }</p>
|
<p className={'line-clamp-2'}>{canManagePlatformFields ? dataFormatter.organizationsOneListFormatter(item.organizations) : 'Pinned to your workspace'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -136,13 +141,14 @@ const ListUsers = ({ users, loading, onDelete, currentPage, numPages, onPageChan
|
|||||||
pathEdit={`/users/users-edit/?id=${item.id}`}
|
pathEdit={`/users/users-edit/?id=${item.id}`}
|
||||||
pathView={`/users/users-view/?id=${item.id}`}
|
pathView={`/users/users-view/?id=${item.id}`}
|
||||||
|
|
||||||
hasUpdatePermission={hasUpdatePermission}
|
hasUpdatePermission={canManageUser}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
{!loading && users.length === 0 && (
|
{!loading && users.length === 0 && (
|
||||||
<div className='col-span-full flex items-center justify-center h-40'>
|
<div className='col-span-full flex items-center justify-center h-40'>
|
||||||
<p className=''>No data to display</p>
|
<p className=''>No data to display</p>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {loadColumns} from "./configureUsersCols";
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import {dataGridStyles} from "../../styles";
|
import {dataGridStyles} from "../../styles";
|
||||||
|
import { canManageUserRecord } from '../../helpers/manageableUsers'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -197,8 +198,25 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteRows = async (selectedRows) => {
|
const onDeleteRows = async (selectedRows) => {
|
||||||
await dispatch(deleteItemsByIds(selectedRows));
|
const manageableRowIds = selectedRows.filter((selectedRowId) =>
|
||||||
|
canManageUserRecord(
|
||||||
|
currentUser,
|
||||||
|
users.find((user) => user.id === selectedRowId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!manageableRowIds.length) {
|
||||||
|
notify('warning', 'Only customer and concierge accounts in your workspace can be deleted here.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manageableRowIds.length !== selectedRows.length) {
|
||||||
|
notify('warning', 'Protected users were skipped from bulk delete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await dispatch(deleteItemsByIds(manageableRowIds));
|
||||||
await loadData(0);
|
await loadData(0);
|
||||||
|
setSelectedRows([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const controlClasses =
|
const controlClasses =
|
||||||
@ -207,6 +225,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
'dark:bg-slate-800 border';
|
'dark:bg-slate-800 border';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const dataGrid = (
|
const dataGrid = (
|
||||||
<div id="usersTable" className='relative overflow-x-auto'>
|
<div id="usersTable" className='relative overflow-x-auto'>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
@ -214,7 +233,7 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
rowHeight={64}
|
rowHeight={64}
|
||||||
sx={dataGridStyles}
|
sx={dataGridStyles}
|
||||||
className={'datagrid--table'}
|
className={'datagrid--table'}
|
||||||
getRowClassName={() => `datagrid--row`}
|
getRowClassName={(params) => `datagrid--row ${canManageUserRecord(currentUser, params.row) ? '' : 'opacity-60'}`}
|
||||||
rows={users ?? []}
|
rows={users ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
initialState={{
|
initialState={{
|
||||||
@ -225,10 +244,17 @@ const TableSampleUsers = ({ filterItems, setFilterItems, filters, showGrid }) =>
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
disableRowSelectionOnClick
|
disableRowSelectionOnClick
|
||||||
|
isRowSelectable={(params) => canManageUserRecord(currentUser, params.row)}
|
||||||
|
isCellEditable={(params) => Boolean(params.colDef.editable) && canManageUserRecord(currentUser, params.row)}
|
||||||
onProcessRowUpdateError={(params) => {
|
onProcessRowUpdateError={(params) => {
|
||||||
console.log('Error', params);
|
console.error('Users grid update error:', params);
|
||||||
}}
|
}}
|
||||||
processRowUpdate={async (newRow, oldRow) => {
|
processRowUpdate={async (newRow, oldRow) => {
|
||||||
|
if (!canManageUserRecord(currentUser, oldRow)) {
|
||||||
|
notify('warning', 'That account is protected and cannot be changed from this workspace.');
|
||||||
|
return oldRow;
|
||||||
|
}
|
||||||
|
|
||||||
const data = dataFormatter.dataGridEditFormatter(newRow);
|
const data = dataFormatter.dataGridEditFormatter(newRow);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,229 +1,181 @@
|
|||||||
import React from 'react';
|
import React from 'react'
|
||||||
import BaseIcon from '../BaseIcon';
|
import axios from 'axios'
|
||||||
import { mdiEye, mdiTrashCan, mdiPencilOutline } from '@mdi/js';
|
import { GridRowParams, GridValueGetterParams } from '@mui/x-data-grid'
|
||||||
import axios from 'axios';
|
import ImageField from '../ImageField'
|
||||||
import {
|
|
||||||
GridActionsCellItem,
|
|
||||||
GridRowParams,
|
|
||||||
GridValueGetterParams,
|
|
||||||
} from '@mui/x-data-grid';
|
|
||||||
import ImageField from '../ImageField';
|
|
||||||
import {saveFile} from "../../helpers/fileSaver";
|
|
||||||
import dataFormatter from '../../helpers/dataFormatter'
|
import dataFormatter from '../../helpers/dataFormatter'
|
||||||
import DataGridMultiSelect from "../DataGridMultiSelect";
|
import DataGridMultiSelect from '../DataGridMultiSelect'
|
||||||
import ListActionsPopover from '../ListActionsPopover';
|
import ListActionsPopover from '../ListActionsPopover'
|
||||||
|
import { hasPermission } from '../../helpers/userPermissions'
|
||||||
|
import { canManagePlatformUserFields, canManageUserRecord } from '../../helpers/manageableUsers'
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
type Params = (id: string) => void
|
||||||
|
|
||||||
type Params = (id: string) => void;
|
|
||||||
|
|
||||||
export const loadColumns = async (
|
export const loadColumns = async (
|
||||||
onDelete: Params,
|
onDelete: Params,
|
||||||
entityName: string,
|
entityName: string,
|
||||||
|
user,
|
||||||
user
|
|
||||||
|
|
||||||
) => {
|
) => {
|
||||||
async function callOptionsApi(entityName: string) {
|
const canManagePlatformFields = canManagePlatformUserFields(user)
|
||||||
|
|
||||||
if (!hasPermission(user, 'READ_' + entityName.toUpperCase())) return [];
|
async function callOptionsApi(targetEntityName: string) {
|
||||||
|
if (!hasPermission(user, `READ_${targetEntityName.toUpperCase()}`)) {
|
||||||
try {
|
return []
|
||||||
const data = await axios(`/${entityName}/autocomplete?limit=100`);
|
|
||||||
return data.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS')
|
const params = new URLSearchParams({ limit: '100' })
|
||||||
|
|
||||||
return [
|
if (targetEntityName === 'roles') {
|
||||||
|
params.set('assignableOnly', 'true')
|
||||||
{
|
params.set('includeHighTrust', canManagePlatformFields ? 'true' : 'false')
|
||||||
field: 'firstName',
|
}
|
||||||
headerName: 'First Name',
|
|
||||||
flex: 1,
|
try {
|
||||||
minWidth: 120,
|
const data = await axios(`/${targetEntityName}/autocomplete?${params.toString()}`)
|
||||||
filterable: false,
|
return data.data
|
||||||
headerClassName: 'datagrid--header',
|
} catch (error) {
|
||||||
cellClassName: 'datagrid--cell',
|
console.error(`Failed to load ${targetEntityName} options for users grid:`, error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
editable: hasUpdatePermission,
|
}
|
||||||
|
|
||||||
|
const hasUpdatePermission = hasPermission(user, 'UPDATE_USERS')
|
||||||
},
|
const canEditRecord = (record) => hasUpdatePermission && canManageUserRecord(user, record)
|
||||||
|
|
||||||
{
|
return [
|
||||||
field: 'lastName',
|
{
|
||||||
headerName: 'Last Name',
|
field: 'firstName',
|
||||||
flex: 1,
|
headerName: 'First Name',
|
||||||
minWidth: 120,
|
flex: 1,
|
||||||
filterable: false,
|
minWidth: 120,
|
||||||
headerClassName: 'datagrid--header',
|
filterable: false,
|
||||||
cellClassName: 'datagrid--cell',
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
|
editable: hasUpdatePermission,
|
||||||
editable: hasUpdatePermission,
|
},
|
||||||
|
{
|
||||||
|
field: 'lastName',
|
||||||
},
|
headerName: 'Last Name',
|
||||||
|
flex: 1,
|
||||||
{
|
minWidth: 120,
|
||||||
field: 'phoneNumber',
|
filterable: false,
|
||||||
headerName: 'Phone Number',
|
headerClassName: 'datagrid--header',
|
||||||
flex: 1,
|
cellClassName: 'datagrid--cell',
|
||||||
minWidth: 120,
|
editable: hasUpdatePermission,
|
||||||
filterable: false,
|
},
|
||||||
headerClassName: 'datagrid--header',
|
{
|
||||||
cellClassName: 'datagrid--cell',
|
field: 'phoneNumber',
|
||||||
|
headerName: 'Phone Number',
|
||||||
|
flex: 1,
|
||||||
editable: hasUpdatePermission,
|
minWidth: 120,
|
||||||
|
filterable: false,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
},
|
cellClassName: 'datagrid--cell',
|
||||||
|
editable: hasUpdatePermission,
|
||||||
{
|
},
|
||||||
field: 'email',
|
{
|
||||||
headerName: 'E-Mail',
|
field: 'email',
|
||||||
flex: 1,
|
headerName: 'E-Mail',
|
||||||
minWidth: 120,
|
flex: 1,
|
||||||
filterable: false,
|
minWidth: 120,
|
||||||
headerClassName: 'datagrid--header',
|
filterable: false,
|
||||||
cellClassName: 'datagrid--cell',
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
|
editable: hasUpdatePermission,
|
||||||
editable: hasUpdatePermission,
|
},
|
||||||
|
{
|
||||||
|
field: 'disabled',
|
||||||
},
|
headerName: 'Disabled',
|
||||||
|
flex: 1,
|
||||||
{
|
minWidth: 120,
|
||||||
field: 'disabled',
|
filterable: false,
|
||||||
headerName: 'Disabled',
|
headerClassName: 'datagrid--header',
|
||||||
flex: 1,
|
cellClassName: 'datagrid--cell',
|
||||||
minWidth: 120,
|
editable: hasUpdatePermission,
|
||||||
filterable: false,
|
type: 'boolean',
|
||||||
headerClassName: 'datagrid--header',
|
},
|
||||||
cellClassName: 'datagrid--cell',
|
{
|
||||||
|
field: 'avatar',
|
||||||
|
headerName: 'Avatar',
|
||||||
editable: hasUpdatePermission,
|
flex: 1,
|
||||||
|
minWidth: 120,
|
||||||
type: 'boolean',
|
filterable: false,
|
||||||
|
headerClassName: 'datagrid--header',
|
||||||
},
|
cellClassName: 'datagrid--cell',
|
||||||
|
editable: false,
|
||||||
{
|
sortable: false,
|
||||||
field: 'avatar',
|
renderCell: (params: GridValueGetterParams) => (
|
||||||
headerName: 'Avatar',
|
<ImageField
|
||||||
flex: 1,
|
name={'Avatar'}
|
||||||
minWidth: 120,
|
image={params?.row?.avatar}
|
||||||
filterable: false,
|
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
|
||||||
headerClassName: 'datagrid--header',
|
/>
|
||||||
cellClassName: 'datagrid--cell',
|
),
|
||||||
|
},
|
||||||
editable: false,
|
{
|
||||||
sortable: false,
|
field: 'app_role',
|
||||||
renderCell: (params: GridValueGetterParams) => (
|
headerName: 'App Role',
|
||||||
<ImageField
|
flex: 1,
|
||||||
name={'Avatar'}
|
minWidth: 120,
|
||||||
image={params?.row?.avatar}
|
filterable: false,
|
||||||
className='w-24 h-24 mx-auto lg:w-6 lg:h-6'
|
headerClassName: 'datagrid--header',
|
||||||
/>
|
cellClassName: 'datagrid--cell',
|
||||||
),
|
editable: hasUpdatePermission,
|
||||||
|
sortable: false,
|
||||||
},
|
type: 'singleSelect',
|
||||||
|
getOptionValue: (value: any) => value?.id,
|
||||||
{
|
getOptionLabel: (value: any) => value?.label,
|
||||||
field: 'app_role',
|
valueOptions: await callOptionsApi('roles'),
|
||||||
headerName: 'App Role',
|
valueGetter: (params: GridValueGetterParams) => params?.value?.id ?? params?.value,
|
||||||
flex: 1,
|
},
|
||||||
minWidth: 120,
|
{
|
||||||
filterable: false,
|
field: 'custom_permissions',
|
||||||
headerClassName: 'datagrid--header',
|
headerName: 'Custom Permissions',
|
||||||
cellClassName: 'datagrid--cell',
|
flex: 1,
|
||||||
|
minWidth: 160,
|
||||||
|
filterable: false,
|
||||||
editable: hasUpdatePermission,
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
sortable: false,
|
editable: false,
|
||||||
type: 'singleSelect',
|
sortable: false,
|
||||||
getOptionValue: (value: any) => value?.id,
|
hide: !canManagePlatformFields,
|
||||||
getOptionLabel: (value: any) => value?.label,
|
type: 'singleSelect',
|
||||||
valueOptions: await callOptionsApi('roles'),
|
valueFormatter: ({ value }) => dataFormatter.permissionsManyListFormatter(value).join(', '),
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
renderEditCell: (params) => <DataGridMultiSelect {...params} entityName={'permissions'} />,
|
||||||
params?.value?.id ?? params?.value,
|
},
|
||||||
|
{
|
||||||
},
|
field: 'organizations',
|
||||||
|
headerName: 'Organizations',
|
||||||
{
|
flex: 1,
|
||||||
field: 'custom_permissions',
|
minWidth: 160,
|
||||||
headerName: 'Custom Permissions',
|
filterable: false,
|
||||||
flex: 1,
|
headerClassName: 'datagrid--header',
|
||||||
minWidth: 120,
|
cellClassName: 'datagrid--cell',
|
||||||
filterable: false,
|
editable: canManagePlatformFields && hasUpdatePermission,
|
||||||
headerClassName: 'datagrid--header',
|
sortable: false,
|
||||||
cellClassName: 'datagrid--cell',
|
hide: !canManagePlatformFields,
|
||||||
|
type: 'singleSelect',
|
||||||
editable: false,
|
getOptionValue: (value: any) => value?.id,
|
||||||
sortable: false,
|
getOptionLabel: (value: any) => value?.label,
|
||||||
type: 'singleSelect',
|
valueOptions: canManagePlatformFields ? await callOptionsApi('organizations') : [],
|
||||||
valueFormatter: ({ value }) =>
|
valueGetter: (params: GridValueGetterParams) => params?.value?.id ?? params?.value,
|
||||||
dataFormatter.permissionsManyListFormatter(value).join(', '),
|
},
|
||||||
renderEditCell: (params) => (
|
{
|
||||||
<DataGridMultiSelect {...params} entityName={'permissions'}/>
|
field: 'actions',
|
||||||
),
|
type: 'actions',
|
||||||
|
minWidth: 30,
|
||||||
},
|
headerClassName: 'datagrid--header',
|
||||||
|
cellClassName: 'datagrid--cell',
|
||||||
{
|
getActions: (params: GridRowParams) => [
|
||||||
field: 'organizations',
|
<div key={params?.row?.id}>
|
||||||
headerName: 'Organizations',
|
<ListActionsPopover
|
||||||
flex: 1,
|
onDelete={onDelete}
|
||||||
minWidth: 120,
|
itemId={params?.row?.id}
|
||||||
filterable: false,
|
pathEdit={`/${entityName}/${entityName}-edit/?id=${params?.row?.id}`}
|
||||||
headerClassName: 'datagrid--header',
|
pathView={`/${entityName}/${entityName}-view/?id=${params?.row?.id}`}
|
||||||
cellClassName: 'datagrid--cell',
|
hasUpdatePermission={canEditRecord(params?.row)}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
editable: hasUpdatePermission,
|
],
|
||||||
|
},
|
||||||
sortable: false,
|
]
|
||||||
type: 'singleSelect',
|
}
|
||||||
getOptionValue: (value: any) => value?.id,
|
|
||||||
getOptionLabel: (value: any) => value?.label,
|
|
||||||
valueOptions: await callOptionsApi('organizations'),
|
|
||||||
valueGetter: (params: GridValueGetterParams) =>
|
|
||||||
params?.value?.id ?? params?.value,
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'actions',
|
|
||||||
type: 'actions',
|
|
||||||
minWidth: 30,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
getActions: (params: GridRowParams) => {
|
|
||||||
|
|
||||||
return [
|
|
||||||
<div key={params?.row?.id}>
|
|
||||||
<ListActionsPopover
|
|
||||||
onDelete={onDelete}
|
|
||||||
itemId={params?.row?.id}
|
|
||||||
pathEdit={`/users/users-edit/?id=${params?.row?.id}`}
|
|
||||||
pathView={`/users/users-view/?id=${params?.row?.id}`}
|
|
||||||
|
|
||||||
hasUpdatePermission={hasUpdatePermission}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</div>,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|||||||
33
frontend/src/helpers/manageableUsers.ts
Normal file
33
frontend/src/helpers/manageableUsers.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { getRoleLaneFromUser } from './roleLanes'
|
||||||
|
|
||||||
|
const HIGH_TRUST_ROLE_NAMES = new Set(['Super Administrator', 'Administrator'])
|
||||||
|
|
||||||
|
export const canManagePlatformUserFields = (currentUser?: any) =>
|
||||||
|
getRoleLaneFromUser(currentUser) === 'super_admin'
|
||||||
|
|
||||||
|
export const canManageUserRecord = (currentUser?: any, userRecord?: any) => {
|
||||||
|
if (!currentUser?.id || !userRecord?.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLane = getRoleLaneFromUser(currentUser)
|
||||||
|
|
||||||
|
if (currentLane === 'super_admin') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLane !== 'admin') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser.id === userRecord.id) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !HIGH_TRUST_ROLE_NAMES.has(userRecord?.app_role?.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserManagementMessage = (currentUser?: any) =>
|
||||||
|
canManagePlatformUserFields(currentUser)
|
||||||
|
? 'You can manage every user account, including platform-level roles and organization assignment.'
|
||||||
|
: 'You can invite and manage concierge/customer users in your own workspace. Administrator accounts remain protected.'
|
||||||
@ -38,6 +38,55 @@ const normalizeTenantRows = (rows: any): LinkedTenantRecord[] => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeOrganizationId = (organizationId: any): string => {
|
||||||
|
if (typeof organizationId === 'string') {
|
||||||
|
return organizationId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(organizationId)) {
|
||||||
|
return typeof organizationId[0] === 'string' ? organizationId[0] : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationId && typeof organizationId === 'object') {
|
||||||
|
return typeof organizationId.id === 'string' ? organizationId.id : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTenantLookupErrorMessage = (error: any): string => {
|
||||||
|
const responseData = error?.response?.data
|
||||||
|
|
||||||
|
if (typeof responseData === 'string') {
|
||||||
|
return responseData.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseData && typeof responseData === 'object') {
|
||||||
|
const candidateMessages = [
|
||||||
|
responseData?.message,
|
||||||
|
responseData?.error,
|
||||||
|
responseData?.errors?.message,
|
||||||
|
].filter((value) => typeof value === 'string')
|
||||||
|
|
||||||
|
if (candidateMessages.length) {
|
||||||
|
return candidateMessages.join(' ').toLowerCase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error?.message === 'string') {
|
||||||
|
return error.message.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const isForbiddenTenantLookup = (error: any) => {
|
||||||
|
const status = error?.response?.status
|
||||||
|
const message = getTenantLookupErrorMessage(error)
|
||||||
|
|
||||||
|
return (status === 400 || status === 403) && (message.includes('forbidden') || message.includes('not authorized'))
|
||||||
|
}
|
||||||
|
|
||||||
export const getTenantSetupHref = (organizationId?: string, organizationName?: string) => {
|
export const getTenantSetupHref = (organizationId?: string, organizationName?: string) => {
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return '/tenants/tenants-new'
|
return '/tenants/tenants-new'
|
||||||
@ -147,32 +196,43 @@ export const mergeEntityOptions = (existingOptions: any[] = [], nextOptions: any
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const loadLinkedTenantSummary = async (organizationId: string): Promise<OrganizationTenantSummary> => {
|
export const loadLinkedTenantSummary = async (organizationId: string): Promise<OrganizationTenantSummary> => {
|
||||||
if (!organizationId) {
|
const normalizedOrganizationId = normalizeOrganizationId(organizationId)
|
||||||
|
|
||||||
|
if (!normalizedOrganizationId) {
|
||||||
return emptyOrganizationTenantSummary
|
return emptyOrganizationTenantSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data } = await axios.get('/tenants', {
|
try {
|
||||||
params: {
|
const { data } = await axios.get('/tenants', {
|
||||||
organizations: organizationId,
|
params: {
|
||||||
limit: 5,
|
organizations: normalizedOrganizationId,
|
||||||
page: 0,
|
limit: 5,
|
||||||
sort: 'asc',
|
page: 0,
|
||||||
field: 'name',
|
sort: 'asc',
|
||||||
},
|
field: 'name',
|
||||||
})
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const normalizedRows = normalizeTenantRows(data?.rows)
|
const normalizedRows = normalizeTenantRows(data?.rows)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows: normalizedRows,
|
rows: normalizedRows,
|
||||||
count: typeof data?.count === 'number' ? data.count : normalizedRows.length,
|
count: typeof data?.count === 'number' ? data.count : normalizedRows.length,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isForbiddenTenantLookup(error)) {
|
||||||
|
console.warn(`Skipping linked tenant summary for organization ${normalizedOrganizationId} because tenant access is not available.`)
|
||||||
|
return emptyOrganizationTenantSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadLinkedTenantSummaries = async (
|
export const loadLinkedTenantSummaries = async (
|
||||||
organizationIds: string[],
|
organizationIds: string[],
|
||||||
): Promise<OrganizationTenantSummaryMap> => {
|
): Promise<OrganizationTenantSummaryMap> => {
|
||||||
const uniqueOrganizationIds = Array.from(new Set(organizationIds.filter(Boolean)))
|
const uniqueOrganizationIds = Array.from(new Set(organizationIds.map((organizationId) => normalizeOrganizationId(organizationId)).filter(Boolean)))
|
||||||
|
|
||||||
if (!uniqueOrganizationIds.length) {
|
if (!uniqueOrganizationIds.length) {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ const EditOrganizationsPage = () => {
|
|||||||
}, [organizations])
|
}, [organizations])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!organizationId) {
|
if (!organizationId || !canReadTenants) {
|
||||||
setLinkedTenantSummary(emptyOrganizationTenantSummary)
|
setLinkedTenantSummary(emptyOrganizationTenantSummary)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ const EditOrganizationsPage = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
}, [organizationId])
|
}, [canReadTenants, organizationId])
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data) => {
|
||||||
const resultAction = await dispatch(update({ id: organizationId, data }))
|
const resultAction = await dispatch(update({ id: organizationId, data }))
|
||||||
|
|||||||
@ -47,7 +47,7 @@ const OrganizationsView = () => {
|
|||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id || typeof id !== 'string') {
|
if (!id || typeof id !== 'string' || !canReadTenants) {
|
||||||
setLinkedTenantSummary(emptyOrganizationTenantSummary)
|
setLinkedTenantSummary(emptyOrganizationTenantSummary)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ const OrganizationsView = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
isActive = false
|
isActive = false
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [canReadTenants, id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { SelectFieldMany } from '../../components/SelectFieldMany'
|
|||||||
import { SwitchField } from '../../components/SwitchField'
|
import { SwitchField } from '../../components/SwitchField'
|
||||||
import { getPageTitle } from '../../config'
|
import { getPageTitle } from '../../config'
|
||||||
import { getRoleLaneFromUser } from '../../helpers/roleLanes'
|
import { getRoleLaneFromUser } from '../../helpers/roleLanes'
|
||||||
|
import { canManageUserRecord } from '../../helpers/manageableUsers'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import { fetch, update } from '../../stores/users/usersSlice'
|
import { fetch, update } from '../../stores/users/usersSlice'
|
||||||
@ -77,6 +78,7 @@ const EditUsersPage = () => {
|
|||||||
assignableOnly: true,
|
assignableOnly: true,
|
||||||
includeHighTrust: canManagePlatformAccess,
|
includeHighTrust: canManagePlatformAccess,
|
||||||
}
|
}
|
||||||
|
const canEditLoadedUser = !users?.id || canManagePlatformAccess || canManageUserRecord(currentUser, users)
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async (values) => {
|
||||||
const payload = { ...values }
|
const payload = { ...values }
|
||||||
@ -106,6 +108,11 @@ const EditUsersPage = () => {
|
|||||||
{introCopy}
|
{introCopy}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!canEditLoadedUser ? (
|
||||||
|
<div className='rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-sm text-amber-900 dark:border-amber-900/40 dark:bg-amber-950/20 dark:text-amber-100'>
|
||||||
|
This account is protected. Customer administrators can view it, but only a super administrator can edit administrator-level users.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Formik enableReinitialize initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik enableReinitialize initialValues={initialValues} onSubmit={handleSubmit}>
|
||||||
<Form>
|
<Form>
|
||||||
<div className='grid gap-5 md:grid-cols-2'>
|
<div className='grid gap-5 md:grid-cols-2'>
|
||||||
@ -187,6 +194,7 @@ const EditUsersPage = () => {
|
|||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
|
)}
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,93 +1,84 @@
|
|||||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash'
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useMemo, useState } from 'react'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import BaseButton from '../../components/BaseButton'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import CardBoxModal from '../../components/CardBoxModal'
|
||||||
|
import DragDropFilePicker from '../../components/DragDropFilePicker'
|
||||||
|
import TableUsers from '../../components/Users/TableUsers'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
|
||||||
import { getPageTitle } from '../../config'
|
import { getPageTitle } from '../../config'
|
||||||
import TableUsers from '../../components/Users/TableUsers'
|
import { canManagePlatformUserFields, getUserManagementMessage } from '../../helpers/manageableUsers'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import { hasPermission } from '../../helpers/userPermissions'
|
||||||
import axios from "axios";
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import Link from "next/link";
|
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
import { setRefetch, uploadCsv } from '../../stores/users/usersSlice'
|
||||||
import CardBoxModal from "../../components/CardBoxModal";
|
|
||||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
|
||||||
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
|
|
||||||
|
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const UsersTablesPage = () => {
|
const UsersTablesPage = () => {
|
||||||
const [filterItems, setFilterItems] = useState([]);
|
const [filterItems, setFilterItems] = useState([])
|
||||||
const [csvFile, setCsvFile] = useState<File | null>(null);
|
const [csvFile, setCsvFile] = useState<File | null>(null)
|
||||||
const [isModalActive, setIsModalActive] = useState(false);
|
const [isModalActive, setIsModalActive] = useState(false)
|
||||||
const [showTableView, setShowTableView] = useState(false);
|
|
||||||
|
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth)
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const dispatch = useAppDispatch()
|
||||||
|
const canManagePlatformAccess = canManagePlatformUserFields(currentUser)
|
||||||
|
const teamManagementMessage = getUserManagementMessage(currentUser)
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const filters = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: 'First Name', title: 'firstName' },
|
||||||
|
{ label: 'Last Name', title: 'lastName' },
|
||||||
|
{ label: 'Phone Number', title: 'phoneNumber' },
|
||||||
|
{ label: 'E-Mail', title: 'email' },
|
||||||
|
{ label: 'App Role', title: 'app_role' },
|
||||||
|
...(canManagePlatformAccess ? [{ label: 'Custom Permissions', title: 'custom_permissions' }] : []),
|
||||||
|
],
|
||||||
|
[canManagePlatformAccess],
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS')
|
||||||
|
|
||||||
const [filters] = useState([{label: 'First Name', title: 'firstName'},{label: 'Last Name', title: 'lastName'},{label: 'Phone Number', title: 'phoneNumber'},{label: 'E-Mail', title: 'email'},
|
const addFilter = () => {
|
||||||
|
const newItem = {
|
||||||
|
id: uniqueId(),
|
||||||
|
fields: {
|
||||||
|
filterValue: '',
|
||||||
|
filterValueFrom: '',
|
||||||
{label: 'App Role', title: 'app_role'},
|
filterValueTo: '',
|
||||||
|
selectedField: filters[0].title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
{label: 'Custom Permissions', title: 'custom_permissions'},
|
|
||||||
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
|
|
||||||
|
|
||||||
|
|
||||||
const addFilter = () => {
|
setFilterItems([...filterItems, newItem])
|
||||||
const newItem = {
|
}
|
||||||
id: uniqueId(),
|
|
||||||
fields: {
|
|
||||||
filterValue: '',
|
|
||||||
filterValueFrom: '',
|
|
||||||
filterValueTo: '',
|
|
||||||
selectedField: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
newItem.fields.selectedField = filters[0].title;
|
|
||||||
setFilterItems([...filterItems, newItem]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getUsersCSV = async () => {
|
const getUsersCSV = async () => {
|
||||||
const response = await axios({url: '/users?filetype=csv', method: 'GET',responseType: 'blob'});
|
const response = await axios({ url: '/users?filetype=csv', method: 'GET', responseType: 'blob' })
|
||||||
const type = response.headers['content-type']
|
const type = response.headers['content-type']
|
||||||
const blob = new Blob([response.data], { type: type })
|
const blob = new Blob([response.data], { type })
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = window.URL.createObjectURL(blob)
|
link.href = window.URL.createObjectURL(blob)
|
||||||
link.download = 'usersCSV.csv'
|
link.download = 'usersCSV.csv'
|
||||||
link.click()
|
link.click()
|
||||||
};
|
}
|
||||||
|
|
||||||
const onModalConfirm = async () => {
|
const onModalConfirm = async () => {
|
||||||
if (!csvFile) return;
|
if (!csvFile) return
|
||||||
await dispatch(uploadCsv(csvFile));
|
|
||||||
dispatch(setRefetch(true));
|
|
||||||
setCsvFile(null);
|
|
||||||
setIsModalActive(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModalCancel = () => {
|
await dispatch(uploadCsv(csvFile))
|
||||||
setCsvFile(null);
|
dispatch(setRefetch(true))
|
||||||
setIsModalActive(false);
|
setCsvFile(null)
|
||||||
};
|
setIsModalActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onModalCancel = () => {
|
||||||
|
setCsvFile(null)
|
||||||
|
setIsModalActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -95,74 +86,38 @@ const UsersTablesPage = () => {
|
|||||||
<title>{getPageTitle('Users')}</title>
|
<title>{getPageTitle('Users')}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title='Users' main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox id="usersList" className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<CardBox className='mb-4 rounded-2xl border border-blue-100 bg-blue-50/70 text-sm text-blue-900 dark:border-blue-900/40 dark:bg-blue-950/20 dark:text-blue-100'>
|
||||||
|
{teamManagementMessage}
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>}
|
</CardBox>
|
||||||
|
<CardBox id='usersList' className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||||
<BaseButton
|
{hasCreatePermission && <BaseButton className='mr-3' href='/users/users-new' color='info' label='Add/Invite User' />}
|
||||||
className={'mr-3'}
|
|
||||||
color='info'
|
<BaseButton className='mr-3' color='info' label='Filter' onClick={addFilter} />
|
||||||
label='Filter'
|
<BaseButton className='mr-3' color='info' label='Download CSV' onClick={getUsersCSV} />
|
||||||
onClick={addFilter}
|
|
||||||
/>
|
{hasCreatePermission && <BaseButton color='info' label='Upload CSV' onClick={() => setIsModalActive(true)} />}
|
||||||
<BaseButton className={'mr-3'} color='info' label='Download CSV' onClick={getUsersCSV} />
|
|
||||||
|
|
||||||
{hasCreatePermission && (
|
|
||||||
<BaseButton
|
|
||||||
color='info'
|
|
||||||
label='Upload CSV'
|
|
||||||
onClick={() => setIsModalActive(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='md:inline-flex items-center ms-auto'>
|
<div className='md:inline-flex items-center ms-auto'>
|
||||||
<div id='delete-rows-button'></div>
|
<div id='delete-rows-button'></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
<CardBox className="mb-6" hasTable>
|
<CardBox className='mb-6' hasTable>
|
||||||
<TableUsers
|
<TableUsers filterItems={filterItems} setFilterItems={setFilterItems} filters={filters} showGrid={false} />
|
||||||
filterItems={filterItems}
|
|
||||||
setFilterItems={setFilterItems}
|
|
||||||
filters={filters}
|
|
||||||
showGrid={false}
|
|
||||||
/>
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
<CardBoxModal
|
<CardBoxModal title='Upload CSV' buttonColor='info' buttonLabel='Confirm' isActive={isModalActive} onConfirm={onModalConfirm} onCancel={onModalCancel}>
|
||||||
title='Upload CSV'
|
<DragDropFilePicker file={csvFile} setFile={setCsvFile} formats={'.csv'} />
|
||||||
buttonColor='info'
|
|
||||||
buttonLabel={'Confirm'}
|
|
||||||
// buttonLabel={false ? 'Deleting...' : 'Confirm'}
|
|
||||||
isActive={isModalActive}
|
|
||||||
onConfirm={onModalConfirm}
|
|
||||||
onCancel={onModalCancel}
|
|
||||||
>
|
|
||||||
<DragDropFilePicker
|
|
||||||
file={csvFile}
|
|
||||||
setFile={setCsvFile}
|
|
||||||
formats={'.csv'}
|
|
||||||
/>
|
|
||||||
</CardBoxModal>
|
</CardBoxModal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
UsersTablesPage.getLayout = function getLayout(page: ReactElement) {
|
UsersTablesPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return <LayoutAuthenticated permission={'READ_USERS'}>{page}</LayoutAuthenticated>
|
||||||
<LayoutAuthenticated
|
|
||||||
|
|
||||||
permission={'READ_USERS'}
|
|
||||||
|
|
||||||
>
|
|
||||||
{page}
|
|
||||||
</LayoutAuthenticated>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UsersTablesPage
|
export default UsersTablesPage
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { mdiChartTimelineVariant } from '@mdi/js'
|
import { mdiChartTimelineVariant } from '@mdi/js'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash'
|
||||||
import React, { ReactElement, useState } from 'react'
|
import React, { ReactElement, useMemo, useState } from 'react'
|
||||||
import CardBox from '../../components/CardBox'
|
import CardBox from '../../components/CardBox'
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||||
import SectionMain from '../../components/SectionMain'
|
import SectionMain from '../../components/SectionMain'
|
||||||
@ -10,13 +10,13 @@ import { getPageTitle } from '../../config'
|
|||||||
import TableUsers from '../../components/Users/TableUsers'
|
import TableUsers from '../../components/Users/TableUsers'
|
||||||
import BaseButton from '../../components/BaseButton'
|
import BaseButton from '../../components/BaseButton'
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Link from "next/link";
|
|
||||||
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
import {useAppDispatch, useAppSelector} from "../../stores/hooks";
|
||||||
import CardBoxModal from "../../components/CardBoxModal";
|
import CardBoxModal from "../../components/CardBoxModal";
|
||||||
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
import DragDropFilePicker from "../../components/DragDropFilePicker";
|
||||||
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
|
import {setRefetch, uploadCsv} from '../../stores/users/usersSlice';
|
||||||
|
|
||||||
|
|
||||||
|
import { canManagePlatformUserFields, getUserManagementMessage } from '../../helpers/manageableUsers'
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
|
||||||
|
|
||||||
@ -32,22 +32,17 @@ const UsersTablesPage = () => {
|
|||||||
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const canManagePlatformAccess = canManagePlatformUserFields(currentUser)
|
||||||
|
const teamManagementMessage = getUserManagementMessage(currentUser)
|
||||||
|
|
||||||
|
const filters = useMemo(() => ([
|
||||||
const [filters] = useState([{label: 'First Name', title: 'firstName'},{label: 'Last Name', title: 'lastName'},{label: 'Phone Number', title: 'phoneNumber'},{label: 'E-Mail', title: 'email'},
|
{ label: 'First Name', title: 'firstName' },
|
||||||
|
{ label: 'Last Name', title: 'lastName' },
|
||||||
|
{ label: 'Phone Number', title: 'phoneNumber' },
|
||||||
|
{ label: 'E-Mail', title: 'email' },
|
||||||
|
{ label: 'App Role', title: 'app_role' },
|
||||||
|
...(canManagePlatformAccess ? [{ label: 'Custom Permissions', title: 'custom_permissions' }] : []),
|
||||||
{label: 'App Role', title: 'app_role'},
|
]), [canManagePlatformAccess]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{label: 'Custom Permissions', title: 'custom_permissions'},
|
|
||||||
|
|
||||||
]);
|
|
||||||
|
|
||||||
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
|
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_USERS');
|
||||||
|
|
||||||
@ -98,6 +93,9 @@ const UsersTablesPage = () => {
|
|||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Users" main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
<CardBox className='mb-4 rounded-2xl border border-blue-100 bg-blue-50/70 text-sm text-blue-900 dark:border-blue-900/40 dark:bg-blue-950/20 dark:text-blue-100'>
|
||||||
|
{teamManagementMessage}
|
||||||
|
</CardBox>
|
||||||
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
<CardBox className='mb-6' cardBoxClassName='flex flex-wrap'>
|
||||||
|
|
||||||
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>}
|
{hasCreatePermission && <BaseButton className={'mr-3'} href={'/users/users-new'} color='info' label='Add/Invite User'/>}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {SwitchField} from "../../components/SwitchField";
|
|||||||
import FormField from "../../components/FormField";
|
import FormField from "../../components/FormField";
|
||||||
|
|
||||||
import {hasPermission} from "../../helpers/userPermissions";
|
import {hasPermission} from "../../helpers/userPermissions";
|
||||||
|
import { canManageUserRecord } from '../../helpers/manageableUsers'
|
||||||
|
|
||||||
|
|
||||||
const UsersView = () => {
|
const UsersView = () => {
|
||||||
@ -34,10 +35,11 @@ const UsersView = () => {
|
|||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
function removeLastCharacter(str) {
|
function removeLastCharacter(str) {
|
||||||
console.log(str,`str`)
|
|
||||||
return str.slice(0, -1);
|
return str.slice(0, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canEditViewedUser = hasPermission(currentUser, 'UPDATE_USERS') && canManageUserRecord(currentUser, users)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetch({ id }));
|
dispatch(fetch({ id }));
|
||||||
}, [dispatch, id]);
|
}, [dispatch, id]);
|
||||||
@ -50,13 +52,20 @@ const UsersView = () => {
|
|||||||
</Head>
|
</Head>
|
||||||
<SectionMain>
|
<SectionMain>
|
||||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View users')} main>
|
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title={removeLastCharacter('View users')} main>
|
||||||
|
{canEditViewedUser ? (
|
||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
label='Edit'
|
label='Edit'
|
||||||
href={`/users/users-edit/?id=${id}`}
|
href={`/users/users-edit/?id=${id}`}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
|
{!canEditViewedUser && hasPermission(currentUser, 'UPDATE_USERS') ? (
|
||||||
|
<div className='mb-6 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-4 text-sm text-amber-900 dark:border-amber-900/40 dark:bg-amber-950/20 dark:text-amber-100'>
|
||||||
|
This account is protected. Customer administrators can review it here, but only super administrators can edit or delete administrator-level users.
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user