Skip to content

Commit

Permalink
feat: allow SuperAdmins to view all users
Browse files Browse the repository at this point in the history
  • Loading branch information
radulescuandrew committed Jul 24, 2024
1 parent 13f440c commit e4b31b8
Show file tree
Hide file tree
Showing 21 changed files with 257 additions and 102 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddOrganizationAliasToUserApplicationsView1721741119685
implements MigrationInterface
{
name = 'AddOrganizationAliasToUserApplicationsView1721741119685';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'UserApplicationsView', 'public'],
);
await queryRunner.query(`DROP VIEW "UserApplicationsView"`);
await queryRunner.query(`CREATE VIEW "UserApplicationsView" AS
SELECT u.id,
u.name,
u.email,
u.phone,
u.status,
u.role,
u.organization_id AS "organizationId",
u.created_on AS "createdOn",
u.updated_on AS "updatedOn",
og.alias as "organizationAlias",
array_agg(DISTINCT a.id) AS "availableAppsIDs",
json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS "availableApps"
FROM "user" u
LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum
LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum
LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum
LEFT JOIN organization o ON u.organization_id = o.id
LEFT JOIN organization_general og ON o.organization_general_id = og.id
WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL
GROUP BY u.id, og.alias;
`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'UserApplicationsView',
"SELECT u.id,\n u.name,\n u.email,\n u.phone,\n u.status,\n u.role,\n u.organization_id AS \"organizationId\",\n u.created_on AS \"createdOn\",\n u.updated_on AS \"updatedOn\",\n og.alias as \"organizationAlias\",\n array_agg(DISTINCT a.id) AS \"availableAppsIDs\",\n json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS \"availableApps\"\n FROM \"user\" u\n LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum\n LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum\n LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum\n LEFT JOIN organization o ON u.organization_id = o.id\n LEFT JOIN organization_general og ON o.organization_general_id = og.id\n WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL\n GROUP BY u.id, og.alias;",
],
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'UserApplicationsView', 'public'],
);
await queryRunner.query(`DROP VIEW "UserApplicationsView"`);
await queryRunner.query(`CREATE VIEW "UserApplicationsView" AS SELECT u.id,
u.name,
u.email,
u.phone,
u.status,
u.role,
u.organization_id AS "organizationId",
u.created_on AS "createdOn",
u.updated_on AS "updatedOn",
og.alias,
array_agg(DISTINCT a.id) AS "availableAppsIDs",
json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS "availableApps"
FROM "user" u
LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum
LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum
LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum
LEFT JOIN organization o ON u.organization_id = o.id
LEFT JOIN organization_general og ON o.organization_general_id = og.id
WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL
GROUP BY u.id, og.alias;`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'UserApplicationsView',
"SELECT u.id,\n u.name,\n u.email,\n u.phone,\n u.status,\n u.role,\n u.organization_id AS \"organizationId\",\n u.created_on AS \"createdOn\",\n u.updated_on AS \"updatedOn\",\n og.alias,\n array_agg(DISTINCT a.id) AS \"availableAppsIDs\",\n json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS \"availableApps\"\n FROM \"user\" u\n LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum\n LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum\n LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum\n LEFT JOIN organization o ON u.organization_id = o.id\n LEFT JOIN organization_general og ON o.organization_general_id = og.id\n WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL\n GROUP BY u.id, og.alias;",
],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -325,14 +325,6 @@ export class OrganizationService {
paginationOptions,
);

for (let index in ongList.items) {
const data = await this.findWithRelations(ongList.items[index].id);
ongList.items[index] = {
...ongList.items[index],
...data,
};
}

// Map the logo url
const items =
await this.fileManagerService.mapLogoToEntity<OrganizationView>(
Expand Down
13 changes: 12 additions & 1 deletion backend/src/modules/user/constants/invites-filters.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@ export const INVITE_FILTERS_CONFIG = {
email: true,
phone: true,
createdOn: true,
role: true,
organization: {
id: true,
organizationGeneral: {
alias: true,
},
},
},
searchableColumns: ['name'],
defaultSortBy: 'name',
defaultOrderDirection: OrderDirection.ASC,
relations: {},
relations: {
organization: {
organizationGeneral: true,
},
},
rangeColumn: 'createdOn',
};
4 changes: 3 additions & 1 deletion backend/src/modules/user/constants/user-filters.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OrderDirection } from 'src/common/enums/order-direction.enum';

