this.setState({ showNewUserForm: false, newUser: {} })}
>
Cancel
this.createUser(e)}
+ disabled={creatingUser}
>
Create User
@@ -298,9 +253,6 @@ class ManageOrgUsers extends React.Component {
Name
Email
-
- Role
-
Status
@@ -351,27 +303,9 @@ class ManageOrgUsers extends React.Component {
{user.email}
-
-
- {
- return { name: role, value: role.toLowerCase() };
- })}
- value={user.role}
- search={false}
- disabled={idChangingRole === user.id}
- onChange={(value) => {
- this.changeNewUserRole(user.id, value);
- }}
- filterOptions={fuzzySearch}
- placeholder="Select.."
- />
- {idChangingRole === user.id && Updating role... }
-
-
{user.status}
{user.status === 'invited' && 'invitation_token' in user ? (
@@ -380,10 +314,14 @@ class ManageOrgUsers extends React.Component {
onCopy={this.invitationLinkCopyHandler}
>
) : (
diff --git a/frontend/src/ResetPassword/ResetPasswordPage.jsx b/frontend/src/ResetPassword/ResetPasswordPage.jsx
index 70219c2853..661afa60bd 100644
--- a/frontend/src/ResetPassword/ResetPasswordPage.jsx
+++ b/frontend/src/ResetPassword/ResetPasswordPage.jsx
@@ -55,7 +55,7 @@ class ResetPassword extends React.Component {
diff --git a/frontend/src/SettingsPage/SettingsPage.jsx b/frontend/src/SettingsPage/SettingsPage.jsx
index 8fbefba418..397db5b5a4 100644
--- a/frontend/src/SettingsPage/SettingsPage.jsx
+++ b/frontend/src/SettingsPage/SettingsPage.jsx
@@ -22,7 +22,7 @@ function SettingsPage(props) {
const changePassword = async () => {
setPasswordChangeInProgress(true);
- const response = userService.changePassword(currentpassword, newPassword);
+ const response = await userService.changePassword(currentpassword, newPassword);
response
.then(() => {
toast.success('Password updated successfully', { hideProgressBar: true, autoClose: 3000 });
@@ -40,7 +40,7 @@ function SettingsPage(props) {
const newPasswordKeyPressHandler = async (event) => {
if (event.key === 'Enter') {
- changePassword();
+ await changePassword();
}
};
@@ -54,7 +54,7 @@ function SettingsPage(props) {
-
Settings
+
Profile Settings
diff --git a/frontend/src/SignupPage/SignupPage.jsx b/frontend/src/SignupPage/SignupPage.jsx
index e432398b95..7d3ec356f2 100644
--- a/frontend/src/SignupPage/SignupPage.jsx
+++ b/frontend/src/SignupPage/SignupPage.jsx
@@ -43,7 +43,7 @@ class SignupPage extends React.Component {
diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx
index edc0b43d5a..a190966022 100644
--- a/frontend/src/_components/DynamicForm.jsx
+++ b/frontend/src/_components/DynamicForm.jsx
@@ -53,7 +53,7 @@ const DynamicForm = ({ schema, optionchanged, createDataSource, options, isSavin
case 'toggle':
return {
defaultChecked: options[$key],
- onChange: () => optionchanged($key, !options[$key]),
+ onChange: () => optionchanged($key, !options[$key].value),
};
case 'dropdown':
case 'dropdown-component-flip':
diff --git a/frontend/src/_components/Header.jsx b/frontend/src/_components/Header.jsx
index 428a435715..3744d9851c 100644
--- a/frontend/src/_components/Header.jsx
+++ b/frontend/src/_components/Header.jsx
@@ -1,18 +1,10 @@
-import React, { useState, useEffect } from 'react';
-import cx from 'classnames';
+import React from 'react';
import { Link } from 'react-router-dom';
import { authenticationService } from '@/_services';
import { history } from '@/_helpers';
import { DarkModeToggle } from './DarkModeToggle';
export const Header = function Header({ switchDarkMode, darkMode }) {
- const [pathName, setPathName] = useState(document.location.pathname);
-
- useEffect(() => {
- setPathName(document.location.pathname);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [document.location.pathname]);
-
function logout() {
authenticationService.logout();
history.push('/login');
@@ -22,7 +14,7 @@ export const Header = function Header({ switchDarkMode, darkMode }) {
history.push('/settings');
}
- const { first_name, last_name } = authenticationService.currentUserValue;
+ const { first_name, last_name, admin } = authenticationService.currentUserValue;
return (
diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js
index b2a86b028a..316578a9ef 100644
--- a/frontend/src/_helpers/appUtils.js
+++ b/frontend/src/_helpers/appUtils.js
@@ -56,7 +56,7 @@ export function runTransformation(_ref, rawData, transformation, query) {
result = evalFunction(data, moment, _, currentState.components, currentState.queries, currentState.globals);
} catch (err) {
console.log('Transformation failed for query: ', query.name, err);
- toast.error(err.message, { hideProgressBar: true });
+ result = { message: err.stack.split('\n')[0], status: 'failed', data: data };
}
return result;
@@ -67,7 +67,7 @@ export async function executeActionsForEventId(_ref, eventId, component, mode) {
const filteredEvents = events.filter((event) => event.eventId === eventId);
for (const event of filteredEvents) {
- await executeAction(_ref, event, mode);
+ await executeAction(_ref, event, mode); // skipcq: JS-0032
}
}
@@ -98,6 +98,11 @@ async function copyToClipboard(text) {
}
function showModal(_ref, modalId, show) {
+ if (_.isEmpty(modalId)) {
+ console.log('No modal is associated with this event.');
+ return Promise.resolve();
+ }
+
const modalMeta = _ref.state.appDefinition.components[modalId];
const newState = {
@@ -115,9 +120,7 @@ function showModal(_ref, modalId, show) {
_ref.setState(newState);
- return new Promise(function (resolve, reject) {
- resolve();
- });
+ return Promise.resolve();
}
function executeAction(_ref, event, mode) {
@@ -125,10 +128,8 @@ function executeAction(_ref, event, mode) {
switch (event.actionId) {
case 'show-alert': {
const message = resolveReferences(event.message, _ref.state.currentState);
- toast(message, { hideProgressBar: true });
- return new Promise(function (resolve, reject) {
- resolve();
- });
+ toast(message, { hideProgressBar: true, type: event.alertType });
+ return Promise.resolve();
}
case 'run-query': {
@@ -139,9 +140,7 @@ function executeAction(_ref, event, mode) {
case 'open-webpage': {
const url = resolveReferences(event.url, _ref.state.currentState);
window.open(url, '_blank');
- return new Promise(function (resolve, reject) {
- resolve();
- });
+ return Promise.resolve();
}
case 'go-to-app': {
@@ -174,9 +173,7 @@ function executeAction(_ref, event, mode) {
window.open(url, '_blank');
}
}
- return new Promise(function (resolve, reject) {
- resolve();
- });
+ return Promise.resolve();
}
case 'show-modal':
@@ -189,9 +186,7 @@ function executeAction(_ref, event, mode) {
const contentToCopy = resolveReferences(event.contentToCopy, _ref.state.currentState);
copyToClipboard(contentToCopy);
- return new Promise(function (resolve, reject) {
- resolve();
- });
+ return Promise.resolve();
}
}
}
@@ -451,6 +446,34 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined) {
if (dataQuery.options.enableTransformation) {
finalData = runTransformation(_self, rawData, dataQuery.options.transformation, dataQuery);
+ if (finalData.status === 'failed') {
+ return _self.setState(
+ {
+ currentState: {
+ ..._self.state.currentState,
+ queries: {
+ ..._self.state.currentState.queries,
+ [queryName]: {
+ ..._self.state.currentState.queries[queryName],
+ isLoading: false,
+ },
+ },
+ errors: {
+ ..._self.state.currentState.errors,
+ [queryName]: {
+ type: 'transformations',
+ data: finalData,
+ options: options,
+ },
+ },
+ },
+ },
+ () => {
+ resolve();
+ onEvent(_self, 'onDataQueryFailure', { definition: { events: dataQuery.options.events } });
+ }
+ );
+ }
}
if (dataQuery.options.showSuccessNotification) {
diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js
index 4a4febb9d3..e26e88b8a7 100644
--- a/frontend/src/_helpers/utils.js
+++ b/frontend/src/_helpers/utils.js
@@ -165,7 +165,7 @@ export const serializeNestedObjectToQueryParams = function (obj, prefix) {
var str = [],
p;
for (p in obj) {
- if (obj.hasOwnProperty(p)) {
+ if (Object.prototype.hasOwnProperty.call(obj, p)) {
var k = prefix ? prefix + '[' + p + ']' : p,
v = obj[p];
str.push(
diff --git a/frontend/src/_hooks/use-popover.jsx b/frontend/src/_hooks/use-popover.jsx
index 530309d89a..93aa930a17 100644
--- a/frontend/src/_hooks/use-popover.jsx
+++ b/frontend/src/_hooks/use-popover.jsx
@@ -13,7 +13,7 @@ const useEscapeHandler = (handler = noop, dependencies = []) => {
document === null || document === void 0 ? void 0 : document.removeEventListener('keyup', escapeHandler);
}, dependencies);
};
-const useClickOutside = (handler = noop, dependencies) => {
+const useClickOutside = (dependencies, handler = noop) => {
const callbackRef = useRef(handler);
const ref = useRef(null);
const outsideClickHandler = (e) => {
@@ -43,7 +43,7 @@ const usePopover = (defaultOpen = false) => {
const toggle = useCallback(() => setOpen(!open), []);
const close = useCallback(() => setOpen(false), []);
useEscapeHandler(close, []);
- const contentRef = useClickOutside(open ? close : undefined, []);
+ const contentRef = useClickOutside([], open ? close : undefined);
const trigger = {
ref: triggerRef,
onClick: toggle,
diff --git a/frontend/src/_services/app.service.js b/frontend/src/_services/app.service.js
index 66a8caa513..adba239878 100644
--- a/frontend/src/_services/app.service.js
+++ b/frontend/src/_services/app.service.js
@@ -6,6 +6,8 @@ export const appService = {
getAll,
createApp,
cloneApp,
+ exportApp,
+ importApp,
deleteApp,
getApp,
getAppBySlug,
@@ -40,6 +42,16 @@ function cloneApp(id) {
return fetch(`${config.apiUrl}/apps/${id}/clone`, requestOptions).then(handleResponse);
}
+function exportApp(id) {
+ const requestOptions = { method: 'GET', headers: authHeader() };
+ return fetch(`${config.apiUrl}/apps/${id}/export`, requestOptions).then(handleResponse);
+}
+
+function importApp(body) {
+ const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
+ return fetch(`${config.apiUrl}/apps/import`, requestOptions).then(handleResponse);
+}
+
function getApp(id) {
const requestOptions = { method: 'GET', headers: authHeader() };
return fetch(`${config.apiUrl}/apps/${id}`, requestOptions).then(handleResponse);
diff --git a/frontend/src/_services/groupPermission.service.js b/frontend/src/_services/groupPermission.service.js
new file mode 100644
index 0000000000..3de153a98a
--- /dev/null
+++ b/frontend/src/_services/groupPermission.service.js
@@ -0,0 +1,120 @@
+import config from 'config';
+import { authHeader, handleResponse } from '@/_helpers';
+
+export const groupPermissionService = {
+ create,
+ update,
+ del,
+ getGroup,
+ getGroups,
+ getAppsInGroup,
+ getAppsNotInGroup,
+ getUsersInGroup,
+ getUsersNotInGroup,
+ updateAppGroupPermission,
+};
+
+function create(group) {
+ const body = {
+ group,
+ };
+
+ const requestOptions = {
+ method: 'POST',
+ headers: authHeader(),
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/group_permissions`, requestOptions).then(handleResponse);
+}
+
+function update(groupPermissionId, params) {
+ const body = {
+ add_apps: params.selectedAppIds,
+ remove_apps: params.removeAppIds,
+ add_users: params.selectedUserIds,
+ remove_users: params.removeUserIds,
+ };
+
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ body: JSON.stringify(body),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
+}
+
+function del(groupPermissionId) {
+ const requestOptions = {
+ method: 'DELETE',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
+}
+
+function getGroup(groupPermissionId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse);
+}
+
+function getGroups() {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions`, requestOptions).then(handleResponse);
+}
+
+function getAppsInGroup(groupPermissionId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/apps`, requestOptions).then(handleResponse);
+}
+
+function getAppsNotInGroup(groupPermissionId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/addable_apps`, requestOptions).then(
+ handleResponse
+ );
+}
+
+function getUsersInGroup(groupPermissionId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/users`, requestOptions).then(handleResponse);
+}
+
+function getUsersNotInGroup(groupPermissionId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: authHeader(),
+ };
+ return fetch(`${config.apiUrl}/group_permissions/${groupPermissionId}/addable_users`, requestOptions).then(
+ handleResponse
+ );
+}
+
+function updateAppGroupPermission(groupPermissionId, appGroupPermissionId, actions) {
+ const body = {
+ actions,
+ };
+
+ const requestOptions = {
+ method: 'PUT',
+ headers: authHeader(),
+ body: JSON.stringify(body),
+ };
+ return fetch(
+ `${config.apiUrl}/group_permissions/${groupPermissionId}/app_group_permissions/${appGroupPermissionId}`,
+ requestOptions
+ ).then(handleResponse);
+}
diff --git a/frontend/src/_services/organization_user.service.js b/frontend/src/_services/organization_user.service.js
index d147461f61..b82d43d6b4 100644
--- a/frontend/src/_services/organization_user.service.js
+++ b/frontend/src/_services/organization_user.service.js
@@ -7,18 +7,18 @@ export const organizationUserService = {
changeRole,
};
-function create(first_name, last_name, email, role) {
+function create(first_name, last_name, email) {
const body = {
first_name,
last_name,
email,
- role,
};
const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
return fetch(`${config.apiUrl}/organization_users`, requestOptions).then(handleResponse);
}
+// Deprecated
function changeRole(id, role) {
const body = {
role,
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index c47b7fa926..90c20d6414 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -388,7 +388,6 @@ body {
z-index: 3;
width: 59.3%;
margin-top: 0px;
- padding: 0.5px;
}
.preview-header {
@@ -893,7 +892,6 @@ body {
.jet-data-table::-webkit-scrollbar {
background: transparent;
- height: 0;
}
.jet-data-table:hover {
@@ -952,7 +950,7 @@ tr:focus {
}
.jet-container {
- border-radius: 8px;
+
}
.select-search__option {
@@ -1797,6 +1795,9 @@ input:focus-visible {
.card {
background-color: #324156 !important;
}
+ .card .table tbody td a{
+ color: inherit;
+ }
.DateInput {
background: #1f2936;
@@ -2013,8 +2014,13 @@ input:focus-visible {
.editor .editor-sidebar .inspector .header {
border: solid rgba(255, 255, 255, 0.09) !important;
border-width: 0px 0px 1px 0px !important;
+ .input-icon .input-icon-addon img {
+ filter: invert(1);
+ }
+ }
+ .editor .editor-sidebar .inspector .hr-text {
+ color: #fff !important;
}
-
.skeleton-line::after {
// background-image: linear-gradient(to right, #232e3c 0, #4c5b79 40%, #4c5b79 80%);
background-image: linear-gradient(to right, #566177 0, #5a6170 40%, #4c5b79 80%);
@@ -2238,3 +2244,7 @@ input[type='text'] {
height: 17px;
}
}
+
+.fw-500 {
+ font-weight: 500;
+}
diff --git a/frontend/src/_styles/widgets/star-rating.scss b/frontend/src/_styles/widgets/star-rating.scss
index c8ecc66414..6274589c38 100644
--- a/frontend/src/_styles/widgets/star-rating.scss
+++ b/frontend/src/_styles/widgets/star-rating.scss
@@ -7,4 +7,7 @@
.label {
flex: 1;
}
+ .star {
+ margin-bottom: 1px;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/_ui/Toggle/index.js b/frontend/src/_ui/Toggle/index.js
index a2505d5f4c..e4409b564f 100644
--- a/frontend/src/_ui/Toggle/index.js
+++ b/frontend/src/_ui/Toggle/index.js
@@ -1,7 +1,7 @@
import React from 'react';
export default ({ defaultChecked, onChange }) => {
return (
-
+
);
diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx
index 7162d1a4e8..b0b6f81d03 100755
--- a/frontend/src/index.jsx
+++ b/frontend/src/index.jsx
@@ -11,7 +11,7 @@ appService
.then((config) => {
window.public_config = config;
- if (window.public_config.APM_VENDOR == 'sentry') {
+ if (window.public_config.APM_VENDOR === 'sentry') {
const history = createBrowserHistory();
const tooljetServerUrl = window.public_config.TOOLJET_SERVER_URL;
const tracingOrigins = ['localhost', /^\//];
diff --git a/package-lock.json b/package-lock.json
index d6d24a880d..86f1a8f7d8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"babel-loader": "^8.0.5",
"html-webpack-plugin": "^5.3.2",
"husky": "^7.0.2",
+ "lint-staged": "^11.2.3",
"path": "^0.12.7",
"webpack": "^5.55.1",
"webpack-cli": "^4.8.0"
@@ -1312,7 +1313,6 @@
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"dev": true,
- "peer": true,
"dependencies": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
@@ -1868,7 +1868,6 @@
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=6"
}
@@ -1954,7 +1953,6 @@
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
- "peer": true,
"dependencies": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
@@ -2013,9 +2011,9 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/colorette": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
- "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
},
"node_modules/colors": {
"version": "1.4.0",
@@ -2265,24 +2263,6 @@
"node": ">= 6"
}
},
- "node_modules/cypress/node_modules/debug": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
- "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
- "dev": true,
- "peer": true,
- "dependencies": {
- "ms": "2.1.2"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
"node_modules/cypress/node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -2349,9 +2329,9 @@
"peer": true
},
"node_modules/debug": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
- "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"dependencies": {
"ms": "2.1.2"
},
@@ -3207,6 +3187,12 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-own-enumerable-property-symbols": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+ "dev": true
+ },
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -3568,7 +3554,6 @@
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -3717,6 +3702,15 @@
"node": ">=0.12.0"
}
},
+ "node_modules/is-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@@ -3739,6 +3733,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -3966,15 +3969,126 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
},
- "node_modules/listr2": {
- "version": "3.11.1",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz",
- "integrity": "sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==",
+ "node_modules/lint-staged": {
+ "version": "11.2.3",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.3.tgz",
+ "integrity": "sha512-Tfmhk8O2XFMD25EswHPv+OYhUjsijy5D7liTdxeXvhG2rsadmOLFtyj8lmlfoFFXY8oXWAIOKpoI+lJe1DB1mw==",
+ "dev": true,
+ "dependencies": {
+ "cli-truncate": "2.1.0",
+ "colorette": "^1.4.0",
+ "commander": "^8.2.0",
+ "cosmiconfig": "^7.0.1",
+ "debug": "^4.3.2",
+ "enquirer": "^2.3.6",
+ "execa": "^5.1.1",
+ "listr2": "^3.12.2",
+ "micromatch": "^4.0.4",
+ "normalize-path": "^3.0.0",
+ "please-upgrade-node": "^3.2.0",
+ "string-argv": "0.3.1",
+ "stringify-object": "3.3.0",
+ "supports-color": "8.1.1"
+ },
+ "bin": {
+ "lint-staged": "bin/lint-staged.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/lint-staged"
+ }
+ },
+ "node_modules/lint-staged/node_modules/commander": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz",
+ "integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/lint-staged/node_modules/cosmiconfig": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
+ "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lint-staged/node_modules/execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/lint-staged/node_modules/get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lint-staged/node_modules/human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/lint-staged/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/listr2": {
+ "version": "3.12.2",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.12.2.tgz",
+ "integrity": "sha512-64xC2CJ/As/xgVI3wbhlPWVPx0wfTqbUAkpb7bjDi0thSWMqrf07UFhrfsGoo8YSXmF049Rp9C0cjLC8rZxK9A==",
"dev": true,
- "peer": true,
"dependencies": {
"cli-truncate": "^2.1.0",
- "colorette": "^1.2.2",
+ "colorette": "^1.4.0",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"rxjs": "^6.6.7",
@@ -4074,7 +4188,6 @@
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
@@ -4093,7 +4206,6 @@
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -4111,7 +4223,6 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -4187,6 +4298,19 @@
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
+ "node_modules/micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/mime-db": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
@@ -4454,7 +4578,6 @@
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
- "peer": true,
"dependencies": {
"aggregate-error": "^3.0.0"
},
@@ -4635,6 +4758,15 @@
"node": ">=8"
}
},
+ "node_modules/please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "dependencies": {
+ "semver-compare": "^1.0.0"
+ }
+ },
"node_modules/pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@@ -4988,6 +5120,12 @@
"node": ">=10"
}
},
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+ "dev": true
+ },
"node_modules/serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
@@ -5053,7 +5191,6 @@
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -5129,6 +5266,15 @@
"safe-buffer": "~5.2.0"
}
},
+ "node_modules/string-argv": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
+ "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
"node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
@@ -5142,6 +5288,20 @@
"node": ">=8"
}
},
+ "node_modules/stringify-object": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
+ "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+ "dev": true,
+ "dependencies": {
+ "get-own-enumerable-property-symbols": "^3.0.0",
+ "is-obj": "^1.0.1",
+ "is-regexp": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@@ -5936,7 +6096,6 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
- "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -7089,7 +7248,6 @@
"resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
"integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"dev": true,
- "peer": true,
"requires": {
"clean-stack": "^2.0.0",
"indent-string": "^4.0.0"
@@ -7486,8 +7644,7 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
- "dev": true,
- "peer": true
+ "dev": true
},
"cli-cursor": {
"version": "3.1.0",
@@ -7546,7 +7703,6 @@
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
- "peer": true,
"requires": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
@@ -7587,9 +7743,9 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"colorette": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
- "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="
},
"colors": {
"version": "1.4.0",
@@ -7794,16 +7950,6 @@
"dev": true,
"peer": true
},
- "debug": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
- "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
- "dev": true,
- "peer": true,
- "requires": {
- "ms": "2.1.2"
- }
- },
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -7857,9 +8003,9 @@
"peer": true
},
"debug": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
- "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
+ "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
"requires": {
"ms": "2.1.2"
}
@@ -8516,6 +8662,12 @@
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true
},
+ "get-own-enumerable-property-symbols": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
+ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
+ "dev": true
+ },
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -8754,8 +8906,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
- "dev": true,
- "peer": true
+ "dev": true
},
"inflight": {
"version": "1.0.6",
@@ -8865,6 +9016,12 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
+ "is-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
+ "dev": true
+ },
"is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
@@ -8881,6 +9038,12 @@
"isobject": "^3.0.1"
}
},
+ "is-regexp": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz",
+ "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=",
+ "dev": true
+ },
"is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -9060,15 +9223,95 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="
},
- "listr2": {
- "version": "3.11.1",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.11.1.tgz",
- "integrity": "sha512-ZXQvQfmH9iWLlb4n3hh31yicXDxlzB0pE7MM1zu6kgbVL4ivEsO4H8IPh4E682sC8RjnYO9anose+zT52rrpyg==",
+ "lint-staged": {
+ "version": "11.2.3",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-11.2.3.tgz",
+ "integrity": "sha512-Tfmhk8O2XFMD25EswHPv+OYhUjsijy5D7liTdxeXvhG2rsadmOLFtyj8lmlfoFFXY8oXWAIOKpoI+lJe1DB1mw==",
+ "dev": true,
+ "requires": {
+ "cli-truncate": "2.1.0",
+ "colorette": "^1.4.0",
+ "commander": "^8.2.0",
+ "cosmiconfig": "^7.0.1",
+ "debug": "^4.3.2",
+ "enquirer": "^2.3.6",
+ "execa": "^5.1.1",
+ "listr2": "^3.12.2",
+ "micromatch": "^4.0.4",
+ "normalize-path": "^3.0.0",
+ "please-upgrade-node": "^3.2.0",
+ "string-argv": "0.3.1",
+ "stringify-object": "3.3.0",
+ "supports-color": "8.1.1"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz",
+ "integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==",
+ "dev": true
+ },
+ "cosmiconfig": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
+ "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==",
+ "dev": true,
+ "requires": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ }
+ },
+ "execa": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
+ "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "get-stream": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+ "dev": true
+ },
+ "human-signals": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
+ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "listr2": {
+ "version": "3.12.2",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.12.2.tgz",
+ "integrity": "sha512-64xC2CJ/As/xgVI3wbhlPWVPx0wfTqbUAkpb7bjDi0thSWMqrf07UFhrfsGoo8YSXmF049Rp9C0cjLC8rZxK9A==",
"dev": true,
- "peer": true,
"requires": {
"cli-truncate": "^2.1.0",
- "colorette": "^1.2.2",
+ "colorette": "^1.4.0",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"rxjs": "^6.6.7",
@@ -9147,7 +9390,6 @@
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dev": true,
- "peer": true,
"requires": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
@@ -9160,7 +9402,6 @@
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
- "peer": true,
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -9172,7 +9413,6 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
- "peer": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -9237,6 +9477,16 @@
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
},
+ "micromatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
+ "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ }
+ },
"mime-db": {
"version": "1.49.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz",
@@ -9445,7 +9695,6 @@
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
"integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
"dev": true,
- "peer": true,
"requires": {
"aggregate-error": "^3.0.0"
}
@@ -9593,6 +9842,15 @@
"find-up": "^4.0.0"
}
},
+ "please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "requires": {
+ "semver-compare": "^1.0.0"
+ }
+ },
"pluralize": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
@@ -9844,6 +10102,12 @@
"lru-cache": "^6.0.0"
}
},
+ "semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=",
+ "dev": true
+ },
"serialize-javascript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
@@ -9894,7 +10158,6 @@
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
- "peer": true,
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -9956,6 +10219,12 @@
"safe-buffer": "~5.2.0"
}
},
+ "string-argv": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
+ "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
+ "dev": true
+ },
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
@@ -9966,6 +10235,17 @@
"strip-ansi": "^6.0.0"
}
},
+ "stringify-object": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz",
+ "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==",
+ "dev": true,
+ "requires": {
+ "get-own-enumerable-property-symbols": "^3.0.0",
+ "is-obj": "^1.0.1",
+ "is-regexp": "^1.0.0"
+ }
+ },
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@@ -10527,7 +10807,6 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
- "peer": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
diff --git a/package.json b/package.json
index f15e0564c8..11912b0b4c 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,6 @@
"eslint --fix"
]
},
- "husky": {
- "hooks": {
- "pre-commit": "lint-staged"
- }
- },
"devDependencies": {
"@4tw/cypress-drag-drop": "^1.8.0",
"@babel/core": "^7.4.3",
@@ -25,6 +20,7 @@
"babel-loader": "^8.0.5",
"html-webpack-plugin": "^5.3.2",
"husky": "^7.0.2",
+ "lint-staged": "^11.2.3",
"path": "^0.12.7",
"webpack": "^5.55.1",
"webpack-cli": "^4.8.0"
diff --git a/server/.version b/server/.version
index ef090a6c47..8adc70fdd9 100644
--- a/server/.version
+++ b/server/.version
@@ -1 +1 @@
-0.7.4
\ No newline at end of file
+0.8.0
\ No newline at end of file
diff --git a/server/migrations/1632382322381-CreateGroupPermissions.ts b/server/migrations/1632382322381-CreateGroupPermissions.ts
new file mode 100644
index 0000000000..638354e748
--- /dev/null
+++ b/server/migrations/1632382322381-CreateGroupPermissions.ts
@@ -0,0 +1,70 @@
+import {
+ MigrationInterface,
+ QueryRunner,
+ Table,
+ TableForeignKey,
+ TableUnique,
+} from "typeorm";
+
+export class CreateGroupPermissions1632382322381 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: "group_permissions",
+ columns: [
+ {
+ name: "id",
+ type: "uuid",
+ isGenerated: true,
+ default: "gen_random_uuid()",
+ isPrimary: true,
+ },
+ {
+ name: "organization_id",
+ type: "uuid",
+ isNullable: false,
+ },
+ {
+ name: "group",
+ type: "varchar",
+ isNullable: false,
+ },
+ {
+ name: "created_at",
+ type: "timestamp",
+ isNullable: false,
+ default: "now()",
+ },
+ {
+ name: "updated_at",
+ type: "timestamp",
+ isNullable: false,
+ default: "now()",
+ },
+ ],
+ }),
+ true
+ );
+
+ await queryRunner.createForeignKey(
+ "group_permissions",
+ new TableForeignKey({
+ columnNames: ["organization_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "organizations",
+ onDelete: "CASCADE",
+ })
+ );
+
+ await queryRunner.createUniqueConstraint(
+ "group_permissions",
+ new TableUnique({
+ columnNames: ["organization_id", "group"],
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable("group_permissions");
+ }
+}
diff --git a/server/migrations/1632383798339-CreateUserGroupPermissions.ts b/server/migrations/1632383798339-CreateUserGroupPermissions.ts
new file mode 100644
index 0000000000..f8f62b0a3a
--- /dev/null
+++ b/server/migrations/1632383798339-CreateUserGroupPermissions.ts
@@ -0,0 +1,74 @@
+import {
+ MigrationInterface,
+ QueryRunner,
+ Table,
+ TableForeignKey,
+} from "typeorm";
+
+export class CreateUserGroupPermissions1632383798339
+ implements MigrationInterface
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: "user_group_permissions",
+ columns: [
+ {
+ name: "id",
+ type: "uuid",
+ isGenerated: true,
+ default: "gen_random_uuid()",
+ isPrimary: true,
+ },
+ {
+ name: "user_id",
+ type: "uuid",
+ isNullable: false,
+ },
+ {
+ name: "group_permission_id",
+ type: "uuid",
+ isNullable: false,
+ },
+ {
+ name: "created_at",
+ type: "timestamp",
+ isNullable: false,
+ default: "now()",
+ },
+ {
+ name: "updated_at",
+ type: "timestamp",
+ isNullable: false,
+ default: "now()",
+ },
+ ],
+ }),
+ true
+ );
+
+ await queryRunner.createForeignKey(
+ "user_group_permissions",
+ new TableForeignKey({
+ columnNames: ["user_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "users",
+ onDelete: "CASCADE",
+ })
+ );
+
+ await queryRunner.createForeignKey(
+ "user_group_permissions",
+ new TableForeignKey({
+ columnNames: ["group_permission_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "group_permissions",
+ onDelete: "CASCADE",
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable("user_group_permissions");
+ }
+}
diff --git a/server/migrations/1632384954344-CreateAppGroupPermissions.ts b/server/migrations/1632384954344-CreateAppGroupPermissions.ts
new file mode 100644
index 0000000000..2baa58fd78
--- /dev/null
+++ b/server/migrations/1632384954344-CreateAppGroupPermissions.ts
@@ -0,0 +1,92 @@
+import {
+ MigrationInterface,
+ QueryRunner,
+ Table,
+ TableForeignKey,
+} from "typeorm";
+
+export class CreateAppGroupPermissions1632384954344
+ implements MigrationInterface
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: "app_group_permissions",
+ columns: [
+ {
+ name: "id",
+ type: "uuid",
+ isGenerated: true,
+ default: "gen_random_uuid()",
+ isPrimary: true,
+ },
+ {
+ name: "app_id",
+ type: "uuid",
+ isNullable: false,
+ },
+ {
+ name: "group_permission_id",
+ type: "uuid",
+ isNullable: false,
+ },
+ {
+ name: "read",
+ type: "boolean",
+ default: false,
+ isNullable: false,
+ },
+ {
+ name: "update",
+ type: "boolean",
+ default: false,
+ isNullable: false,
+ },
+ {
+ name: "delete",
+ type: "boolean",
+ default: false,
+ isNullable: false,
+ },
+ {
+ name: "created_at",
+ type: "timestamp",
+ isNullable: false,
+ default: "now()",
+ },
+ {
+ name: "updated_at",
+ type: "timestamp",
+ isNullable: false,
+ default: "now()",
+ },
+ ],
+ }),
+ true
+ );
+
+ await queryRunner.createForeignKey(
+ "app_group_permissions",
+ new TableForeignKey({
+ columnNames: ["app_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "apps",
+ onDelete: "CASCADE",
+ })
+ );
+
+ await queryRunner.createForeignKey(
+ "app_group_permissions",
+ new TableForeignKey({
+ columnNames: ["group_permission_id"],
+ referencedColumnNames: ["id"],
+ referencedTableName: "group_permissions",
+ onDelete: "CASCADE",
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.dropTable("app_group_permissions");
+ }
+}
diff --git a/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts b/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts
new file mode 100644
index 0000000000..32d40578aa
--- /dev/null
+++ b/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts
@@ -0,0 +1,137 @@
+import { EntityManager, In, MigrationInterface, QueryRunner } from "typeorm";
+import { Organization } from "../src/entities/organization.entity";
+import { GroupPermission } from "../src/entities/group_permission.entity";
+import { AppGroupPermission } from "../src/entities/app_group_permission.entity";
+import { UserGroupPermission } from "../src/entities/user_group_permission.entity";
+import { App } from "../src/entities/app.entity";
+
+export class PopulateUserGroupsFromOrganizationRoles1632468258787
+ implements MigrationInterface
+{
+ public async up(queryRunner: QueryRunner): Promise {
+ const entityManager = queryRunner.manager;
+ const OrganizationRepository = entityManager.getRepository(Organization);
+
+ const organizations = await OrganizationRepository.find({
+ relations: ["users"],
+ });
+
+ for (let organization of organizations) {
+ const groupPermissions = await setupInitialGroupPermissions(
+ entityManager,
+ organization
+ );
+ await setupUserAndAppGroupPermissions(
+ entityManager,
+ organization,
+ groupPermissions
+ );
+ }
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ const entityManager = queryRunner.manager;
+
+ entityManager
+ .createQueryBuilder()
+ .delete()
+ .from(GroupPermission)
+ .execute();
+
+ entityManager
+ .createQueryBuilder()
+ .delete()
+ .from(AppGroupPermission)
+ .execute();
+
+ entityManager
+ .createQueryBuilder()
+ .delete()
+ .from(UserGroupPermission)
+ .execute();
+ }
+}
+
+async function setupInitialGroupPermissions(
+ entityManager: EntityManager,
+ organization: Organization
+): Promise> {
+ const existingRoles = ["admin", "developer", "viewer"];
+ const groupsToCreate = ["all_users", ...existingRoles];
+ const createdGroupPermissions = [];
+
+ const groupPermissionRepository =
+ entityManager.getRepository(GroupPermission);
+
+ for (const group of groupsToCreate) {
+ const groupPermission = groupPermissionRepository.create({
+ organizationId: organization.id,
+ group: group,
+ });
+ await groupPermissionRepository.save(groupPermission);
+ createdGroupPermissions.push(groupPermission);
+ }
+
+ return createdGroupPermissions;
+}
+
+async function setupUserAndAppGroupPermissions(
+ entityManager: EntityManager,
+ organization: Organization,
+ createdGroupPermissions: Array
+): Promise {
+ const userGroupPermissionRepository =
+ entityManager.getRepository(UserGroupPermission);
+
+ const appGroupPermissionRepository =
+ entityManager.getRepository(AppGroupPermission);
+
+ const appRepository = entityManager.getRepository(App);
+
+ const organizationApps = await appRepository.find({
+ organizationId: organization.id,
+ });
+
+ for (const groupPermission of createdGroupPermissions) {
+ const usersForGroup = organization.users.filter(
+ (u) =>
+ u.organizationUsers[0].role == groupPermission.group || groupPermission.group == "all_users"
+ );
+
+ for (const user of usersForGroup) {
+ const userGroupPermission = userGroupPermissionRepository.create({
+ groupPermissionId: groupPermission.id,
+ userId: user.id,
+ });
+ await userGroupPermissionRepository.save(userGroupPermission);
+ }
+
+ const permissions = determinePermissionsForGroup(groupPermission.group);
+
+ for (const app of organizationApps) {
+ const appGroupPermission = appGroupPermissionRepository.create({
+ groupPermissionId: groupPermission.id,
+ appId: app.id,
+ ...permissions,
+ });
+ await appGroupPermissionRepository.save(appGroupPermission);
+ }
+ }
+}
+
+function determinePermissionsForGroup(group: string): {
+ read: boolean;
+ update: boolean;
+ delete: boolean;
+} {
+ switch (group) {
+ case "all_users":
+ return { read: true, update: false, delete: false };
+ case "admin":
+ return { read: true, update: true, delete: true };
+ case "developer":
+ return { read: true, update: true, delete: true };
+ case "viewer":
+ return { read: true, update: false, delete: false };
+ }
+}
diff --git a/server/plugins/datasources/restapi/index.ts b/server/plugins/datasources/restapi/index.ts
index c4a0e9b615..deffb97dea 100644
--- a/server/plugins/datasources/restapi/index.ts
+++ b/server/plugins/datasources/restapi/index.ts
@@ -80,7 +80,7 @@ export default class RestapiQueryService implements QueryService {
let result = {};
/* Prefixing the base url of datasouce if datasource exists */
- const url = hasDataSource ? `${sourceOptions.url}${queryOptions.url}` : queryOptions.url;
+ const url = hasDataSource ? `${sourceOptions.url}${queryOptions.url || ''}` : queryOptions.url;
const method = queryOptions['method'];
const json = method !== 'get' ? this.body(sourceOptions, queryOptions, hasDataSource) : undefined;
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index cd1d07f9cb..625c560a6d 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -16,7 +16,6 @@ import { CaslModule } from './modules/casl/casl.module';
import { EmailService } from '@services/email.service';
import { MetaModule } from './modules/meta/meta.module';
import { AppController } from './controllers/app.controller';
-import { AppService } from './services/app.service';
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { AppConfigModule } from './modules/app_config/app_config.module';
@@ -27,6 +26,7 @@ import { DataQueriesModule } from './modules/data_queries/data_queries.module';
import { DataSourcesModule } from './modules/data_sources/data_sources.module';
import { OrganizationsModule } from './modules/organizations/organizations.module';
import { join } from 'path';
+import { GroupPermissionsModule } from './modules/group_permissions/group_permissions.module';
const imports = [
ConfigModule.forRoot({
@@ -63,6 +63,7 @@ const imports = [
OrganizationsModule,
CaslModule,
MetaModule,
+ GroupPermissionsModule,
];
if (process.env.SERVE_CLIENT !== 'false') {
@@ -86,7 +87,7 @@ if (process.env.APM_VENDOR == 'sentry') {
@Module({
imports,
controllers: [AppController],
- providers: [AppService, EmailService, SeedsService],
+ providers: [EmailService, SeedsService],
})
export class AppModule implements OnModuleInit, OnApplicationBootstrap {
constructor(private connection: Connection) {}
@@ -98,11 +99,11 @@ export class AppModule implements OnModuleInit, OnApplicationBootstrap {
});
}
- onModuleInit() {
+ onModuleInit(): void {
console.log(`Initializing ToolJet server modules 📡 `);
}
- onApplicationBootstrap() {
+ onApplicationBootstrap(): void {
console.log(`Initialized ToolJet server, waiting for requests 🚀`);
}
}
diff --git a/server/src/controllers/app_users.controller.ts b/server/src/controllers/app_users.controller.ts
index d08c0e8ed5..eced2f52e0 100644
--- a/server/src/controllers/app_users.controller.ts
+++ b/server/src/controllers/app_users.controller.ts
@@ -13,6 +13,7 @@ export class AppUsersController {
private appsAbilityFactory: AppsAbilityFactory
) {}
+ // TODO: remove deprecated
@UseGuards(JwtAuthGuard)
@Post()
async create(@Request() req) {
@@ -22,7 +23,7 @@ export class AppUsersController {
const { role } = params;
const app = await this.appsService.find(appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, { id: appId });
if (!ability.can('createUsers', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
diff --git a/server/src/controllers/apps.controller.ts b/server/src/controllers/apps.controller.ts
index ecad5cd452..40f6d0979d 100644
--- a/server/src/controllers/apps.controller.ts
+++ b/server/src/controllers/apps.controller.ts
@@ -16,11 +16,14 @@ import { decamelizeKeys } from 'humps';
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
import { AppAuthGuard } from 'src/modules/auth/app-auth.guard';
import { FoldersService } from '@services/folders.service';
+import { App } from 'src/entities/app.entity';
+import { AppImportExportService } from '@services/app_import_export.service';
@Controller('apps')
export class AppsController {
constructor(
private appsService: AppsService,
+ private appImportExportService: AppImportExportService,
private foldersService: FoldersService,
private appsAbilityFactory: AppsAbilityFactory
) {}
@@ -28,12 +31,12 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@Post()
async create(@Request() req) {
- const app = await this.appsService.create(req.user);
const ability = await this.appsAbilityFactory.appsActions(req.user, {});
- if (!ability.can('createApp', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ if (!ability.can('createApp', App)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
+ const app = await this.appsService.create(req.user);
await this.appsService.update(req.user, app.id, {
slug: app.id,
@@ -46,6 +49,11 @@ export class AppsController {
@Get(':id')
async show(@Request() req, @Param() params) {
const app = await this.appsService.find(params.id);
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+
+ if (!ability.can('viewApp', app)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
const response = decamelizeKeys(app);
const seralizedQueries = [];
@@ -68,10 +76,12 @@ export class AppsController {
async appFromSlug(@Request() req, @Param() params) {
if (req.user) {
const app = await this.appsService.findBySlug(params.slug);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: app.id,
+ });
if (!ability.can('viewApp', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
}
@@ -92,10 +102,10 @@ export class AppsController {
@Put(':id')
async update(@Request() req, @Param() params) {
const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('updateParams', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const result = await this.appsService.update(req.user, params.id, req.body.app);
@@ -108,10 +118,10 @@ export class AppsController {
@Post(':id/clone')
async clone(@Request() req, @Param() params) {
const existingApp = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('cloneApp', existingApp)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const result = await this.appsService.clone(existingApp, req.user);
@@ -120,11 +130,38 @@ export class AppsController {
return response;
}
+ @UseGuards(JwtAuthGuard)
+ @Get(':id/export')
+ async export(@Request() req, @Param() params) {
+ const appToExport = await this.appsService.find(params.id);
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+
+ if (!ability.can('viewApp', appToExport)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+
+ const app = await this.appImportExportService.export(req.user, params.id);
+ return app;
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @Post('/import')
+ async import(@Request() req) {
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+
+ if (!ability.can('createApp', App)) {
+ throw new ForbiddenException('You do not have permissions to perform this action');
+ }
+ await this.appImportExportService.import(req.user, req.body);
+
+ return;
+ }
+
@UseGuards(JwtAuthGuard)
@Delete(':id')
async delete(@Request() req, @Param() params) {
const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('deleteApp', app)) {
throw new ForbiddenException('Only administrators are allowed to delete apps.');
@@ -172,14 +209,15 @@ export class AppsController {
return decamelizeKeys(response);
}
+ // deprecated
@UseGuards(JwtAuthGuard)
@Get(':id/users')
async fetchUsers(@Request() req, @Param() params) {
const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('fetchUsers', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const result = await this.appsService.fetchUsers(req.user, params.id);
@@ -190,10 +228,10 @@ export class AppsController {
@Get(':id/versions')
async fetchVersions(@Request() req, @Param() params) {
const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('fetchVersions', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const result = await this.appsService.fetchVersions(req.user, params.id);
@@ -206,10 +244,10 @@ export class AppsController {
const versionName = req.body['versionName'];
const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('createVersions', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const appUser = await this.appsService.createVersion(req.user, app, versionName);
@@ -220,10 +258,10 @@ export class AppsController {
@Get(':id/versions/:versionId')
async version(@Request() req, @Param() params) {
const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('fetchVersions', app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const appVersion = await this.appsService.findVersion(params.versionId);
@@ -237,10 +275,10 @@ export class AppsController {
const definition = req.body['definition'];
const version = await this.appsService.findVersion(params.versionId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, params);
if (!ability.can('updateVersions', version.app)) {
- throw new ForbiddenException('you do not have permissions to perform this action');
+ throw new ForbiddenException('You do not have permissions to perform this action');
}
const appUser = await this.appsService.updateVersion(req.user, version, definition);
diff --git a/server/src/controllers/data_queries.controller.ts b/server/src/controllers/data_queries.controller.ts
index 5097b22431..9e22fbd746 100644
--- a/server/src/controllers/data_queries.controller.ts
+++ b/server/src/controllers/data_queries.controller.ts
@@ -32,7 +32,9 @@ export class DataQueriesController {
@Get()
async index(@Request() req, @Query() query) {
const app = await this.appsService.find(query.app_id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: query.app_id,
+ });
if (!ability.can('getQueries', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -61,7 +63,9 @@ export class DataQueriesController {
const appId = req.body.app_id;
const app = await this.appsService.find(appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: appId,
+ });
if (!ability.can('createQuery', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -88,7 +92,9 @@ export class DataQueriesController {
const dataQueryId = params.id;
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: dataQuery.appId,
+ });
if (!ability.can('updateQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -104,7 +110,9 @@ export class DataQueriesController {
const dataQueryId = params.id;
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: dataQuery.appId,
+ });
if (!ability.can('deleteQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -123,7 +131,9 @@ export class DataQueriesController {
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
if (req.user) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: dataQuery.appId,
+ });
if (!ability.can('runQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -166,7 +176,9 @@ export class DataQueriesController {
};
if (dataQueryEntity.dataSource) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: dataQueryEntity.dataSource.appId,
+ });
if (!ability.can('previewQuery', dataQueryEntity.dataSource.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
diff --git a/server/src/controllers/data_sources.controller.ts b/server/src/controllers/data_sources.controller.ts
index a0f3d7a4a8..9d5a0833b2 100644
--- a/server/src/controllers/data_sources.controller.ts
+++ b/server/src/controllers/data_sources.controller.ts
@@ -19,7 +19,9 @@ export class DataSourcesController {
@Get()
async index(@Request() req, @Query() query) {
const app = await this.appsService.find(query.app_id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: app.id,
+ });
if (!ability.can('getDataSources', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -38,7 +40,9 @@ export class DataSourcesController {
const appId = req.body.app_id;
const app = await this.appsService.find(appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: appId,
+ });
if (!ability.can('createDataSource', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -57,7 +61,9 @@ export class DataSourcesController {
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
const app = await this.appsService.find(dataSource.appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: app.id,
+ });
if (!ability.can('updateDataSource', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -90,7 +96,9 @@ export class DataSourcesController {
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
const app = await this.appsService.find(dataSource.appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ const ability = await this.appsAbilityFactory.appsActions(req.user, {
+ id: app.id,
+ });
if (!ability.can('authorizeOauthForSource', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
diff --git a/server/src/controllers/group_permissions.controller.ts b/server/src/controllers/group_permissions.controller.ts
new file mode 100644
index 0000000000..2e70df219a
--- /dev/null
+++ b/server/src/controllers/group_permissions.controller.ts
@@ -0,0 +1,109 @@
+import { Controller, Post, Get, Put, Delete, Request, UseGuards, Param } from '@nestjs/common';
+
+import { decamelizeKeys } from 'humps';
+import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
+import { GroupPermissionsService } from '../services/group_permissions.service';
+import { PoliciesGuard } from 'src/modules/casl/policies.guard';
+import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
+import { AppAbility } from 'src/modules/casl/casl-ability.factory';
+import { User } from 'src/entities/user.entity';
+
+@Controller('group_permissions')
+export class GroupPermissionsController {
+ constructor(private groupPermissionsService: GroupPermissionsService) {}
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Post()
+ async create(@Request() req) {
+ const groupPermission = await this.groupPermissionsService.create(req.user, req.body.group);
+
+ return decamelizeKeys(groupPermission);
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Get(':id')
+ async show(@Request() req, @Param() params) {
+ const groupPermission = await this.groupPermissionsService.findOne(req.user, params.id);
+
+ return decamelizeKeys(groupPermission);
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Put(':id/app_group_permissions/:appGroupPermissionId')
+ async updateAppGroupPermission(@Request() req, @Param() params) {
+ const groupPermission = await this.groupPermissionsService.updateAppGroupPermission(
+ req.user,
+ params.id,
+ params.appGroupPermissionId,
+ req.body.actions
+ );
+
+ return decamelizeKeys(groupPermission);
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Put(':id')
+ async update(@Request() req, @Param() params) {
+ const groupPermission = await this.groupPermissionsService.update(req.user, params.id, req.body);
+
+ return decamelizeKeys(groupPermission);
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Get()
+ async index(@Request() req) {
+ const groupPermissions = await this.groupPermissionsService.findAll(req.user);
+
+ return decamelizeKeys({ groupPermissions });
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Delete(':id')
+ async destroy(@Request() req, @Param() params) {
+ const groupPermission = await this.groupPermissionsService.destroy(req.user, params.id);
+
+ return decamelizeKeys(groupPermission);
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Get(':id/apps')
+ async apps(@Request() req, @Param() params) {
+ const apps = await this.groupPermissionsService.findApps(req.user, params.id);
+
+ return decamelizeKeys({ apps });
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Get(':id/addable_apps')
+ async addableApps(@Request() req, @Param() params) {
+ const apps = await this.groupPermissionsService.findAddableApps(req.user, params.id);
+
+ return decamelizeKeys({ apps });
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Get(':id/users')
+ async users(@Request() req, @Param() params) {
+ const users = await this.groupPermissionsService.findUsers(req.user, params.id);
+
+ return decamelizeKeys({ users });
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @Get(':id/addable_users')
+ async addableUsers(@Request() req, @Param() params) {
+ const users = await this.groupPermissionsService.findAddableUsers(req.user, params.id);
+
+ return decamelizeKeys({ users });
+ }
+}
diff --git a/server/src/entities/app.entity.ts b/server/src/entities/app.entity.ts
index 61399a6a3b..4ddce83e55 100644
--- a/server/src/entities/app.entity.ts
+++ b/server/src/entities/app.entity.ts
@@ -9,11 +9,17 @@ import {
OneToMany,
AfterLoad,
BaseEntity,
+ ManyToMany,
+ JoinTable,
+ AfterInsert,
+ getRepository,
} from 'typeorm';
import { User } from './user.entity';
import { AppVersion } from './app_version.entity';
import { DataQuery } from './data_query.entity';
import { DataSource } from './data_source.entity';
+import { GroupPermission } from './group_permission.entity';
+import { AppGroupPermission } from './app_group_permission.entity';
@Entity({ name: 'apps' })
export class App extends BaseEntity {
@@ -45,17 +51,47 @@ export class App extends BaseEntity {
@JoinColumn({ name: 'user_id' })
user: User;
- @OneToMany(() => AppVersion, (appVersion) => appVersion.app, { eager: true, onDelete: 'CASCADE' })
+ @OneToMany(() => AppVersion, (appVersion) => appVersion.app, {
+ eager: true,
+ onDelete: 'CASCADE',
+ })
appVersions: AppVersion[];
- @OneToMany(() => DataQuery, (dataQuery) => dataQuery.app, { onDelete: 'CASCADE' })
+ @OneToMany(() => DataQuery, (dataQuery) => dataQuery.app, {
+ onDelete: 'CASCADE',
+ })
dataQueries: DataQuery[];
- @OneToMany(() => DataSource, (dataSource) => dataSource.app, { onDelete: 'CASCADE' })
+ @OneToMany(() => DataSource, (dataSource) => dataSource.app, {
+ onDelete: 'CASCADE',
+ })
dataSources: DataSource[];
+ @ManyToMany(() => GroupPermission)
+ @JoinTable({
+ name: 'app_group_permissions',
+ joinColumn: {
+ name: 'app_id',
+ },
+ inverseJoinColumn: {
+ name: 'group_permission_id',
+ },
+ })
+ groupPermissions: GroupPermission[];
+
+ @OneToMany(() => AppGroupPermission, (appGroupPermission) => appGroupPermission.app, { onDelete: 'CASCADE' })
+ appGroupPermissions: AppGroupPermission[];
+
public editingVersion;
+ @AfterInsert()
+ updateSlug(): void {
+ if (!this.slug) {
+ const appRepository = getRepository(App);
+ appRepository.update(this.id, { slug: this.id });
+ }
+ }
+
@AfterLoad()
async afterLoad(): Promise {
if (this.currentVersionId) {
diff --git a/server/src/entities/app_group_permission.entity.ts b/server/src/entities/app_group_permission.entity.ts
new file mode 100644
index 0000000000..83e289af45
--- /dev/null
+++ b/server/src/entities/app_group_permission.entity.ts
@@ -0,0 +1,47 @@
+import {
+ BaseEntity,
+ Column,
+ CreateDateColumn,
+ Entity,
+ JoinColumn,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { GroupPermission } from './group_permission.entity';
+import { App } from './app.entity';
+
+@Entity({ name: 'app_group_permissions' })
+export class AppGroupPermission extends BaseEntity {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ name: 'app_id' })
+ appId: string;
+
+ @Column({ name: 'group_permission_id' })
+ groupPermissionId: string;
+
+ @Column({ default: false })
+ read: boolean;
+
+ @Column({ default: false })
+ update: boolean;
+
+ @Column({ default: false })
+ delete: boolean;
+
+ @CreateDateColumn({ default: () => 'now()', name: 'created_at' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
+ updatedAt: Date;
+
+ @ManyToOne(() => App, (app) => app.id)
+ @JoinColumn({ name: 'app_id' })
+ app: App;
+
+ @ManyToOne(() => GroupPermission, (groupPermission) => groupPermission.id)
+ @JoinColumn({ name: 'group_permission_id' })
+ groupPermission: GroupPermission;
+}
diff --git a/server/src/entities/group_permission.entity.ts b/server/src/entities/group_permission.entity.ts
new file mode 100644
index 0000000000..d0d8ffd514
--- /dev/null
+++ b/server/src/entities/group_permission.entity.ts
@@ -0,0 +1,70 @@
+import {
+ BaseEntity,
+ Column,
+ CreateDateColumn,
+ Entity,
+ JoinColumn,
+ JoinTable,
+ ManyToMany,
+ ManyToOne,
+ OneToMany,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { App } from './app.entity';
+import { AppGroupPermission } from './app_group_permission.entity';
+import { Organization } from './organization.entity';
+import { User } from './user.entity';
+import { UserGroupPermission } from './user_group_permission.entity';
+
+@Entity({ name: 'group_permissions' })
+export class GroupPermission extends BaseEntity {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ name: 'organization_id' })
+ organizationId: string;
+
+ @Column()
+ group: string;
+
+ @CreateDateColumn({ default: () => 'now()', name: 'created_at' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
+ updatedAt: Date;
+
+ @ManyToOne(() => Organization, (organization) => organization.id)
+ @JoinColumn({ name: 'organization_id' })
+ organization: Organization;
+
+ @OneToMany(() => UserGroupPermission, (userGroupPermission) => userGroupPermission.groupPermission)
+ userGroupPermission: UserGroupPermission[];
+
+ @OneToMany(() => AppGroupPermission, (appGroupPermission) => appGroupPermission.groupPermission)
+ appGroupPermission: AppGroupPermission[];
+
+ @ManyToMany(() => User)
+ @JoinTable({
+ name: 'user_group_permissions',
+ joinColumn: {
+ name: 'group_permission_id',
+ },
+ inverseJoinColumn: {
+ name: 'user_id',
+ },
+ })
+ users: Promise;
+
+ @ManyToMany(() => App)
+ @JoinTable({
+ name: 'app_group_permissions',
+ joinColumn: {
+ name: 'group_permission_id',
+ },
+ inverseJoinColumn: {
+ name: 'app_id',
+ },
+ })
+ apps: Promise;
+}
diff --git a/server/src/entities/organization.entity.ts b/server/src/entities/organization.entity.ts
index e49c9c2aa7..b81630b723 100644
--- a/server/src/entities/organization.entity.ts
+++ b/server/src/entities/organization.entity.ts
@@ -1,4 +1,14 @@
-import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import {
+ Entity,
+ Column,
+ PrimaryGeneratedColumn,
+ CreateDateColumn,
+ UpdateDateColumn,
+ OneToMany,
+ JoinColumn,
+} from 'typeorm';
+import { GroupPermission } from './group_permission.entity';
+import { User } from './user.entity';
@Entity({ name: 'organizations' })
export class Organization {
@@ -16,4 +26,12 @@ export class Organization {
@UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
updatedAt: Date;
+
+ @OneToMany(() => GroupPermission, (groupPermission) => groupPermission.organization, { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'organization_id' })
+ groupPermissions: GroupPermission[];
+
+ @OneToMany(() => User, (user) => user.organization)
+ @JoinColumn({ name: 'organization_id' })
+ users: User[];
}
diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts
index f0de62cf00..54432eab43 100644
--- a/server/src/entities/user.entity.ts
+++ b/server/src/entities/user.entity.ts
@@ -9,18 +9,21 @@ import {
OneToMany,
ManyToOne,
JoinColumn,
- AfterLoad,
BaseEntity,
+ ManyToMany,
+ JoinTable,
} from 'typeorm';
+import { GroupPermission } from './group_permission.entity';
import { Organization } from './organization.entity';
const bcrypt = require('bcrypt');
import { OrganizationUser } from './organization_user.entity';
+import { UserGroupPermission } from './user_group_permission.entity';
@Entity({ name: 'users' })
export class User extends BaseEntity {
@BeforeInsert()
@BeforeUpdate()
- hashPassword() {
+ hashPassword(): void {
if (this.password) {
this.password = bcrypt.hashSync(this.password, 10);
}
@@ -63,14 +66,18 @@ export class User extends BaseEntity {
@JoinColumn({ name: 'organization_id' })
organization: Organization;
- public isAdmin;
- public isDeveloper;
- public role;
+ @ManyToMany(() => GroupPermission)
+ @JoinTable({
+ name: 'user_group_permissions',
+ joinColumn: {
+ name: 'user_id',
+ },
+ inverseJoinColumn: {
+ name: 'group_permission_id',
+ },
+ })
+ groupPermissions: Promise;
- @AfterLoad()
- computeUserRole(): void {
- this.isAdmin = this.organizationUsers[0].role === 'admin';
- this.isDeveloper = this.organizationUsers[0].role === 'developer';
- this.role = this.organizationUsers[0].role;
- }
+ @OneToMany(() => UserGroupPermission, (userGroupPermission) => userGroupPermission.user, { onDelete: 'CASCADE' })
+ userGroupPermissions: UserGroupPermission[];
}
diff --git a/server/src/entities/user_group_permission.entity.ts b/server/src/entities/user_group_permission.entity.ts
new file mode 100644
index 0000000000..d0047eee2f
--- /dev/null
+++ b/server/src/entities/user_group_permission.entity.ts
@@ -0,0 +1,38 @@
+import {
+ BaseEntity,
+ Column,
+ CreateDateColumn,
+ Entity,
+ JoinColumn,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+ UpdateDateColumn,
+} from 'typeorm';
+import { GroupPermission } from './group_permission.entity';
+import { User } from './user.entity';
+
+@Entity({ name: 'user_group_permissions' })
+export class UserGroupPermission extends BaseEntity {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ name: 'user_id' })
+ userId: string;
+
+ @Column({ name: 'group_permission_id' })
+ groupPermissionId: string;
+
+ @CreateDateColumn({ default: () => 'now()', name: 'created_at' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ default: () => 'now()', name: 'updated_at' })
+ updatedAt: Date;
+
+ @ManyToOne(() => User, (user) => user.id)
+ @JoinColumn({ name: 'user_id' })
+ user: User;
+
+ @ManyToOne(() => GroupPermission, (groupPermission) => groupPermission.id)
+ @JoinColumn({ name: 'group_permission_id' })
+ groupPermission: GroupPermission;
+}
diff --git a/server/src/modules/apps/apps.module.ts b/server/src/modules/apps/apps.module.ts
index 9ae5c5a5d8..ff32845a53 100644
--- a/server/src/modules/apps/apps.module.ts
+++ b/server/src/modules/apps/apps.module.ts
@@ -18,6 +18,10 @@ import { Folder } from 'src/entities/folder.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
import { DataSource } from 'src/entities/data_source.entity';
import { AppCloneService } from '@services/app_clone.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { AppImportExportService } from '@services/app_import_export.service';
@Module({
imports: [
@@ -32,10 +36,13 @@ import { AppCloneService } from '@services/app_clone.service';
User,
Organization,
DataSource,
+ GroupPermission,
+ AppGroupPermission,
+ UserGroupPermission,
]),
CaslModule,
],
- providers: [AppsService, AppUsersService, UsersService, FoldersService, AppCloneService],
+ providers: [AppsService, AppUsersService, UsersService, FoldersService, AppCloneService, AppImportExportService],
controllers: [AppsController, AppUsersController],
})
export class AppsModule {}
diff --git a/server/src/modules/auth/auth.module.ts b/server/src/modules/auth/auth.module.ts
index baa0487b4c..eb289f1bed 100644
--- a/server/src/modules/auth/auth.module.ts
+++ b/server/src/modules/auth/auth.module.ts
@@ -13,12 +13,13 @@ import { OrganizationsService } from 'src/services/organizations.service';
import { OrganizationUsersService } from 'src/services/organization_users.service';
import { ConfigService } from '@nestjs/config';
import { EmailService } from '@services/email.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
@Module({
imports: [
UsersModule,
PassportModule,
- TypeOrmModule.forFeature([User, Organization, OrganizationUser]),
+ TypeOrmModule.forFeature([User, Organization, OrganizationUser, GroupPermission]),
JwtModule.registerAsync({
useFactory: (config: ConfigService) => {
return {
diff --git a/server/src/modules/casl/abilities/apps-ability.factory.ts b/server/src/modules/casl/abilities/apps-ability.factory.ts
index b4adf7244d..0309bcb3a4 100644
--- a/server/src/modules/casl/abilities/apps-ability.factory.ts
+++ b/server/src/modules/casl/abilities/apps-ability.factory.ts
@@ -3,6 +3,7 @@ import { InferSubjects, AbilityBuilder, Ability, AbilityClass, ExtractSubjectTyp
import { Injectable } from '@nestjs/common';
import { App } from 'src/entities/app.entity';
import { AppVersion } from 'src/entities/app_version.entity';
+import { UsersService } from 'src/services/users.service';
type Actions =
| 'authorizeOauthForSource'
@@ -32,21 +33,36 @@ export type AppsAbility = Ability<[Actions, Subjects]>;
@Injectable()
export class AppsAbilityFactory {
+ constructor(private usersService: UsersService) {}
+
async appsActions(user: User, params: any) {
const { can, build } = new AbilityBuilder>(Ability as AbilityClass);
- // Only admins can update app params such as name, friendly url & visibility
- if (user.isAdmin) {
- can('updateParams', App, { organizationId: user.organizationId });
+ if (await this.usersService.userCan(user, 'create', 'App')) {
can('createUsers', App, { organizationId: user.organizationId });
- can('deleteApp', App, { organizationId: user.organizationId });
- }
-
- // Only developers and admins can create new versions
- if (user.isAdmin || user.isDeveloper) {
can('createApp', App);
can('cloneApp', App, { organizationId: user.organizationId });
+ }
+ if (await this.usersService.userCan(user, 'read', 'App', params.id)) {
+ can('viewApp', App, { organizationId: user.organizationId });
+
+ can('fetchUsers', App, { organizationId: user.organizationId });
+ can('fetchVersions', App, { organizationId: user.organizationId });
+
+ can('runQuery', App, { organizationId: user.organizationId });
+ can('getQueries', App, { organizationId: user.organizationId });
+ can('previewQuery', App, { organizationId: user.organizationId });
+
+ // policies for datasources
+ can('getDataSources', App, { organizationId: user.organizationId });
+ can('authorizeOauthForSource', App, {
+ organizationId: user.organizationId,
+ });
+ }
+
+ if (await this.usersService.userCan(user, 'update', 'App', params.id)) {
+ can('updateParams', App, { organizationId: user.organizationId });
can('createVersions', App, { organizationId: user.organizationId });
can('updateVersions', App, { organizationId: user.organizationId });
@@ -58,24 +74,12 @@ export class AppsAbilityFactory {
can('createDataSource', App, { organizationId: user.organizationId });
}
- // All organization users can view the app users
- can('fetchUsers', App, { organizationId: user.organizationId });
- can('fetchVersions', App, { organizationId: user.organizationId });
+ if (await this.usersService.userCan(user, 'delete', 'App', params.id)) {
+ can('deleteApp', App, { organizationId: user.organizationId });
+ }
- // Can view public apps
can('viewApp', App, { isPublic: true });
- can('viewApp', App, { organizationId: user.organizationId });
-
- // if app belongs to org, queries can be run
- can('runQuery', App, { organizationId: user.organizationId });
- can('getQueries', App, { organizationId: user.organizationId });
- can('previewQuery', App, { organizationId: user.organizationId });
-
- // policies for datasources
- can('getDataSources', App, { organizationId: user.organizationId });
- can('authorizeOauthForSource', App, {
- organizationId: user.organizationId,
- });
+ can('runQuery', App, { isPublic: true });
return build({
detectSubjectType: (item) => item.constructor as ExtractSubjectType,
diff --git a/server/src/modules/casl/casl-ability.factory.spec.ts b/server/src/modules/casl/casl-ability.factory.spec.ts
deleted file mode 100644
index 613b677bb6..0000000000
--- a/server/src/modules/casl/casl-ability.factory.spec.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { CaslAbilityFactory } from './casl-ability.factory';
-
-describe('CaslAbilityFactory', () => {
- it('should be defined', () => {
- expect(new CaslAbilityFactory()).toBeDefined();
- });
-});
diff --git a/server/src/modules/casl/casl-ability.factory.ts b/server/src/modules/casl/casl-ability.factory.ts
index 7d806197a1..2181e4c2ec 100644
--- a/server/src/modules/casl/casl-ability.factory.ts
+++ b/server/src/modules/casl/casl-ability.factory.ts
@@ -2,9 +2,9 @@ import { User } from 'src/entities/user.entity';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { InferSubjects, AbilityBuilder, Ability, AbilityClass, ExtractSubjectType } from '@casl/ability';
import { Injectable } from '@nestjs/common';
-import { OrganizationUsersService } from '@services/organization_users.service';
+import { UsersService } from '@services/users.service';
-type Actions = 'changeRole' | 'archiveUser' | 'inviteUser';
+type Actions = 'changeRole' | 'archiveUser' | 'inviteUser' | 'accessGroupPermission';
type Subjects = InferSubjects | 'all';
@@ -12,29 +12,21 @@ export type AppAbility = Ability<[Actions, Subjects]>;
@Injectable()
export class CaslAbilityFactory {
- constructor(private organizationUsersService: OrganizationUsersService) {}
+ constructor(private usersService: UsersService) {}
async organizationUserActions(user: User, params: any) {
const { can, build } = new AbilityBuilder>(Ability as AbilityClass);
- const currentUserBelongsToSameOrg = await this.isSameOrganisation(user, params);
-
- if (user.isAdmin) can('inviteUser', User);
- if (user.isAdmin && currentUserBelongsToSameOrg) {
+ const isAdmin = await this.usersService.hasGroup(user, 'admin');
+ if (isAdmin) {
+ can('inviteUser', User);
can('archiveUser', User);
can('changeRole', User);
+ can('accessGroupPermission', User);
}
return build({
detectSubjectType: (item) => item.constructor as ExtractSubjectType,
});
}
-
- async isSameOrganisation(currentUser, params) {
- if (!params.id) return false;
- const organizationUser = await this.organizationUsersService.findOne(params.id);
- if (!organizationUser) return false;
-
- return organizationUser.organizationId === currentUser.organizationId;
- }
}
diff --git a/server/src/modules/data_queries/data_queries.module.ts b/server/src/modules/data_queries/data_queries.module.ts
index 551c1a0124..3deaa0a82d 100644
--- a/server/src/modules/data_queries/data_queries.module.ts
+++ b/server/src/modules/data_queries/data_queries.module.ts
@@ -15,10 +15,29 @@ import { AppVersion } from 'src/entities/app_version.entity';
import { AppUser } from 'src/entities/app_user.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
import { AppCloneService } from '@services/app_clone.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UsersService } from '@services/users.service';
+import { User } from 'src/entities/user.entity';
+import { OrganizationUser } from 'src/entities/organization_user.entity';
+import { Organization } from 'src/entities/organization.entity';
@Module({
imports: [
- TypeOrmModule.forFeature([App, AppVersion, AppUser, DataQuery, Credential, DataSource, FolderApp]),
+ TypeOrmModule.forFeature([
+ App,
+ AppVersion,
+ AppUser,
+ DataQuery,
+ Credential,
+ DataSource,
+ FolderApp,
+ GroupPermission,
+ AppGroupPermission,
+ User,
+ OrganizationUser,
+ Organization,
+ ]),
CaslModule,
],
providers: [
@@ -28,6 +47,7 @@ import { AppCloneService } from '@services/app_clone.service';
DataSourcesService,
AppsService,
AppCloneService,
+ UsersService,
],
controllers: [DataQueriesController],
})
diff --git a/server/src/modules/data_sources/data_sources.module.ts b/server/src/modules/data_sources/data_sources.module.ts
index 2194ced1ce..ef6072ac52 100644
--- a/server/src/modules/data_sources/data_sources.module.ts
+++ b/server/src/modules/data_sources/data_sources.module.ts
@@ -15,10 +15,29 @@ import { DataQueriesService } from '@services/data_queries.service';
import { DataQuery } from 'src/entities/data_query.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
import { AppCloneService } from '@services/app_clone.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UsersService } from '@services/users.service';
+import { User } from 'src/entities/user.entity';
+import { OrganizationUser } from 'src/entities/organization_user.entity';
+import { Organization } from 'src/entities/organization.entity';
@Module({
imports: [
- TypeOrmModule.forFeature([DataSource, DataQuery, Credential, App, AppVersion, AppUser, FolderApp]),
+ TypeOrmModule.forFeature([
+ DataSource,
+ DataQuery,
+ Credential,
+ App,
+ AppVersion,
+ AppUser,
+ FolderApp,
+ GroupPermission,
+ AppGroupPermission,
+ User,
+ OrganizationUser,
+ Organization,
+ ]),
CaslModule,
],
providers: [
@@ -28,6 +47,7 @@ import { AppCloneService } from '@services/app_clone.service';
AppsService,
DataQueriesService,
AppCloneService,
+ UsersService,
],
controllers: [DataSourcesController],
})
diff --git a/server/src/modules/folders/folders.module.ts b/server/src/modules/folders/folders.module.ts
index 9c9bdf1c56..75b6839ec5 100644
--- a/server/src/modules/folders/folders.module.ts
+++ b/server/src/modules/folders/folders.module.ts
@@ -5,10 +5,14 @@ import { Folder } from '../../entities/folder.entity';
import { FoldersController } from '../../controllers/folders.controller';
import { FoldersService } from '../../services/folders.service';
import { App } from 'src/entities/app.entity';
+import { UsersService } from '@services/users.service';
+import { User } from 'src/entities/user.entity';
+import { OrganizationUser } from 'src/entities/organization_user.entity';
+import { Organization } from 'src/entities/organization.entity';
@Module({
controllers: [FoldersController],
- imports: [TypeOrmModule.forFeature([App, Folder, FolderApp])],
- providers: [FoldersService],
+ imports: [TypeOrmModule.forFeature([App, Folder, FolderApp, User, OrganizationUser, Organization])],
+ providers: [FoldersService, UsersService],
})
export class FoldersModule {}
diff --git a/server/src/modules/group_permissions/group_permissions.module.ts b/server/src/modules/group_permissions/group_permissions.module.ts
new file mode 100644
index 0000000000..85473dd9f9
--- /dev/null
+++ b/server/src/modules/group_permissions/group_permissions.module.ts
@@ -0,0 +1,31 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { GroupPermission } from '../../../src/entities/group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { GroupPermissionsController } from '../../controllers/group_permissions.controller';
+import { GroupPermissionsService } from '../../services/group_permissions.service';
+import { CaslModule } from '../casl/casl.module';
+import { UsersService } from '@services/users.service';
+import { User } from 'src/entities/user.entity';
+import { OrganizationUser } from 'src/entities/organization_user.entity';
+import { Organization } from 'src/entities/organization.entity';
+import { App } from 'src/entities/app.entity';
+
+@Module({
+ controllers: [GroupPermissionsController],
+ imports: [
+ TypeOrmModule.forFeature([
+ GroupPermission,
+ UserGroupPermission,
+ AppGroupPermission,
+ User,
+ OrganizationUser,
+ Organization,
+ App,
+ ]),
+ CaslModule,
+ ],
+ providers: [GroupPermissionsService, UsersService],
+})
+export class GroupPermissionsModule {}
diff --git a/server/src/modules/organizations/organizations.module.ts b/server/src/modules/organizations/organizations.module.ts
index 7ab5123cfa..0d67116467 100644
--- a/server/src/modules/organizations/organizations.module.ts
+++ b/server/src/modules/organizations/organizations.module.ts
@@ -10,9 +10,10 @@ import { OrganizationUsersController } from '@controllers/organization_users.con
import { UsersService } from 'src/services/users.service';
import { CaslModule } from '../casl/casl.module';
import { EmailService } from '@services/email.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
@Module({
- imports: [TypeOrmModule.forFeature([Organization, OrganizationUser, User]), CaslModule],
+ imports: [TypeOrmModule.forFeature([Organization, OrganizationUser, User, GroupPermission]), CaslModule],
providers: [OrganizationsService, OrganizationUsersService, UsersService, EmailService],
controllers: [OrganizationsController, OrganizationUsersController],
})
diff --git a/server/src/services/app.service.ts b/server/src/services/app.service.ts
deleted file mode 100644
index 927d7cca0b..0000000000
--- a/server/src/services/app.service.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Injectable } from '@nestjs/common';
-
-@Injectable()
-export class AppService {
- getHello(): string {
- return 'Hello World!';
- }
-}
diff --git a/server/src/services/app_clone.service.ts b/server/src/services/app_clone.service.ts
index 1d919e3f72..8556d505e6 100644
--- a/server/src/services/app_clone.service.ts
+++ b/server/src/services/app_clone.service.ts
@@ -7,6 +7,8 @@ import { AppVersion } from 'src/entities/app_version.entity';
import { DataSource } from 'src/entities/data_source.entity';
import { DataQuery } from 'src/entities/data_query.entity';
import { Credential } from 'src/entities/credential.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
@Injectable()
export class AppCloneService {
@@ -18,6 +20,7 @@ export class AppCloneService {
await this.entityManager.transaction(async (manager) => {
clonedApp = await this.createClonedAppForUser(manager, existingApp, user);
await this.buildClonedAppAssociations(manager, clonedApp, existingApp);
+ await this.createAdminGroupPermissions(manager, clonedApp);
});
return clonedApp;
@@ -40,7 +43,7 @@ export class AppCloneService {
return newApp;
}
- async buildClonedAppAssociations(manager, newApp: App, existingApp: App) {
+ async buildClonedAppAssociations(manager: EntityManager, newApp: App, existingApp: App) {
const dataSourceMapping = {};
const newDefinition = existingApp.editingVersion?.definition;
@@ -90,13 +93,13 @@ export class AppCloneService {
});
}
- async cloneOptionsWithNewCredentials(manager, options) {
+ async cloneOptionsWithNewCredentials(manager: EntityManager, options: any) {
for (const key of Object.keys(options)) {
if ('credential_id' in options[key]) {
const existingCredential = await manager.findOne(Credential, {
id: options[key]['credential_id'],
});
- const newCredential = await manager.create(Credential, {
+ const newCredential = manager.create(Credential, {
valueCiphertext: existingCredential.valueCiphertext,
});
await manager.save(newCredential);
@@ -106,4 +109,29 @@ export class AppCloneService {
return options;
}
+
+ async createAdminGroupPermissions(manager: EntityManager, app: App) {
+ const orgDefaultGroupPermissions = await manager.find(GroupPermission, {
+ where: {
+ organizationId: app.organizationId,
+ group: 'admin',
+ },
+ });
+
+ const adminPermissions = {
+ read: true,
+ update: true,
+ delete: true,
+ };
+
+ for (const groupPermission of orgDefaultGroupPermissions) {
+ const appGroupPermission = manager.create(AppGroupPermission, {
+ groupPermissionId: groupPermission.id,
+ appId: app.id,
+ ...adminPermissions,
+ });
+
+ return await manager.save(AppGroupPermission, appGroupPermission);
+ }
+ }
}
diff --git a/server/src/services/app_config.service.ts b/server/src/services/app_config.service.ts
index 466ad7fea9..3e144012ee 100644
--- a/server/src/services/app_config.service.ts
+++ b/server/src/services/app_config.service.ts
@@ -2,8 +2,6 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class AppConfigService {
- constructor() {}
-
async public_config() {
const whitelistedConfigVars = process.env.ALLOWED_CLIENT_CONFIG_VARS
? this.fetchAllowedConfigFromEnv()
diff --git a/server/src/services/app_import_export.service.ts b/server/src/services/app_import_export.service.ts
new file mode 100644
index 0000000000..5d4b12088b
--- /dev/null
+++ b/server/src/services/app_import_export.service.ts
@@ -0,0 +1,156 @@
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { App } from 'src/entities/app.entity';
+import { EntityManager, Repository } from 'typeorm';
+import { User } from 'src/entities/user.entity';
+import { DataSource } from 'src/entities/data_source.entity';
+import { DataQuery } from 'src/entities/data_query.entity';
+import { AppVersion } from 'src/entities/app_version.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { Credential } from 'src/entities/credential.entity';
+
+@Injectable()
+export class AppImportExportService {
+ constructor(
+ @InjectRepository(App)
+ private appsRepository: Repository,
+ private readonly entityManager: EntityManager
+ ) {}
+
+ async export(user: User, id: string): Promise {
+ const appToExport = this.appsRepository.findOne(id, {
+ relations: ['dataQueries', 'dataSources', 'appVersions'],
+ });
+
+ return appToExport;
+ }
+
+ async import(user: User, appParams: any): Promise {
+ if (typeof appParams !== 'object') {
+ throw new BadRequestException('Invalid params for app import');
+ }
+
+ let importedApp: App;
+
+ await this.entityManager.transaction(async (manager) => {
+ importedApp = await this.createImportedAppForUser(manager, appParams, user);
+ await this.buildImportedAppAssociations(manager, importedApp, appParams);
+ await this.createAdminGroupPermissions(manager, importedApp);
+ });
+
+ // FIXME: App slug updation callback doesnt work while wrapped in transaction
+ // hence updating slug explicitly
+ await importedApp.reload();
+ importedApp.slug = importedApp.id;
+ await this.entityManager.save(importedApp);
+
+ return importedApp;
+ }
+
+ async createImportedAppForUser(manager: EntityManager, appParams: any, user: User): Promise {
+ const importedApp = manager.create(App, {
+ name: appParams.name,
+ organizationId: user.organizationId,
+ user: user,
+ slug: null, // Prevent db unique constraint error. App entity afterload callback will set this as id.
+ isPublic: true,
+ });
+ await manager.save(importedApp);
+ return importedApp;
+ }
+
+ async buildImportedAppAssociations(manager: EntityManager, importedApp: App, appParams: any) {
+ const dataSourceMapping = {};
+ let currentVersionId: string;
+ const dataSources = appParams?.dataSources || [];
+ const dataQueries = appParams?.dataQueries || [];
+ const appVersions = appParams?.appVersions || [];
+
+ for (const source of dataSources) {
+ const newOptions = await this.copyOptionsWithNewCredentials(manager, source.options);
+
+ const newSource = manager.create(DataSource, {
+ app: importedApp,
+ name: source.name,
+ kind: source.kind,
+ options: newOptions,
+ });
+
+ await manager.save(newSource);
+ dataSourceMapping[source.id] = newSource.id;
+ }
+
+ for (const query of dataQueries) {
+ const newQuery = manager.create(DataQuery, {
+ app: importedApp,
+ name: query.name,
+ options: query.options,
+ kind: query.kind,
+ dataSourceId: dataSourceMapping[query.dataSourceId],
+ });
+ await manager.save(newQuery);
+ }
+
+ for (const appVersion of appVersions) {
+ const version = manager.create(AppVersion, {
+ app: importedApp,
+ definition: appVersion.definition,
+ name: appVersion.name,
+ });
+
+ await manager.save(version);
+
+ if (appVersion.id == appParams.currentVersionId) {
+ currentVersionId = version.id;
+
+ await manager.update(App, importedApp, { currentVersionId });
+ }
+ }
+ }
+
+ async copyOptionsWithNewCredentials(manager: EntityManager, options: any) {
+ for (const key of Object.keys(options)) {
+ if ('credential_id' in options[key]) {
+ const existingCredential = await manager.findOne(Credential, {
+ id: options[key]['credential_id'],
+ });
+
+ if (existingCredential) {
+ const newCredential = manager.create(Credential, {
+ valueCiphertext: existingCredential.valueCiphertext,
+ });
+ await manager.save(newCredential);
+ options[key]['credential_id'] = newCredential.id;
+ }
+ }
+ }
+
+ return options;
+ }
+
+ async createAdminGroupPermissions(manager: EntityManager, app: App) {
+ const orgDefaultGroupPermissions = await manager.find(GroupPermission, {
+ where: {
+ organizationId: app.organizationId,
+ group: 'admin',
+ },
+ });
+
+ const adminPermissions = {
+ read: true,
+ update: true,
+ delete: true,
+ };
+
+ for (const groupPermission of orgDefaultGroupPermissions) {
+ const appGroupPermission = manager.create(AppGroupPermission, {
+ groupPermissionId: groupPermission.id,
+ appId: app.id,
+ ...adminPermissions,
+ });
+
+ return await manager.save(AppGroupPermission, appGroupPermission);
+ }
+ }
+}
diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts
index 0e58737557..d7801b100a 100644
--- a/server/src/services/apps.service.ts
+++ b/server/src/services/apps.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { App } from 'src/entities/app.entity';
-import { Repository } from 'typeorm';
+import { createQueryBuilder, Repository } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { AppUser } from 'src/entities/app_user.entity';
import { AppVersion } from 'src/entities/app_version.entity';
@@ -10,6 +10,10 @@ import { DataSource } from 'src/entities/data_source.entity';
import { DataQuery } from 'src/entities/data_query.entity';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { AppCloneService } from './app_clone.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { UsersService } from './users.service';
@Injectable()
export class AppsService {
@@ -32,7 +36,14 @@ export class AppsService {
@InjectRepository(FolderApp)
private folderAppsRepository: Repository,
- private AppCloneService: AppCloneService
+ @InjectRepository(GroupPermission)
+ private groupPermissionsRepository: Repository,
+
+ @InjectRepository(AppGroupPermission)
+ private appGroupPermissionsRepository: Repository,
+
+ private AppCloneService: AppCloneService,
+ private usersService: UsersService
) {}
async find(id: string): Promise {
@@ -77,35 +88,99 @@ export class AppsService {
})
);
+ await this.createAdminGroupPermissions(app);
+
return app;
}
+ async createAdminGroupPermissions(app: App) {
+ const orgDefaultGroupPermissions = await this.groupPermissionsRepository.find({
+ where: {
+ organizationId: app.organizationId,
+ group: 'admin',
+ },
+ });
+
+ for (const groupPermission of orgDefaultGroupPermissions) {
+ const appGroupPermission = this.appGroupPermissionsRepository.create({
+ groupPermissionId: groupPermission.id,
+ appId: app.id,
+ ...this.determineDefaultAppGroupPermissions(groupPermission.group),
+ });
+
+ await this.appGroupPermissionsRepository.save(appGroupPermission);
+ }
+ }
+
+ determineDefaultAppGroupPermissions(group: string): {
+ read: boolean;
+ update: boolean;
+ delete: boolean;
+ } {
+ switch (group) {
+ case 'all_users':
+ return { read: true, update: false, delete: false };
+ case 'admin':
+ return { read: true, update: true, delete: true };
+ default:
+ throw `${group} is not a default group`;
+ }
+ }
+
async clone(existingApp: App, user: User): Promise {
const clonedApp = await this.AppCloneService.perform(existingApp, user);
return clonedApp;
}
- async count(user: User) {
- return await this.appsRepository.count({
- where: {
+ async count(user: User): Promise {
+ return await createQueryBuilder(App, 'apps')
+ .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.is_public = :value AND apps.organization_id = :organizationId', {
+ value: true,
organizationId: user.organizationId,
- },
- });
+ })
+ .getCount();
}
async all(user: User, page: number): Promise {
- return await this.appsRepository.find({
- relations: ['user'],
- where: {
+ const viewableAppsQb = await createQueryBuilder(App, 'apps')
+ .innerJoin('apps.groupPermissions', 'group_permissions')
+ .innerJoinAndSelect('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.is_public = :value AND apps.organization_id = :organizationId', {
+ value: true,
organizationId: user.organizationId,
- },
- take: 10,
- skip: 10 * (page - 1),
- order: {
- createdAt: 'DESC',
- },
- });
+ });
+
+ // FIXME:
+ // TypeORM gives error when using query builder with order by
+ // https://github.com/typeorm/typeorm/issues/8213
+ // hence sorting results in memory
+ if (page) {
+ const viewableApps = await viewableAppsQb
+ .take(10)
+ .skip(10 * (page - 1))
+ .getMany();
+
+ return viewableApps.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
+ }
+
+ return await viewableAppsQb.orderBy('apps.created_at', 'DESC').getMany();
}
async update(user: User, appId: string, params: any) {
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 22ed319869..f3f2abcfe5 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -20,7 +20,6 @@ export class AuthService {
async validateUser(email: string, password: string): Promise {
const user = await this.usersService.findByEmail(email);
-
if (!user) return null;
const isVerified = await bcrypt.compare(password, user.password);
@@ -39,7 +38,7 @@ export class AuthService {
email: user.email,
first_name: user.firstName,
last_name: user.lastName,
- role: user.role,
+ admin: await this.usersService.hasGroup(user, 'admin'),
};
} else {
throw new UnauthorizedException('Invalid credentials');
@@ -54,9 +53,9 @@ export class AuthService {
const { email } = params;
const organization = await this.organizationsService.create('Untitled organization');
- const user = await this.usersService.create({ email }, organization);
+ const user = await this.usersService.create({ email }, organization, ['all_users', 'admin']);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const organizationUser = await this.organizationUsersService.create(user, organization, 'admin');
+ const organizationUser = await this.organizationUsersService.create(user, organization);
this.emailService.sendWelcomeEmail(user.email, user.firstName, user.invitationToken);
@@ -75,7 +74,10 @@ export class AuthService {
if (!user) {
throw new NotFoundException('Invalid token');
} else {
- this.usersService.update(user.id, { password, forgotPasswordToken: null });
+ this.usersService.update(user.id, {
+ password,
+ forgotPasswordToken: null,
+ });
}
}
}
diff --git a/server/src/services/data_queries.service.ts b/server/src/services/data_queries.service.ts
index aac1863f3e..5b4858873a 100644
--- a/server/src/services/data_queries.service.ts
+++ b/server/src/services/data_queries.service.ts
@@ -122,7 +122,7 @@ export class DataQueriesService {
}
async parseQueryOptions(object: any, options: object): Promise {
- if (typeof object === 'object') {
+ if (typeof object === 'object' && object !== null) {
for (const key of Object.keys(object)) {
object[key] = await this.parseQueryOptions(object[key], options);
}
diff --git a/server/src/services/email.service.ts b/server/src/services/email.service.ts
index 80bc864e56..862100a56f 100644
--- a/server/src/services/email.service.ts
+++ b/server/src/services/email.service.ts
@@ -15,9 +15,11 @@ export class EmailService {
}
async sendEmail(to: string, subject: string, html: string) {
+ const port = +process.env.SMTP_PORT || 587;
const transporter = nodemailer.createTransport({
host: process.env.SMTP_DOMAIN,
- port: process.env.SMTP_PORT || 587,
+ port: port,
+ secure: port == 465,
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD,
@@ -84,12 +86,14 @@ export class EmailService {
Hi ${name || ''},
+
${sender} has invited you to use ToolJet. Use the link below to set up your account and get started.
${inviteUrl}
+
Welcome aboard,
ToolJet Team
diff --git a/server/src/services/folders.service.ts b/server/src/services/folders.service.ts
index 290deb4b3c..aef1ae4525 100644
--- a/server/src/services/folders.service.ts
+++ b/server/src/services/folders.service.ts
@@ -2,9 +2,11 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { App } from 'src/entities/app.entity';
import { FolderApp } from 'src/entities/folder_app.entity';
-import { Repository } from 'typeorm';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { createQueryBuilder, Repository } from 'typeorm';
import { User } from '../../src/entities/user.entity';
import { Folder } from '../entities/folder.entity';
+import { UsersService } from './users.service';
@Injectable()
export class FoldersService {
@@ -14,7 +16,8 @@ export class FoldersService {
@InjectRepository(FolderApp)
private folderAppsRepository: Repository,
@InjectRepository(App)
- private appsRepository: Repository
+ private appsRepository: Repository,
+ private usersService: UsersService
) {}
async create(user: User, folderName): Promise {
@@ -29,15 +32,47 @@ export class FoldersService {
}
async all(user: User): Promise {
- return await this.foldersRepository.find({
- where: {
+ if (await this.usersService.hasGroup(user, 'admin')) {
+ return await this.foldersRepository.find({
+ where: {
+ organizationId: user.organizationId,
+ },
+ relations: ['folderApps'],
+ order: {
+ name: 'ASC',
+ },
+ });
+ }
+
+ 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.is_public = :value and apps.organization_id = :organizationId', {
+ value: true,
organizationId: user.organizationId,
- },
- relations: ['folderApps'],
- order: {
- name: 'ASC',
- },
- });
+ })
+ .getMany();
+ const allViewableAppIds = allViewableApps.map((app) => app.id);
+
+ return await createQueryBuilder(Folder, 'folders')
+ .innerJoinAndSelect('folders.folderApps', 'folder_apps')
+ .where('folder_apps.app_id IN(:...allViewableAppIds)', {
+ allViewableAppIds,
+ })
+ .andWhere('folders.organization_id = :organizationId', {
+ organizationId: user.organizationId,
+ })
+ .orWhere('folder_apps.app_id IS NULL')
+ .orderBy('folders.name', 'ASC')
+ .getMany();
}
async findOne(folderId: string): Promise {
@@ -45,15 +80,32 @@ export class FoldersService {
}
async userAppCount(user: User, folder: Folder) {
- const result = await this.foldersRepository
- .createQueryBuilder('folder')
- .where('id = :id', { id: folder.id })
- .loadRelationCountAndMap('folder.appCount', 'folder.apps', 'apps', (qb) =>
- qb.andWhere('apps.user_id = :user_id', { user_id: user.id })
- )
- .getMany();
+ const folderApps = await this.folderAppsRepository.find({
+ where: {
+ folderId: folder.id,
+ },
+ });
+ const folderAppIds = folderApps.map((folderApp) => folderApp.appId);
- return result[0].appCount;
+ if (folderAppIds.length == 0) {
+ return 0;
+ }
+
+ return await createQueryBuilder(App, 'apps')
+ .innerJoin('apps.groupPermissions', 'group_permissions')
+ .innerJoinAndSelect('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 })
+ .andWhere('app_group_permissions.app_id IN(:...folderAppIds)', {
+ folderAppIds,
+ })
+ .orWhere('apps.is_public = :value', { value: true })
+ .getCount();
}
async getAppsFor(user: User, folder: Folder, page: number): Promise {
@@ -62,22 +114,37 @@ export class FoldersService {
folderId: folder.id,
},
});
+ const folderAppIds = folderApps.map((folderApp) => folderApp.appId);
- const apps = await this.appsRepository.findByIds(
- folderApps.map((folderApp) => folderApp.appId),
- {
- where: {
- user,
- },
- relations: ['user'],
- take: 10,
- skip: 10 * (page - 1),
- order: {
- createdAt: 'DESC',
- },
- }
- );
+ let viewableApps: App[];
- return apps;
+ if (folderAppIds.length == 0) {
+ viewableApps = [];
+ } else {
+ viewableApps = await createQueryBuilder(App, 'apps')
+ .innerJoin('apps.groupPermissions', 'group_permissions')
+ .innerJoinAndSelect('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 })
+ .andWhere('app_group_permissions.app_id IN(:...folderAppIds)', {
+ folderAppIds,
+ })
+ .orWhere('apps.is_public = :value', { value: true })
+ .take(10)
+ .skip(10 * (page - 1))
+ // .orderBy('apps.created_at', 'DESC')
+ .getMany();
+ }
+
+ // FIXME:
+ // TypeORM gives error when using query builder with order by
+ // https://github.com/typeorm/typeorm/issues/8213
+ // hence sorting results in memory
+ return viewableApps.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
}
}
diff --git a/server/src/services/group_permissions.service.ts b/server/src/services/group_permissions.service.ts
new file mode 100644
index 0000000000..097650897e
--- /dev/null
+++ b/server/src/services/group_permissions.service.ts
@@ -0,0 +1,239 @@
+import { BadRequestException, Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository, createQueryBuilder, getManager, In, Not } from 'typeorm';
+import { User } from 'src/entities/user.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { App } from 'src/entities/app.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { UsersService } from './users.service';
+
+@Injectable()
+export class GroupPermissionsService {
+ constructor(
+ @InjectRepository(GroupPermission)
+ private groupPermissionsRepository: Repository,
+
+ @InjectRepository(AppGroupPermission)
+ private appGroupPermissionsRepository: Repository,
+
+ @InjectRepository(UserGroupPermission)
+ private userGroupPermissionsRepository: Repository,
+
+ @InjectRepository(User)
+ private userRepository: Repository,
+
+ @InjectRepository(App)
+ private appRepository: Repository,
+
+ private usersService: UsersService
+ ) {}
+
+ async create(user: User, group: string): Promise {
+ return this.groupPermissionsRepository.save(
+ this.groupPermissionsRepository.create({
+ organizationId: user.organizationId,
+ group: group,
+ })
+ );
+ }
+
+ async destroy(user: User, groupPermissionId: string) {
+ let result;
+
+ const groupPermission = await this.groupPermissionsRepository.findOne({
+ id: groupPermissionId,
+ });
+
+ if (groupPermission.group == 'admin' || groupPermission.group == 'all_users') {
+ throw new BadRequestException('Cannot delete default group');
+ }
+ getManager().transaction(async (manager) => {
+ const relationalEntitiesToBeDeleted = [AppGroupPermission, UserGroupPermission];
+
+ for (const entityToDelete of relationalEntitiesToBeDeleted) {
+ const entities = await manager.find(entityToDelete, {
+ where: { groupPermissionId },
+ });
+
+ for (const entity of entities) {
+ await manager.delete(entityToDelete, entity.id);
+ }
+ }
+
+ result = await manager.delete(GroupPermission, {
+ organizationId: user.organizationId,
+ id: groupPermissionId,
+ });
+ });
+ return result;
+ }
+
+ async updateAppGroupPermission(user: User, groupPermissionId: string, appGroupPermissionId: string, actions: any) {
+ const appGroupPermission = await this.appGroupPermissionsRepository.findOne({
+ id: appGroupPermissionId,
+ groupPermissionId: groupPermissionId,
+ });
+ const groupPermission = await this.groupPermissionsRepository.findOne({
+ id: appGroupPermission.groupPermissionId,
+ });
+
+ if (groupPermission.organizationId !== user.organizationId) {
+ throw new BadRequestException();
+ }
+ if (groupPermission.group == 'admin') {
+ throw new BadRequestException('Cannot update admin group');
+ }
+
+ return this.appGroupPermissionsRepository.update(appGroupPermissionId, actions);
+ }
+
+ async update(user: User, groupPermissionId: string, body: any) {
+ const groupPermission = await this.groupPermissionsRepository.findOne({
+ id: groupPermissionId,
+ organizationId: user.organizationId,
+ });
+
+ await this.appGroupPermissionsRepository.manager.transaction(async (manager) => {
+ if (body.remove_apps) {
+ if (groupPermission.group == 'admin') {
+ throw new BadRequestException('Cannot update admin group');
+ }
+ for (const appId of body.remove_apps) {
+ manager.delete(AppGroupPermission, {
+ appId: appId,
+ groupPermissionId: groupPermissionId,
+ });
+ }
+ }
+
+ if (body.add_apps) {
+ if (groupPermission.group == 'admin') {
+ throw new BadRequestException('Cannot update admin group');
+ }
+ for (const appId of body.add_apps) {
+ manager.save(
+ AppGroupPermission,
+ manager.create(AppGroupPermission, {
+ appId: appId,
+ groupPermissionId: groupPermissionId,
+ read: true,
+ })
+ );
+ }
+ }
+ });
+
+ await this.userGroupPermissionsRepository.manager.transaction(async (manager) => {
+ if (body.remove_users) {
+ for (const userId of body.remove_users) {
+ const params = {
+ removeGroups: [groupPermission.group],
+ };
+ await this.usersService.update(userId, params, manager);
+ }
+ }
+
+ if (body.add_users) {
+ for (const userId of body.add_users) {
+ const params = {
+ addGroups: [groupPermission.group],
+ };
+ await this.usersService.update(userId, params, manager);
+ }
+ }
+ });
+
+ return this.groupPermissionsRepository.findOne({ id: groupPermissionId });
+ }
+
+ async findOne(user: User, groupPermissionId: string): Promise {
+ return this.groupPermissionsRepository.findOne({
+ organizationId: user.organizationId,
+ id: groupPermissionId,
+ });
+ }
+
+ async findAll(user: User): Promise {
+ return this.groupPermissionsRepository.find({
+ organizationId: user.organizationId,
+ });
+ }
+
+ async findApps(user: User, groupPermissionId: string): Promise {
+ return createQueryBuilder(App, 'apps')
+ .innerJoinAndSelect('apps.groupPermissions', 'group_permissions')
+ .innerJoinAndSelect('apps.appGroupPermissions', 'app_group_permissions')
+ .where('group_permissions.id = :groupPermissionId', {
+ groupPermissionId,
+ })
+ .andWhere('group_permissions.organization_id = :organizationId', {
+ organizationId: user.organizationId,
+ })
+ .andWhere('app_group_permissions.group_permission_id = :groupPermissionId', { groupPermissionId })
+ .orderBy('apps.created_at', 'DESC')
+ .getMany();
+ }
+
+ async findAddableApps(user: User, groupPermissionId: string): Promise {
+ const groupPermission = await this.groupPermissionsRepository.findOne({
+ id: groupPermissionId,
+ organizationId: user.organizationId,
+ });
+
+ const appsInGroup = await groupPermission.apps;
+ const appsInGroupIds = appsInGroup.map((u) => u.id);
+
+ return await this.appRepository.find({
+ where: {
+ id: Not(In(appsInGroupIds)),
+ organizationId: user.organizationId,
+ },
+ loadEagerRelations: false,
+ relations: ['groupPermissions', 'appGroupPermissions'],
+ });
+ }
+
+ async findUsers(user: User, groupPermissionId: string): Promise {
+ return createQueryBuilder(User, 'users')
+ .innerJoinAndSelect('users.groupPermissions', 'group_permissions')
+ .innerJoinAndSelect('users.userGroupPermissions', 'user_group_permissions')
+ .where('group_permissions.id = :groupPermissionId', {
+ groupPermissionId,
+ })
+ .andWhere('group_permissions.organization_id = :organizationId', {
+ organizationId: user.organizationId,
+ })
+ .andWhere('user_group_permissions.group_permission_id = :groupPermissionId', { groupPermissionId })
+ .orderBy('users.created_at', 'DESC')
+ .getMany();
+ }
+
+ async findAddableUsers(user: User, groupPermissionId: string): Promise {
+ const groupPermission = await this.groupPermissionsRepository.findOne({
+ id: groupPermissionId,
+ organizationId: user.organizationId,
+ });
+
+ const userInGroup = await groupPermission.users;
+ const usersInGroupIds = userInGroup.map((u) => u.id);
+
+ const adminUsers = await createQueryBuilder(UserGroupPermission, 'user_group_permissions')
+ .innerJoin(
+ GroupPermission,
+ 'group_permissions',
+ 'group_permissions.id = user_group_permissions.group_permission_id'
+ )
+ .where('group_permissions.group = :group', { group: 'admin' })
+ .andWhere('group_permissions.organization_id = :organizationId', {
+ organizationId: user.organizationId,
+ })
+ .getMany();
+ const adminUserIds = adminUsers.map((u) => u.userId);
+
+ return await this.userRepository.find({
+ id: Not(In([...usersInGroupIds, ...adminUserIds])),
+ organizationId: user.organizationId,
+ });
+ }
+}
diff --git a/server/src/services/organization_users.service.ts b/server/src/services/organization_users.service.ts
index cee481bb40..6c877dcf9c 100644
--- a/server/src/services/organization_users.service.ts
+++ b/server/src/services/organization_users.service.ts
@@ -28,8 +28,8 @@ export class OrganizationUsersService {
email: params['email'],
};
- const user = await this.usersService.create(userParams, currentUser.organization);
- const organizationUser = await this.create(user, currentUser.organization, params.role);
+ const user = await this.usersService.create(userParams, currentUser.organization, ['all_users']);
+ const organizationUser = await this.create(user, currentUser.organization);
this.emailService.sendOrganizationUserWelcomeEmail(
user.email,
@@ -41,12 +41,12 @@ export class OrganizationUsersService {
return organizationUser;
}
- async create(user: User, organization: Organization, role: string): Promise {
+ async create(user: User, organization: Organization): Promise {
return await this.organizationUsersRepository.save(
this.organizationUsersRepository.create({
user,
organization,
- role,
+ role: 'all_users',
createdAt: new Date(),
updatedAt: new Date(),
})
diff --git a/server/src/services/organizations.service.ts b/server/src/services/organizations.service.ts
index 6747426743..2b8830c209 100644
--- a/server/src/services/organizations.service.ts
+++ b/server/src/services/organizations.service.ts
@@ -3,6 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { OrganizationUser } from '../entities/organization_user.entity';
import { Repository } from 'typeorm';
import { Organization } from 'src/entities/organization.entity';
+import { UsersService } from './users.service';
+import { GroupPermission } from 'src/entities/group_permission.entity';
@Injectable()
export class OrganizationsService {
@@ -10,17 +12,40 @@ export class OrganizationsService {
@InjectRepository(Organization)
private organizationsRepository: Repository,
@InjectRepository(OrganizationUser)
- private organizationUsersRepository: Repository
+ private organizationUsersRepository: Repository,
+ @InjectRepository(GroupPermission)
+ private groupPermissionsRepository: Repository,
+ private usersService: UsersService
) {}
async create(name: string): Promise {
- return this.organizationsRepository.save(
+ const organization = await this.organizationsRepository.save(
this.organizationsRepository.create({
name,
createdAt: new Date(),
updatedAt: new Date(),
})
);
+
+ await this.createDefaultGroupPermissionsForOrganization(organization);
+
+ return organization;
+ }
+
+ async createDefaultGroupPermissionsForOrganization(organization: Organization) {
+ const defaultGroups = ['all_users', 'admin'];
+ const createdGroupPermissions = [];
+
+ for (const group of defaultGroups) {
+ const groupPermission = this.groupPermissionsRepository.create({
+ organizationId: organization.id,
+ group: group,
+ });
+ await this.groupPermissionsRepository.save(groupPermission);
+ createdGroupPermissions.push(groupPermission);
+ }
+
+ return createdGroupPermissions;
}
async fetchUsers(user: any): Promise {
@@ -42,7 +67,7 @@ export class OrganizationsService {
status: orgUser.status,
};
- if (user.isAdmin && orgUser.user.invitationToken)
+ if (await this.usersService.hasGroup(user, 'admin') && orgUser.user.invitationToken)
serializedUser['invitationToken'] = orgUser.user.invitationToken;
serializedUsers.push(serializedUser);
diff --git a/server/src/services/seeds.service.ts b/server/src/services/seeds.service.ts
index 10cd4bbb04..978433bbee 100644
--- a/server/src/services/seeds.service.ts
+++ b/server/src/services/seeds.service.ts
@@ -3,44 +3,74 @@ import { EntityManager } from 'typeorm/entity-manager/EntityManager';
import { User } from '../entities/user.entity';
import { Organization } from '../entities/organization.entity';
import { OrganizationUser } from '../entities/organization_user.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
@Injectable()
export class SeedsService {
constructor(private readonly entityManager: EntityManager) {}
async perform(): Promise {
- const defaultUser = await this.entityManager.findOne(User, {
- email: 'dev@tooljet.io',
- });
+ await this.entityManager.transaction(async (manager) => {
+ const defaultUser = await manager.findOne(User, {
+ email: 'dev@tooljet.io',
+ });
- if (defaultUser) {
- console.log('Default user already present. Skipping seed.');
- return;
+ if (defaultUser) {
+ console.log('Default user already present. Skipping seed.');
+ return;
+ }
+
+ const organization = manager.create(Organization, {
+ name: 'My organization',
+ });
+
+ await manager.save(organization);
+
+ const user = manager.create(User, {
+ firstName: 'The',
+ lastName: 'Developer',
+ email: 'dev@tooljet.io',
+ password: 'password',
+ organizationId: organization.id,
+ });
+
+ await manager.save(user);
+
+ // TODO: Remove role usage
+ const organizationUser = manager.create(OrganizationUser, {
+ organizationId: organization.id,
+ userId: user.id,
+ role: 'all_users',
+ status: 'active',
+ });
+
+ await manager.save(organizationUser);
+
+ await this.createDefaultUserGroups(manager, user);
+ });
+ }
+
+ async createDefaultUserGroups(manager: EntityManager, user: User): Promise {
+ const defaultGroups = ['all_users', 'admin'];
+ for (const group of defaultGroups) {
+ await this.createGroupAndAssociateUser(group, manager, user);
}
+ }
- const organization = this.entityManager.create(Organization, {
- name: 'My organization',
+ async createGroupAndAssociateUser(group: string, manager: EntityManager, user: User): Promise {
+ const groupPermission = manager.create(GroupPermission, {
+ organizationId: user.organizationId,
+ group: group,
});
- await this.entityManager.save(organization);
+ await manager.save(groupPermission);
- const user = this.entityManager.create(User, {
- firstName: 'The',
- lastName: 'Developer',
- email: 'dev@tooljet.io',
- password: 'password',
- organizationId: organization.id,
- });
-
- await this.entityManager.save(user);
-
- const organizationUser = this.entityManager.create(OrganizationUser, {
- organizationId: organization.id,
+ const userGroupPermission = manager.create(UserGroupPermission, {
+ groupPermissionId: groupPermission.id,
userId: user.id,
- role: 'admin',
- status: 'active',
});
- await this.entityManager.save(organizationUser);
+ await manager.save(userGroupPermission);
}
}
diff --git a/server/src/services/users.service.ts b/server/src/services/users.service.ts
index 601392bf0a..cee3c6d7c5 100644
--- a/server/src/services/users.service.ts
+++ b/server/src/services/users.service.ts
@@ -2,8 +2,12 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../entities/user.entity';
import { Organization } from 'src/entities/organization.entity';
-import { Repository } from 'typeorm';
+import { createQueryBuilder, EntityManager, getManager, getRepository, In, Repository } from 'typeorm';
import { OrganizationUser } from '../entities/organization_user.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { BadRequestException } from '@nestjs/common';
const uuid = require('uuid');
const bcrypt = require('bcrypt');
@@ -35,14 +39,15 @@ export class UsersService {
});
}
- async create(userParams: any, organization): Promise {
+ async create(userParams: any, organization: Organization, groups?: string[]): Promise {
const password = uuid.v4();
const invitationToken = uuid.v4();
const { email, firstName, lastName } = userParams;
+ let user: User;
- return this.usersRepository.save(
- this.usersRepository.create({
+ await getManager().transaction(async (manager) => {
+ user = manager.create(User, {
email,
firstName,
lastName,
@@ -51,8 +56,28 @@ export class UsersService {
organizationId: organization.id,
createdAt: new Date(),
updatedAt: new Date(),
- })
- );
+ });
+ await manager.save(user);
+
+ for (const group of groups) {
+ const orgGroupPermission = await manager.findOne(GroupPermission, {
+ organizationId: organization.id,
+ group: group,
+ });
+
+ if (orgGroupPermission) {
+ const userGroupPermission = manager.create(UserGroupPermission, {
+ groupPermissionId: orgGroupPermission.id,
+ userId: user.id,
+ });
+ manager.save(userGroupPermission);
+ } else {
+ throw new BadRequestException(`${group} group does not exist for current organization`);
+ }
+ }
+ });
+
+ return user;
}
async setupAccountFromInvitationToken(params: any) {
@@ -65,19 +90,30 @@ export class UsersService {
if (user) {
// beforeUpdate hook will not trigger if using update method of repository
- await this.usersRepository.save(Object.assign(user, { firstName, lastName, password, invitationToken: null }));
+ await this.usersRepository.save(
+ Object.assign(user, {
+ firstName,
+ lastName,
+ password,
+ invitationToken: null,
+ })
+ );
const organizationUser = user.organizationUsers[0];
- this.organizationUsersRepository.update(organizationUser.id, { status: 'active' });
+ this.organizationUsersRepository.update(organizationUser.id, {
+ status: 'active',
+ });
if (newSignup) {
- this.organizationsRepository.update(user.organizationId, { name: organization });
+ this.organizationsRepository.update(user.organizationId, {
+ name: organization,
+ });
}
}
}
- async update(userId: string, params: any) {
- const { forgotPasswordToken, password, firstName, lastName } = params;
+ async update(userId: string, params: any, manager?: EntityManager) {
+ const { forgotPasswordToken, password, firstName, lastName, addGroups, removeGroups } = params;
const hashedPassword = password ? bcrypt.hashSync(password, 10) : undefined;
@@ -93,6 +129,157 @@ export class UsersService {
updateableParams[key] === undefined ? delete updateableParams[key] : {}
);
- return await this.usersRepository.update(userId, updateableParams);
+ let user: User;
+
+ const performUpdateInTransaction = async (manager) => {
+ await manager.update(User, userId, { ...updateableParams });
+ user = await manager.findOne(User, { id: userId });
+
+ await this.removeUserGroupPermissionsIfExists(manager, user, removeGroups);
+
+ await this.addUserGroupPermissions(manager, user, addGroups);
+ };
+
+ if (manager) {
+ await performUpdateInTransaction(manager);
+ } else {
+ await getManager().transaction(async (manager) => {
+ await performUpdateInTransaction(manager);
+ });
+ }
+
+ return user;
+ }
+
+ async addUserGroupPermissions(manager: EntityManager, user: User, addGroups: string[]) {
+ if (addGroups) {
+ const orgGroupPermissions = await this.groupPermissionsForOrganization(user.organizationId);
+
+ for (const group of addGroups) {
+ const orgGroupPermission = orgGroupPermissions.find((permission) => permission.group == group);
+
+ if (orgGroupPermission) {
+ const userGroupPermission = manager.create(UserGroupPermission, {
+ groupPermissionId: orgGroupPermission.id,
+ userId: user.id,
+ });
+ manager.save(userGroupPermission);
+ } else {
+ throw new BadRequestException(`${group} group does not exist for current organization`);
+ }
+ }
+ }
+ }
+
+ async removeUserGroupPermissionsIfExists(manager: EntityManager, user: User, removeGroups: string[]) {
+ if (removeGroups) {
+ await this.throwErrorIfRemovingLastActiveAdmin(user, removeGroups);
+ if (removeGroups.includes('all_users')) {
+ throw new BadRequestException('Cannot remove user from default group.');
+ }
+
+ const groupPermissions = await manager.find(GroupPermission, {
+ group: In(removeGroups),
+ organizationId: user.organizationId,
+ });
+ const groupIdsToMaybeRemove = groupPermissions.map((permission) => permission.id);
+
+ await manager.delete(UserGroupPermission, {
+ groupPermissionId: In(groupIdsToMaybeRemove),
+ userId: user.id,
+ });
+ }
+ }
+
+ async throwErrorIfRemovingLastActiveAdmin(user: User, removeGroups: string[]) {
+ const removingAdmin = removeGroups.includes('admin');
+ if (!removingAdmin) return;
+
+ const result = await createQueryBuilder(User, 'users')
+ .innerJoin('users.groupPermissions', 'group_permissions')
+ .innerJoin('users.organizationUsers', 'organization_users')
+ .where('organization_users.user_id != :userId', { userId: user.id })
+ .andWhere('organization_users.status = :status', { status: 'active' })
+ .andWhere('group_permissions.group = :group', { group: 'admin' })
+ .andWhere('group_permissions.organization_id = :organizationId', {
+ organizationId: user.organizationId,
+ })
+ .getCount();
+
+ if (result == 0) throw new BadRequestException('Atleast one active admin is required.');
+ }
+
+ async hasGroup(user: User, group: string, organizationId?: string): Promise {
+ // Currently user can be part of single organization and
+ // the organization id is present on the user itself
+ const orgId = organizationId || user.organizationId;
+
+ const result = await createQueryBuilder(GroupPermission, 'group_permissions')
+ .innerJoin('group_permissions.userGroupPermission', 'user_group_permissions')
+ .where('group_permissions.organization_id = :organizationId', {
+ organizationId: orgId,
+ })
+ .andWhere('group_permissions.group = :group ', { group })
+ .andWhere('user_group_permissions.user_id = :userId', { userId: user.id })
+ .getCount();
+
+ return result > 0;
+ }
+
+ async userCan(user: User, action: string, entityName: string, resourceId?: string): Promise {
+ switch (entityName) {
+ case 'App':
+ if (action == 'create') {
+ return await this.hasGroup(user, 'admin');
+ } else {
+ return this.canAnyGroupPerformAction(action, await this.appGroupPermissions(user, resourceId));
+ }
+
+ default:
+ return false;
+ }
+ }
+
+ canAnyGroupPerformAction(action: string, permissions: AppGroupPermission[]): boolean {
+ return permissions.some((p) => p[action.toLowerCase()]);
+ }
+
+ async groupPermissions(user: User, organizationId?: string): Promise {
+ const orgUserGroupPermissions = await this.userGroupPermissions(user, organizationId);
+ const groupIds = orgUserGroupPermissions.map((p) => p.groupPermissionId);
+ const groupPermissionRepository = getRepository(GroupPermission);
+
+ return await groupPermissionRepository.findByIds(groupIds);
+ }
+
+ async groupPermissionsForOrganization(organizationId: string) {
+ const groupPermissionRepository = getRepository(GroupPermission);
+
+ return await groupPermissionRepository.find({ organizationId });
+ }
+
+ async appGroupPermissions(user: User, appId: string, organizationId?: string): Promise {
+ const orgUserGroupPermissions = await this.userGroupPermissions(user, organizationId);
+ const groupIds = orgUserGroupPermissions.map((p) => p.groupPermissionId);
+ const appGroupPermissionRepository = getRepository(AppGroupPermission);
+
+ return await appGroupPermissionRepository.find({
+ groupPermissionId: In(groupIds),
+ appId: appId,
+ });
+ }
+
+ async userGroupPermissions(user: User, organizationId?: string): Promise {
+ // Currently user can be part of single organization
+ // and hence we can use organization_id on user entity
+ const orgId = organizationId || user.organizationId;
+
+ return await createQueryBuilder(UserGroupPermission, 'user_group_permissions')
+ .innerJoin('user_group_permissions.groupPermission', 'group_permissions')
+ .where('group_permissions.organization_id = :organizationId', {
+ organizationId: orgId,
+ })
+ .andWhere('user_group_permissions.user_id = :userId', { userId: user.id })
+ .getMany();
}
}
diff --git a/server/test/controllers/app.e2e-spec.ts b/server/test/controllers/app.e2e-spec.ts
index 32e1bd522d..25f23e3d6c 100644
--- a/server/test/controllers/app.e2e-spec.ts
+++ b/server/test/controllers/app.e2e-spec.ts
@@ -4,14 +4,10 @@ import { INestApplication } from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from 'src/entities/user.entity';
import { clearDB, createUser, createNestAppInstance } from '../test.helper';
-import { Organization } from 'src/entities/organization.entity';
-import { OrganizationUser } from 'src/entities/organization_user.entity';
describe('Authentication', () => {
let app: INestApplication;
let userRepository: Repository;
- let organizationRepository: Repository;
- let organizationUsersRepository: Repository;
beforeEach(async () => {
await clearDB();
@@ -22,34 +18,34 @@ describe('Authentication', () => {
app = await createNestAppInstance();
userRepository = app.get('UserRepository');
- organizationRepository = app.get('OrganizationRepository');
- organizationUsersRepository = app.get('OrganizationUserRepository');
});
it('should create new users', async () => {
- const response = await request(app.getHttpServer()).post('/signup').send({ email: 'test@tooljet.io' });
-
+ const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
expect(response.statusCode).toBe(201);
const id = response.body['id'];
const user = await userRepository.findOne(id, { relations: ['organization'] });
- expect(user.organization.name).toBe('Untitled organization');
- const orgUser = user.organizationUsers[0];
- expect(orgUser.role).toBe('admin');
+ expect(await user.organization.name).toBe('Untitled organization');
+
+ const groupPermissions = await user.groupPermissions;
+ const groupNames = groupPermissions.map((x) => x.group);
+
+ expect(new Set(['all_users', 'admin'])).toEqual(new Set(groupNames));
});
- it(`authenticate if valid credentials`, async () => {
- return request(app.getHttpServer())
- .post('/authenticate')
+ it('authenticate if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
.send({ email: 'admin@tooljet.io', password: 'password' })
.expect(201);
});
- it(`throw 401 if invalid credentials`, async () => {
- return request(app.getHttpServer())
- .post('/authenticate')
- .send({ email: 'adnin@tooljet.io', password: 'pwd' })
+ it('throw 401 if invalid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'amdin@tooljet.io', password: 'pwd' })
.expect(401);
});
diff --git a/server/test/controllers/app_users.e2e-spec.ts b/server/test/controllers/app_users.e2e-spec.ts
index 6699c65097..f0cede81b1 100644
--- a/server/test/controllers/app_users.e2e-spec.ts
+++ b/server/test/controllers/app_users.e2e-spec.ts
@@ -14,85 +14,105 @@ describe('app_users controller', () => {
});
it('should allow only authenticated users to create new app users', async () => {
- await request(app.getHttpServer()).post('/app_users').expect(401);
+ await request(app.getHttpServer()).post('/api/app_users').expect(401);
});
- it('should be able to create a new app user if admin of same organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ xit('should be able to create a new app user if admin of same organization', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
- const application = await createApplication(app, { user: adminUserData.user });
+ const application = await createApplication(app, {
+ user: adminUserData.user,
+ });
const response = await request(app.getHttpServer())
- .post(`/app_users`)
+ .post(`/api/app_users`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send({
app_id: application.id,
org_user_id: developerUserData.orgUser.id,
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
expect(response.statusCode).toBe(201);
});
it('should not be able to create new app user if admin of another organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
- const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
+ const anotherOrgAdminUserData = await createUser(app, {
+ email: 'another@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
const response = await request(app.getHttpServer())
- .post(`/app_users`)
+ .post(`/api/app_users`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user))
.send({
app_id: application.id,
org_user_id: adminUserData.orgUser.id,
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
expect(response.statusCode).toBe(403);
});
it('should not allow developers and viewers to create app users', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
let response = await request(app.getHttpServer())
- .post(`/app_users/`)
+ .post(`/api/app_users/`)
.set('Authorization', authHeaderForUser(developerUserData.user))
.send({
app_id: application.id,
org_user_id: viewerUserData.orgUser.id,
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
expect(response.statusCode).toBe(403);
response = response = await request(app.getHttpServer())
- .post(`/app_users/`)
+ .post(`/api/app_users/`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.send({
app_id: application.id,
org_user_id: developerUserData.orgUser.id,
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
await application.reload();
diff --git a/server/test/controllers/apps.e2e-spec.ts b/server/test/controllers/apps.e2e-spec.ts
index a51d0f33cc..5b7a1a6ff8 100644
--- a/server/test/controllers/apps.e2e-spec.ts
+++ b/server/test/controllers/apps.e2e-spec.ts
@@ -9,12 +9,15 @@ import {
createApplicationVersion,
createDataQuery,
createDataSource,
+ createAppGroupPermission,
} from '../test.helper';
import { App } from 'src/entities/app.entity';
import { AppVersion } from 'src/entities/app_version.entity';
import { DataQuery } from 'src/entities/data_query.entity';
import { DataSource } from 'src/entities/data_source.entity';
import { AppUser } from 'src/entities/app_user.entity';
+import { getManager, getRepository } from 'typeorm';
+import { GroupPermission } from 'src/entities/group_permission.entity';
describe('apps controller', () => {
let app: INestApplication;
@@ -25,30 +28,32 @@ describe('apps controller', () => {
beforeAll(async () => {
app = await createNestAppInstance();
+ app.setGlobalPrefix('/api');
+ await app.init();
});
- describe('/apps/uuid', () => {
+ describe('/api/apps/uuid', () => {
it('should allow only authenticated users to update app params', async () => {
- await request(app.getHttpServer()).put('/apps/uuid').expect(401);
+ await request(app.getHttpServer()).put('/api/apps/uuid').expect(401);
});
});
- describe('/apps', () => {
+ describe('/api/apps', () => {
describe('authorization', () => {
- it('should be able to create app if user is either admin or developer', async () => {
+ it('should be able to create app if user has admin group', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const organization = adminUserData.organization;
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization,
});
@@ -58,31 +63,31 @@ describe('apps controller', () => {
});
await createApplicationVersion(app, application);
- for (const userData of [adminUserData, developerUserData]) {
+ for (const userData of [viewerUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/apps`)
+ .post(`/api/apps`)
.set('Authorization', authHeaderForUser(userData.user));
- expect(response.statusCode).toBe(201);
- expect(response.body.name).toBe('Untitled app');
+ expect(response.statusCode).toBe(403);
}
const response = await request(app.getHttpServer())
- .post(`/apps`)
- .set('Authorization', authHeaderForUser(viewerUserData.user));
+ .post(`/api/apps`)
+ .set('Authorization', authHeaderForUser(adminUserData.user));
- expect(response.statusCode).toBe(403);
+ expect(response.statusCode).toBe(201);
+ expect(response.body.name).toBe('Untitled app');
});
});
it('should create app with default values', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const response = await request(app.getHttpServer())
- .post(`/apps`)
+ .post(`/api/apps`)
.set('Authorization', authHeaderForUser(adminUserData.user));
expect(response.statusCode).toBe(201);
@@ -96,22 +101,22 @@ describe('apps controller', () => {
});
});
- describe('/apps/:id/clone', () => {
- it('should be able to clone the app if user is admin or developer', async () => {
+ describe('/api/apps/:id/clone', () => {
+ it('should be able to clone the app if user group is admin', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
@@ -121,27 +126,23 @@ describe('apps controller', () => {
});
let response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/clone`)
+ .post(`/api/apps/${application.id}/clone`)
.set('Authorization', authHeaderForUser(adminUserData.user));
expect(response.statusCode).toBe(201);
- let appId = response.body.id;
- let clonedApplication = await App.findOne({ id: appId });
+ const appId = response.body.id;
+ const clonedApplication = await App.findOne({ id: appId });
expect(clonedApplication.name).toBe('App to clone');
response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/clone`)
+ .post(`/api/apps/${application.id}/clone`)
.set('Authorization', authHeaderForUser(developerUserData.user));
- expect(response.statusCode).toBe(201);
-
- appId = response.body.id;
- clonedApplication = await App.findOne({ id: appId });
- expect(clonedApplication.name).toBe('App to clone');
+ expect(response.statusCode).toBe(403);
response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/clone`)
+ .post(`/api/apps/${application.id}/clone`)
.set('Authorization', authHeaderForUser(viewerUserData.user));
expect(response.statusCode).toBe(403);
@@ -150,11 +151,11 @@ describe('apps controller', () => {
it('should not be able to clone the app if app is of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -162,25 +163,25 @@ describe('apps controller', () => {
});
const response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/clone`)
+ .post(`/api/apps/${application.id}/clone`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
});
});
- describe('/apps/:id', () => {
+ describe('/api/apps/:id', () => {
it('should be able to update name of the app if admin of same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
user: adminUserData.user,
});
const response = await request(app.getHttpServer())
- .put(`/apps/${application.id}`)
+ .put(`/api/apps/${application.id}`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send({ app: { name: 'new name' } });
@@ -192,11 +193,11 @@ describe('apps controller', () => {
it('should not be able to update name of the app if admin of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -204,7 +205,7 @@ describe('apps controller', () => {
});
const response = await request(app.getHttpServer())
- .put(`/apps/${application.id}`)
+ .put(`/api/apps/${application.id}`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user))
.send({ app: { name: 'new name' } });
@@ -213,10 +214,10 @@ describe('apps controller', () => {
expect(application.name).toBe('name');
});
- it('should not allow developers and viewers to change the name of apps', async () => {
+ it('should not allow custom groups without app create permission to change the name of apps', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -225,23 +226,23 @@ describe('apps controller', () => {
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
let response = await request(app.getHttpServer())
- .put(`/apps/${application.id}`)
+ .put(`/api/apps/${application.id}`)
.set('Authorization', authHeaderForUser(developerUserData.user))
.send({ app: { name: 'new name' } });
expect(response.statusCode).toBe(403);
response = await request(app.getHttpServer())
- .put(`/apps/${application.id}`)
+ .put(`/api/apps/${application.id}`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.send({ app: { name: 'new name' } });
expect(response.statusCode).toBe(403);
@@ -254,7 +255,7 @@ describe('apps controller', () => {
it('should be possible for the admin to delete an app, cascaded with its versions, queries and data sources', async () => {
const admin = await createUser(app, {
email: 'adminForDelete@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'AppTObeDeleted',
@@ -272,7 +273,7 @@ describe('apps controller', () => {
});
const response = await request(app.getHttpServer())
- .delete(`/apps/${application.id}`)
+ .delete(`/api/apps/${application.id}`)
.set('Authorization', authHeaderForUser(admin.user));
expect(response.statusCode).toBe(200);
@@ -287,7 +288,7 @@ describe('apps controller', () => {
it('should not be possible for non-admin user to delete an app, cascaded with its versions, queries and data sources', async () => {
const developer = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
});
const application = await createApplication(app, {
name: 'AppTObeDeleted',
@@ -302,7 +303,7 @@ describe('apps controller', () => {
});
const response = await request(app.getHttpServer())
- .delete(`/apps/${application.id}`)
+ .delete(`/api/apps/${application.id}`)
.set('Authorization', authHeaderForUser(developer.user));
expect(response.statusCode).toBe(403);
@@ -312,21 +313,22 @@ describe('apps controller', () => {
});
});
- describe('/apps/uuid/users', () => {
+ describe('/api/apps/uuid/users', () => {
it('should allow only authenticated users to access app users endpoint', async () => {
- await request(app.getHttpServer()).get('/apps/uuid/users').expect(401);
+ await request(app.getHttpServer()).get('/api/apps/uuid/users').expect(401);
});
});
- describe('/apps/:id/users', () => {
- it('should not be able to fetch app users if admin of another organization', async () => {
+ // TODO: Remove deprecated endpoint
+ describe('/api/apps/:id/users', () => {
+ xit('should not be able to fetch app users if admin of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -334,26 +336,26 @@ describe('apps controller', () => {
});
const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/users`)
+ .get(`/api/apps/${application.id}/users`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
});
- it('should be able to fetch app users if admin/developer/viewer of same organization', async () => {
+ xit('should be able to fetch app users if group is admin/developer/viewer of same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const organization = adminUserData.organization;
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization,
});
@@ -364,7 +366,7 @@ describe('apps controller', () => {
for (const userData of [adminUserData, developerUserData, viewerUserData]) {
const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/users`)
+ .get(`/api/apps/${application.id}/users`)
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(200);
@@ -373,23 +375,18 @@ describe('apps controller', () => {
});
});
- describe('/apps/:id/versions', () => {
+ describe('/api/apps/:id/versions', () => {
describe('get versions', () => {
describe('authorization', () => {
- it('should be able to fetch app versions if admin/developer/viewer of same organization', async () => {
+ it('should be able to fetch app versions with app read permission group', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const organization = adminUserData.organization;
- const developerUserData = await createUser(app, {
+ const defaultUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
- organization,
- });
- const viewerUserData = await createUser(app, {
- email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users'],
organization,
});
@@ -399,9 +396,18 @@ describe('apps controller', () => {
});
await createApplicationVersion(app, application);
- for (const userData of [adminUserData, developerUserData, viewerUserData]) {
+ const allUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'all_users',
+ });
+ await createAppGroupPermission(app, application, allUserGroup.id, {
+ read: true,
+ update: false,
+ delete: false,
+ });
+
+ for (const userData of [adminUserData, defaultUserData]) {
const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions`)
+ .get(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(200);
@@ -416,11 +422,11 @@ describe('apps controller', () => {
it('should not be able to fetch app versions if user of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -429,29 +435,39 @@ describe('apps controller', () => {
await createApplicationVersion(app, application);
const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions`)
+ .get(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
});
- it('should be able to create a new app version if admin or developer of same organization', async () => {
+ it('should be able to create a new app version if group is admin or has app update permission group in same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
user: adminUserData.user,
});
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: false,
+ update: true,
+ delete: false,
+ });
+
for (const userData of [adminUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/versions`)
+ .post(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
versionName: 'v0',
@@ -464,11 +480,11 @@ describe('apps controller', () => {
it('should not be able to create app versions if user of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -477,7 +493,7 @@ describe('apps controller', () => {
await createApplicationVersion(app, application);
const response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/versions`)
+ .post(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user))
.send({
versionName: 'v0',
@@ -486,14 +502,14 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(403);
});
- it('should not be able to fetch app versions if user is a viewer', async () => {
+ it('should not be able to create app versions if user does not have app create permission group', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
@@ -503,7 +519,7 @@ describe('apps controller', () => {
await createApplicationVersion(app, application);
const response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/versions`)
+ .post(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.send({
versionName: 'v0',
@@ -517,14 +533,14 @@ describe('apps controller', () => {
it('should return null when no previous versions exists', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
user: adminUserData.user,
});
let response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/versions`)
+ .post(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send({
versionName: 'v0',
@@ -533,7 +549,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(201);
response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions`)
+ .get(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(adminUserData.user));
expect(response.statusCode).toBe(200);
@@ -543,7 +559,7 @@ describe('apps controller', () => {
it('should return previous version definition when previous versions exists', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
@@ -553,7 +569,7 @@ describe('apps controller', () => {
const version = await createApplicationVersion(app, application);
let response = await request(app.getHttpServer())
- .put(`/apps/${application.id}/versions/${version.id}`)
+ .put(`/api/apps/${application.id}/versions/${version.id}`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send({
definition: { foo: 'bar' },
@@ -562,7 +578,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(200);
response = await request(app.getHttpServer())
- .post(`/apps/${application.id}/versions`)
+ .post(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send({
versionName: 'v1',
@@ -571,7 +587,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(201);
response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions`)
+ .get(`/api/apps/${application.id}/versions`)
.set('Authorization', authHeaderForUser(adminUserData.user));
expect(response.statusCode).toBe(200);
@@ -584,17 +600,17 @@ describe('apps controller', () => {
});
});
- describe('/apps/:id/versions/:version_id', () => {
+ describe('/api/apps/:id/versions/:version_id', () => {
describe('get app version', () => {
describe('authorization', () => {
- it('should be able to get app version if admin or developer of same organization', async () => {
+ it('should be able to get app version by users having app read permission within same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
@@ -602,45 +618,32 @@ describe('apps controller', () => {
});
const version = await createApplicationVersion(app, application);
+ const allUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'all_users',
+ });
+ await createAppGroupPermission(app, application, allUserGroup.id, {
+ read: true,
+ update: false,
+ delete: false,
+ });
+
for (const userData of [adminUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions/${version.id}`)
+ .get(`/api/apps/${application.id}/versions/${version.id}`)
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(200);
}
});
- it('should be able to get app version if viewers of same organization', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
- const viewerUserData = await createUser(app, {
- email: 'dev@tooljet.io',
- role: 'viewer',
- organization: adminUserData.organization,
- });
- const application = await createApplication(app, {
- user: adminUserData.user,
- });
- const version = await createApplicationVersion(app, application);
-
- const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions/${version.id}`)
- .set('Authorization', authHeaderForUser(viewerUserData.user));
-
- expect(response.statusCode).toBe(200);
- });
-
it('should not be able to get app versions if user of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -649,7 +652,7 @@ describe('apps controller', () => {
const version = await createApplicationVersion(app, application);
const response = await request(app.getHttpServer())
- .get(`/apps/${application.id}/versions/${version.id}`)
+ .get(`/api/apps/${application.id}/versions/${version.id}`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
@@ -658,14 +661,14 @@ describe('apps controller', () => {
});
describe('update app version', () => {
- it('should be able to update app version if admin or developer of same organization', async () => {
+ it('should be able to update app version if has group admin or app update permission group in same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
@@ -673,9 +676,17 @@ describe('apps controller', () => {
});
const version = await createApplicationVersion(app, application);
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({ group: 'developer' });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: false,
+ update: true,
+ delete: false,
+ });
+
for (const userData of [adminUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .put(`/apps/${application.id}/versions/${version.id}`)
+ .put(`/api/apps/${application.id}/versions/${version.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
definition: { components: {} },
@@ -686,14 +697,14 @@ describe('apps controller', () => {
}
});
- it('should not be able to update app version if viewers of same organization', async () => {
+ it('should not be able to update app version if no app create permission within same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const viewerUserData = await createUser(app, {
email: 'dev@tooljet.io',
- role: 'viewer',
+ groups: ['all_users'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
@@ -702,7 +713,7 @@ describe('apps controller', () => {
const version = await createApplicationVersion(app, application);
const response = await request(app.getHttpServer())
- .put(`/apps/${application.id}/versions/${version.id}`)
+ .put(`/api/apps/${application.id}/versions/${version.id}`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.send({
definition: { components: {} },
@@ -714,11 +725,11 @@ describe('apps controller', () => {
it('should not be able to update app versions if user of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -727,7 +738,7 @@ describe('apps controller', () => {
const version = await createApplicationVersion(app, application);
const response = await request(app.getHttpServer())
- .put(`/apps/${application.id}/versions/${version.id}`)
+ .put(`/api/apps/${application.id}/versions/${version.id}`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user))
.send({
definition: { components: {} },
@@ -744,30 +755,49 @@ describe('apps controller', () => {
Public apps can be launched by anyone ( even unauthenticated users )
By view app endpoint, we assume the apps/slugs/:id endpoint
*/
- describe('/apps/slugs/:slug', () => {
- it('should be able to fetch app using slug if member of an organization', async () => {
+ describe('/api/apps/slugs/:slug', () => {
+ it('should be able to fetch app using slug if has read permission within an organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
name: 'name',
user: adminUserData.user,
+ slug: 'foo',
+ });
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+ // setup app permissions for viewer
+ const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'viewer',
+ });
+ await createAppGroupPermission(app, application, viewerUserGroup.id, {
+ read: true,
+ update: false,
+ delete: false,
});
for (const userData of [adminUserData, developerUserData, viewerUserData]) {
const response = await request(app.getHttpServer())
- .get(`/apps/slugs/${application.id}`)
+ .get('/api/apps/slugs/foo')
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(200);
@@ -777,19 +807,20 @@ describe('apps controller', () => {
it('should not be able to fetch app using slug if member of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
- const application = await createApplication(app, {
+ await createApplication(app, {
name: 'name',
user: adminUserData.user,
+ slug: 'foo',
});
const response = await request(app.getHttpServer())
- .get(`/apps/slugs/${application.id}`)
+ .get('/api/apps/slugs/foo')
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
@@ -798,18 +829,159 @@ describe('apps controller', () => {
it('should be able to fetch app using slug if a public app ( even if unauthenticated )', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
+ });
+
+ await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ slug: 'foo',
+ isPublic: true,
+ });
+
+ const response = await request(app.getHttpServer()).get('/api/apps/slugs/foo');
+
+ expect(response.statusCode).toBe(200);
+ });
+ });
+
+ describe('/api/apps/:id/export', () => {
+ it('should be able to export app if user has read permission within an organization', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const developerUserData = await createUser(app, {
+ email: 'developer@tooljet.io',
+ groups: ['all_users', 'developer'],
+ organization: adminUserData.organization,
+ });
+ const viewerUserData = await createUser(app, {
+ email: 'viewer@tooljet.io',
+ groups: ['all_users', 'viewer'],
+ organization: adminUserData.organization,
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ slug: 'foo',
+ });
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+ // setup app permissions for viewer
+ const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'viewer',
+ });
+ await createAppGroupPermission(app, application, viewerUserGroup.id, {
+ read: true,
+ update: false,
+ delete: false,
+ });
+
+ for (const userData of [adminUserData, developerUserData, viewerUserData]) {
+ const response = await request(app.getHttpServer())
+ .get(`/api/apps/${application.id}/export`)
+ .set('Authorization', authHeaderForUser(userData.user));
+
+ expect(response.statusCode).toBe(200);
+ expect(response.body.id).toBe(application.id);
+ expect(response.body.name).toBe(application.name);
+ expect(response.body.isPublic).toBe(application.isPublic);
+ expect(response.body.organizationId).toBe(application.organizationId);
+ }
+ });
+
+ it('should not be able to export app if member of another organization', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const anotherOrgAdminUserData = await createUser(app, {
+ email: 'another@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ slug: 'foo',
+ });
+
+ const response = await request(app.getHttpServer())
+ .get(`/api/apps/${application.id}/export`)
+ .set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should not be able to export app if it is a public app for an unauthenticated user', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
user: adminUserData.user,
+ slug: 'foo',
isPublic: true,
});
- const response = await request(app.getHttpServer()).get(`/apps/slugs/${application.id}`);
+ const response = await request(app.getHttpServer()).get(`/api/apps/${application.id}/export`);
+ expect(response.statusCode).toBe(401);
+ });
+ });
- expect(response.statusCode).toBe(200);
+ describe('/api/apps/import', () => {
+ it('should be able to import app only if user has admin group', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const organization = adminUserData.organization;
+ const developerUserData = await createUser(app, {
+ email: 'developer@tooljet.io',
+ groups: ['all_users', 'developer'],
+ organization,
+ });
+ const viewerUserData = await createUser(app, {
+ email: 'viewer@tooljet.io',
+ groups: ['all_users', 'viewer'],
+ organization,
+ });
+
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
+ await createApplicationVersion(app, application);
+
+ for (const userData of [viewerUserData, developerUserData]) {
+ const response = await request(app.getHttpServer())
+ .post('/api/apps/import')
+ .set('Authorization', authHeaderForUser(userData.user));
+
+ expect(response.statusCode).toBe(403);
+ }
+
+ const response = await request(app.getHttpServer())
+ .post('/api/apps/import')
+ .set('Authorization', authHeaderForUser(adminUserData.user))
+ .send({ name: 'Imported App' });
+
+ expect(response.statusCode).toBe(201);
+
+ const importedApp = await getManager().find(App, {
+ name: 'Imported App',
+ });
+
+ expect(importedApp).toHaveLength(1);
});
});
diff --git a/server/test/controllers/data_queries.e2e-spec.ts b/server/test/controllers/data_queries.e2e-spec.ts
index 1d47403b4c..005e1b03e3 100644
--- a/server/test/controllers/data_queries.e2e-spec.ts
+++ b/server/test/controllers/data_queries.e2e-spec.ts
@@ -8,7 +8,10 @@ import {
createNestAppInstance,
createDataQuery,
createDataSource,
+ createAppGroupPermission,
} from '../test.helper';
+import { getRepository } from 'typeorm';
+import { GroupPermission } from 'src/entities/group_permission.entity';
describe('data queries controller', () => {
let app: INestApplication;
@@ -21,20 +24,50 @@ describe('data queries controller', () => {
app = await createNestAppInstance();
});
- it('should be able to update queries of an app only if admin/developer of same organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ it('should be able to update queries of an app only if group is admin or group has app update permission', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
- const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
+ const anotherOrgAdminUserData = await createUser(app, {
+ email: 'another@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
+
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+
+ // setup app permissions for viewer
+ const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'viewer',
+ });
+ await createAppGroupPermission(app, application, viewerUserGroup.id, {
+ read: true,
+ update: false,
+ delete: false,
+ });
const dataQuery = await createDataQuery(app, {
application,
@@ -51,7 +84,7 @@ describe('data queries controller', () => {
for (const userData of [adminUserData, developerUserData]) {
const newOptions = { method: userData.user.email };
const response = await request(app.getHttpServer())
- .patch(`/data_queries/${dataQuery.id}`)
+ .patch(`/api/data_queries/${dataQuery.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
options: newOptions,
@@ -66,7 +99,7 @@ describe('data queries controller', () => {
for (const userData of [anotherOrgAdminUserData, viewerUserData]) {
const oldOptions = dataQuery.options;
const response = await request(app.getHttpServer())
- .patch(`/data_queries/${dataQuery.id}`)
+ .patch(`/api/data_queries/${dataQuery.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
options: { method: '' },
@@ -81,27 +114,37 @@ describe('data queries controller', () => {
it('should be able to delete queries of an app only if admin/developer of same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
user: adminUserData.user,
});
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+
for (const userData of [adminUserData, developerUserData]) {
const dataQuery = await createDataQuery(app, {
application,
@@ -117,7 +160,7 @@ describe('data queries controller', () => {
const newOptions = { method: userData.user.email };
const response = await request(app.getHttpServer())
- .delete(`/data_queries/${dataQuery.id}`)
+ .delete(`/api/data_queries/${dataQuery.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
options: newOptions,
@@ -142,7 +185,7 @@ describe('data queries controller', () => {
const oldOptions = dataQuery.options;
const response = await request(app.getHttpServer())
- .delete(`/data_queries/${dataQuery.id}`)
+ .delete(`/api/data_queries/${dataQuery.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
options: { method: '' },
@@ -154,20 +197,39 @@ describe('data queries controller', () => {
}
});
- it('should be able to get queries of an app only if the user belongs to the same organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ it('should be able to get queries only if the user has app read permission and belongs to the same organization', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
- const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', role: 'admin' });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
+ const anotherOrgAdminUserData = await createUser(app, {
+ email: 'another@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
await createDataQuery(app, {
application,
@@ -175,37 +237,62 @@ describe('data queries controller', () => {
options: { method: 'get' },
});
- for (const userData of [adminUserData, developerUserData, viewerUserData]) {
+ for (const userData of [adminUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .get(`/data_queries?app_id=${application.id}`)
+ .get(`/api/data_queries?app_id=${application.id}`)
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(200);
expect(response.body.data_queries.length).toBe(1);
}
+ let response = await request(app.getHttpServer())
+ .get(`/api/data_queries?app_id=${application.id}`)
+ .set('Authorization', authHeaderForUser(viewerUserData.user));
+
+ expect(response.statusCode).toBe(200);
+
// Forbidden if user of another organization
- const response = await request(app.getHttpServer())
- .get(`/data_queries?app_id=${application.id}`)
+ response = await request(app.getHttpServer())
+ .get(`/api/data_queries?app_id=${application.id}`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
});
- it('should be able to create queries for an app only if the user is admin/developer and belongs to the same organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ it('should be able to create queries for an app only if the user has admin group or update permission', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
- const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', role: 'admin' });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
+ const anotherOrgAdminUserData = await createUser(app, {
+ email: 'another@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
const queryParams = {
app_id: application.id,
@@ -215,7 +302,7 @@ describe('data queries controller', () => {
for (const userData of [adminUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/data_queries`)
+ .post(`/api/data_queries`)
.set('Authorization', authHeaderForUser(userData.user))
.send(queryParams);
@@ -225,7 +312,7 @@ describe('data queries controller', () => {
// Forbidden if a viewer or a user of another organization
for (const userData of [anotherOrgAdminUserData, viewerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/data_queries`)
+ .post(`/api/data_queries`)
.set('Authorization', authHeaderForUser(userData.user))
.send(queryParams);
@@ -234,9 +321,18 @@ describe('data queries controller', () => {
});
it('should not be able to create queries if datasource belongs to another app', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
- const anotherApplication = await createApplication(app, { name: 'name', user: adminUserData.user });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
+ const anotherApplication = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
const dataSource = await createDataSource(app, {
name: 'name',
kind: 'postgres',
@@ -253,7 +349,7 @@ describe('data queries controller', () => {
// Create query if data source belongs to same app
let response = await request(app.getHttpServer())
- .post(`/data_queries`)
+ .post(`/api/data_queries`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send(queryParams);
@@ -268,7 +364,7 @@ describe('data queries controller', () => {
// Fordbidden if data source belongs to another app
response = await request(app.getHttpServer())
- .post(`/data_queries`)
+ .post(`/api/data_queries`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.send(queryParams);
@@ -276,18 +372,24 @@ describe('data queries controller', () => {
});
it('should be able to run queries of an app if the user belongs to the same organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
const dataQuery = await createDataQuery(app, {
application,
@@ -301,9 +403,29 @@ describe('data queries controller', () => {
},
});
+ // setup app permissions for developer
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+
+ // setup app permissions for viewer
+ const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'viewer',
+ });
+ await createAppGroupPermission(app, application, viewerUserGroup.id, {
+ read: true,
+ update: false,
+ delete: false,
+ });
+
for (const userData of [adminUserData, developerUserData, viewerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/data_queries/${dataQuery.id}/run`)
+ .post(`/api/data_queries/${dataQuery.id}/run`)
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(201);
@@ -312,9 +434,18 @@ describe('data queries controller', () => {
});
it('should not be able to run queries of an app if the user belongs to another organization', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
- const anotherOrgAdminUserData = await createUser(app, { email: 'another@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const anotherOrgAdminUserData = await createUser(app, {
+ email: 'another@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
const dataQuery = await createDataQuery(app, {
application,
@@ -329,15 +460,22 @@ describe('data queries controller', () => {
});
const response = await request(app.getHttpServer())
- .post(`/data_queries/${dataQuery.id}/run`)
+ .post(`/api/data_queries/${dataQuery.id}/run`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
});
it('should be able to run queries of an app if a public app ( even if an unauthenticated user )', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user, isPublic: true });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ isPublic: true,
+ });
const dataQuery = await createDataQuery(app, {
application,
kind: 'restapi',
@@ -350,15 +488,22 @@ describe('data queries controller', () => {
},
});
- const response = await request(app.getHttpServer()).post(`/data_queries/${dataQuery.id}/run`);
+ const response = await request(app.getHttpServer()).post(`/api/data_queries/${dataQuery.id}/run`);
expect(response.statusCode).toBe(201);
expect(response.body.data.length).toBe(30);
});
it('should not be able to run queries if app not not public and user is not authenticated', async () => {
- const adminUserData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
- const application = await createApplication(app, { name: 'name', user: adminUserData.user, isPublic: false });
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ isPublic: false,
+ });
const dataQuery = await createDataQuery(app, {
application,
kind: 'restapi',
@@ -371,7 +516,7 @@ describe('data queries controller', () => {
},
});
- const response = await request(app.getHttpServer()).post(`/data_queries/${dataQuery.id}/run`);
+ const response = await request(app.getHttpServer()).post(`/api/data_queries/${dataQuery.id}/run`);
expect(response.statusCode).toBe(401);
});
diff --git a/server/test/controllers/data_sources.e2e-spec.ts b/server/test/controllers/data_sources.e2e-spec.ts
index f36a61154d..69819e8052 100644
--- a/server/test/controllers/data_sources.e2e-spec.ts
+++ b/server/test/controllers/data_sources.e2e-spec.ts
@@ -7,8 +7,11 @@ import {
createUser,
createNestAppInstance,
createDataSource,
+ createAppGroupPermission,
} from '../test.helper';
import { Credential } from 'src/entities/credential.entity';
+import { getRepository } from 'typeorm';
+import { GroupPermission } from 'src/entities/group_permission.entity';
describe('data sources controller', () => {
let app: INestApplication;
@@ -21,30 +24,39 @@ describe('data sources controller', () => {
app = await createNestAppInstance();
});
- it('should be able to create data sources of an app only if admin/developer of same organization', async () => {
+ it('should be able to create data sources only if user has admin group or app update permission in same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users'],
organization: adminUserData.organization,
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
user: adminUserData.user,
});
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: false,
+ update: true,
+ delete: false,
+ });
+
const dataSourceParams = {
name: 'name',
options: [{ key: 'foo', value: 'bar', encrypted: 'true' }],
@@ -54,7 +66,7 @@ describe('data sources controller', () => {
for (const userData of [adminUserData, developerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/data_sources`)
+ .post(`/api/data_sources`)
.set('Authorization', authHeaderForUser(userData.user))
.send(dataSourceParams);
@@ -67,7 +79,7 @@ describe('data sources controller', () => {
// Should not update if viewer or if user of another org
for (const userData of [anotherOrgAdminUserData, viewerUserData]) {
const response = await request(app.getHttpServer())
- .post(`/data_sources`)
+ .post(`/api/data_sources`)
.set('Authorization', authHeaderForUser(userData.user))
.send(dataSourceParams);
@@ -75,29 +87,37 @@ describe('data sources controller', () => {
}
});
- it('should be able to update data sources of an app only if admin/developer of same organization', async () => {
+ it('should be able to update data sources only if user has group admin or app update permission in same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users', 'developer'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users', 'viewer'],
organization: adminUserData.organization,
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
user: adminUserData.user,
});
+ const developerUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'developer',
+ });
+ await createAppGroupPermission(app, application, developerUserGroup.id, {
+ read: false,
+ update: true,
+ delete: false,
+ });
const dataSource = await createDataSource(app, {
name: 'name',
options: [{ key: 'foo', value: 'bar', encrypted: 'true' }],
@@ -115,7 +135,7 @@ describe('data sources controller', () => {
{ key: 'foo', value: 'baz', encrypted: 'true' },
];
const response = await request(app.getHttpServer())
- .put(`/data_sources/${dataSource.id}`)
+ .put(`/api/data_sources/${dataSource.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
options: newOptions,
@@ -136,7 +156,7 @@ describe('data sources controller', () => {
{ key: 'foo', value: 'baz', encrypted: 'true' },
];
const response = await request(app.getHttpServer())
- .put(`/data_sources/${dataSource.id}`)
+ .put(`/api/data_sources/${dataSource.id}`)
.set('Authorization', authHeaderForUser(userData.user))
.send({
options: newOptions,
@@ -146,19 +166,19 @@ describe('data sources controller', () => {
}
});
- it('should be able to list (get) datasources for an app only if admin/developer of same organization', async () => {
+ it('should be able to list (get) datasources for an app by all users of same organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['all_users'],
organization: adminUserData.organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['all_users'],
organization: adminUserData.organization,
});
const application = await createApplication(app, {
@@ -167,19 +187,28 @@ describe('data sources controller', () => {
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const dataSource = await createDataSource(app, {
+ await createDataSource(app, {
name: 'name',
kind: 'postgres',
application: application,
user: adminUserData.user,
});
+ const allUserGroup = await getRepository(GroupPermission).findOne({
+ group: 'all_users',
+ organizationId: adminUserData.organization.id,
+ });
+ await createAppGroupPermission(app, application, allUserGroup.id, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+
for (const userData of [adminUserData, developerUserData, viewerUserData]) {
const response = await request(app.getHttpServer())
- .get(`/data_sources?app_id=${application.id}`)
+ .get(`/api/data_sources?app_id=${application.id}`)
.set('Authorization', authHeaderForUser(userData.user));
expect(response.statusCode).toBe(200);
@@ -188,7 +217,7 @@ describe('data sources controller', () => {
// Forbidden if user of another organization
const response = await request(app.getHttpServer())
- .get(`/data_sources?app_id=${application.id}`)
+ .get(`/api/data_sources?app_id=${application.id}`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user));
expect(response.statusCode).toBe(403);
@@ -197,11 +226,11 @@ describe('data sources controller', () => {
it('should not be able to authorize OAuth code for a REST API source if user of another organization', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const anotherOrgAdminUserData = await createUser(app, {
email: 'another@tooljet.io',
- role: 'admin',
+ groups: ['all_users', 'admin'],
});
const application = await createApplication(app, {
name: 'name',
@@ -217,7 +246,7 @@ describe('data sources controller', () => {
// Should not update if user of another org
const response = await request(app.getHttpServer())
- .post(`/data_sources/${dataSource.id}/authorize_oauth2`)
+ .post(`/api/data_sources/${dataSource.id}/authorize_oauth2`)
.set('Authorization', authHeaderForUser(anotherOrgAdminUserData.user))
.send({
code: 'oauth-auth-code',
diff --git a/server/test/controllers/group_permissions.e2e-spec.ts b/server/test/controllers/group_permissions.e2e-spec.ts
new file mode 100644
index 0000000000..e8175f8b4e
--- /dev/null
+++ b/server/test/controllers/group_permissions.e2e-spec.ts
@@ -0,0 +1,656 @@
+import * as request from 'supertest';
+import { INestApplication } from '@nestjs/common';
+import { authHeaderForUser, clearDB, createUser, createNestAppInstance, createApplication } from '../test.helper';
+import { getManager } from 'typeorm';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+
+describe('group permissions controller', () => {
+ let nestApp: INestApplication;
+
+ beforeEach(async () => {
+ await clearDB();
+ });
+
+ beforeAll(async () => {
+ nestApp = await createNestAppInstance();
+ });
+
+ describe('POST /group_permissions', () => {
+ it('should not allow non admin to create group permission', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(defaultUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should be able to create group permission for authenticated admin', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(201);
+ expect(response.body.group).toBe('avengers');
+ expect(response.body.organization_id).toBe(organization.id);
+ expect(response.body.id).toBeDefined();
+ expect(response.body.created_at).toBeDefined();
+ expect(response.body.updated_at).toBeDefined();
+ });
+
+ it('should validate uniqueness of group permission group name', async () => {
+ const {
+ organization: { adminUser },
+ } = await setupOrganizations(nestApp);
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(201);
+
+ response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ // FIXME: setup postgres error codes and handle error gracefully
+ expect(response.statusCode).toBe(500);
+ });
+
+ it('should allow different organization to have same group name', async () => {
+ const {
+ organization: { adminUser },
+ anotherOrganization: { anotherAdminUser },
+ } = await setupOrganizations(nestApp);
+
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(201);
+
+ response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(anotherAdminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(201);
+ });
+ });
+
+ describe('GET /group_permissions/:id', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions/id')
+ .set('Authorization', authHeaderForUser(defaultUser));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should get group permission for authenticated admin within organization', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ const groupPermissionId = response.body.id;
+
+ response = await request(nestApp.getHttpServer())
+ .get(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser));
+
+ expect(response.statusCode).toBe(200);
+ expect(response.body.group).toBe('avengers');
+ expect(response.body.organization_id).toBe(organization.id);
+ expect(response.body.id).toBeDefined();
+ expect(response.body.created_at).toBeDefined();
+ expect(response.body.updated_at).toBeDefined();
+ });
+
+ it('should not get group permission for authenticated admin not within organization', async () => {
+ const {
+ organization: { adminUser },
+ anotherOrganization: { anotherAdminUser },
+ } = await setupOrganizations(nestApp);
+
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ const groupPermissionId = response.body.id;
+
+ response = await request(nestApp.getHttpServer())
+ .post(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(anotherAdminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(404);
+ });
+ });
+
+ describe('PUT /group_permissions/:id', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .put('/api/group_permissions/id')
+ .set('Authorization', authHeaderForUser(defaultUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to add and remove apps to group permission', async () => {
+ const {
+ organization: { adminUser, app },
+ } = await setupOrganizations(nestApp);
+
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ const groupPermissionId = response.body.id;
+
+ response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ add_apps: [app.id] });
+
+ expect(response.statusCode).toBe(200);
+
+ const manager = getManager();
+ let appsInGroup = await manager.find(AppGroupPermission, {
+ where: { groupPermissionId },
+ });
+
+ expect(appsInGroup).toHaveLength(1);
+
+ const addedApp = appsInGroup[0];
+
+ expect(addedApp.appId).toBe(app.id);
+ expect(addedApp.read).toBe(true);
+ expect(addedApp.update).toBe(false);
+ expect(addedApp.delete).toBe(false);
+
+ response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ remove_apps: [app.id] });
+
+ expect(response.statusCode).toBe(200);
+
+ appsInGroup = await manager.find(AppGroupPermission, {
+ where: { groupPermissionId },
+ });
+
+ expect(appsInGroup).toHaveLength(0);
+ });
+
+ it('should allow admin to add and remove users to group permission', async () => {
+ const {
+ organization: { adminUser, defaultUser },
+ } = await setupOrganizations(nestApp);
+
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ const groupPermissionId = response.body.id;
+
+ response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ add_users: [defaultUser.id] });
+
+ expect(response.statusCode).toBe(200);
+
+ const manager = getManager();
+ let usersInGroup = await manager.find(UserGroupPermission, {
+ where: { groupPermissionId },
+ });
+
+ expect(usersInGroup).toHaveLength(1);
+
+ const addedUser = usersInGroup[0];
+
+ expect(addedUser.userId).toBe(defaultUser.id);
+
+ response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ remove_users: [defaultUser.id] });
+
+ expect(response.statusCode).toBe(200);
+
+ usersInGroup = await manager.find(UserGroupPermission, {
+ where: { groupPermissionId },
+ });
+
+ expect(usersInGroup).toHaveLength(0);
+ });
+
+ it('should not allow to remove users from admin group permission without any atleast one active admin', async () => {
+ const {
+ organization: { adminUser, defaultUser },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ group: 'admin',
+ });
+
+ const response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${adminGroupPermission.id}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ remove_users: [defaultUser.id] });
+
+ expect(response.statusCode).toBe(400);
+ expect(response.body.message).toBe('Atleast one active admin is required.');
+ });
+
+ it('should not allow to remove any users from all_users group permission', async () => {
+ const {
+ organization: { adminUser, defaultUser },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ group: 'all_users',
+ });
+
+ const response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${adminGroupPermission.id}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ remove_users: [defaultUser.id] });
+
+ expect(response.statusCode).toBe(400);
+ expect(response.body.message).toBe('Cannot remove user from default group.');
+ });
+ });
+
+ describe('GET /group_permissions', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(defaultUser));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to list group permission', async () => {
+ const {
+ organization: { adminUser, defaultUser, app, organization },
+ } = await setupOrganizations(nestApp);
+
+ // create group permission
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(201);
+
+ const groupPermissionId = response.body.id;
+
+ // add apps and users to group permission
+ response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ add_apps: [app.id], add_users: [defaultUser.id] });
+
+ expect(response.statusCode).toBe(200);
+
+ // list group permission
+ response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser));
+ expect(response.statusCode).toBe(200);
+
+ const groupPermissions = response.body.group_permissions;
+ const groups = groupPermissions.map((gp) => gp.group);
+ const organizationId = [...new Set(groupPermissions.map((gp) => gp.organization_id))];
+
+ expect(new Set(groups)).toEqual(new Set(['avengers', 'all_users', 'admin']));
+ expect(organizationId).toEqual([organization.id]);
+ });
+ });
+
+ describe('GET /group_permissions/:id/apps', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions/id/apps')
+ .set('Authorization', authHeaderForUser(defaultUser));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to list apps in group permission', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ group: 'admin',
+ organizationId: organization.id,
+ });
+
+ const response = await request(nestApp.getHttpServer())
+ .get(`/api/group_permissions/${adminGroupPermission.id}/apps`)
+ .set('Authorization', authHeaderForUser(adminUser));
+
+ expect(response.statusCode).toBe(200);
+
+ const apps = response.body.apps;
+ const sampleApp = apps[0];
+
+ expect(apps).toHaveLength(1);
+ expect(sampleApp.organization_id).toBe(organization.id);
+ expect(sampleApp.name).toBe('sample app');
+
+ expect(sampleApp.group_permissions).toHaveLength(1);
+ expect(sampleApp.group_permissions[0].group).toBe('admin');
+
+ expect(sampleApp.app_group_permissions).toHaveLength(1);
+ expect(sampleApp.app_group_permissions[0].group_permission_id).toBe(sampleApp.group_permissions[0].id);
+ expect(sampleApp.app_group_permissions[0].read).toBe(true);
+ expect(sampleApp.app_group_permissions[0].update).toBe(true);
+ expect(sampleApp.app_group_permissions[0].delete).toBe(true);
+ });
+ });
+
+ describe('GET /group_permissions/:id/addable_apps', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions/id/addable_apps')
+ .set('Authorization', authHeaderForUser(defaultUser));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to list apps not in group permission', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+
+ // create group permission
+ let response = await request(nestApp.getHttpServer())
+ .post('/api/group_permissions')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ group: 'avengers' });
+
+ expect(response.statusCode).toBe(201);
+
+ const groupPermissionId = response.body.id;
+
+ response = await request(nestApp.getHttpServer())
+ .get(`/api/group_permissions/${groupPermissionId}/addable_apps`)
+ .set('Authorization', authHeaderForUser(adminUser));
+
+ expect(response.statusCode).toBe(200);
+
+ const apps = response.body.apps;
+ const sampleApp = apps[0];
+
+ expect(apps).toHaveLength(1);
+ expect(sampleApp.organization_id).toBe(organization.id);
+ expect(sampleApp.name).toBe('sample app');
+ expect(sampleApp.group_permissions).toHaveLength(2);
+
+ const adminGroupPermission = sampleApp.group_permissions.find((a) => a.group == 'admin');
+ const adminAppGroupPermission = sampleApp.app_group_permissions.find(
+ (a) => a.group_permission_id == adminGroupPermission.id
+ );
+ expect(adminAppGroupPermission.read).toBe(true);
+ expect(adminAppGroupPermission.update).toBe(true);
+ expect(adminAppGroupPermission.delete).toBe(true);
+
+ const userGroupPermission = sampleApp.group_permissions.find((a) => a.group == 'all_users');
+ const userAppGroupPermission = sampleApp.app_group_permissions.find(
+ (a) => a.group_permission_id == userGroupPermission.id
+ );
+ expect(userAppGroupPermission.read).toBe(true);
+ expect(userAppGroupPermission.update).toBe(false);
+ expect(userAppGroupPermission.delete).toBe(false);
+ });
+ });
+
+ describe('GET /group_permissions/:id/users', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions/id/users')
+ .set('Authorization', authHeaderForUser(defaultUser));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to list users in group permission', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ group: 'admin',
+ organizationId: organization.id,
+ });
+
+ const response = await request(nestApp.getHttpServer())
+ .get(`/api/group_permissions/${adminGroupPermission.id}/users`)
+ .set('Authorization', authHeaderForUser(adminUser));
+
+ expect(response.statusCode).toBe(200);
+
+ const users = response.body.users;
+ const user = users[0];
+
+ expect(users).toHaveLength(1);
+ expect(user.organization_id).toBe(organization.id);
+ expect(user.email).toBe('admin@tooljet.io');
+ });
+ });
+
+ describe('GET /group_permissions/:id/addable_users', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .get('/api/group_permissions/id/addable_users')
+ .set('Authorization', authHeaderForUser(defaultUser));
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to list users not in group permission', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ group: 'admin',
+ organizationId: organization.id,
+ });
+ const groupPermissionId = adminGroupPermission.id;
+ const response = await request(nestApp.getHttpServer())
+ .get(`/api/group_permissions/${groupPermissionId}/addable_users`)
+ .set('Authorization', authHeaderForUser(adminUser));
+
+ expect(response.statusCode).toBe(200);
+
+ const users = response.body.users;
+ const user = users[0];
+
+ expect(users).toHaveLength(1);
+ expect(user.organization_id).toBe(organization.id);
+ expect(user.email).toBe('developer@tooljet.io');
+ });
+ });
+
+ describe('PUT /group_permissions/:id/app_group_permissions/:appGroupPermisionId', () => {
+ it('should not allow unauthenicated admin', async () => {
+ const {
+ organization: { defaultUser },
+ } = await setupOrganizations(nestApp);
+ const response = await request(nestApp.getHttpServer())
+ .put('/api/group_permissions/id/app_group_permissions/id')
+ .set('Authorization', authHeaderForUser(defaultUser))
+ .send({ read: true });
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow admin to update app group permission', async () => {
+ const {
+ organization: { adminUser, organization },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ where: {
+ organizationId: organization.id,
+ group: 'all_users',
+ },
+ });
+ const groupPermissionId = adminGroupPermission.id;
+ const appGroupPermission = await manager.findOne(AppGroupPermission, {
+ groupPermissionId,
+ });
+ const appGroupPermissionId = appGroupPermission.id;
+
+ expect(appGroupPermission.read).toBe(true);
+ expect(appGroupPermission.update).toBe(false);
+
+ const response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}/app_group_permissions/${appGroupPermissionId}`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ actions: { read: false, update: true } });
+
+ expect(response.statusCode).toBe(200);
+
+ await appGroupPermission.reload();
+
+ expect(appGroupPermission.read).toBe(false);
+ expect(appGroupPermission.update).toBe(true);
+ });
+
+ it('should not allow admin to update app group permission of different organization', async () => {
+ const {
+ organization: { organization },
+ anotherOrganization: { anotherAdminUser },
+ } = await setupOrganizations(nestApp);
+
+ const manager = getManager();
+ const adminGroupPermission = await manager.findOne(GroupPermission, {
+ where: {
+ organizationId: organization.id,
+ group: 'all_users',
+ },
+ });
+ const groupPermissionId = adminGroupPermission.id;
+ const appGroupPermission = await manager.findOne(AppGroupPermission, {
+ groupPermissionId,
+ });
+ const appGroupPermissionId = appGroupPermission.id;
+
+ expect(appGroupPermission.read).toBe(true);
+ expect(appGroupPermission.update).toBe(false);
+
+ const response = await request(nestApp.getHttpServer())
+ .put(`/api/group_permissions/${groupPermissionId}/app_group_permissions/${appGroupPermissionId}`)
+ .set('Authorization', authHeaderForUser(anotherAdminUser))
+ .send({ actions: { read: false, update: true } });
+
+ expect(response.statusCode).toBe(400);
+ });
+ });
+
+ async function setupOrganizations(nestApp) {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ const organization = adminUserData.organization;
+ const defaultUserData = await createUser(nestApp, {
+ email: 'developer@tooljet.io',
+ groups: ['all_users'],
+ organization,
+ });
+ const defaultUser = defaultUserData.user;
+
+ const app = await createApplication(nestApp, {
+ user: adminUser,
+ name: 'sample app',
+ isPublic: false,
+ });
+
+ const anotherAdminUserData = await createUser(nestApp, {
+ email: 'another_admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const anotherAdminUser = anotherAdminUserData.user;
+ const anotherOrganization = anotherAdminUserData.organization;
+ const anotherDefaultUserData = await createUser(nestApp, {
+ email: 'another_developer@tooljet.io',
+ groups: ['all_users'],
+ anotherOrganization,
+ });
+ const anotherDefaultUser = anotherDefaultUserData.user;
+
+ const anotherApp = await createApplication(nestApp, {
+ user: anotherAdminUser,
+ name: 'another app',
+ isPublic: false,
+ });
+
+ return {
+ organization: { adminUser, defaultUser, organization, app },
+ anotherOrganization: {
+ anotherAdminUser,
+ anotherDefaultUser,
+ anotherOrganization,
+ anotherApp,
+ },
+ };
+ }
+
+ afterAll(async () => {
+ await nestApp.close();
+ });
+});
diff --git a/server/test/controllers/organization_users.e2e-spec.ts b/server/test/controllers/organization_users.e2e-spec.ts
index 64f41acbba..50df54f076 100644
--- a/server/test/controllers/organization_users.e2e-spec.ts
+++ b/server/test/controllers/organization_users.e2e-spec.ts
@@ -16,70 +16,70 @@ describe('organization users controller', () => {
it('should allow only admin to be able to invite new users', async () => {
// setup a pre existing user of different organization
- await createUser(app, { email: 'someUser@tooljet.io', role: 'admin' });
+ await createUser(app, { email: 'someUser@tooljet.io', groups: ['admin', 'all_users'] });
// setup organization and user setup to test against
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['admin', 'all_users'],
});
const organization = adminUserData.organization;
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['developer', 'all_users'],
organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['viewer', 'all_users'],
organization,
});
await request(app.getHttpServer())
- .post(`/organization_users`)
+ .post(`/api/organization_users`)
.set('Authorization', authHeaderForUser(adminUserData.user))
- .send({ email: 'test@tooljet.io', role: 'Viewer' })
+ .send({ email: 'test@tooljet.io', groups: ['Viewer', 'all_users'] })
.expect(201);
await request(app.getHttpServer())
- .post(`/organization_users`)
+ .post(`/api/organization_users`)
.set('Authorization', authHeaderForUser(developerUserData.user))
- .send({ email: 'test2@tooljet.io', role: 'Viewer' })
+ .send({ email: 'test2@tooljet.io', groups: ['Viewer', 'all_users'] })
.expect(403);
await request(app.getHttpServer())
- .post(`/organization_users`)
+ .post(`/api/organization_users`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
- .send({ email: 'test3@tooljet.io', role: 'Viewer' })
+ .send({ email: 'test3@tooljet.io', groups: ['Viewer', 'all_users'] })
.expect(403);
});
it('should allow only authenticated users to archive org users', async () => {
- await request(app.getHttpServer()).post('/organization_users/random-id/archive').expect(401);
+ await request(app.getHttpServer()).post('/api/organization_users/random-id/archive/').expect(401);
});
it('should allow only admin users to archive org users', async () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
+ groups: ['admin', 'all_users'],
});
const organization = adminUserData.organization;
const developerUserData = await createUser(app, {
email: 'developer@tooljet.io',
- role: 'developer',
+ groups: ['developer', 'all_users'],
organization,
});
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
- role: 'viewer',
+ groups: ['viewer', 'all_users'],
organization,
});
let response = await request(app.getHttpServer())
- .post(`/organization_users/${adminUserData.orgUser.id}/archive`)
+ .post(`/api/organization_users/${adminUserData.orgUser.id}/archive`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
.expect(403);
@@ -87,7 +87,7 @@ describe('organization users controller', () => {
expect(adminUserData.orgUser.status).toBe('invited');
response = await request(app.getHttpServer())
- .post(`/organization_users/${adminUserData.orgUser.id}/archive`)
+ .post(`/api/organization_users/${adminUserData.orgUser.id}/archive`)
.set('Authorization', authHeaderForUser(developerUserData.user))
.expect(403);
@@ -95,7 +95,7 @@ describe('organization users controller', () => {
expect(adminUserData.orgUser.status).toBe('invited');
response = await request(app.getHttpServer())
- .post(`/organization_users/${developerUserData.orgUser.id}/archive`)
+ .post(`/api/organization_users/${developerUserData.orgUser.id}/archive`)
.set('Authorization', authHeaderForUser(adminUserData.user))
.expect(201);
@@ -103,107 +103,6 @@ describe('organization users controller', () => {
expect(developerUserData.orgUser.status).toBe('archived');
});
- it('should allow only admin users to change role of org users', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
- const organization = adminUserData.organization;
- const developerUserData = await createUser(app, {
- email: 'developer@tooljet.io',
- role: 'developer',
- organization,
- });
- const viewerUserData = await createUser(app, {
- email: 'viewer@tooljet.io',
- role: 'viewer',
- organization,
- });
-
- let response = await request(app.getHttpServer())
- .post(`/organization_users/${viewerUserData.orgUser.id}/change_role`)
- .set('Authorization', authHeaderForUser(developerUserData.user))
- .send({ role: 'developer' })
- .expect(403);
-
- await viewerUserData.orgUser.reload();
- expect(viewerUserData.orgUser.role).toBe('viewer');
-
- response = await request(app.getHttpServer())
- .post(`/organization_users/${viewerUserData.orgUser.id}/change_role`)
- .set('Authorization', authHeaderForUser(viewerUserData.user))
- .send({ role: 'viewer' })
- .expect(403);
-
- await developerUserData.orgUser.reload();
- expect(developerUserData.orgUser.role).toBe('developer');
-
- response = await request(app.getHttpServer())
- .post(`/organization_users/${developerUserData.orgUser.id}/change_role`)
- .set('Authorization', authHeaderForUser(adminUserData.user))
- .send({ role: 'viewer' })
- .expect(201);
-
- await developerUserData.orgUser.reload();
- expect(developerUserData.orgUser.role).toBe('viewer');
- });
-
- it('should allow only admin users to change role of org users', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
- const developerUserData = await createUser(app, {
- email: 'developer@tooljet.io',
- role: 'developer',
- });
-
- const response = await request(app.getHttpServer())
- .post(`/organization_users/${developerUserData.orgUser.id}/change_role`)
- .set('Authorization', authHeaderForUser(adminUserData.user))
- .send({ role: 'viewer' })
- .expect(403);
-
- await developerUserData.orgUser.reload();
- expect(developerUserData.orgUser.role).toBe('developer');
- });
-
- it('should not allow to change role from admin when no other admins are present', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- status: 'active',
- });
-
- const response = await request(app.getHttpServer())
- .post(`/organization_users/${adminUserData.orgUser.id}/change_role`)
- .set('Authorization', authHeaderForUser(adminUserData.user))
- .send({ role: 'viewer' })
- .expect(400);
-
- await adminUserData.orgUser.reload();
- expect(adminUserData.orgUser.role).toBe('admin');
- });
-
- it('should allow only admin users to archive org users', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
- const developerUserData = await createUser(app, {
- email: 'developer@tooljet.io',
- role: 'developer',
- });
-
- const response = await request(app.getHttpServer())
- .post(`/organization_users/${developerUserData.orgUser.id}/archive`)
- .set('Authorization', authHeaderForUser(adminUserData.user))
- .expect(403);
-
- await developerUserData.orgUser.reload();
- expect(developerUserData.orgUser.status).toBe('invited');
- });
-
afterAll(async () => {
await app.close();
});
diff --git a/server/test/controllers/organizations.e2e-spec.ts b/server/test/controllers/organizations.e2e-spec.ts
index d8302846be..cfd41a7e23 100644
--- a/server/test/controllers/organizations.e2e-spec.ts
+++ b/server/test/controllers/organizations.e2e-spec.ts
@@ -14,7 +14,7 @@ describe('organizations controller', () => {
});
it('should allow only authenticated users to list org users', async () => {
- await request(app.getHttpServer()).get('/organizations/users').expect(401);
+ await request(app.getHttpServer()).get('/api/organizations/users').expect(401);
});
it('should list organization users', async () => {
@@ -23,7 +23,7 @@ describe('organizations controller', () => {
const { organization, user, orgUser } = userData;
const response = await request(app.getHttpServer())
- .get('/organizations/users')
+ .get('/api/organizations/users')
.set('Authorization', authHeaderForUser(user));
expect(response.statusCode).toBe(200);
diff --git a/server/test/controllers/users.e2e-spec.ts b/server/test/controllers/users.e2e-spec.ts
index b0ff216e97..629fe6c7d9 100644
--- a/server/test/controllers/users.e2e-spec.ts
+++ b/server/test/controllers/users.e2e-spec.ts
@@ -22,7 +22,7 @@ describe('users controller', () => {
const [firstName, lastName] = ['Daenerys', 'Targaryen', 'drogo666'];
const response = await request(app.getHttpServer())
- .patch('/users/update')
+ .patch('/api/users/update')
.set('Authorization', authHeaderForUser(user))
.send({ firstName, lastName });
@@ -43,7 +43,7 @@ describe('users controller', () => {
const oldPassword = user.password;
const response = await request(app.getHttpServer())
- .patch('/users/change_password')
+ .patch('/api/users/change_password')
.set('Authorization', authHeaderForUser(user))
.send({ currentPassword: 'password', newPassword: 'new password' });
@@ -61,7 +61,7 @@ describe('users controller', () => {
const oldPassword = user.password;
const response = await request(app.getHttpServer())
- .patch('/users/change_password')
+ .patch('/api/users/change_password')
.set('Authorization', authHeaderForUser(user))
.send({ currentPassword: 'wrong password', newPassword: 'new password' });
diff --git a/server/test/services/app_import_export.service.spec.ts b/server/test/services/app_import_export.service.spec.ts
new file mode 100644
index 0000000000..e06edc85cd
--- /dev/null
+++ b/server/test/services/app_import_export.service.spec.ts
@@ -0,0 +1,242 @@
+import {
+ clearDB,
+ createUser,
+ createNestAppInstance,
+ createApplication,
+ createApplicationVersion,
+ createDataQuery,
+ createDataSource,
+} from '../test.helper';
+import { INestApplication } from '@nestjs/common';
+import { getManager, In } from 'typeorm';
+import { App } from 'src/entities/app.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { AppImportExportService } from '@services/app_import_export.service';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+
+describe('UsersService', () => {
+ let nestApp: INestApplication;
+ let service: AppImportExportService;
+
+ beforeEach(async () => {
+ await clearDB();
+ });
+
+ beforeAll(async () => {
+ nestApp = await createNestAppInstance();
+ service = nestApp.get(AppImportExportService);
+ });
+
+ describe('.export', () => {
+ it('should export app with empty related associations', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ const app = await createApplication(nestApp, {
+ user: adminUser,
+ name: 'sample app',
+ isPublic: true,
+ });
+
+ const result = await service.export(adminUser, app.id);
+
+ expect(result.id).toBe(app.id);
+ expect(result.name).toBe(app.name);
+ expect(result.isPublic).toBe(app.isPublic);
+ expect(result.organizationId).toBe(app.organizationId);
+ expect(result.currentVersionId).toBe(null);
+ expect(result.appVersions).toEqual([]);
+ expect(result.dataQueries).toEqual([]);
+ expect(result.dataSources).toEqual([]);
+ });
+
+ it('should export app', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ const application = await createApplication(nestApp, {
+ user: adminUser,
+ name: 'sample app',
+ isPublic: true,
+ });
+ await createApplicationVersion(nestApp, application);
+ const dataSource = await createDataSource(nestApp, {
+ application,
+ kind: 'test_kind',
+ name: 'test_name',
+ });
+ await createDataQuery(nestApp, {
+ application,
+ dataSource,
+ kind: 'test_kind',
+ });
+
+ const exportedApp = await getManager().findOne(App, application.id, {
+ relations: ['dataQueries', 'dataSources', 'appVersions'],
+ });
+
+ const result = await service.export(adminUser, exportedApp.id);
+
+ expect(result.id).toBe(exportedApp.id);
+ expect(result.name).toBe(exportedApp.name);
+ expect(result.isPublic).toBe(exportedApp.isPublic);
+ expect(result.organizationId).toBe(exportedApp.organizationId);
+ expect(result.currentVersionId).toBe(null);
+ expect(result.appVersions).toEqual(exportedApp.appVersions);
+ expect(result.dataQueries).toEqual(exportedApp.dataQueries);
+ expect(result.dataSources).toEqual(exportedApp.dataSources);
+ });
+ });
+
+ describe('.import', () => {
+ it('should throw error with invalid params', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ await expect(service.import(adminUser, 'hello world')).rejects.toThrow('Invalid params for app import');
+ });
+
+ it('should create apps with empty params', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ await service.import(adminUser, {});
+ const apps = await getManager().find(App);
+
+ expect(apps).toHaveLength(1);
+ });
+
+ it('should import app with empty related associations', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ const app = await createApplication(nestApp, {
+ user: adminUser,
+ name: 'sample app',
+ isPublic: true,
+ });
+
+ const exportedApp = await service.export(adminUser, app.id);
+
+ const result = await service.import(adminUser, exportedApp);
+ const importedApp = await getManager().findOne(App, result.id, {
+ relations: ['dataQueries', 'dataSources', 'appVersions'],
+ });
+
+ expect(importedApp.id == exportedApp.id).toBeFalsy();
+ expect(importedApp.name).toBe(exportedApp.name);
+ expect(importedApp.isPublic).toBe(exportedApp.isPublic);
+ expect(importedApp.organizationId).toBe(exportedApp.organizationId);
+ expect(importedApp.currentVersionId).toBe(null);
+ expect(importedApp.appVersions).toEqual([]);
+ expect(importedApp.dataQueries).toEqual([]);
+ expect(importedApp.dataSources).toEqual([]);
+
+ // assert group permissions are valid
+ const appGroupPermissions = await getManager().find(AppGroupPermission, {
+ appId: importedApp.id,
+ });
+ const groupPermissionIds = appGroupPermissions.map((agp) => agp.groupPermissionId);
+ const groupPermissions = await getManager().find(GroupPermission, {
+ id: In(groupPermissionIds),
+ });
+
+ expect(new Set(groupPermissions.map((gp) => gp.organizationId))).toEqual(new Set([adminUser.organizationId]));
+ expect(new Set(groupPermissions.map((gp) => gp.group))).toEqual(new Set(['admin']));
+ });
+
+ it('should import app with related associations', async () => {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ const application = await createApplication(nestApp, {
+ user: adminUser,
+ name: 'sample app',
+ isPublic: true,
+ });
+ await createApplicationVersion(nestApp, application);
+ let dataSource = await createDataSource(nestApp, {
+ application,
+ kind: 'test_kind',
+ name: 'test_name',
+ });
+ await createDataQuery(nestApp, {
+ application,
+ dataSource,
+ kind: 'test_kind',
+ });
+
+ const exportedApp = await service.export(adminUser, application.id);
+ const result = await service.import(adminUser, exportedApp);
+ const importedApp = await getManager().findOne(App, result.id, {
+ relations: ['dataQueries', 'dataSources', 'appVersions'],
+ });
+
+ expect(importedApp.id == exportedApp.id).toBeFalsy();
+ expect(importedApp.name).toBe(exportedApp.name);
+ expect(importedApp.isPublic).toBe(exportedApp.isPublic);
+ expect(importedApp.organizationId).toBe(exportedApp.organizationId);
+ expect(importedApp.currentVersionId).toBe(null);
+
+ // assert relations
+ const appVersion = importedApp.appVersions[0];
+ expect(appVersion.appId).toEqual(importedApp.id);
+
+ dataSource = importedApp.dataSources[0];
+ expect(dataSource.appId).toEqual(importedApp.id);
+
+ const dataQuery = importedApp.dataQueries[0];
+ expect(dataQuery.appId).toEqual(importedApp.id);
+ expect(dataQuery.dataSourceId).toEqual(dataSource.id);
+
+ // assert all fields except primary keys, foriegn keys and timestamps are same
+ const deleteFieldsNotToCheck = (entity) => {
+ delete entity.id;
+ delete entity.appId;
+ delete entity.dataSourceId;
+ delete entity.createdAt;
+ delete entity.updatedAt;
+
+ return entity;
+ };
+ const importedAppVersions = importedApp.appVersions.map((version) => deleteFieldsNotToCheck(version));
+ const exportedAppVersions = exportedApp.appVersions.map((version) => deleteFieldsNotToCheck(version));
+ const importedDataSources = importedApp.dataSources.map((source) => deleteFieldsNotToCheck(source));
+ const exportedDataSources = exportedApp.dataSources.map((source) => deleteFieldsNotToCheck(source));
+ const importedDataQueries = importedApp.dataQueries.map((query) => deleteFieldsNotToCheck(query));
+ const exportedDataQueries = exportedApp.dataQueries.map((query) => deleteFieldsNotToCheck(query));
+
+ expect(importedAppVersions).toEqual(exportedAppVersions);
+ expect(importedDataSources).toEqual(exportedDataSources);
+ expect(importedDataQueries).toEqual(exportedDataQueries);
+
+ // assert group permissions are valid
+ const appGroupPermissions = await getManager().find(AppGroupPermission, {
+ appId: importedApp.id,
+ });
+ const groupPermissionIds = appGroupPermissions.map((agp) => agp.groupPermissionId);
+ const groupPermissions = await getManager().find(GroupPermission, {
+ id: In(groupPermissionIds),
+ });
+
+ expect(new Set(groupPermissions.map((gp) => gp.organizationId))).toEqual(new Set([adminUser.organizationId]));
+ expect(new Set(groupPermissions.map((gp) => gp.group))).toEqual(new Set(['admin']));
+ });
+ });
+
+ afterAll(async () => {
+ await nestApp.close();
+ });
+});
diff --git a/server/src/services/data_queries.service.spec.ts b/server/test/services/data_queries.service.spec.ts
similarity index 97%
rename from server/src/services/data_queries.service.spec.ts
rename to server/test/services/data_queries.service.spec.ts
index ddf48ab02c..5d0bfc8684 100644
--- a/server/src/services/data_queries.service.spec.ts
+++ b/server/test/services/data_queries.service.spec.ts
@@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../../src/app.module';
import { DataQueriesModule } from '../../src/modules/data_queries/data_queries.module';
import { DataSourcesModule } from '../../src/modules/data_sources/data_sources.module';
-import { DataQueriesService } from './data_queries.service';
+import { DataQueriesService } from '../../src/services/data_queries.service';
describe('DataQueriesService', () => {
let service: DataQueriesService;
diff --git a/server/test/services/users.service.spec.ts b/server/test/services/users.service.spec.ts
new file mode 100644
index 0000000000..6cabf00dc1
--- /dev/null
+++ b/server/test/services/users.service.spec.ts
@@ -0,0 +1,289 @@
+import {
+ clearDB,
+ createUser,
+ createNestAppInstance,
+ createApplication,
+ createAppGroupPermission,
+ createUserGroupPermissions,
+ createGroupPermission,
+} from '../test.helper';
+import { UsersService } from '../../src/services/users.service';
+import { INestApplication } from '@nestjs/common';
+import { getManager } from 'typeorm';
+import { User } from 'src/entities/user.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+
+describe('UsersService', () => {
+ let nestApp: INestApplication;
+ let service: UsersService;
+
+ beforeEach(async () => {
+ await clearDB();
+ });
+
+ beforeAll(async () => {
+ nestApp = await createNestAppInstance();
+ service = nestApp.get(UsersService);
+ });
+
+ describe('.create', () => {
+ it('should create user', async () => {
+ const { adminUser } = await setupOrganization(nestApp);
+
+ await service.create(
+ {
+ email: 'john@example.com',
+ firstName: 'John',
+ lastName: 'Wick',
+ },
+ adminUser.organization,
+ ['all_users']
+ );
+
+ const manager = getManager();
+ const newUser = await manager.findOne(User, { email: 'john@example.com' });
+ expect(newUser.firstName).toEqual('John');
+ expect(newUser.lastName).toEqual('Wick');
+ expect(newUser.organizationId).toBe(adminUser.organizationId);
+
+ // expect default group permission is associated
+ const userGroups = await manager.find(UserGroupPermission, { userId: newUser.id });
+ expect(userGroups).toHaveLength(1);
+
+ const groupPermission = await manager.findOne(GroupPermission, { id: userGroups[0].groupPermissionId });
+ expect(groupPermission.group).toEqual('all_users');
+ expect(groupPermission.organizationId).toEqual(adminUser.organizationId);
+ });
+ });
+
+ describe('.update', () => {
+ it('should update user', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+
+ await service.update(defaultUser.id, { firstName: 'Updated Name' });
+ await defaultUser.reload();
+
+ expect(defaultUser.firstName).toEqual('Updated Name');
+ });
+
+ it('should throw error when adding non existent user groups', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+
+ await expect(service.update(defaultUser.id, { addGroups: ['admin', 'non-existent'] })).rejects.toThrow(
+ 'non-existent group does not exist for current organization'
+ );
+ });
+
+ it('should add user groups', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+ await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+
+ await service.update(defaultUser.id, { addGroups: ['new-group'] });
+ await defaultUser.reload();
+
+ const userGroups = (await defaultUser.groupPermissions).map((groupPermission) => groupPermission.group);
+
+ expect(userGroups.includes('new-group')).toBeTruthy;
+ });
+
+ it('should not add duplicate user groups', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+ await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+
+ await service.update(defaultUser.id, { addGroups: ['new-group'] });
+ await defaultUser.reload();
+
+ await service.update(defaultUser.id, { addGroups: ['new-group', 'new-group'] });
+ await defaultUser.reload();
+
+ const allUserGroups = (await defaultUser.groupPermissions).map((x) => x.group);
+ expect(new Set(allUserGroups)).toEqual(new Set(['all_users', 'new-group']));
+ });
+
+ it('should remove user groups', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+ await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+
+ await service.update(defaultUser.id, { addGroups: ['new-group'] });
+ await defaultUser.reload();
+ expect(await defaultUser.groupPermissions).toHaveLength(2);
+
+ await service.update(defaultUser.id, { removeGroups: ['new-group'] });
+ await defaultUser.reload();
+ const allUserGroups = (await defaultUser.groupPermissions).map((x) => x.group);
+ expect(new Set(allUserGroups)).toEqual(new Set(['all_users']));
+ });
+
+ it('should remove user groups only if it exists', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+ await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+
+ await service.update(defaultUser.id, { addGroups: ['new-group'] });
+ await defaultUser.reload();
+ expect(await defaultUser.groupPermissions).toHaveLength(2);
+
+ await service.update(defaultUser.id, { removeGroups: ['new-group', 'new-group', 'non-existent'] });
+ await defaultUser.reload();
+ const allUserGroups = (await defaultUser.groupPermissions).map((x) => x.group);
+ expect(new Set(allUserGroups)).toEqual(new Set(['all_users']));
+ });
+
+ it('should throw error when trying to remove admin user group if there is only one admin', async () => {
+ const { adminUser } = await setupOrganization(nestApp);
+
+ await expect(service.update(adminUser.id, { removeGroups: ['admin'] })).rejects.toThrow(
+ 'Atleast one active admin is required.'
+ );
+ });
+ });
+
+ describe('.groupPermissions', () => {
+ it('should return group permissions for the user', async () => {
+ const { adminUser, defaultUser } = await setupOrganization(nestApp);
+
+ await createGroupPermission(nestApp, { organizationId: adminUser.organizationId, group: 'group1' });
+ await service.update(adminUser.id, { addGroups: ['group1'] });
+ await adminUser.reload();
+
+ await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'group2' });
+ await service.update(defaultUser.id, { addGroups: ['group2'] });
+ await defaultUser.reload();
+
+ let groupPermissions = (await service.groupPermissions(adminUser)).map((x) => x.group);
+ expect(new Set(groupPermissions)).toEqual(new Set(['all_users', 'admin', 'group1']));
+
+ groupPermissions = (await service.groupPermissions(defaultUser)).map((x) => x.group);
+ expect(new Set(groupPermissions)).toEqual(new Set(['all_users', 'group2']));
+ });
+ });
+
+ describe('.appGroupPermissions', () => {
+ it('should return app group permissions for the user', async () => {
+ const { defaultUser, app } = await setupOrganization(nestApp);
+ const groupPermissionIdsFromApp = (await service.appGroupPermissions(defaultUser, app.id)).map(
+ (x) => x.groupPermissionId
+ );
+
+ const groupPermissionIds = (await service.groupPermissions(defaultUser))
+ .filter((x) => x.group == 'admin')
+ .map((x) => x.id);
+
+ expect(new Set(groupPermissionIdsFromApp)).toEqual(new Set(groupPermissionIds));
+ });
+ });
+
+ describe('.groupPermissionsForOrganization', () => {
+ it('should return all group permissions within organization', async () => {
+ const { defaultUser } = await setupOrganization(nestApp);
+ const groupPermissions = (await service.groupPermissionsForOrganization(defaultUser.organizationId)).map(
+ (x) => x.group
+ );
+
+ expect(new Set(groupPermissions)).toEqual(new Set(['all_users', 'admin']));
+ });
+ });
+
+ describe('.hasGroup', () => {
+ it('should return false if user has group', async () => {
+ const { adminUser } = await setupOrganization(nestApp);
+ expect(await service.hasGroup(adminUser, 'admin')).toBeTruthy();
+ });
+
+ it('should return true if user has group', async () => {
+ const { adminUser } = await setupOrganization(nestApp);
+ expect(await service.hasGroup(adminUser, 'superduper-admin')).toBeFalsy();
+ });
+ });
+
+ describe('.userCan', () => {
+ describe('perform action on invalid entity', () => {
+ it('should return false', async () => {
+ const { adminUser, app } = await setupOrganization(nestApp);
+
+ expect(await service.userCan(adminUser, 'create', 'Ice cream', app.id)).toEqual(false);
+ expect(await service.userCan(adminUser, 'read', 'Ice cream', app.id)).toEqual(false);
+ expect(await service.userCan(adminUser, 'update', 'Ice cream', app.id)).toEqual(false);
+ expect(await service.userCan(adminUser, 'delete', 'Ice cream', app.id)).toEqual(false);
+ });
+ });
+
+ describe("perform action on 'App' entity", () => {
+ it('should return boolean based on permissible actions', async () => {
+ const { adminUser, app } = await setupOrganization(nestApp);
+
+ expect(await service.userCan(adminUser, 'create', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(adminUser, 'read', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(adminUser, 'update', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(adminUser, 'delete', 'App', app.id)).toEqual(true);
+ });
+
+ it('should allow actions with custom groups based on app permissions', async () => {
+ const { defaultUser, app } = await setupOrganization(nestApp);
+ const userGroups = await createUserGroupPermissions(nestApp, defaultUser, ['developer']);
+ const developerUserGroup = userGroups[0];
+ await createAppGroupPermission(nestApp, app, developerUserGroup.groupPermissionId, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+
+ expect(await service.userCan(defaultUser, 'create', 'App', app.id)).toEqual(false);
+ expect(await service.userCan(defaultUser, 'read', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(defaultUser, 'update', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(defaultUser, 'delete', 'App', app.id)).toEqual(false);
+ });
+
+ it('should opt the permissible group among multiple groups', async () => {
+ const { defaultUser, app } = await setupOrganization(nestApp);
+ const userGroups = await createUserGroupPermissions(nestApp, defaultUser, ['updater', 'deleter']);
+
+ const updaterUserGroup = userGroups[0];
+ await createAppGroupPermission(nestApp, app, updaterUserGroup.groupPermissionId, {
+ read: true,
+ update: true,
+ delete: false,
+ });
+
+ const deleterUserGroup = userGroups[1];
+ await createAppGroupPermission(nestApp, app, deleterUserGroup.groupPermissionId, {
+ read: false,
+ update: false,
+ delete: true,
+ });
+
+ expect(await service.userCan(defaultUser, 'create', 'App', app.id)).toEqual(false);
+ expect(await service.userCan(defaultUser, 'read', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(defaultUser, 'update', 'App', app.id)).toEqual(true);
+ expect(await service.userCan(defaultUser, 'delete', 'App', app.id)).toEqual(true);
+ });
+ });
+ });
+
+ async function setupOrganization(nestApp) {
+ const adminUserData = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+ const adminUser = adminUserData.user;
+ const organization = adminUserData.organization;
+ const defaultUserData = await createUser(nestApp, {
+ email: 'developer@tooljet.io',
+ groups: ['all_users'],
+ organization,
+ });
+ const defaultUser = defaultUserData.user;
+
+ const app = await createApplication(nestApp, {
+ user: adminUser,
+ name: 'sample app',
+ isPublic: false,
+ });
+
+ return { adminUser, defaultUser, app };
+ }
+
+ afterAll(async () => {
+ await nestApp.close();
+ });
+});
diff --git a/server/test/test.helper.ts b/server/test/test.helper.ts
index 7d999666d6..0d0e48bd09 100644
--- a/server/test/test.helper.ts
+++ b/server/test/test.helper.ts
@@ -9,12 +9,14 @@ import { App } from 'src/entities/app.entity';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { AppModule } from 'src/app.module';
-import { AppUser } from 'src/entities/app_user.entity';
import { AppVersion } from 'src/entities/app_version.entity';
import { DataQuery } from 'src/entities/data_query.entity';
import { DataSource } from 'src/entities/data_source.entity';
import { DataSourcesService } from 'src/services/data_sources.service';
import { DataSourcesModule } from 'src/modules/data_sources/data_sources.module';
+import { GroupPermission } from 'src/entities/group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
export async function createNestAppInstance() {
let app: INestApplication;
@@ -25,12 +27,13 @@ export async function createNestAppInstance() {
}).compile();
app = moduleRef.createNestApplication();
+ app.setGlobalPrefix('api');
await app.init();
return app;
}
-export function authHeaderForUser(user: any) {
+export function authHeaderForUser(user: any): string {
const configService = new ConfigService();
const jwtService = new JwtService({
secret: configService.get('SECRET_KEY_BASE'),
@@ -48,18 +51,17 @@ export async function clearDB() {
}
}
-export async function createApplication(app, { name, user, isPublic }: any) {
+export async function createApplication(nestApp, { name, user, isPublic, slug }: any) {
let appRepository: Repository;
- appRepository = app.get('AppRepository');
- let appUsersRepository: Repository;
- appUsersRepository = app.get('AppUserRepository');
+ appRepository = nestApp.get('AppRepository');
- user = user || (await (await createUser(app, {})).user);
+ user = user || (await (await createUser(nestApp, {})).user);
const newApp = await appRepository.save(
appRepository.create({
name,
user,
+ slug,
isPublic: isPublic || false,
organizationId: user.organization.id,
createdAt: new Date(),
@@ -67,22 +69,15 @@ export async function createApplication(app, { name, user, isPublic }: any) {
})
);
- await appUsersRepository.save(
- appUsersRepository.create({
- app: newApp,
- user,
- role: 'admin',
- createdAt: new Date(),
- updatedAt: new Date(),
- })
- );
+ await maybeCreateAdminAppGroupPermissions(nestApp, newApp);
+ await maybeCreateAllUsersAppGroupPermissions(nestApp, newApp);
return newApp;
}
-export async function createApplicationVersion(app, application) {
+export async function createApplicationVersion(nestApp, application) {
let appVersionsRepository: Repository;
- appVersionsRepository = app.get('AppVersionRepository');
+ appVersionsRepository = nestApp.get('AppVersionRepository');
return await appVersionsRepository.save(
appVersionsRepository.create({
@@ -92,14 +87,14 @@ export async function createApplicationVersion(app, application) {
);
}
-export async function createUser(app, { firstName, lastName, email, role, organization, status }: any) {
+export async function createUser(nestApp, { firstName, lastName, email, groups, organization, status }: any) {
let userRepository: Repository;
let organizationRepository: Repository;
let organizationUsersRepository: Repository;
- userRepository = app.get('UserRepository');
- organizationRepository = app.get('OrganizationRepository');
- organizationUsersRepository = app.get('OrganizationUserRepository');
+ userRepository = nestApp.get('UserRepository');
+ organizationRepository = nestApp.get('OrganizationRepository');
+ organizationUsersRepository = nestApp.get('OrganizationUserRepository');
organization =
organization ||
@@ -127,21 +122,180 @@ export async function createUser(app, { firstName, lastName, email, role, organi
organizationUsersRepository.create({
user: user,
organization,
- role: role || 'admin',
status: status || 'invited',
+ role: 'all_users',
createdAt: new Date(),
updatedAt: new Date(),
})
);
+ await maybeCreateDefaultGroupPermissions(nestApp, user.organizationId);
+ await createUserGroupPermissions(
+ nestApp,
+ user,
+ groups || ['all_users', 'admin'] // default groups
+ );
+
return { organization, user, orgUser };
}
-export async function createDataSource(nestInstance, { name, application, kind, options }: any) {
- let dataSourceRepository: Repository;
- dataSourceRepository = nestInstance.get('DataSourceRepository');
+export async function createUserGroupPermissions(nestApp, user, groups) {
+ const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
- const dataSourcesService = nestInstance.select(DataSourcesModule).get(DataSourcesService);
+ const userGroupPermissionRepository: Repository = nestApp.get('UserGroupPermissionRepository');
+
+ let userGroupPermissions = [];
+
+ for (const group of groups) {
+ let groupPermission: GroupPermission;
+
+ if (group == 'admin' || group == 'all_users') {
+ groupPermission = await groupPermissionRepository.findOneOrFail({
+ where: {
+ organizationId: user.organizationId,
+ group: group,
+ },
+ });
+ } else {
+ groupPermission = groupPermissionRepository.create({
+ organizationId: user.organizationId,
+ group: group,
+ });
+ await groupPermissionRepository.save(groupPermission);
+ }
+
+ const userGroupPermission = userGroupPermissionRepository.create({
+ groupPermissionId: groupPermission.id,
+ userId: user.id,
+ });
+ await userGroupPermissionRepository.save(userGroupPermission);
+ userGroupPermissions.push(userGroupPermission);
+ }
+
+ return userGroupPermissions;
+}
+
+export async function createAppGroupPermission(nestApp, app, groupId, permissions) {
+ const appGroupPermissionRepository: Repository = nestApp.get('AppGroupPermissionRepository');
+
+ const appGroupPermission = appGroupPermissionRepository.create({
+ groupPermissionId: groupId,
+ appId: app.id,
+ ...permissions,
+ });
+ await appGroupPermissionRepository.save(appGroupPermission);
+
+ return appGroupPermission;
+}
+
+export async function createGroupPermission(nestApp, params) {
+ const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
+ let groupPermission = groupPermissionRepository.create({
+ ...params,
+ });
+ await groupPermissionRepository.save(groupPermission);
+
+ return groupPermission;
+}
+
+export async function maybeCreateDefaultGroupPermissions(nestApp, organizationId) {
+ const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
+
+ const defaultGroups = ['all_users', 'admin'];
+
+ for (let group of defaultGroups) {
+ const orgDefaultGroupPermissions = await groupPermissionRepository.find({
+ where: {
+ organizationId: organizationId,
+ group: group,
+ },
+ });
+
+ if (orgDefaultGroupPermissions.length == 0) {
+ const groupPermission = groupPermissionRepository.create({
+ organizationId: organizationId,
+ group: group,
+ });
+ await groupPermissionRepository.save(groupPermission);
+ }
+ }
+}
+
+export async function maybeCreateAdminAppGroupPermissions(nestApp, app) {
+ const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
+ const appGroupPermissionRepository: Repository = nestApp.get('AppGroupPermissionRepository');
+
+ const orgAdminGroupPermissions = await groupPermissionRepository.findOne({
+ organizationId: app.organizationId,
+ group: 'admin',
+ });
+
+ if (orgAdminGroupPermissions) {
+ const adminGroupPermissions = {
+ read: true,
+ update: true,
+ delete: true,
+ };
+
+ const appGroupPermission = appGroupPermissionRepository.create({
+ groupPermissionId: orgAdminGroupPermissions.id,
+ appId: app.id,
+ ...adminGroupPermissions,
+ });
+ await appGroupPermissionRepository.save(appGroupPermission);
+ }
+}
+
+export async function maybeCreateAllUsersAppGroupPermissions(nestApp, app) {
+ const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
+ const appGroupPermissionRepository: Repository = nestApp.get('AppGroupPermissionRepository');
+
+ const orgGroupPermissions = await groupPermissionRepository.findOne({
+ organizationId: app.organizationId,
+ group: 'all_users',
+ });
+
+ if (orgGroupPermissions) {
+ const permissions = {
+ read: true,
+ update: false,
+ delete: false,
+ };
+
+ const appGroupPermission = appGroupPermissionRepository.create({
+ groupPermissionId: orgGroupPermissions.id,
+ appId: app.id,
+ ...permissions,
+ });
+ await appGroupPermissionRepository.save(appGroupPermission);
+ }
+}
+
+export async function addAllUsersGroupToUser(nestApp, user) {
+ const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
+ const userGroupPermissionRepository: Repository = nestApp.get('UserGroupPermissionRepository');
+
+ const orgDefaultGroupPermissions = await groupPermissionRepository.findOne({
+ where: {
+ organizationId: user.organizationId,
+ group: 'all_users',
+ },
+ });
+
+ const userGroupPermission = userGroupPermissionRepository.create({
+ groupPermissionId: orgDefaultGroupPermissions.id,
+ userId: user.id,
+ });
+ await userGroupPermissionRepository.save(userGroupPermission);
+
+ return user;
+}
+
+export async function createDataSource(nestApp, { name, application, kind, options }: any) {
+ let dataSourceRepository: Repository;
+ dataSourceRepository = nestApp.get('DataSourceRepository');
+
+ const dataSourcesService = nestApp.select(DataSourcesModule).get(DataSourcesService);
return await dataSourceRepository.save(
dataSourceRepository.create({
@@ -155,9 +309,9 @@ export async function createDataSource(nestInstance, { name, application, kind,
);
}
-export async function createDataQuery(nestInstance, { application, kind, dataSource, options }: any) {
+export async function createDataQuery(nestApp, { application, kind, dataSource, options }: any) {
let dataQueryRepository: Repository;
- dataQueryRepository = nestInstance.get('DataQueryRepository');
+ dataQueryRepository = nestApp.get('DataQueryRepository');
return await dataQueryRepository.save(
dataQueryRepository.create({