Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/src/api/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export default (app) => {
`/tenant/:tenantId/integration/autocomplete`,
safeWrap(require('./integrationAutocomplete').default),
)
app.get(`/tenant/:tenantId/integration/global`, safeWrap(require('./integrationGlobal').default))
app.get(
`/tenant/:tenantId/integration/global/status`,
safeWrap(require('./integrationGlobalStatus').default),
)
Comment on lines +32 to +36
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Implement feature flags for global integration endpoints

Authentication and authorization checks are in place. However, consider adding feature flags to control access to these endpoints if they handle sensitive integration data.

🔗 Analysis chain

Verify authentication and authorization checks

The new global integration endpoints follow the existing patterns, but please ensure:

  1. The handlers (integrationGlobal.ts and integrationGlobalStatus.ts) implement proper authentication and authorization checks
  2. Consider if these endpoints should be protected by feature flags if they expose sensitive integration data

Let's verify the authentication checks in the handlers:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for authentication/authorization in the handlers

# Check for auth middleware or permission checks in the handlers
echo "Checking integrationGlobal.ts for auth checks:"
rg -A 5 "permissions\.|authMiddleware|PermissionChecker" "backend/src/api/integration/integrationGlobal.ts" || echo "No auth checks found"

echo -e "\nChecking integrationGlobalStatus.ts for auth checks:"
rg -A 5 "permissions\.|authMiddleware|PermissionChecker" "backend/src/api/integration/integrationGlobalStatus.ts" || echo "No auth checks found"

Length of output: 1106


Script:

#!/bin/bash
# Description: Check for feature flag usage in the handlers

echo "Checking integrationGlobal.ts for feature flags:"
rg -A 5 "featureFlag|isFeatureEnabled|Flags\." "backend/src/api/integration/integrationGlobal.ts" || echo "No feature flag usage found"

echo -e "\nChecking integrationGlobalStatus.ts for feature flags:"
rg -A 5 "featureFlag|isFeatureEnabled|Flags\." "backend/src/api/integration/integrationGlobalStatus.ts" || echo "No feature flag usage found"

Length of output: 563

app.get(`/tenant/:tenantId/integration`, safeWrap(require('./integrationList').default))
app.get(`/tenant/:tenantId/integration/:id`, safeWrap(require('./integrationFind').default))

Expand Down
13 changes: 13 additions & 0 deletions backend/src/api/integration/integrationGlobal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Permissions from '../../security/permissions'
import IntegrationService from '../../services/integrationService'
import PermissionChecker from '../../services/user/permissionChecker'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.integrationRead)
const payload = await new IntegrationService(req).findGlobalIntegrations(
req.params.tenantId,
req.query,
)

await req.responseHandler.success(req, res, payload)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Inconsistent Usage of responseHandler.success Detected

  • backend/src/api/webhooks/stripe.ts
  • backend/src/api/webhooks/sendgrid.ts
  • backend/src/api/webhooks/github.ts
  • backend/src/api/webhooks/discourse.ts
  • backend/src/api/member/memberUnmergePreview.ts
  • backend/src/api/member/memberMerge.ts
🔗 Analysis chain

Verify response handler implementation.

The success response handling needs verification for consistency.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check responseHandler implementation

# Search for responseHandler.success implementation and usage
rg "responseHandler\.success" -A 3

# Check for consistent response structure
ast-grep --pattern 'responseHandler.success($$$)'

Length of output: 41838


Script:

#!/bin/bash
# Description: Locate the definition of responseHandler.success and verify its implementation

# Find the definition of responseHandler.success
rg "function\s+success\s*\(" -- backend/src/api/**/*.ts

# Extract the signature of responseHandler.success
ast-grep --pattern 'function success($_, $_, $_, $_)' backend/src/api/**/*.ts

# Check for inconsistent usage by counting parameters in each call
rg "responseHandler\.success\s*\(" backend/src/api/**/*.ts | awk -F'(' '{print $2}' | awk -F',' '{print NF}'

Length of output: 28975


Script:

#!/bin/bash
# Description: Identify all instances of responseHandler.success with their parameter counts