export const USER_FILTERS_CONFIG = {
export const USER_APPLICATIONS_FILTERS_CONFIG = {
selectColumns: {
id: true,
name: true,
Expand All @@ -10,6 +10,8 @@ export const USER_FILTERS_CONFIG = {
status: true,
availableApps: true,
availableAppsIDs: true,
organizationId: true,
organizationAlias: true,
},
searchableColumns: ['name', 'email'],
defaultSortBy: 'name',
Expand Down
47 changes: 24 additions & 23 deletions backend/src/modules/user/entities/user-applications-view.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,27 @@ import { Role } from '../enums/role.enum';

@ViewEntity('UserApplicationsView', {
expression: `
SELECT
u.id,
u.name,
u.email,
u.phone,
u.status,
u.role,
u.organization_id as "organizationId",
u.created_on as "createdOn",
u.updated_on as "updatedOn",
ARRAY_AGG(DISTINCT a.id) as "availableAppsIDs",
json_agg(json_build_object('id', a.id, 'name', a.name, 'type', a.type)) as "availableApps"
FROM
"user" u
LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'
LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'
LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent') AND a.status = 'active'
WHERE
"u"."role" = 'employee' AND
"u"."status" IN('active', 'restricted') AND
"u"."deleted_on" IS NULL
GROUP BY
u.id
SELECT u.id,
u.name,
u.email,
u.phone,
u.status,
u.role,
u.organization_id AS "organizationId",
u.created_on AS "createdOn",
u.updated_on AS "updatedOn",
og.alias as "organizationAlias",
array_agg(DISTINCT a.id) AS "availableAppsIDs",
json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS "availableApps"
FROM "user" u
LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum
LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum
LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum
LEFT JOIN organization o ON u.organization_id = o.id
LEFT JOIN organization_general og ON o.organization_general_id = og.id
WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL
GROUP BY u.id, og.alias;
`,
})
export class UserApplicationsView {
Expand Down Expand Up @@ -75,4 +73,7 @@ export class UserApplicationsView {

@ViewColumn()
organizationId: number;

@ViewColumn()
organizationAlias: string;
}
25 changes: 3 additions & 22 deletions backend/src/modules/user/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
Not,
UpdateResult,
} from 'typeorm';
import { USER_FILTERS_CONFIG } from '../constants/user-filters.config';
import { USER_APPLICATIONS_FILTERS_CONFIG } from '../constants/user-filters.config';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
import { UserFilterDto } from '../dto/user-filter.dto';
Expand Down Expand Up @@ -194,26 +194,7 @@ export class UserService {

// For Admin user we will sort by organizationId
return this.userApplicationsView.getManyPaginated(
USER_FILTERS_CONFIG,
organizationId
? { ...paginationOptions, organizationId }
: paginationOptions,
);
}

public async findAll(
options: UserFilterDto,
organizationId?: number,
): Promise<Pagination<User>> {
const paginationOptions: any = {
role: Role.EMPLOYEE,
status: `$in:${UserStatus.ACTIVE},${UserStatus.RESTRICTED}`,
...options,
};

// For Admin user we will sort by organizationId
return this.userRepository.getManyPaginated(
USER_FILTERS_CONFIG,
USER_APPLICATIONS_FILTERS_CONFIG,
organizationId
? { ...paginationOptions, organizationId }
: paginationOptions,
Expand Down Expand Up @@ -353,7 +334,7 @@ export class UserService {

// For Admin user we will sort by organizationId
const users = await this.userApplicationsView.getManyPaginated(
USER_FILTERS_CONFIG,
USER_APPLICATIONS_FILTERS_CONFIG,
organizationId
? { ...paginationOptions, organizationId }
: paginationOptions,
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/assets/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -843,13 +843,16 @@
"email": "E-mail",
"phone": "Phone",
"status": "General access",
"created": "Date added"
"created": "Date added",
"organizationAlias": "Organization"
},
"invites_header": {
"name": "Name",
"email": "E-mail",
"phone": "Phone",
"added_on": "Date added"
"added_on": "Date added",
"role": "Role",
"organizationAlias": "Organization"
},
"status": {
"active": "Activ",
Expand Down Expand Up @@ -1580,4 +1583,4 @@
"date_added": "Date of feedback"
}
}
}
}
9 changes: 6 additions & 3 deletions frontend/src/assets/locales/ro/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -846,13 +846,16 @@
"email": "E-mail",
"phone": "Telefon",
"status": "Acces general",
"created": "Data adăugării"
"created": "Data adăugării",
"organizationAlias": "Organizație"
},
"invites_header": {
"name": "Nume",
"email": "E-mail",
"phone": "Telefon",
"added_on": "Data adăugării"
"added_on": "Data adăugării",
"role": "Rol",
"organizationAlias": "Organizație"
},
"status": {
"active": "Activ",
Expand Down Expand Up @@ -1584,4 +1587,4 @@
"date_added": "Data acordării feedback-ului"
}
}
}
}
5 changes: 3 additions & 2 deletions frontend/src/common/router/Routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const ADMIN_ROUTES = [
export const SUPER_ADMIN_ROUTES = [
{ id: 0, name: translations.dashboard, href: '', icon: RectangleGroupIcon },
{ id: 1, name: translations.organizations, href: 'organizations', icon: CircleStackIcon },
{ id: 2, name: translations.requests, href: 'requests', icon: UserPlusIcon },
{ id: 3, name: translations.store, href: 'applications', icon: RectangleStackIcon },
{ id: 2, name: translations.users, href: 'users/list', icon: UserGroupIcon },
{ id: 3, name: translations.requests, href: 'requests', icon: UserPlusIcon },
{ id: 4, name: translations.store, href: 'applications', icon: RectangleStackIcon },
];
20 changes: 17 additions & 3 deletions frontend/src/pages/users/components/UserInvites/UserInvites.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { SortOrder, TableColumn } from 'react-data-table-component';
import DataTableFilters from '../../../../components/data-table-filters/DataTableFilters';
import DataTableComponent from '../../../../components/data-table/DataTableComponent';
Expand All @@ -10,13 +10,18 @@ import {
useResendInviteMutation,
} from '../../../../services/user/User.queries';
import { useUser } from '../../../../store/selectors';
import { UserInvitesTableHeaders } from './table-headers/UserInvitesTable.headers';
import {
UserInvitesTableHeaderSuperAdmin,
UserInvitesTableHeaders,
} from './table-headers/UserInvitesTable.headers';
import { useTranslation } from 'react-i18next';
import { ArrowUturnLeftIcon, TrashIcon } from '@heroicons/react/24/outline';
import { useErrorToast, useSuccessToast } from '../../../../common/hooks/useToast';
import ConfirmationModal from '../../../../components/confim-removal-modal/ConfirmationModal';
import { IInvite } from '../../interfaces/Invite.interface';
import { OrderDirection } from '../../../../common/enums/sort-direction.enum';
import { useAuthContext } from '../../../../contexts/AuthContext';
import { UserRole } from '../../enums/UserRole.enum';

const UserInvites = () => {
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = useState<boolean>(false);
Expand All @@ -28,6 +33,7 @@ const UserInvites = () => {
const [orderByColumn, setOrderByColumn] = useState<string>();
const [orderDirection, setOrderDirection] = useState<OrderDirection>();

const { role } = useAuthContext();
const { invites } = useUser();

const { t } = useTranslation(['user', 'common']);
Expand All @@ -53,6 +59,14 @@ const UserInvites = () => {
if (selectedInviteeForDeletion) setIsConfirmRemoveModalOpen(true);
}, [selectedInviteeForDeletion]);

const ListTableHeader = useMemo(() => {
if (role === UserRole.SUPER_ADMIN) {
return [...UserInvitesTableHeaderSuperAdmin, ...UserInvitesTableHeaders];
}

return UserInvitesTableHeaders;
}, [role]);

const buildUserActionColumn = (): TableColumn<IInvite> => {
const pendingUserMenuItems = [
{
Expand Down Expand Up @@ -164,7 +178,7 @@ const UserInvites = () => {
</button> */}
</div>
<DataTableComponent
columns={[...UserInvitesTableHeaders, buildUserActionColumn()]}
columns={[...ListTableHeader, buildUserActionColumn()]}
data={invites}
loading={isLoading}
sortServer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const translations = {
email: i18n.t('user:invites_header.email'),
phone: i18n.t('user:invites_header.phone'),
added_on: i18n.t('user:invites_header.added_on'),
organizationAlias: i18n.t('user:invites_header.organizationAlias'),
role: i18n.t('user:invites_header.role'),
};

export const UserInvitesTableHeaders: TableColumn<IInvite>[] = [
Expand Down Expand Up @@ -42,3 +44,24 @@ export const UserInvitesTableHeaders: TableColumn<IInvite>[] = [
selector: (row: IInvite) => formatDate(row?.createdOn),
},
];

export const UserInvitesTableHeaderSuperAdmin: TableColumn<IInvite>[] = [
{
id: 'organization.organizationGeneral.alias',
name: <DataTableNameHeader text={translations.organizationAlias} />,
sortable: true,
grow: 1,
wrap: false,
minWidth: '15rem',
selector: (row: IInvite) => row.organization.organizationGeneral.alias,
},
{
id: 'role',
name: <DataTableNameHeader text={translations.role} />,
sortable: true,
grow: 1,
wrap: false,
minWidth: '10rem',
selector: (row: IInvite) => row.role,
},
];
Loading

0 comments on commit e4b31b8

Please sign in to comment.