diff --git a/packages/backend/src/controllers/api/v1/apps/get-app.js b/packages/backend/src/controllers/api/v1/apps/get-app.js new file mode 100644 index 00000000..ccac3bd3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-app.js @@ -0,0 +1,8 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const app = await App.findOneByKey(request.params.appKey); + + renderObject(response, app); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-app.test.js b/packages/backend/src/controllers/api/v1/apps/get-app.test.js new file mode 100644 index 00000000..d6c2cc2d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-app.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getAppMock from '../../../../../test/mocks/rest/api/v1/apps/get-app.js'; + +describe('GET /api/v1/apps/:appKey', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = createAuthTokenByUserId(currentUser.id); + }); + + it('should return the app info', async () => { + const response = await request(app) + .get('/api/v1/apps/github') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppMock('github'); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/helpers/error-handler.js b/packages/backend/src/helpers/error-handler.js index e9131370..41ad4b39 100644 --- a/packages/backend/src/helpers/error-handler.js +++ b/packages/backend/src/helpers/error-handler.js @@ -9,12 +9,23 @@ const errorHandler = (error, request, response, next) => { response.status(404).end(); } + if (notFoundAppError(error)) { + response.status(404).end(); + } + if (error instanceof DataError) { response.status(400).end(); } logger.error(error.message + '\n' + error.stack); - response.status(error.statusCode || 500); + response.status(error.statusCode || 500).end(); +}; + +const notFoundAppError = (error) => { + return ( + error.message.includes('An application with the') || + error.message.includes("key couldn't be found.") + ); }; export default errorHandler; diff --git a/packages/backend/src/routes/api/v1/apps.js b/packages/backend/src/routes/api/v1/apps.js new file mode 100644 index 00000000..b5d23b4a --- /dev/null +++ b/packages/backend/src/routes/api/v1/apps.js @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import getAppAction from '../../../controllers/api/v1/apps/get-app.js'; + +const router = Router(); + +router.get('/:appKey', authenticateUser, asyncHandler(getAppAction)); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 996dfc99..0933ac94 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -8,6 +8,7 @@ import usersRouter from './api/v1/users.js'; import paymentRouter from './api/v1/payment.ee.js'; import appAuthClientsRouter from './api/v1/app-auth-clients.js'; import flowsRouter from './api/v1/flows.js'; +import appsRouter from './api/v1/apps.js'; import samlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; @@ -25,6 +26,7 @@ router.use('/api/v1/users', usersRouter); router.use('/api/v1/payment', paymentRouter); router.use('/api/v1/app-auth-clients', appAuthClientsRouter); router.use('/api/v1/flows', flowsRouter); +router.use('/api/v1/apps', appsRouter); router.use('/api/v1/admin/saml-auth-providers', samlAuthProvidersRouter); router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-app.js b/packages/backend/test/mocks/rest/api/v1/apps/get-app.js new file mode 100644 index 00000000..952fa7e1 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-app.js @@ -0,0 +1,604 @@ +const getAppMock = (appKey) => { + if (!appKey === 'github') return; + + return { + data: { + actions: [ + { + description: 'Creates a new issue.', + key: 'createIssue', + name: 'Create issue', + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + arguments: [ + { + key: 'repo', + label: 'Repo', + required: false, + source: { + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + name: 'getDynamicData', + type: 'query', + }, + type: 'dropdown', + variables: true, + }, + { + key: 'title', + label: 'Title', + required: true, + type: 'string', + variables: true, + }, + { + key: 'body', + label: 'Body', + required: true, + type: 'string', + variables: true, + }, + ], + key: 'chooseTrigger', + name: 'Set up action', + }, + { + key: 'testStep', + name: 'Test action', + }, + ], + }, + ], + apiBaseUrl: 'https://api.github.com', + auth: { + authenticationSteps: [ + { + arguments: [ + { + name: 'key', + value: '{key}', + }, + { + name: 'formattedData', + value: '{fields.all}', + }, + ], + name: 'createConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + name: 'generateAuthUrl', + type: 'mutation', + }, + { + arguments: [ + { + name: 'url', + value: '{generateAuthUrl.url}', + }, + ], + name: 'openAuthPopup', + type: 'openWithPopup', + }, + { + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + { + name: 'formattedData', + value: '{openAuthPopup.all}', + }, + ], + name: 'updateConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + name: 'verifyConnection', + type: 'mutation', + }, + ], + fields: [ + { + clickToCopy: true, + description: + 'When asked to input an OAuth callback or redirect URL in Github OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/github#oauth-redirect-url', + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + placeholder: null, + readOnly: true, + required: true, + type: 'string', + value: 'http://localhost:3000/app/github/connections/add', + }, + { + clickToCopy: false, + description: null, + docUrl: 'https://automatisch.io/docs/github#client-id', + key: 'consumerKey', + label: 'Client ID', + placeholder: null, + readOnly: false, + required: true, + type: 'string', + value: null, + }, + { + clickToCopy: false, + description: null, + docUrl: 'https://automatisch.io/docs/github#client-secret', + key: 'consumerSecret', + label: 'Client Secret', + placeholder: null, + readOnly: false, + required: true, + type: 'string', + value: null, + }, + ], + reconnectionSteps: [ + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + name: 'resetConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + { + name: 'formattedData', + value: '{fields.all}', + }, + ], + name: 'updateConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + name: 'generateAuthUrl', + type: 'mutation', + }, + { + arguments: [ + { + name: 'url', + value: '{generateAuthUrl.url}', + }, + ], + name: 'openAuthPopup', + type: 'openWithPopup', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + { + name: 'formattedData', + value: '{openAuthPopup.all}', + }, + ], + name: 'updateConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + name: 'verifyConnection', + type: 'mutation', + }, + ], + sharedAuthenticationSteps: [ + { + arguments: [ + { + name: 'key', + value: '{key}', + }, + { + name: 'appAuthClientId', + value: '{appAuthClientId}', + }, + ], + name: 'createConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + name: 'generateAuthUrl', + type: 'mutation', + }, + { + arguments: [ + { + name: 'url', + value: '{generateAuthUrl.url}', + }, + ], + name: 'openAuthPopup', + type: 'openWithPopup', + }, + { + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + { + name: 'formattedData', + value: '{openAuthPopup.all}', + }, + ], + name: 'updateConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + name: 'verifyConnection', + type: 'mutation', + }, + ], + sharedReconnectionSteps: [ + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + name: 'resetConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + { + name: 'appAuthClientId', + value: '{appAuthClientId}', + }, + ], + name: 'updateConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + name: 'generateAuthUrl', + type: 'mutation', + }, + { + arguments: [ + { + name: 'url', + value: '{generateAuthUrl.url}', + }, + ], + name: 'openAuthPopup', + type: 'openWithPopup', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + { + name: 'formattedData', + value: '{openAuthPopup.all}', + }, + ], + name: 'updateConnection', + type: 'mutation', + }, + { + arguments: [ + { + name: 'id', + value: '{connection.id}', + }, + ], + name: 'verifyConnection', + type: 'mutation', + }, + ], + }, + authDocUrl: 'https://automatisch.io/docs/apps/github/connection', + baseUrl: 'https://github.com', + beforeRequest: [null], + dynamicData: [ + { + key: 'listLabels', + name: 'List labels', + }, + { + key: 'listRepos', + name: 'List repos', + }, + ], + iconUrl: 'http://localhost:3000/apps/github/assets/favicon.svg', + key: 'github', + name: 'GitHub', + primaryColor: '000000', + supportsConnections: true, + triggers: [ + { + description: 'Triggers when a new issue is created', + key: 'newIssues', + name: 'New issues', + pollInterval: 15, + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + arguments: [ + { + key: 'repo', + label: 'Repo', + required: false, + source: { + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + name: 'getDynamicData', + type: 'query', + }, + type: 'dropdown', + variables: false, + }, + { + description: 'Defaults to any issue you can see.', + key: 'issueType', + label: 'Which types of issues should this trigger on?', + options: [ + { + label: 'Any issue you can see', + value: 'all', + }, + { + label: 'Only issues assigned to you', + value: 'assigned', + }, + { + label: 'Only issues created by you', + value: 'created', + }, + { + label: "Only issues you're mentioned in", + value: 'mentioned', + }, + { + label: "Only issues you're subscribed to", + value: 'subscribed', + }, + ], + required: true, + type: 'dropdown', + value: 'all', + variables: false, + }, + { + dependsOn: ['parameters.repo'], + description: + 'Only trigger on issues when this label is added.', + key: 'label', + label: 'Label', + required: false, + source: { + arguments: [ + { + name: 'key', + value: 'listLabels', + }, + { + name: 'parameters.repo', + value: '{parameters.repo}', + }, + ], + name: 'getDynamicData', + type: 'query', + }, + type: 'dropdown', + variables: false, + }, + ], + key: 'chooseTrigger', + name: 'Set up a trigger', + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + }, + { + description: 'Triggers when a new pull request is created', + key: 'newPullRequests', + name: 'New pull requests', + pollInterval: 15, + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + arguments: [ + { + key: 'repo', + label: 'Repo', + required: true, + source: { + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + name: 'getDynamicData', + type: 'query', + }, + type: 'dropdown', + variables: false, + }, + ], + key: 'chooseTrigger', + name: 'Set up a trigger', + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + }, + { + description: 'Triggers when a user stars a repository', + key: 'newStargazers', + name: 'New stargazers', + pollInterval: 15, + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + arguments: [ + { + key: 'repo', + label: 'Repo', + required: true, + source: { + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + name: 'getDynamicData', + type: 'query', + }, + type: 'dropdown', + variables: false, + }, + ], + key: 'chooseTrigger', + name: 'Set up a trigger', + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + }, + { + description: 'Triggers when a user watches a repository', + key: 'newWatchers', + name: 'New watchers', + pollInterval: 15, + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + arguments: [ + { + key: 'repo', + label: 'Repo', + required: true, + source: { + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + name: 'getDynamicData', + type: 'query', + }, + type: 'dropdown', + variables: false, + }, + ], + key: 'chooseTrigger', + name: 'Set up a trigger', + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + }, + ], + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getAppMock;