diff --git a/packages/backend/src/controllers/api/v1/connections/generate-auth-url.js b/packages/backend/src/controllers/api/v1/connections/generate-auth-url.js new file mode 100644 index 00000000..167190ce --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/generate-auth-url.js @@ -0,0 +1,14 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + let connection = await request.currentUser + .$relatedQuery('connections') + .findOne({ + id: request.params.connectionId, + }) + .throwIfNotFound(); + + connection = await connection.generateAuthUrl(); + + renderObject(response, connection); +}; diff --git a/packages/backend/src/controllers/api/v1/connections/generate-auth-url.test.js b/packages/backend/src/controllers/api/v1/connections/generate-auth-url.test.js new file mode 100644 index 00000000..8dee64e3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/generate-auth-url.test.js @@ -0,0 +1,90 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; + +describe('POST /api/v1/connections/:connectionId/auth-url', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + + await createPermission({ + action: 'create', + subject: 'Connection', + roleId: currentUser.roleId, + conditions: ['isCreator'], + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should generate auth url for the connection', async () => { + const connection = await createConnection({ + userId: currentUser.id, + key: 'gitlab', + formattedData: { + clientId: 'CLIENT_ID', + oAuthRedirectUrl: 'http://localhost:3001/app/gitlab/connections/add', + }, + verified: false, + }); + + const response = await request(app) + .post(`/api/v1/connections/${connection.id}/auth-url`) + .set('Authorization', token) + .expect(200); + + expect(response.body.data).toStrictEqual({ + url: expect.stringContaining('https://gitlab.com/oauth/authorize?'), + }); + + expect(response.body.data).toStrictEqual({ + url: expect.stringContaining('client_id=CLIENT_ID'), + }); + + expect(response.body.data).toStrictEqual({ + url: expect.stringContaining( + `redirect_uri=${encodeURIComponent( + 'http://localhost:3001/app/gitlab/connections/add' + )}` + ), + }); + }); + + it(`should return internal server error response for invalid connection data`, async () => { + const connection = await createConnection({ + userId: currentUser.id, + key: 'gitlab', + formattedData: { + instanceUrl: 123, + }, + verified: false, + }); + + await request(app) + .post(`/api/v1/connections/${connection.id}/auth-url`) + .set('Authorization', token) + .expect(500); + }); + + it('should return not found response for not existing connection UUID', async () => { + const notExistingConnectionUUID = Crypto.randomUUID(); + + await request(app) + .post(`/api/v1/connections/${notExistingConnectionUUID}/auth-url`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await request(app) + .post('/api/v1/connections/invalidConnectionUUID/auth-url') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js index 4dfe49a4..8d09f584 100644 --- a/packages/backend/src/graphql/mutation-resolvers.js +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -2,7 +2,6 @@ import createConnection from './mutations/create-connection.js'; import createUser from './mutations/create-user.ee.js'; import deleteFlow from './mutations/delete-flow.js'; import duplicateFlow from './mutations/duplicate-flow.js'; -import generateAuthUrl from './mutations/generate-auth-url.js'; import resetConnection from './mutations/reset-connection.js'; import updateConnection from './mutations/update-connection.js'; import updateFlowStatus from './mutations/update-flow-status.js'; @@ -16,6 +15,7 @@ import verifyConnection from './mutations/verify-connection.js'; import createFlow from './mutations/create-flow.js'; import deleteCurrentUser from './mutations/delete-current-user.ee.js'; import updateCurrentUser from './mutations/update-current-user.js'; +import generateAuthUrl from './mutations/generate-auth-url.js'; const mutationResolvers = { createConnection, diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js index 1fff9d88..a603cbe4 100644 --- a/packages/backend/src/helpers/authorization.js +++ b/packages/backend/src/helpers/authorization.js @@ -101,6 +101,10 @@ const authorizationList = { action: 'create', subject: 'Connection', }, + 'POST /api/v1/connections/:connectionId/auth-url': { + action: 'create', + subject: 'Connection', + }, }; export const authorizeUser = async (request, response, next) => { diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js index 54819fad..22371069 100644 --- a/packages/backend/src/models/connection.js +++ b/packages/backend/src/models/connection.js @@ -238,6 +238,17 @@ class Connection extends Base { return app.auth.verifyWebhook($); } + + async generateAuthUrl() { + const app = await App.findOneByKey(this.key); + const $ = await globalVariable({ connection: this, app }); + + await app.auth.generateAuthUrl($); + + const url = this.formattedData.url; + + return { url }; + } } export default Connection; diff --git a/packages/backend/src/routes/api/v1/connections.js b/packages/backend/src/routes/api/v1/connections.js index 71356038..458e9b53 100644 --- a/packages/backend/src/routes/api/v1/connections.js +++ b/packages/backend/src/routes/api/v1/connections.js @@ -5,6 +5,7 @@ import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js import testConnectionAction from '../../../controllers/api/v1/connections/test-connection.js'; import verifyConnectionAction from '../../../controllers/api/v1/connections/verify-connection.js'; import deleteConnectionAction from '../../../controllers/api/v1/connections/delete-connection.js'; +import generateAuthUrlAction from '../../../controllers/api/v1/connections/generate-auth-url.js'; import resetConnectionAction from '../../../controllers/api/v1/connections/reset-connection.js'; const router = Router(); @@ -37,6 +38,13 @@ router.post( resetConnectionAction ); +router.post( + '/:connectionId/auth-url', + authenticateUser, + authorizeUser, + generateAuthUrlAction +); + router.post( '/:connectionId/verify', authenticateUser,