Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,165 @@
import { FastifyInstance } from 'fastify'
import { StatusCodes } from 'http-status-codes'
import { initializeDatabase } from '../../../../src/app/database'
import { databaseConnection } from '../../../../src/app/database/database-connection'
import { setupServer } from '../../../../src/app/server'
import {
createMockSignInRequest,
createMockSignUpRequest,
} from '../../../helpers/mocks/authn'
let app: FastifyInstance | null = null
beforeAll(async () => {
await initializeDatabase({ runMigrations: false })
app = await setupServer()
})
beforeEach(async () => {
await databaseConnection().getRepository('flag').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('project').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('platform').createQueryBuilder().delete().execute()
await databaseConnection().getRepository('user').createQueryBuilder().delete().execute()
})
afterAll(async () => {
await databaseConnection().destroy()
await app?.close()
})
describe('Authentication API', () => {
describe('Sign up Endpoint', () => {
it('Adds new user', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.id).toHaveLength(21)
expect(responseBody?.created).toBeDefined()
expect(responseBody?.updated).toBeDefined()
expect(responseBody?.verified).toBe(true)
expect(responseBody?.email).toBe(mockSignUpRequest.email.toLocaleLowerCase().trim())
expect(responseBody?.firstName).toBe(mockSignUpRequest.firstName)
expect(responseBody?.lastName).toBe(mockSignUpRequest.lastName)
expect(responseBody?.trackEvents).toBe(mockSignUpRequest.trackEvents)
expect(responseBody?.newsLetter).toBe(mockSignUpRequest.newsLetter)
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.platformId).toBeDefined()
expect(responseBody?.externalId).toBe(null)
expect(responseBody?.projectId).toHaveLength(21)
expect(responseBody?.token).toBeDefined()
})
it('Creates new project for user', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
const responseBody = response?.json()
// assert
expect(response?.statusCode).toBe(StatusCodes.OK)
const project = await databaseConnection()
.getRepository('project')
.findOneBy({
id: responseBody.projectId,
})
expect(project?.ownerId).toBe(responseBody.id)
expect(project?.displayName).toBeDefined()
expect(project?.platformId).toBeDefined()
})
})
describe('Sign in Endpoint', () => {
it('Logs in existing users', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// First sign up the user
const signUpResponse = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
const signUpBody = signUpResponse?.json()
// Then try to sign in
const mockSignInRequest = createMockSignInRequest({
email: mockSignUpRequest.email,
password: mockSignUpRequest.password,
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
// assert
const responseBody = response?.json()
expect(response?.statusCode).toBe(StatusCodes.OK)
expect(responseBody?.id).toBe(signUpBody.id)
expect(responseBody?.email).toBe(mockSignUpRequest.email.toLowerCase().trim())
expect(responseBody?.firstName).toBe(mockSignUpRequest.firstName)
expect(responseBody?.lastName).toBe(mockSignUpRequest.lastName)
expect(responseBody?.trackEvents).toBe(mockSignUpRequest.trackEvents)
expect(responseBody?.newsLetter).toBe(mockSignUpRequest.newsLetter)
expect(responseBody?.password).toBeUndefined()
expect(responseBody?.status).toBe('ACTIVE')
expect(responseBody?.verified).toBe(true)
expect(responseBody?.platformId).toBe(signUpBody.platformId)
expect(responseBody?.externalId).toBe(null)
expect(responseBody?.projectId).toBe(signUpBody.projectId)
expect(responseBody?.token).toBeDefined()
})
it('Fails if password doesn\'t match', async () => {
// arrange
const mockSignUpRequest = createMockSignUpRequest()
// First sign up the user
await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-up',
body: mockSignUpRequest,
})
const mockSignInRequest = createMockSignInRequest({
email: mockSignUpRequest.email,
password: 'wrong password',
})
// act
const response = await app?.inject({
method: 'POST',
url: '/v1/authentication/sign-in',
body: mockSignInRequest,
})
// assert
expect(response?.statusCode).toBe(StatusCodes.UNAUTHORIZED)
const responseBody = response?.json()
expect(responseBody?.code).toBe('INVALID_CREDENTIALS')
})
})
})

View File

@@ -0,0 +1,94 @@
import bcrypt from 'bcrypt'
import { passwordHasher } from '../../../../src/app/authentication/lib/password-hasher'
const SCRYPT_SEPARATOR = '~'
describe('Password Hasher', () => {
const plainTextPassword = 'password123'
describe('hash', () => {
it('should not produce the same hash for the same password', async () => {
const hashedPassword1 = await bcrypt.hash(plainTextPassword, 10)
const hashedPassword2 = await bcrypt.hash(plainTextPassword, 10)
expect(hashedPassword1).not.toBe(hashedPassword2)
})
it('should verify hashed password correctly', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const result = await bcrypt.compare(plainTextPassword, hashedPassword)
expect(result).toBe(true)
})
it('should fail to verify incorrect password', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const incorrectPassword = 'incorrectPassword'
const result = await bcrypt.compare(incorrectPassword, hashedPassword)
expect(result).toBe(false)
})
})
describe('compare', () => {
it('should return true for identical bcrypt passwords', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const result = await passwordHasher.compare(
plainTextPassword,
hashedPassword,
)
expect(result).toBe(true)
})
it('should return false for different bcrypt passwords', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const differentPassword = 'differentPassword'
const result = await passwordHasher.compare(
differentPassword,
hashedPassword,
)
expect(result).toBe(false)
})
it('should return false for empty password bcrypt comparison', async () => {
const hashedPassword = await bcrypt.hash(plainTextPassword, 10)
const result = await passwordHasher.compare('', hashedPassword)
expect(result).toBe(false)
})
it('should return false for empty hash comparison', async () => {
const result = await passwordHasher.compare(plainTextPassword, '')
expect(result).toBe(false)
})
it('should return false for both empty password and hash', async () => {
const result = await passwordHasher.compare('', '')
expect(result).toBe(false)
})
})
describe('compare - Scrypt', () => {
const plainTextPassword = 'BusyBeaver$LOL99'
const salt = 'sPtDhWcd1MfdAw=='
const hashedPassword =
'iu1iqj6i6g9D7aBiE/Qdqv88GNnV/Ea67JK1kfLmzNgxsyCL8mhUxxI5VIHM9D+62xGHuZgjrfEBF+17wxyFIQ=='
it('should return true for identical scrypt passwords', async () => {
const result = await passwordHasher.compare(
plainTextPassword,
`$scrypt$${hashedPassword}${SCRYPT_SEPARATOR}${salt}`,
)
expect(result).toBe(true)
})
it('should return false for different scrypt passwords', async () => {
const differentPassword = 'differentPassword'
const result = await passwordHasher.compare(
differentPassword,
`$scrypt$${hashedPassword}${SCRYPT_SEPARATOR}${salt}`,
)
expect(result).toBe(false)
})
})
})