| import { |
| Body, |
| Controller, |
| Get, |
| HttpException, |
| Param, |
| Post, |
| UseFilters, |
| } from '@nestjs/common'; |
| import { ioRedis } from '@gitroom/nestjs-libraries/redis/redis.service'; |
| import { ConnectIntegrationDto } from '@gitroom/nestjs-libraries/dtos/integrations/connect.integration.dto'; |
| import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager'; |
| import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service'; |
| import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability'; |
| import { ApiTags } from '@nestjs/swagger'; |
| import { NotEnoughScopesFilter } from '@gitroom/nestjs-libraries/integrations/integration.missing.scopes'; |
| import { AuthService } from '@gitroom/helpers/auth/auth.service'; |
| import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface'; |
| import { NotEnoughScopes } from '@gitroom/nestjs-libraries/integrations/social.abstract'; |
| import { |
| AuthorizationActions, |
| Sections, |
| } from '@gitroom/backend/services/auth/permissions/permission.exception.class'; |
| import { RefreshIntegrationService } from '@gitroom/nestjs-libraries/integrations/refresh.integration.service'; |
| import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service'; |
|
|
| @ApiTags('Integrations') |
| @Controller('/integrations') |
| export class NoAuthIntegrationsController { |
| constructor( |
| private _integrationManager: IntegrationManager, |
| private _integrationService: IntegrationService, |
| private _refreshIntegrationService: RefreshIntegrationService, |
| private _organizationService: OrganizationService |
| ) {} |
|
|
| @Get('/') |
| getIntegrations() { |
| return this._integrationManager.getAllIntegrations(); |
| } |
|
|
| @Post('/social-connect/:integration') |
| @CheckPolicies([AuthorizationActions.Create, Sections.CHANNEL]) |
| @UseFilters(new NotEnoughScopesFilter()) |
| async connectSocialMedia( |
| @Param('integration') integration: string, |
| @Body() body: ConnectIntegrationDto |
| ) { |
| if ( |
| !this._integrationManager |
| .getAllowedSocialsIntegrations() |
| .includes(integration) |
| ) { |
| throw new Error('Integration not allowed'); |
| } |
|
|
| const integrationProvider = |
| this._integrationManager.getSocialIntegration(integration); |
|
|
| const getCodeVerifier = integrationProvider.customFields |
| ? 'none' |
| : await ioRedis.get(`login:${body.state}`); |
| if (!getCodeVerifier) { |
| throw new Error('Invalid state'); |
| } |
|
|
| const organization = await ioRedis.get(`organization:${body.state}`); |
| if (!organization) { |
| throw new Error('Organization not found'); |
| } |
|
|
| const org = await this._organizationService.getOrgById(organization); |
|
|
| if (!integrationProvider.customFields) { |
| await ioRedis.del(`login:${body.state}`); |
| } |
|
|
| const details = integrationProvider.externalUrl |
| ? await ioRedis.get(`external:${body.state}`) |
| : undefined; |
|
|
| if (details) { |
| await ioRedis.del(`external:${body.state}`); |
| } |
|
|
| const refresh = await ioRedis.get(`refresh:${body.state}`); |
| if (refresh) { |
| await ioRedis.del(`refresh:${body.state}`); |
| } |
|
|
| const onboarding = await ioRedis.get(`onboarding:${body.state}`); |
| if (onboarding) { |
| await ioRedis.del(`onboarding:${body.state}`); |
| } |
|
|
| const { |
| error, |
| accessToken, |
| expiresIn, |
| refreshToken, |
| id, |
| name, |
| picture, |
| username, |
| additionalSettings, |
| |
| } = await new Promise<AuthTokenDetails>(async (res) => { |
| try { |
| const auth = await integrationProvider.authenticate( |
| { |
| code: body.code, |
| codeVerifier: getCodeVerifier, |
| refresh: body.refresh, |
| }, |
| details ? JSON.parse(details) : undefined |
| ); |
|
|
| if (typeof auth === 'string') { |
| return res({ |
| error: auth, |
| accessToken: '', |
| id: '', |
| name: '', |
| picture: '', |
| username: '', |
| additionalSettings: [], |
| }); |
| } |
|
|
| if (refresh && integrationProvider.reConnect) { |
| console.log('reconnect'); |
| try { |
| const newAuth = await integrationProvider.reConnect( |
| auth.id, |
| refresh, |
| auth.accessToken |
| ); |
| return res({ ...newAuth, refreshToken: body.refresh }); |
| } catch (err: any) { |
| return res({ |
| error: err.message, |
| accessToken: '', |
| id: '', |
| name: '', |
| picture: '', |
| username: '', |
| additionalSettings: [], |
| }); |
| } |
| } |
|
|
| return res(auth); |
| } catch (err) { |
| if (err instanceof NotEnoughScopes) { |
| return res({ |
| error: err.message, |
| accessToken: '', |
| id: '', |
| name: '', |
| picture: '', |
| username: '', |
| additionalSettings: [], |
| }); |
| } |
|
|
| return res({ |
| error: 'Authentication failed', |
| accessToken: '', |
| id: '', |
| name: '', |
| picture: '', |
| username: '', |
| additionalSettings: [], |
| }); |
| } |
| }); |
|
|
| if (error) { |
| throw new NotEnoughScopes(error); |
| } |
|
|
| if (!id) { |
| throw new NotEnoughScopes('Invalid API key'); |
| } |
|
|
| if (refresh && String(id) !== String(refresh)) { |
| throw new NotEnoughScopes( |
| 'Please refresh the channel that needs to be refreshed' |
| ); |
| } |
|
|
| let validName = name; |
| if (!validName) { |
| if (username) { |
| validName = username.split('.')[0] ?? username; |
| } else { |
| validName = `Channel_${String(id).slice(0, 8)}`; |
| } |
| } |
|
|
| if ( |
| process.env.STRIPE_PUBLISHABLE_KEY && |
| org.isTrailing && |
| (await this._integrationService.checkPreviousConnections( |
| org.id, |
| String(id) |
| )) |
| ) { |
| throw new HttpException('', 412); |
| } |
|
|
| const createUpdate = |
| await this._integrationService.createOrUpdateIntegration( |
| additionalSettings, |
| !!integrationProvider.oneTimeToken, |
| org.id, |
| validName.trim(), |
| picture, |
| 'social', |
| String(id), |
| integration, |
| accessToken, |
| refreshToken, |
| expiresIn, |
| username, |
| refresh ? false : integrationProvider.isBetweenSteps, |
| body.refresh, |
| +body.timezone, |
| details |
| ? AuthService.fixedEncryption(details) |
| : integrationProvider.customFields |
| ? AuthService.fixedEncryption( |
| Buffer.from(body.code, 'base64').toString() |
| ) |
| : integrationProvider.isChromeExtension |
| ? AuthService.fixedEncryption( |
| Buffer.from(body.code, 'base64').toString() |
| ) |
| : undefined |
| ); |
|
|
| this._refreshIntegrationService |
| .startRefreshWorkflow(org.id, createUpdate.id, integrationProvider) |
| .catch((err) => { |
| console.log(err); |
| }); |
|
|
| |
| let pages: any[] = []; |
| if (integrationProvider.isBetweenSteps && !refresh) { |
| try { |
| |
| const fetchMethod = |
| 'pages' in integrationProvider |
| ? 'pages' |
| : 'companies' in integrationProvider |
| ? 'companies' |
| : null; |
|
|
| if (fetchMethod) { |
| |
| pages = await integrationProvider[fetchMethod](accessToken); |
| } |
| } catch (err) { |
| console.log('Failed to fetch pages:', err); |
| } |
| } |
|
|
| const webhookUrl = await ioRedis.get(`webhookUrl:${body.state}`); |
| if (webhookUrl) { |
| try { |
| await fetch(webhookUrl, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| params: AuthService.signJWT({ |
| apiKey: org.apiKey, |
| }), |
| }), |
| }); |
| } catch (err) {} |
|
|
| await ioRedis.del(`webhookUrl:${body.state}`); |
| } |
|
|
| const returnURL = await ioRedis.get(`redirect:${body.state}`); |
| if (returnURL) { |
| await ioRedis.del(`redirect:${body.state}`); |
| } |
|
|
| const extensionToken = integrationProvider.isChromeExtension |
| ? AuthService.signJWT({ |
| integrationId: createUpdate.id, |
| organizationId: org.id, |
| internalId: String(id), |
| provider: integration, |
| }) |
| : undefined; |
|
|
| |
| |
| |
| const { |
| token: _token, |
| refreshToken: _refreshToken, |
| customInstanceDetails: _customInstanceDetails, |
| ...safeIntegration |
| } = createUpdate as any; |
|
|
| return { |
| ...safeIntegration, |
| onboarding: onboarding === 'true', |
| pages, |
| ...(returnURL ? { returnURL } : {}), |
| ...(extensionToken ? { extensionToken } : {}), |
| }; |
| } |
|
|
| @Post('/public/provider/:id/connect') |
| async saveProviderPage(@Param('id') id: string, @Body() body: any) { |
| if (!body.state) { |
| throw new Error('Invalid state'); |
| } |
|
|
| const organization = await ioRedis.get(`organization:${body.state}`); |
| if (!organization) { |
| throw new Error('Organization not found'); |
| } |
|
|
| const org = await this._organizationService.getOrgById(organization); |
|
|
| return this._integrationService.saveProviderPage(org.id, id, body); |
| } |
|
|
| @Post('/extension-refresh') |
| async extensionRefreshCookies( |
| @Body() body: { jwt: string; cookies: string } |
| ) { |
| let payload: any; |
| try { |
| payload = AuthService.verifyJWT(body.jwt); |
| } catch { |
| throw new HttpException('Invalid token', 401); |
| } |
|
|
| const { integrationId, organizationId, internalId, provider } = payload; |
| if (!integrationId || !organizationId || !internalId || !provider) { |
| throw new HttpException('Invalid token payload', 400); |
| } |
|
|
| const integration = await this._integrationService.getIntegrationById( |
| organizationId, |
| integrationId |
| ); |
| if (!integration || integration.internalId !== internalId) { |
| throw new HttpException('Integration not found', 404); |
| } |
|
|
| const integrationProvider = |
| this._integrationManager.getSocialIntegration(provider); |
| if (!integrationProvider?.isChromeExtension) { |
| throw new HttpException('Not a Chrome extension integration', 400); |
| } |
|
|
| const authResult = await integrationProvider.authenticate({ |
| code: body.cookies, |
| codeVerifier: '', |
| }); |
|
|
| if (typeof authResult === 'string') { |
| throw new HttpException(authResult, 400); |
| } |
|
|
| if (String(authResult.id) !== String(integration.internalId)) { |
| await this._integrationService.refreshNeeded( |
| organizationId, |
| integrationId |
| ); |
| return { success: false, reason: 'account_mismatch' }; |
| } |
|
|
| await this._integrationService.createOrUpdateIntegration( |
| undefined, |
| false, |
| organizationId, |
| integration.name, |
| undefined, |
| 'social', |
| integration.internalId, |
| integration.providerIdentifier, |
| authResult.accessToken, |
| '', |
| authResult.expiresIn, |
| undefined, |
| false, |
| undefined, |
| undefined, |
| AuthService.signJWT( |
| JSON.parse(Buffer.from(body.cookies, 'base64').toString()) |
| ) |
| ); |
|
|
| return { success: true }; |
| } |
| } |
|
|