refactor permissions

This commit is contained in:
Simon Larsen
2022-08-15 11:49:05 +01:00
parent 0cab9a88cf
commit c4bf99101d
13 changed files with 206 additions and 217 deletions

View File

@@ -22,7 +22,7 @@ import Phone from '../Types/Phone';
import PositiveNumber from '../Types/PositiveNumber';
import Route from '../Types/API/Route';
import TableColumnType from '../Types/Database/TableColumnType';
import Permission from '../Types/Permission';
import Permission, { PermissionHelper, UserPermission, UserProjectAccessPermission } from '../Types/Permission';
import { ColumnAccessControl } from '../Types/Database/AccessControl/AccessControl';
import { getColumnAccessControlForAllColumns } from '../Types/Database/AccessControl/ColumnAccessControl';
@@ -376,4 +376,49 @@ export default class BaseModel extends BaseEntity {
public isUserModel(): boolean {
return false;
}
public hasReadPermissions(userProjectPermissions: UserProjectAccessPermission): boolean{
return Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
this.readRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
}
public hasDeletePermissions(userProjectPermissions: UserProjectAccessPermission): boolean{
return Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
this.deleteRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
}
public hasUpdatePermissions(userProjectPermissions: UserProjectAccessPermission): boolean{
return Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
this.updateRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
}
}

View File