# Find all calls to responseHandler.success and count the number of parameters
rg "responseHandler\.success\s*\(" backend/src/api/**/*.ts | while IFS= read -r line
do
  # Extract the file path
  file=$(echo "$line" | cut -d':' -f1)
  
  # Extract the parameter list
  params=$(echo "$line" | grep -oP 'responseHandler\.success\s*\(\s*\K[^)]*')
  
  # Count the number of parameters by splitting on commas, accounting for nested objects
  param_count=$(echo "$params" | awk -F',' '{print NF}')
  
  # Output the file and parameter count
  echo "$file: $param_count parameters"
done

Length of output: 119712

}
14 changes: 14 additions & 0 deletions backend/src/api/integration/integrationGlobalStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Permissions from '../../security/permissions'
import IntegrationService from '../../services/integrationService'
import PermissionChecker from '../../services/user/permissionChecker'

export default async (req, res) => {
new PermissionChecker(req).validateHas(Permissions.values.integrationRead)

const payload = await new IntegrationService(req).findGlobalIntegrationsStatusCount(
req.params.tenantId,
req.query,
)

await req.responseHandler.success(req, res, payload)
}
65 changes: 65 additions & 0 deletions backend/src/database/repositories/integrationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import Sequelize, { QueryTypes } from 'sequelize'

import { captureApiChange, integrationConnectAction } from '@crowd/audit-logs'
import { Error404 } from '@crowd/common'
import {
fetchGlobalIntegrations,
fetchGlobalIntegrationsCount,
fetchGlobalIntegrationsStatusCount,
fetchGlobalNotConnectedIntegrations,
fetchGlobalNotConnectedIntegrationsCount,
} from '@crowd/data-access-layer/src/integrations'
import { IntegrationRunState, PlatformType } from '@crowd/types'

import SequelizeFilterUtils from '../utils/sequelizeFilterUtils'
Expand Down Expand Up @@ -393,6 +400,64 @@ class IntegrationRepository {
})
}

/**
* Finds global integrations based on the provided parameters.
*
* @param {string} tenantId - The ID of the tenant for which integrations are to be found.
* @param {Object} filters - An object containing various filter options.
* @param {string} [filters.platform=null] - The platform to filter integrations by.
* @param {string[]} [filters.status=['done']] - The status of the integrations to be filtered.
* @param {string} [filters.query=''] - The search query to filter integrations.
* @param {number} [filters.limit=20] - The maximum number of integrations to return.
* @param {number} [filters.offset=0] - The offset for pagination.
* @param {IRepositoryOptions} options - The repository options for querying.
* @returns {Promise<Object>} The result containing the rows of integrations and metadata about the query.
*/
static async findGlobalIntegrations(
tenantId: string,
{ platform = null, status = ['done'], query = '', limit = 20, offset = 0 },
options: IRepositoryOptions,
) {
const qx = SequelizeRepository.getQueryExecutor(options)
if (status.includes('not-connected')) {
const rows = await fetchGlobalNotConnectedIntegrations(
qx,
tenantId,
platform,
query,
limit,
offset,
)
const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, query)
return { rows, count: +result.count, limit: +limit, offset: +offset }
}

const rows = await fetchGlobalIntegrations(qx, tenantId, status, platform, query, limit, offset)
const [result] = await fetchGlobalIntegrationsCount(qx, tenantId, status, platform, query)
return { rows, count: +result.count, limit: +limit, offset: +offset }
}
Comment on lines +403 to +438
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and error handling

While the implementation is solid, consider these essential improvements:

  1. Add input validation for limit and offset
  2. Add error handling for database operations
 static async findGlobalIntegrations(
   tenantId: string,
   { platform = null, status = ['done'], query = '', limit = 20, offset = 0 },
   options: IRepositoryOptions,
 ) {
+  if (limit < 0 || offset < 0) {
+    throw new Error('Limit and offset must be non-negative');
+  }
+
   const qx = SequelizeRepository.getQueryExecutor(options)
-  if (status.includes('not-connected')) {
-    const rows = await fetchGlobalNotConnectedIntegrations(
-      qx,
-      tenantId,
-      platform,
-      query,
-      limit,
-      offset,
-    )
-    const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, query)
-    return { rows, count: +result.count, limit: +limit, offset: +offset }
-  }
+  try {
+    if (status.includes('not-connected')) {
+      const rows = await fetchGlobalNotConnectedIntegrations(
+        qx,
+        tenantId,
+        platform,
+        query,
+        limit,
+        offset,
+      )
+      const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, query)
+      return { rows, count: +result.count, limit: +limit, offset: +offset }
+    }

