Skip to content
Open
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
112 changes: 109 additions & 3 deletions packages/server/lib/controllers/connection/postConnection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as z from 'zod';

import db from '@nangohq/database';
import { logContextGetter } from '@nangohq/logs';
import { defaultOperationExpiration, logContextGetter } from '@nangohq/logs';
import {
EndUserMapper,
buildTagsFromEndUser,
Expand All @@ -27,7 +27,8 @@ import {
connectionTagsSchema,
endUserSchema
} from '../../helpers/validation.js';
import { connectionCreated, connectionCreationStartCapCheck, connectionRefreshSuccess } from '../../hooks/hooks.js';
import { validateConnection } from '../../hooks/connection/on/validate-connection.js';
import { connectionCreated, connectionCreationStartCapCheck, connectionRefreshSuccess, testConnectionCredentials } from '../../hooks/hooks.js';
import { asyncWrapper } from '../../utils/asyncWrapper.js';

import type { AuthOperationType, ConnectionConfig, ConnectionUpsertResponse, EndUser, PostPublicConnection, ProviderGithubApp } from '@nangohq/types';
Expand Down Expand Up @@ -141,6 +142,20 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
return;
}

const logCtx = await logContextGetter.create(
{
operation: { type: 'auth', action: 'create_connection' },
meta: { authType: 'connection_api' },
expiresAt: defaultOperationExpiration.auth()
},
{ account, environment }
);
await logCtx.enrichOperation({
integrationId: integration.id!,
integrationName: integration.unique_key,
providerName
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

you can pass all this integration info to create. no need to use enrichOperation


let updatedConnection: ConnectionUpsertResponse | undefined;

const connCreatedHook = (res: ConnectionUpsertResponse) => {
Expand Down Expand Up @@ -187,13 +202,33 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
}
case 'API_KEY':
case 'BASIC': {
// the testconnection only works with API_KEY, BASIC and TBA from this list
const connectionConfig = body.connection_config || {};
const connectionResponse = await testConnectionCredentials({
config: integration,
connectionConfig,
connectionId,
credentials: body.credentials,
provider,
logCtx
});
if (connectionResponse.isErr()) {
void logCtx.error('Connection test failed', {
error: connectionResponse.error,
providerConfigKey: body.provider_config_key
});
await logCtx.failed();
res.status(400).send({ error: { code: 'connection_test_failed', message: connectionResponse.error.message } });
return;
}

const [imported] = await connectionService.importApiAuthConnection({
connectionId,
providerConfigKey: body.provider_config_key,
metadata: body.metadata || {},
environment,
credentials: body.credentials,
connectionConfig: body.connection_config || {},
connectionConfig,
connectionCreatedHook: connCreatedHook,
tags: mergedTags
});
Expand All @@ -215,6 +250,11 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
connectionConfig
});
if (credentialsRes.isErr()) {
void logCtx.error('GitHub App credentials creation failed (APP)', {
error: credentialsRes.error,
providerConfigKey: body.provider_config_key
});
await logCtx.failed();
res.status(500).send({ error: { code: 'server_error', message: credentialsRes.error.message } });
return;
}
Expand Down Expand Up @@ -248,6 +288,11 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
connectionConfig
});
if (credentialsRes.isErr()) {
void logCtx.error('GitHub (App OAuth) credentials creation failed', {
error: credentialsRes.error,
providerConfigKey: body.provider_config_key
});
await logCtx.failed();
res.status(500).send({ error: { code: 'server_error', message: credentialsRes.error.message } });
return;
}
Expand All @@ -270,13 +315,34 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
}
case 'TBA': {
if (!body.connection_config || !body.connection_config['accountId']) {
void logCtx.error('Missing accountId in connection_config for TBA');
await logCtx.failed();
res.status(400).send({
error: { code: 'invalid_body', message: 'Missing accountId in connection_config. This is required to create a TBA connection.' }
});

return;
}

const connectionConfig = body.connection_config || {};
const connectionResponse = await testConnectionCredentials({
config: integration,
connectionConfig,
connectionId,
credentials: body.credentials,
provider,
logCtx
});
if (connectionResponse.isErr()) {
void logCtx.error('Connection test failed (TBA)', {
error: connectionResponse.error,
providerConfigKey: body.provider_config_key
});
await logCtx.failed();
res.status(400).send({ error: { code: 'connection_test_failed', message: connectionResponse.error.message } });
return;
}

const [imported] = await connectionService.upsertAuthConnection({
connectionId,
providerConfigKey: body.provider_config_key,
Expand Down Expand Up @@ -316,6 +382,8 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
}
default:
// Missing Bill, Signature, JWT, TwoStep, AppStore
void logCtx.error('Unsupported auth type for connection API', { authMode: provider.auth_mode });
await logCtx.failed();
res.status(400).send({ error: { code: 'invalid_body', message: `Unsupported auth type ${provider.auth_mode}` } });
return;
}
Expand All @@ -326,10 +394,39 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
}

if (!updatedConnection) {
void logCtx.error('Connection creation returned no result', { providerConfigKey: body.provider_config_key });
await logCtx.failed();
res.status(500).send({ error: { code: 'server_error', message: `Failed to create connection` } });
return;
}

const customValidationResponse = await validateConnection({
connection: updatedConnection.connection,
config: integration,
account,
logCtx
});

if (customValidationResponse.isErr()) {
if (updatedConnection.operation === 'creation') {
// since this is a new invalid connection, delete it with no trace of it
await connectionService.hardDelete(updatedConnection.connection.id);
}

const payload = customValidationResponse.error?.payload;
const message = typeof payload['message'] === 'string' ? payload['message'] : 'Connection failed validation';

void logCtx.error('Connection failed custom validation', { error: customValidationResponse.error });
await logCtx.failed();
res.status(400).send({
error: {
code: 'connection_validation_failed',
message
}
});
return;
}

let endUser: EndUser | undefined;
if (body.end_user) {
await db.knex.transaction(async (trx) => {
Expand All @@ -340,6 +437,8 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re
endUser: EndUserMapper.apiToEndUser(body.end_user!)
});
if (endUserRes.isErr()) {
void logCtx.error('Failed to upsert end user', { error: endUserRes.error });
await logCtx.failed();
res.status(500).send({ error: { code: 'server_error', message: 'Failed to update end user' } });
return;
}
Expand All @@ -356,6 +455,13 @@ export const postPublicConnection = asyncWrapper<PostPublicConnection>(async (re

const connection = encryptionManager.decryptConnection(updatedConnection.connection);

await logCtx.enrichOperation({
connectionId: updatedConnection.connection.id,
connectionName: updatedConnection.connection.connection_id
});
void logCtx.info('Connection creation was successful');
await logCtx.success();

res.status(201).send(
connectionFullToPublicApi({ data: connection, provider: providerName, activeLog: [], endUser: endUser ? EndUserMapper.to(endUser) : null })
);
Expand Down
1 change: 1 addition & 0 deletions packages/types/lib/connection/api/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type PostPublicConnection = Endpoint<{
end_user?: EndUserInput | undefined;
tags?: Tags | undefined;
};
Error: ApiError<'connection_test_failed'> | ApiError<'connection_validation_failed'>;
Success: ApiPublicConnectionFull;
}>;

Expand Down
Loading