import express from 'express'; import type { INodeCredentialTestResult } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow'; import * as ResponseHelper from '@/ResponseHelper'; import config from '@/config'; import { EECredentialsController } from './credentials.controller.ee'; import { CredentialsService } from './credentials.service'; import type { ICredentialsDb } from '@/Interfaces'; import type { CredentialRequest, ListQuery } from '@/requests'; import { Container } from 'typedi'; import { InternalHooks } from '@/InternalHooks'; import { listQueryMiddleware } from '@/middlewares'; import { Logger } from '@/Logger'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { NamingService } from '@/services/naming.service'; export const credentialsController = express.Router(); credentialsController.use('/', EECredentialsController); /** * GET /credentials */ credentialsController.get( '/', listQueryMiddleware, ResponseHelper.send(async (req: ListQuery.Request) => { return CredentialsService.getMany(req.user, { listQueryOptions: req.listQueryOptions }); }), ); /** * GET /credentials/new * * Generate a unique credential name. */ credentialsController.get( '/new', ResponseHelper.send(async (req: CredentialRequest.NewName) => { const requestedName = req.query.name ?? config.getEnv('credentials.defaultName'); return { name: await Container.get(NamingService).getUniqueCredentialName(requestedName), }; }), ); /** * GET /credentials/:id */ credentialsController.get( '/:id(\\w+)', ResponseHelper.send(async (req: CredentialRequest.Get) => { const { id: credentialId } = req.params; const includeDecryptedData = req.query.includeData === 'true'; const sharing = await CredentialsService.getSharing( req.user, credentialId, { allowGlobalScope: true, globalScope: 'credential:read' }, ['credentials'], ); if (!sharing) { throw new NotFoundError(`Credential with ID "${credentialId}" could not be found.`); } const { credentials: credential } = sharing; const { data: _, ...rest } = credential; if (!includeDecryptedData) { return { ...rest }; } const decryptedData = CredentialsService.redact( CredentialsService.decrypt(credential), credential, ); return { data: decryptedData, ...rest }; }), ); /** * POST /credentials/test * * Test if a credential is valid. */ credentialsController.post( '/test', ResponseHelper.send(async (req: CredentialRequest.Test): Promise => { const { credentials } = req.body; const sharing = await CredentialsService.getSharing(req.user, credentials.id, { allowGlobalScope: true, globalScope: 'credential:read', }); const mergedCredentials = deepCopy(credentials); if (mergedCredentials.data && sharing?.credentials) { const decryptedData = CredentialsService.decrypt(sharing.credentials); mergedCredentials.data = CredentialsService.unredact(mergedCredentials.data, decryptedData); } return CredentialsService.test(req.user, mergedCredentials); }), ); /** * POST /credentials */ credentialsController.post( '/', ResponseHelper.send(async (req: CredentialRequest.Create) => { const newCredential = await CredentialsService.prepareCreateData(req.body); const encryptedData = CredentialsService.createEncryptedData(null, newCredential); const credential = await CredentialsService.save(newCredential, encryptedData, req.user); void Container.get(InternalHooks).onUserCreatedCredentials({ user: req.user, credential_name: newCredential.name, credential_type: credential.type, credential_id: credential.id, public_api: false, }); return credential; }), ); /** * PATCH /credentials/:id */ credentialsController.patch( '/:id(\\w+)', ResponseHelper.send(async (req: CredentialRequest.Update): Promise => { const { id: credentialId } = req.params; const sharing = await CredentialsService.getSharing( req.user, credentialId, { allowGlobalScope: true, globalScope: 'credential:update', }, ['credentials', 'role'], ); if (!sharing) { Container.get(Logger).info( 'Attempt to update credential blocked due to lack of permissions', { credentialId, userId: req.user.id, }, ); throw new NotFoundError( 'Credential to be updated not found. You can only update credentials owned by you', ); } if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:update')) { Container.get(Logger).info( 'Attempt to update credential blocked due to lack of permissions', { credentialId, userId: req.user.id, }, ); throw new UnauthorizedError('You can only update credentials owned by you'); } const { credentials: credential } = sharing; const decryptedData = CredentialsService.decrypt(credential); const preparedCredentialData = await CredentialsService.prepareUpdateData( req.body, decryptedData, ); const newCredentialData = CredentialsService.createEncryptedData( credentialId, preparedCredentialData, ); const responseData = await CredentialsService.update(credentialId, newCredentialData); if (responseData === null) { throw new NotFoundError(`Credential ID "${credentialId}" could not be found to be updated.`); } // Remove the encrypted data as it is not needed in the frontend const { data: _, ...rest } = responseData; Container.get(Logger).verbose('Credential updated', { credentialId }); return { ...rest }; }), ); /** * DELETE /credentials/:id */ credentialsController.delete( '/:id(\\w+)', ResponseHelper.send(async (req: CredentialRequest.Delete) => { const { id: credentialId } = req.params; const sharing = await CredentialsService.getSharing( req.user, credentialId, { allowGlobalScope: true, globalScope: 'credential:delete', }, ['credentials', 'role'], ); if (!sharing) { Container.get(Logger).info( 'Attempt to delete credential blocked due to lack of permissions', { credentialId, userId: req.user.id, }, ); throw new NotFoundError( 'Credential to be deleted not found. You can only removed credentials owned by you', ); } if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:delete')) { Container.get(Logger).info( 'Attempt to delete credential blocked due to lack of permissions', { credentialId, userId: req.user.id, }, ); throw new UnauthorizedError('You can only remove credentials owned by you'); } const { credentials: credential } = sharing; await CredentialsService.delete(credential); return true; }), );