-  const rows = await fetchGlobalIntegrations(qx, tenantId, status, platform, query, limit, offset)
-  const [result] = await fetchGlobalIntegrationsCount(qx, tenantId, status, platform, query)
-  return { rows, count: +result.count, limit: +limit, offset: +offset }
+    const rows = await fetchGlobalIntegrations(qx, tenantId, status, platform, query, limit, offset)
+    const [result] = await fetchGlobalIntegrationsCount(qx, tenantId, status, platform, query)
+    return { rows, count: +result.count, limit: +limit, offset: +offset }
+  } catch (error) {
+    throw new Error(`Failed to fetch global integrations: ${error.message}`);
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Finds global integrations based on the provided parameters.
*
* @param {string} tenantId - The ID of the tenant for which integrations are to be found.
* @param {Object} filters - An object containing various filter options.
* @param {string} [filters.platform=null] - The platform to filter integrations by.
* @param {string[]} [filters.status=['done']] - The status of the integrations to be filtered.
* @param {string} [filters.query=''] - The search query to filter integrations.
* @param {number} [filters.limit=20] - The maximum number of integrations to return.
* @param {number} [filters.offset=0] - The offset for pagination.
* @param {IRepositoryOptions} options - The repository options for querying.
* @returns {Promise<Object>} The result containing the rows of integrations and metadata about the query.
*/
static async findGlobalIntegrations(
tenantId: string,
{ platform = null, status = ['done'], query = '', limit = 20, offset = 0 },
options: IRepositoryOptions,
) {
const qx = SequelizeRepository.getQueryExecutor(options)
if (status.includes('not-connected')) {
const rows = await fetchGlobalNotConnectedIntegrations(
qx,
tenantId,
platform,
query,
limit,
offset,
)
const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, query)
return { rows, count: +result.count, limit: +limit, offset: +offset }
}
const rows = await fetchGlobalIntegrations(qx, tenantId, status, platform, query, limit, offset)
const [result] = await fetchGlobalIntegrationsCount(qx, tenantId, status, platform, query)
return { rows, count: +result.count, limit: +limit, offset: +offset }
}
/**
* Finds global integrations based on the provided parameters.
*
* @param {string} tenantId - The ID of the tenant for which integrations are to be found.
* @param {Object} filters - An object containing various filter options.
* @param {string} [filters.platform=null] - The platform to filter integrations by.
* @param {string[]} [filters.status=['done']] - The status of the integrations to be filtered.
* @param {string} [filters.query=''] - The search query to filter integrations.
* @param {number} [filters.limit=20] - The maximum number of integrations to return.
* @param {number} [filters.offset=0] - The offset for pagination.
* @param {IRepositoryOptions} options - The repository options for querying.
* @returns {Promise<Object>} The result containing the rows of integrations and metadata about the query.
*/
static async findGlobalIntegrations(
tenantId: string,
{ platform = null, status = ['done'], query = '', limit = 20, offset = 0 },
options: IRepositoryOptions,
) {
if (limit < 0 || offset < 0) {
throw new Error('Limit and offset must be non-negative');
}
const qx = SequelizeRepository.getQueryExecutor(options)
try {
if (status.includes('not-connected')) {
const rows = await fetchGlobalNotConnectedIntegrations(
qx,
tenantId,
platform,
query,
limit,
offset,
)
const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, query)
return { rows, count: +result.count, limit: +limit, offset: +offset }
}
const rows = await fetchGlobalIntegrations(qx, tenantId, status, platform, query, limit, offset)
const [result] = await fetchGlobalIntegrationsCount(qx, tenantId, status, platform, query)
return { rows, count: +result.count, limit: +limit, offset: +offset }
} catch (error) {
throw new Error(`Failed to fetch global integrations: ${error.message}`);
}
}


