diff --git a/.version b/.version index 5bb76b575e..198ec23ccf 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -1.25.3 +1.25.6 diff --git a/frontend/src/Editor/Components/Calendar.jsx b/frontend/src/Editor/Components/Calendar.jsx index d0584ea6cd..f582277513 100644 --- a/frontend/src/Editor/Components/Calendar.jsx +++ b/frontend/src/Editor/Components/Calendar.jsx @@ -3,6 +3,7 @@ import { Calendar as ReactCalendar, momentLocalizer } from 'react-big-calendar'; import moment from 'moment'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import { CalendarEventPopover } from './CalendarPopover'; +import _ from 'lodash'; const localizer = momentLocalizer(moment); @@ -12,7 +13,16 @@ const prepareEvent = (event, dateFormat) => ({ end: moment(event.end, dateFormat).toDate(), }); -const parseDate = (date, dateFormat) => moment(date, dateFormat).toDate(); +const parseDate = (date, dateFormat) => { + const parsed = moment(date, dateFormat).toDate(); + + //handle invalid dates + if (isNaN(parsed.getTime())) { + return null; + } + + return parsed; +}; const allowedCalendarViews = ['month', 'week', 'day']; @@ -39,6 +49,7 @@ export const Calendar = function ({ const startTime = properties.startTime ? parseDate(properties.startTime, properties.dateFormat) : todayStartTime; const endTime = properties.endTime ? parseDate(properties.endTime, properties.dateFormat) : todayEndTime; + const [currentDate, setCurrentDate] = useState(defaultDate); const [eventPopoverOptions, setEventPopoverOptions] = useState({ show: false }); const eventPropGetter = (event) => { @@ -82,9 +93,15 @@ export const Calendar = function ({ : allowedCalendarViews[0]; useEffect(() => { - setExposedVariable('currentView', defaultView); + //check if the default date is a valid date + + if (defaultDate !== null && !_.isEqual(exposedVariables.currentDate, properties.defaultDate)) { + setExposedVariable('currentDate', moment(defaultDate).format(properties.dateFormat)); + setCurrentDate(defaultDate); + } + // eslint-disable-next-line react-hooks/exhaustive-deps - }, [defaultView]); + }, [JSON.stringify(moment(defaultDate).format('DD-MM-YYYY'))]); const components = { timeGutterHeader: () =>
All day
, @@ -93,7 +110,10 @@ export const Calendar = function ({ }, }; - if (exposedVariables.currentDate === undefined) setExposedVariable('currentDate', properties.defaultDate); + //! hack + if (exposedVariables.currentDate === undefined) { + setExposedVariable('currentDate', moment(defaultDate).format(properties.dateFormat)); + } return (
@@ -105,7 +125,7 @@ export const Calendar = function ({ ${exposedVariables.currentView === 'week' ? 'resources-week-cls' : ''} ${properties.displayViewSwitcher ? '' : 'hide-view-switcher'}`} localizer={localizer} - defaultDate={defaultDate} + date={currentDate} events={events} startAccessor="start" endAccessor="end" @@ -136,7 +156,9 @@ export const Calendar = function ({ }); }} onNavigate={(date) => { - setExposedVariable('currentDate', moment(date).format(properties.dateFormat)); + const formattedDate = moment(date).format(properties.dateFormat); + setExposedVariable('currentDate', formattedDate); + setCurrentDate(date); fireEvent('onCalendarNavigate'); }} selectable={true} diff --git a/frontend/src/SignupPage/SignupPage.jsx b/frontend/src/SignupPage/SignupPage.jsx index ec85fe7eec..40b450273d 100644 --- a/frontend/src/SignupPage/SignupPage.jsx +++ b/frontend/src/SignupPage/SignupPage.jsx @@ -15,8 +15,6 @@ class SignupPageComponent extends React.Component { isLoading: false, }; - console.log('window.public_config?.SSO_DISABLE_SIGNUPS--- ', window.public_config?.SSO_DISABLE_SIGNUPS != true); - this.ssoConfigs = { enableSignUp: window.public_config?.DISABLE_MULTI_WORKSPACE !== 'true' && diff --git a/frontend/src/_components/DynamicForm.jsx b/frontend/src/_components/DynamicForm.jsx index cb17b8814e..a326c2daf6 100644 --- a/frontend/src/_components/DynamicForm.jsx +++ b/frontend/src/_components/DynamicForm.jsx @@ -132,6 +132,7 @@ const DynamicForm = ({ auth_key: options.auth_key?.value, custom_auth_params: options.custom_auth_params?.value, custom_query_params: options.custom_query_params?.value, + multiple_auth_enabled: options.multiple_auth_enabled?.value, optionchanged, }; case 'react-component-google-sheets': diff --git a/frontend/src/_helpers/appUtils.js b/frontend/src/_helpers/appUtils.js index 12efa5ee16..69765e158d 100644 --- a/frontend/src/_helpers/appUtils.js +++ b/frontend/src/_helpers/appUtils.js @@ -667,7 +667,7 @@ export function runQuery(_ref, queryId, queryName, confirmed = undefined, mode = .then((data) => { if (data.status === 'needs_oauth') { const url = data.data.auth_url; // Backend generates and return sthe auth url - fetchOAuthToken(url, dataQuery.data_source_id); + fetchOAuthToken(url, dataQuery['data_source_id'] || dataQuery['dataSourceId']); } if (data.status === 'failed') { diff --git a/frontend/src/_ui/OAuth/Authentication.jsx b/frontend/src/_ui/OAuth/Authentication.jsx index 45a3794233..76b8ddb553 100644 --- a/frontend/src/_ui/OAuth/Authentication.jsx +++ b/frontend/src/_ui/OAuth/Authentication.jsx @@ -20,6 +20,7 @@ const Authentication = ({ bearer_token, password, auth_url, + multiple_auth_enabled, optionchanged, }) => { if (auth_type === 'oauth2') { @@ -153,6 +154,15 @@ const Authentication = ({ width={'100%'} useMenuPortal={false} /> +
)} diff --git a/frontend/src/_ui/OAuth/index.js b/frontend/src/_ui/OAuth/index.js index 7faa4d67a4..37abc22aa6 100644 --- a/frontend/src/_ui/OAuth/index.js +++ b/frontend/src/_ui/OAuth/index.js @@ -19,6 +19,7 @@ const OAuth = ({ auth_url, header_prefix, add_token_to, + multiple_auth_enabled, optionchanged, }) => { return ( @@ -48,6 +49,7 @@ const OAuth = ({ client_id={client_id} client_secret={client_secret} client_auth={client_auth} + multiple_auth_enabled={multiple_auth_enabled} scopes={scopes} username={username} password={password} diff --git a/plugins/packages/common/lib/app.type.ts b/plugins/packages/common/lib/app.type.ts new file mode 100644 index 0000000000..7f5179100d --- /dev/null +++ b/plugins/packages/common/lib/app.type.ts @@ -0,0 +1,4 @@ +export type App = { + id: string; + isPublic: boolean; +}; diff --git a/plugins/packages/common/lib/index.ts b/plugins/packages/common/lib/index.ts index e22d446c57..de1538e1ae 100644 --- a/plugins/packages/common/lib/index.ts +++ b/plugins/packages/common/lib/index.ts @@ -1,7 +1,9 @@ import { QueryError, OAuthUnauthorizedClientError } from './query.error'; import { QueryResult } from './query_result.type'; +import { User } from './user.type'; +import { App } from './app.type'; import { QueryService } from './query_service.interface'; -import { cacheConnection, getCachedConnection, parseJson, cleanSensitiveData } from './utils.helper'; +import { cacheConnection, getCachedConnection, parseJson, cleanSensitiveData, getCurrentToken } from './utils.helper'; import { ConnectionTestResult } from './connection_test_result.type'; export { @@ -9,9 +11,12 @@ export { OAuthUnauthorizedClientError, QueryResult, QueryService, + User, + App, cacheConnection, getCachedConnection, parseJson, ConnectionTestResult, cleanSensitiveData, + getCurrentToken, }; diff --git a/plugins/packages/common/lib/query_service.interface.ts b/plugins/packages/common/lib/query_service.interface.ts index e538696903..056930f4d4 100644 --- a/plugins/packages/common/lib/query_service.interface.ts +++ b/plugins/packages/common/lib/query_service.interface.ts @@ -1,12 +1,14 @@ +import { App } from './app.type'; import { ConnectionTestResult } from './connection_test_result.type'; import { QueryResult } from './query_result.type'; - +import { User } from './user.type'; export interface QueryService { run( sourceOptions: object, queryOptions: object, dataSourceId?: string, - dataSourceUpdatedAt?: string + dataSourceUpdatedAt?: string, + context?: { user?: User; app?: App } ): Promise; getConnection?(queryOptions: object, options: any, checkCache: boolean, dataSourceId: string): Promise; testConnection?(sourceOptions: object): Promise; diff --git a/plugins/packages/common/lib/user.type.ts b/plugins/packages/common/lib/user.type.ts new file mode 100644 index 0000000000..8fb9abe7cb --- /dev/null +++ b/plugins/packages/common/lib/user.type.ts @@ -0,0 +1,3 @@ +export type User = { + id: string; +}; diff --git a/plugins/packages/common/lib/utils.helper.ts b/plugins/packages/common/lib/utils.helper.ts index a6d688fd43..7d3ccdda23 100644 --- a/plugins/packages/common/lib/utils.helper.ts +++ b/plugins/packages/common/lib/utils.helper.ts @@ -51,3 +51,16 @@ function clearData(data, keys) { } } } + +export const getCurrentToken = (isMultiAuthEnabled: boolean, tokenData: any, userId: string, isAppPublic: boolean) => { + if (isMultiAuthEnabled) { + if (!tokenData || !Array.isArray(tokenData)) return null; + return !isAppPublic + ? tokenData.find((token: any) => token.user_id === userId) + : userId + ? tokenData.find((token: any) => token.user_id === userId) + : tokenData[0]; + } else { + return tokenData; + } +}; diff --git a/plugins/packages/restapi/lib/index.ts b/plugins/packages/restapi/lib/index.ts index 75d4bad127..de9cbdd8de 100644 --- a/plugins/packages/restapi/lib/index.ts +++ b/plugins/packages/restapi/lib/index.ts @@ -1,7 +1,15 @@ const urrl = require('url'); import { readFileSync } from 'fs'; import * as tls from 'tls'; -import { QueryError, QueryResult, QueryService, cleanSensitiveData } from '@tooljet-plugins/common'; +import { + QueryError, + QueryResult, + QueryService, + cleanSensitiveData, + User, + App, + getCurrentToken, +} from '@tooljet-plugins/common'; const JSON5 = require('json5'); import got, { Headers, HTTPError, OptionsOfTextResponseBody } from 'got'; @@ -85,7 +93,13 @@ export default class RestapiQueryService implements QueryService { return true; } - async run(sourceOptions: any, queryOptions: any, dataSourceId: string): Promise { + async run( + sourceOptions: any, + queryOptions: any, + dataSourceId: string, + dataSourceUpdatedAt: string, + context?: { user?: User; app?: App } + ): Promise { /* REST API queries can be adhoc or associated with a REST API datasource */ const hasDataSource = dataSourceId !== undefined; const authType = sourceOptions['auth_type']; @@ -94,12 +108,20 @@ export default class RestapiQueryService implements QueryService { const headers = this.headers(sourceOptions, queryOptions, hasDataSource); const customQueryParams = sanitizeCustomParams(sourceOptions['custom_query_params']); const isUrlEncoded = this.checkIfContentTypeIsURLenc(queryOptions['headers']); + const isMultiAuthEnabled = sourceOptions['multiple_auth_enabled']; /* Chceck if OAuth tokens exists for the source if query requires OAuth */ if (requiresOauth) { const tokenData = sourceOptions['tokenData']; + const isAppPublic = context?.app.isPublic; + const userData = context?.user; + const currentToken = getCurrentToken(isMultiAuthEnabled, tokenData, userData?.id, isAppPublic); - if (!tokenData) { + if (!currentToken && !userData?.id && isAppPublic) { + throw new QueryError('Missing access token', {}, {}); + } + + if (!currentToken) { const tooljetHost = process.env.TOOLJET_HOST; const authUrl = new URL( `${sourceOptions['auth_url']}?response_type=code&client_id=${sourceOptions['client_id']}&redirect_uri=${tooljetHost}/oauth2/authorize&scope=${sourceOptions['scopes']}` @@ -111,7 +133,7 @@ export default class RestapiQueryService implements QueryService { data: { auth_url: authUrl }, }; } else { - const accessToken = tokenData['access_token']; + const accessToken = currentToken['access_token']; if (sourceOptions['add_token_to'] === 'header') { const headerPrefix = sourceOptions['header_prefix']; headers['Authorization'] = `${headerPrefix}${accessToken}`; @@ -214,8 +236,10 @@ export default class RestapiQueryService implements QueryService { return contentType === 'application/x-www-form-urlencoded'; } - async refreshToken(sourceOptions, error) { - const refreshToken = sourceOptions['tokenData']['refresh_token']; + async refreshToken(sourceOptions: any, error: any, userId: string, isAppPublic: boolean) { + const isMultiAuthEnabled = sourceOptions['multiple_auth_enabled']; + const currentToken = getCurrentToken(isMultiAuthEnabled, sourceOptions['tokenData'], userId, isAppPublic); + const refreshToken = currentToken['refresh_token']; if (!refreshToken) { throw new QueryError('Refresh token not found', error.response, {}); } diff --git a/plugins/packages/restapi/lib/manifest.json b/plugins/packages/restapi/lib/manifest.json index 5cce3cf2d7..6ffacaa5e1 100644 --- a/plugins/packages/restapi/lib/manifest.json +++ b/plugins/packages/restapi/lib/manifest.json @@ -35,7 +35,7 @@ "password": { "encrypted": true }, - "bearer_token":{ + "bearer_token": { "encrypted": true }, "scopes": { @@ -105,36 +105,19 @@ "value": "header" }, "headers": { - "value": [ - [ - "", - "" - ] - ] + "value": [["", ""]] }, "custom_query_params": { - "value": [ - [ - "", - "" - ] - ] + "value": [["", ""]] }, "custom_auth_params": { - "value": [ - [ - "", - "" - ] - ] + "value": [["", ""]] }, "access_token_custom_headers": { - "value": [ - [ - "", - "" - ] - ] + "value": [["", ""]] + }, + "multiple_auth_enabled": { + "value": false } }, "properties": { @@ -157,7 +140,5 @@ "description": "key-value pair headers for rest api" } }, - "required": [ - "url" - ] + "required": ["url"] } diff --git a/plugins/packages/stripe/lib/index.ts b/plugins/packages/stripe/lib/index.ts index 7cc19c4b23..a482f78309 100644 --- a/plugins/packages/stripe/lib/index.ts +++ b/plugins/packages/stripe/lib/index.ts @@ -35,10 +35,11 @@ export default class StripeQueryService implements QueryService { searchParams: queryParams, }); } else { + const resolvedBodyParams = this.resolveBodyparams(bodyParams); response = await got(url, { method: operation, headers: this.authHeader(apiKey), - form: bodyParams, + form: resolvedBodyParams, searchParams: queryParams, }); } @@ -53,4 +54,24 @@ export default class StripeQueryService implements QueryService { data: result, }; } + + private resolveBodyparams(bodyParams: object): object { + if (typeof bodyParams === 'string') { + return bodyParams; + } + + const expectedResult = {}; + + for (const key of Object.keys(bodyParams)) { + if (typeof bodyParams[key] === 'object') { + for (const subKey of Object.keys(bodyParams[key])) { + expectedResult[`${key}[${subKey}]`] = bodyParams[key][subKey]; + } + } else { + expectedResult[key] = bodyParams[key]; + } + } + + return expectedResult; + } } diff --git a/server/.version b/server/.version index 5bb76b575e..198ec23ccf 100644 --- a/server/.version +++ b/server/.version @@ -1 +1 @@ -1.25.3 +1.25.6 diff --git a/server/src/controllers/data_sources.controller.ts b/server/src/controllers/data_sources.controller.ts index b008ddb238..5c6b5c0498 100644 --- a/server/src/controllers/data_sources.controller.ts +++ b/server/src/controllers/data_sources.controller.ts @@ -140,6 +140,6 @@ export class DataSourcesController { throw new ForbiddenException('you do not have permissions to perform this action'); } - return await this.dataQueriesService.authorizeOauth2(dataSource, code); + return await this.dataQueriesService.authorizeOauth2(dataSource, code, user.id); } } diff --git a/server/src/modules/auth/query-auth.guard.ts b/server/src/modules/auth/query-auth.guard.ts index b8e744cad7..b71eee60a8 100644 --- a/server/src/modules/auth/query-auth.guard.ts +++ b/server/src/modules/auth/query-auth.guard.ts @@ -16,6 +16,8 @@ export class QueryAuthGuard extends AuthGuard('jwt') { const dataQuery = await this.dataQueriesService.findOne(request.params.id); const app = dataQuery.app; + if (app.isPublic === true && request.headers['authorization']) return super.canActivate(context); + if (app.isPublic === true) { return true; } diff --git a/server/src/services/data_queries.service.ts b/server/src/services/data_queries.service.ts index a76397e496..0cd17f1b0f 100644 --- a/server/src/services/data_queries.service.ts +++ b/server/src/services/data_queries.service.ts @@ -96,6 +96,7 @@ export class DataQueriesService { async runQuery(user: User, dataQuery: any, queryOptions: object): Promise { const dataSource = dataQuery.dataSource?.id ? dataQuery.dataSource : {}; + const app = dataQuery?.app; const organizationId = user ? user.organizationId : await this.getOrgIdfromApp(dataQuery.appId); let { sourceOptions, parsedQueryOptions, service } = await this.fetchServiceAndParsedParams( dataSource, @@ -106,7 +107,10 @@ export class DataQueriesService { let result; try { - return await service.run(sourceOptions, parsedQueryOptions, dataSource.id, dataSource.updatedAt); + return await service.run(sourceOptions, parsedQueryOptions, dataSource.id, dataSource.updatedAt, { + user: { id: user?.id }, + app: { id: app?.id, isPublic: app?.isPublic }, + }); } catch (error) { const statusCode = error?.data?.responseObject?.statusCode; @@ -116,8 +120,13 @@ export class DataQueriesService { ) { console.log('Access token expired. Attempting refresh token flow.'); - const accessTokenDetails = await service.refreshToken(sourceOptions, dataSource.id); - await this.dataSourcesService.updateOAuthAccessToken(accessTokenDetails, dataSource.options, dataSource.id); + const accessTokenDetails = await service.refreshToken(sourceOptions, dataSource.id, user?.id, app?.isPublic); + await this.dataSourcesService.updateOAuthAccessToken( + accessTokenDetails, + dataSource.options, + dataSource.id, + user?.id + ); await dataSource.reload(); ({ sourceOptions, parsedQueryOptions, service } = await this.fetchServiceAndParsedParams( @@ -127,7 +136,10 @@ export class DataQueriesService { organizationId )); - result = await service.run(sourceOptions, parsedQueryOptions, dataSource.id, dataSource.updatedAt); + result = await service.run(sourceOptions, parsedQueryOptions, dataSource.id, dataSource.updatedAt, { + user: { id: user?.id }, + app: { id: app?.id, isPublic: app?.isPublic }, + }); } else { throw error; } @@ -149,7 +161,7 @@ export class DataQueriesService { } /* This function fetches the access token from the token url set in REST API (oauth) datasource */ - async fetchOAuthToken(sourceOptions: any, code: string): Promise { + async fetchOAuthToken(sourceOptions: any, code: string, userId: any, isMultiAuthEnabled: boolean): Promise { const tooljetHost = process.env.TOOLJET_HOST; const isUrlEncoded = this.checkIfContentTypeIsURLenc(sourceOptions['access_token_custom_headers']); const accessTokenUrl = sourceOptions['access_token_url']; @@ -177,7 +189,11 @@ export class DataQueriesService { }); const result = JSON.parse(response.body); - return { access_token: result['access_token'], refresh_token: result['refresh_token'] }; + return { + ...(isMultiAuthEnabled ? { user_id: userId } : {}), + access_token: result['access_token'], + refresh_token: result['refresh_token'], + }; } catch (err) { throw new BadRequestException(this.parseErrorResponse(err?.response?.body, err?.response?.statusCode)); } @@ -195,10 +211,26 @@ export class DataQueriesService { return JSON.stringify(errorObj); } + private getCurrentToken = (isMultiAuthEnabled: boolean, tokenData: any, newToken: any) => { + if (isMultiAuthEnabled) { + let tokensArray = []; + if (tokenData && Array.isArray(tokenData)) { + tokensArray = [...tokenData, newToken]; + } else { + tokensArray.push(newToken); + } + return tokensArray; + } else { + return newToken; + } + }; + /* This function fetches access token from authorization code */ - async authorizeOauth2(dataSource: DataSource, code: string): Promise { + async authorizeOauth2(dataSource: DataSource, code: string, userId: string): Promise { const sourceOptions = await this.parseSourceOptions(dataSource.options); - const tokenData = await this.fetchOAuthToken(sourceOptions, code); + const isMultiAuthEnabled = dataSource.options['multiple_auth_enabled']?.value; + const newToken = await this.fetchOAuthToken(sourceOptions, code, userId, isMultiAuthEnabled); + const tokenData = this.getCurrentToken(isMultiAuthEnabled, dataSource.options['tokenData']?.value, newToken); const tokenOptions = [ { diff --git a/server/src/services/data_sources.service.ts b/server/src/services/data_sources.service.ts index 4b41edb022..3d7b960435 100644 --- a/server/src/services/data_sources.service.ts +++ b/server/src/services/data_sources.service.ts @@ -19,7 +19,23 @@ export class DataSourcesService { const { app_id: appId, app_version_id: appVersionId }: any = query; const whereClause = { appId, ...(appVersionId && { appVersionId }) }; - return await this.dataSourcesRepository.find({ where: whereClause }); + const result = await this.dataSourcesRepository.find({ where: whereClause }); + + //remove tokenData from restapi datasources + const dataSources = result?.map((ds) => { + if (ds.kind === 'restapi') { + const options = {}; + Object.keys(ds.options).filter((key) => { + if (key !== 'tokenData') { + return (options[key] = ds.options[key]); + } + }); + ds.options = options; + } + return ds; + }); + + return dataSources; } async findOne(dataSourceId: string): Promise { @@ -52,6 +68,14 @@ export class DataSourcesService { async update(dataSourceId: string, name: string, options: Array): Promise { const dataSource = await this.findOne(dataSourceId); + // if datasource is restapi then reset the token data + if (dataSource.kind === 'restapi') + options.push({ + key: 'tokenData', + value: undefined, + encrypted: false, + }); + const updateableParams = { id: dataSourceId, name, @@ -190,16 +214,46 @@ export class DataSourcesService { return parsedOptions; } - async updateOAuthAccessToken(accessTokenDetails: object, dataSourceOptions: object, dataSourceId: string) { + private changeCurrentToken = ( + tokenData: any, + userId: string, + accessTokenDetails: any, + isMultiAuthEnabled: boolean + ) => { + if (isMultiAuthEnabled) { + return tokenData?.value.map((token: any) => { + if (token.userId === userId) { + return { ...token, ...accessTokenDetails }; + } + return token; + }); + } else { + return accessTokenDetails; + } + }; + + async updateOAuthAccessToken( + accessTokenDetails: object, + dataSourceOptions: object, + dataSourceId: string, + userId: string + ) { const existingCredentialId = dataSourceOptions['access_token'] && dataSourceOptions['access_token']['credential_id']; if (existingCredentialId) { await this.credentialsService.update(existingCredentialId, accessTokenDetails['access_token']); } else if (dataSourceId) { + const isMultiAuthEnabled = dataSourceOptions['multiple_auth_enabled']?.value; + const updatedTokenData = this.changeCurrentToken( + dataSourceOptions['tokenData'], + userId, + accessTokenDetails, + isMultiAuthEnabled + ); const tokenOptions = [ { key: 'tokenData', - value: accessTokenDetails, + value: updatedTokenData, encrypted: false, }, ];