- {admin && (
-
- Manage Users
-
- )}
- {admin && (
-
- Manage Groups
-
- )}
Profile
diff --git a/frontend/src/_components/Menu.jsx b/frontend/src/_components/Menu.jsx
new file mode 100644
index 0000000000..6f4dde36b6
--- /dev/null
+++ b/frontend/src/_components/Menu.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+
+export function Menu({ onChange, items, selected }) {
+ return (
+
+
+ {items &&
+ items.map((item) => (
+ onChange(item.id)} className={selected === item.id ? 'active' : ''}>
+ {item.label}
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/_components/Organization.jsx b/frontend/src/_components/Organization.jsx
new file mode 100644
index 0000000000..9b65d3f4a9
--- /dev/null
+++ b/frontend/src/_components/Organization.jsx
@@ -0,0 +1,349 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { authenticationService, organizationService } from '@/_services';
+import Modal from '../HomePage/Modal';
+import { toast } from 'react-hot-toast';
+import { SearchBox } from './SearchBox';
+
+export const Organization = function Organization() {
+ const isSingleOrganization = window.public_config?.MULTI_ORGANIZATION !== 'true';
+ const { admin, organization_id } = authenticationService.currentUserValue;
+ const [organization, setOrganization] = useState(authenticationService.currentUserValue?.organization);
+ const [showCreateOrg, setShowCreateOrg] = useState(false);
+ const [showEditOrg, setShowEditOrg] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+ const [searchText, setSearchText] = useState('');
+ const [organizationList, setOrganizationList] = useState([]);
+ const [getOrgStatus, setGetOrgStatus] = useState('loading');
+ const [isListOrganizations, setIsListOrganizations] = useState(false);
+ const [newOrgName, setNewOrgName] = useState('');
+
+ const getAvatar = (organization) => {
+ if (!organization) return;
+
+ const orgName = organization.split(' ');
+ if (orgName.length > 1) {
+ return `${orgName[0]?.[0]}${orgName[1]?.[0]}`;
+ } else {
+ return `${organization[0]}${organization[1]}`;
+ }
+ };
+
+ useEffect(() => {
+ !isSingleOrganization && getOrganizations();
+ }, [isSingleOrganization]);
+
+ const getOrganizations = () => {
+ setGetOrgStatus('loading');
+ organizationService.getOrganizations().then(
+ (data) => {
+ setOrganizationList(data.organizations);
+ setGetOrgStatus('success');
+ },
+ () => {
+ setGetOrgStatus('failure');
+ }
+ );
+ };
+
+ const showEditModal = () => {
+ setNewOrgName(organization);
+ setShowEditOrg(true);
+ };
+
+ const showCreateModal = () => {
+ setNewOrgName('');
+ setShowCreateOrg(true);
+ };
+
+ const createOrganization = () => {
+ if (!(newOrgName && newOrgName.trim())) {
+ toast.error("organization name can't be empty.", {
+ position: 'top-center',
+ });
+ return;
+ }
+ setIsCreating(true);
+ organizationService.createOrganization(newOrgName).then(
+ (data) => {
+ authenticationService.updateCurrentUserDetails(data);
+ window.location.href = '/';
+ },
+ () => {
+ toast.error('Error while creating organization', {
+ position: 'top-center',
+ });
+ }
+ );
+ setIsCreating(false);
+ };
+
+ const editOrganization = () => {
+ if (!(newOrgName && newOrgName.trim())) {
+ toast.error("organization name can't be empty.", {
+ position: 'top-center',
+ });
+ return;
+ }
+ setIsCreating(true);
+ organizationService.editOrganization({ name: newOrgName }).then(
+ () => {
+ authenticationService.updateCurrentUserDetails({ organization: newOrgName });
+ toast.success('Organization updated', {
+ position: 'top-center',
+ });
+ setOrganization(newOrgName);
+ },
+ () => {
+ toast.error('Error while editing organization', {
+ position: 'top-center',
+ });
+ }
+ );
+ setIsCreating(false);
+ setShowEditOrg(false);
+ };
+
+ const switchOrganization = (orgId) => {
+ organizationService.switchOrganization(orgId).then((response) => {
+ response.text().then((text) => {
+ if (!response.ok) {
+ return (window.location.href = `/login/${orgId}`);
+ }
+ const data = text && JSON.parse(text);
+ authenticationService.updateCurrentUserDetails(data);
+ window.location.href = '/';
+ });
+ });
+ };
+
+ const listOrganization = () => {
+ return (
+ organizationList &&
+ organizationList
+ .filter((org) => org.name.toLowerCase().includes(searchText ? searchText.toLowerCase() : ''))
+ .map((org) => {
+ return (
+
switchOrganization(org.id)}
+ className="dropdown-item org-list-item"
+ >
+
+ {getAvatar(org.name)}
+
+
+
+ {organization_id === org.id && (
+
+ )}
+
+
+ );
+ })
+ );
+ };
+
+ const searchOrganizations = (text) => {
+ setSearchText(text);
+ };
+
+ const getListOrganizations = () => {
+ return (
+
+
+
+
setIsListOrganizations(false)}>
+
+
+
+
+
+
setIsListOrganizations(false)}>
+ Back
+
+
+
+
+
+
+
+ {getOrgStatus === 'success' ? (
+ listOrganization()
+ ) : (
+
+ )}
+
+
+ );
+ };
+
+ const getOrganizationMenu = () => {
+ return (
+
+
+
+
+ {getAvatar(organization)}
+
+
+
+ {organization}
+
+ {admin && (
+
+ Edit
+
+ )}
+
+ {!isSingleOrganization && (
+
+
setIsListOrganizations(true)}>
+
+
+
+
+
+
+ )}
+
+
+ {!isSingleOrganization && (
+
+ )}
+ {admin && (
+ <>
+
+
+ Manage Users
+
+
+ Manage Groups
+
+
+ Manage SSO
+
+ >
+ )}
+
+ );
+ };
+
+ return (
+
+
setIsListOrganizations(false)}>
+
+ {organization}
+
+ {(!isSingleOrganization || admin) && (
+
+ {isListOrganizations ? getListOrganizations() : getOrganizationMenu()}
+
+ )}
+
+
setShowCreateOrg(false)} title="Create organization">
+
+
+ setNewOrgName(e.target.value)}
+ className="form-control"
+ placeholder="organization name"
+ disabled={isCreating}
+ maxLength={25}
+ />
+
+
+
+
+ setShowCreateOrg(false)}>
+ Cancel
+
+
+ Create organization
+
+
+
+
+
setShowEditOrg(false)} title="Edit organization">
+
+
+ setNewOrgName(e.target.value)}
+ className="form-control"
+ placeholder="organization name"
+ disabled={isCreating}
+ value={newOrgName}
+ maxLength={25}
+ />
+
+
+
+
+ setShowEditOrg(false)}>
+ Cancel
+
+
+ Save
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/_components/SearchBox.jsx b/frontend/src/_components/SearchBox.jsx
index e639e504b8..afc279a739 100644
--- a/frontend/src/_components/SearchBox.jsx
+++ b/frontend/src/_components/SearchBox.jsx
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import useDebounce from '@/_hooks/useDebounce';
-export function SearchBox({ onSubmit, debounceDelay = 300 }) {
+export function SearchBox({ width = '200px', onSubmit, debounceDelay = 300 }) {
const [searchText, setSearchText] = useState('');
const debouncedSearchTerm = useDebounce(searchText, debounceDelay);
const [isFocused, setFocussed] = useState(false);
@@ -16,7 +16,6 @@ export function SearchBox({ onSubmit, debounceDelay = 300 }) {
};
useEffect(() => {
- console.log(debouncedSearchTerm);
onSubmit(debouncedSearchTerm);
}, [debouncedSearchTerm, onSubmit]);
@@ -44,6 +43,7 @@ export function SearchBox({ onSubmit, debounceDelay = 300 }) {
)}
{
// store user details and jwt token in local storage to keep user logged in between page refreshes
- localStorage.setItem('currentUser', JSON.stringify(user));
- currentUserSubject.next(user);
-
+ updateUser(user);
return user;
});
}
+function getOrganizationConfigs(organizationId) {
+ const requestOptions = {
+ method: 'GET',
+ headers: { 'Content-Type': 'application/json' },
+ };
+
+ return fetch(
+ `${config.apiUrl}/organizations/${organizationId ? `${organizationId}/` : ''}public-configs`,
+ requestOptions
+ )
+ .then(handleResponse)
+ .then((configs) => configs?.sso_configs);
+}
+
function updateCurrentUserDetails(details) {
const currentUserDetails = JSON.parse(localStorage.getItem('currentUser'));
const updatedUserDetails = Object.assign({}, currentUserDetails, details);
- localStorage.setItem('currentUser', JSON.stringify(updatedUserDetails));
- currentUserSubject.next(updatedUserDetails);
+ updateUser(updatedUserDetails);
}
function signup(email) {
@@ -70,25 +83,40 @@ function resetPassword(params) {
}
function logout() {
+ clearUser();
+ history.push(`/login?redirectTo=${window.location.pathname?.startsWith('/sso/') ? '/' : window.location.pathname}`);
+}
+
+function clearUser() {
// remove user from local storage to log user out
localStorage.removeItem('currentUser');
currentUserSubject.next(null);
- history.push(`/login?redirectTo=${window.location.pathname}`);
}
-function signInViaOAuth(ssoResponse) {
+function signInViaOAuth(configId, ssoResponse) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(ssoResponse),
};
- return fetch(`${config.apiUrl}/oauth/sign-in`, requestOptions)
- .then(handleResponse)
+ return fetch(`${config.apiUrl}/oauth/sign-in/${configId}`, requestOptions)
+ .then((response) => {
+ return response.text().then((text) => {
+ const data = text && JSON.parse(text);
+ if (!response.ok) {
+ const error = (data && data.message) || response.statusText;
+ return Promise.reject({ error, data });
+ }
+ return data;
+ });
+ })
.then((user) => {
- localStorage.setItem('currentUser', JSON.stringify(user));
- currentUserSubject.next(user);
-
+ updateUser(user);
return user;
});
}
+function updateUser(user) {
+ localStorage.setItem('currentUser', JSON.stringify(user));
+ currentUserSubject.next(user);
+}
diff --git a/frontend/src/_services/comments.service.js b/frontend/src/_services/comments.service.js
index b5efc14e82..94f31d94a2 100644
--- a/frontend/src/_services/comments.service.js
+++ b/frontend/src/_services/comments.service.js
@@ -9,15 +9,15 @@ function getThreads(appId, appVersionsId) {
}
function createThread(data) {
- return adapter.post(`/threads/create`, data);
+ return adapter.post(`/threads`, data);
}
function updateThread(threadId, data) {
- return adapter.patch(`/threads/edit/${threadId}`, data);
+ return adapter.patch(`/threads/${threadId}`, data);
}
function deleteThread(threadId) {
- return adapter.delete(`/threads/delete/${threadId}`);
+ return adapter.delete(`/threads/${threadId}`);
}
function getComments(threadId, appVersionsId) {
@@ -25,15 +25,15 @@ function getComments(threadId, appVersionsId) {
}
function createComment(data) {
- return adapter.post(`/comments/create`, data);
+ return adapter.post(`/comments`, data);
}
function updateComment(commentId, data) {
- return adapter.patch(`/comments/edit/${commentId}`, data);
+ return adapter.patch(`/comments/${commentId}`, data);
}
function deleteComment(commentId) {
- return adapter.delete(`/comments/delete/${commentId}`);
+ return adapter.delete(`/comments/${commentId}`);
}
function getNotifications(appId, isResolved, appVersionsId) {
diff --git a/frontend/src/_services/organization.service.js b/frontend/src/_services/organization.service.js
index 5ea3820ba8..9fdb5bf27a 100644
--- a/frontend/src/_services/organization.service.js
+++ b/frontend/src/_services/organization.service.js
@@ -3,9 +3,45 @@ import { authHeader, handleResponse } from '@/_helpers';
export const organizationService = {
getUsers,
+ createOrganization,
+ editOrganization,
+ getOrganizations,
+ switchOrganization,
+ getSSODetails,
+ editOrganizationConfigs,
};
function getUsers() {
const requestOptions = { method: 'GET', headers: authHeader() };
return fetch(`${config.apiUrl}/organizations/users`, requestOptions).then(handleResponse);
}
+
+function createOrganization(name) {
+ const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify({ name }) };
+ return fetch(`${config.apiUrl}/organizations`, requestOptions).then(handleResponse);
+}
+
+function editOrganization(params) {
+ const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(params) };
+ return fetch(`${config.apiUrl}/organizations/`, requestOptions).then(handleResponse);
+}
+
+function getOrganizations() {
+ const requestOptions = { method: 'GET', headers: authHeader() };
+ return fetch(`${config.apiUrl}/organizations`, requestOptions).then(handleResponse);
+}
+
+function switchOrganization(organizationId) {
+ const requestOptions = { method: 'GET', headers: authHeader() };
+ return fetch(`${config.apiUrl}/switch/${organizationId}`, requestOptions);
+}
+
+function getSSODetails() {
+ const requestOptions = { method: 'GET', headers: authHeader() };
+ return fetch(`${config.apiUrl}/organizations/configs`, requestOptions).then(handleResponse);
+}
+
+function editOrganizationConfigs(params) {
+ const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(params) };
+ return fetch(`${config.apiUrl}/organizations/configs`, requestOptions).then(handleResponse);
+}
diff --git a/frontend/src/_services/user.service.js b/frontend/src/_services/user.service.js
index b896f00c74..60006945cd 100644
--- a/frontend/src/_services/user.service.js
+++ b/frontend/src/_services/user.service.js
@@ -8,6 +8,7 @@ export const userService = {
setPasswordFromToken,
updateCurrentUser,
changePassword,
+ acceptInvite,
};
function getAll() {
@@ -32,13 +33,12 @@ function deleteUser(id) {
return fetch(`${config.apiUrl}/users/${id}`, requestOptions).then(handleResponse);
}
-function setPasswordFromToken({ token, password, organization, role, newSignup, firstName, lastName }) {
+function setPasswordFromToken({ token, password, organization, role, firstName, lastName }) {
const body = {
token,
password,
organization,
role,
- new_signup: newSignup,
first_name: firstName,
last_name: lastName,
};
@@ -47,6 +47,16 @@ function setPasswordFromToken({ token, password, organization, role, newSignup,
return fetch(`${config.apiUrl}/users/set_password_from_token`, requestOptions).then(handleResponse);
}
+function acceptInvite({ token, password }) {
+ const body = {
+ token,
+ password,
+ };
+
+ const requestOptions = { method: 'POST', headers: authHeader(), body: JSON.stringify(body) };
+ return fetch(`${config.apiUrl}/users/accept-invite`, requestOptions).then(handleResponse);
+}
+
function updateCurrentUser(firstName, lastName) {
const body = { first_name: firstName, last_name: lastName };
const requestOptions = { method: 'PATCH', headers: authHeader(), body: JSON.stringify(body) };
diff --git a/frontend/src/_styles/colors.scss b/frontend/src/_styles/colors.scss
index 6da3674ba5..71e4b7dd36 100644
--- a/frontend/src/_styles/colors.scss
+++ b/frontend/src/_styles/colors.scss
@@ -12,6 +12,7 @@ $dark-background: #1f2936;
$bg-light: #EEF3F9;
$bg-dark: #22272E;
$bg-dark-light: #232e3c;
+$primary-light: #7A95FB;
.color-primary {
color: $primary !important;
diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss
index e9e71b110e..c0e2b1e233 100644
--- a/frontend/src/_styles/theme.scss
+++ b/frontend/src/_styles/theme.scss
@@ -731,7 +731,7 @@ button {
.page-body,
.homepage-body {
- height: 100vh;
+ height: 90.6vh;
.list-group.list-group-transparent.dark .all-apps-link,
.list-group-item-action.dark.active {
@@ -1219,6 +1219,17 @@ button {
filter: invert(89%) sepia(2%) saturate(127%) hue-rotate(175deg) brightness(99%) contrast(96%);
}
}
+ .organization-list {
+ margin-top: 5px;
+ .btn {
+ border: 0px;
+ }
+ .dropdown-toggle div {
+ max-width: 200px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
}
.pagination {
@@ -3227,6 +3238,12 @@ input:focus-visible {
.app-version-name.form-select {
border-color: $border-grey-dark;
}
+ .organization-list {
+ .btn {
+ background-color: #273342;
+ color: #656d77;
+ }
+ }
}
.main-wrapper {
@@ -3914,7 +3931,7 @@ input[type="text"] {
}
.close-icon {
position: fixed;
- top: 45px;
+ top: 84px;
right: 0;
width: 60px;
height: 22;
@@ -3973,9 +3990,9 @@ input[type="text"] {
color: #36af8b;
}
-.layout-buttons {
- position: absolute;
- left: 50%;
+.undo-redo-buttons {
+ flex: 1;
+ padding-left: .5rem;
}
.app-version-menu {
@@ -4716,11 +4733,351 @@ div#driver-page-overlay {
#transformation-popover-container {
margin-left: 80px !important;
margin-bottom: -2px !important;
- // top: -10px !important;
- // left: 100px !important;
- // background-color: #0565ff;
}
+.organization-list {
+ margin-top: 5px;
+ .btn {
+ border: 0px;
+ }
+ .dropdown-toggle div {
+ max-width: 200px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ .org-name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100%;
+ font-weight: bold;
+ }
+ .org-actions div {
+ color: #0565ff;
+ cursor: pointer;
+ font-size: 12px;
+ }
+ .dropdown-menu {
+ min-width: 14rem;
+ }
+ .org-avatar {
+ display: block;
+ }
+ .org-avatar:hover {
+ .avatar {
+ background: #fcfcfc no-repeat center/cover;
+ }
+ .arrow-container {
+ svg {
+ filter: invert(48%) sepia(6%) saturate(6%) hue-rotate(315deg) brightness(103%) contrast(96%);
+ }
+ }
+ }
+ .arrow-container {
+ padding: 5px 0px;
+ }
+ .arrow-container {
+ svg {
+ cursor: pointer;
+ height: 30px;
+ width: 30px;
+ padding: 0px 0px;
+ filter: invert(84%) sepia(13%) saturate(11%) hue-rotate(352deg) brightness(90%) contrast(91%);
+ }
+ }
+ .org-edit {
+ span {
+ color: #0565ff;
+ cursor: pointer;
+ font-size: 10px;
+ }
+ }
+ .organization-switchlist {
+ .back-btn {
+ font-size: 12px;
+ padding: 2px 0px;
+ cursor: pointer;
+ }
+ .back-ico {
+ cursor: pointer;
+ svg {
+ height: 20px;
+ width: 20px;
+ filter: invert(84%) sepia(13%) saturate(11%) hue-rotate(352deg) brightness(90%) contrast(91%);
+ }
+ }
+ .dd-item-padding {
+ padding: 0.5rem 0.75rem 0rem 0.75rem;
+ }
+ .search-box {
+ margin-top: 10px;
+ }
+ .org-list {
+ max-height: 60vh;
+ overflow: auto;
+ }
+ .tick-ico {
+ filter: invert(50%) sepia(13%) saturate(208%) hue-rotate(153deg) brightness(99%) contrast(86%);
+ }
+ .org-list-item {
+ cursor: pointer;
+ }
+ .org-list-item:hover {
+ .avatar {
+ background: #fcfcfc no-repeat center/cover;
+ }
+ .tick-ico {
+ filter: invert(35%) sepia(17%) saturate(238%) hue-rotate(153deg) brightness(94%) contrast(89%);
+ }
+ }
+ }
+}
+
+// Left Menu
+.left-menu {
+ background-color: #e8ebf4;
+ padding: 1rem 0.5rem;
+ border-radius: 5px;
+ ul {
+ overflow: auto;
+ margin: 0px;
+ padding: 0px;
+ li {
+ float: left;
+ list-style: none;
+ width: 100%;
+ padding: 5px 10px;
+ font-weight: bold;
+ border-radius: 5px;
+ cursor: pointer;
+ margin: 3px 0px;
+ }
+ li.active {
+ background-color: $primary;
+ color: $white;
+ }
+ li:not(.active):hover {
+ background-color: #d2daf0;
+ }
+ }
+}
+.manage-sso {
+ .title-with-toggle {
+ width: 100%;
+ input[type=checkbox] {
+ /* Double-sized Checkboxes */
+ -ms-transform: scale(1.5); /* IE */
+ -moz-transform: scale(1.5); /* FF */
+ -webkit-transform: scale(1.5); /* Safari and Chrome */
+ -o-transform: scale(1.5); /* Opera */
+ transform: scale(1.5);
+ margin-top: 5px;
+ }
+ }
+}
+.help-text {
+ overflow: auto;
+ div {
+ margin: 0px 0px 5px 0px;
+ background-color: #eaeaea;
+ float: left;
+ padding: 2px 10px;
+ font-size: 11px;
+ border-radius: 5px;
+ border: 1px solid #e1e1e1;
+ }
+}
+.org-invite-or {
+ padding: 1rem 0rem;
+ h2 {
+ width: 100%;
+ text-align: center;
+ border-bottom: 1px solid #000;
+ line-height: 0.1em;
+ margin: 10px 0 20px;
+ }
+
+ h2 span {
+ background:#fff;
+ padding:0 10px;
+ }
+}
+
+
+.theme-dark .json-tree-container {
+ .json-tree-node-icon {
+ svg {
+ filter: invert(89%) sepia(2%) saturate(127%) hue-rotate(175deg) brightness(99%) contrast(96%);
+ }
+ }
+ .json-tree-svg-icon.component-icon {
+ filter: brightness(0) invert(1);
+ }
+
+ .node-key-outline {
+ height: 1rem!important;
+ border: 1px solid transparent!important;
+ color: #ccd4df;
+ }
+
+ .selected-node {
+ border-color: $primary-light !important;
+ }
+ .json-tree-icon-container .selected-node > svg:first-child {
+ filter: invert(65%) sepia(62%) saturate(4331%) hue-rotate(204deg) brightness(106%) contrast(97%);
+ }
+ .node-length-color {
+ color: #B8C7FD;
+ }
+ .node-type {
+ color: #8a96a6;
+ }
+ .group-border {
+ border-color: rgb(97, 101, 111);
+ }
+
+ .action-icons-group {
+ img, svg {
+ filter: invert(89%) sepia(2%) saturate(127%) hue-rotate(175deg) brightness(99%) contrast(96%);
+ }
+ }
+
+ .hovered-node.node-key.badge {
+ color: #8092AB !important;
+ border-color: #8092AB !important;
+ }
+}
+
+.json-tree-container {
+ .json-tree-svg-icon.component-icon {
+ height: 16px;
+ width: 16px;
+ }
+ .json-tree-icon-container {
+ max-width: 20px;
+ margin-right: 6px;
+ }
+ .node-type {
+ color: #A6B6CC;
+ padding-top: 2px;
+ }
+ .json-tree-valuetype {
+ font-size: 10px;
+ padding-top: 2px;
+ }
+ .node-length-color {
+ color: #3650AF;
+ padding-top: 3px;
+ }
+ .json-tree-node-value {
+ font-size: 11px;
+ }
+ .json-tree-node-string {
+ color: #F6820C;
+ }
+ .json-tree-node-boolean {
+ color: #3EB25F;
+ }
+ .json-tree-node-number {
+ color: #F4B2B0;
+ }
+ .json-tree-node-null {
+ color: red;
+ }
+
+ .group-border {
+ border-left: 0.5px solid #dadcde;
+ margin-top: 16px;
+ margin-left: -12px;
+ }
+
+ .selected-node {
+ border-color: #4D72FA !important;
+ }
+
+ .selected-node .group-object-container .badge {
+ font-weight: 400 !important;
+ height: 1rem !important;
+ }
+
+ .group-object-container {
+ margin-left: 0.72rem;
+ margin-top: -16px;
+ }
+
+ .json-node-element {
+ cursor: pointer;
+ }
+
+ .hide-show-icon {
+ cursor: pointer;
+ margin-left: 1rem;
+ &:hover {
+ color: $primary;
+ }
+ }
+
+ .action-icons-group {
+ margin-right: 4rem !important;
+ margin-left: 2rem !important;
+ }
+
+ .action-icons-group {
+ cursor: pointer;
+ }
+
+ .hovered-node {
+ font-weight: 400 !important;
+ height: 1rem !important;
+ color:#8092AB;
+ }
+
+ .node-key {
+ font-weight: 400!important;
+ margin-left: -0.25rem!important;
+ }
+
+ .node-key-outline {
+ height: 1rem!important;
+ border: 1px solid transparent!important;
+ color: #3e525b;
+ }
+
+}
+
+.popover-more-actions {
+ font-weight: 400!important;
+
+ &:hover {
+ background: #d2ddec !important;
+ }
+}
+
+.popover-dark-themed .popover-more-actions {
+ color: #ccd4df;
+
+ &:hover {
+ background-color: #324156 !important;
+ }
+}
+
+#json-tree-popover {
+ padding: 0.25rem !important;
+}
+
+// Font sizes
+.fs-9 {
+ font-size: 9px !important;
+}
+.fs-10 {
+ font-size: 10px !important;
+}
+
+.fs-12 {
+ font-size: 12px !important;
+}
+
+
.realtime-avatars {
position: absolute;
left: 35%;
@@ -4752,3 +5109,9 @@ div#driver-page-overlay {
.list-timeline:not(.list-timeline-simple) .list-timeline-time {
top: auto;
}
+.editor-actions {
+ border-bottom: 1px solid #eee;
+ padding: 5px;
+ display: flex;
+ justify-content: end;
+}
\ No newline at end of file
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
new file mode 100644
index 0000000000..97f348e6cd
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNode.jsx
@@ -0,0 +1,362 @@
+import React from 'react';
+import _ from 'lodash';
+import cx from 'classnames';
+import { ToolTip } from '@/_components/ToolTip';
+import CopyToClipboardComponent from '@/_components/CopyToClipboard';
+import { Popover } from 'react-bootstrap';
+import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
+import JSONNodeObject from './JSONNodeObject';
+import JSONNodeArray from './JSONNodeArray';
+import JSONNodeValue from './JSONNodeValue';
+import JSONNodeIndicator from './JSONNodeIndicator';
+
+export const JSONNode = ({ data, ...restProps }) => {
+ const {
+ path,
+ shouldExpandNode,
+ currentNode,
+ selectedNode,
+ hoveredNode,
+ getCurrentPath,
+ getCurrentNodeType,
+ getLength,
+ toUseNodeIcons,
+ renderNodeIcons,
+ useIndentedBlock,
+ updateSelectedNode,
+ updateHoveredNode,
+ useActions,
+ enableCopyToClipboard,
+ getNodeShowHideComponents,
+ getOnSelectLabelDispatchActions,
+ expandWithLabels,
+ getAbsoluteNodePath,
+ actionsList,
+ updateParentState = () => null,
+ } = restProps;
+
+ const [expandable, set] = React.useState(() =>
+ typeof shouldExpandNode === 'function' ? shouldExpandNode(path, data) : shouldExpandNode
+ );
+
+ const [showHiddenOptionsForNode, setShowHiddenOptionsForNode] = React.useState(false);
+ const [showHiddenOptionButtons, setShowHiddenOptionButtons] = React.useState([]);
+ const [onSelectDispatchActions, setOnSelectDispatchActions] = React.useState([]);
+
+ React.useEffect(() => {
+ if (showHiddenOptionButtons) {
+ setShowHiddenOptionButtons(() => getNodeShowHideComponents(currentNode, path));
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ React.useEffect(() => {
+ if (useActions && currentNode) {
+ const onSelectDispatchActions = getOnSelectLabelDispatchActions(currentNode, path).filter(
+ (action) => action.onSelect
+ );
+ if (onSelectDispatchActions.length > 0) {
+ setOnSelectDispatchActions(onSelectDispatchActions);
+ }
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selectedNode]);
+
+ const toggleExpandNode = (node) => {
+ if (expandable) {
+ updateSelectedNode(null);
+ } else {
+ updateSelectedNode(node, path);
+ }
+
+ set((prev) => !prev);
+ };
+
+ const onSelect = (data, currentNode, path) => {
+ const actions = onSelectDispatchActions;
+ actions.forEach((action) => action.dispatchAction(data, currentNode));
+
+ if (!expandWithLabels) {
+ updateSelectedNode(currentNode, path);
+ set(true);
+ }
+ };
+
+ const handleOnClickLabels = (data, currentNode, path) => {
+ if (expandWithLabels) {
+ toggleExpandNode(currentNode);
+ }
+
+ if (useActions) {
+ onSelect(data, currentNode, path);
+ }
+ };
+
+ const typeofCurrentNode = getCurrentNodeType(data);
+ const currentNodePath = getCurrentPath(path, currentNode);
+ const toExpandNode = (data instanceof Array || data instanceof Object) && !_.isEmpty(data);
+ const toShowNodeIndicator = (data instanceof Array || data instanceof Object) && typeofCurrentNode !== 'Function';
+ const numberOfEntries = getLength(typeofCurrentNode, data);
+ const toRenderSelector = (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array') && numberOfEntries > 0;
+
+ let $VALUE = null;
+ let $NODEType = null;
+ let $NODEIcon = null;
+
+ const checkSelectedNode = (_selectedNode, _currentNode, parent, toExpand) => {
+ if (selectedNode?.parent && parent) {
+ return _selectedNode.parent === parent && _selectedNode?.node === _currentNode && toExpand;
+ }
+
+ return toExpand && _selectedNode?.node === _currentNode;
+ };
+
+ const parent = path && typeof path?.length === 'number' ? path[path.length - 2] : null;
+
+ const applySelectedNodeStyles = toExpandNode
+ ? checkSelectedNode(selectedNode, currentNode, parent, expandable)
+ : false;
+
+ React.useEffect(() => {
+ if (!expandable) {
+ updateSelectedNode(null);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [expandable]);
+
+ React.useEffect(() => {
+ if (selectedNode?.node === currentNode) {
+ set(true);
+ }
+ }, [selectedNode, currentNode]);
+
+ React.useEffect(() => {
+ if (hoveredNode?.node === currentNode && hoveredNode?.parent === parent) {
+ setShowHiddenOptionsForNode(true);
+ }
+
+ return () => {
+ setShowHiddenOptionsForNode(false);
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [hoveredNode]);
+
+ if (toUseNodeIcons && currentNode) {
+ $NODEIcon = renderNodeIcons(currentNode);
+ }
+
+ switch (typeofCurrentNode) {
+ case 'String':
+ case 'Boolean':
+ case 'Number':
+ case 'Null':
+ case 'Undefined':
+ case 'Function':
+ $VALUE =
;
+ $NODEType =
;
+ break;
+
+ case 'Object':
+ $VALUE =
;
+ $NODEType = (
+
+
+ {`${numberOfEntries} ${numberOfEntries > 1 ? 'entries' : 'entry'}`}{' '}
+
+
+ );
+ break;
+
+ case 'Array':
+ $VALUE =
;
+ $NODEType = (
+
+
+ {`${numberOfEntries} ${numberOfEntries > 1 ? 'items' : 'item'}`}{' '}
+
+
+ );
+
+ break;
+
+ default:
+ $VALUE =
{String(data)} ;
+ $NODEType = typeofCurrentNode;
+ }
+
+ let $key = (
+
toExpandNode && handleOnClickLabels(data, currentNode, path)}
+ style={{ marginTop: '1px', cursor: 'pointer', textTransform: 'none' }}
+ className={cx('node-key fs-12 mx-0 badge badge-outline', {
+ 'color-primary': applySelectedNodeStyles && !showHiddenOptionsForNode,
+ 'hovered-node': showHiddenOptionsForNode,
+ 'node-key-outline': !applySelectedNodeStyles && !showHiddenOptionsForNode,
+ })}
+ >
+ {String(currentNode)}
+
+ );
+
+ if (!currentNode) {
+ return $VALUE;
+ }
+
+ const shouldDisplayIntendedBlock =
+ useIndentedBlock && expandable && (typeofCurrentNode === 'Object' || typeofCurrentNode === 'Array');
+
+ function moreActionsPopover(actions) {
+ //Todo: For adding more actions to the menu popover!
+ const darkMode = localStorage.getItem('darkMode') === 'true';
+
+ return (
+
+
+ {actions?.map((action, index) => (
+ {
+ action.dispatchAction(data, currentNode);
+ updateParentState();
+ }}
+ >
+ {action.name}
+
+ ))}
+
+
+ );
+ }
+
+ const renderHiddenOptionsForNode = () => {
+ const moreActions = actionsList.filter((action) => action.for === 'all')[0];
+
+ const renderOptions = () => {
+ if (!useActions || showHiddenOptionButtons?.length === 0) return null;
+
+ return showHiddenOptionButtons?.map((actionOption, index) => {
+ const { name, icon, src, iconName, dispatchAction, width = 12, height = 12 } = actionOption;
+ if (icon) {
+ return (
+
+ dispatchAction(data, currentNode)}
+ >
+
+
+
+ );
+ }
+ });
+ };
+
+ return (
+
+ {enableCopyToClipboard && (
+
+ )}
+ {renderOptions()}
+
+ {moreActions.actions?.length > 0 && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
updateHoveredNode(currentNode, currentNodePath)}
+ onMouseLeave={() => updateHoveredNode(null)}
+ className={cx('d-flex', {
+ 'group-object-container': shouldDisplayIntendedBlock,
+ 'mx-2': typeofCurrentNode !== 'Object' && typeofCurrentNode !== 'Array',
+ })}
+ >
+ {$NODEIcon &&
{$NODEIcon}
}
+ {$key} {$NODEType}
+ {!toExpandNode && !expandable && !toRenderSelector ? $VALUE : null}
+
{showHiddenOptionsForNode && renderHiddenOptionsForNode()}
+
+ {toRenderSelector && (toExpandNode && !expandable ? null : $VALUE)}
+
+
+ );
+};
+
+const DisplayNodeLabel = ({ type = '', children }) => {
+ if (type === 'Null' || type === 'Undefined') {
+ return null;
+ }
+ return (
+ <>
+
{type}
+ {children}
+ >
+ );
+};
+
+JSONNode.DisplayNodeLabel = DisplayNodeLabel;
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNodeArray.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNodeArray.jsx
new file mode 100644
index 0000000000..396e8c7b63
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNodeArray.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { JSONNode } from './JSONNode';
+
+const JSONTreeArrayNode = ({ data, path, ...restProps }) => {
+ const keys = [];
+
+ for (let i = 0; i < data.length; i++) {
+ keys.push(String(i));
+ }
+
+ return keys.map((key, index) => {
+ const currentPath = [...path, key];
+ const _currentNode = key;
+ const props = { ...restProps };
+ props.currentNode = _currentNode;
+
+ return
;
+ });
+};
+
+export default JSONTreeArrayNode;
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNodeIndicator.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNodeIndicator.jsx
new file mode 100644
index 0000000000..b4cebc69c7
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNodeIndicator.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+
+const JSONTreeNodeIndicator = ({ toExpand, toShowNodeIndicator, handleToggle, ...restProps }) => {
+ const {
+ renderCustomIndicator,
+ typeofCurrentNode,
+ currentNode,
+ isSelected,
+ toExpandNode,
+ data,
+ path,
+ toExpandWithLabels,
+ toggleWithLabels,
+ } = restProps;
+
+ const defaultStyles = {
+ transform: toExpandNode && toExpand ? 'rotate(90deg)' : 'rotate(0deg)',
+ transition: '0.2s all',
+ display: 'inline-block',
+ cursor: 'pointer',
+ };
+
+ const handleToggleForNode = () => {
+ if (toExpandWithLabels) {
+ return toggleWithLabels(data, currentNode, path);
+ }
+
+ return handleToggle(currentNode);
+ };
+
+ const renderDefaultIndicator = () => (
+
+
+
+ );
+
+ if (!toShowNodeIndicator && (typeofCurrentNode !== 'Object' || typeofCurrentNode !== 'Array')) return null;
+
+ return (
+
+
+ {renderCustomIndicator ? renderCustomIndicator() : renderDefaultIndicator()}
+
+
+ );
+};
+
+export default JSONTreeNodeIndicator;
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNodeObject.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNodeObject.jsx
new file mode 100644
index 0000000000..cf466db6db
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNodeObject.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { JSONNode } from './JSONNode';
+
+const JSONTreeObjectNode = ({ data, path, ...restProps }) => {
+ const nodeKeys = Object.keys(data);
+
+ return nodeKeys.map((key, index) => {
+ const currentPath = [...path, key];
+ const _currentNode = key;
+ const props = { ...restProps };
+ props.currentNode = _currentNode;
+
+ return
;
+ });
+};
+
+export default JSONTreeObjectNode;
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx b/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx
new file mode 100644
index 0000000000..1596a50d83
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/JSONNodeValue.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+const JSONTreeValueNode = ({ data, type }) => {
+ if (type === 'Function') {
+ const functionString = `${data.toString().split('{')[0].trim()}{...}`;
+ return (
+
+
+ {functionString}
+
+
+ );
+ }
+
+ const value = type === 'String' ? `"${data}"` : String(data);
+ const clsForUndefinedOrNull = (type === 'Undefined' || type === 'Null') && 'badge badge-secondary';
+ return (
+
+ {value}
+
+ );
+};
+
+export default JSONTreeValueNode;
diff --git a/frontend/src/_ui/JSONTreeViewer/JSONTreeViewer.jsx b/frontend/src/_ui/JSONTreeViewer/JSONTreeViewer.jsx
new file mode 100644
index 0000000000..82186dadc4
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/JSONTreeViewer.jsx
@@ -0,0 +1,229 @@
+import _ from 'lodash';
+import React from 'react';
+import { JSONNode } from './JSONNode';
+import ErrorBoundary from '@/Editor/ErrorBoundary';
+
+export class JSONTreeViewer extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ data: this.props.data,
+ shouldExpandNode: false,
+ currentNode: 'Root',
+ selectedNode: null,
+ hoveredNode: null,
+ darkTheme: false,
+ showHideActions: false,
+ enableCopyToClipboard: false,
+ actionsList: [],
+ };
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (!_.isEqual(prevProps, this.props)) {
+ this.setState({
+ data: this.props.data,
+ shouldExpandNode: this.props.shouldExpandNode,
+ ...this.props,
+ });
+ }
+
+ if (prevState.selectedComponent !== this.state.selectedComponent && this.props.treeType === 'inspector') {
+ if (this.getCurrentNodeType(this.state.data) === 'Object') {
+ const matchedWidget = Object.keys(this.state.data.components).filter(
+ (component) => this.state.data.components[component].id === this.state.selectedComponent.id
+ )[0];
+
+ if (matchedWidget) {
+ this.setState(
+ {
+ selectedWidget: matchedWidget,
+ },
+ () => {
+ this.updateSelectedNode(matchedWidget);
+ }
+ );
+ }
+ }
+ }
+ }
+
+ getCurrentNodePath(path, node) {
+ let currentPath = path ?? [];
+ if (node) {
+ if (!currentPath[currentPath.length - 1] === node) {
+ currentPath = [...currentPath, node];
+ }
+ }
+ return currentPath;
+ }
+
+ getCurrentNodeType(node) {
+ const typeofCurrentNode = Object.prototype.toString.call(node).slice(8, -1);
+ //Todo: Handle more types (Custom type or Iterable type)
+
+ return typeofCurrentNode;
+ }
+
+ getLength(type, collection) {
+ if (!collection) return 0;
+ if (type === 'Object') {
+ return Object.keys(collection).length;
+ } else if (type === 'Array') {
+ return collection.length;
+ }
+
+ return 0;
+ }
+
+ renderNodeIcons = (node) => {
+ const icon = this.props.iconsList.filter((icon) => icon?.iconName === node)[0];
+
+ if (icon && icon.iconPath) {
+ return (
+
+ );
+ }
+ if (icon && icon.jsx) {
+ return icon.jsx();
+ }
+ };
+
+ updateSelectedNode = (node, path) => {
+ if (node) {
+ this.setState({
+ selectedNode: { node: node, parent: path?.length ? path[path.length - 2] : null },
+ });
+ }
+ };
+ updateHoveredNode = (node, path) => {
+ this.setState({
+ hoveredNode: { node: node, parent: path?.length ? path[path.length - 2] : null },
+ });
+ };
+
+ getDispatchActionsForNode = (node) => {
+ if (!node) return null;
+ return this.state.actionsList.filter((action) => action.for === node)[0];
+ };
+
+ getNodeShowHideComponents = (currentNode, path) => {
+ const showHideComponents = [];
+ const parent = path ? path[path.length - 2] : 'root';
+ const dispatchActionForCurrentNode = this.getDispatchActionsForNode(parent);
+
+ if (currentNode === parent) return;
+
+ if (dispatchActionForCurrentNode && dispatchActionForCurrentNode['enableFor1stLevelChildren']) {
+ dispatchActionForCurrentNode['actions'].map((action) => showHideComponents.push(action));
+ }
+
+ return showHideComponents;
+ //Todo: if actions should be available for all children
+ };
+
+ getOnSelectLabelDispatchActions = (currentNode, path) => {
+ const actions = [];
+ const parent = path ? path[path.length - 2] : 'root';
+ const dispatchActionForCurrentNode = this.getDispatchActionsForNode(parent);
+ if (currentNode === parent) return;
+
+ if (dispatchActionForCurrentNode && dispatchActionForCurrentNode['enableFor1stLevelChildren']) {
+ dispatchActionForCurrentNode['actions'].map((action) => actions.push(action));
+ }
+ //Todo: if actions should be available for all children
+ return actions;
+ };
+
+ getAbsoluteNodePath = (path) => {
+ const data = this.state.data;
+ if (!data || _.isEmpty(data)) return null;
+ const map = new Map();
+
+ // loop through the data and build the map
+ const buildMap = (data, path = '') => {
+ const keys = Object.keys(data);
+ keys.forEach((key) => {
+ const value = data[key];
+ const _type = Object.prototype.toString.call(value).slice(8, -1);
+ let newPath = '';
+ if (path === '') {
+ newPath = key;
+ } else {
+ newPath = `${path}.${key}`;
+ }
+
+ if (_.isObject(value) || _.isArray(value)) {
+ buildMap(value, newPath);
+ } else if (_.isFunction(value)) {
+ map.set(newPath, { type: _type });
+ } else {
+ map.set(newPath, { type: _type });
+ }
+ });
+ };
+
+ const computeAbsolutePath = (path) => {
+ let prevPath, prevType, prevRelPath, currentPath, abs;
+
+ for (let i = 0; i < path.length; i++) {
+ prevType = map.get(prevRelPath)?.type;
+ const node = path[i];
+
+ currentPath = prevRelPath ? `${prevRelPath}.${node}` : node;
+
+ if (prevType === 'Object') {
+ abs = `${prevPath}.${node}`;
+ } else if (prevType === 'Array') {
+ abs = `${prevPath}[${node}]`;
+ } else {
+ abs = currentPath;
+ }
+ prevPath = abs;
+ prevRelPath = currentPath;
+ }
+
+ return abs;
+ };
+
+ buildMap(data);
+
+ return computeAbsolutePath(path);
+ };
+
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/_ui/JSONTreeViewer/index.js b/frontend/src/_ui/JSONTreeViewer/index.js
new file mode 100644
index 0000000000..b4a8432a22
--- /dev/null
+++ b/frontend/src/_ui/JSONTreeViewer/index.js
@@ -0,0 +1,3 @@
+import { JSONTreeViewer } from './JSONTreeViewer';
+
+export default JSONTreeViewer;
diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js
index fa4e2dbf1a..b5b9c8f36f 100644
--- a/frontend/webpack.config.js
+++ b/frontend/webpack.config.js
@@ -110,6 +110,7 @@ module.exports = {
apiUrl: `${API_URL[environment] || ''}/api`,
SERVER_IP: process.env.SERVER_IP,
COMMENT_FEATURE_ENABLE: true,
+ ENABLE_MULTIPLAYER_EDITING: true,
}),
},
};
diff --git a/package.json b/package.json
index 475fb9eedd..7762cd75a9 100644
--- a/package.json
+++ b/package.json
@@ -56,4 +56,4 @@
"cy:open": "cypress open --env db.name=$TEST_PG_DB,db.user=$TEST_PG_USERNAME,db.password=$TEST_PG_PASSWORD",
"prepare": "husky install"
}
-}
+}
\ No newline at end of file
diff --git a/plugins/package-lock.json b/plugins/package-lock.json
index f37acb9fea..1571570ce3 100644
--- a/plugins/package-lock.json
+++ b/plugins/package-lock.json
@@ -27,6 +27,7 @@
"@tooljet-plugins/mssql": "file:packages/mssql",
"@tooljet-plugins/mysql": "file:packages/mysql",
"@tooljet-plugins/n8n": "file:packages/n8n",
+ "@tooljet-plugins/notion": "file:packages/notion",
"@tooljet-plugins/openapi": "file:packages/openapi",
"@tooljet-plugins/oracledb": "file:packages/oracledb",
"@tooljet-plugins/postgresql": "file:packages/postgresql",
@@ -4068,6 +4069,18 @@
"node": ">= 8"
}
},
+ "node_modules/@notionhq/client": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-1.0.4.tgz",
+ "integrity": "sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw==",
+ "dependencies": {
+ "@types/node-fetch": "^2.5.10",
+ "node-fetch": "^2.6.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@npmcli/ci-detect": {
"version": "1.4.0",
"dev": true,
@@ -4459,8 +4472,9 @@
}
},
"node_modules/@sindresorhus/is": {
- "version": "4.2.0",
- "license": "MIT",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"engines": {
"node": ">=10"
},
@@ -4570,6 +4584,10 @@
"resolved": "packages/n8n",
"link": true
},
+ "node_modules/@tooljet-plugins/notion": {
+ "resolved": "packages/notion",
+ "link": true
+ },
"node_modules/@tooljet-plugins/openapi": {
"resolved": "packages/openapi",
"link": true
@@ -8416,6 +8434,11 @@
"node": ">= 6"
}
},
+ "node_modules/form-data-encoder": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz",
+ "integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg=="
+ },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -10696,11 +10719,9 @@
"license": "ISC"
},
"node_modules/json5": {
- "version": "2.2.0",
- "license": "MIT",
- "dependencies": {
- "minimist": "^1.2.5"
- },
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"bin": {
"json5": "lib/cli.js"
},
@@ -16529,9 +16550,98 @@
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
+ "got": "^12.0.3",
+ "json5": "^2.2.1",
"react": "^17.0.2"
}
},
+ "packages/baserow/node_modules/@szmarczak/http-timer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+ "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+ "dependencies": {
+ "defer-to-connect": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "packages/baserow/node_modules/cacheable-lookup": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
+ "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "packages/baserow/node_modules/got": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
+ "integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
+ "dependencies": {
+ "@sindresorhus/is": "^4.6.0",
+ "@szmarczak/http-timer": "^5.0.1",
+ "@types/cacheable-request": "^6.0.2",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^6.0.4",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "form-data-encoder": "1.7.1",
+ "get-stream": "^6.0.1",
+ "http2-wrapper": "^2.1.10",
+ "lowercase-keys": "^3.0.0",
+ "p-cancelable": "^3.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "packages/baserow/node_modules/http2-wrapper": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
+ "integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "packages/baserow/node_modules/lowercase-keys": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+ "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/baserow/node_modules/p-cancelable": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+ "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "packages/baserow/node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"packages/bigquery": {
"name": "@tooljet-plugins/bigquery",
"version": "1.0.0",
@@ -16707,11 +16817,109 @@
"react": "^17.0.2"
}
},
+ "packages/notion": {
+ "version": "1.0.0",
+ "dependencies": {
+ "@notionhq/client": "^1.0.4",
+ "@tooljet-plugins/common": "file:../common",
+ "react": "^17.0.2"
+ }
+ },
"packages/openapi": {
+ "name": "@tooljet-plugins/openapi",
"version": "1.0.0",
"dependencies": {
"@tooljet-plugins/common": "file:../common",
- "react": "^17.0.2"
+ "got": "^12.0.3",
+ "react": "^17.0.2",
+ "tough-cookie": "^4.0.0"
+ }
+ },
+ "packages/openapi/node_modules/@szmarczak/http-timer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+ "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+ "dependencies": {
+ "defer-to-connect": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "packages/openapi/node_modules/cacheable-lookup": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
+ "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A==",
+ "engines": {
+ "node": ">=10.6.0"
+ }
+ },
+ "packages/openapi/node_modules/got": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
+ "integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
+ "dependencies": {
+ "@sindresorhus/is": "^4.6.0",
+ "@szmarczak/http-timer": "^5.0.1",
+ "@types/cacheable-request": "^6.0.2",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^6.0.4",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "form-data-encoder": "1.7.1",
+ "get-stream": "^6.0.1",
+ "http2-wrapper": "^2.1.10",
+ "lowercase-keys": "^3.0.0",
+ "p-cancelable": "^3.0.0",
+ "responselike": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
+ }
+ },
+ "packages/openapi/node_modules/http2-wrapper": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
+ "integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
+ "packages/openapi/node_modules/lowercase-keys": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+ "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "packages/openapi/node_modules/p-cancelable": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+ "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "packages/openapi/node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/oracledb": {
@@ -19982,6 +20190,15 @@
"fastq": "^1.6.0"
}
},
+ "@notionhq/client": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@notionhq/client/-/client-1.0.4.tgz",
+ "integrity": "sha512-m7zZ5l3RUktayf1lRBV1XMb8HSKsmWTv/LZPqP7UGC1NMzOlc+bbTOPNQ4CP/c1P4cP61VWLb/zBq7a3c0nMaw==",
+ "requires": {
+ "@types/node-fetch": "^2.5.10",
+ "node-fetch": "^2.6.1"
+ }
+ },
"@npmcli/ci-detect": {
"version": "1.4.0",
"dev": true
@@ -20277,7 +20494,9 @@
}
},
"@sindresorhus/is": {
- "version": "4.2.0"
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
},
"@sinonjs/commons": {
"version": "1.8.3",
@@ -20321,7 +20540,68 @@
"version": "file:packages/baserow",
"requires": {
"@tooljet-plugins/common": "file:../common",
+ "got": "^12.0.3",
+ "json5": "^2.2.1",
"react": "^17.0.2"
+ },
+ "dependencies": {
+ "@szmarczak/http-timer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+ "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+ "requires": {
+ "defer-to-connect": "^2.0.1"
+ }
+ },
+ "cacheable-lookup": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
+ "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A=="
+ },
+ "got": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
+ "integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
+ "requires": {
+ "@sindresorhus/is": "^4.6.0",
+ "@szmarczak/http-timer": "^5.0.1",
+ "@types/cacheable-request": "^6.0.2",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^6.0.4",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "form-data-encoder": "1.7.1",
+ "get-stream": "^6.0.1",
+ "http2-wrapper": "^2.1.10",
+ "lowercase-keys": "^3.0.0",
+ "p-cancelable": "^3.0.0",
+ "responselike": "^2.0.0"
+ }
+ },
+ "http2-wrapper": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
+ "integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
+ "requires": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+ "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="
+ },
+ "p-cancelable": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+ "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="
+ },
+ "quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
+ }
}
},
"@tooljet-plugins/bigquery": {
@@ -20461,11 +20741,80 @@
"react": "^17.0.2"
}
},
+ "@tooljet-plugins/notion": {
+ "version": "file:packages/notion",
+ "requires": {
+ "@notionhq/client": "^1.0.4",
+ "@tooljet-plugins/common": "file:../common",
+ "react": "^17.0.2"
+ }
+ },
"@tooljet-plugins/openapi": {
"version": "file:packages/openapi",
"requires": {
"@tooljet-plugins/common": "file:../common",
- "react": "^17.0.2"
+ "got": "^12.0.3",
+ "react": "^17.0.2",
+ "tough-cookie": "^4.0.0"
+ },
+ "dependencies": {
+ "@szmarczak/http-timer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+ "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+ "requires": {
+ "defer-to-connect": "^2.0.1"
+ }
+ },
+ "cacheable-lookup": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz",
+ "integrity": "sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A=="
+ },
+ "got": {
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/got/-/got-12.0.4.tgz",
+ "integrity": "sha512-2Eyz4iU/ktq7wtMFXxzK7g5p35uNYLLdiZarZ5/Yn3IJlNEpBd5+dCgcAyxN8/8guZLszffwe3wVyw+DEVrpBg==",
+ "requires": {
+ "@sindresorhus/is": "^4.6.0",
+ "@szmarczak/http-timer": "^5.0.1",
+ "@types/cacheable-request": "^6.0.2",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^6.0.4",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "form-data-encoder": "1.7.1",
+ "get-stream": "^6.0.1",
+ "http2-wrapper": "^2.1.10",
+ "lowercase-keys": "^3.0.0",
+ "p-cancelable": "^3.0.0",
+ "responselike": "^2.0.0"
+ }
+ },
+ "http2-wrapper": {
+ "version": "2.1.11",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.1.11.tgz",
+ "integrity": "sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==",
+ "requires": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.2.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+ "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="
+ },
+ "p-cancelable": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+ "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="
+ },
+ "quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
+ }
}
},
"@tooljet-plugins/oracledb": {
@@ -23219,6 +23568,11 @@
"mime-types": "^2.1.12"
}
},
+ "form-data-encoder": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.1.tgz",
+ "integrity": "sha512-EFRDrsMm/kyqbTQocNvRXMLjc7Es2Vk+IQFx/YW7hkUH1eBl4J1fqiP34l74Yt0pFLCNpc06fkbVk00008mzjg=="
+ },
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -24718,10 +25072,9 @@
"version": "5.0.1"
},
"json5": {
- "version": "2.2.0",
- "requires": {
- "minimist": "^1.2.5"
- }
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
},
"jsonfile": {
"version": "6.1.0",
diff --git a/plugins/packages/elasticsearch/lib/index.ts b/plugins/packages/elasticsearch/lib/index.ts
index 6afa7aeadf..838990c0b9 100644
--- a/plugins/packages/elasticsearch/lib/index.ts
+++ b/plugins/packages/elasticsearch/lib/index.ts
@@ -1,4 +1,4 @@
-import { ConnectionTestResult, QueryService, QueryResult } from '@tooljet-plugins/common';
+import { ConnectionTestResult, QueryService, QueryResult, QueryError } from '@tooljet-plugins/common';
import { getDocument, updateDocument } from './operations';
import { indexDocument, search } from './operations';
import { Client } from '@opensearch-project/opensearch';
@@ -27,6 +27,7 @@ export default class ElasticsearchService implements QueryService {
}
} catch (err) {
console.log(err);
+ throw new QueryError('Query could not be completed', err.message, {});
}
return {
diff --git a/server/ee/controllers/oauth.controller.ts b/server/ee/controllers/oauth.controller.ts
index 0f2d7721c3..f92015d2aa 100644
--- a/server/ee/controllers/oauth.controller.ts
+++ b/server/ee/controllers/oauth.controller.ts
@@ -1,13 +1,13 @@
-import { Body, Controller, Post, Request } from '@nestjs/common';
+import { Body, Controller, Param, Post } from '@nestjs/common';
import { OauthService } from '../services/oauth/oauth.service';
@Controller('oauth')
export class OauthController {
constructor(private oauthService: OauthService) {}
- @Post('sign-in')
- async create(@Request() req, @Body() body) {
- const result = await this.oauthService.signIn(body);
+ @Post('sign-in/:configId')
+ async create(@Param('configId') configId, @Body() body) {
+ const result = await this.oauthService.signIn(body, configId);
return result;
}
}
diff --git a/server/ee/services/oauth/git_oauth.service.ts b/server/ee/services/oauth/git_oauth.service.ts
index c147279fa8..4b21fc6a34 100644
--- a/server/ee/services/oauth/git_oauth.service.ts
+++ b/server/ee/services/oauth/git_oauth.service.ts
@@ -5,35 +5,47 @@ import UserResponse from './models/user_response';
@Injectable()
export class GitOAuthService {
- constructor(private readonly configService: ConfigService) {
- this.clientId = this.configService.get
('SSO_GIT_OAUTH2_CLIENT_ID');
- this.clientSecret = this.configService.get('SSO_GIT_OAUTH2_CLIENT_SECRET');
- }
- private readonly clientId: string;
- private readonly clientSecret: string;
+ constructor(private readonly configService: ConfigService) {}
private readonly authUrl = 'https://github.com/login/oauth/access_token';
private readonly getUserUrl = 'https://api.github.com/user';
+ private readonly getUserEmailUrl = 'https://api.github.com/user/emails';
async #getUserDetails({ access_token }: AuthResponse): Promise {
const response: any = await got(this.getUserUrl, {
method: 'get',
headers: { Accept: 'application/json', Authorization: `token ${access_token}` },
}).json();
- const { name, email } = response;
+ const { name } = response;
+ let { email } = response;
const words = name?.split(' ');
const firstName = words?.[0] || '';
const lastName = words?.length > 1 ? words[words.length - 1] : '';
+ if (!email) {
+ // email visibility not set to public
+ email = await this.#getEmailId(access_token);
+ }
+
return { userSSOId: access_token, firstName, lastName, email, sso: 'git' };
}
- async signIn(code: string): Promise {
+ async #getEmailId(access_token: string) {
+ const response: any = await got(this.getUserEmailUrl, {
+ method: 'get',
+ headers: { Accept: 'application/json', Authorization: `token ${access_token}` },
+ }).json();
+
+ return response?.find((emails) => emails.primary)?.email;
+ }
+
+ async signIn(code: string, configs: any): Promise {
const response: any = await got(this.authUrl, {
method: 'post',
headers: { Accept: 'application/json' },
- json: { client_id: this.clientId, client_secret: this.clientSecret, code },
+ json: { client_id: configs.clientId, client_secret: configs.clientSecret, code },
}).json();
+
return await this.#getUserDetails(response);
}
}
diff --git a/server/ee/services/oauth/google_oauth.service.ts b/server/ee/services/oauth/google_oauth.service.ts
index f00179d19e..d95bbafcf0 100644
--- a/server/ee/services/oauth/google_oauth.service.ts
+++ b/server/ee/services/oauth/google_oauth.service.ts
@@ -1,32 +1,27 @@
import { Injectable } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
import { OAuth2Client, TokenPayload } from 'google-auth-library';
import UserResponse from './models/user_response';
@Injectable()
export class GoogleOAuthService {
- constructor(private readonly configService: ConfigService) {
- this.clientId = this.configService.get('SSO_GOOGLE_OAUTH2_CLIENT_ID');
- this.client = new OAuth2Client(this.clientId);
- }
- private readonly client: OAuth2Client;
- private readonly clientId: string;
+ constructor() {}
#extractDetailsFromPayload(payload: TokenPayload): UserResponse {
const email = payload.email;
const userSSOId = payload.sub;
- const domain = payload.hd;
const words = payload.name?.split(' ');
const firstName = words?.[0] || '';
const lastName = words?.length > 1 ? words[words.length - 1] : '';
- return { userSSOId, firstName, lastName, email, domain, sso: 'google' };
+
+ return { userSSOId, firstName, lastName, email, sso: 'google' };
}
- async signIn(token: string): Promise {
- const ticket = await this.client.verifyIdToken({
+ async signIn(token: string, configs: any): Promise {
+ const client: OAuth2Client = new OAuth2Client(configs.clientId);
+ const ticket = await client.verifyIdToken({
idToken: token,
- audience: this.clientId,
+ audience: configs.clientId,
});
const payload = ticket.getPayload();
return this.#extractDetailsFromPayload(payload);
diff --git a/server/ee/services/oauth/models/user_response.ts b/server/ee/services/oauth/models/user_response.ts
index b03c701d2b..3d590e0ebf 100644
--- a/server/ee/services/oauth/models/user_response.ts
+++ b/server/ee/services/oauth/models/user_response.ts
@@ -3,6 +3,5 @@ export default interface UserResponse {
firstName?: string;
lastName?: string;
email: string;
- domain?: string;
sso: string;
}
diff --git a/server/ee/services/oauth/oauth.service.ts b/server/ee/services/oauth/oauth.service.ts
index 5bc321f83e..969da0625b 100644
--- a/server/ee/services/oauth/oauth.service.ts
+++ b/server/ee/services/oauth/oauth.service.ts
@@ -1,6 +1,5 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
-import { ConfigService } from '@nestjs/config';
import { User } from 'src/entities/user.entity';
import { OrganizationsService } from '@services/organizations.service';
import { OrganizationUsersService } from '@services/organization_users.service';
@@ -9,6 +8,9 @@ import { GoogleOAuthService } from './google_oauth.service';
import { decamelizeKeys } from 'humps';
import { GitOAuthService } from './git_oauth.service';
import UserResponse from './models/user_response';
+import { OrganizationUser } from 'src/entities/organization_user.entity';
+import { Organization } from 'src/entities/organization.entity';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
@Injectable()
export class OauthService {
@@ -18,12 +20,14 @@ export class OauthService {
private readonly jwtService: JwtService,
private readonly organizationUsersService: OrganizationUsersService,
private readonly googleOAuthService: GoogleOAuthService,
- private readonly gitOAuthService: GitOAuthService,
- private readonly configService: ConfigService
+ private readonly gitOAuthService: GitOAuthService
) {}
- #isValidDomain(domain: string): boolean {
- const restrictedDomain = this.configService.get('SSO_RESTRICTED_DOMAIN');
+ #isValidDomain(email: string, restrictedDomain: string): boolean {
+ if (!email) {
+ return false;
+ }
+ const domain = email.substring(email.lastIndexOf('@') + 1);
if (!restrictedDomain) {
return true;
@@ -34,6 +38,7 @@ export class OauthService {
if (
!restrictedDomain
.split(',')
+ .map((e) => e && e.trim())
.filter((e) => !!e)
.includes(domain)
) {
@@ -42,63 +47,71 @@ export class OauthService {
return true;
}
- async #findOrCreateUser({ userSSOId, firstName, lastName, email, sso }: UserResponse): Promise {
- const organization = await this.organizationService.findFirst();
+ async #findOrCreateUser({ firstName, lastName, email }: UserResponse, organization: Organization): Promise {
const { user, newUserCreated } = await this.usersService.findOrCreateByEmail(
- { firstName, lastName, email, ssoId: userSSOId, sso },
- organization
+ { firstName, lastName, email },
+ organization.id
);
if (newUserCreated) {
const organizationUser = await this.organizationUsersService.create(user, organization);
await this.organizationUsersService.activate(organizationUser);
- } else if (userSSOId) {
- await this.usersService.updateSSODetails(user, { userSSOId, sso });
}
return user;
}
- async #findAndActivateUser(email: string): Promise {
- const user = await this.usersService.findByEmail(email);
+ async #findAndActivateUser(email: string, organizationId: string): Promise {
+ const user = await this.usersService.findByEmail(email, organizationId);
if (!user) {
- throw new UnauthorizedException('Invalid credentials');
+ throw new UnauthorizedException('User not exist in the organization');
+ }
+ const organizationUser: OrganizationUser = user.organizationUsers?.[0];
+
+ if (!organizationUser) {
+ throw new UnauthorizedException('User not exist in the organization');
}
- const organizationUser = user.organizationUsers[0];
if (organizationUser.status != 'active') await this.organizationUsersService.activate(organizationUser);
return user;
}
- async #generateLoginResultPayload(user: User): Promise {
- const JWTPayload: JWTPayload = { username: user.id, sub: user.email, ssoId: user.ssoId, sso: user.sso };
+ async #generateLoginResultPayload(user: User, organization: Organization): Promise {
+ const JWTPayload: JWTPayload = { username: user.id, sub: user.email, organizationId: organization.id };
+ user.organizationId = organization.id;
+
return decamelizeKeys({
id: user.id,
auth_token: this.jwtService.sign(JWTPayload),
email: user.email,
first_name: user.firstName,
last_name: user.lastName,
+ organizationId: organization.id,
+ organization: organization.name,
admin: await this.usersService.hasGroup(user, 'admin'),
group_permissions: await this.usersService.groupPermissions(user),
app_group_permissions: await this.usersService.appGroupPermissions(user),
});
}
- async signIn(ssoResponse: SSOResponse): Promise {
- const ssoSignUpDisabled =
- this.configService.get('SSO_DISABLE_SIGNUP') &&
- this.configService.get('SSO_DISABLE_SIGNUP') === 'true';
+ async signIn(ssoResponse: SSOResponse, configId: string): Promise {
+ const ssoConfigs: SSOConfigs = await this.organizationService.getConfigs(configId);
- const { token, origin } = ssoResponse;
+ if (!(ssoConfigs && ssoConfigs?.organization)) {
+ throw new UnauthorizedException();
+ }
+ const organization = ssoConfigs.organization;
+
+ const { enableSignUp, domain } = ssoConfigs.organization;
+ const { sso, configs } = ssoConfigs;
+ const { token } = ssoResponse;
let userResponse: UserResponse;
- switch (origin) {
+ switch (sso) {
case 'google':
- userResponse = await this.googleOAuthService.signIn(token);
- if (!this.#isValidDomain(userResponse.domain))
- throw new UnauthorizedException(`You cannot sign in using a ${userResponse.domain} id`);
+ userResponse = await this.googleOAuthService.signIn(token, configs);
break;
case 'git':
- userResponse = await this.gitOAuthService.signIn(token);
+ userResponse = await this.gitOAuthService.signIn(token, configs);
break;
default:
@@ -108,28 +121,33 @@ export class OauthService {
if (!(userResponse.userSSOId && userResponse.email)) {
throw new UnauthorizedException('Invalid credentials');
}
- const user: User = await (ssoSignUpDisabled
- ? this.#findAndActivateUser(userResponse.email)
- : this.#findOrCreateUser(userResponse));
+ if (!this.#isValidDomain(userResponse.email, domain)) {
+ throw new UnauthorizedException(`You cannot sign in using the mail id - Domain verification failed`);
+ }
+
+ // If name not found
+ if (!(userResponse.firstName && userResponse.lastName)) {
+ userResponse.firstName = userResponse.email?.split('@')?.[0];
+ }
+ const user: User = await (!enableSignUp
+ ? this.#findAndActivateUser(userResponse.email, organization.id)
+ : this.#findOrCreateUser(userResponse, organization));
if (!user) {
throw new UnauthorizedException(`Email id ${userResponse.email} is not registered`);
}
- return await this.#generateLoginResultPayload(user);
+ return await this.#generateLoginResultPayload(user, organization);
}
}
interface SSOResponse {
token: string;
- origin: 'google' | 'git';
state?: string;
- redirectUri?: string;
}
interface JWTPayload {
username: string;
sub: string;
- ssoId: string;
- sso: string;
+ organizationId: string;
}
diff --git a/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts b/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts
index 2da0bdb51b..16f875d67e 100644
--- a/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts
+++ b/server/migrations/1632468258787-PopulateUserGroupsFromOrganizationRoles.ts
@@ -11,7 +11,7 @@ export class PopulateUserGroupsFromOrganizationRoles1632468258787 implements Mig
const entityManager = queryRunner.manager;
const OrganizationRepository = entityManager.getRepository(Organization);
- const organizations = await OrganizationRepository.find();
+ const organizations = await OrganizationRepository.find({ select: ['id'] });
for (const organization of organizations) {
const groupPermissions = await setupInitialGroupPermissions(entityManager, organization);
diff --git a/server/migrations/1639734070615-BackfillDataSourcesAndQueriesForAppVersions.ts b/server/migrations/1639734070615-BackfillDataSourcesAndQueriesForAppVersions.ts
index 8bb90026fa..b2fc7907d3 100644
--- a/server/migrations/1639734070615-BackfillDataSourcesAndQueriesForAppVersions.ts
+++ b/server/migrations/1639734070615-BackfillDataSourcesAndQueriesForAppVersions.ts
@@ -13,7 +13,9 @@ import { cloneDeep } from 'lodash';
export class BackfillDataSourcesAndQueriesForAppVersions1639734070615 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
const entityManager = queryRunner.manager;
- const organizations = await entityManager.find(Organization);
+ const organizations = await entityManager.find(Organization, {
+ select: ['id', 'name'],
+ });
const nestApp = await NestFactory.createApplicationContext(AppModule);
const dataSourcesService = nestApp.get(DataSourcesService);
diff --git a/server/migrations/1645864719155-MultiOrganization.ts b/server/migrations/1645864719155-MultiOrganization.ts
new file mode 100644
index 0000000000..cfe1c38236
--- /dev/null
+++ b/server/migrations/1645864719155-MultiOrganization.ts
@@ -0,0 +1,18 @@
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class MultiOrganization1645864719155 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.addColumn(
+ 'organization_users',
+ new TableColumn({
+ name: 'invitation_token',
+ type: 'varchar',
+ isNullable: true,
+ })
+ );
+ await queryRunner.dropColumn('users', 'sso');
+ await queryRunner.dropColumn('users', 'sso_id');
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {}
+}
diff --git a/server/migrations/1646823984673-OrganizationConfigs.ts b/server/migrations/1646823984673-OrganizationConfigs.ts
new file mode 100644
index 0000000000..abd160b98b
--- /dev/null
+++ b/server/migrations/1646823984673-OrganizationConfigs.ts
@@ -0,0 +1,65 @@
+import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
+
+export class OrganizationConfigs1646823984673 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.createTable(
+ new Table({
+ name: 'sso_configs',
+ columns: [
+ {
+ name: 'id',
+ type: 'uuid',
+ isGenerated: true,
+ default: 'gen_random_uuid()',
+ isPrimary: true,
+ },
+ {
+ name: 'organization_id',
+ type: 'uuid',
+ isNullable: false,
+ },
+ {
+ name: 'sso',
+ type: 'varchar',
+ isNullable: false,
+ },
+ {
+ name: 'configs',
+ type: 'json',
+ isNullable: true,
+ },
+ {
+ name: 'enabled',
+ type: 'boolean',
+ default: true,
+ },
+ {
+ name: 'created_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ {
+ name: 'updated_at',
+ type: 'timestamp',
+ isNullable: true,
+ default: 'now()',
+ },
+ ],
+ }),
+ true
+ );
+
+ await queryRunner.createForeignKey(
+ 'sso_configs',
+ new TableForeignKey({
+ columnNames: ['organization_id'],
+ referencedColumnNames: ['id'],
+ referencedTableName: 'organizations',
+ onDelete: 'CASCADE',
+ })
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {}
+}
diff --git a/server/migrations/1650455299630-OrganizationEnableSignup.ts b/server/migrations/1650455299630-OrganizationEnableSignup.ts
new file mode 100644
index 0000000000..68bc972711
--- /dev/null
+++ b/server/migrations/1650455299630-OrganizationEnableSignup.ts
@@ -0,0 +1,15 @@
+import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
+
+export class OrganizationEnableSignup1650455299630 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.addColumns('organizations', [
+ new TableColumn({
+ name: 'enable_sign_up',
+ type: 'boolean',
+ default: false,
+ }),
+ ]);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {}
+}
diff --git a/server/migrations/1650485473528-PopulateSSOConfigs.ts b/server/migrations/1650485473528-PopulateSSOConfigs.ts
new file mode 100644
index 0000000000..d22725d66d
--- /dev/null
+++ b/server/migrations/1650485473528-PopulateSSOConfigs.ts
@@ -0,0 +1,105 @@
+import { Organization } from 'src/entities/organization.entity';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
+import { MigrationInterface, QueryRunner } from 'typeorm';
+import { EncryptionService } from 'src/services/encryption.service';
+
+export class PopulateSSOConfigs1650485473528 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ const entityManager = queryRunner.manager;
+ const encryptionService = new EncryptionService();
+ const OrganizationRepository = entityManager.getRepository(Organization);
+
+ const isSingleOrganization = process.env.MULTI_ORGANIZATION !== 'true';
+ const enableSignUp = process.env.SSO_DISABLE_SIGNUP !== 'true';
+ const domain = process.env.SSO_RESTRICTED_DOMAIN;
+
+ const googleEnabled = !!process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID;
+ const googleConfigs = {
+ clientId: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
+ };
+
+ const gitEnabled = !!process.env.SSO_GIT_OAUTH2_CLIENT_ID;
+
+ const gitConfigs = {
+ clientId: process.env.SSO_GIT_OAUTH2_CLIENT_ID,
+ clientSecret:
+ process.env.SSO_GIT_OAUTH2_CLIENT_SECRET &&
+ (await encryptionService.encryptColumnValue(
+ 'ssoConfigs',
+ 'clientSecret',
+ process.env.SSO_GIT_OAUTH2_CLIENT_SECRET
+ )),
+ };
+
+ const passwordEnabled = process.env.DISABLE_PASSWORD_LOGIN !== 'true';
+
+ const organizations: Organization[] = await OrganizationRepository.find({
+ relations: ['ssoConfigs'],
+ select: ['ssoConfigs', 'id'],
+ });
+
+ if (organizations && organizations.length > 0) {
+ for (const organization of organizations) {
+ await OrganizationRepository.update({ id: organization.id }, { enableSignUp, ...(domain ? { domain } : {}) });
+ // adding form configs for organizations which does not have any
+ if (
+ !organization.ssoConfigs?.some((og) => {
+ og?.sso === 'form';
+ })
+ ) {
+ await entityManager
+ .createQueryBuilder()
+ .insert()
+ .into(SSOConfigs, ['organizationId', 'sso', 'enabled'])
+ .values({
+ organizationId: organization.id,
+ sso: 'form',
+ enabled: !isSingleOrganization ? true : passwordEnabled,
+ })
+ .execute();
+ }
+ if (
+ isSingleOrganization &&
+ googleEnabled &&
+ !organization.ssoConfigs?.some((og) => {
+ og?.sso === 'google';
+ })
+ ) {
+ await entityManager
+ .createQueryBuilder()
+ .insert()
+ .into(SSOConfigs, ['organizationId', 'sso', 'enabled', 'configs'])
+ .values({
+ organizationId: organization.id,
+ sso: 'google',
+ enabled: googleEnabled,
+ configs: googleConfigs,
+ })
+ .execute();
+ }
+
+ if (
+ isSingleOrganization &&
+ gitEnabled &&
+ !organization.ssoConfigs?.some((og) => {
+ og?.sso === 'git';
+ })
+ ) {
+ await entityManager
+ .createQueryBuilder()
+ .insert()
+ .into(SSOConfigs, ['organizationId', 'sso', 'enabled', 'configs'])
+ .values({
+ organizationId: organization.id,
+ sso: 'git',
+ enabled: gitEnabled,
+ configs: gitConfigs,
+ })
+ .execute();
+ }
+ }
+ }
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {}
+}
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
index 5072ba0236..2a50da9fb7 100644
--- a/server/src/app.module.ts
+++ b/server/src/app.module.ts
@@ -77,6 +77,7 @@ const imports = [
MetaModule,
LibraryAppModule,
GroupPermissionsModule,
+ EventsModule,
];
if (process.env.SERVE_CLIENT !== 'false') {
@@ -98,7 +99,7 @@ if (process.env.APM_VENDOR == 'sentry') {
}
if (process.env.COMMENT_FEATURE_ENABLE !== 'false') {
- imports.unshift(CommentModule, ThreadModule, EventsModule);
+ imports.unshift(CommentModule, ThreadModule);
}
@Module({
diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts
index bd1b5b6cbd..bd625fc52c 100644
--- a/server/src/controllers/app.controller.ts
+++ b/server/src/controllers/app.controller.ts
@@ -1,22 +1,30 @@
+import { Controller, Get, Request, Post, UseGuards, Body, Param, BadRequestException } from '@nestjs/common';
+import { User } from 'src/decorators/user.decorator';
+import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { AppAuthenticationDto, AppForgotPasswordDto, AppPasswordResetDto } from '@dto/app-authentication.dto';
-import { Controller, Get, Request, Post, UseGuards, Body } from '@nestjs/common';
-import { PasswordLoginDisabledGuard } from 'src/modules/auth/password-login-disabled.guard';
import { AuthService } from '../services/auth.service';
@Controller()
export class AppController {
constructor(private authService: AuthService) {}
- @UseGuards(PasswordLoginDisabledGuard)
- @Post('authenticate')
- async login(@Body() appAuthDto: AppAuthenticationDto) {
- return this.authService.login(appAuthDto);
+ @Post(['authenticate', 'authenticate/:organizationId'])
+ async login(@Body() appAuthDto: AppAuthenticationDto, @Param('organizationId') organizationId) {
+ return this.authService.login(appAuthDto.email, appAuthDto.password, organizationId);
+ }
+
+ @UseGuards(JwtAuthGuard)
+ @Get('switch/:organizationId')
+ async switch(@Param('organizationId') organizationId, @User() user) {
+ if (!organizationId) {
+ throw new BadRequestException();
+ }
+ return await this.authService.switchOrganization(organizationId, user);
}
- @UseGuards(PasswordLoginDisabledGuard)
@Post('signup')
async signup(@Body() appAuthDto: AppAuthenticationDto) {
- return this.authService.signup(appAuthDto);
+ return this.authService.signup(appAuthDto.email);
}
@Post('/forgot_password')
diff --git a/server/src/controllers/app_users.controller.ts b/server/src/controllers/app_users.controller.ts
index eced2f52e0..361bb3df5e 100644
--- a/server/src/controllers/app_users.controller.ts
+++ b/server/src/controllers/app_users.controller.ts
@@ -23,7 +23,7 @@ export class AppUsersController {
const { role } = params;
const app = await this.appsService.find(appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, { id: appId });
+ const ability = await this.appsAbilityFactory.appsActions(req.user, 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 d41e270a88..7e1c7d694e 100644
--- a/server/src/controllers/apps.controller.ts
+++ b/server/src/controllers/apps.controller.ts
@@ -1,16 +1,4 @@
-import {
- Controller,
- ForbiddenException,
- Get,
- Param,
- Post,
- Put,
- Delete,
- Query,
- Request,
- UseGuards,
- Body,
-} from '@nestjs/common';
+import { Controller, ForbiddenException, Get, Param, Post, Put, Delete, Query, UseGuards, Body } from '@nestjs/common';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { AppsService } from '../services/apps.service';
import { camelizeKeys, decamelizeKeys } from 'humps';
@@ -19,6 +7,7 @@ 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';
+import { User } from 'src/decorators/user.decorator';
import { AppUpdateDto } from '@dto/app-update.dto';
import { VersionCreateDto } from '@dto/version-create.dto';
@@ -33,26 +22,26 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@Post()
- async create(@Request() req) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ async create(@User() user) {
+ const ability = await this.appsAbilityFactory.appsActions(user);
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);
+ const app = await this.appsService.create(user);
const appUpdateDto = new AppUpdateDto();
appUpdateDto.slug = app.id;
- await this.appsService.update(req.user, app.id, appUpdateDto);
+ await this.appsService.update(user, app.id, appUpdateDto);
return decamelizeKeys(app);
}
@UseGuards(JwtAuthGuard)
@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);
+ async show(@User() user, @Param('id') id) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('viewApp', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -86,19 +75,17 @@ export class AppsController {
@UseGuards(AppAuthGuard) // This guard will allow access for unauthenticated user if the app is public
@Get('slugs/:slug')
- 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, {
- id: app.id,
- });
+ async appFromSlug(@User() user, @Param('slug') slug) {
+ if (user) {
+ const app = await this.appsService.findBySlug(slug);
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('viewApp', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
}
- const app = await this.appsService.findBySlug(params.slug);
+ const app = await this.appsService.findBySlug(slug);
const versionToLoad = app.currentVersionId
? await this.appsService.findVersion(app.currentVersionId)
: await this.appsService.findVersion(app.editingVersion?.id);
@@ -117,15 +104,15 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@Put(':id')
- async update(@Request() req, @Param() params, @Body('app') appUpdateDto: AppUpdateDto) {
- const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+ async update(@User() user, @Param('id') id, @Body('app') appUpdateDto: AppUpdateDto) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateParams', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const result = await this.appsService.update(req.user, params.id, appUpdateDto);
+ const result = await this.appsService.update(user, id, appUpdateDto);
const response = decamelizeKeys(result);
return response;
@@ -133,15 +120,15 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@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, params);
+ async clone(@User() user, @Param('id') id) {
+ const existingApp = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('cloneApp', existingApp)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const result = await this.appsService.clone(existingApp, req.user);
+ const result = await this.appsService.clone(existingApp, user);
const response = decamelizeKeys(result);
return response;
@@ -149,15 +136,15 @@ export class AppsController {
@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);
+ async export(@User() user, @Param('id') id) {
+ const appToExport = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
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);
+ const app = await this.appImportExportService.export(user, id);
return {
...app,
tooljetVersion: globalThis.TOOLJET_VERSION,
@@ -166,28 +153,28 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@Post('/import')
- async import(@Request() req, @Body() body) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ async import(@User() user, @Body() body) {
+ const ability = await this.appsAbilityFactory.appsActions(user);
if (!ability.can('createApp', App)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- await this.appImportExportService.import(req.user, body);
+ await this.appImportExportService.import(user, 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, params);
+ async delete(@User() user, @Param('id') id) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('deleteApp', app)) {
throw new ForbiddenException('Only administrators are allowed to delete apps.');
}
- const result = await this.appsService.delete(params.id);
+ const result = await this.appsService.delete(id);
const response = decamelizeKeys(result);
return response;
@@ -195,7 +182,7 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@Get()
- async index(@Request() req, @Query() query) {
+ async index(@User() user, @Query() query) {
const page = query.page;
const folderId = query.folder;
const searchKey = query.searchKey || '';
@@ -205,15 +192,15 @@ export class AppsController {
if (folderId) {
const folder = await this.foldersService.findOne(folderId);
- apps = await this.foldersService.getAppsFor(req.user, folder, page, searchKey);
- totalFolderCount = await this.foldersService.userAppCount(req.user, folder, searchKey);
+ apps = await this.foldersService.getAppsFor(user, folder, page, searchKey);
+ totalFolderCount = await this.foldersService.userAppCount(user, folder, searchKey);
} else {
- apps = await this.appsService.all(req.user, page, searchKey);
+ apps = await this.appsService.all(user, page, searchKey);
}
//remove password from user info
apps.forEach((app) => (app.user.password = undefined));
- const totalCount = await this.appsService.count(req.user, searchKey);
+ const totalCount = await this.appsService.count(user, searchKey);
const totalPageCount = folderId ? totalFolderCount : totalCount;
@@ -235,44 +222,44 @@ export class AppsController {
// 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, params);
+ async fetchUsers(@User() user, @Param('id') id) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('fetchUsers', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const result = await this.appsService.fetchUsers(req.user, params.id);
+ const result = await this.appsService.fetchUsers(user, id);
return decamelizeKeys({ users: result });
}
@UseGuards(JwtAuthGuard)
@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, params);
+ async fetchVersions(@User() user, @Param('id') id) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('fetchVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const result = await this.appsService.fetchVersions(req.user, params.id);
+ const result = await this.appsService.fetchVersions(user, id);
return { versions: result };
}
@UseGuards(JwtAuthGuard)
@Post(':id/versions')
- async createVersion(@Request() req, @Param() params, @Body() versionCreateDto: VersionCreateDto) {
- const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+ async createVersion(@User() user, @Param('id') id, @Body() versionCreateDto: VersionCreateDto) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('createVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const appUser = await this.appsService.createVersion(
- req.user,
+ user,
app,
versionCreateDto.versionName,
versionCreateDto.versionFromId
@@ -282,38 +269,38 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@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, params);
+ async version(@User() user, @Param('id') id, @Param('versionId') versionId) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('fetchVersions', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const appVersion = await this.appsService.findVersion(params.versionId);
+ const appVersion = await this.appsService.findVersion(versionId);
return { ...appVersion, data_queries: appVersion.dataQueries };
}
@UseGuards(JwtAuthGuard)
@Put(':id/versions/:versionId')
- async updateVersion(@Request() req, @Param() params, @Body('definition') definition) {
- const version = await this.appsService.findVersion(params.versionId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+ async updateVersion(@User() user, @Param('id') id, @Param('versionId') versionId, @Body('definition') definition) {
+ const version = await this.appsService.findVersion(versionId);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateVersions', version.app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const appUser = await this.appsService.updateVersion(req.user, version, definition);
+ const appUser = await this.appsService.updateVersion(user, version, definition);
return decamelizeKeys(appUser);
}
@UseGuards(JwtAuthGuard)
@Delete(':id/versions/:versionId')
- async deleteVersion(@Request() req, @Param() params) {
- const version = await this.appsService.findVersion(params.versionId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+ async deleteVersion(@User() user, @Param('id') id, @Param('versionId') versionId) {
+ const version = await this.appsService.findVersion(versionId);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!version || !ability.can('deleteVersions', version.app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -324,9 +311,9 @@ export class AppsController {
@UseGuards(JwtAuthGuard)
@Put(':id/icons')
- async updateIcon(@Request() req, @Param() params, @Body('icon') icon) {
- const app = await this.appsService.find(params.id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, params);
+ async updateIcon(@User() user, @Param('id') id, @Body('icon') icon) {
+ const app = await this.appsService.find(id);
+ const ability = await this.appsAbilityFactory.appsActions(user, id);
if (!ability.can('updateIcon', app)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -334,7 +321,7 @@ export class AppsController {
const appUpdateDto = new AppUpdateDto();
appUpdateDto.icon = icon;
- const appUser = await this.appsService.update(req.user, params.id, appUpdateDto);
+ const appUser = await this.appsService.update(user, id, appUpdateDto);
return decamelizeKeys(appUser);
}
}
diff --git a/server/src/controllers/comment.controller.ts b/server/src/controllers/comment.controller.ts
index da6cfdf164..6ef13c54ee 100644
--- a/server/src/controllers/comment.controller.ts
+++ b/server/src/controllers/comment.controller.ts
@@ -1,6 +1,5 @@
import {
Controller,
- Request,
Get,
Post,
Body,
@@ -17,34 +16,35 @@ import { Comment } from '../entities/comment.entity';
import { Thread } from '../entities/thread.entity';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { CommentsAbilityFactory } from 'src/modules/casl/abilities/comments-ability.factory';
+import { User } from 'src/decorators/user.decorator';
@Controller('comments')
export class CommentController {
constructor(private commentService: CommentService, private commentsAbilityFactory: CommentsAbilityFactory) {}
@UseGuards(JwtAuthGuard)
- @Post('create')
- public async createComment(@Request() req, @Body() createCommentDto: CreateCommentDto): Promise {
+ @Post()
+ public async createComment(@User() user, @Body() createCommentDto: CreateCommentDto): Promise {
const _response = await Thread.findOne({
where: { id: createCommentDto.threadId },
});
- const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.appId });
+ const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.appId });
if (!ability.can('createComment', Comment)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const comment = await this.commentService.createComment(createCommentDto, req.user.id, req.user.organization.id);
+ const comment = await this.commentService.createComment(createCommentDto, user.id, user.organizationId);
return comment;
}
@UseGuards(JwtAuthGuard)
@Get('/:threadId/all')
- public async getComments(@Request() req, @Param('threadId') threadId: string, @Query() query): Promise {
+ public async getComments(@User() user, @Param('threadId') threadId: string, @Query() query): Promise {
const _response = await Thread.findOne({
where: { id: threadId },
});
- const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.appId });
+ const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.appId });
if (!ability.can('fetchComments', Comment)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -56,18 +56,13 @@ export class CommentController {
@UseGuards(JwtAuthGuard)
@Get('/:appId/notifications')
- public async getNotifications(@Request() req, @Param('appId') appId: string, @Query() query): Promise {
- const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: appId });
+ public async getNotifications(@User() user, @Param('appId') appId: string, @Query() query): Promise {
+ const ability = await this.commentsAbilityFactory.appsActions(user, { id: appId });
if (!ability.can('fetchComments', Comment)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const comments = await this.commentService.getNotifications(
- appId,
- req.user.id,
- query.isResolved,
- query.appVersionsId
- );
+ const comments = await this.commentService.getNotifications(appId, user.id, query.isResolved, query.appVersionsId);
return comments;
}
@@ -79,9 +74,9 @@ export class CommentController {
}
@UseGuards(JwtAuthGuard)
- @Patch('/edit/:commentId')
+ @Patch('/:commentId')
public async editComment(
- @Request() req,
+ @User() user,
@Body() updateCommentDto: UpdateCommentDto,
@Param('commentId') commentId: string
): Promise {
@@ -89,7 +84,7 @@ export class CommentController {
where: { id: commentId },
relations: ['thread'],
});
- const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.thread.appId });
+ const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.thread.appId });
if (!ability.can('updateComment', Comment)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -99,13 +94,13 @@ export class CommentController {
}
@UseGuards(JwtAuthGuard)
- @Delete('/delete/:commentId')
- public async deleteComment(@Request() req, @Param('commentId') commentId: string) {
+ @Delete('/:commentId')
+ public async deleteComment(@User() user, @Param('commentId') commentId: string) {
const _response = await Comment.findOne({
where: { id: commentId },
relations: ['thread'],
});
- const ability = await this.commentsAbilityFactory.appsActions(req.user, { id: _response.thread.appId });
+ const ability = await this.commentsAbilityFactory.appsActions(user, { id: _response.thread.appId });
if (!ability.can('deleteComment', Comment)) {
throw new ForbiddenException('You do not have permissions to perform this action');
diff --git a/server/src/controllers/data_queries.controller.ts b/server/src/controllers/data_queries.controller.ts
index d20dd71cbe..8ec9d43553 100644
--- a/server/src/controllers/data_queries.controller.ts
+++ b/server/src/controllers/data_queries.controller.ts
@@ -7,7 +7,6 @@ import {
Patch,
Delete,
Query,
- Request,
UseGuards,
ForbiddenException,
} from '@nestjs/common';
@@ -19,6 +18,7 @@ import { QueryAuthGuard } from 'src/modules/auth/query-auth.guard';
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
import { AppsService } from '@services/apps.service';
import { CreateDataQueryDto, UpdateDataQueryDto } from '@dto/data-query.dto';
+import { User } from 'src/decorators/user.decorator';
@Controller('data_queries')
export class DataQueriesController {
@@ -31,17 +31,15 @@ export class DataQueriesController {
@UseGuards(JwtAuthGuard)
@Get()
- async index(@Request() req, @Query() query) {
+ async index(@User() user, @Query() query) {
const app = await this.appsService.find(query.app_id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: query.app_id,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, query.app_id);
if (!ability.can('getQueries', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}
- const queries = await this.dataQueriesService.all(req.user, query);
+ const queries = await this.dataQueriesService.all(user, query);
const seralizedQueries = [];
// serialize
@@ -59,16 +57,14 @@ export class DataQueriesController {
@UseGuards(JwtAuthGuard)
@Post()
- async create(@Request() req, @Body() dataQueryDto: CreateDataQueryDto): Promise {
+ async create(@User() user, @Body() dataQueryDto: CreateDataQueryDto): Promise {
const { kind, name, options, app_id, app_version_id, data_source_id } = dataQueryDto;
const appId = app_id;
const appVersionId = app_version_id;
const dataSourceId = data_source_id;
const app = await this.appsService.find(appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: appId,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, appId);
if (!ability.can('createQuery', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -83,7 +79,7 @@ export class DataQueriesController {
}
const dataQuery = await this.dataQueriesService.create(
- req.user,
+ user,
name,
kind,
options,
@@ -96,32 +92,28 @@ export class DataQueriesController {
@UseGuards(JwtAuthGuard)
@Patch(':id')
- async update(@Request() req, @Param() params, @Body() updateDataQueryDto: UpdateDataQueryDto) {
+ async update(@User() user, @Param() params, @Body() updateDataQueryDto: UpdateDataQueryDto) {
const { name, options } = updateDataQueryDto;
const dataQueryId = params.id;
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: dataQuery.appId,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, dataQuery.appId);
if (!ability.can('updateQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}
- const result = await this.dataQueriesService.update(req.user, dataQueryId, name, options);
+ const result = await this.dataQueriesService.update(user, dataQueryId, name, options);
return decamelizeKeys(result);
}
@UseGuards(JwtAuthGuard)
@Delete(':id')
- async delete(@Request() req, @Param() params) {
+ async delete(@User() user, @Param() params) {
const dataQueryId = params.id;
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: dataQuery.appId,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, dataQuery.appId);
if (!ability.can('deleteQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -133,16 +125,13 @@ export class DataQueriesController {
@UseGuards(QueryAuthGuard)
@Post(':id/run')
- async runQuery(@Request() req, @Param() params, @Body() updateDataQueryDto: UpdateDataQueryDto) {
- const dataQueryId = params.id;
+ async runQuery(@User() user, @Param('id') dataQueryId, @Body() updateDataQueryDto: UpdateDataQueryDto) {
const { options } = updateDataQueryDto;
const dataQuery = await this.dataQueriesService.findOne(dataQueryId);
- if (req.user) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: dataQuery.appId,
- });
+ if (user) {
+ const ability = await this.appsAbilityFactory.appsActions(user, dataQuery.appId);
if (!ability.can('runQuery', dataQuery.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -152,7 +141,7 @@ export class DataQueriesController {
let result = {};
try {
- result = await this.dataQueriesService.runQuery(req.user, dataQuery, options);
+ result = await this.dataQueriesService.runQuery(user, dataQuery, options);
} catch (error) {
if (error.constructor.name === 'QueryError') {
result = {
@@ -177,7 +166,7 @@ export class DataQueriesController {
@UseGuards(JwtAuthGuard)
@Post('/preview')
- async previewQuery(@Request() req, @Body() updateDataQueryDto: UpdateDataQueryDto) {
+ async previewQuery(@User() user, @Body() updateDataQueryDto: UpdateDataQueryDto) {
const { options, query } = updateDataQueryDto;
const dataQueryEntity = {
...query,
@@ -185,9 +174,7 @@ export class DataQueriesController {
};
if (dataQueryEntity.dataSource) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: dataQueryEntity.dataSource.appId,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, dataQueryEntity.dataSource.appId);
if (!ability.can('previewQuery', dataQueryEntity.dataSource.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -197,7 +184,7 @@ export class DataQueriesController {
let result = {};
try {
- result = await this.dataQueriesService.runQuery(req.user, dataQueryEntity, options);
+ result = await this.dataQueriesService.runQuery(user, dataQueryEntity, options);
} catch (error) {
if (error.constructor.name === 'QueryError') {
result = {
diff --git a/server/src/controllers/data_sources.controller.ts b/server/src/controllers/data_sources.controller.ts
index 2908a27aa6..b008ddb238 100644
--- a/server/src/controllers/data_sources.controller.ts
+++ b/server/src/controllers/data_sources.controller.ts
@@ -8,7 +8,6 @@ import {
Delete,
Put,
Query,
- Request,
UseGuards,
BadRequestException,
} from '@nestjs/common';
@@ -25,6 +24,7 @@ import {
TestDataSourceDto,
UpdateDataSourceDto,
} from '@dto/data-source.dto';
+import { User } from 'src/decorators/user.decorator';
@Controller('data_sources')
export class DataSourcesController {
@@ -37,17 +37,15 @@ export class DataSourcesController {
@UseGuards(JwtAuthGuard)
@Get()
- async index(@Request() req, @Query() query) {
+ async index(@User() user, @Query() query) {
const app = await this.appsService.find(query.app_id);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: app.id,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('getDataSources', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}
- const dataSources = await this.dataSourcesService.all(req.user, query);
+ const dataSources = await this.dataSourcesService.all(user, query);
const response = decamelizeKeys({ data_sources: dataSources });
return response;
@@ -55,15 +53,13 @@ export class DataSourcesController {
@UseGuards(JwtAuthGuard)
@Post()
- async create(@Request() req, @Body() createDataSourceDto: CreateDataSourceDto) {
+ async create(@User() user, @Body() createDataSourceDto: CreateDataSourceDto) {
const { kind, name, options, app_id, app_version_id } = createDataSourceDto;
const appId = app_id;
const appVersionId = app_version_id;
const app = await this.appsService.find(appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: appId,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, appId);
if (!ability.can('createDataSource', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -75,16 +71,13 @@ export class DataSourcesController {
@UseGuards(JwtAuthGuard)
@Put(':id')
- async update(@Request() req, @Param() params, @Body() updateDataSourceDto: UpdateDataSourceDto) {
- const dataSourceId = params.id;
+ async update(@User() user, @Param('id') dataSourceId, @Body() updateDataSourceDto: UpdateDataSourceDto) {
const { name, options } = updateDataSourceDto;
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
const app = await this.appsService.find(dataSource.appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: app.id,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('updateDataSource', app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
@@ -96,21 +89,17 @@ export class DataSourcesController {
@UseGuards(JwtAuthGuard)
@Delete(':id')
- async delete(@Request() req, @Param() params) {
- const dataSourceId = params.id;
-
+ async delete(@User() user, @Param('id') dataSourceId) {
const dataSource = await this.dataSourcesService.findOne(dataSourceId);
const app = await this.appsService.find(dataSource.appId);
- const ability = await this.appsAbilityFactory.appsActions(req.user, {
- id: app.id,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, app.id);
if (!ability.can('deleteDataSource', dataSource.app)) {
throw new ForbiddenException('you do not have permissions to perform this action');
}
- const result = await this.dataSourcesService.delete(params.id);
+ const result = await this.dataSourcesService.delete(dataSourceId);
if (result.affected == 1) {
return;
} else {
@@ -120,14 +109,14 @@ export class DataSourcesController {
@UseGuards(JwtAuthGuard)
@Post('test_connection')
- async testConnection(@Request() req, @Body() testDataSourceDto: TestDataSourceDto) {
- const { kind, options } = req.body;
+ async testConnection(@User() user, @Body() testDataSourceDto: TestDataSourceDto) {
+ const { kind, options } = testDataSourceDto;
return await this.dataSourcesService.testConnection(kind, options);
}
@UseGuards(JwtAuthGuard)
@Post('fetch_oauth2_base_url')
- async getAuthUrl(@Request() req, @Body() getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto) {
+ async getAuthUrl(@User() user, @Body() getDataSourceOauthUrlDto: GetDataSourceOauthUrlDto) {
const { provider } = getDataSourceOauthUrlDto;
return await this.dataSourcesService.getAuthUrl(provider);
}
@@ -135,7 +124,7 @@ export class DataSourcesController {
@UseGuards(JwtAuthGuard)
@Post(':id/authorize_oauth2')
async authorizeOauth2(
- @Request() req,
+ @User() user,
@Param() params,
@Body() authorizeDataSourceOauthDto: AuthorizeDataSourceOauthDto
) {
@@ -145,9 +134,7 @@ 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, {
- id: app.id,
- });
+ const ability = await this.appsAbilityFactory.appsActions(user, 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
index e4f39c332b..1a8a44ccca 100644
--- a/server/src/controllers/group_permissions.controller.ts
+++ b/server/src/controllers/group_permissions.controller.ts
@@ -1,11 +1,12 @@
-import { Controller, Body, Post, Get, Put, Delete, Request, UseGuards, Param } from '@nestjs/common';
+import { Controller, Body, Post, Get, Put, Delete, 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';
+import { User } from 'src/decorators/user.decorator';
+import { User as UserEntity } from 'src/entities/user.entity';
import { CreateGroupPermissionDto, UpdateGroupPermissionDto } from '@dto/group-permission.dto';
@Controller('group_permissions')
@@ -13,35 +14,35 @@ export class GroupPermissionsController {
constructor(private groupPermissionsService: GroupPermissionsService) {}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Post()
- async create(@Request() req, @Body() createGroupPermissionDto: CreateGroupPermissionDto) {
- const groupPermission = await this.groupPermissionsService.create(req.user, createGroupPermissionDto.group);
-
+ async create(@User() user, @Body() createGroupPermissionDto: CreateGroupPermissionDto) {
+ const groupPermission = await this.groupPermissionsService.create(user, createGroupPermissionDto.group);
return decamelizeKeys(groupPermission);
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Get(':id')
- async show(@Request() req, @Param() params) {
- const groupPermission = await this.groupPermissionsService.findOne(req.user, params.id);
+ async show(@User() user, @Param('id') id: string) {
+ const groupPermission = await this.groupPermissionsService.findOne(user, id);
return decamelizeKeys(groupPermission);
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Put(':id/app_group_permissions/:appGroupPermissionId')
async updateAppGroupPermission(
- @Request() req,
- @Param() params,
- @Body() updateGroupPermissionDto: UpdateGroupPermissionDto
+ @Body() updateGroupPermissionDto: UpdateGroupPermissionDto,
+ @User() user,
+ @Param('id') id: string,
+ @Param('appGroupPermissionId') appGroupPermissionId: string
) {
const groupPermission = await this.groupPermissionsService.updateAppGroupPermission(
- req.user,
- params.id,
- params.appGroupPermissionId,
+ user,
+ id,
+ appGroupPermissionId,
updateGroupPermissionDto.actions
);
@@ -49,64 +50,64 @@ export class GroupPermissionsController {
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Put(':id')
- async update(@Request() req, @Param() params) {
- const groupPermission = await this.groupPermissionsService.update(req.user, params.id, req.body);
+ async update(@User() user, @Param('id') id, @Body() body) {
+ const groupPermission = await this.groupPermissionsService.update(user, id, body);
return decamelizeKeys(groupPermission);
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Get()
- async index(@Request() req) {
- const groupPermissions = await this.groupPermissionsService.findAll(req.user);
+ async index(@User() user) {
+ const groupPermissions = await this.groupPermissionsService.findAll(user);
return decamelizeKeys({ groupPermissions });
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Delete(':id')
- async destroy(@Request() req, @Param() params) {
- const groupPermission = await this.groupPermissionsService.destroy(req.user, params.id);
+ async destroy(@User() user, @Param('id') id) {
+ const groupPermission = await this.groupPermissionsService.destroy(user, id);
return decamelizeKeys(groupPermission);
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Get(':id/apps')
- async apps(@Request() req, @Param() params) {
- const apps = await this.groupPermissionsService.findApps(req.user, params.id);
+ async apps(@User() user, @Param('id') id) {
+ const apps = await this.groupPermissionsService.findApps(user, id);
return decamelizeKeys({ apps });
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Get(':id/addable_apps')
- async addableApps(@Request() req, @Param() params) {
- const apps = await this.groupPermissionsService.findAddableApps(req.user, params.id);
+ async addableApps(@User() user, @Param('id') id) {
+ const apps = await this.groupPermissionsService.findAddableApps(user, id);
return decamelizeKeys({ apps });
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Get(':id/users')
- async users(@Request() req, @Param() params) {
- const users = await this.groupPermissionsService.findUsers(req.user, params.id);
+ async users(@User() user, @Param('id') id) {
+ const users = await this.groupPermissionsService.findUsers(user, id);
return decamelizeKeys({ users });
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('accessGroupPermission', UserEntity))
@Get(':id/addable_users')
- async addableUsers(@Request() req, @Param() params) {
- const users = await this.groupPermissionsService.findAddableUsers(req.user, params.id);
+ async addableUsers(@User() user, @Param('id') id) {
+ const users = await this.groupPermissionsService.findAddableUsers(user, id);
return decamelizeKeys({ users });
}
diff --git a/server/src/controllers/library_apps.controller.ts b/server/src/controllers/library_apps.controller.ts
index 45cbe3a90a..74c6a032d8 100644
--- a/server/src/controllers/library_apps.controller.ts
+++ b/server/src/controllers/library_apps.controller.ts
@@ -1,5 +1,6 @@
-import { Controller, Post, Request, Param, UseGuards, Get, ForbiddenException } from '@nestjs/common';
+import { Controller, Post, UseGuards, Get, ForbiddenException, Body } from '@nestjs/common';
import { LibraryAppCreationService } from '@services/library_app_creation.service';
+import { User } from 'src/decorators/user.decorator';
import { App } from 'src/entities/app.entity';
import { AppsAbilityFactory } from 'src/modules/casl/abilities/apps-ability.factory';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
@@ -14,21 +15,20 @@ export class LibraryAppsController {
@Post()
@UseGuards(JwtAuthGuard)
- async create(@Request() req, @Param() _params) {
- const ability = await this.appsAbilityFactory.appsActions(req.user, {});
+ async create(@User() user, @Body('identifier') identifier) {
+ const ability = await this.appsAbilityFactory.appsActions(user);
if (!ability.can('createApp', App)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const { identifier } = req.body;
- const newApp = await this.libraryAppCreationService.perform(req.user, identifier);
+ const newApp = await this.libraryAppCreationService.perform(user, identifier);
return newApp;
}
@Get()
@UseGuards(JwtAuthGuard)
- async index(@Request() _req, @Param() _params) {
+ async index() {
return { template_app_manifests: TemplateAppManifests };
}
}
diff --git a/server/src/controllers/organization_users.controller.ts b/server/src/controllers/organization_users.controller.ts
index 80f99150f3..df769cf139 100644
--- a/server/src/controllers/organization_users.controller.ts
+++ b/server/src/controllers/organization_users.controller.ts
@@ -1,12 +1,13 @@
-import { Controller, Param, Post, Request, UseGuards, Body } from '@nestjs/common';
+import { Controller, Param, Post, UseGuards, Body } from '@nestjs/common';
import { OrganizationUsersService } from 'src/services/organization_users.service';
import { decamelizeKeys } from 'humps';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
import { AppAbility } from 'src/modules/casl/casl-ability.factory';
import { PoliciesGuard } from 'src/modules/casl/policies.guard';
import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
+import { User as UserEntity } from 'src/entities/user.entity';
+import { User } from 'src/decorators/user.decorator';
import { InviteNewUserDto } from '../dto/invite-new-user.dto';
-import { User } from 'src/entities/user.entity';
@Controller('organization_users')
export class OrganizationUsersController {
@@ -14,35 +15,35 @@ export class OrganizationUsersController {
// Endpoint for inviting new organization users
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('inviteUser', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('inviteUser', UserEntity))
@Post()
- async create(@Request() req, @Body() inviteNewUserDto: InviteNewUserDto) {
- const result = await this.organizationUsersService.inviteNewUser(req.user, inviteNewUserDto);
+ async create(@User() user, @Body() inviteNewUserDto: InviteNewUserDto) {
+ const result = await this.organizationUsersService.inviteNewUser(user, inviteNewUserDto);
return decamelizeKeys({ users: result });
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('archiveUser', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('archiveUser', UserEntity))
@Post(':id/archive')
- async archive(@Request() req, @Param() params) {
- const result = await this.organizationUsersService.archive(params.id);
+ async archive(@Param('id') id: string) {
+ const result = await this.organizationUsersService.archive(id);
return decamelizeKeys({ result });
}
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('archiveUser', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('archiveUser', UserEntity))
@Post(':id/unarchive')
- async unarchive(@Request() req, @Param() params) {
- const result = await this.organizationUsersService.unarchive(req.user, params.id);
+ async unarchive(@User() user, @Param('id') id: string) {
+ const result = await this.organizationUsersService.unarchive(user, id);
return decamelizeKeys({ result });
}
// Deprecated
@UseGuards(JwtAuthGuard, PoliciesGuard)
- @CheckPolicies((ability: AppAbility) => ability.can('changeRole', User))
+ @CheckPolicies((ability: AppAbility) => ability.can('changeRole', UserEntity))
@Post(':id/change_role')
- async changeRole(@Request() req, @Param() params) {
- const result = await this.organizationUsersService.changeRole(req.user, params.id, req.body.role);
+ async changeRole(@Param('id') id, @Body('role') role) {
+ const result = await this.organizationUsersService.changeRole(id, role);
return decamelizeKeys({ result });
}
}
diff --git a/server/src/controllers/organizations.controller.ts b/server/src/controllers/organizations.controller.ts
index e587d90a0b..ef560507ae 100644
--- a/server/src/controllers/organizations.controller.ts
+++ b/server/src/controllers/organizations.controller.ts
@@ -1,16 +1,87 @@
-import { Controller, Get, Request, UseGuards } from '@nestjs/common';
+import { BadRequestException, Body, Controller, Get, Param, Patch, Post, Request, UseGuards } from '@nestjs/common';
import { OrganizationsService } from '@services/organizations.service';
import { decamelizeKeys } from 'humps';
+import { User } from 'src/decorators/user.decorator';
import { JwtAuthGuard } from '../../src/modules/auth/jwt-auth.guard';
+import { AuthService } from '@services/auth.service';
+import { AppAbility } from 'src/modules/casl/casl-ability.factory';
+import { CheckPolicies } from 'src/modules/casl/check_policies.decorator';
+import { PoliciesGuard } from 'src/modules/casl/policies.guard';
+import { User as UserEntity } from 'src/entities/user.entity';
+import { ConfigService } from '@nestjs/config';
+import { MultiOrganizationGuard } from 'src/modules/auth/multi-organization.guard';
@Controller('organizations')
export class OrganizationsController {
- constructor(private organizationsService: OrganizationsService) {}
+ constructor(
+ private organizationsService: OrganizationsService,
+ private authService: AuthService,
+ private readonly configService: ConfigService
+ ) {}
@UseGuards(JwtAuthGuard)
@Get('users')
- async create(@Request() req) {
+ async getUsers(@Request() req) {
const result = await this.organizationsService.fetchUsers(req.user);
return decamelizeKeys({ users: result });
}
+
+ @UseGuards(JwtAuthGuard)
+ @Get()
+ async get(@User() user) {
+ const result = await this.organizationsService.fetchOrganisations(user);
+ return decamelizeKeys({ organizations: result });
+ }
+
+ @UseGuards(JwtAuthGuard, MultiOrganizationGuard)
+ @Post()
+ async create(@Body('name') name, @User() user) {
+ if (!name) {
+ throw new BadRequestException('name can not be empty');
+ }
+ const result = await this.organizationsService.create(name, user);
+
+ if (!result) {
+ throw new Error();
+ }
+ return await this.authService.switchOrganization(result.id, user, true);
+ }
+
+ @Get(['/:organizationId/public-configs', '/public-configs'])
+ async getOrganizationDetails(@Param('organizationId') organizationId: string) {
+ if (!organizationId && this.configService.get('MULTI_ORGANIZATION') !== 'true') {
+ // Request from single organization login page - find one from organization and setting
+ organizationId = (await this.organizationsService.getSingleOrganization()).id;
+ }
+ if (!organizationId) {
+ throw new BadRequestException();
+ }
+
+ const result = await this.organizationsService.fetchOrganisationDetails(organizationId, [true], true);
+ return decamelizeKeys({ ssoConfigs: result });
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('updateOrganizations', UserEntity))
+ @Get('/configs')
+ async getConfigs(@User() user) {
+ const result = await this.organizationsService.fetchOrganisationDetails(user.organizationId);
+ return decamelizeKeys({ organizationDetails: result });
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('updateOrganizations', UserEntity))
+ @Patch()
+ async update(@Body() body, @User() user) {
+ await this.organizationsService.updateOrganization(user.organizationId, body);
+ return {};
+ }
+
+ @UseGuards(JwtAuthGuard, PoliciesGuard)
+ @CheckPolicies((ability: AppAbility) => ability.can('updateOrganizations', UserEntity))
+ @Patch('/configs')
+ async updateConfigs(@Body() body, @User() user) {
+ const result: any = await this.organizationsService.updateOrganizationConfigs(user.organizationId, body);
+ return decamelizeKeys({ id: result.id });
+ }
}
diff --git a/server/src/controllers/thread.controller.ts b/server/src/controllers/thread.controller.ts
index ae28505eb1..2d2a33c0e4 100644
--- a/server/src/controllers/thread.controller.ts
+++ b/server/src/controllers/thread.controller.ts
@@ -1,6 +1,5 @@
import {
Controller,
- Request,
Post,
Body,
Get,
@@ -16,43 +15,44 @@ import { CreateThreadDto, UpdateThreadDto } from '../dto/thread.dto';
import { Thread } from '../entities/thread.entity';
import { JwtAuthGuard } from '../modules/auth/jwt-auth.guard';
import { ThreadsAbilityFactory } from 'src/modules/casl/abilities/threads-ability.factory';
+import { User } from 'src/decorators/user.decorator';
@Controller('threads')
export class ThreadController {
constructor(private threadService: ThreadService, private threadsAbilityFactory: ThreadsAbilityFactory) {}
@UseGuards(JwtAuthGuard)
- @Post('create')
- public async createThread(@Request() req, @Body() createThreadDto: CreateThreadDto): Promise {
- const ability = await this.threadsAbilityFactory.appsActions(req.user, { id: createThreadDto.appId });
+ @Post()
+ public async createThread(@User() user, @Body() createThreadDto: CreateThreadDto): Promise {
+ const ability = await this.threadsAbilityFactory.appsActions(user, createThreadDto.appId);
if (!ability.can('createThread', Thread)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const thread = await this.threadService.createThread(createThreadDto, req.user.id, req.user.organization.id);
+ const thread = await this.threadService.createThread(createThreadDto, user.id, user.organizationId);
return thread;
}
@UseGuards(JwtAuthGuard)
@Get('/:appId/all')
- public async getThreads(@Request() req, @Param('appId') appId: string, @Query() query): Promise {
- const ability = await this.threadsAbilityFactory.appsActions(req.user, { id: appId });
+ public async getThreads(@User() user, @Param('appId') appId: string, @Query() query): Promise {
+ const ability = await this.threadsAbilityFactory.appsActions(user, appId);
if (!ability.can('fetchThreads', Thread)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
- const threads = await this.threadService.getThreads(appId, req.user.organization.id, query.appVersionsId);
+ const threads = await this.threadService.getThreads(appId, user.organizationId, query.appVersionsId);
return threads;
}
@UseGuards(JwtAuthGuard)
@Get('/:threadId')
- public async getThread(@Param('threadId') threadId: number, @Request() req) {
+ public async getThread(@Param('threadId') threadId: number, @User() user) {
const _response = await Thread.findOne({
where: { id: threadId },
});
- const ability = await this.threadsAbilityFactory.appsActions(req.user, { id: _response.appId });
+ const ability = await this.threadsAbilityFactory.appsActions(user, _response.appId);
if (!ability.can('fetchThreads', Thread)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -62,17 +62,17 @@ export class ThreadController {
}
@UseGuards(JwtAuthGuard)
- @Patch('/edit/:threadId')
+ @Patch('/:threadId')
public async editThread(
@Body() updateThreadDto: UpdateThreadDto,
@Param('threadId') threadId: string,
- @Request() req
+ @User() user
): Promise {
const _response = await Thread.findOne({
where: { id: threadId },
});
- const ability = await this.threadsAbilityFactory.appsActions(req.user, { id: _response.appId });
+ const ability = await this.threadsAbilityFactory.appsActions(user, _response.appId);
if (!ability.can('updateThread', Thread)) {
throw new ForbiddenException('You do not have permissions to perform this action');
@@ -82,13 +82,13 @@ export class ThreadController {
}
@UseGuards(JwtAuthGuard)
- @Delete('/delete/:threadId')
- public async deleteThread(@Param('threadId') threadId: string, @Request() req) {
+ @Delete('/:threadId')
+ public async deleteThread(@Param('threadId') threadId: string, @User() user) {
const _response = await Thread.findOne({
where: { id: threadId },
});
- const ability = await this.threadsAbilityFactory.appsActions(req.user, { id: _response.appId });
+ const ability = await this.threadsAbilityFactory.appsActions(user, _response.appId);
if (!ability.can('deleteThread', Thread)) {
throw new ForbiddenException('You do not have permissions to perform this action');
diff --git a/server/src/controllers/users.controller.ts b/server/src/controllers/users.controller.ts
index 6fb11c0bea..b78fd9b735 100644
--- a/server/src/controllers/users.controller.ts
+++ b/server/src/controllers/users.controller.ts
@@ -1,39 +1,46 @@
-import { Body, Controller, Post, Patch, Request, UseGuards } from '@nestjs/common';
+import { Body, Controller, Post, Patch, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from 'src/modules/auth/jwt-auth.guard';
import { PasswordRevalidateGuard } from 'src/modules/auth/password-revalidate.guard';
import { UsersService } from 'src/services/users.service';
+import { User } from 'src/decorators/user.decorator';
+import { MultiOrganizationGuard } from 'src/modules/auth/multi-organization.guard';
+import { SignupDisableGuard } from 'src/modules/auth/signup-disable.guard';
import { CreateUserDto, UpdateUserDto } from '@dto/user.dto';
+import { AcceptInviteDto } from '@dto/accept-organization-invite.dto';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
+ @UseGuards(MultiOrganizationGuard, SignupDisableGuard)
@Post('set_password_from_token')
async create(@Body() userCreateDto: CreateUserDto) {
- const result = await this.usersService.setupAccountFromInvitationToken(userCreateDto);
- return result;
+ await this.usersService.setupAccountFromInvitationToken(userCreateDto);
+ return {};
+ }
+
+ @Post('accept-invite')
+ async acceptInvite(@Body() acceptInviteDto: AcceptInviteDto) {
+ await this.usersService.acceptOrganizationInvite(acceptInviteDto);
+ return {};
}
@UseGuards(JwtAuthGuard)
@Patch('update')
- async update(@Request() req, @Body() updateUserDto: UpdateUserDto) {
- const { first_name, last_name } = updateUserDto;
- await this.usersService.update(req.user.id, {
- firstName: first_name,
- lastName: last_name,
- });
- await req.user.reload();
+ async update(@User() user, @Body() updateUserDto: UpdateUserDto) {
+ const { first_name: firstName, last_name: lastName } = updateUserDto;
+ await this.usersService.update(user.id, { firstName, lastName });
+ await user.reload();
return {
- first_name: req.user.firstName,
- last_name: req.user.lastName,
+ first_name: user.firstName,
+ last_name: user.lastName,
};
}
@UseGuards(JwtAuthGuard, PasswordRevalidateGuard)
@Patch('change_password')
- async changePassword(@Request() req, @Body() body) {
- const { newPassword } = body;
- return await this.usersService.update(req.user.id, {
+ async changePassword(@User() user, @Body('newPassword') newPassword) {
+ return await this.usersService.update(user.id, {
password: newPassword,
});
}
diff --git a/server/src/decorators/user.decorator.ts b/server/src/decorators/user.decorator.ts
new file mode 100644
index 0000000000..3970fdeb54
--- /dev/null
+++ b/server/src/decorators/user.decorator.ts
@@ -0,0 +1,6 @@
+import { createParamDecorator, ExecutionContext } from '@nestjs/common';
+
+export const User = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
+ const request = ctx.switchToHttp().getRequest();
+ return request.user;
+});
diff --git a/server/src/dto/accept-organization-invite.dto.ts b/server/src/dto/accept-organization-invite.dto.ts
new file mode 100644
index 0000000000..5ba68d32bc
--- /dev/null
+++ b/server/src/dto/accept-organization-invite.dto.ts
@@ -0,0 +1,12 @@
+import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
+
+export class AcceptInviteDto {
+ @IsString()
+ @IsOptional()
+ @IsNotEmpty()
+ password: string;
+
+ @IsString()
+ @IsNotEmpty()
+ token: string;
+}
diff --git a/server/src/dto/user.dto.ts b/server/src/dto/user.dto.ts
index 338dda9f85..e2e3f62176 100644
--- a/server/src/dto/user.dto.ts
+++ b/server/src/dto/user.dto.ts
@@ -1,4 +1,4 @@
-import { IsString, IsOptional, IsNotEmpty, IsBoolean } from 'class-validator';
+import { IsString, IsOptional, IsNotEmpty } from 'class-validator';
import { Transform } from 'class-transformer';
import { sanitizeInput } from 'src/helpers/utils.helper';
import { PartialType } from '@nestjs/mapped-types';
@@ -34,10 +34,6 @@ export class CreateUserDto {
@IsOptional()
@Transform(({ value }) => sanitizeInput(value))
role: string;
-
- @IsBoolean()
- @IsOptional()
- new_signup: boolean;
}
export class UpdateUserDto extends PartialType(CreateUserDto) {}
diff --git a/server/src/entities/organization.entity.ts b/server/src/entities/organization.entity.ts
index b81630b723..530bdea0a9 100644
--- a/server/src/entities/organization.entity.ts
+++ b/server/src/entities/organization.entity.ts
@@ -6,12 +6,14 @@ import {
UpdateDateColumn,
OneToMany,
JoinColumn,
+ BaseEntity,
} from 'typeorm';
import { GroupPermission } from './group_permission.entity';
-import { User } from './user.entity';
+import { SSOConfigs } from './sso_config.entity';
+import { OrganizationUser } from './organization_user.entity';
@Entity({ name: 'organizations' })
-export class Organization {
+export class Organization extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@@ -21,6 +23,9 @@ export class Organization {
@Column({ name: 'domain' })
domain: string;
+ @Column({ name: 'enable_sign_up' })
+ enableSignUp: boolean;
+
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
createdAt: Date;
@@ -31,7 +36,9 @@ export class Organization {
@JoinColumn({ name: 'organization_id' })
groupPermissions: GroupPermission[];
- @OneToMany(() => User, (user) => user.organization)
- @JoinColumn({ name: 'organization_id' })
- users: User[];
+ @OneToMany(() => SSOConfigs, (ssoConfigs) => ssoConfigs.organization, { cascade: ['insert'] })
+ ssoConfigs: SSOConfigs[];
+
+ @OneToMany(() => OrganizationUser, (organizationUser) => organizationUser.organization)
+ organizationUsers: OrganizationUser[];
}
diff --git a/server/src/entities/organization_user.entity.ts b/server/src/entities/organization_user.entity.ts
index 59530ff9e3..016a311c6c 100644
--- a/server/src/entities/organization_user.entity.ts
+++ b/server/src/entities/organization_user.entity.ts
@@ -28,6 +28,9 @@ export class OrganizationUser extends BaseEntity {
@Column({ name: 'user_id' })
userId: string;
+ @Column({ name: 'invitation_token' })
+ invitationToken: string;
+
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
createdAt: Date;
diff --git a/server/src/entities/sso_config.entity.ts b/server/src/entities/sso_config.entity.ts
new file mode 100644
index 0000000000..96c44c1caa
--- /dev/null
+++ b/server/src/entities/sso_config.entity.ts
@@ -0,0 +1,45 @@
+import {
+ Entity,
+ Column,
+ PrimaryGeneratedColumn,
+ CreateDateColumn,
+ UpdateDateColumn,
+ ManyToOne,
+ JoinColumn,
+} from 'typeorm';
+import { Organization } from './organization.entity';
+
+type Google = {
+ clientId: string;
+};
+type Git = {
+ clientId: string;
+ clientSecret: string;
+};
+@Entity({ name: 'sso_configs' })
+export class SSOConfigs {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({ name: 'organization_id' })
+ organizationId: string;
+
+ @Column({ name: 'sso' })
+ sso: 'google' | 'git' | 'form';
+
+ @Column({ type: 'json' })
+ configs: Google | Git;
+
+ @Column({ name: 'enabled' })
+ enabled: boolean;
+
+ @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;
+}
diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts
index 9302085bf2..db6062a5f8 100644
--- a/server/src/entities/user.entity.ts
+++ b/server/src/entities/user.entity.ts
@@ -7,14 +7,11 @@ import {
BeforeInsert,
BeforeUpdate,
OneToMany,
- ManyToOne,
- JoinColumn,
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';
@@ -51,17 +48,11 @@ export class User extends BaseEntity {
password: string;
@Column({ name: 'organization_id' })
- organizationId: string;
+ defaultOrganizationId: string;
@Column({ name: 'role' })
role: string;
- @Column({ name: 'sso_id' })
- ssoId: string;
-
- @Column({ name: 'sso' })
- sso: string;
-
@CreateDateColumn({ default: () => 'now()', name: 'created_at' })
createdAt: Date;
@@ -71,10 +62,6 @@ export class User extends BaseEntity {
@OneToMany(() => OrganizationUser, (organizationUser) => organizationUser.user, { eager: true })
organizationUsers: OrganizationUser[];
- @ManyToOne(() => Organization, (organization) => organization.id)
- @JoinColumn({ name: 'organization_id' })
- organization: Organization;
-
@ManyToMany(() => GroupPermission)
@JoinTable({
name: 'user_group_permissions',
@@ -89,4 +76,7 @@ export class User extends BaseEntity {
@OneToMany(() => UserGroupPermission, (userGroupPermission) => userGroupPermission.user, { onDelete: 'CASCADE' })
userGroupPermissions: UserGroupPermission[];
+
+ organizationId: string;
+ isPasswordLogin: boolean;
}
diff --git a/server/src/events/events.module.ts b/server/src/events/events.module.ts
index 14566e4b67..f805d81481 100644
--- a/server/src/events/events.module.ts
+++ b/server/src/events/events.module.ts
@@ -3,8 +3,18 @@ import { EventsGateway } from './events.gateway';
import { YjsGateway } from './yjs.gateway';
import { AuthModule } from 'src/modules/auth/auth.module';
+const providers = [];
+
+if (process.env.COMMENT_FEATURE_ENABLE !== 'false') {
+ providers.unshift(EventsGateway);
+}
+
+if (process.env.ENABLE_MULTIPLAYER_EDITING !== 'false') {
+ providers.unshift(YjsGateway);
+}
+
@Module({
imports: [AuthModule],
- providers: [EventsGateway, YjsGateway],
+ providers,
})
export class EventsModule {}
diff --git a/server/src/helpers/utils.helper.ts b/server/src/helpers/utils.helper.ts
index 259126b467..2cb1f971c4 100644
--- a/server/src/helpers/utils.helper.ts
+++ b/server/src/helpers/utils.helper.ts
@@ -31,6 +31,16 @@ export async function getCachedConnection(dataSourceId, dataSourceUpdatedAt): Pr
}
}
+export function cleanObject(obj: any): any {
+ // This will remove undefined properties, for self and its children
+ Object.keys(obj).forEach((key) => {
+ obj[key] === undefined && delete obj[key];
+ if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
+ cleanObject(obj[key]);
+ }
+ });
+}
+
export function sanitizeInput(value: string) {
return sanitizeHtml(value, {
allowedTags: [],
diff --git a/server/src/main.ts b/server/src/main.ts
index 7bfa31b883..e902fa503c 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -22,10 +22,7 @@ async function bootstrap() {
app.useLogger(app.get(Logger));
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)));
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
-
- if (process.env.COMMENT_FEATURE_ENABLE !== 'false') {
- app.useWebSocketAdapter(new WsAdapter(app));
- }
+ app.useWebSocketAdapter(new WsAdapter(app));
app.setGlobalPrefix('api');
app.enableCors();
diff --git a/server/src/modules/auth/auth.module.ts b/server/src/modules/auth/auth.module.ts
index 6fe0312511..2ba7b73159 100644
--- a/server/src/modules/auth/auth.module.ts
+++ b/server/src/modules/auth/auth.module.ts
@@ -17,12 +17,26 @@ import { OauthService, GoogleOAuthService, GitOAuthService } from '@ee/services/
import { OauthController } from '@ee/controllers/oauth.controller';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { App } from 'src/entities/app.entity';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
+import { GroupPermissionsService } from '@services/group_permissions.service';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { EncryptionService } from '@services/encryption.service';
@Module({
imports: [
UsersModule,
PassportModule,
- TypeOrmModule.forFeature([User, Organization, OrganizationUser, GroupPermission, App]),
+ TypeOrmModule.forFeature([
+ User,
+ Organization,
+ OrganizationUser,
+ GroupPermission,
+ App,
+ SSOConfigs,
+ AppGroupPermission,
+ UserGroupPermission,
+ ]),
JwtModule.registerAsync({
useFactory: (config: ConfigService) => {
return {
@@ -45,6 +59,8 @@ import { App } from 'src/entities/app.entity';
OauthService,
GoogleOAuthService,
GitOAuthService,
+ GroupPermissionsService,
+ EncryptionService,
],
controllers: [OauthController],
exports: [AuthService],
diff --git a/server/src/modules/auth/jwt.strategy.ts b/server/src/modules/auth/jwt.strategy.ts
index 10b18b3da8..7417f5eaa1 100644
--- a/server/src/modules/auth/jwt.strategy.ts
+++ b/server/src/modules/auth/jwt.strategy.ts
@@ -3,6 +3,7 @@ import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { UsersService } from '../../../src/services/users.service';
import { ConfigService } from '@nestjs/config';
+import { User } from 'src/entities/user.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
@@ -15,7 +16,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
async validate(payload: any) {
- const user = await this.usersService.findByEmail(payload.sub);
+ if (!payload.organizationId) return false;
+ const user: User = await this.usersService.findByEmail(payload.sub, payload.organizationId);
+ if (!user) return false;
+
+ user.organizationId = payload.organizationId;
+ user.isPasswordLogin = payload.isPasswordLogin;
if (user && (await this.usersService.status(user)) !== 'archived') return user;
else return false;
diff --git a/server/src/modules/auth/multi-organization.guard.ts b/server/src/modules/auth/multi-organization.guard.ts
new file mode 100644
index 0000000000..ef84dbefd4
--- /dev/null
+++ b/server/src/modules/auth/multi-organization.guard.ts
@@ -0,0 +1,12 @@
+import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { Observable } from 'rxjs';
+
+@Injectable()
+export class MultiOrganizationGuard implements CanActivate {
+ constructor(private configService: ConfigService) {}
+
+ canActivate(context: ExecutionContext): boolean | Promise | Observable {
+ return this.configService.get('MULTI_ORGANIZATION') === 'true';
+ }
+}
diff --git a/server/src/modules/auth/password-login-disabled.guard.ts b/server/src/modules/auth/password-login-disabled.guard.ts
deleted file mode 100644
index 97069aa7ad..0000000000
--- a/server/src/modules/auth/password-login-disabled.guard.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
-import { Observable } from 'rxjs';
-
-@Injectable()
-export class PasswordLoginDisabledGuard implements CanActivate {
- canActivate(context: ExecutionContext): boolean | Promise | Observable {
- return process.env.DISABLE_PASSWORD_LOGIN != 'true';
- }
-}
diff --git a/server/src/modules/auth/signup-disable.guard.ts b/server/src/modules/auth/signup-disable.guard.ts
new file mode 100644
index 0000000000..ceaeec6325
--- /dev/null
+++ b/server/src/modules/auth/signup-disable.guard.ts
@@ -0,0 +1,12 @@
+import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import { Observable } from 'rxjs';
+
+@Injectable()
+export class SignupDisableGuard implements CanActivate {
+ constructor(private configService: ConfigService) {}
+
+ canActivate(context: ExecutionContext): boolean | Promise | Observable {
+ return this.configService.get('DISABLE_SIGNUPS') !== 'true';
+ }
+}
diff --git a/server/src/modules/casl/abilities/apps-ability.factory.ts b/server/src/modules/casl/abilities/apps-ability.factory.ts
index 9755c721f7..aa59364f6d 100644
--- a/server/src/modules/casl/abilities/apps-ability.factory.ts
+++ b/server/src/modules/casl/abilities/apps-ability.factory.ts
@@ -38,7 +38,7 @@ export type AppsAbility = Ability<[Actions, Subjects]>;
export class AppsAbilityFactory {
constructor(private usersService: UsersService) {}
- async appsActions(user: User, params: any) {
+ async appsActions(user: User, id?: string) {
const { can, build } = new AbilityBuilder>(Ability as AbilityClass);
if (await this.usersService.userCan(user, 'create', 'User')) {
@@ -50,7 +50,7 @@ export class AppsAbilityFactory {
can('cloneApp', App, { organizationId: user.organizationId });
}
- if (await this.usersService.userCan(user, 'read', 'App', params.id)) {
+ if (await this.usersService.userCan(user, 'read', 'App', id)) {
can('viewApp', App, { organizationId: user.organizationId });
can('fetchUsers', App, { organizationId: user.organizationId });
@@ -67,7 +67,7 @@ export class AppsAbilityFactory {
});
}
- if (await this.usersService.userCan(user, 'update', 'App', params.id)) {
+ if (await this.usersService.userCan(user, 'update', 'App', id)) {
can('updateParams', App, { organizationId: user.organizationId });
can('createVersions', App, { organizationId: user.organizationId });
can('deleteVersions', App, { organizationId: user.organizationId });
@@ -83,7 +83,7 @@ export class AppsAbilityFactory {
can('deleteDataSource', App, { organizationId: user.organizationId });
}
- if (await this.usersService.userCan(user, 'delete', 'App', params.id)) {
+ if (await this.usersService.userCan(user, 'delete', 'App', id)) {
can('deleteApp', App, { organizationId: user.organizationId });
}
diff --git a/server/src/modules/casl/abilities/threads-ability.factory.ts b/server/src/modules/casl/abilities/threads-ability.factory.ts
index 0905a9c810..1e79d169ea 100644
--- a/server/src/modules/casl/abilities/threads-ability.factory.ts
+++ b/server/src/modules/casl/abilities/threads-ability.factory.ts
@@ -14,22 +14,22 @@ export type ThreadsAbility = Ability<[Actions, Subjects]>;
export class ThreadsAbilityFactory {
constructor(private usersService: UsersService) {}
- async appsActions(user: User, params: any) {
+ async appsActions(user: User, id: string) {
const { can, build } = new AbilityBuilder>(Ability as AbilityClass);
- if (await this.usersService.userCan(user, 'create', 'Thread', params.id)) {
+ if (await this.usersService.userCan(user, 'create', 'Thread', id)) {
can('createThread', Thread, { organizationId: user.organizationId });
}
- if (await this.usersService.userCan(user, 'read', 'Thread', params.id)) {
+ if (await this.usersService.userCan(user, 'read', 'Thread', id)) {
can('fetchThreads', Thread, { organizationId: user.organizationId });
}
- if (await this.usersService.userCan(user, 'update', 'Thread', params.id)) {
+ if (await this.usersService.userCan(user, 'update', 'Thread', id)) {
can('updateThread', Thread, { organizationId: user.organizationId });
}
- if (await this.usersService.userCan(user, 'delete', 'Thread', params.id)) {
+ if (await this.usersService.userCan(user, 'delete', 'Thread', id)) {
can('deleteThread', Thread, { organizationId: user.organizationId });
}
diff --git a/server/src/modules/casl/casl-ability.factory.ts b/server/src/modules/casl/casl-ability.factory.ts
index 2181e4c2ec..3c8dfca270 100644
--- a/server/src/modules/casl/casl-ability.factory.ts
+++ b/server/src/modules/casl/casl-ability.factory.ts
@@ -4,7 +4,7 @@ import { InferSubjects, AbilityBuilder, Ability, AbilityClass, ExtractSubjectTyp
import { Injectable } from '@nestjs/common';
import { UsersService } from '@services/users.service';
-type Actions = 'changeRole' | 'archiveUser' | 'inviteUser' | 'accessGroupPermission';
+type Actions = 'changeRole' | 'archiveUser' | 'inviteUser' | 'accessGroupPermission' | 'updateOrganizations';
type Subjects = InferSubjects | 'all';
@@ -23,6 +23,7 @@ export class CaslAbilityFactory {
can('archiveUser', User);
can('changeRole', User);
can('accessGroupPermission', User);
+ can('updateOrganizations', User);
}
return build({
diff --git a/server/src/modules/organizations/organizations.module.ts b/server/src/modules/organizations/organizations.module.ts
index 8d0612fe2a..d27061eed3 100644
--- a/server/src/modules/organizations/organizations.module.ts
+++ b/server/src/modules/organizations/organizations.module.ts
@@ -12,10 +12,49 @@ import { CaslModule } from '../casl/casl.module';
import { EmailService } from '@services/email.service';
import { GroupPermission } from 'src/entities/group_permission.entity';
import { App } from 'src/entities/app.entity';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
+import { AuthService } from '@services/auth.service';
+import { JwtModule } from '@nestjs/jwt';
+import { ConfigService } from '@nestjs/config';
+import { GroupPermissionsService } from '@services/group_permissions.service';
+import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
+import { UserGroupPermission } from 'src/entities/user_group_permission.entity';
+import { EncryptionService } from '@services/encryption.service';
@Module({
- imports: [TypeOrmModule.forFeature([Organization, OrganizationUser, User, GroupPermission, App]), CaslModule],
- providers: [OrganizationsService, OrganizationUsersService, UsersService, EmailService],
+ imports: [
+ TypeOrmModule.forFeature([
+ Organization,
+ OrganizationUser,
+ User,
+ GroupPermission,
+ App,
+ SSOConfigs,
+ AppGroupPermission,
+ UserGroupPermission,
+ ]),
+ CaslModule,
+ JwtModule.registerAsync({
+ useFactory: (config: ConfigService) => {
+ return {
+ secret: config.get('SECRET_KEY_BASE'),
+ signOptions: {
+ expiresIn: config.get('JWT_EXPIRATION_TIME') || '30d',
+ },
+ };
+ },
+ inject: [ConfigService],
+ }),
+ ],
+ providers: [
+ OrganizationsService,
+ OrganizationUsersService,
+ UsersService,
+ EmailService,
+ AuthService,
+ GroupPermissionsService,
+ EncryptionService,
+ ],
controllers: [OrganizationsController, OrganizationUsersController],
})
export class OrganizationsModule {}
diff --git a/server/src/services/app_config.service.ts b/server/src/services/app_config.service.ts
index 3992a9f438..066c7b43e2 100644
--- a/server/src/services/app_config.service.ts
+++ b/server/src/services/app_config.service.ts
@@ -22,9 +22,8 @@ export class AppConfigService {
'APM_VENDOR',
'SENTRY_DNS',
'SENTRY_DEBUG',
- 'SSO_GOOGLE_OAUTH2_CLIENT_ID',
- 'SSO_GIT_OAUTH2_CLIENT_ID',
- 'DISABLE_PASSWORD_LOGIN',
+ 'DISABLE_SIGNUPS',
+ 'MULTI_ORGANIZATION',
];
}
diff --git a/server/src/services/apps.service.ts b/server/src/services/apps.service.ts
index 2b631ec0fb..607478fd76 100644
--- a/server/src/services/apps.service.ts
+++ b/server/src/services/apps.service.ts
@@ -15,6 +15,7 @@ import { UsersService } from './users.service';
import { AppImportExportService } from './app_import_export.service';
import { DataSourcesService } from './data_sources.service';
import { Credential } from 'src/entities/credential.entity';
+import { cleanObject } from 'src/helpers/utils.helper';
import { AppUpdateDto } from '@dto/app-update.dto';
@Injectable()
@@ -82,7 +83,7 @@ export class AppsService {
name: 'Untitled app',
createdAt: new Date(),
updatedAt: new Date(),
- organizationId: user.organization.id,
+ organizationId: user.organizationId,
user: user,
})
);
@@ -152,15 +153,15 @@ export class AppsService {
'user_group_permissions',
'app_group_permissions.group_permission_id = user_group_permissions.group_permission_id'
)
- .where(
+ .where('apps.organization_id = :organizationId', { organizationId: user.organizationId })
+ .andWhere(
new Brackets((qb) => {
qb.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) OR apps.user_id = :userId', {
+ .orWhere('apps.is_public = :value OR apps.user_id = :userId', {
value: true,
- organizationId: user.organizationId,
userId: user.id,
});
})
@@ -183,15 +184,15 @@ export class AppsService {
'user_group_permissions',
'app_group_permissions.group_permission_id = user_group_permissions.group_permission_id'
)
- .where(
+ .where('apps.organization_id = :organizationId', { organizationId: user.organizationId })
+ .andWhere(
new Brackets((qb) => {
qb.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) OR apps.user_id = :userId', {
+ .orWhere('apps.is_public = :value OR apps.user_id = :userId', {
value: true,
- organizationId: user.organizationId,
userId: user.id,
});
})
@@ -229,9 +230,7 @@ export class AppsService {
};
// removing keys with undefined values
- Object.keys(updateableParams).forEach((key) =>
- updateableParams[key] === undefined ? delete updateableParams[key] : {}
- );
+ cleanObject(updateableParams);
return await this.appsRepository.update(appId, updateableParams);
}
diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts
index 8fe05b662a..61653c45b3 100644
--- a/server/src/services/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, NotFoundException, UnauthorizedException, NotAcceptableException } from '@nestjs/common';
+import { Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common';
import { UsersService } from './users.service';
import { OrganizationsService } from './organizations.service';
import { JwtService } from '@nestjs/jwt';
@@ -6,7 +6,9 @@ import { User } from '../entities/user.entity';
import { OrganizationUsersService } from './organization_users.service';
import { EmailService } from './email.service';
import { decamelizeKeys } from 'humps';
-import { AppAuthenticationDto } from '@dto/app-authentication.dto';
+import { Organization } from 'src/entities/organization.entity';
+import { ConfigService } from '@nestjs/config';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
const bcrypt = require('bcrypt');
const uuid = require('uuid');
@@ -17,7 +19,8 @@ export class AuthService {
private jwtService: JwtService,
private organizationsService: OrganizationsService,
private organizationUsersService: OrganizationUsersService,
- private emailService: EmailService
+ private emailService: EmailService,
+ private configService: ConfigService
) {}
verifyToken(token: string) {
@@ -29,8 +32,9 @@ export class AuthService {
}
}
- async validateUser(email: string, password: string): Promise {
- const user = await this.usersService.findByEmail(email);
+ private async validateUser(email: string, password: string, organisationId?: string): Promise {
+ const user = await this.usersService.findByEmail(email, organisationId);
+
if (!user) return null;
const isVerified = await bcrypt.compare(password, user.password);
@@ -38,11 +42,64 @@ export class AuthService {
return isVerified ? user : null;
}
- async login(appAuthDto: AppAuthenticationDto) {
- const user = await this.validateUser(appAuthDto.email, appAuthDto.password);
+ async login(email: string, password: string, organizationId?: string) {
+ let organization: Organization;
+
+ const user = await this.validateUser(email, password, organizationId);
if (user && (await this.usersService.status(user)) !== 'archived') {
- const payload = { username: user.id, sub: user.email };
+ if (!organizationId) {
+ // Global login
+ // Determine the organization to be loaded
+ if (this.configService.get('MULTI_ORGANIZATION') !== 'true') {
+ // Single organization
+ organization = await this.organizationsService.getSingleOrganization();
+ if (!organization?.ssoConfigs?.find((oc) => oc.sso == 'form' && oc.enabled)) {
+ throw new UnauthorizedException();
+ }
+ } else {
+ const organizationList: Organization[] = await this.organizationsService.findOrganizationSupportsFormLogin(
+ user
+ );
+
+ const defaultOrgDetails: Organization = organizationList?.find((og) => og.id === user.defaultOrganizationId);
+ // Multi organization
+ if (defaultOrgDetails) {
+ // default organization form login enabled
+ organization = defaultOrgDetails;
+ } else if (organizationList?.length > 0) {
+ // default organization form login not enabled, picking first one from form enabled list
+ organization = organizationList[0];
+ } else {
+ // no form login enabled organization available for user - creating new one
+ organization = await this.organizationsService.create('Untitled organization', user);
+ }
+ }
+ user.organizationId = organization.id;
+ } else {
+ // organization specific login
+ user.organizationId = organizationId;
+
+ organization = await this.organizationsService.get(user.organizationId);
+ const formConfigs: SSOConfigs = organization?.ssoConfigs?.find((sso) => sso.sso === 'form');
+
+ if (!formConfigs?.enabled) {
+ // no configurations in organization side or Form login disabled for the organization
+ throw new UnauthorizedException('Password login is disabled for the organization');
+ }
+ }
+
+ if (user.defaultOrganizationId !== user.organizationId) {
+ // Updating default organization Id
+ await this.usersService.updateDefaultOrganization(user, organization.id);
+ }
+
+ const payload = {
+ username: user.id,
+ sub: user.email,
+ organizationId: user.organizationId,
+ isPasswordLogin: true,
+ };
return decamelizeKeys({
id: user.id,
@@ -50,6 +107,8 @@ export class AuthService {
email: user.email,
first_name: user.firstName,
last_name: user.lastName,
+ organizationId: user.organizationId,
+ organization: organization.name,
admin: await this.usersService.hasGroup(user, 'admin'),
group_permissions: await this.usersService.groupPermissions(user),
app_group_permissions: await this.usersService.appGroupPermissions(user),
@@ -59,23 +118,79 @@ export class AuthService {
}
}
- async signup(appAuthDto: AppAuthenticationDto) {
- // Check if the installation allows user signups
- if (process.env.DISABLE_SIGNUPS === 'true') {
- return {};
+ async switchOrganization(newOrganizationId: string, user: User, isNewOrganization?: boolean) {
+ if (!(isNewOrganization || user.isPasswordLogin)) {
+ throw new UnauthorizedException();
}
+ if (this.configService.get('MULTI_ORGANIZATION') !== 'true') {
+ throw new UnauthorizedException();
+ }
+ const newUser = await this.usersService.findByEmail(user.email, newOrganizationId);
- const { email } = appAuthDto;
+ if (newUser && (await this.usersService.status(newUser)) !== 'archived') {
+ newUser.organizationId = newOrganizationId;
+
+ const organization: Organization = await this.organizationsService.get(newUser.organizationId);
+
+ const formConfigs: SSOConfigs = organization?.ssoConfigs?.find((sso) => sso.sso === 'form');
+
+ if (!formConfigs?.enabled) {
+ // no configurations in organization side or Form login disabled for the organization
+ throw new UnauthorizedException('Password login disabled for the organization');
+ }
+
+ // Updating default organization Id
+ await this.usersService.updateDefaultOrganization(newUser, newUser.organizationId);
+
+ const payload = {
+ username: user.id,
+ sub: user.email,
+ organizationId: newUser.organizationId,
+ isPasswordLogin: true,
+ };
+
+ return decamelizeKeys({
+ id: newUser.id,
+ auth_token: this.jwtService.sign(payload),
+ email: newUser.email,
+ first_name: newUser.firstName,
+ last_name: newUser.lastName,
+ organizationId: newUser.organizationId,
+ organization: organization.name,
+ admin: await this.usersService.hasGroup(newUser, 'admin'),
+ group_permissions: await this.usersService.groupPermissions(newUser),
+ app_group_permissions: await this.usersService.appGroupPermissions(newUser),
+ });
+ } else {
+ throw new UnauthorizedException('Invalid credentials');
+ }
+ }
+
+ async signup(email: string) {
const existingUser = await this.usersService.findByEmail(email);
- if (existingUser) {
+ if (existingUser?.invitationToken || existingUser?.organizationUsers?.some((ou) => ou.status === 'active')) {
throw new NotAcceptableException('Email already exists');
}
- const organization = await this.organizationsService.create('Untitled 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);
+ let organization: Organization;
+ // Check if the configs allows user signups
+ if (this.configService.get('MULTI_ORGANIZATION') !== 'true') {
+ // Single organization checking if organization exist
+ organization = await this.organizationsService.getSingleOrganization();
+ if (organization) {
+ throw new NotAcceptableException('Multi organization not supported - organization exist');
+ }
+ } else {
+ // Multi organization
+ if (this.configService.get('DISABLE_SIGNUPS') === 'true') {
+ throw new NotAcceptableException();
+ }
+ }
+ // Create default organization
+ organization = await this.organizationsService.create('Untitled organization');
+ const user = await this.usersService.create({ email }, organization.id, ['all_users', 'admin'], existingUser, true);
+ await this.organizationUsersService.create(user, organization, true);
await this.emailService.sendWelcomeEmail(user.email, user.firstName, user.invitationToken);
return {};
diff --git a/server/src/services/data_queries.service.ts b/server/src/services/data_queries.service.ts
index f2633a37ae..2095cbe38e 100644
--- a/server/src/services/data_queries.service.ts
+++ b/server/src/services/data_queries.service.ts
@@ -31,7 +31,7 @@ export class DataQueriesService {
return await this.dataQueriesRepository.find({
where: whereClause,
- order: { name: 'ASC' },
+ order: { createdAt: 'DESC' }, // Latest query should be on top
});
}
diff --git a/server/src/services/data_sources.service.ts b/server/src/services/data_sources.service.ts
index d72fd92c21..4b41edb022 100644
--- a/server/src/services/data_sources.service.ts
+++ b/server/src/services/data_sources.service.ts
@@ -5,6 +5,7 @@ import { getManager, Repository } from 'typeorm';
import { User } from '../../src/entities/user.entity';
import { DataSource } from '../../src/entities/data_source.entity';
import { CredentialsService } from './credentials.service';
+import { cleanObject } from 'src/helpers/utils.helper';
@Injectable()
export class DataSourcesService {
@@ -59,9 +60,7 @@ export class DataSourcesService {
};
// Remove keys with undefined values
- Object.keys(updateableParams).forEach((key) =>
- updateableParams[key] === undefined ? delete updateableParams[key] : {}
- );
+ cleanObject(updateableParams);
return this.dataSourcesRepository.save(updateableParams);
}
diff --git a/server/src/services/email.service.ts b/server/src/services/email.service.ts
index e37aec3563..5a0a6dfd05 100644
--- a/server/src/services/email.service.ts
+++ b/server/src/services/email.service.ts
@@ -55,7 +55,7 @@ export class EmailService {
async sendWelcomeEmail(to: string, name: string, invitationtoken: string) {
const subject = 'Welcome to ToolJet';
- const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}?signup=true`;
+ const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}`;
const html = `
@@ -81,9 +81,15 @@ export class EmailService {
await this.sendEmail(to, subject, html);
}
- async sendOrganizationUserWelcomeEmail(to: string, name: string, sender: string, invitationtoken: string) {
+ async sendOrganizationUserWelcomeEmail(
+ to: string,
+ name: string,
+ sender: string,
+ invitationtoken: string,
+ organisationName: string
+ ) {
const subject = 'Welcome to ToolJet';
- const inviteUrl = `${this.TOOLJET_HOST}/invitations/${invitationtoken}`;
+ const inviteUrl = `${this.TOOLJET_HOST}/organization-invitations/${invitationtoken}`;
const html = `
@@ -94,7 +100,7 @@ export class EmailService {
Hi ${name || ''},
- ${sender} has invited you to use ToolJet. Use the link below to set up your account and get started.
+ ${sender} has invited you to use ToolJet organisation ${organisationName}. Use the link below to set up your account and get started.
${inviteUrl}
diff --git a/server/src/services/folder_apps.service.ts b/server/src/services/folder_apps.service.ts
index e3823927a4..31f1d6819f 100644
--- a/server/src/services/folder_apps.service.ts
+++ b/server/src/services/folder_apps.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FolderApp } from '../../src/entities/folder_app.entity';
@@ -11,6 +11,14 @@ export class FolderAppsService {
) {}
async create(folderId: string, appId: string): Promise {
+ const existingFolderApp = await this.folderAppsRepository.findOne({
+ where: { appId, folderId },
+ });
+
+ if (existingFolderApp) {
+ throw new BadRequestException('App has been already added to the folder');
+ }
+
const newFolderApp = this.folderAppsRepository.create({
folderId,
appId,
diff --git a/server/src/services/group_permissions.service.ts b/server/src/services/group_permissions.service.ts
index 8fcf857357..d3979f4c10 100644
--- a/server/src/services/group_permissions.service.ts
+++ b/server/src/services/group_permissions.service.ts
@@ -165,7 +165,7 @@ export class GroupPermissionsService {
const params = {
removeGroups: [groupPermission.group],
};
- await this.usersService.update(userId, params, manager);
+ await this.usersService.update(userId, params, manager, user.organizationId);
}
}
@@ -174,7 +174,7 @@ export class GroupPermissionsService {
const params = {
addGroups: [groupPermission.group],
};
- await this.usersService.update(userId, params, manager);
+ await this.usersService.update(userId, params, manager, user.organizationId);
}
}
});
@@ -272,9 +272,23 @@ export class GroupPermissionsService {
.getMany();
const adminUserIds = adminUsers.map((u) => u.userId);
- return await this.userRepository.find({
- id: Not(In([...usersInGroupIds, ...adminUserIds])),
- organizationId: user.organizationId,
- });
+ return await createQueryBuilder(User, 'user')
+ .innerJoin(
+ 'user.organizationUsers',
+ 'organization_users',
+ 'organization_users.organizationId = :organizationId',
+ { organizationId: user.organizationId }
+ )
+ .where('user.id NOT IN (:...userList)', { userList: [...usersInGroupIds, ...adminUserIds] })
+ .getMany();
+ }
+
+ async createUserGroupPermission(userId: string, groupPermissionId: string) {
+ await this.userGroupPermissionsRepository.save(
+ this.userGroupPermissionsRepository.create({
+ userId,
+ groupPermissionId,
+ })
+ );
}
}
diff --git a/server/src/services/organization_users.service.ts b/server/src/services/organization_users.service.ts
index db080f1bcd..d27f0f7ed5 100644
--- a/server/src/services/organization_users.service.ts
+++ b/server/src/services/organization_users.service.ts
@@ -1,12 +1,13 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from '../entities/user.entity';
-import { getManager, Repository } from 'typeorm';
-import { Organization } from 'src/entities/organization.entity';
+import { createQueryBuilder, getManager, Repository } from 'typeorm';
import { UsersService } from 'src/services/users.service';
import { OrganizationUser } from 'src/entities/organization_user.entity';
import { BadRequestException } from '@nestjs/common';
import { EmailService } from './email.service';
+import { Organization } from 'src/entities/organization.entity';
+import { GroupPermission } from 'src/entities/group_permission.entity';
import { InviteNewUserDto } from '@dto/invite-new-user.dto';
const uuid = require('uuid');
@@ -19,7 +20,7 @@ export class OrganizationUsersService {
private emailService: EmailService
) {}
- async findOne(id: string): Promise {
+ async findOrganization(id: string): Promise {
return await this.organizationUsersRepository.findOne({ where: { id } });
}
@@ -30,40 +31,55 @@ export class OrganizationUsersService {
email: inviteNewUserDto.email,
};
- const existingUser = await this.usersService.findByEmail(userParams.email);
- if (existingUser) {
+ let user = await this.usersService.findByEmail(userParams.email);
+
+ if (user?.organizationUsers?.some((ou) => ou.organizationId === currentUser.organizationId)) {
throw new BadRequestException('User with such email already exists.');
}
- const user = await this.usersService.create(userParams, currentUser.organization, ['all_users']);
- const organizationUser = await this.create(user, currentUser.organization);
+
+ if (user?.invitationToken) {
+ // user sign up not completed, name will be empty - updating name
+ await this.usersService.update(user.id, { firstName: userParams.firstName, lastName: userParams.lastName });
+ }
+
+ user = await this.usersService.create(userParams, currentUser.organizationId, ['all_users'], user);
+
+ const currentOrganization: Organization = (
+ await this.organizationUsersRepository.findOne({
+ where: { userId: currentUser.id, organizationId: currentUser.organizationId },
+ relations: ['organization'],
+ })
+ )?.organization;
+
+ const organizationUser: OrganizationUser = await this.create(user, currentOrganization, true);
await this.emailService.sendOrganizationUserWelcomeEmail(
user.email,
user.firstName,
currentUser.firstName,
- user.invitationToken
+ organizationUser.invitationToken,
+ currentOrganization.name
);
return organizationUser;
}
- async create(user: User, organization: Organization): Promise {
+ async create(user: User, organization: Organization, isInvite?: boolean): Promise {
return await this.organizationUsersRepository.save(
this.organizationUsersRepository.create({
user,
organization,
- role: 'all_users',
+ invitationToken: isInvite ? uuid.v4() : null,
+ status: isInvite ? 'invited' : 'active',
+ role: 'all-users',
createdAt: new Date(),
updatedAt: new Date(),
})
);
}
- async changeRole(user: User, id: string, role: string) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const organizationUser = await this.organizationUsersRepository.findOne({
- where: { id },
- });
+ async changeRole(id: string, role: string) {
+ const organizationUser = await this.organizationUsersRepository.findOne({ where: { id } });
if (organizationUser.role == 'admin') {
const lastActiveAdmin = await this.lastActiveAdmin(organizationUser.organizationId);
@@ -83,10 +99,9 @@ export class OrganizationUsersService {
where: { id: organizationUser.userId },
});
- await this.usersService.throwErrorIfRemovingLastActiveAdmin(user);
+ await this.usersService.throwErrorIfRemovingLastActiveAdmin(user, undefined, organizationUser.organizationId);
- await manager.update(User, user.id, { invitationToken: null });
- await manager.update(OrganizationUser, id, { status: 'archived' });
+ await manager.update(OrganizationUser, id, { status: 'archived', invitationToken: null });
});
return true;
@@ -98,23 +113,31 @@ export class OrganizationUsersService {
});
if (organizationUser.status !== 'archived') return false;
+ const invitationToken = uuid.v4();
+
await getManager().transaction(async (manager) => {
await manager.update(OrganizationUser, organizationUser.id, {
status: 'invited',
+ invitationToken,
});
- await manager.update(User, organizationUser.userId, {
- invitationToken: uuid.v4(),
- password: uuid.v4(),
- });
+ await manager.update(User, organizationUser.userId, { password: uuid.v4() });
});
const updatedUser = await this.usersService.findOne(organizationUser.userId);
+ const currentOrganization: Organization = (
+ await this.organizationUsersRepository.findOne({
+ where: { userId: user.id, organizationId: user.organizationId },
+ relations: ['organization'],
+ })
+ )?.organization;
+
await this.emailService.sendOrganizationUserWelcomeEmail(
updatedUser.email,
updatedUser.firstName,
user.firstName,
- updatedUser.invitationToken
+ invitationToken,
+ currentOrganization.name
);
return true;
@@ -133,12 +156,10 @@ export class OrganizationUsersService {
}
async activeAdminCount(organizationId: string) {
- return await this.organizationUsersRepository.count({
- where: {
- organizationId: organizationId,
- role: 'admin',
- status: 'active',
- },
- });
+ return await createQueryBuilder(GroupPermission, 'group_permissions')
+ .innerJoin('group_permissions.userGroupPermission', 'user_group_permission')
+ .where('group_permissions.group = :admin', { admin: 'admin' })
+ .andWhere('group_permissions.organization = :organizationId', { organizationId })
+ .getCount();
}
}
diff --git a/server/src/services/organizations.service.ts b/server/src/services/organizations.service.ts
index 080952a378..20cf667a90 100644
--- a/server/src/services/organizations.service.ts
+++ b/server/src/services/organizations.service.ts
@@ -1,37 +1,70 @@
-import { Injectable } from '@nestjs/common';
+import { BadRequestException, Injectable } from '@nestjs/common';
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';
+import { Organization } from 'src/entities/organization.entity';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
+import { User } from 'src/entities/user.entity';
+import { cleanObject } from 'src/helpers/utils.helper';
+import { createQueryBuilder, Repository } from 'typeorm';
+import { OrganizationUser } from '../entities/organization_user.entity';
+import { EncryptionService } from './encryption.service';
+import { GroupPermissionsService } from './group_permissions.service';
+import { OrganizationUsersService } from './organization_users.service';
+import { UsersService } from './users.service';
@Injectable()
export class OrganizationsService {
constructor(
@InjectRepository(Organization)
private organizationsRepository: Repository,
+ @InjectRepository(SSOConfigs)
+ private ssoConfigRepository: Repository,
@InjectRepository(OrganizationUser)
private organizationUsersRepository: Repository,
@InjectRepository(GroupPermission)
private groupPermissionsRepository: Repository,
- private usersService: UsersService
+ private usersService: UsersService,
+ private organizationUserService: OrganizationUsersService,
+ private groupPermissionService: GroupPermissionsService,
+ private encryptionService: EncryptionService
) {}
- async create(name: string): Promise {
+ async create(name: string, user?: User): Promise {
const organization = await this.organizationsRepository.save(
this.organizationsRepository.create({
+ ssoConfigs: [
+ {
+ sso: 'form',
+ enabled: true,
+ },
+ ],
name,
createdAt: new Date(),
updatedAt: new Date(),
})
);
- await this.createDefaultGroupPermissionsForOrganization(organization);
+ const createdGroupPermissions = await this.createDefaultGroupPermissionsForOrganization(organization);
+
+ if (user) {
+ await this.organizationUserService.create(user, organization, false);
+
+ for (const groupPermission of createdGroupPermissions) {
+ await this.groupPermissionService.createUserGroupPermission(user.id, groupPermission.id);
+ }
+ }
return organization;
}
+ async get(id: string): Promise {
+ return await this.organizationsRepository.findOne({ where: { id }, relations: ['ssoConfigs'] });
+ }
+
+ async getSingleOrganization(): Promise {
+ return await this.organizationsRepository.findOne({ relations: ['ssoConfigs'] });
+ }
+
async createDefaultGroupPermissionsForOrganization(organization: Organization) {
const defaultGroups = ['all_users', 'admin'];
const createdGroupPermissions = [];
@@ -58,6 +91,8 @@ export class OrganizationsService {
relations: ['user'],
});
+ const isAdmin = await this.usersService.hasGroup(user, 'admin');
+
// serialize
const serializedUsers = [];
for (const orgUser of organizationUsers) {
@@ -71,17 +106,206 @@ export class OrganizationsService {
status: orgUser.status,
};
- if ((await this.usersService.hasGroup(user, 'admin')) && orgUser.user.invitationToken)
- serializedUser['invitationToken'] = orgUser.user.invitationToken;
-
+ if (isAdmin && orgUser.invitationToken) {
+ serializedUser['invitationToken'] = orgUser.invitationToken;
+ }
serializedUsers.push(serializedUser);
}
return serializedUsers;
}
- async findFirst(): Promise {
- const organizations = await this.organizationsRepository.find();
- return organizations[0];
+ async fetchOrganisations(user: any): Promise {
+ return await createQueryBuilder(Organization, 'organization')
+ .innerJoin(
+ 'organization.organizationUsers',
+ 'organisation_users',
+ 'organisation_users.status IN(:...statusList)',
+ {
+ statusList: ['active'],
+ }
+ )
+ .andWhere('organisation_users.userId = :userId', {
+ userId: user.id,
+ })
+ .orderBy('name', 'ASC')
+ .getMany();
+ }
+
+ async findOrganizationSupportsFormLogin(user: any): Promise {
+ return await createQueryBuilder(Organization, 'organization')
+ .innerJoin('organization.ssoConfigs', 'organisation_sso', 'organisation_sso.sso = :form', {
+ form: 'form',
+ })
+ .innerJoin(
+ 'organization.organizationUsers',
+ 'organisation_users',
+ 'organisation_users.status IN(:...statusList)',
+ {
+ statusList: ['active'],
+ }
+ )
+ .where('organisation_sso.enabled = :enabled', {
+ enabled: true,
+ })
+ .andWhere('organisation_users.userId = :userId', {
+ userId: user.id,
+ })
+ .orderBy('name', 'ASC')
+ .getMany();
+ }
+
+ async getSSOConfigs(organizationId: string, sso: string): Promise {
+ return await createQueryBuilder(Organization, 'organization')
+ .leftJoinAndSelect('organization.ssoConfigs', 'organisation_sso', 'organisation_sso.sso = :sso', {
+ sso,
+ })
+ .andWhere('organization.id = :organizationId', {
+ organizationId,
+ })
+ .getOne();
+ }
+
+ async fetchOrganisationDetails(
+ organizationId: string,
+ statusList?: Array,
+ isHideSensitiveData?: boolean
+ ): Promise {
+ const result = await createQueryBuilder(Organization, 'organization')
+ .innerJoinAndSelect(
+ 'organization.ssoConfigs',
+ 'organisation_sso',
+ 'organisation_sso.enabled IN (:...statusList)',
+ {
+ statusList: statusList || [true, false], // Return enabled and disabled sso if status list not passed
+ }
+ )
+ .andWhere('organization.id = :organizationId', {
+ organizationId,
+ })
+ .getOne();
+
+ if (!(result?.ssoConfigs?.length > 0)) {
+ return;
+ }
+
+ for (const sso of result?.ssoConfigs) {
+ await this.decryptSecret(sso?.configs);
+ }
+
+ if (!isHideSensitiveData) {
+ return result;
+ }
+ return this.hideSSOSensitiveData(result?.ssoConfigs, result?.name);
+ }
+
+ private hideSSOSensitiveData(ssoConfigs: SSOConfigs[], organizationName): any {
+ const configs = { name: organizationName };
+ if (ssoConfigs?.length > 0) {
+ for (const config of ssoConfigs) {
+ const configId = config['id'];
+ delete config['id'];
+ delete config['organizationId'];
+ delete config['createdAt'];
+ delete config['updatedAt'];
+
+ configs[config.sso] = this.buildConfigs(config, configId);
+ }
+ }
+ return configs;
+ }
+
+ private buildConfigs(config: any, configId: string) {
+ if (!config) return config;
+ return {
+ ...config,
+ configs: {
+ ...(config?.configs || {}),
+ ...(config?.configs ? { clientSecret: '' } : {}),
+ },
+ configId,
+ };
+ }
+
+ private async encryptSecret(configs) {
+ if (!configs || typeof configs !== 'object') return configs;
+ await Promise.all(
+ Object.keys(configs).map(async (key) => {
+ if (key.toLowerCase().includes('secret')) {
+ if (configs[key]) {
+ configs[key] = await this.encryptionService.encryptColumnValue('ssoConfigs', key, configs[key]);
+ }
+ }
+ })
+ );
+ }
+
+ private async decryptSecret(configs) {
+ if (!configs || typeof configs !== 'object') return configs;
+ await Promise.all(
+ Object.keys(configs).map(async (key) => {
+ if (key.toLowerCase().includes('secret')) {
+ if (configs[key]) {
+ configs[key] = await this.encryptionService.decryptColumnValue('ssoConfigs', key, configs[key]);
+ }
+ }
+ })
+ );
+ }
+
+ async updateOrganization(organizationId: string, params) {
+ const { name, domain, enableSignUp } = params;
+
+ const updateableParams = {
+ name,
+ domain,
+ enableSignUp,
+ };
+
+ // removing keys with undefined values
+ cleanObject(updateableParams);
+
+ return await this.organizationsRepository.update(organizationId, updateableParams);
+ }
+
+ async updateOrganizationConfigs(organizationId: string, params: any) {
+ const { type, configs, enabled } = params;
+
+ if (!(type && ['git', 'google', 'form'].includes(type))) {
+ throw new BadRequestException();
+ }
+
+ await this.encryptSecret(configs);
+ const organization: Organization = await this.getSSOConfigs(organizationId, type);
+
+ if (organization?.ssoConfigs?.length > 0) {
+ const ssoConfigs: SSOConfigs = organization.ssoConfigs[0];
+
+ const updateableParams = {
+ configs,
+ enabled,
+ };
+
+ // removing keys with undefined values
+ cleanObject(updateableParams);
+ return await this.ssoConfigRepository.update(ssoConfigs.id, updateableParams);
+ } else {
+ const newSSOConfigs = this.ssoConfigRepository.create({
+ organization,
+ sso: type,
+ configs,
+ enabled: !!enabled,
+ });
+ return await this.ssoConfigRepository.save(newSSOConfigs);
+ }
+ }
+
+ async getConfigs(id: string): Promise {
+ const result: SSOConfigs = await this.ssoConfigRepository.findOne({
+ where: { id, enabled: true },
+ relations: ['organization'],
+ });
+ await this.decryptSecret(result?.configs);
+ return result;
}
}
diff --git a/server/src/services/seeds.service.ts b/server/src/services/seeds.service.ts
index 9f053202ef..fe8599e721 100644
--- a/server/src/services/seeds.service.ts
+++ b/server/src/services/seeds.service.ts
@@ -24,6 +24,12 @@ export class SeedsService {
}
const organization = manager.create(Organization, {
+ ssoConfigs: [
+ {
+ enabled: true,
+ sso: 'form',
+ },
+ ],
name: 'My organization',
});
@@ -34,8 +40,9 @@ export class SeedsService {
lastName: 'Developer',
email: 'dev@tooljet.io',
password: 'password',
- organizationId: organization.id,
+ defaultOrganizationId: organization.id,
});
+ user.organizationId = organization.id;
await manager.save(user);
diff --git a/server/src/services/users.service.ts b/server/src/services/users.service.ts
index 42297d6fd5..38b9ec6c96 100644
--- a/server/src/services/users.service.ts
+++ b/server/src/services/users.service.ts
@@ -9,6 +9,7 @@ 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';
+import { cleanObject } from 'src/helpers/utils.helper';
import { CreateUserDto } from '@dto/user.dto';
const uuid = require('uuid');
const bcrypt = require('bcrypt');
@@ -30,17 +31,23 @@ export class UsersService {
return this.usersRepository.findOne({ where: { id } });
}
- async findByEmail(email: string): Promise {
- return this.usersRepository.findOne({
- where: { email },
- relations: ['organization'],
- });
- }
-
- async findBySSOId(ssoId: string): Promise {
- return this.usersRepository.findOne({
- where: { ssoId },
- });
+ async findByEmail(email: string, organisationId?: string): Promise {
+ if (!organisationId) {
+ return this.usersRepository.findOne({
+ where: { email },
+ });
+ } else {
+ return await createQueryBuilder(User, 'users')
+ .innerJoinAndSelect(
+ 'users.organizationUsers',
+ 'organization_users',
+ 'organization_users.organizationId = :organisationId',
+ { organisationId }
+ )
+ .where('organization_users.status = :active', { active: 'active' })
+ .andWhere('users.email = :email', { email })
+ .getOne();
+ }
}
async findByPasswordResetToken(token: string): Promise {
@@ -49,32 +56,48 @@ export class UsersService {
});
}
- async create(userParams: any, organization: Organization, groups?: string[]): Promise {
+ async create(
+ userParams: any,
+ organizationId: string,
+ groups?: string[],
+ existingUser?: User,
+ isInvite?: boolean
+ ): Promise {
const password = uuid.v4();
- const invitationToken = uuid.v4();
- const { email, firstName, lastName, ssoId, sso } = userParams;
+ const { email, firstName, lastName } = userParams;
let user: User;
await getManager().transaction(async (manager) => {
- user = manager.create(User, {
- email,
- firstName,
- lastName,
- password,
- invitationToken,
- ssoId,
- sso,
- organizationId: organization.id,
- createdAt: new Date(),
- updatedAt: new Date(),
- });
- await manager.save(user);
+ if (!existingUser) {
+ user = manager.create(User, {
+ email,
+ firstName,
+ lastName,
+ password,
+ invitationToken: isInvite ? uuid.v4() : null,
+ defaultOrganizationId: organizationId,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ });
+ await manager.save(user);
+ } else {
+ if (isInvite) {
+ // user already invited to an organization, but not active - user tries to sign up
+ await manager.save(
+ Object.assign(existingUser, {
+ invitationToken: uuid.v4(),
+ defaultOrganizationId: organizationId,
+ })
+ );
+ }
+ user = existingUser;
+ }
for (const group of groups) {
const orgGroupPermission = await manager.findOne(GroupPermission, {
where: {
- organizationId: organization.id,
+ organizationId: organizationId,
group: group,
},
});
@@ -94,75 +117,123 @@ export class UsersService {
return user;
}
- async updateSSODetails(user: User, { userSSOId, sso }) {
- await this.usersRepository.save({
- ...user,
- ssoId: userSSOId,
- sso,
- });
- }
-
async status(user: User) {
const orgUser = await this.organizationUsersRepository.findOne({ where: { user } });
return orgUser.status;
}
- async findOrCreateByEmail(
- userParams: any,
- organization: Organization
- ): Promise<{ user: User; newUserCreated: boolean }> {
+ async findOrCreateByEmail(userParams: any, organizationId: string): Promise<{ user: User; newUserCreated: boolean }> {
let user: User;
let newUserCreated = false;
user = await this.findByEmail(userParams.email);
- if (!user) {
- const groups = ['all_users'];
- user = await this.create({ ...userParams }, organization, groups);
- newUserCreated = true;
+ if (user?.organizationUsers?.some((ou) => ou.organizationId === organizationId)) {
+ // User exist in current organization
+ return { user, newUserCreated };
}
+ const groups = ['all_users'];
+ user = await this.create({ ...userParams }, organizationId, groups, user);
+ newUserCreated = true;
+
return { user, newUserCreated };
}
async setupAccountFromInvitationToken(userCreateDto: CreateUserDto) {
- const { organization, password, token, role } = userCreateDto;
- const firstName = userCreateDto.first_name;
- const lastName = userCreateDto.last_name;
- const newSignup = userCreateDto.new_signup;
+ const { organization, password, token, role, first_name: firstName, last_name: lastName } = userCreateDto;
if (!token) {
throw new BadRequestException('Invalid token');
}
- const user = await this.usersRepository.findOne({ where: { invitationToken: token } });
+ const user: User = await this.usersRepository.findOne({ where: { invitationToken: token } });
- if (user) {
- // beforeUpdate hook will not trigger if using update method of repository
- await this.usersRepository.save(
- Object.assign(user, {
- firstName,
- lastName,
- password,
- role,
- invitationToken: null,
- })
- );
+ if (!user?.organizationUsers) {
+ throw new BadRequestException('Invalid invitation link');
+ }
+ const organizationUser: OrganizationUser = user.organizationUsers.find(
+ (ou) => ou.organizationId === user.defaultOrganizationId
+ );
- const organizationUser = user.organizationUsers[0];
- await this.organizationUsersRepository.update(organizationUser.id, {
+ if (!organizationUser) {
+ throw new BadRequestException('Invalid invitation link');
+ }
+
+ await this.usersRepository.save(
+ Object.assign(user, {
+ firstName,
+ lastName,
+ password,
+ role,
+ invitationToken: null,
+ })
+ );
+
+ await this.organizationUsersRepository.save(
+ Object.assign(organizationUser, {
+ invitationToken: null,
status: 'active',
- });
+ })
+ );
- if (newSignup) {
- await this.organizationsRepository.update(user.organizationId, {
- name: organization,
- });
- }
+ if (organization) {
+ await this.organizationsRepository.update(user.defaultOrganizationId, {
+ name: organization,
+ });
}
}
- async update(userId: string, params: any, manager?: EntityManager) {
+ async acceptOrganizationInvite(params: any) {
+ const { password, token } = params;
+
+ const organizationUser = await this.organizationUsersRepository.findOne({
+ where: { invitationToken: token },
+ relations: ['user'],
+ });
+
+ if (!organizationUser?.user) {
+ throw new BadRequestException('Invalid invitation link');
+ }
+ const user: User = organizationUser.user;
+
+ if (user.invitationToken) {
+ // User sign up link send - not activated account
+ const defaultOrganizationUser = await this.organizationUsersRepository.findOne({
+ where: { organizationId: user.defaultOrganizationId, status: 'invited' },
+ });
+
+ if (defaultOrganizationUser) {
+ await this.organizationUsersRepository.save(
+ Object.assign(defaultOrganizationUser, {
+ invitationToken: null,
+ status: 'active',
+ })
+ );
+ }
+ }
+
+ // set new password if entered
+ await this.usersRepository.save(
+ Object.assign(user, {
+ ...(password ? { password } : {}),
+ invitationToken: null,
+ })
+ );
+
+ await this.organizationUsersRepository.save(
+ Object.assign(organizationUser, {
+ invitationToken: null,
+ status: 'active',
+ })
+ );
+ }
+
+ async updateDefaultOrganization(user: User, organizationId: string) {
+ await this.usersRepository.update(user.id, { defaultOrganizationId: organizationId });
+ }
+
+ async update(userId: string, params: any, manager?: EntityManager, organizationId?: string) {
const { forgotPasswordToken, password, firstName, lastName, addGroups, removeGroups } = params;
const hashedPassword = password ? bcrypt.hashSync(password, 10) : undefined;
@@ -175,9 +246,7 @@ export class UsersService {
};
// removing keys with undefined values
- Object.keys(updateableParams).forEach((key) =>
- updateableParams[key] === undefined ? delete updateableParams[key] : {}
- );
+ cleanObject(updateableParams);
let user: User;
@@ -185,9 +254,9 @@ export class UsersService {
await manager.update(User, userId, { ...updateableParams });
user = await manager.findOne(User, { where: { id: userId } });
- await this.removeUserGroupPermissionsIfExists(manager, user, removeGroups);
+ await this.removeUserGroupPermissionsIfExists(manager, user, removeGroups, organizationId);
- await this.addUserGroupPermissions(manager, user, addGroups);
+ await this.addUserGroupPermissions(manager, user, addGroups, organizationId);
};
if (manager) {
@@ -201,9 +270,10 @@ export class UsersService {
return user;
}
- async addUserGroupPermissions(manager: EntityManager, user: User, addGroups: string[]) {
+ async addUserGroupPermissions(manager: EntityManager, user: User, addGroups: string[], organizationId?: string) {
+ const orgId = organizationId || user.defaultOrganizationId;
if (addGroups) {
- const orgGroupPermissions = await this.groupPermissionsForOrganization(user.organizationId);
+ const orgGroupPermissions = await this.groupPermissionsForOrganization(orgId);
for (const group of addGroups) {
const orgGroupPermission = orgGroupPermissions.find((permission) => permission.group == group);
@@ -221,16 +291,22 @@ export class UsersService {
}
}
- async removeUserGroupPermissionsIfExists(manager: EntityManager, user: User, removeGroups: string[]) {
+ async removeUserGroupPermissionsIfExists(
+ manager: EntityManager,
+ user: User,
+ removeGroups: string[],
+ organizationId?: string
+ ) {
+ const orgId = organizationId || user.defaultOrganizationId;
if (removeGroups) {
- await this.throwErrorIfRemovingLastActiveAdmin(user, removeGroups);
+ await this.throwErrorIfRemovingLastActiveAdmin(user, removeGroups, orgId);
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,
+ organizationId: orgId,
});
const groupIdsToMaybeRemove = groupPermissions.map((permission) => permission.id);
@@ -241,7 +317,7 @@ export class UsersService {
}
}
- async throwErrorIfRemovingLastActiveAdmin(user: User, removeGroups: string[] = ['admin']) {
+ async throwErrorIfRemovingLastActiveAdmin(user: User, removeGroups: string[] = ['admin'], organizationId: string) {
const removingAdmin = removeGroups.includes('admin');
if (!removingAdmin) return;
@@ -252,7 +328,7 @@ export class UsersService {
.andWhere('organization_users.status = :status', { status: 'active' })
.andWhere('group_permissions.group = :group', { group: 'admin' })
.andWhere('group_permissions.organization_id = :organizationId', {
- organizationId: user.organizationId,
+ organizationId,
})
.getCount();
@@ -260,8 +336,6 @@ export class UsersService {
}
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')
@@ -289,7 +363,7 @@ export class UsersService {
return await this.canUserPerformActionOnApp(user, 'update', resourceId);
case 'Folder':
- return await this.canUserPerformActionOnFolder(user, action, resourceId);
+ return await this.canUserPerformActionOnFolder(user, action);
default:
return false;
@@ -323,7 +397,7 @@ export class UsersService {
return permissionGrant;
}
- async canUserPerformActionOnFolder(user: User, action: string, folderId?: string): Promise {
+ async canUserPerformActionOnFolder(user: User, action: string): Promise {
let permissionGrant: boolean;
switch (action) {
@@ -338,22 +412,22 @@ export class UsersService {
return permissionGrant;
}
- async isUserOwnerOfApp(user, appId): Promise {
- const app = await this.appsRepository.findOne({
+ async isUserOwnerOfApp(user: User, appId: string): Promise {
+ const app: App = await this.appsRepository.findOne({
where: {
id: appId,
userId: user.id,
},
});
- return !!app;
+ return !!app && app.organizationId === user.organizationId;
}
canAnyGroupPerformAction(action: string, permissions: AppGroupPermission[] | GroupPermission[]): boolean {
return permissions.some((p) => p[action]);
}
- async groupPermissions(user: User, organizationId?: string): Promise {
- const orgUserGroupPermissions = await this.userGroupPermissions(user, organizationId);
+ async groupPermissions(user: User): Promise {
+ const orgUserGroupPermissions = await this.userGroupPermissions(user, user.organizationId);
const groupIds = orgUserGroupPermissions.map((p) => p.groupPermissionId);
const groupPermissionRepository = getRepository(GroupPermission);
@@ -366,26 +440,32 @@ export class UsersService {
return await groupPermissionRepository.find({ organizationId });
}
- async appGroupPermissions(user: User, appId?: string, organizationId?: string): Promise {
- const orgUserGroupPermissions = await this.userGroupPermissions(user, organizationId);
+ async appGroupPermissions(user: User, appId?: string): Promise {
+ const orgUserGroupPermissions = await this.userGroupPermissions(user, user.organizationId);
const groupIds = orgUserGroupPermissions.map((p) => p.groupPermissionId);
- const appGroupPermissionRepository = getRepository(AppGroupPermission);
+
+ if (!groupIds || groupIds.length === 0) {
+ return [];
+ }
+
+ const query = createQueryBuilder(AppGroupPermission, 'app_group_permissions')
+ .innerJoin(
+ 'app_group_permissions.groupPermission',
+ 'group_permissions',
+ 'group_permissions.organization_id = :organizationId',
+ {
+ organizationId: user.organizationId,
+ }
+ )
+ .where('app_group_permissions.groupPermissionId IN (:...groupIds)', { groupIds });
if (appId) {
- return await appGroupPermissionRepository.find({
- groupPermissionId: In(groupIds),
- appId: appId,
- });
- } else {
- return await appGroupPermissionRepository.find({
- groupPermissionId: In(groupIds),
- });
+ query.andWhere('app_group_permissions.appId = :appId', { appId });
}
+ return await query.getMany();
}
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')
diff --git a/server/test/controllers/app.e2e-spec.ts b/server/test/controllers/app.e2e-spec.ts
index 4a16664a1a..74f9fa4f75 100644
--- a/server/test/controllers/app.e2e-spec.ts
+++ b/server/test/controllers/app.e2e-spec.ts
@@ -3,111 +3,394 @@ import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
import { getManager, Repository } from 'typeorm';
import { User } from 'src/entities/user.entity';
-import { clearDB, createUser, createNestAppInstance, authHeaderForUser } from '../test.helper';
+import { clearDB, createUser, authHeaderForUser, createNestAppInstanceWithEnvMock } from '../test.helper';
import { OrganizationUser } from 'src/entities/organization_user.entity';
+import { Organization } from 'src/entities/organization.entity';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
import { EmailService } from '@services/email.service';
describe('Authentication', () => {
let app: INestApplication;
let userRepository: Repository;
+ let orgRepository: Repository;
let orgUserRepository: Repository;
- const originalEnv = process.env;
+ let ssoConfigsRepository: Repository;
+ let mockConfig;
+ let current_organization: Organization;
+ let current_user: User;
beforeEach(async () => {
await clearDB();
- await createUser(app, { email: 'admin@tooljet.io' });
});
beforeAll(async () => {
- app = await createNestAppInstance();
+ ({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
userRepository = app.get('UserRepository');
+ orgRepository = app.get('OrganizationRepository');
orgUserRepository = app.get('OrganizationUserRepository');
+ ssoConfigsRepository = app.get('SSOConfigsRepository');
});
- it('should create new users', async () => {
- const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
- expect(response.statusCode).toBe(201);
-
- const user = await userRepository.findOne({
- where: { email: 'test@tooljet.io' },
- relations: ['organization'],
- });
-
- expect(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));
-
- const adminGroup = groupPermissions.find((x) => x.group == 'admin');
- expect(adminGroup.appCreate).toBeTruthy();
- expect(adminGroup.appDelete).toBeTruthy();
- expect(adminGroup.folderCreate).toBeTruthy();
-
- const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
- expect(allUserGroup.appCreate).toBeFalsy();
- expect(allUserGroup.appDelete).toBeFalsy();
- expect(allUserGroup.folderCreate).toBeFalsy();
+ afterEach(() => {
+ jest.resetAllMocks();
+ jest.clearAllMocks();
});
- 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 user is archived', async () => {
- await createUser(app, { email: 'user@tooljet.io', status: 'archived' });
-
- await request(app.getHttpServer())
- .post('/api/authenticate')
- .send({ email: 'user@tooljet.io', password: 'password' })
- .expect(401);
-
- const adminUser = await userRepository.findOne({
- email: 'admin@tooljet.io',
- });
- await orgUserRepository.update({ userId: adminUser.id }, { status: 'archived' });
-
- await request(app.getHttpServer())
- .get('/api/organizations/users')
- .set('Authorization', authHeaderForUser(adminUser))
- .expect(401);
- });
-
- it('throw 401 if invalid credentials', async () => {
- await request(app.getHttpServer())
- .post('/api/authenticate')
- .send({ email: 'amdin@tooljet.io', password: 'pwd' })
- .expect(401);
- });
-
- describe('if password login is disabled', () => {
- beforeAll(async () => {
- process.env = { ...originalEnv, DISABLE_PASSWORD_LOGIN: 'true' };
- });
-
- it('should not create new users', async () => {
+ describe('Single organization', () => {
+ it('should create new users and organization', async () => {
const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
- expect(response.statusCode).toBe(403);
- });
+ expect(response.statusCode).toBe(201);
- it('does not authenticate if valid credentials', async () => {
- await request(app.getHttpServer())
- .post('/api/authenticate')
- .send({ email: 'admin@tooljet.io', password: 'password' })
- .expect(403);
- });
+ const user = await userRepository.findOneOrFail({
+ where: { email: 'test@tooljet.io' },
+ relations: ['organizationUsers'],
+ });
- afterAll(async () => {
- process.env = { ...originalEnv };
+ const organization = await orgRepository.findOneOrFail({
+ where: { id: user?.organizationUsers?.[0]?.organizationId },
+ });
+
+ expect(user.defaultOrganizationId).toBe(user?.organizationUsers?.[0]?.organizationId);
+ expect(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));
+
+ const adminGroup = groupPermissions.find((x) => x.group == 'admin');
+ expect(adminGroup.appCreate).toBeTruthy();
+ expect(adminGroup.appDelete).toBeTruthy();
+ expect(adminGroup.folderCreate).toBeTruthy();
+
+ const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
+ expect(allUserGroup.appCreate).toBeFalsy();
+ expect(allUserGroup.appDelete).toBeFalsy();
+ expect(allUserGroup.folderCreate).toBeFalsy();
+ });
+ describe('Single organization operations', () => {
+ beforeEach(async () => {
+ current_organization = (await createUser(app, { email: 'admin@tooljet.io' })).organization;
+ });
+ it('should not create new users since organization already exist', async () => {
+ const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
+ expect(response.statusCode).toBe(406);
+ });
+ it('authenticate if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(201);
+ });
+ it('authenticate to organization if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate/' + current_organization.id)
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(201);
+ });
+ it('throw unauthorized error if user not exist in given organization if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate/82249621-efc1-4cd2-9986-5c22182fa8a7')
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(401);
+ });
+ it('throw 401 if user is archived', async () => {
+ await createUser(app, { email: 'user@tooljet.io', status: 'archived' });
+
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'user@tooljet.io', password: 'password' })
+ .expect(401);
+
+ const adminUser = await userRepository.findOneOrFail({
+ email: 'admin@tooljet.io',
+ });
+ await orgUserRepository.update({ userId: adminUser.id }, { status: 'archived' });
+
+ await request(app.getHttpServer())
+ .get('/api/organizations/users')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .expect(401);
+ });
+ it('throw 401 if invalid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'amdin@tooljet.io', password: 'pwd' })
+ .expect(401);
+ });
+ it('should throw 401 if form login is disabled', async () => {
+ await ssoConfigsRepository.update({ organizationId: current_organization.id }, { enabled: false });
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(401);
+ });
+ });
+ });
+
+ describe('Multi organization', () => {
+ beforeEach(async () => {
+ const { organization, user } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ firstName: 'user',
+ lastName: 'name',
+ });
+ current_organization = organization;
+ current_user = user;
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'DISABLE_SIGNUPS':
+ return 'false';
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ });
+ describe('sign up disabled', () => {
+ beforeEach(async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'DISABLE_SIGNUPS':
+ return 'true';
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ });
+ it('should not create new users', async () => {
+ const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
+ expect(response.statusCode).toBe(406);
+ });
+ });
+ describe('sign up enabled and authorization', () => {
+ it('should create new users', async () => {
+ const response = await request(app.getHttpServer()).post('/api/signup').send({ email: 'test@tooljet.io' });
+ expect(response.statusCode).toBe(201);
+
+ const user = await userRepository.findOneOrFail({
+ where: { email: 'test@tooljet.io' },
+ relations: ['organizationUsers'],
+ });
+
+ const organization = await orgRepository.findOneOrFail({
+ where: { id: user?.organizationUsers?.[0]?.organizationId },
+ });
+
+ expect(user.defaultOrganizationId).toBe(user?.organizationUsers?.[0]?.organizationId);
+ expect(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));
+
+ const adminGroup = groupPermissions.find((x) => x.group == 'admin');
+ expect(adminGroup.appCreate).toBeTruthy();
+ expect(adminGroup.appDelete).toBeTruthy();
+ expect(adminGroup.folderCreate).toBeTruthy();
+
+ const allUserGroup = groupPermissions.find((x) => x.group == 'all_users');
+ expect(allUserGroup.appCreate).toBeFalsy();
+ expect(allUserGroup.appDelete).toBeFalsy();
+ expect(allUserGroup.folderCreate).toBeFalsy();
+ });
+ it('authenticate if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(201);
+ });
+ it('authenticate to organization if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate/' + current_organization.id)
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(201);
+ });
+ it('throw unauthorized error if user not exist in given organization if valid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate/82249621-efc1-4cd2-9986-5c22182fa8a7')
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(401);
+ });
+ it('throw 401 if user is archived', async () => {
+ await createUser(app, { email: 'user@tooljet.io', status: 'archived' });
+
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'user@tooljet.io', password: 'password' })
+ .expect(401);
+
+ const adminUser = await userRepository.findOneOrFail({
+ email: 'admin@tooljet.io',
+ });
+ await orgUserRepository.update({ userId: adminUser.id }, { status: 'archived' });
+
+ await request(app.getHttpServer())
+ .get('/api/organizations/users')
+ .set('Authorization', authHeaderForUser(adminUser))
+ .expect(401);
+ });
+ it('throw 401 if invalid credentials', async () => {
+ await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'amdin@tooljet.io', password: 'pwd' })
+ .expect(401);
+ });
+ it('should throw 401 if form login is disabled', async () => {
+ await ssoConfigsRepository.update({ organizationId: current_organization.id }, { enabled: false });
+ await request(app.getHttpServer())
+ .post('/api/authenticate/' + current_organization.id)
+ .send({ email: 'admin@tooljet.io', password: 'password' })
+ .expect(401);
+ });
+ it('should create new organization if login is disabled for default organization', async () => {
+ await ssoConfigsRepository.update({ organizationId: current_organization.id }, { enabled: false });
+ const response = await request(app.getHttpServer())
+ .post('/api/authenticate')
+ .send({ email: 'admin@tooljet.io', password: 'password' });
+ expect(response.statusCode).toBe(201);
+ expect(response.body.organization_id).not.toBe(current_organization.id);
+ expect(response.body.organization).toBe('Untitled organization');
+ });
+ it('should be able to switch between organizations with admin privilage', async () => {
+ const { organization: invited_organization } = await createUser(
+ app,
+ { organizationName: 'New Organization' },
+ current_user
+ );
+ const response = await request(app.getHttpServer())
+ .get('/api/switch/' + invited_organization.id)
+ .set('Authorization', authHeaderForUser(current_user));
+
+ expect(response.statusCode).toBe(200);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual(current_user.email);
+ expect(first_name).toEqual(current_user.firstName);
+ expect(last_name).toEqual(current_user.lastName);
+ expect(admin).toBeTruthy();
+ expect(organization_id).toBe(invited_organization.id);
+ expect(organization).toBe(invited_organization.name);
+ expect(group_permissions).toHaveLength(2);
+ expect(group_permissions.some((gp) => gp.group === 'all_users')).toBeTruthy();
+ expect(group_permissions.some((gp) => gp.group === 'admin')).toBeTruthy();
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ await current_user.reload();
+ expect(current_user.defaultOrganizationId).toBe(invited_organization.id);
+ });
+ it('should be able to switch between organizations with user privilage', async () => {
+ const { organization: invited_organization } = await createUser(
+ app,
+ { groups: ['all_users'], organizationName: 'New Organization' },
+ current_user
+ );
+ const response = await request(app.getHttpServer())
+ .get('/api/switch/' + invited_organization.id)
+ .set('Authorization', authHeaderForUser(current_user));
+
+ expect(response.statusCode).toBe(200);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual(current_user.email);
+ expect(first_name).toEqual(current_user.firstName);
+ expect(last_name).toEqual(current_user.lastName);
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(invited_organization.id);
+ expect(organization).toBe(invited_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ await current_user.reload();
+ expect(current_user.defaultOrganizationId).toBe(invited_organization.id);
+ });
});
});
describe('POST /api/forgot_password', () => {
+ beforeEach(async () => {
+ await createUser(app, {
+ email: 'admin@tooljet.io',
+ firstName: 'user',
+ lastName: 'name',
+ });
+ });
it('should return error if required params are not present', async () => {
const response = await request(app.getHttpServer()).post('/api/forgot_password');
@@ -134,6 +417,13 @@ describe('Authentication', () => {
});
describe('POST /api/reset_password', () => {
+ beforeEach(async () => {
+ await createUser(app, {
+ email: 'admin@tooljet.io',
+ firstName: 'user',
+ lastName: 'name',
+ });
+ });
it('should return error if required params are not present', async () => {
const response = await request(app.getHttpServer()).post('/api/reset_password');
diff --git a/server/test/controllers/apps.e2e-spec.ts b/server/test/controllers/apps.e2e-spec.ts
index 800eda9245..a80ff6a243 100644
--- a/server/test/controllers/apps.e2e-spec.ts
+++ b/server/test/controllers/apps.e2e-spec.ts
@@ -99,7 +99,7 @@ describe('apps controller', () => {
expect(response.body.name).toBe('Untitled app');
const appId = response.body.id;
- const application = await App.findOne({ where: { id: appId } });
+ const application = await App.findOneOrFail({ where: { id: appId } });
expect(application.name).toBe('Untitled app');
expect(application.id).toBe(application.slug);
@@ -120,7 +120,7 @@ describe('apps controller', () => {
groups: ['all_users', 'admin'],
});
const organization = adminUserData.organization;
- const allUserGroup = await getManager().findOne(GroupPermission, {
+ const allUserGroup = await getManager().findOneOrFail(GroupPermission, {
where: {
group: 'all_users',
organization: adminUserData.organization,
@@ -422,7 +422,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(201);
const appId = response.body.id;
- const clonedApplication = await App.findOne({ where: { id: appId } });
+ const clonedApplication = await App.findOneOrFail({ where: { id: appId } });
expect(clonedApplication.name).toBe('App to clone');
response = await request(app.getHttpServer())
@@ -569,11 +569,11 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(200);
- expect(await App.findOne({ where: { id: application.id } })).toBeUndefined();
- expect(await AppVersion.findOne({ where: { id: version.id } })).toBeUndefined();
- expect(await DataQuery.findOne({ where: { id: dataQuery.id } })).toBeUndefined();
- expect(await DataSource.findOne({ where: { id: dataSource.id } })).toBeUndefined();
- expect(await AppUser.findOne({ where: { appId: application.id } })).toBeUndefined();
+ await expect(App.findOneOrFail({ where: { id: application.id } })).rejects.toThrow(expect.any(Error));
+ await expect(AppVersion.findOneOrFail({ where: { id: version.id } })).rejects.toThrow(expect.any(Error));
+ await expect(DataQuery.findOneOrFail({ where: { id: dataQuery.id } })).rejects.toThrow(expect.any(Error));
+ await expect(DataSource.findOneOrFail({ where: { id: dataSource.id } })).rejects.toThrow(expect.any(Error));
+ await expect(AppUser.findOneOrFail({ where: { appId: application.id } })).rejects.toThrow(expect.any(Error));
});
it('should be possible for app creator to delete an app', async () => {
@@ -598,7 +598,7 @@ describe('apps controller', () => {
.set('Authorization', authHeaderForUser(developer.user));
expect(response.statusCode).toBe(200);
- expect(await App.findOne({ where: { id: application.id } })).toBeUndefined();
+ await expect(App.findOneOrFail({ where: { id: application.id } })).rejects.toThrow(expect.any(Error));
});
it('should not be possible for non admin to delete an app', async () => {
@@ -623,7 +623,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(403);
- expect(await App.findOne({ where: { id: application.id } })).not.toBeUndefined();
+ await expect(App.findOneOrFail({ where: { id: application.id } })).resolves;
});
});
@@ -709,7 +709,7 @@ describe('apps controller', () => {
});
await createApplicationVersion(app, application);
- const allUserGroup = await getRepository(GroupPermission).findOne({
+ const allUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'all_users',
},
@@ -770,7 +770,7 @@ describe('apps controller', () => {
});
const version = await createApplicationVersion(app, application);
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -816,7 +816,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(201);
- const v2 = await getManager().findOne(AppVersion, {
+ const v2 = await getManager().findOneOrFail(AppVersion, {
where: { name: 'v2' },
});
expect(v2.definition).toEqual(v1.definition);
@@ -985,13 +985,13 @@ describe('apps controller', () => {
email: 'admin@tooljet.io',
});
const application = await importAppFromTemplates(app, adminUserData.user, 'customer-dashboard');
- const dataSource = await getManager().findOne(DataSource, {
+ const dataSource = await getManager().findOneOrFail(DataSource, {
where: { appId: application },
});
let dataSources = await getManager().find(DataSource);
let dataQueries = await getManager().find(DataQuery);
- const credential = await getManager().findOne(Credential, {
+ const credential = await getManager().findOneOrFail(Credential, {
where: { id: dataSource.options['password']['credential_id'] },
});
credential.valueCiphertext = 'strongPassword';
@@ -1007,7 +1007,7 @@ describe('apps controller', () => {
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe('More than one version found. Version to create from not specified.');
- const initialVersion = await getManager().findOne(AppVersion, {
+ const initialVersion = await getManager().findOneOrFail(AppVersion, {
where: { appId: application.id, name: 'v0' },
});
@@ -1111,7 +1111,7 @@ describe('apps controller', () => {
const version2 = await createApplicationVersion(app, application);
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -1197,7 +1197,7 @@ describe('apps controller', () => {
});
const version = await createApplicationVersion(app, application);
- const allUserGroup = await getRepository(GroupPermission).findOne({
+ const allUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'all_users',
},
@@ -1257,7 +1257,9 @@ describe('apps controller', () => {
const version = await createApplicationVersion(app, application);
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({ where: { group: 'developer' } });
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
+ where: { group: 'developer' },
+ });
await createAppGroupPermission(app, application, developerUserGroup.id, {
read: false,
update: true,
@@ -1380,7 +1382,7 @@ describe('apps controller', () => {
});
await createApplicationVersion(app, application);
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -1391,7 +1393,7 @@ describe('apps controller', () => {
delete: false,
});
// setup app permissions for viewer
- const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ const viewerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'viewer',
},
@@ -1475,7 +1477,7 @@ describe('apps controller', () => {
slug: 'foo',
});
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -1486,7 +1488,7 @@ describe('apps controller', () => {
delete: false,
});
// setup app permissions for viewer
- const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ const viewerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'viewer',
},
diff --git a/server/test/controllers/comment.e2e-spec.ts b/server/test/controllers/comment.e2e-spec.ts
index b18331b6ff..b096078dd0 100644
--- a/server/test/controllers/comment.e2e-spec.ts
+++ b/server/test/controllers/comment.e2e-spec.ts
@@ -26,7 +26,7 @@ describe('comment controller', () => {
});
it('should list all comments in a thread', async () => {
- const userData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
+ const userData = await createUser(app, { email: 'admin@tooljet.io' });
const { user } = userData;
@@ -42,7 +42,7 @@ describe('comment controller', () => {
x: 100,
y: 200,
userId: userData.user.id,
- organizationId: user.organization.id,
+ organizationId: user.organizationId,
appVersionsId: version.id,
});
diff --git a/server/test/controllers/data_queries.e2e-spec.ts b/server/test/controllers/data_queries.e2e-spec.ts
index d7f123730f..2d2d890064 100644
--- a/server/test/controllers/data_queries.e2e-spec.ts
+++ b/server/test/controllers/data_queries.e2e-spec.ts
@@ -52,7 +52,7 @@ describe('data queries controller', () => {
});
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -64,7 +64,7 @@ describe('data queries controller', () => {
});
// setup app permissions for viewer
- const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ const viewerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'viewer',
},
@@ -142,7 +142,7 @@ describe('data queries controller', () => {
});
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -229,7 +229,7 @@ describe('data queries controller', () => {
groups: ['all_users', 'admin'],
});
- const allUserGroup = await getManager().findOne(GroupPermission, {
+ const allUserGroup = await getManager().findOneOrFail(GroupPermission, {
where: { group: 'all_users', organization: adminUserData.organization },
});
await getManager().update(
@@ -239,7 +239,7 @@ describe('data queries controller', () => {
);
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -337,7 +337,7 @@ describe('data queries controller', () => {
});
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -440,6 +440,69 @@ describe('data queries controller', () => {
expect(response.statusCode).toBe(403);
});
+ it('should be able to get queries sorted created wise', async () => {
+ const adminUserData = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users', 'admin'],
+ });
+
+ const application = await createApplication(app, {
+ name: 'name',
+ user: adminUserData.user,
+ });
+
+ const dataSource = await createDataSource(app, {
+ name: 'name',
+ kind: 'postgres',
+ application: application,
+ user: adminUserData.user,
+ });
+
+ const appVersion = await createApplicationVersion(app, application);
+
+ const options = {
+ method: 'get',
+ url: null,
+ url_params: [['', '']],
+ headers: [['', '']],
+ body: [['', '']],
+ json_body: null,
+ body_toggle: false,
+ };
+
+ const createdQueries = [];
+ const totalQueries = 15;
+
+ for (let i = 1; i <= totalQueries; i++) {
+ const queryParams = {
+ name: `restapi${i}`,
+ app_id: application.id,
+ data_source_id: dataSource.id,
+ kind: 'restapi',
+ options,
+ app_version_id: appVersion.id,
+ };
+
+ const response = await request(app.getHttpServer())
+ .post(`/api/data_queries`)
+ .set('Authorization', authHeaderForUser(adminUserData.user))
+ .send(queryParams);
+
+ createdQueries.push(response.body);
+ }
+
+ // Latest query should be on top
+ createdQueries.reverse();
+
+ const response = await request(app.getHttpServer())
+ .get(`/api/data_queries?app_id=${application.id}&app_version_id=${appVersion.id}`)
+ .set('Authorization', authHeaderForUser(adminUserData.user));
+
+ expect(response.statusCode).toBe(200);
+ expect(response.body.data_queries.length).toBe(totalQueries);
+ expect(createdQueries).toMatchObject(response.body.data_queries);
+ });
+
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',
@@ -473,7 +536,7 @@ describe('data queries controller', () => {
});
// setup app permissions for developer
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -485,7 +548,7 @@ describe('data queries controller', () => {
});
// setup app permissions for viewer
- const viewerUserGroup = await getRepository(GroupPermission).findOne({
+ const viewerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'viewer',
},
diff --git a/server/test/controllers/data_sources.e2e-spec.ts b/server/test/controllers/data_sources.e2e-spec.ts
index 43d4232630..bbcd8d20d9 100644
--- a/server/test/controllers/data_sources.e2e-spec.ts
+++ b/server/test/controllers/data_sources.e2e-spec.ts
@@ -51,7 +51,7 @@ describe('data sources controller', () => {
});
const applicationVersion = await createApplicationVersion(app, application);
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -124,7 +124,7 @@ describe('data sources controller', () => {
name: 'name',
user: adminUserData.user,
});
- const developerUserGroup = await getRepository(GroupPermission).findOne({
+ const developerUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'developer',
},
@@ -212,7 +212,7 @@ describe('data sources controller', () => {
user: adminUserData.user,
});
- const allUserGroup = await getRepository(GroupPermission).findOne({
+ const allUserGroup = await getRepository(GroupPermission).findOneOrFail({
where: {
group: 'all_users',
organizationId: adminUserData.organization.id,
diff --git a/server/test/controllers/folder_apps.e2e-spec.ts b/server/test/controllers/folder_apps.e2e-spec.ts
index 036d47ebbe..eb5c64ddf7 100644
--- a/server/test/controllers/folder_apps.e2e-spec.ts
+++ b/server/test/controllers/folder_apps.e2e-spec.ts
@@ -40,6 +40,29 @@ describe('folder apps controller', () => {
expect(folder_id).toBe(folder.id);
});
+ it('should not add an app to a folder more than once', async () => {
+ const { adminUser, app } = await setupOrganization(nestApp);
+ const manager = getManager();
+
+ // create a new folder
+ const folder = await manager.save(
+ manager.create(Folder, { name: 'folder', organizationId: adminUser.organizationId })
+ );
+
+ await request(nestApp.getHttpServer())
+ .post(`/api/folder_apps`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ folder_id: folder.id, app_id: app.id });
+
+ const response = await request(nestApp.getHttpServer())
+ .post(`/api/folder_apps`)
+ .set('Authorization', authHeaderForUser(adminUser))
+ .send({ folder_id: folder.id, app_id: app.id });
+
+ expect(response.statusCode).toBe(400);
+ expect(response.body.message).toBe('App has been already added to the folder');
+ });
+
it('should remove an app from a folder', async () => {
const { adminUser, app } = await setupOrganization(nestApp);
const manager = getManager();
diff --git a/server/test/controllers/folders.e2e-spec.ts b/server/test/controllers/folders.e2e-spec.ts
index cd1389b580..623ab826e4 100644
--- a/server/test/controllers/folders.e2e-spec.ts
+++ b/server/test/controllers/folders.e2e-spec.ts
@@ -34,7 +34,6 @@ describe('folders controller', () => {
it('should list all folders in an organization', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
- role: 'admin',
});
const { user } = adminUserData;
@@ -66,7 +65,6 @@ describe('folders controller', () => {
const anotherUserData = await createUser(nestApp, {
email: 'admin@organization.com',
- role: 'admin',
});
await getManager().save(Folder, {
name: 'Folder1',
@@ -187,7 +185,6 @@ describe('folders controller', () => {
const anotherUserData = await createUser(nestApp, {
email: 'admin@organization.com',
- role: 'admin',
});
await getManager().save(Folder, {
name: 'another org folder',
@@ -229,7 +226,7 @@ describe('folders controller', () => {
folderCreate: false,
organization: newUserData.organization,
});
- const group = await getManager().findOne(GroupPermission, {
+ const group = await getManager().findOneOrFail(GroupPermission, {
where: { group: 'folder-handler' },
});
await createAppGroupPermission(nestApp, appInFolder, group.id, {
@@ -278,7 +275,6 @@ describe('folders controller', () => {
it('should create new folder in an organization', async () => {
const adminUserData = await createUser(nestApp, {
email: 'admin@tooljet.io',
- role: 'admin',
});
const { user } = adminUserData;
diff --git a/server/test/controllers/group_permissions.e2e-spec.ts b/server/test/controllers/group_permissions.e2e-spec.ts
index e63daa3c46..277b65e93a 100644
--- a/server/test/controllers/group_permissions.e2e-spec.ts
+++ b/server/test/controllers/group_permissions.e2e-spec.ts
@@ -251,22 +251,22 @@ describe('group permissions controller', () => {
});
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 { user, organization } = await createUser(nestApp, {
+ email: 'admin@tooljet.io',
+ });
const manager = getManager();
- const adminGroupPermission = await manager.findOne(GroupPermission, {
+ const adminGroupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
group: 'admin',
- organizationId: adminUser.organizationId,
+ organizationId: organization.id,
},
});
const response = await request(nestApp.getHttpServer())
.put(`/api/group_permissions/${adminGroupPermission.id}`)
- .set('Authorization', authHeaderForUser(adminUser))
- .send({ remove_users: [defaultUser.id] });
+ .set('Authorization', authHeaderForUser(user))
+ .send({ remove_users: [user.id] });
expect(response.statusCode).toBe(400);
expect(response.body.message).toBe('Atleast one active admin is required.');
@@ -278,7 +278,7 @@ describe('group permissions controller', () => {
} = await setupOrganizations(nestApp);
const manager = getManager();
- const adminGroupPermission = await manager.findOne(GroupPermission, {
+ const adminGroupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
group: 'all_users',
organizationId: adminUser.organizationId,
@@ -363,7 +363,7 @@ describe('group permissions controller', () => {
} = await setupOrganizations(nestApp);
const manager = getManager();
- const adminGroupPermission = await manager.findOne(GroupPermission, {
+ const adminGroupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
group: 'admin',
organizationId: organization.id,
@@ -471,7 +471,7 @@ describe('group permissions controller', () => {
} = await setupOrganizations(nestApp);
const manager = getManager();
- const adminGroupPermission = await manager.findOne(GroupPermission, {
+ const adminGroupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
group: 'admin',
organizationId: organization.id,
@@ -485,10 +485,11 @@ describe('group permissions controller', () => {
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.default_organization_id).toBe(organization.id);
expect(user.email).toBe('admin@tooljet.io');
});
});
@@ -506,21 +507,24 @@ describe('group permissions controller', () => {
});
it('should allow admin to list users not in group permission', async () => {
- const {
- organization: { adminUser, organization },
- } = await setupOrganizations(nestApp);
+ const adminUser = await createUser(nestApp, { email: 'admin@tooljet.io' });
+ const userone = await createUser(nestApp, {
+ email: 'userone@tooljet.io',
+ groups: ['all_users'],
+ organization: adminUser.organization,
+ });
const manager = getManager();
- const adminGroupPermission = await manager.findOne(GroupPermission, {
+ const adminGroupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
group: 'admin',
- organizationId: organization.id,
+ organizationId: adminUser.organization.id,
},
});
const groupPermissionId = adminGroupPermission.id;
const response = await request(nestApp.getHttpServer())
.get(`/api/group_permissions/${groupPermissionId}/addable_users`)
- .set('Authorization', authHeaderForUser(adminUser));
+ .set('Authorization', authHeaderForUser(adminUser.user));
expect(response.statusCode).toBe(200);
@@ -528,8 +532,8 @@ describe('group permissions controller', () => {
const user = users[0];
expect(users).toHaveLength(1);
- expect(user.organization_id).toBe(organization.id);
- expect(user.email).toBe('developer@tooljet.io');
+ expect(user.default_organization_id).toBe(userone.organization.id);
+ expect(user.email).toBe('userone@tooljet.io');
});
});
@@ -552,14 +556,14 @@ describe('group permissions controller', () => {
} = await setupOrganizations(nestApp);
const manager = getManager();
- const groupPermission = await manager.findOne(GroupPermission, {
+ const groupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
organizationId: organization.id,
group: 'all_users',
},
});
const groupPermissionId = groupPermission.id;
- const appGroupPermission = await manager.findOne(AppGroupPermission, {
+ const appGroupPermission = await manager.findOneOrFail(AppGroupPermission, {
where: {
groupPermissionId,
},
@@ -589,14 +593,14 @@ describe('group permissions controller', () => {
} = await setupOrganizations(nestApp);
const manager = getManager();
- const groupPermission = await manager.findOne(GroupPermission, {
+ const groupPermission = await manager.findOneOrFail(GroupPermission, {
where: {
organizationId: organization.id,
group: 'all_users',
},
});
const groupPermissionId = groupPermission.id;
- const appGroupPermission = await manager.findOne(AppGroupPermission, {
+ const appGroupPermission = await manager.findOneOrFail(AppGroupPermission, {
where: {
groupPermissionId,
},
@@ -644,7 +648,7 @@ describe('group permissions controller', () => {
const anotherDefaultUserData = await createUser(nestApp, {
email: 'another_developer@tooljet.io',
groups: ['all_users'],
- anotherOrganization,
+ organization: anotherOrganization,
});
const anotherDefaultUser = anotherDefaultUserData.user;
diff --git a/server/test/controllers/oauth.e2e-spec.ts b/server/test/controllers/oauth.e2e-spec.ts
index db5b79ee04..1d043ee4d0 100644
--- a/server/test/controllers/oauth.e2e-spec.ts
+++ b/server/test/controllers/oauth.e2e-spec.ts
@@ -4,18 +4,23 @@ import { clearDB, createUser, createNestAppInstanceWithEnvMock } from '../test.h
import { OAuth2Client } from 'google-auth-library';
import { mocked } from 'ts-jest/utils';
import got from 'got';
+import { Organization } from 'src/entities/organization.entity';
+import { Repository } from 'typeorm';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
jest.mock('got');
const mockedGot = mocked(got);
describe('oauth controller', () => {
let app: INestApplication;
+ let ssoConfigsRepository: Repository;
+ let orgRepository: Repository;
let mockConfig;
beforeEach(async () => {
await clearDB();
jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
- if (key === 'SSO_DISABLE_SIGNUP') {
+ if (key === 'MULTI_ORGANIZATION') {
return 'false';
} else {
return process.env[key];
@@ -25,6 +30,8 @@ describe('oauth controller', () => {
beforeAll(async () => {
({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
+ ssoConfigsRepository = app.get('SSOConfigsRepository');
+ orgRepository = app.get('OrganizationRepository');
});
afterEach(() => {
@@ -32,426 +39,936 @@ describe('oauth controller', () => {
jest.clearAllMocks();
});
- describe('sign in via Google OAuth', () => {
- it('should return login info when the user does not exist', async () => {
- const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
- googleVerifyMock.mockImplementation(() => ({
- getPayload: () => ({
- sub: 'someSSOId',
- email: 'ssoUser@tooljet.io',
- name: 'SSO User',
- hd: 'tooljet.io',
- }),
- }));
-
- // Calling the createUser helper function to have an Organization created. This user is irrelevant for the test
- await createUser(app, { email: 'anotherUser@tooljet.io', role: 'admin' });
-
- const token = 'someStuff';
-
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'google' });
-
- expect(googleVerifyMock).toHaveBeenCalledWith({
- idToken: token,
- audience: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
- });
-
- expect(response.statusCode).toBe(201);
- expect(Object.keys(response.body).sort()).toEqual(
- [
- 'id',
- 'email',
- 'first_name',
- 'last_name',
- 'auth_token',
- 'admin',
- 'group_permissions',
- 'app_group_permissions',
- ].sort()
- );
-
- const { email, first_name, last_name, admin, group_permissions, app_group_permissions } = response.body;
-
- expect(email).toEqual('ssoUser@tooljet.io');
- expect(first_name).toEqual('SSO');
- expect(last_name).toEqual('User');
- expect(admin).toBeFalsy();
- expect(group_permissions).toHaveLength(1);
- expect(group_permissions[0].group).toEqual('all_users');
- expect(Object.keys(group_permissions[0]).sort()).toEqual(
- [
- 'id',
- 'organization_id',
- 'group',
- 'app_create',
- 'app_delete',
- 'updated_at',
- 'created_at',
- 'folder_create',
- ].sort()
- );
- expect(app_group_permissions).toHaveLength(0);
- });
-
- it('should be forbid logging in when the user does not exist and signups are disabled', async () => {
- jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
- if (key === 'SSO_DISABLE_SIGNUP') {
- return 'true';
- } else {
- return process.env[key];
- }
- });
- const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
- googleVerifyMock.mockImplementation(() => ({
- getPayload: () => ({
- sub: 'someSSOId',
- email: 'ssoUser@tooljet.io',
- name: 'SSO User',
- hd: 'tooljet.io',
- }),
- }));
-
- // Calling the createUser helper function to have an Organization created. This user is irrelevant for the test
- await createUser(app, { email: 'anotherUser@tooljet.io', role: 'admin' });
-
- const token = 'someStuff';
-
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'google' });
-
- expect(googleVerifyMock).toHaveBeenCalledWith({
- idToken: token,
- audience: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
- });
-
- expect(response.statusCode).toBe(401);
- });
-
- it('should be forbid logging in when the restricted domin is configured and domain not match', async () => {
- jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
- if (key === 'SSO_RESTRICTED_DOMAIN') {
- return 'tooljet.com,tooljet.in';
- } else {
- return process.env[key];
- }
- });
- const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
- googleVerifyMock.mockImplementation(() => ({
- getPayload: () => ({
- sub: 'someSSOId',
- email: 'ssoUser@tooljet.io',
- name: 'SSO User',
- hd: 'tooljet.io',
- }),
- }));
-
- // Calling the createUser helper function to have an Organization created. This user is irrelevant for the test
- await createUser(app, { email: 'anotherUser@tooljet.io', role: 'admin' });
-
- const token = 'someStuff';
-
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'google' });
-
- expect(googleVerifyMock).toHaveBeenCalledWith({
- idToken: token,
- audience: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
- });
-
- expect(response.statusCode).toBe(401);
- });
-
- it('should be success when the restricted domin is configured and domain matches', async () => {
- jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
- if (key === 'SSO_RESTRICTED_DOMAIN') {
- return 'tooljet.com,tooljet.io';
- } else {
- return process.env[key];
- }
- });
- const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
- googleVerifyMock.mockImplementation(() => ({
- getPayload: () => ({
- sub: 'someSSOId',
- email: 'ssoUser@tooljet.io',
- name: 'SSO User',
- hd: 'tooljet.io',
- }),
- }));
-
- // Calling the createUser helper function to have an Organization created. This user is irrelevant for the test
- await createUser(app, { email: 'anotherUser@tooljet.io', role: 'admin' });
-
- const token = 'someStuff';
-
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'google' });
-
- expect(googleVerifyMock).toHaveBeenCalledWith({
- idToken: token,
- audience: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
- });
-
- expect(response.statusCode).toBe(201);
- });
-
- it('should return login info when the user exists', async () => {
- const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
- googleVerifyMock.mockImplementation(() => ({
- getPayload: () => ({
- sub: 'someSSOId',
- email: 'ssoUser@tooljet.io',
- name: 'New name',
- hd: 'tooljet.io',
- }),
- }));
-
- await createUser(app, {
- email: 'ssoUser@tooljet.io',
- role: 'developer',
- ssoId: 'someSSOId',
- firstName: 'Existing',
- lastName: 'Name',
- groups: ['all_users', 'admin'],
- });
-
- const token = 'someStuff';
-
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'google' });
-
- expect(googleVerifyMock).toHaveBeenCalledWith({
- idToken: token,
- audience: process.env.SSO_GOOGLE_OAUTH2_CLIENT_ID,
- });
-
- expect(response.statusCode).toBe(201);
- expect(new Set(Object.keys(response.body))).toEqual(
- new Set([
- 'id',
- 'email',
- 'first_name',
- 'last_name',
- 'auth_token',
- 'admin',
- 'group_permissions',
- 'app_group_permissions',
- ])
- );
-
- const { email, first_name, last_name, admin, group_permissions, app_group_permissions } = response.body;
-
- expect(email).toEqual('ssoUser@tooljet.io');
- expect(first_name).toEqual('Existing');
- expect(last_name).toEqual('Name');
- expect(admin).toBeTruthy();
- expect(group_permissions).toHaveLength(2);
- expect(group_permissions.map((p) => p.group).sort()).toEqual(['all_users', 'admin'].sort());
- expect(Object.keys(group_permissions[0]).sort()).toEqual(
- [
- 'id',
- 'organization_id',
- 'group',
- 'app_create',
- 'app_delete',
- 'folder_create',
- 'updated_at',
- 'created_at',
- ].sort()
- );
- expect(app_group_permissions).toHaveLength(0);
- });
- });
-
- describe('sign in via Git OAuth', () => {
- it('should return login info when the user does not exist', async () => {
- const gitAuthResponse = jest.fn();
- gitAuthResponse.mockImplementation(() => {
- return {
- json: () => {
- return {
- access_token: 'some-access-token',
- scope: 'scope',
- token_type: 'bearer',
- };
+ describe('SSO Login', () => {
+ let current_organization: Organization;
+ beforeEach(async () => {
+ const { organization } = await createUser(app, {
+ email: 'anotherUser@tooljet.io',
+ ssoConfigs: [
+ { sso: 'google', enabled: true, configs: { clientId: 'client-id' } },
+ {
+ sso: 'git',
+ enabled: true,
+ configs: { clientId: 'client-id' },
},
- };
+ ],
+ enableSignUp: true,
});
- const gitGetUserResponse = jest.fn();
- gitGetUserResponse.mockImplementation(() => {
- return {
- json: () => {
- return {
- name: 'SSO UserGit',
- email: 'ssoUserGit@tooljet.io',
- };
- },
- };
- });
-
- mockedGot.mockImplementationOnce(gitAuthResponse);
- mockedGot.mockImplementationOnce(gitGetUserResponse);
- const token = 'some-token';
-
- // Calling the createUser helper function to have an Organization created. This user is irrelevant for the test
- await createUser(app, { email: 'anotherUser@tooljet.io', role: 'admin' });
-
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'git' });
-
- expect(response.statusCode).toBe(201);
- expect(Object.keys(response.body).sort()).toEqual(
- [
- 'id',
- 'email',
- 'first_name',
- 'last_name',
- 'auth_token',
- 'admin',
- 'group_permissions',
- 'app_group_permissions',
- ].sort()
- );
-
- const { email, first_name, last_name, admin, group_permissions, app_group_permissions } = response.body;
-
- expect(email).toEqual('ssoUserGit@tooljet.io');
- expect(first_name).toEqual('SSO');
- expect(last_name).toEqual('UserGit');
- expect(admin).toBeFalsy();
- expect(group_permissions).toHaveLength(1);
- expect(group_permissions[0].group).toEqual('all_users');
- expect(Object.keys(group_permissions[0]).sort()).toEqual(
- [
- 'id',
- 'organization_id',
- 'group',
- 'app_create',
- 'app_delete',
- 'updated_at',
- 'created_at',
- 'folder_create',
- ].sort()
- );
- expect(app_group_permissions).toHaveLength(0);
+ current_organization = organization;
});
- it('should be forbid logging in when the user does not exist and signups are disabled', async () => {
- jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
- if (key === 'SSO_DISABLE_SIGNUP') {
- return 'true';
- } else {
- return process.env[key];
- }
+ describe('sign in via Google OAuth', () => {
+ let sso_configs;
+ const token = 'some-Token';
+ beforeEach(() => {
+ sso_configs = current_organization.ssoConfigs.find((conf) => conf.sso === 'google');
});
- const gitAuthResponse = jest.fn();
- gitAuthResponse.mockImplementation(() => {
- return {
- json: () => {
- return {
- access_token: 'some-access-token',
- scope: 'scope',
- token_type: 'bearer',
- };
- },
- };
- });
- const gitGetUserResponse = jest.fn();
- gitGetUserResponse.mockImplementation(() => {
- return {
- json: () => {
- return {
- name: 'SSO UserGit',
- email: 'ssoUserGit@tooljet.io',
- };
- },
- };
+ it('should return 401 if google sign in is disabled', async () => {
+ await ssoConfigsRepository.update(sso_configs.id, { enabled: false });
+ await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token })
+ .expect(401);
});
- mockedGot.mockImplementationOnce(gitAuthResponse);
- mockedGot.mockImplementationOnce(gitGetUserResponse);
+ it('should return 401 when the user does not exist and sign up is disabled', async () => {
+ await orgRepository.update(current_organization.id, { enableSignUp: false });
+ const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
+ googleVerifyMock.mockImplementation(() => ({
+ getPayload: () => ({
+ sub: 'someSSOId',
+ email: 'ssoUser@tooljet.io',
+ name: 'SSO User',
+ hd: 'tooljet.io',
+ }),
+ }));
+ await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token })
+ .expect(401);
+ });
- // Calling the createUser helper function to have an Organization created. This user is irrelevant for the test
- await createUser(app, { email: 'anotherUser@tooljet.io', role: 'admin' });
+ it('should return 401 when the user does not exist domain mismatch', async () => {
+ await orgRepository.update(current_organization.id, { domain: 'tooljet.io,tooljet.com' });
+ const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
+ googleVerifyMock.mockImplementation(() => ({
+ getPayload: () => ({
+ sub: 'someSSOId',
+ email: 'ssoUser@tooljett.io',
+ name: 'SSO User',
+ hd: 'tooljet.io',
+ }),
+ }));
+ await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token })
+ .expect(401);
+ });
- const token = 'someStuff';
+ it('should return login info when the user does not exist and domain matches and sign up is enabled', async () => {
+ await orgRepository.update(current_organization.id, { domain: 'tooljet.io,tooljet.com' });
+ const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
+ googleVerifyMock.mockImplementation(() => ({
+ getPayload: () => ({
+ sub: 'someSSOId',
+ email: 'ssoUser@tooljet.io',
+ name: 'SSO User',
+ hd: 'tooljet.io',
+ }),
+ }));
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'git' });
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
- expect(response.statusCode).toBe(401);
+ expect(googleVerifyMock).toHaveBeenCalledWith({
+ idToken: token,
+ audience: sso_configs.configs.clientId,
+ });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('ssoUser@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('User');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+
+ it('should return login info when the user does not exist and sign up is enabled', async () => {
+ const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
+ googleVerifyMock.mockImplementation(() => ({
+ getPayload: () => ({
+ sub: 'someSSOId',
+ email: 'ssoUser@tooljet.io',
+ name: 'SSO User',
+ hd: 'tooljet.io',
+ }),
+ }));
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(googleVerifyMock).toHaveBeenCalledWith({
+ idToken: token,
+ audience: sso_configs.configs.clientId,
+ });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('ssoUser@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('User');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+ it('should return login info when the user does not exist and name not available and sign up is enabled', async () => {
+ const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
+ googleVerifyMock.mockImplementation(() => ({
+ getPayload: () => ({
+ sub: 'someSSOId',
+ email: 'ssoUser@tooljet.io',
+ name: '',
+ hd: 'tooljet.io',
+ }),
+ }));
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(googleVerifyMock).toHaveBeenCalledWith({
+ idToken: token,
+ audience: sso_configs.configs.clientId,
+ });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const { email, first_name, admin, group_permissions, app_group_permissions, organization_id, organization } =
+ response.body;
+
+ expect(email).toEqual('ssoUser@tooljet.io');
+ expect(first_name).toEqual('ssoUser');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+ it('should return login info when the user exist', async () => {
+ await createUser(app, {
+ firstName: 'SSO',
+ lastName: 'userExist',
+ email: 'anotherUser1@tooljet.io',
+ groups: ['all_users'],
+ organization: current_organization,
+ status: 'active',
+ });
+ const googleVerifyMock = jest.spyOn(OAuth2Client.prototype, 'verifyIdToken');
+ googleVerifyMock.mockImplementation(() => ({
+ getPayload: () => ({
+ sub: 'someSSOId',
+ email: 'anotherUser1@tooljet.io',
+ name: 'SSO User',
+ hd: 'tooljet.io',
+ }),
+ }));
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(googleVerifyMock).toHaveBeenCalledWith({
+ idToken: token,
+ audience: sso_configs.configs.clientId,
+ });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('anotherUser1@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('userExist');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
});
-
- it('should return login info when the user exists', async () => {
- const gitAuthResponse = jest.fn();
- gitAuthResponse.mockImplementation(() => {
- return {
- json: () => {
- return {
- access_token: 'some-access-token',
- scope: 'scope',
- token_type: 'bearer',
- };
- },
- };
+ describe('sign in via Git OAuth', () => {
+ let sso_configs;
+ const token = 'some-Token';
+ beforeEach(() => {
+ sso_configs = current_organization.ssoConfigs.find((conf) => conf.sso === 'git');
});
- const gitGetUserResponse = jest.fn();
- gitGetUserResponse.mockImplementation(() => {
- return {
- json: () => {
- return {
- name: 'Existing Name',
- email: 'ssoUserGit@tooljet.io',
- };
- },
- };
+ it('should return 401 if git sign in is disabled', async () => {
+ await ssoConfigsRepository.update(sso_configs.id, { enabled: false });
+ await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token })
+ .expect(401);
});
- mockedGot.mockImplementationOnce(gitAuthResponse);
- mockedGot.mockImplementationOnce(gitGetUserResponse);
+ it('should return 401 when the user does not exist and sign up is disabled', async () => {
+ await orgRepository.update(current_organization.id, { enableSignUp: false });
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: 'SSO UserGit',
+ email: 'ssoUserGit@tooljet.io',
+ };
+ },
+ };
+ });
- await createUser(app, {
- email: 'ssoUserGit@tooljet.io',
- role: 'developer',
- ssoId: 'someSSOId',
- firstName: 'Existing',
- lastName: 'Name',
- groups: ['all_users', 'admin'],
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+ await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token })
+ .expect(401);
});
- const token = 'someStuff';
+ it('should return 401 when the user does not exist domain mismatch', async () => {
+ await orgRepository.update(current_organization.id, { domain: 'tooljet.io,tooljet.com' });
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: 'SSO UserGit',
+ email: 'ssoUserGit@tooljett.io',
+ };
+ },
+ };
+ });
- const response = await request(app.getHttpServer()).post('/api/oauth/sign-in').send({ token, origin: 'git' });
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
- expect(response.statusCode).toBe(201);
- expect(new Set(Object.keys(response.body))).toEqual(
- new Set([
- 'id',
- 'email',
- 'first_name',
- 'last_name',
- 'auth_token',
- 'admin',
- 'group_permissions',
- 'app_group_permissions',
- ])
- );
+ await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token })
+ .expect(401);
+ });
- const { email, first_name, last_name, admin, group_permissions, app_group_permissions } = response.body;
+ it('should return login info when the user does not exist and domain matches and sign up is enabled', async () => {
+ await orgRepository.update(current_organization.id, { domain: 'tooljet.io,tooljet.com' });
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: 'SSO UserGit',
+ email: 'ssoUserGit@tooljet.io',
+ };
+ },
+ };
+ });
- expect(email).toEqual('ssoUserGit@tooljet.io');
- expect(first_name).toEqual('Existing');
- expect(last_name).toEqual('Name');
- expect(admin).toBeTruthy();
- expect(group_permissions).toHaveLength(2);
- expect(group_permissions.map((p) => p.group).sort()).toEqual(['all_users', 'admin'].sort());
- expect(Object.keys(group_permissions[0]).sort()).toEqual(
- [
- 'id',
- 'organization_id',
- 'group',
- 'app_create',
- 'app_delete',
- 'folder_create',
- 'updated_at',
- 'created_at',
- ].sort()
- );
- expect(app_group_permissions).toHaveLength(0);
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('ssoUserGit@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('UserGit');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+
+ it('should return login info when the user does not exist and domain includes spance matches and sign up is enabled', async () => {
+ await orgRepository.update(current_organization.id, {
+ domain: ' tooljet.io , tooljet.com, , , gmail.com',
+ });
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: 'SSO UserGit',
+ email: 'ssoUserGit@tooljet.io',
+ };
+ },
+ };
+ });
+
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('ssoUserGit@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('UserGit');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+
+ it('should return login info when the user does not exist and sign up is enabled', async () => {
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: 'SSO UserGit',
+ email: 'ssoUserGit@tooljet.io',
+ };
+ },
+ };
+ });
+
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('ssoUserGit@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('UserGit');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+ it('should return login info when the user does not exist and name not available and sign up is enabled', async () => {
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: '',
+ email: 'ssoUserGit@tooljet.io',
+ };
+ },
+ };
+ });
+
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const { email, first_name, admin, group_permissions, app_group_permissions, organization_id, organization } =
+ response.body;
+
+ expect(email).toEqual('ssoUserGit@tooljet.io');
+ expect(first_name).toEqual('ssoUserGit');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+ it('should return login info when the user does not exist and email id not available and sign up is enabled', async () => {
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: '',
+ email: '',
+ };
+ },
+ };
+ });
+ const gitGetUserEmailResponse = jest.fn();
+ gitGetUserEmailResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return [
+ {
+ email: 'ssoUserGit@tooljet.io',
+ primary: true,
+ verified: true,
+ },
+ {
+ email: 'ssoUserGit2@tooljet.io',
+ primary: false,
+ verified: true,
+ },
+ ];
+ },
+ };
+ });
+
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+ mockedGot.mockImplementationOnce(gitGetUserEmailResponse);
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const { email, first_name, admin, group_permissions, app_group_permissions, organization_id, organization } =
+ response.body;
+
+ expect(email).toEqual('ssoUserGit@tooljet.io');
+ expect(first_name).toEqual('ssoUserGit');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
+ it('should return login info when the user exist', async () => {
+ await createUser(app, {
+ firstName: 'SSO',
+ lastName: 'userExist',
+ email: 'anotherUser1@tooljet.io',
+ groups: ['all_users'],
+ organization: current_organization,
+ status: 'active',
+ });
+
+ const gitAuthResponse = jest.fn();
+ gitAuthResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ access_token: 'some-access-token',
+ scope: 'scope',
+ token_type: 'bearer',
+ };
+ },
+ };
+ });
+ const gitGetUserResponse = jest.fn();
+ gitGetUserResponse.mockImplementation(() => {
+ return {
+ json: () => {
+ return {
+ name: 'SSO userExist',
+ email: 'anotherUser1@tooljet.io',
+ };
+ },
+ };
+ });
+
+ mockedGot.mockImplementationOnce(gitAuthResponse);
+ mockedGot.mockImplementationOnce(gitGetUserResponse);
+
+ const response = await request(app.getHttpServer())
+ .post('/api/oauth/sign-in/' + sso_configs.id)
+ .send({ token });
+
+ expect(response.statusCode).toBe(201);
+ expect(Object.keys(response.body).sort()).toEqual(
+ [
+ 'id',
+ 'email',
+ 'first_name',
+ 'last_name',
+ 'auth_token',
+ 'admin',
+ 'organization_id',
+ 'organization',
+ 'group_permissions',
+ 'app_group_permissions',
+ ].sort()
+ );
+
+ const {
+ email,
+ first_name,
+ last_name,
+ admin,
+ group_permissions,
+ app_group_permissions,
+ organization_id,
+ organization,
+ } = response.body;
+
+ expect(email).toEqual('anotherUser1@tooljet.io');
+ expect(first_name).toEqual('SSO');
+ expect(last_name).toEqual('userExist');
+ expect(admin).toBeFalsy();
+ expect(organization_id).toBe(current_organization.id);
+ expect(organization).toBe(current_organization.name);
+ expect(group_permissions).toHaveLength(1);
+ expect(group_permissions[0].group).toEqual('all_users');
+ expect(Object.keys(group_permissions[0]).sort()).toEqual(
+ [
+ 'id',
+ 'organization_id',
+ 'group',
+ 'app_create',
+ 'app_delete',
+ 'updated_at',
+ 'created_at',
+ 'folder_create',
+ ].sort()
+ );
+ expect(app_group_permissions).toHaveLength(0);
+ });
});
});
diff --git a/server/test/controllers/organization_users.e2e-spec.ts b/server/test/controllers/organization_users.e2e-spec.ts
index dbf87eeb27..46ade25d99 100644
--- a/server/test/controllers/organization_users.e2e-spec.ts
+++ b/server/test/controllers/organization_users.e2e-spec.ts
@@ -44,19 +44,19 @@ describe('organization users controller', () => {
await request(app.getHttpServer())
.post(`/api/organization_users/`)
.set('Authorization', authHeaderForUser(adminUserData.user))
- .send({ email: 'test@tooljet.io', groups: ['Viewer', 'all_users'] })
+ .send({ email: 'test@tooljet.io' })
.expect(201);
await request(app.getHttpServer())
.post(`/api/organization_users/`)
.set('Authorization', authHeaderForUser(developerUserData.user))
- .send({ email: 'test2@tooljet.io', groups: ['Viewer', 'all_users'] })
+ .send({ email: 'test2@tooljet.io' })
.expect(403);
await request(app.getHttpServer())
.post(`/api/organization_users/`)
.set('Authorization', authHeaderForUser(viewerUserData.user))
- .send({ email: 'test3@tooljet.io', groups: ['Viewer', 'all_users'] })
+ .send({ email: 'test3@tooljet.io' })
.expect(403);
});
@@ -103,7 +103,6 @@ describe('organization users controller', () => {
const adminUserData = await createUser(app, {
email: 'admin@tooljet.io',
groups: ['admin', 'all_users'],
- status: 'active',
});
const organization = adminUserData.organization;
const developerUserData = await createUser(app, {
@@ -115,6 +114,7 @@ describe('organization users controller', () => {
email: 'viewer@tooljet.io',
groups: ['viewer', 'all_users'],
organization,
+ status: 'invited',
});
await request(app.getHttpServer())
@@ -156,8 +156,6 @@ describe('organization users controller', () => {
const viewerUserData = await createUser(app, {
email: 'viewer@tooljet.io',
status: 'archived',
- invitationToken: 'old-token',
- password: 'old-password',
groups: ['viewer', 'all_users'],
organization,
});
@@ -186,7 +184,7 @@ describe('organization users controller', () => {
await viewerUserData.orgUser.reload();
await viewerUserData.user.reload();
expect(viewerUserData.orgUser.status).toBe('invited');
- expect(viewerUserData.user.invitationToken).not.toBe('old-token');
+ expect(viewerUserData.user.invitationToken).not.toBe('');
expect(viewerUserData.user.password).not.toBe('old-password');
});
diff --git a/server/test/controllers/organizations.e2e-spec.ts b/server/test/controllers/organizations.e2e-spec.ts
index cfd41a7e23..98e9f842cd 100644
--- a/server/test/controllers/organizations.e2e-spec.ts
+++ b/server/test/controllers/organizations.e2e-spec.ts
@@ -1,44 +1,345 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
-import { authHeaderForUser, clearDB, createUser, createNestAppInstance } from '../test.helper';
+import { authHeaderForUser, clearDB, createUser, createNestAppInstanceWithEnvMock } from '../test.helper';
+import { Repository } from 'typeorm';
+import { SSOConfigs } from 'src/entities/sso_config.entity';
+import { User } from 'src/entities/user.entity';
describe('organizations controller', () => {
let app: INestApplication;
+ let ssoConfigsRepository: Repository;
+ let userRepository: Repository;
+ let mockConfig;
beforeEach(async () => {
await clearDB();
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'false';
+ default:
+ return process.env[key];
+ }
+ });
});
beforeAll(async () => {
- app = await createNestAppInstance();
+ ({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
+ ssoConfigsRepository = app.get('SSOConfigsRepository');
+ userRepository = app.get('UserRepository');
});
- it('should allow only authenticated users to list org users', async () => {
- await request(app.getHttpServer()).get('/api/organizations/users').expect(401);
+ afterEach(() => {
+ jest.resetAllMocks();
+ jest.clearAllMocks();
});
- it('should list organization users', async () => {
- const userData = await createUser(app, { email: 'admin@tooljet.io', role: 'admin' });
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { organization, user, orgUser } = userData;
+ describe('list organization users', () => {
+ it('should allow only authenticated users to list org users', async () => {
+ await request(app.getHttpServer()).get('/api/organizations/users').expect(401);
+ });
- const response = await request(app.getHttpServer())
- .get('/api/organizations/users')
- .set('Authorization', authHeaderForUser(user));
+ it('should list organization users', async () => {
+ const userData = await createUser(app, { email: 'admin@tooljet.io' });
+ const { user, orgUser } = userData;
- expect(response.statusCode).toBe(200);
- expect(response.body.users.length).toBe(1);
+ const response = await request(app.getHttpServer())
+ .get('/api/organizations/users')
+ .set('Authorization', authHeaderForUser(user));
- await orgUser.reload();
+ expect(response.statusCode).toBe(200);
+ expect(response.body.users.length).toBe(1);
- expect(response.body.users[0]).toStrictEqual({
- email: user.email,
- first_name: user.firstName,
- id: orgUser.id,
- last_name: user.lastName,
- name: `${user.firstName} ${user.lastName}`,
- role: orgUser.role,
- status: orgUser.status,
+ await orgUser.reload();
+
+ expect(response.body.users[0]).toStrictEqual({
+ email: user.email,
+ first_name: user.firstName,
+ id: orgUser.id,
+ last_name: user.lastName,
+ name: `${user.firstName} ${user.lastName}`,
+ role: orgUser.role,
+ status: orgUser.status,
+ });
+ });
+
+ describe('create organization', () => {
+ it('should allow only authenticated users to create organization', async () => {
+ await request(app.getHttpServer()).post('/api/organizations').send({ name: 'My organization' }).expect(401);
+ });
+ it('should create new organization if multi organization supported', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ const { user, organization } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .post('/api/organizations')
+ .send({ name: 'My organization' })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(201);
+ expect(response.body.organization_id).not.toBe(organization.id);
+ expect(response.body.organization).toBe('My organization');
+ expect(response.body.admin).toBeTruthy();
+
+ const newUser = await userRepository.findOneOrFail({ where: { id: user.id } });
+ expect(newUser.defaultOrganizationId).toBe(response.body.organization_id);
+ });
+
+ it('should throw error if name is empty', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ const { user } = await createUser(app, { email: 'admin@tooljet.io' });
+ const response = await request(app.getHttpServer())
+ .post('/api/organizations')
+ .send({ name: '' })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(400);
+ });
+
+ it('should not create new organization if multi organization not supported', async () => {
+ const { user } = await createUser(app, { email: 'admin@tooljet.io' });
+ await request(app.getHttpServer())
+ .post('/api/organizations')
+ .send({ name: 'My organization' })
+ .set('Authorization', authHeaderForUser(user))
+ .expect(403);
+ });
+
+ it('should create new organization if multi organization supported and user logged in via SSO', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ const { user, organization } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .post('/api/organizations')
+ .send({ name: 'My organization' })
+ .set('Authorization', authHeaderForUser(user, null, false));
+
+ expect(response.statusCode).toBe(201);
+ expect(response.body.organization_id).not.toBe(organization.id);
+ expect(response.body.organization).toBe('My organization');
+ expect(response.body.admin).toBeTruthy();
+ });
+ });
+ describe('update organization', () => {
+ it('should change organization params if changes are done by admin', async () => {
+ const { user, organization } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations')
+ .send({ name: 'new name', domain: 'tooljet.io', enableSignUp: true })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(200);
+ await organization.reload();
+ expect(organization.name).toBe('new name');
+ expect(organization.domain).toBe('tooljet.io');
+ expect(organization.enableSignUp).toBeTruthy();
+ });
+
+ it('should not change organization params if changes are not done by admin', async () => {
+ const { organization } = await createUser(app, { email: 'admin@tooljet.io' });
+ const developerUserData = await createUser(app, {
+ email: 'developer@tooljet.io',
+ groups: ['all_users'],
+ organization,
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations')
+ .send({ name: 'new name', domain: 'tooljet.io', enableSignUp: true })
+ .set('Authorization', authHeaderForUser(developerUserData.user));
+
+ expect(response.statusCode).toBe(403);
+ });
+ });
+ describe('update organization configs', () => {
+ it('should change organization configs if changes are done by admin', async () => {
+ const { user } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations/configs')
+ .send({ type: 'git', configs: { clientId: 'client-id', clientSecret: 'client-secret' }, enabled: true })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(200);
+ const ssoConfigs = await ssoConfigsRepository.findOneOrFail({ where: { id: response.body.id } });
+ expect(ssoConfigs.sso).toBe('git');
+ expect(ssoConfigs.enabled).toBeTruthy();
+ expect(ssoConfigs.configs.clientId).toBe('client-id');
+ expect(ssoConfigs.configs['clientSecret']).not.toBe('client-secret');
+ });
+
+ it('should not change organization configs if changes are not done by admin', async () => {
+ const { user } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users'],
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations/configs')
+ .send({ type: 'git', configs: { clientId: 'client-id', clientSecret: 'client-secret' }, enabled: true })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(403);
+ });
+ });
+ describe('get organization configs', () => {
+ it('should get organization details if requested by admin', async () => {
+ const { user, organization } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations/configs')
+ .send({ type: 'git', configs: { clientId: 'client-id', clientSecret: 'client-secret' }, enabled: true })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(200);
+
+ const getResponse = await request(app.getHttpServer())
+ .get('/api/organizations/configs')
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(getResponse.statusCode).toBe(200);
+
+ expect(getResponse.body.organization_details.id).toBe(organization.id);
+ expect(getResponse.body.organization_details.name).toBe(organization.name);
+ expect(getResponse.body.organization_details.sso_configs.length).toBe(2);
+ expect(getResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'form').organization_id).toBe(
+ organization.id
+ );
+ expect(getResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'git').enabled).toBeTruthy();
+ expect(getResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'git').configs).toEqual({
+ client_id: 'client-id',
+ client_secret: 'client-secret',
+ });
+ });
+
+ it('should not get organization configs if request not done by admin', async () => {
+ const { user } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ groups: ['all_users'],
+ });
+ const response = await request(app.getHttpServer())
+ .get('/api/organizations/configs')
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(403);
+ });
+ });
+
+ describe('get public organization configs', () => {
+ it('should get organization details for all users for single organization', async () => {
+ const { user } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations/configs')
+ .send({ type: 'git', configs: { clientId: 'client-id', clientSecret: 'client-secret' }, enabled: true })
+ .set('Authorization', authHeaderForUser(user));
+
+ const authGetResponse = await request(app.getHttpServer())
+ .get('/api/organizations/configs')
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(authGetResponse.statusCode).toBe(200);
+
+ expect(response.statusCode).toBe(200);
+
+ const getResponse = await request(app.getHttpServer()).get('/api/organizations/public-configs');
+
+ expect(getResponse.statusCode).toBe(200);
+ expect(getResponse.body).toEqual({
+ sso_configs: {
+ name: 'Test Organization',
+ form: {
+ config_id: authGetResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'form').id,
+ sso: 'form',
+ configs: {},
+ enabled: true,
+ },
+ git: {
+ config_id: authGetResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'git').id,
+ sso: 'git',
+ configs: { client_id: 'client-id', client_secret: '' },
+ enabled: true,
+ },
+ },
+ });
+ });
+
+ it('should get organization specific details for all users for multiple organization deployment', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ const { user, organization } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const response = await request(app.getHttpServer())
+ .patch('/api/organizations/configs')
+ .send({ type: 'git', configs: { clientId: 'client-id', clientSecret: 'client-secret' }, enabled: true })
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(response.statusCode).toBe(200);
+
+ const getResponse = await request(app.getHttpServer()).get(
+ `/api/organizations/${organization.id}/public-configs`
+ );
+
+ expect(getResponse.statusCode).toBe(200);
+
+ const authGetResponse = await request(app.getHttpServer())
+ .get('/api/organizations/configs')
+ .set('Authorization', authHeaderForUser(user));
+
+ expect(authGetResponse.statusCode).toBe(200);
+
+ expect(getResponse.statusCode).toBe(200);
+ expect(getResponse.body).toEqual({
+ sso_configs: {
+ name: 'Test Organization',
+ form: {
+ config_id: authGetResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'form').id,
+ sso: 'form',
+ configs: {},
+ enabled: true,
+ },
+ git: {
+ config_id: authGetResponse.body.organization_details.sso_configs.find((ob) => ob.sso === 'git').id,
+ sso: 'git',
+ configs: { client_id: 'client-id', client_secret: '' },
+ enabled: true,
+ },
+ },
+ });
+ });
});
});
diff --git a/server/test/controllers/thread.e2e-spec.ts b/server/test/controllers/thread.e2e-spec.ts
index 9c82b99b85..f87b3214bd 100644
--- a/server/test/controllers/thread.e2e-spec.ts
+++ b/server/test/controllers/thread.e2e-spec.ts
@@ -28,7 +28,6 @@ describe('thread controller', () => {
it('should list all threads in an application', async () => {
const userData = await createUser(app, {
email: 'admin@tooljet.io',
- role: 'admin',
});
const application = await createApplication(app, {
name: 'App',
@@ -41,7 +40,7 @@ describe('thread controller', () => {
x: 100,
y: 200,
userId: userData.user.id,
- organizationId: user.organization.id,
+ organizationId: user.organizationId,
appVersionsId: version.id,
});
diff --git a/server/test/controllers/users.e2e-spec.ts b/server/test/controllers/users.e2e-spec.ts
index bc2d2778e6..975ad27312 100644
--- a/server/test/controllers/users.e2e-spec.ts
+++ b/server/test/controllers/users.e2e-spec.ts
@@ -1,26 +1,36 @@
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
-import { authHeaderForUser, clearDB, createUser, createNestAppInstance } from '../test.helper';
+import { authHeaderForUser, clearDB, createUser, createNestAppInstanceWithEnvMock } from '../test.helper';
import { getManager } from 'typeorm';
import { User } from 'src/entities/user.entity';
+import { v4 as uuidv4 } from 'uuid';
+import { OrganizationUser } from 'src/entities/organization_user.entity';
describe('users controller', () => {
let app: INestApplication;
+ let mockConfig;
beforeEach(async () => {
await clearDB();
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'DISABLE_SIGNUPS':
+ return 'false';
+ case 'MULTI_ORGANIZATION':
+ return 'false';
+ default:
+ return process.env[key];
+ }
+ });
});
beforeAll(async () => {
- app = await createNestAppInstance();
+ ({ app, mockConfig } = await createNestAppInstanceWithEnvMock());
});
describe('PATCH /api/users/change_password', () => {
it('should allow users to update their password', async () => {
- const userData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
+ const userData = await createUser(app, { email: 'admin@tooljet.io' });
const { user } = userData;
const oldPassword = user.password;
@@ -31,18 +41,12 @@ describe('users controller', () => {
.send({ currentPassword: 'password', newPassword: 'new password' });
expect(response.statusCode).toBe(200);
-
- const updatedUser = await getManager().findOne(User, {
- where: { email: user.email },
- });
+ const updatedUser = await getManager().findOneOrFail(User, { where: { email: user.email } });
expect(updatedUser.password).not.toEqual(oldPassword);
});
it('should not allow users to update their password if entered current password is wrong', async () => {
- const userData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
+ const userData = await createUser(app, { email: 'admin@tooljet.io' });
const { user } = userData;
const oldPassword = user.password;
@@ -57,19 +61,14 @@ describe('users controller', () => {
expect(response.statusCode).toBe(403);
- const updatedUser = await getManager().findOne(User, {
- where: { email: user.email },
- });
+ const updatedUser = await getManager().findOneOrFail(User, { where: { email: user.email } });
expect(updatedUser.password).toEqual(oldPassword);
});
});
describe('PATCH /api/users/update', () => {
it('should allow users to update their firstName, lastName and password', async () => {
- const userData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
- });
+ const userData = await createUser(app, { email: 'admin@tooljet.io' });
const { user } = userData;
const [firstName, lastName] = ['Daenerys', 'Targaryen'];
@@ -81,57 +80,67 @@ describe('users controller', () => {
expect(response.statusCode).toBe(200);
- const updatedUser = await getManager().findOne(User, {
- where: { email: user.email },
- });
+ const updatedUser = await getManager().findOneOrFail(User, { where: { email: user.email } });
expect(updatedUser.firstName).toEqual(firstName);
expect(updatedUser.lastName).toEqual(lastName);
});
});
describe('POST /api/users/set_password_from_token', () => {
- it('should allow users to set password from token', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
+ it('should allow users to setup account after sign up using multi organization', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'DISABLE_SIGNUPS':
+ return 'false';
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
});
- const organization = adminUserData.organization;
- const anotherUserData = await createUser(app, {
- email: 'developer@tooljet.io',
- groups: ['all_users'],
- invitationToken: 'token',
- organization,
+ const invitationToken = uuidv4();
+ const userData = await createUser(app, {
+ email: 'signup@tooljet.io',
+ invitationToken,
+ status: 'invited',
});
+ const { user, organization } = userData;
const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
- first_name: 'Khal',
- last_name: 'Drogo',
- token: 'token',
- organization: 'Dothraki Pvt Limited',
- password: 'Khaleesi',
- new_signup: true,
+ first_name: 'signupuser',
+ last_name: 'user',
+ organization: 'org1',
+ password: uuidv4(),
+ token: invitationToken,
+ role: 'developer',
});
expect(response.statusCode).toBe(201);
- const updatedUser = await getManager().findOne(User, {
- where: { email: anotherUserData.user.email },
- });
- expect(updatedUser.firstName).toEqual('Khal');
- expect(updatedUser.lastName).toEqual('Drogo');
+ const updatedUser = await getManager().findOneOrFail(User, { where: { email: user.email } });
+ expect(updatedUser.firstName).toEqual('signupuser');
+ expect(updatedUser.lastName).toEqual('user');
+ expect(updatedUser.defaultOrganizationId).toEqual(organization.id);
+ const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
+ expect(organizationUser.status).toEqual('active');
});
- it('should return error if required params are not present', async () => {
- const adminUserData = await createUser(app, {
- email: 'admin@tooljet.io',
- role: 'admin',
+ it('should return error if required params are not present - multi organization', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'DISABLE_SIGNUPS':
+ return 'false';
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
});
- const organization = adminUserData.organization;
+ const invitationToken = uuidv4();
await createUser(app, {
- email: 'developer@tooljet.io',
- groups: ['all_users'],
- invitationToken: 'token',
- organization,
+ email: 'signup@tooljet.io',
+ invitationToken,
+ status: 'invited',
});
const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token');
@@ -144,6 +153,225 @@ describe('users controller', () => {
'token must be a string',
]);
});
+
+ it('should not allow users to setup account for single organization', async () => {
+ const invitationToken = uuidv4();
+ await createUser(app, {
+ email: 'signup@tooljet.io',
+ invitationToken,
+ status: 'invited',
+ });
+
+ const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
+ first_name: 'signupuser',
+ last_name: 'user',
+ organization: 'org1',
+ password: uuidv4(),
+ token: invitationToken,
+ role: 'developer',
+ });
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should not allow users to setup account for multi organization and sign up disabled', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'DISABLE_SIGNUPS':
+ return 'true';
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ const invitationToken = uuidv4();
+ await createUser(app, {
+ email: 'signup@tooljet.io',
+ invitationToken,
+ status: 'invited',
+ });
+
+ const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
+ first_name: 'signupuser',
+ last_name: 'user',
+ organization: 'org1',
+ password: uuidv4(),
+ token: invitationToken,
+ role: 'developer',
+ });
+
+ expect(response.statusCode).toBe(403);
+ });
+
+ it('should allow users to setup account if already invited to an organization but not activated', async () => {
+ const org = (
+ await createUser(app, {
+ email: 'admin@tooljet.io',
+ })
+ ).organization;
+ const invitedUser = await createUser(app, {
+ email: 'invited@tooljet.io',
+ status: 'invited',
+ organization: org,
+ });
+
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+
+ const signUpResponse = await request(app.getHttpServer())
+ .post('/api/signup')
+ .send({ email: 'invited@tooljet.io' });
+
+ expect(signUpResponse.statusCode).toBe(201);
+
+ const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });
+
+ expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);
+
+ const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
+ first_name: 'signupuser',
+ last_name: 'user',
+ organization: 'org1',
+ password: uuidv4(),
+ token: invitedUserDetails.invitationToken,
+ role: 'developer',
+ });
+
+ expect(response.statusCode).toBe(201);
+ const updatedUser = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });
+ expect(updatedUser.firstName).toEqual('signupuser');
+ expect(updatedUser.lastName).toEqual('user');
+ expect(updatedUser.defaultOrganizationId).not.toBe(org.id);
+ const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
+ where: { userId: invitedUser.user.id, organizationId: org.id },
+ });
+ const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
+ where: { userId: invitedUser.user.id, organizationId: invitedUserDetails.defaultOrganizationId },
+ });
+ expect(organizationUser.status).toEqual('invited');
+ expect(defaultOrganizationUser.status).toEqual('active');
+ });
+
+ it('should not allow users to setup account if already invited to an organization and activated account through invite link after sign up', async () => {
+ const { organization: org } = await createUser(app, {
+ email: 'admin@tooljet.io',
+ });
+ const invitedUser = await createUser(app, {
+ email: 'invited@tooljet.io',
+ status: 'invited',
+ organization: org,
+ });
+
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+
+ const signUpResponse = await request(app.getHttpServer())
+ .post('/api/signup')
+ .send({ email: 'invited@tooljet.io' });
+
+ expect(signUpResponse.statusCode).toBe(201);
+
+ const invitedUserDetails = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });
+
+ expect(invitedUserDetails.defaultOrganizationId).not.toBe(org.id);
+
+ const acceptInviteResponse = await request(app.getHttpServer()).post('/api/users/accept-invite').send({
+ token: invitedUser.orgUser.invitationToken,
+ password: 'new-password',
+ });
+
+ expect(acceptInviteResponse.statusCode).toBe(201);
+
+ const organizationUser = await getManager().findOneOrFail(OrganizationUser, {
+ where: { userId: invitedUser.user.id, organizationId: org.id },
+ });
+ const defaultOrganizationUser = await getManager().findOneOrFail(OrganizationUser, {
+ where: { userId: invitedUser.user.id, organizationId: invitedUserDetails.defaultOrganizationId },
+ });
+ expect(organizationUser.status).toEqual('active');
+ expect(defaultOrganizationUser.status).toEqual('active');
+
+ const updatedUser = await getManager().findOneOrFail(User, { where: { email: invitedUser.user.email } });
+ expect(updatedUser.defaultOrganizationId).toBe(defaultOrganizationUser.organizationId);
+
+ const response = await request(app.getHttpServer()).post('/api/users/set_password_from_token').send({
+ first_name: 'signupuser',
+ last_name: 'user',
+ organization: 'org1',
+ password: uuidv4(),
+ token: invitedUserDetails.invitationToken,
+ role: 'developer',
+ });
+
+ expect(response.statusCode).toBe(400);
+ });
+ });
+
+ describe('POST /api/users/accept-invite', () => {
+ it('should allow users to accept invitation when multi organization is enabled', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'true';
+ default:
+ return process.env[key];
+ }
+ });
+ const userData = await createUser(app, {
+ email: 'organizationUser@tooljet.io',
+ status: 'invited',
+ });
+ const { user, orgUser } = userData;
+
+ const response = await request(app.getHttpServer()).post('/api/users/accept-invite').send({
+ token: orgUser.invitationToken,
+ password: uuidv4(),
+ });
+
+ expect(response.statusCode).toBe(201);
+
+ const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
+ expect(organizationUser.status).toEqual('active');
+ });
+
+ it('should allow users to accept invitation when multi organization is disabled', async () => {
+ jest.spyOn(mockConfig, 'get').mockImplementation((key: string) => {
+ switch (key) {
+ case 'MULTI_ORGANIZATION':
+ return 'false';
+ default:
+ return process.env[key];
+ }
+ });
+ const userData = await createUser(app, {
+ email: 'organizationUser@tooljet.io',
+ status: 'invited',
+ });
+ const { user, orgUser } = userData;
+
+ const response = await request(app.getHttpServer()).post('/api/users/accept-invite').send({
+ token: orgUser.invitationToken,
+ password: uuidv4(),
+ });
+
+ expect(response.statusCode).toBe(201);
+
+ const organizationUser = await getManager().findOneOrFail(OrganizationUser, { where: { userId: user.id } });
+ expect(organizationUser.status).toEqual('active');
+ });
});
afterAll(async () => {
diff --git a/server/test/services/app_import_export.service.spec.ts b/server/test/services/app_import_export.service.spec.ts
index 474aaaf9c4..53c6fa95de 100644
--- a/server/test/services/app_import_export.service.spec.ts
+++ b/server/test/services/app_import_export.service.spec.ts
@@ -75,7 +75,7 @@ describe('AppImportExportService', () => {
kind: 'test_kind',
});
- const exportedApp = await getManager().findOne(App, {
+ const exportedApp = await getManager().findOneOrFail(App, {
where: { id: application.id },
relations: ['dataQueries', 'dataSources', 'appVersions'],
});
@@ -130,7 +130,7 @@ describe('AppImportExportService', () => {
const exportedApp = await service.export(adminUser, app.id);
const result = await service.import(adminUser, exportedApp);
- const importedApp = await getManager().findOne(App, {
+ const importedApp = await getManager().findOneOrFail(App, {
where: { id: result.id },
relations: ['dataQueries', 'dataSources', 'appVersions'],
});
@@ -182,7 +182,7 @@ describe('AppImportExportService', () => {
const exportedApp = await service.export(adminUser, application.id);
const result = await service.import(adminUser, exportedApp);
- const importedApp = await getManager().findOne(App, {
+ const importedApp = await getManager().findOneOrFail(App, {
where: { id: result.id },
relations: ['dataQueries', 'dataSources', 'appVersions'],
});
diff --git a/server/src/services/encryption.service.spec.ts b/server/test/services/encryption.service.spec.ts
similarity index 95%
rename from server/src/services/encryption.service.spec.ts
rename to server/test/services/encryption.service.spec.ts
index 30f3d947dc..268e341e89 100644
--- a/server/src/services/encryption.service.spec.ts
+++ b/server/test/services/encryption.service.spec.ts
@@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
-import { EncryptionService } from './encryption.service';
+import { EncryptionService } from '../../src/services/encryption.service';
describe('EncryptionService', () => {
let service: EncryptionService;
diff --git a/server/test/services/folder_apps.service.spec.ts b/server/test/services/folder_apps.service.spec.ts
index 8f54f4375e..c2bd9eafe8 100644
--- a/server/test/services/folder_apps.service.spec.ts
+++ b/server/test/services/folder_apps.service.spec.ts
@@ -30,7 +30,7 @@ describe('FolderAppsService', () => {
// add app to folder
await service.create(folder.id, app.id);
- const newFolder = await manager.findOne(FolderApp, { where: { folderId: folder.id, appId: app.id } });
+ const newFolder = await manager.findOneOrFail(FolderApp, { where: { folderId: folder.id, appId: app.id } });
expect(newFolder.folderId).toBe(folder.id);
expect(newFolder.appId).toBe(app.id);
});
@@ -50,8 +50,9 @@ describe('FolderAppsService', () => {
await service.remove(folder.id, app.id);
await foldersService.create(adminUser, 'folder');
- const result = await manager.findOne(FolderApp, { where: { folderId: folder.id, appId: app.id } });
- expect(result).toBeUndefined();
+ await expect(manager.findOneOrFail(FolderApp, { where: { folderId: folder.id, appId: app.id } })).rejects.toThrow(
+ expect.any(Error)
+ );
});
});
diff --git a/server/test/services/users.service.spec.ts b/server/test/services/users.service.spec.ts
index b05d859c84..b8dff710b0 100644
--- a/server/test/services/users.service.spec.ts
+++ b/server/test/services/users.service.spec.ts
@@ -36,21 +36,21 @@ describe('UsersService', () => {
firstName: 'John',
lastName: 'Wick',
},
- adminUser.organization,
+ adminUser.defaultOrganizationId,
['all_users']
);
const manager = getManager();
- const newUser = await manager.findOne(User, { where: { email: 'john@example.com' } });
+ const newUser = await manager.findOneOrFail(User, { where: { email: 'john@example.com' } });
expect(newUser.firstName).toEqual('John');
expect(newUser.lastName).toEqual('Wick');
- expect(newUser.organizationId).toBe(adminUser.organizationId);
+ expect(newUser.defaultOrganizationId).toBe(adminUser.defaultOrganizationId);
// 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, {
+ const groupPermission = await manager.findOneOrFail(GroupPermission, {
where: { id: userGroups[0].groupPermissionId },
});
expect(groupPermission.group).toEqual('all_users');
@@ -78,7 +78,7 @@ describe('UsersService', () => {
it('should add user groups', async () => {
const { defaultUser } = await setupOrganization(nestApp);
- await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+ await createGroupPermission(nestApp, { organizationId: defaultUser.defaultOrganizationId, group: 'new-group' });
await service.update(defaultUser.id, { addGroups: ['new-group'] });
await defaultUser.reload();
@@ -90,7 +90,7 @@ describe('UsersService', () => {
it('should not add duplicate user groups', async () => {
const { defaultUser } = await setupOrganization(nestApp);
- await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+ await createGroupPermission(nestApp, { organizationId: defaultUser.defaultOrganizationId, group: 'new-group' });
await service.update(defaultUser.id, { addGroups: ['new-group'] });
await defaultUser.reload();
@@ -104,7 +104,7 @@ describe('UsersService', () => {
it('should remove user groups', async () => {
const { defaultUser } = await setupOrganization(nestApp);
- await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'new-group' });
+ await createGroupPermission(nestApp, { organizationId: defaultUser.defaultOrganizationId, group: 'new-group' });
await service.update(defaultUser.id, { addGroups: ['new-group'] });
await defaultUser.reload();
@@ -118,7 +118,7 @@ describe('UsersService', () => {
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 createGroupPermission(nestApp, { organizationId: defaultUser.defaultOrganizationId, group: 'new-group' });
await service.update(defaultUser.id, { addGroups: ['new-group'] });
await defaultUser.reload();
@@ -147,7 +147,7 @@ describe('UsersService', () => {
await service.update(adminUser.id, { addGroups: ['group1'] });
await adminUser.reload();
- await createGroupPermission(nestApp, { organizationId: defaultUser.organizationId, group: 'group2' });
+ await createGroupPermission(nestApp, { organizationId: defaultUser.defaultOrganizationId, group: 'group2' });
await service.update(defaultUser.id, { addGroups: ['group2'] });
await defaultUser.reload();
@@ -185,7 +185,7 @@ describe('UsersService', () => {
await getManager().find(GroupPermission, {
where: {
group: 'all_users',
- organizationId: defaultUser.organizationId,
+ organizationId: defaultUser.defaultOrganizationId,
},
})
).map((gp) => gp.id);
@@ -197,7 +197,7 @@ describe('UsersService', () => {
describe('.groupPermissionsForOrganization', () => {
it('should return all group permissions within organization', async () => {
const { defaultUser } = await setupOrganization(nestApp);
- const groupPermissions = (await service.groupPermissionsForOrganization(defaultUser.organizationId)).map(
+ const groupPermissions = (await service.groupPermissionsForOrganization(defaultUser.defaultOrganizationId)).map(
(x) => x.group
);
diff --git a/server/test/test.helper.ts b/server/test/test.helper.ts
index 86d8c96d8e..d428755cf7 100644
--- a/server/test/test.helper.ts
+++ b/server/test/test.helper.ts
@@ -24,6 +24,7 @@ import { WsAdapter } from '@nestjs/platform-ws';
import { AppsModule } from 'src/modules/apps/apps.module';
import { LibraryAppCreationService } from '@services/library_app_creation.service';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
+import { v4 as uuidv4 } from 'uuid';
export async function createNestAppInstance(): Promise {
let app: INestApplication;
@@ -69,12 +70,17 @@ export async function createNestAppInstanceWithEnvMock(): Promise<{
return { app, mockConfig: moduleRef.get(ConfigService) };
}
-export function authHeaderForUser(user: any): string {
+export function authHeaderForUser(user: User, organizationId?: string, isPasswordLogin = true): string {
const configService = new ConfigService();
const jwtService = new JwtService({
secret: configService.get('SECRET_KEY_BASE'),
});
- const authPayload = { username: user.id, sub: user.email };
+ const authPayload = {
+ username: user.id,
+ sub: user.email,
+ organizationId: organizationId || user.defaultOrganizationId,
+ isPasswordLogin,
+ };
const authToken = jwtService.sign(authPayload);
return `Bearer ${authToken}`;
}
@@ -99,7 +105,7 @@ export async function createApplication(nestApp, { name, user, isPublic, slug }:
user,
slug,
isPublic: isPublic || false,
- organizationId: user.organization.id,
+ organizationId: user.organizationId,
createdAt: new Date(),
updatedAt: new Date(),
})
@@ -132,7 +138,32 @@ export async function createApplicationVersion(nestApp, application, { name = 'v
export async function createUser(
nestApp,
- { firstName, lastName, email, groups, organization, ssoId, status, invitationToken }: any
+ {
+ firstName,
+ lastName,
+ email,
+ groups,
+ organization,
+ status,
+ invitationToken,
+ formLoginStatus = true,
+ organizationName = 'Test Organization',
+ ssoConfigs = [],
+ enableSignUp = false,
+ }: {
+ firstName?: string;
+ lastName?: string;
+ email?: string;
+ groups?: Array;
+ organization?: Organization;
+ status?: string;
+ invitationToken?: string;
+ formLoginStatus?: boolean;
+ organizationName?: string;
+ ssoConfigs?: Array;
+ enableSignUp?: boolean;
+ },
+ existingUser?: User
) {
let userRepository: Repository;
let organizationRepository: Repository;
@@ -146,31 +177,46 @@ export async function createUser(
organization ||
(await organizationRepository.save(
organizationRepository.create({
- name: 'Test Organization',
+ name: organizationName,
+ enableSignUp,
createdAt: new Date(),
updatedAt: new Date(),
+ ssoConfigs: [
+ {
+ sso: 'form',
+ enabled: formLoginStatus,
+ },
+ ...ssoConfigs,
+ ],
})
));
- const user = await userRepository.save(
- userRepository.create({
- firstName: firstName || 'test',
- lastName: lastName || 'test',
- email: email || 'dev@tooljet.io',
- password: 'password',
- invitationToken,
- organization,
- ssoId,
- createdAt: new Date(),
- updatedAt: new Date(),
- })
- );
+ let user: User;
+
+ if (!existingUser) {
+ user = await userRepository.save(
+ userRepository.create({
+ firstName: firstName || 'test',
+ lastName: lastName || 'test',
+ email: email || 'dev@tooljet.io',
+ password: 'password',
+ invitationToken,
+ defaultOrganizationId: organization.id,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ })
+ );
+ } else {
+ user = existingUser;
+ }
+ user.organizationId = organization.id;
const orgUser = await organizationUsersRepository.save(
organizationUsersRepository.create({
user: user,
organization,
- status: status || 'invited',
+ invitationToken: status === 'invited' ? uuidv4() : null,
+ status: status || 'active',
role: 'all_users',
createdAt: new Date(),
updatedAt: new Date(),
@@ -345,7 +391,7 @@ export async function addAllUsersGroupToUser(nestApp, user) {
const groupPermissionRepository: Repository = nestApp.get('GroupPermissionRepository');
const userGroupPermissionRepository: Repository = nestApp.get('UserGroupPermissionRepository');
- const orgDefaultGroupPermissions = await groupPermissionRepository.findOne({
+ const orgDefaultGroupPermissions = await groupPermissionRepository.findOneOrFail({
where: {
organizationId: user.organizationId,
group: 'all_users',