@@ -1129,14 +1129,17 @@ class DatabaseService<TBaseModel extends BaseModel> {
}
private serializePopulate(onBeforeFind: FindBy<TBaseModel>): FindBy<TBaseModel> {
debugger;
for (const key in onBeforeFind.populate) {
if (typeof onBeforeFind.populate[key] === Typeof.Object) {
// for (const selectKey in onBeforeFind.populate[key]) {
// (onBeforeFind.select as any)[`${key}.${selectKey}`] = true;
// }
(onBeforeFind.select as any)[key] = (onBeforeFind.populate as any)[key];
(onBeforeFind.populate as any)[key] = true;
} else {
// if you want to populate the whole object, you only do the id because of security.
(onBeforeFind.select as any)[key] = {
_id: true
} as any;
(onBeforeFind.populate as any)[key] = true;
}
}

View File

@@ -14,6 +14,8 @@ export enum ButtonStyleType {
NORMAL,
DANGER,
DANGER_OUTLINE,
SUCCESS,
SUCCESS_OUTLINE
}
export enum ButtonSize {
@@ -114,6 +116,14 @@ const Button: FunctionComponent<ComponentProps> = ({
'btn-outline-secondary background-very-light-grey-on-hover';
}
if (buttonStyle === ButtonStyleType.SUCCESS) {
buttonStyleCssClass = 'btn-success';
}
if (buttonStyle === ButtonStyleType.SUCCESS_OUTLINE) {
buttonStyleCssClass = 'btn-outline-success';
}
return (
<button
style={style}

View File

@@ -19,9 +19,7 @@ import Fields from '../Forms/Types/Fields';
import SortOrder from 'Common/Types/Database/SortOrder';
import FieldType from '../Types/FieldType';
import Dictionary from 'Common/Types/Dictionary';
import ActionButtonSchema, {
ActionType,
} from '../Table/Types/ActionButtonSchema';
import ActionButtonSchema from '../Table/Types/ActionButtonSchema';
import ObjectID from 'Common/Types/ObjectID';
import ConfirmModal from '../Modal/ConfirmModal';
import Permission, {
@@ -41,14 +39,14 @@ import Populate from '../../Utils/ModelAPI/Populate';
import List from '../Table/List';
export interface ComponentProps<TBaseModel extends BaseModel> {
modelType: { new (): TBaseModel };
modelType: { new(): TBaseModel };
id: string;
onFetchInit?:
| undefined
| ((pageNumber: number, itemsOnPage: number) => void);
| undefined
| ((pageNumber: number, itemsOnPage: number) => void);
onFetchSuccess?:
| undefined
| ((data: Array<TBaseModel>, totalCount: number) => void);
| undefined
| ((data: Array<TBaseModel>, totalCount: number) => void);
cardProps?: CardComponentProps | undefined;
columns: Columns<TBaseModel>;
initialItemsOnPage?: number;
@@ -68,6 +66,10 @@ export interface ComponentProps<TBaseModel extends BaseModel> {
showAsList?: boolean | undefined;
singularName?: string | undefined;
pluralName?: string | undefined;
actionButtons?: Array<ActionButtonSchema> | undefined;
deleteButtonText?: string | undefined;
editButtonText?: string | undefined;
viewButtonText?: string | undefined;
}
enum ModalType {
@@ -119,7 +121,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
try {
setError(
((err as HTTPErrorResponse).data as JSONObject)[
'error'
'error'
] as string
);
} catch (e) {
@@ -151,8 +153,8 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
getSelect(),
sortBy
? {
[sortBy as any]: sortOrder,
}
[sortBy as any]: sortOrder,
}
: {},
getPopulate()
);
@@ -163,7 +165,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
try {
setError(
((err as HTTPErrorResponse).data as JSONObject)[
'error'
'error'
] as string
);
} catch (e) {
@@ -223,15 +225,15 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
PermissionUtil.getProjectPermissions();
const hasPermissionToCreate: boolean = Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
model.createRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
model.createRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
if (props.isCreateable && hasPermissionToCreate) {
@@ -374,7 +376,7 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
columns.push({
...column,
disableSort: column.disableSort || shouldDisableSort(key),
key: column.selectedProperty ? key+"."+column.selectedProperty: key,
key: column.selectedProperty ? key + "." + column.selectedProperty : key,
});
if (key) {
@@ -390,48 +392,10 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
const userProjectPermissions: UserProjectAccessPermission | null =
PermissionUtil.getProjectPermissions();
const hasPermissionToDelete: boolean = Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
model.deleteRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
const hasPermissionToUpdate: boolean = Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
model.updateRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
const hasPermissionToView: boolean = Boolean(
userProjectPermissions &&
userProjectPermissions.permissions &&
PermissionHelper.doesPermissionsIntersect(
model.readRecordPermissions,
userProjectPermissions.permissions.map(
(item: UserPermission) => {
return item.permission;
}
)
)
);
if (
(props.isDeleteable && hasPermissionToDelete) ||
(props.isEditable && hasPermissionToUpdate)
userProjectPermissions &&
((props.isDeleteable && model.hasDeletePermissions(userProjectPermissions)) ||
(props.isEditable && model.hasUpdatePermissions(userProjectPermissions)))
) {
columns.push({
title: 'Actions',
@@ -439,40 +403,80 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
});
}
const actionsSchema: Array<ActionButtonSchema> = [];
if (props.isViewable && hasPermissionToView) {
actionsSchema.push({
title: 'View',
buttonStyleType: ButtonStyleType.OUTLINE,
actionType: ActionType.View,
});
}
if (props.isEditable && hasPermissionToUpdate) {
actionsSchema.push({
title: 'Edit',
buttonStyleType: ButtonStyleType.NORMAL,
actionType: ActionType.Edit,
});
}
if (props.isDeleteable && hasPermissionToDelete) {
actionsSchema.push({
title: 'Delete',
icon: IconProp.Trash,
buttonStyleType: ButtonStyleType.DANGER_OUTLINE,
actionType: ActionType.Delete,
});
}
setActionButtonSchema(actionsSchema);
setActionSchema();
setHeaderButtons();
setColumns(columns);
}, []);
const setActionSchema: Function = () => {
const userProjectPermissions: UserProjectAccessPermission | null =
PermissionUtil.getProjectPermissions();
const actionsSchema: Array<ActionButtonSchema> = [];
// add actions buttons from props.
if (props.actionButtons) {
for (const moreSchema of props.actionButtons) {
actionsSchema.push(moreSchema);
}
}
if (userProjectPermissions) {
if (props.isViewable && model.hasReadPermissions(userProjectPermissions)) {
actionsSchema.push({
title: props.viewButtonText ||'View',
buttonStyleType: ButtonStyleType.OUTLINE,
onClick: (item: JSONObject) => {
if (!props.currentPageRoute) {
throw new BadDataException(
'Please populate curentPageRoute in ModelTable'
);
}
Navigation.navigate(
new Route(
props.currentPageRoute.toString()
).addRoute('/' + item['_id'])
);
}
});
}
if (props.isEditable && model.hasUpdatePermissions(userProjectPermissions)) {
actionsSchema.push({
title: props.editButtonText ||'Edit',
buttonStyleType: ButtonStyleType.NORMAL,
onClick: (item: JSONObject) => {
setModalType(ModalType.Edit);
setShowModal(true);
setCurrentEditableItem(item);
}
});
}
if (props.isDeleteable && model.hasDeletePermissions(userProjectPermissions)) {
actionsSchema.push({
title: props.deleteButtonText || 'Delete',
icon: IconProp.Trash,
buttonStyleType: ButtonStyleType.DANGER_OUTLINE,
onClick: (item: JSONObject) => {
setShowDeleteConfirmModal(true);
setCurrentDeleteableItem(item);
}
});
}
}
setActionButtonSchema(actionsSchema);
}
const getTable = (): ReactElement => {
return (<Table
onFilterChanged={(
@@ -537,31 +541,6 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
fetchItems();
}}
actionButtons={actionButtonSchema}
onActionEvent={(key: ActionType, item: JSONObject) => {
if (key === ActionType.Edit) {
setModalType(ModalType.Edit);
setShowModal(true);
setCurrentEditableItem(item);
}
if (key === ActionType.Delete) {
setShowDeleteConfirmModal(true);
setCurrentDeleteableItem(item);
}
if (key === ActionType.View) {
if (!props.currentPageRoute) {
throw new BadDataException(
'Please populate curentPageRoute in ModelTable'
);
}
Navigation.navigate(
new Route(
props.currentPageRoute.toString()
).addRoute('/' + item['_id'])
);
}
}}
/>)
}
@@ -629,48 +608,23 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
fetchItems();
}}
actionButtons={actionButtonSchema}
onActionEvent={(key: ActionType, item: JSONObject) => {
if (key === ActionType.Edit) {
setModalType(ModalType.Edit);
setShowModal(true);
setCurrentEditableItem(item);
}
if (key === ActionType.Delete) {
setShowDeleteConfirmModal(true);
setCurrentDeleteableItem(item);
}
if (key === ActionType.View) {
if (!props.currentPageRoute) {
throw new BadDataException(
'Please populate curentPageRoute in ModelTable'
);
}
Navigation.navigate(
new Route(
props.currentPageRoute.toString()
).addRoute('/' + item['_id'])
);
}
}}
/>)
}
const getCardComponent = (): ReactElement => {
if (props.showAsList) {
return <div>
{props.cardProps && <Card
{...props.cardProps}
cardBodyStyle={{ padding: '0px' }}
buttons={cardButtons}
>
{getList()}
</Card>}
{props.cardProps && <Card
{...props.cardProps}
cardBodyStyle={{ padding: '0px' }}
buttons={cardButtons}
>
{getList()}
</Card>}
{!props.cardProps && getList()}
</div>
{!props.cardProps && getList()}
</div>
}
@@ -689,17 +643,16 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
return (
<>
{getCardComponent()}
{getCardComponent()}
{showModel ? (
<ModelFormModal<TBaseModel>
title={
modalType === ModalType.Create
? `${props.createVerb || 'Create'} New ${
props.singularName || model.singularName
}`
? `${props.createVerb || 'Create'} New ${props.singularName || model.singularName
}`
: `Edit ${props.singularName || model.singularName}`
}
onClose={() => {
@@ -707,9 +660,8 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
}}
submitButtonText={
modalType === ModalType.Create
? `${props.createVerb || 'Create'} ${
props.singularName || model.singularName
}`
? `${props.createVerb || 'Create'} ${props.singularName || model.singularName
}`
: `Save Changes`
}
onSuccess={(_item: TBaseModel) => {

View File

@@ -4,7 +4,7 @@ import Columns from './Types/Columns';
import Pagination from './Pagination';
import SortOrder from 'Common/Types/Database/SortOrder';
import Dictionary from 'Common/Types/Dictionary';
import ActionButtonSchema, { ActionType } from './Types/ActionButtonSchema';
import ActionButtonSchema from './Types/ActionButtonSchema';
import Search from 'Common/Types/Database/Search';
import ErrorMessage from './ErrorMessage';
import TableLoader from './Loader';
@@ -31,9 +31,6 @@ export interface ComponentProps {
onFilterChanged?:
| undefined
| ((filterData: Dictionary<string | boolean | Search | Date>) => void);
onActionEvent?:
| ((actionType: ActionType, item: JSONObject) => void)
| undefined;
}
const List: FunctionComponent<ComponentProps> = (
@@ -67,7 +64,6 @@ const List: FunctionComponent<ComponentProps> = (
data={props.data}
columns={props.columns}
actionButtons={props.actionButtons}
onActionEvent={props.onActionEvent}
/>
);
};

View File

@@ -1,7 +1,7 @@
import { JSONObject } from 'Common/Types/JSON';
import React, { FunctionComponent, ReactElement } from 'react';
import ListRow from './ListRow';
import ActionButtonSchema, { ActionType } from './Types/ActionButtonSchema';
import ActionButtonSchema from './Types/ActionButtonSchema';
import Columns from './Types/Columns';
export interface ComponentProps {
@@ -9,9 +9,6 @@ export interface ComponentProps {
id: string;
columns: Columns;
actionButtons?: undefined | Array<ActionButtonSchema> | undefined;
onActionEvent?:
| ((actionType: ActionType, item: JSONObject) => void)
| undefined;
}
const ListBody: FunctionComponent<ComponentProps> = (
@@ -27,7 +24,6 @@ const ListBody: FunctionComponent<ComponentProps> = (
item={item}
columns={props.columns}
actionButtons={props.actionButtons}
onActionEvent={props.onActionEvent}
/>
);
})}

View File

@@ -3,15 +3,12 @@ import React, { FunctionComponent, ReactElement, useEffect, useState } from 'rea
import Button, { ButtonSize } from '../Button/Button';
import Detail from '../Detail/Detail';
import Field from '../Detail/Field';
import ActionButtonSchema, { ActionType } from './Types/ActionButtonSchema';
import ActionButtonSchema from './Types/ActionButtonSchema';
import Columns from './Types/Columns';
export interface ComponentProps {
item: JSONObject;
columns: Columns;
onActionEvent?:
| ((actionType: ActionType, item: JSONObject) => void)
| undefined;
actionButtons?: Array<ActionButtonSchema> | undefined;
}
@@ -76,14 +73,9 @@ const ListRow: FunctionComponent<ComponentProps> = (
button.buttonStyleType
}
onClick={() => {
if (
props.onActionEvent
) {
props.onActionEvent(
button.actionType,
props.item
);
}
if (button.onClick) {
button.onClick(props.item);
}
}}
/>
</span>

View File

@@ -6,7 +6,7 @@ import Columns from './Types/Columns';
import Pagination from './Pagination';
import SortOrder from 'Common/Types/Database/SortOrder';
import Dictionary from 'Common/Types/Dictionary';
import ActionButtonSchema, { ActionType } from './Types/ActionButtonSchema';
import ActionButtonSchema from './Types/ActionButtonSchema';
import Search from 'Common/Types/Database/Search';
import ErrorMessage from './ErrorMessage';
import TableLoader from './Loader';
@@ -32,9 +32,6 @@ export interface ComponentProps {
onFilterChanged?:
| undefined
| ((filterData: Dictionary<string | boolean | Search | Date>) => void);
onActionEvent?:
| ((actionType: ActionType, item: JSONObject) => void)
| undefined;
}
const Table: FunctionComponent<ComponentProps> = (
@@ -85,7 +82,6 @@ const Table: FunctionComponent<ComponentProps> = (
data={props.data}
columns={props.columns}
actionButtons={props.actionButtons}
onActionEvent={props.onActionEvent}
/>
);
};

View File

@@ -1,7 +1,7 @@
import { JSONObject } from 'Common/Types/JSON';
import React, { FunctionComponent, ReactElement } from 'react';
import TableRow from './TableRow';
import ActionButtonSchema, { ActionType } from './Types/ActionButtonSchema';
import ActionButtonSchema from './Types/ActionButtonSchema';
import Columns from './Types/Columns';
export interface ComponentProps {
@@ -9,9 +9,6 @@ export interface ComponentProps {
id: string;
columns: Columns;
actionButtons?: undefined | Array<ActionButtonSchema> | undefined;
onActionEvent?:
| ((actionType: ActionType, item: JSONObject) => void)
| undefined;
}
const TableBody: FunctionComponent<ComponentProps> = (
@@ -27,7 +24,6 @@ const TableBody: FunctionComponent<ComponentProps> = (
item={item}
columns={props.columns}
actionButtons={props.actionButtons}
onActionEvent={props.onActionEvent}
/>
);
})}

View File

@@ -3,7 +3,7 @@ import { JSONObject } from 'Common/Types/JSON';
import React, { FunctionComponent, ReactElement } from 'react';
import Button, { ButtonSize } from '../Button/Button';
import Icon, { IconProp, ThickProp } from '../Icon/Icon';
import ActionButtonSchema, { ActionType } from './Types/ActionButtonSchema';
import ActionButtonSchema from './Types/ActionButtonSchema';
import Column from './Types/Column';
import Columns from './Types/Columns';
import FieldType from '../Types/FieldType';
@@ -12,9 +12,6 @@ import _ from 'lodash';
export interface ComponentProps {
item: JSONObject;
columns: Columns;
onActionEvent?:
| ((actionType: ActionType, item: JSONObject) => void)
| undefined;
actionButtons?: Array<ActionButtonSchema> | undefined;
}
@@ -99,13 +96,8 @@ const TableRow: FunctionComponent<ComponentProps> = (
button.buttonStyleType
}
onClick={() => {
if (
props.onActionEvent
) {
props.onActionEvent(
button.actionType,
props.item
);
if (button.onClick) {
button.onClick(props.item);
}
}}
/>

View File

@@ -1,17 +1,12 @@
import { JSONObject } from 'Common/Types/JSON';
import { ButtonStyleType } from '../../Button/Button';
import { IconProp } from '../../Icon/Icon';
export enum ActionType {
View,
Edit,
Delete,
}
interface ActionButtonSchema {
title: string;
icon?: undefined | IconProp;
buttonStyleType: ButtonStyleType;
actionType: ActionType;
onClick: ((item: JSONObject) => void);
}
export default ActionButtonSchema;

View File

@@ -17,6 +17,7 @@ import FullPageModal from "CommonUI/src/Components/FullPageModal/FullPageModal";
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
import { IconProp } from 'CommonUI/src/Components/Icon/Icon';
import FieldType from 'CommonUI/src/Components/Types/FieldType';
import { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
export interface ComponentProps {
selectedProject: Project | null;
@@ -99,10 +100,23 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
}
singularName="Project Invitation"
pluralName="Project Invitations"
actionButtons={[
{
title: "Accept",
buttonStyleType: ButtonStyleType.SUCCESS_OUTLINE,
icon: IconProp.Check,
onClick: () => {
}
}
]}
deleteButtonText="Reject"
columns={[
{
field: {
project: true,
project: {
name: true
},
},
title: 'Project Invited to',
type: FieldType.Text,
@@ -111,7 +125,9 @@ const DashboardHeader: FunctionComponent<ComponentProps> = (
},
{
field: {
team: true,
team: {
name: true
}
},
title: 'Team Invited to',
type: FieldType.Text,

View File

@@ -35,7 +35,7 @@
"uninstall": "docker stop $(docker ps -a -q) && docker rm $(docker ps -a -q)",
"delete-all-local-branches": "git branch | grep -v 'master' | xargs git branch -D",
"lint": "ejslint home/views/*.ejs && eslint '**/*.ts*' -c .eslintrc.json --ignore-path .eslintignore ",
"fix-lint": "eslint '**/*.ts*' -c .eslintrc.json --ignore-path .eslintignore --fix ",
"fix-lint": " node --max_old_space_size=8192 ./node_modules/eslint '**/*.ts*' -c .eslintrc.json --ignore-path .eslintignore --fix ",
"fix": "npm run fix-lint",
"build": "docker-compose --env-file ./docker-enterprise.env build",
"build-ci": "docker-compose --env-file ./docker-enterprise.env -f docker-compose.ci.yml build $npm_config_services",