[Feature] Added ability to update and delete app folders (#3132)

* Added migration to add forder_delete column

* Added new group permission

* Added deleteFolder ability

* Added delete folder api

* Added menu icon

* Added new defualt permissions of admin

* Implemented folder menu and delete action

* Implemented update folder name in frontend

* Added folder name update feature

* Refactoring code

* Added specs for update and delete apis
- Updated test-helper function with new permissions

* Resolved failing specs

* corrected method name & add count checking to delete spec

* added organizationId scope

* Changed toast and modal texts

* Resolved a mistake

* Added a check box for update permission

* Now, an user can only delete folders, if he has the permission to view all apps

* Edited update and delete spec cases

* Added error toasts

* Refactored code

* Resolved PR changes
- Changed permission name in the frontend
- Refactored the code

* capitalized all toasts
- Changed error message

* Fixed new user permission issue

* Update a spec

Co-authored-by: gsmithun4 <gsmithun4@gmail.com>
This commit is contained in:
Muhsin Shah C P 2022-06-16 19:41:38 +05:30 committed by GitHub
parent 8f7a1baf44
commit 2fdcfcc117
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 596 additions and 35 deletions

View file

@ -6,6 +6,7 @@ export const commonSelectors={
firstWidget:"[data-cy=widget-list]:eq(0)",
canvas:"[data-cy=real-canvas]",
appCardOptions: "[data-cy=app-card-menu-icon]",
folderItemOptions: "[data-cy=folder-item-menu-icon]",
deleteApp: "[data-cy=card-options] :nth-child(5)>span",
confirmButton: "[data-cy=confirm-yes-button]",
autoSave: "[data-cy=autosave-indicator]",

View file

@ -0,0 +1,54 @@
import React from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
export const FolderMenu = function FolderMenu({
deleteFolder,
editFolder,
canDeleteFolder,
canUpdateFolder,
onMenuOpen,
darkMode,
}) {
const closeMenu = () => {
document.body.click();
};
const Field = ({ text, onClick, customClass }) => {
return (
<div className={`field mb-3${customClass ? ` ${customClass}` : ''}`}>
<span
role="button"
onClick={() => {
closeMenu();
onClick();
}}
>
{text}
</span>
</div>
);
};
return (
<OverlayTrigger
trigger="click"
placement="bottom-end"
rootClose
onToggle={onMenuOpen}
overlay={
<Popover id="popover-app-menu" className={darkMode && 'popover-dark-themed'}>
<Popover.Content bsPrefix="popover-body">
<div data-cy="card-options">
{canUpdateFolder && <Field text="Edit folder" onClick={editFolder} />}
{canDeleteFolder && <Field text="Delete folder" customClass="field__danger" onClick={deleteFolder} />}
</div>
</Popover.Content>
</Popover>
}
>
<div className={`d-grid menu-ico menu-ico`}>
<img className="svg-icon" src="/assets/images/icons/three-dots.svg" data-cy="folder-item-menu-icon" />
</div>
</OverlayTrigger>
);
};

View file

@ -1,7 +1,10 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { folderService } from '@/_services';
import { toast } from 'react-hot-toast';
import Modal from './Modal';
import { FolderMenu } from './FolderMenu';
import useHover from '@/_hooks/useHover';
import { ConfirmDialog } from '@/_components';
export const Folders = function Folders({
folders,
@ -10,9 +13,27 @@ export const Folders = function Folders({
folderChanged,
foldersChanged,
canCreateFolder,
canUpdateFolder,
canDeleteFolder,
darkMode,
}) {
const [isLoading, setLoadingStatus] = useState(foldersLoading);
const [isMenuOpen, setMenuOpen] = useState(false);
const [hoverRef, isHovered] = useHover();
const [focused, setFocused] = useState(false);
const onMenuToggle = useCallback(
(status) => {
setMenuOpen(!!status);
!status && !isHovered && setFocused(false);
},
[isHovered]
);
useEffect(() => {
!isMenuOpen && setFocused(!!isHovered);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isHovered]);
useEffect(() => {
setLoadingStatus(foldersLoading);
@ -20,26 +41,28 @@ export const Folders = function Folders({
const [showForm, setShowForm] = useState(false);
const [isCreating, setCreationStatus] = useState(false);
const [isDeleting, setDeletionStatus] = useState(false);
const [isUpdating, setUpdationStatus] = useState(false);
const [deletingFolder, setDeletingFolder] = useState(null);
const [updatingFolder, setUpdatingFolder] = useState(null);
const [newFolderName, setNewFolderName] = useState('');
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const [showUpdateForm, setShowUpdateForm] = useState(false);
const [activeFolder, setActiveFolder] = useState(currentFolder || {});
function saveFolder() {
if (!newFolderName || !newFolderName.trim()) {
toast.error("folder name can't be empty.", {
position: 'top-center',
if (validateName()) {
setCreationStatus(true);
folderService.create(newFolderName).then(() => {
toast.success('Folder created.', {
position: 'top-center',
});
setCreationStatus(false);
setShowForm(false);
setNewFolderName('');
foldersChanged();
});
return;
}
setCreationStatus(true);
folderService.create(newFolderName).then(() => {
toast.success('folder created.', {
position: 'top-center',
});
setCreationStatus(false);
setShowForm(false);
setNewFolderName('');
foldersChanged();
});
}
function handleFolderChange(folder) {
@ -47,8 +70,85 @@ export const Folders = function Folders({
folderChanged(folder);
}
function deleteFolder(folder) {
setShowDeleteConfirmation(true);
setDeletingFolder(folder);
}
function updateFolder(folder) {
setNewFolderName(folder.name);
setShowUpdateForm(true);
setUpdatingFolder(folder);
}
function executeDeletion() {
setDeletionStatus(true);
folderService
.deleteFolder(deletingFolder.id)
.then(() => {
toast.success('Folder has been deleted.', {
position: 'top-center',
});
setShowDeleteConfirmation(false);
setDeletionStatus(false);
foldersChanged();
})
.catch(({ error }) => {
toast.error(error);
setShowDeleteConfirmation(false);
setDeletionStatus(false);
});
}
function cancelDeleteDialog() {
setShowDeleteConfirmation(false);
setDeletingFolder(null);
}
function validateName() {
if (!newFolderName?.trim()) {
toast.error("Folder name can't be empty.", {
position: 'top-center',
});
return false;
}
return true;
}
function executeEditFolder() {
if (validateName()) {
setUpdationStatus(true);
folderService
.updateFolder(newFolderName, updatingFolder.id)
.then(() => {
toast.success('Folder has been updated.', {
position: 'top-center',
});
setUpdationStatus(false);
setShowUpdateForm(false);
setNewFolderName('');
foldersChanged();
})
.catch(({ error }) => {
toast.error(error);
setNewFolderName('');
setUpdationStatus(false);
});
}
}
return (
<div className="w-100 px-3 pe-lg-4 folder-list">
<ConfirmDialog
show={showDeleteConfirmation}
message={`Are you sure you want to delete the folder?
Apps within the folder will not be deleted.`}
confirmButtonLoading={isDeleting}
onConfirm={() => executeDeletion()}
onCancel={() => cancelDeleteDialog()}
darkMode={darkMode}
/>
<div
data-testid="applicationFoldersList"
className={`list-group list-group-transparent mb-3 ${darkMode && 'dark'}`}
@ -65,7 +165,13 @@ export const Folders = function Folders({
<div className="d-flex justify-content-between mb-3">
<div className="folder-info">Folders</div>
{canCreateFolder && (
<div className="folder-create-btn" onClick={() => setShowForm(true)}>
<div
className="folder-create-btn"
onClick={() => {
setNewFolderName('');
setShowForm(true);
}}
>
+ Create new folder
</div>
)}
@ -88,22 +194,40 @@ export const Folders = function Folders({
? folders.map((folder, index) => (
<a
key={index}
ref={hoverRef}
className={`list-group-item list-group-item-action d-flex align-items-center ${
activeFolder.id === folder.id ? 'active' : ''
} ${darkMode && 'dark'}`}
onClick={() => handleFolderChange(folder)}
} ${darkMode && 'dark'} ${focused ? ' highlight' : ''}`}
>
<span className="me-2">
<img src="/assets/images/icons/folder.svg" alt="" width="14" height="14" className="folder-ico" />
</span>
{`${folder.name}${folder.count > 0 ? ` (${folder.count})` : ''}`}
<div onClick={() => handleFolderChange(folder)} className="flex-grow-1">
<span className="me-2">
<img src="/assets/images/icons/folder.svg" alt="" width="14" height="14" className="folder-ico" />
</span>
{`${folder.name}${folder.count > 0 ? ` (${folder.count})` : ''}`}
</div>
<div className="pt-1">
{(canDeleteFolder || canUpdateFolder) && (
<FolderMenu
onMenuOpen={onMenuToggle}
canDeleteFolder={canDeleteFolder}
canUpdateFolder={canUpdateFolder}
deleteFolder={() => deleteFolder(folder)}
editFolder={() => updateFolder(folder)}
darkMode={darkMode}
/>
)}
</div>
</a>
))
: !isLoading && (
<div className="folder-info">You haven&apos;t created any folders. Use folders to organize your apps</div>
)}
<Modal show={showForm} closeModal={() => setShowForm(false)} title="Create folder">
<Modal
show={showForm || showUpdateForm}
closeModal={() => (showUpdateForm ? setShowUpdateForm(false) : setShowForm(false))}
title={showUpdateForm ? 'Update Folder' : 'Create folder'}
>
<div className="row">
<div className="col modal-main">
<input
@ -111,18 +235,25 @@ export const Folders = function Folders({
onChange={(e) => setNewFolderName(e.target.value)}
className="form-control"
placeholder="folder name"
disabled={isCreating}
disabled={isCreating || isUpdating}
value={newFolderName}
maxLength={25}
/>
</div>
</div>
<div className="row">
<div className="col d-flex modal-footer-btn">
<button className="btn btn-light" onClick={() => setShowForm(false)}>
<button
className="btn btn-light"
onClick={() => (showUpdateForm ? setShowUpdateForm(false) : setShowForm(false))}
>
Cancel
</button>
<button className={`btn btn-primary ${isCreating ? 'btn-loading' : ''}`} onClick={saveFolder}>
Create folder
<button
className={`btn btn-primary ${isCreating || isUpdating ? 'btn-loading' : ''}`}
onClick={showUpdateForm ? executeEditFolder : saveFolder}
>
{showUpdateForm ? 'Update folder' : 'Create folder'}
</button>
</div>
</div>

View file

@ -274,6 +274,14 @@ class HomePage extends React.Component {
return this.canAnyGroupPerformAction('folder_create', this.state.currentUser.group_permissions);
};
canDeleteFolder = () => {
return this.canAnyGroupPerformAction('folder_delete', this.state.currentUser.group_permissions);
};
canUpdateFolder = () => {
return this.canAnyGroupPerformAction('folder_update', this.state.currentUser.group_permissions);
};
cancelDeleteAppDialog = () => {
this.setState({
isDeletingApp: false,
@ -622,6 +630,8 @@ class HomePage extends React.Component {
folderChanged={this.folderChanged}
foldersChanged={this.foldersChanged}
canCreateFolder={this.canCreateFolder()}
canDeleteFolder={this.canDeleteFolder()}
canUpdateFolder={this.canUpdateFolder()}
darkMode={this.props.darkMode}
/>
</div>

View file

@ -280,6 +280,10 @@ class ManageGroupPermissionResources extends React.Component {
selectedUserIds,
} = this.state;
const folder_permission = groupPermission
? groupPermission.folder_create && groupPermission.folder_delete && groupPermission.folder_update
: false;
const appSelectOptions = appsNotInGroup.map((app) => {
return { name: app.name, value: app.id };
});
@ -606,13 +610,15 @@ class ManageGroupPermissionResources extends React.Component {
type="checkbox"
onChange={() => {
this.updateGroupPermission(groupPermission.id, {
folder_create: !groupPermission.folder_create,
folder_create: !folder_permission,
folder_delete: !folder_permission,
folder_update: !folder_permission,
});
}}
checked={groupPermission.folder_create}
checked={folder_permission}
disabled={groupPermission.group === 'admin'}
/>
<span className="form-check-label">Create</span>
<span className="form-check-label">Create/Update/Delete</span>
</label>
</div>
</td>

View file

@ -3,9 +3,11 @@ import { authHeader, handleResponse } from '@/_helpers';
export const folderService = {
create,
deleteFolder,
getAll,
addToFolder,
removeAppFromFolder,
updateFolder,
};
function getAll(searchKey = '') {
@ -26,6 +28,27 @@ function create(name) {
return fetch(`${config.apiUrl}/folders`, requestOptions).then(handleResponse);
}
function updateFolder(name, id) {
const body = {
name,
};
const requestOptions = {
method: 'PUT',
headers: authHeader(),
body: JSON.stringify(body),
};
return fetch(`${config.apiUrl}/folders/${id}`, requestOptions).then(handleResponse);
}
function deleteFolder(id) {
const requestOptions = {
method: 'DELETE',
headers: authHeader(),
};
return fetch(`${config.apiUrl}/folders/${id}`, requestOptions).then(handleResponse);
}
function addToFolder(appId, folderId) {
const body = {
app_id: appId,

View file

@ -4164,6 +4164,25 @@ input[type="text"] {
color: #0565ff;
cursor: pointer;
}
.menu-ico {
cursor: pointer;
padding: 3px;
border-radius: 13px;
&__open {
background-color: #d2ddec;
}
img {
padding: 0px;
height: 14px;
width: 14px;
vertical-align: unset;
}
}
.menu-ico:hover {
background-color: #d2ddec;
}
}
/**

View file

@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddFolderUpdateAndDeletePermissionToGroupPermissions1653391166172 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'group_permissions',
new TableColumn({
name: 'folder_delete',
type: 'boolean',
default: false,
isNullable: false,
})
);
await queryRunner.addColumn(
'group_permissions',
new TableColumn({
name: 'folder_update',
type: 'boolean',
default: false,
isNullable: false,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('group_permissions', 'folder_delete');
await queryRunner.dropColumn('group_permissions', 'folder_update');
}
}

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { GroupPermission } from '../src/entities/group_permission.entity';
export class BackfillFolderDeleteFolderUpdatePermissionsAsTruthyForAdminGroup1653474337657
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const GroupPermissionRepostory = entityManager.getRepository(GroupPermission);
await GroupPermissionRepostory.update({ group: 'admin' }, { folderUpdate: true, folderDelete: true });
}
public async down(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const GroupPermissionRepostory = entityManager.getRepository(GroupPermission);
await GroupPermissionRepostory.update({ group: 'admin' }, { folderUpdate: false, folderDelete: false });
}
}

View file

@ -1,4 +1,4 @@
import { Controller, Get, Post, Query, Request, UseGuards, Body } from '@nestjs/common';
import { Controller, Get, Post, Query, Request, UseGuards, Body, Delete, Param, Put } from '@nestjs/common';
import { decamelizeKeys } from 'humps';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { FoldersService } from '../services/folders.service';
@ -6,6 +6,7 @@ import { ForbiddenException } from '@nestjs/common';
import { FoldersAbilityFactory } from 'src/modules/casl/abilities/folders-ability.factory';
import { Folder } from 'src/entities/folder.entity';
import { CreateFolderDto } from '@dto/create-folder.dto';
import { User } from 'src/decorators/user.decorator';
@Controller('folders')
export class FoldersController {
@ -31,4 +32,28 @@ export class FoldersController {
const folder = await this.foldersService.create(req.user, folderName);
return decamelizeKeys(folder);
}
@UseGuards(JwtAuthGuard)
@Put(':id')
async update(@User() user, @Param('id') id, @Body('name') folderName: string) {
const ability = await this.foldersAbilityFactory.folderActions(user, {});
if (!ability.can('updateFolder', Folder)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const folder = await this.foldersService.update(id, folderName);
return decamelizeKeys(folder);
}
@UseGuards(JwtAuthGuard)
@Delete(':id')
async delete(@User() user, @Param('id') id) {
const ability = await this.foldersAbilityFactory.folderActions(user, {});
if (!ability.can('deleteFolder', Folder)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
return await this.foldersService.delete(user, id);
}
}

View file

@ -37,6 +37,12 @@ export class GroupPermission extends BaseEntity {
@Column({ name: 'folder_create', default: false })
folderCreate: boolean;
@Column({ name: 'folder_delete', default: false })
folderDelete: boolean;
@Column({ name: 'folder_update', default: false })
folderUpdate: boolean;
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
createdAt: Date;

View file

@ -4,7 +4,7 @@ import { Injectable } from '@nestjs/common';
import { UsersService } from 'src/services/users.service';
import { Folder } from 'src/entities/folder.entity';
type Actions = 'createFolder';
type Actions = 'createFolder' | 'updateFolder' | 'deleteFolder';
type Subjects = InferSubjects<typeof User | typeof Folder> | 'all';
@ -21,6 +21,14 @@ export class FoldersAbilityFactory {
can('createFolder', Folder);
}
if (await this.usersService.userCan(user, 'update', 'Folder')) {
can('updateFolder', Folder);
}
if (await this.usersService.userCan(user, 'delete', 'Folder')) {
can('deleteFolder', Folder);
}
return build({
detectSubjectType: (item) => item.constructor as ExtractSubjectType<Subjects>,
});

View file

@ -1,9 +1,9 @@
import { Injectable } from '@nestjs/common';
import { Injectable, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { App } from 'src/entities/app.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
import { Brackets, createQueryBuilder, Repository } from 'typeorm';
import { Brackets, createQueryBuilder, Repository, UpdateResult } from 'typeorm';
import { User } from '../../src/entities/user.entity';
import { Folder } from '../entities/folder.entity';
import { UsersService } from './users.service';
@ -31,6 +31,10 @@ export class FoldersService {
);
}
async update(folderId: string, folderName: string): Promise<UpdateResult> {
return this.foldersRepository.update({ id: folderId }, { name: folderName });
}
async allFolders(user: User): Promise<Folder[]> {
if (await this.usersService.hasGroup(user, 'admin')) {
return await this.foldersRepository.find({
@ -292,4 +296,36 @@ export class FoldersService {
return viewableAppsInFolder;
}
async delete(user: User, id: string) {
const folder = await this.foldersRepository.findOneOrFail({ id, organizationId: user.organizationId });
const allViewableApps = await createQueryBuilder(App, 'apps')
.select('apps.id')
.innerJoin('apps.groupPermissions', 'group_permissions')
.innerJoin('apps.appGroupPermissions', 'app_group_permissions')
.innerJoin(
UserGroupPermission,
'user_group_permissions',
'app_group_permissions.group_permission_id = user_group_permissions.group_permission_id'
)
.where('user_group_permissions.user_id = :userId', { userId: user.id })
.andWhere('app_group_permissions.read = :value', { value: true })
.orWhere('apps.user_id = :userId', {
value: true,
organizationId: user.organizationId,
userId: user.id,
})
.getMany();
const allViewableAppIds = allViewableApps.map((app) => app.id);
folder.folderApps.map((folderApp: FolderApp) => {
if (!allViewableAppIds.includes(folderApp.appId)) {
throw new ForbiddenException(
'Applications not authorised for you are included in the folder, please contact administrator to remove them and try again'
);
}
});
return await this.foldersRepository.delete({ id, organizationId: user.organizationId });
}
}

View file

@ -123,7 +123,17 @@ export class GroupPermissionsService {
},
});
const { app_create, app_delete, add_apps, remove_apps, add_users, remove_users, folder_create } = body;
const {
app_create,
app_delete,
add_apps,
remove_apps,
add_users,
remove_users,
folder_create,
folder_delete,
folder_update,
} = body;
await getManager().transaction(async (manager) => {
// update group permissions
@ -131,6 +141,8 @@ export class GroupPermissionsService {
...(typeof app_create === 'boolean' && { appCreate: app_create }),
...(typeof app_delete === 'boolean' && { appDelete: app_delete }),
...(typeof folder_create === 'boolean' && { folderCreate: folder_create }),
...(typeof folder_delete === 'boolean' && { folderDelete: folder_delete }),
...(typeof folder_update === 'boolean' && { folderUpdate: folder_update }),
};
if (Object.keys(groupPermissionUpdateParams).length !== 0) {
await manager.update(GroupPermission, groupPermissionId, groupPermissionUpdateParams);

View file

@ -93,6 +93,8 @@ export class OrganizationsService {
appCreate: isAdmin,
appDelete: isAdmin,
folderCreate: isAdmin,
folderUpdate: isAdmin,
folderDelete: isAdmin,
});
await this.groupPermissionsRepository.save(groupPermission);
createdGroupPermissions.push(groupPermission);

View file

@ -75,6 +75,8 @@ export class SeedsService {
appCreate: group == 'admin',
appDelete: group == 'admin',
folderCreate: group == 'admin',
folderUpdate: group == 'admin',
folderDelete: group == 'admin',
});
await manager.save(groupPermission);

View file

@ -311,6 +311,12 @@ export class UsersService {
case 'create':
permissionGrant = this.canAnyGroupPerformAction('folderCreate', await this.groupPermissions(user));
break;
case 'update':
permissionGrant = this.canAnyGroupPerformAction('folderUpdate', await this.groupPermissions(user));
break;
case 'delete':
permissionGrant = this.canAnyGroupPerformAction('folderDelete', await this.groupPermissions(user));
break;
default:
permissionGrant = false;
break;

View file

@ -76,11 +76,15 @@ describe('Authentication', () => {
expect(adminGroup.appCreate).toBeTruthy();
expect(adminGroup.appDelete).toBeTruthy();
expect(adminGroup.folderCreate).toBeTruthy();
expect(adminGroup.folderUpdate).toBeTruthy();
expect(adminGroup.folderDelete).toBeTruthy();
const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
expect(allUserGroup.appCreate).toBeFalsy();
expect(allUserGroup.appDelete).toBeFalsy();
expect(allUserGroup.folderCreate).toBeFalsy();
expect(allUserGroup.folderUpdate).toBeFalsy();
expect(allUserGroup.folderDelete).toBeFalsy();
});
describe('Single organization operations', () => {
beforeEach(async () => {
@ -220,11 +224,15 @@ describe('Authentication', () => {
expect(adminGroup.appCreate).toBeTruthy();
expect(adminGroup.appDelete).toBeTruthy();
expect(adminGroup.folderCreate).toBeTruthy();
expect(adminGroup.folderUpdate).toBeTruthy();
expect(adminGroup.folderDelete).toBeTruthy();
const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
expect(allUserGroup.appCreate).toBeFalsy();
expect(allUserGroup.appDelete).toBeFalsy();
expect(allUserGroup.folderCreate).toBeFalsy();
expect(allUserGroup.folderUpdate).toBeFalsy();
expect(allUserGroup.folderDelete).toBeFalsy();
});
it('authenticate if valid credentials', async () => {
await request(app.getHttpServer())
@ -380,6 +388,8 @@ describe('Authentication', () => {
'updated_at',
'created_at',
'folder_create',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -441,6 +451,8 @@ describe('Authentication', () => {
'updated_at',
'created_at',
'folder_create',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);

View file

@ -294,6 +294,114 @@ describe('folders controller', () => {
});
});
describe('PUT /api/folders/:id', () => {
it('should be able to update an existing folder if group is admin or has update permission in the same organization', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(nestApp, {
email: 'dev@tooljet.io',
groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(nestApp, {
email: 'viewer@tooljet.io',
groups: ['viewer', 'all_users'],
organization: adminUserData.organization,
});
const developerGroup = await getManager().findOneOrFail(GroupPermission, {
where: { group: 'developer' },
});
await getManager().update(GroupPermission, developerGroup.id, {
folderUpdate: true,
});
const folder = await getManager().save(Folder, {
name: 'Folder1',
organizationId: adminUserData.organization.id,
});
for (const userData of [adminUserData, developerUserData]) {
await request(nestApp.getHttpServer())
.put(`/api/folders/${folder.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({ name: 'My folder' })
.expect(200);
const updatedFolder = await getManager().findOne(Folder, folder.id);
expect(updatedFolder.name).toEqual('My folder');
}
await request(nestApp.getHttpServer())
.put(`/api/folders/${folder.id}`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.send({ name: 'My folder' })
.expect(403);
});
});
describe('DELETE /api/folders/:id', () => {
it('should be able to delete an existing folder if group is admin or has delete permission in the same organization', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(nestApp, {
email: 'dev@tooljet.io',
groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(nestApp, {
email: 'viewer@tooljet.io',
groups: ['viewer', 'all_users'],
organization: adminUserData.organization,
});
const developerGroup = await getManager().findOneOrFail(GroupPermission, {
where: { group: 'developer' },
});
await getManager().update(GroupPermission, developerGroup.id, {
folderDelete: true,
});
for (const userData of [adminUserData, developerUserData]) {
const folder = await getManager().save(Folder, {
name: 'Folder1',
organizationId: adminUserData.organization.id,
});
const preCount = await getManager().count(Folder);
await request(nestApp.getHttpServer())
.delete(`/api/folders/${folder.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send()
.expect(200);
const postCount = await getManager().count(Folder);
expect(postCount).toEqual(preCount - 1);
}
const folder = await getManager().save(Folder, {
name: 'Folder1',
organizationId: adminUserData.organization.id,
});
await request(nestApp.getHttpServer())
.delete(`/api/folders/${folder.id}`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.send()
.expect(403);
});
});
afterAll(async () => {
await nestApp.close();
});

View file

@ -165,6 +165,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -235,6 +237,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -295,6 +299,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -372,6 +378,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -449,6 +457,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -617,6 +627,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -702,6 +714,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -784,6 +798,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -856,6 +872,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -948,6 +966,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1038,6 +1058,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1128,6 +1150,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1261,6 +1285,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1331,6 +1357,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1391,6 +1419,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1468,6 +1498,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1545,6 +1577,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1713,6 +1747,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1798,6 +1834,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1880,6 +1918,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -1952,6 +1992,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -2044,6 +2086,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -2134,6 +2178,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_update',
'folder_delete',
].sort()
);
expect(app_group_permissions).toHaveLength(0);
@ -2224,6 +2270,8 @@ describe('oauth controller', () => {
'updated_at',
'created_at',
'folder_create',
'folder_delete',
'folder_update',
].sort()
);
expect(app_group_permissions).toHaveLength(0);

View file

@ -321,6 +321,8 @@ export async function maybeCreateDefaultGroupPermissions(nestApp, organizationId
appCreate: group == 'admin',
appDelete: group == 'admin',
folderCreate: group == 'admin',
folderUpdate: group == 'admin',
folderDelete: group == 'admin',
});
await groupPermissionRepository.save(groupPermission);
}