/**
* Retrieves the count of global integrations statuses for a specified tenant and platform.
* This method aggregates the count of different integration statuses including a 'not-connected' status.
*
* @param {string} tenantId - The unique identifier for the tenant.
* @param {Object} param1 - The optional parameters.
* @param {string|null} [param1.platform=null] - The platform to filter the integrations. Default is null.
* @param {IRepositoryOptions} options - The options for the repository operations.
* @return {Promise<Array<Object>>} A promise that resolves to an array of objects containing the statuses and their counts.
*/
static async findGlobalIntegrationsStatusCount(
tenantId: string,
{ platform = null },
options: IRepositoryOptions,
) {
const qx = SequelizeRepository.getQueryExecutor(options)
const [result] = await fetchGlobalNotConnectedIntegrationsCount(qx, tenantId, platform, '')
const rows = await fetchGlobalIntegrationsStatusCount(qx, tenantId, platform)
return [...rows, { status: 'not-connected', count: +result.count }]
}

static async findAndCountAll(
{ filter = {} as any, advancedFilter = null as any, limit = 0, offset = 0, orderBy = '' },
options: IRepositoryOptions,
Expand Down
22 changes: 22 additions & 0 deletions backend/src/services/integrationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,28 @@ export default class IntegrationService {
return IntegrationRepository.findAndCountAll(args, this.options)
}

/**
* Retrieves global integrations for the specified tenant.
*
* @param {string} tenantId - The unique identifier of the tenant.
* @param {any} args - Additional arguments that define search criteria or constraints.
* @return {Promise<any>} A promise that resolves to the list of global integrations matching the criteria.
*/
async findGlobalIntegrations(tenantId: string, args: any) {
return IntegrationRepository.findGlobalIntegrations(tenantId, args, this.options)
}

/**
* Fetches the global count of integration statuses for a given tenant.
*
* @param {string} tenantId - The ID of the tenant for which to fetch the count.
* @param {Object} args - Additional arguments to refine the query.
* @return {Promise<number>} A promise that resolves to the count of global integration statuses.
*/
async findGlobalIntegrationsStatusCount(tenantId: string, args: any) {
return IntegrationRepository.findGlobalIntegrationsStatusCount(tenantId, args, this.options)
}
Comment on lines +301 to +321
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling to the new methods.

The methods would benefit from basic error handling to improve robustness and debugging capabilities.

 async findGlobalIntegrations(tenantId: string, args: GlobalIntegrationsArgs) {
+  if (!tenantId) {
+    throw new Error400(this.options.language, 'errors.tenant.required');
+  }
+
+  try {
     return IntegrationRepository.findGlobalIntegrations(tenantId, args, this.options)
+  } catch (error) {
+    this.options.log.error('Error in findGlobalIntegrations:', error);
+    throw error;
+  }
 }

 async findGlobalIntegrationsStatusCount(tenantId: string, args: GlobalIntegrationsArgs) {
+  if (!tenantId) {
+    throw new Error400(this.options.language, 'errors.tenant.required');
+  }
+
+  try {
     return IntegrationRepository.findGlobalIntegrationsStatusCount(tenantId, args, this.options)
+  } catch (error) {
+    this.options.log.error('Error in findGlobalIntegrationsStatusCount:', error);
+    throw error;
+  }
 }

Committable suggestion skipped: line range outside the PR's diff.


async query(data) {
const advancedFilter = data.filter
const orderBy = data.orderBy
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<template>
<lf-dropdown placement="bottom-end" width="20rem">
<template #trigger>
<lf-button type="secondary" class="!font-normal">
<template v-if="!model">
<lf-icon-old name="apps-2-line" :size="16" />
All integrations
</template>
<template v-else>
<img :src="lfIntegrations[model]?.image" :alt="lfIntegrations[model]?.name" class="w-4 h-4 object-contain">
{{ lfIntegrations[model]?.name }}
</template>
<lf-icon-old name="arrow-down-s-line" :size="16" />
</lf-button>
</template>
<div class="max-h-80 overflow-y-scroll -m-2 p-2">
<lf-dropdown-item :selected="!model" @click="model = ''">
<div class="flex items-center gap-2">
<lf-icon-old name="apps-2-line" :size="16" />
All integrations
</div>
</lf-dropdown-item>
<lf-dropdown-separator />

<lf-dropdown-item
v-for="(integration, key) in lfIntegrations"
:key="key"
:selected="model === key"
@click="model = key"
>
<div class="flex items-center gap-2">
<img :src="integration.image" :alt="integration.name" class="w-4 h-4 object-contain">
{{ integration.name }}
</div>
</lf-dropdown-item>
</div>
</lf-dropdown>
</template>

<script lang="ts" setup>
import { computed } from 'vue';
import LfButton from '@/ui-kit/button/Button.vue';
import LfIconOld from '@/ui-kit/icon/IconOld.vue';
import LfDropdown from '@/ui-kit/dropdown/Dropdown.vue';
import LfDropdownItem from '@/ui-kit/dropdown/DropdownItem.vue';
import LfDropdownSeparator from '@/ui-kit/dropdown/DropdownSeparator.vue';
import { lfIntegrations } from '@/config/integrations';

const props = defineProps<{
modelValue?: string,
}>();

const emit = defineEmits<{(e: 'update:modelValue', value: string)}>();

const model = computed({
get() {
return props.modelValue || '';
},
set(value: string) {
emit('update:modelValue', value);
},
});
</script>

<script lang="ts">
export default {
name: 'LfAdminIntegrationPlatformSelect',
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con
const connecting: IntegrationStatusConfig = {
key: 'connecting',
show: (integration: any) => integration.status === 'in-progress',
statuses: ['in-progress'],
status: {
text: 'Connecting',
icon: 'loader-4-line animate-spin',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con
const done: IntegrationStatusConfig = {
key: 'done',
show: (integration: any) => integration.status === 'done',
statuses: ['done'],
status: {
text: 'Connected',
icon: 'checkbox-circle-fill',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/con
const error: IntegrationStatusConfig = {
key: 'error',
show: (integration: any) => integration.status === 'error',
statuses: ['error'],
status: {
text: 'Connection failed',
icon: 'error-warning-fill',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import done from './done';
import error from './error';
import waitingForAction from './waiting-for-action';
import waitingApproval from './waiting-approval';
import connecting from './connecting';
import notConnected from './not-connected';

export interface IntegrationStatusConfig {
key: string;
show: (integration: any) => boolean;
statuses: string[],
status: {
text: string;
icon: string;
Expand All @@ -27,7 +28,6 @@ export const lfIntegrationStatuses: Record<string, IntegrationStatusConfig> = {
done,
error,
waitingForAction,
waitingApproval,
connecting,
};

Expand All @@ -36,6 +36,7 @@ export const lfIntegrationStatusesTabs: Record<string, IntegrationStatusConfig>
connecting,
waitingForAction,
error,
notConnected,
};

export const getIntegrationStatus = (integration: any): IntegrationStatusConfig => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IntegrationStatusConfig } from '@/modules/admin/modules/integration/config/status/index';

const notConnected: IntegrationStatusConfig = {
key: 'notConnected',
show: (integration: any) => !integration || integration.status === 'not-connected',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety in the show function.

The function uses any type which bypasses TypeScript's type checking benefits. Consider defining a proper interface for the integration parameter.

-  show: (integration: any) => !integration || integration.status === 'not-connected',
+  show: (integration: { status?: string }) => !integration || integration.status === 'not-connected',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
show: (integration: any) => !integration || integration.status === 'not-connected',
show: (integration: { status?: string }) => !integration || integration.status === 'not-connected',

statuses: ['not-connected'],
status: {
text: 'Not-connected',
icon: '',
color: 'text-gray-600',
},
actionBar: {
background: 'bg-gray-50',
color: 'text-gray-900',
},
tabs: {
text: 'Not connected',
empty: 'No integrations to be connected',
badge: 'bg-gray-100',
},
};

export default notConnected;
Loading
Loading