Executions
25 |├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── check-image-build.yml
│ └── release.yml
├── .gitignore
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── deployment-examples
├── config-with-runner
│ ├── backend-config.yaml
│ ├── docker-compose.yaml
│ └── runner-config.yaml
├── env-with-runner
│ └── docker-compose.yaml
└── minimal-with-config
│ ├── backend-config.yaml
│ └── docker-compose.yaml
├── docker-compose.yaml
├── release-notes.md
└── services
├── backend
├── Dockerfile
├── config
│ ├── config.yaml
│ └── main.go
├── database
│ ├── create_settings.go
│ ├── init.go
│ └── migrations
│ │ ├── 0_create_tables.go
│ │ ├── 1_flow_failure_pipelines.go
│ │ ├── 2_flow_failure_pipeline_id.go
│ │ ├── 3_execution_heartbeat.go
│ │ ├── 4_runner_api_url.go
│ │ ├── 5_runner_api_token.go
│ │ ├── 6_execution_schedule_every.go
│ │ ├── migrations.go
│ │ └── sample.md
├── functions
│ ├── admin_stats
│ │ ├── failed_executions.go
│ │ ├── flow_creation.go
│ │ ├── incoming_payloads.go
│ │ ├── project_creation.go
│ │ ├── started_executions.go
│ │ ├── user_registrations.go
│ │ ├── users_per_plan.go
│ │ └── users_per_role.go
│ ├── auth
│ │ ├── alertflowAutoRunnerToken.go
│ │ ├── jwt.go
│ │ ├── projectAutoRunnerToken.go
│ │ ├── projectToken.go
│ │ ├── runnerToken.go
│ │ ├── serviceToken.go
│ │ ├── userToken.go
│ │ └── validateTokenDB.go
│ ├── background_checks
│ │ ├── checkDisconnectedAutoRunners.go
│ │ ├── checkForFlowActionUpdates.go
│ │ ├── checkHangingExecutionSteps.go
│ │ ├── checkHangingExecutions.go
│ │ ├── checkScheduledExecutions.go
│ │ ├── main.go
│ │ └── scheduleFlowExecutions.go
│ ├── encryption
│ │ ├── execution_step_action_message.go
│ │ ├── params.go
│ │ └── payload.go
│ ├── flow
│ │ └── startExecution.go
│ ├── flow_stats
│ │ ├── executions.go
│ │ └── executions_trends.go
│ ├── gatekeeper
│ │ ├── checkAccountStatus.go
│ │ ├── checkAdmin.go
│ │ ├── checkProjectAccess.go
│ │ └── checkRequestUserProjectModifyRole.go
│ ├── httperror
│ │ ├── internalServerError.go
│ │ ├── statusBadRequest.go
│ │ ├── statusConflict.go
│ │ ├── statusNotFound.go
│ │ └── unauthorized.go
│ ├── project
│ │ ├── checkIfUserIsProjectMember.go
│ │ └── createAuditEntry.go
│ ├── runner
│ │ ├── generate_alertflow_auto_join_token.go
│ │ ├── generate_project_auto_join_token.go
│ │ └── generate_runner_token.go
│ └── user
│ │ └── sendUserNotification.go
├── go.mod
├── go.sum
├── handlers
│ ├── admins
│ │ ├── change_flow_status.go
│ │ ├── change_project_status.go
│ │ ├── change_runner_status.go
│ │ ├── create_user.go
│ │ ├── delete_token.go
│ │ ├── delete_user.go
│ │ ├── disable_user.go
│ │ ├── generate_service_token.go
│ │ ├── get_executions.go
│ │ ├── get_flows.go
│ │ ├── get_folders.go
│ │ ├── get_projects.go
│ │ ├── get_runners.go
│ │ ├── get_settings.go
│ │ ├── get_tokens.go
│ │ ├── get_users.go
│ │ ├── rotate-auto-join-token.go
│ │ ├── stats.go
│ │ ├── update_settings.go
│ │ ├── update_token.go
│ │ ├── update_user.go
│ │ └── user_send_admin_notify.go
│ ├── auths
│ │ ├── checkUserTaken.go
│ │ └── registerUser.go
│ ├── executions
│ │ ├── cancel.go
│ │ ├── create_step.go
│ │ ├── delete.go
│ │ ├── executions_attention.go
│ │ ├── get_all_executions.go
│ │ ├── get_execution.go
│ │ ├── get_pending.go
│ │ ├── get_running_executions.go
│ │ ├── get_step.go
│ │ ├── get_steps.go
│ │ ├── heartbeat.go
│ │ ├── schedule.go
│ │ ├── start.go
│ │ ├── update.go
│ │ └── update_step.go
│ ├── flows
│ │ ├── add_actions.go
│ │ ├── add_failure_pipeline.go
│ │ ├── add_failure_pipeline_actions.go
│ │ ├── change_maintenance.go
│ │ ├── create.go
│ │ ├── delete.go
│ │ ├── delete_action.go
│ │ ├── delete_failure_pipeline.go
│ │ ├── delete_failure_pipeline_action.go
│ │ ├── get_executions.go
│ │ ├── get_flow.go
│ │ ├── get_flows.go
│ │ ├── get_stats.go
│ │ ├── set_maintenance.go
│ │ ├── update.go
│ │ ├── update_actions.go
│ │ ├── update_actions_details.go
│ │ ├── update_failure_pipeline.go
│ │ └── update_failure_pipeline_actions.go
│ ├── folders
│ │ ├── create.go
│ │ ├── delete.go
│ │ ├── get_folder.go
│ │ ├── get_folders.go
│ │ └── update.go
│ ├── pages
│ │ └── get_settings.go
│ ├── projects
│ │ ├── accept_invite.go
│ │ ├── add_member.go
│ │ ├── create.go
│ │ ├── decline_invite.go
│ │ ├── delete.go
│ │ ├── delete_member.go
│ │ ├── delete_token.go
│ │ ├── edit_member.go
│ │ ├── generate_project_token.go
│ │ ├── get_audit_logs.go
│ │ ├── get_project.go
│ │ ├── get_projects.go
│ │ ├── get_runners.go
│ │ ├── get_tokens.go
│ │ ├── leave_project.go
│ │ ├── rotate-auto-join-token.go
│ │ ├── transfer_ownership.go
│ │ ├── update.go
│ │ └── update_token.go
│ ├── runners
│ │ ├── busy.go
│ │ ├── create.go
│ │ ├── delete.go
│ │ ├── edit.go
│ │ ├── get_links.go
│ │ ├── get_runners.go
│ │ ├── heartbeat.go
│ │ ├── register.go
│ │ └── set_actions.go
│ ├── tokens
│ │ ├── delete_runner_token.go
│ │ ├── generate.go
│ │ ├── refresh.go
│ │ ├── update.go
│ │ ├── validate.go
│ │ └── validate_service_token.go
│ └── users
│ │ ├── archive_notification.go
│ │ ├── changePassword.go
│ │ ├── change_details.go
│ │ ├── delete.go
│ │ ├── details.go
│ │ ├── disable.go
│ │ ├── get_notifications.go
│ │ ├── get_stats.go
│ │ ├── read_notification.go
│ │ ├── unarchive_notification.go
│ │ ├── unread_notification.go
│ │ └── welcomed.go
├── main.go
├── middlewares
│ ├── admin.go
│ ├── auth.go
│ ├── mixed.go
│ └── runner.go
├── pkg
│ └── models
│ │ ├── audit.go
│ │ ├── auth.go
│ │ ├── execution_steps.go
│ │ ├── executions.go
│ │ ├── flows.go
│ │ ├── folders.go
│ │ ├── notifications.go
│ │ ├── project_members.go
│ │ ├── projects.go
│ │ ├── runners.go
│ │ ├── settings.go
│ │ ├── stats.go
│ │ ├── tokens.go
│ │ └── users.go
└── router
│ ├── admin.go
│ ├── auth.go
│ ├── executions.go
│ ├── flows.go
│ ├── folders.go
│ ├── health.go
│ ├── main.go
│ ├── page.go
│ ├── projects.go
│ ├── runners.go
│ ├── token.go
│ └── user.go
└── frontend
├── .env
├── .npmrc
├── Dockerfile
├── app
├── admin
│ ├── executions
│ │ └── page.tsx
│ ├── flows
│ │ └── page.tsx
│ ├── page-settings
│ │ └── page.tsx
│ ├── projects
│ │ └── page.tsx
│ ├── runners
│ │ └── page.tsx
│ └── users
│ │ └── page.tsx
├── auth
│ ├── login
│ │ └── page.tsx
│ └── signup
│ │ └── page.tsx
├── error.tsx
├── flows
│ ├── [id]
│ │ ├── execution
│ │ │ └── [executionID]
│ │ │ │ ├── loading.tsx
│ │ │ │ └── page.tsx
│ │ └── page.tsx
│ └── page.tsx
├── layout.tsx
├── maintenance
│ ├── layout.tsx
│ └── page.tsx
├── page.tsx
├── profile
│ └── page.tsx
├── projects
│ ├── [id]
│ │ └── page.tsx
│ └── page.tsx
├── providers.tsx
├── runners
│ └── page.tsx
└── sw.ts
├── components
├── admin
│ ├── flows
│ │ ├── heading.tsx
│ │ └── list.tsx
│ ├── projects
│ │ ├── heading.tsx
│ │ └── list.tsx
│ ├── runners
│ │ └── heading.tsx
│ ├── settings
│ │ ├── heading.tsx
│ │ └── list.tsx
│ └── users
│ │ ├── heading.tsx
│ │ └── list.tsx
├── auth
│ ├── login.tsx
│ ├── loginPage.tsx
│ └── signupPage.tsx
├── cn
│ └── cn.ts
├── dashboard
│ ├── home.tsx
│ └── stats.tsx
├── error
│ └── ErrorCard.tsx
├── executions
│ ├── execution
│ │ ├── adminExecutionActions.tsx
│ │ ├── adminStepActions.tsx
│ │ ├── details.tsx
│ │ ├── execution.tsx
│ │ ├── executionStepsAccordion.tsx
│ │ └── executionStepsTable.tsx
│ ├── executions.tsx
│ ├── executionsCompact.tsx
│ ├── executionsList.tsx
│ └── executionsTable.tsx
├── flows
│ ├── flow
│ │ ├── actions.tsx
│ │ ├── details.tsx
│ │ ├── heading.tsx
│ │ ├── settings.tsx
│ │ ├── stats.tsx
│ │ └── tabs.tsx
│ ├── heading.tsx
│ └── list.tsx
├── footer
│ └── footer.tsx
├── icons.tsx
├── magicui
│ ├── particles.tsx
│ └── sparkles-text.tsx
├── modals
│ ├── actions
│ │ ├── add.tsx
│ │ ├── copy.tsx
│ │ ├── delete.tsx
│ │ ├── edit.tsx
│ │ ├── editDetails.tsx
│ │ ├── transferCopy.tsx
│ │ └── upgrade.tsx
│ ├── admin
│ │ ├── createUser.tsx
│ │ ├── deleteUser.tsx
│ │ ├── editUser.tsx
│ │ └── rotateSharedAutoJoinToken.tsx
│ ├── executions
│ │ ├── delete.tsx
│ │ └── schedule.tsx
│ ├── failurePipelines
│ │ ├── create.tsx
│ │ ├── delete.tsx
│ │ └── edit.tsx
│ ├── flows
│ │ ├── changeMaintenance.tsx
│ │ ├── changeStatus.tsx
│ │ ├── copy.tsx
│ │ ├── create.tsx
│ │ ├── delete.tsx
│ │ └── edit.tsx
│ ├── folders
│ │ ├── create.tsx
│ │ ├── delete.tsx
│ │ └── update.tsx
│ ├── projects
│ │ ├── cell-wrapper.tsx
│ │ ├── changeStatus.tsx
│ │ ├── changeTokenStatus.tsx
│ │ ├── cn.ts
│ │ ├── create.tsx
│ │ ├── createToken.tsx
│ │ ├── delete.tsx
│ │ ├── deleteToken.tsx
│ │ ├── edit.tsx
│ │ ├── editMember.tsx
│ │ ├── leave.tsx
│ │ ├── members.tsx
│ │ ├── removeMember.tsx
│ │ ├── rotateAutoJoinToken.tsx
│ │ ├── transferOwnership.tsx
│ │ └── user-cell.tsx
│ ├── runner
│ │ ├── changeStatus.tsx
│ │ ├── create.tsx
│ │ ├── delete.tsx
│ │ ├── details.tsx
│ │ └── edit.tsx
│ ├── tokens
│ │ ├── deleteRunnerToken.tsx
│ │ └── edit.tsx
│ └── user
│ │ ├── changePassword.tsx
│ │ ├── delete.tsx
│ │ ├── disable.tsx
│ │ └── welcome.tsx
├── navbar.tsx
├── primitives.ts
├── projects
│ ├── heading.tsx
│ ├── list.tsx
│ ├── project.tsx
│ └── project
│ │ ├── RunnerDetails.tsx
│ │ ├── tables
│ │ ├── AuditTable.tsx
│ │ ├── TokensTable.tsx
│ │ └── UserTable.tsx
│ │ └── tabs.tsx
├── reloader
│ └── Reloader.tsx
├── runners
│ ├── heading.tsx
│ └── list.tsx
├── search
│ ├── component-files.ts
│ ├── data.ts
│ ├── mock-data.ts
│ ├── new-chip.tsx
│ ├── popover.tsx
│ ├── regex-constants.ts
│ ├── search.tsx
│ ├── sort.ts
│ └── use-update-effect.ts
├── steps
│ ├── minimal-row-steps.tsx
│ └── row-steps.tsx
├── theme-switch.tsx
└── user
│ ├── cell-wrapper.tsx
│ └── profile.tsx
├── config
├── fonts.ts
└── site.ts
├── eslint.config.mjs
├── lib
├── IconWrapper.tsx
├── auth
│ ├── checkTaken.ts
│ ├── deleteSession.ts
│ ├── login.ts
│ ├── signup.ts
│ └── updateSession.ts
├── fetch
│ ├── admin
│ │ ├── DELETE
│ │ │ ├── DeleteToken.ts
│ │ │ └── delete_user.ts
│ │ ├── POST
│ │ │ ├── CreateServiceToken.ts
│ │ │ ├── CreateUser.ts
│ │ │ └── sendUserNotification.ts
│ │ ├── PUT
│ │ │ ├── ChangeFlowStatus.ts
│ │ │ ├── ChangeProjectStatus.ts
│ │ │ ├── ChangeRunnerStatus.ts
│ │ │ ├── EditToken.ts
│ │ │ ├── RotateAutoJoinToken.ts
│ │ │ ├── UpdateSettings.ts
│ │ │ ├── UpdateUser.ts
│ │ │ └── UpdateUserState.ts
│ │ ├── executions.ts
│ │ ├── flows.ts
│ │ ├── folders.ts
│ │ ├── projects.ts
│ │ ├── runners.ts
│ │ ├── settings.ts
│ │ ├── stats.ts
│ │ ├── tokens.ts
│ │ └── users.ts
│ ├── executions
│ │ ├── DELETE
│ │ │ └── delete.ts
│ │ ├── PUT
│ │ │ ├── step_interact.ts
│ │ │ ├── update.ts
│ │ │ └── updateStep.ts
│ │ ├── all.ts
│ │ ├── attention.ts
│ │ ├── cancel.ts
│ │ ├── execution.ts
│ │ ├── running.ts
│ │ ├── schedule.ts
│ │ ├── start.ts
│ │ └── steps.ts
│ ├── flow
│ │ ├── DELETE
│ │ │ ├── DeleteAction.ts
│ │ │ ├── DeleteFailurePipeline.ts
│ │ │ ├── DeleteFailurePipelineAction.ts
│ │ │ └── DeleteFlow.ts
│ │ ├── POST
│ │ │ ├── AddFlowActions.ts
│ │ │ ├── AddFlowFailurePipeline.ts
│ │ │ ├── AddFlowFailurePipelineActions.ts
│ │ │ ├── CopyFlow.ts
│ │ │ └── CreateFlow.ts
│ │ ├── PUT
│ │ │ ├── ChangeFlowMaintenance.ts
│ │ │ ├── UpdateAction.ts
│ │ │ ├── UpdateActions.ts
│ │ │ ├── UpdateActionsDetails.ts
│ │ │ ├── UpdateFailurePipeline.ts
│ │ │ ├── UpdateFailurePipelineActions.ts
│ │ │ └── UpdateFlow.ts
│ │ ├── alerts.ts
│ │ ├── all.ts
│ │ ├── executions.ts
│ │ ├── flow.ts
│ │ └── stats.ts
│ ├── folder
│ │ ├── POST
│ │ │ └── create.ts
│ │ ├── all.ts
│ │ ├── delete.ts
│ │ ├── single.ts
│ │ └── update.ts
│ ├── page
│ │ └── settings.ts
│ ├── project
│ │ ├── DELETE
│ │ │ ├── DeleteProject.ts
│ │ │ ├── DeleteProjectToken.ts
│ │ │ ├── DeleteRunner.ts
│ │ │ ├── DeleteRunnerToken.ts
│ │ │ ├── leave.ts
│ │ │ └── removeProjectMember.ts
│ │ ├── POST
│ │ │ ├── AddProjectMember.ts
│ │ │ ├── CreateProject.ts
│ │ │ ├── CreateProjectToken.ts
│ │ │ └── CreateRunnerToken.ts
│ │ ├── PUT
│ │ │ ├── AcceptProjectInvite.ts
│ │ │ ├── ChangeProjectTokenStatus.ts
│ │ │ ├── DeclineProjectInvite.ts
│ │ │ ├── RotateAutoJoinToken.ts
│ │ │ ├── UpdateProject.ts
│ │ │ ├── editProjectMember.ts
│ │ │ └── transferOwnership.ts
│ │ ├── all.ts
│ │ ├── audit.ts
│ │ ├── data.ts
│ │ ├── runners.ts
│ │ └── tokens.ts
│ ├── runner
│ │ ├── GetRunnerFlowLinks.ts
│ │ ├── POST
│ │ │ └── AddRunner.ts
│ │ ├── PUT
│ │ │ └── Edit.ts
│ │ └── get.ts
│ ├── tokens
│ │ ├── update.ts
│ │ └── validate.ts
│ └── user
│ │ ├── DELETE
│ │ └── delete.ts
│ │ ├── PUT
│ │ ├── changeDetails.ts
│ │ ├── changePassword.ts
│ │ ├── disable.ts
│ │ └── welcomed.ts
│ │ ├── getDetails.ts
│ │ ├── notifications.ts
│ │ └── stats.ts
├── functions
│ ├── canEditProject.tsx
│ ├── executionStyles.tsx
│ ├── userExecutionStepStyle.tsx
│ └── userExecutionsStyle.tsx
├── logout.ts
├── setSession.ts
└── utils.ts
├── middleware.ts
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── android
│ ├── android-launchericon-144-144.png
│ ├── android-launchericon-192-192.png
│ ├── android-launchericon-48-48.png
│ ├── android-launchericon-512-512.png
│ ├── android-launchericon-72-72.png
│ └── android-launchericon-96-96.png
├── favicon.ico
├── images
│ ├── bg-gradient.png
│ ├── ef_logo.png
│ ├── ef_logo_512.png
│ └── full_dashboard.png
├── ios
│ ├── 100.png
│ ├── 1024.png
│ ├── 114.png
│ ├── 120.png
│ ├── 128.png
│ ├── 144.png
│ ├── 152.png
│ ├── 16.png
│ ├── 167.png
│ ├── 180.png
│ ├── 192.png
│ ├── 20.png
│ ├── 256.png
│ ├── 29.png
│ ├── 32.png
│ ├── 40.png
│ ├── 50.png
│ ├── 512.png
│ ├── 57.png
│ ├── 58.png
│ ├── 60.png
│ ├── 64.png
│ ├── 72.png
│ ├── 76.png
│ ├── 80.png
│ └── 87.png
├── manifest.json
└── windows11
│ ├── LargeTile.scale-100.png
│ ├── LargeTile.scale-125.png
│ ├── LargeTile.scale-150.png
│ ├── LargeTile.scale-200.png
│ ├── LargeTile.scale-400.png
│ ├── SmallTile.scale-100.png
│ ├── SmallTile.scale-125.png
│ ├── SmallTile.scale-150.png
│ ├── SmallTile.scale-200.png
│ ├── SmallTile.scale-400.png
│ ├── SplashScreen.scale-100.png
│ ├── SplashScreen.scale-125.png
│ ├── SplashScreen.scale-150.png
│ ├── SplashScreen.scale-200.png
│ ├── SplashScreen.scale-400.png
│ ├── Square150x150Logo.scale-100.png
│ ├── Square150x150Logo.scale-125.png
│ ├── Square150x150Logo.scale-150.png
│ ├── Square150x150Logo.scale-200.png
│ ├── Square150x150Logo.scale-400.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-20.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-30.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-36.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-40.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-44.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-60.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-64.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-72.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-80.png
│ ├── Square44x44Logo.altform-lightunplated_targetsize-96.png
│ ├── Square44x44Logo.altform-unplated_targetsize-16.png
│ ├── Square44x44Logo.altform-unplated_targetsize-20.png
│ ├── Square44x44Logo.altform-unplated_targetsize-24.png
│ ├── Square44x44Logo.altform-unplated_targetsize-256.png
│ ├── Square44x44Logo.altform-unplated_targetsize-30.png
│ ├── Square44x44Logo.altform-unplated_targetsize-32.png
│ ├── Square44x44Logo.altform-unplated_targetsize-36.png
│ ├── Square44x44Logo.altform-unplated_targetsize-40.png
│ ├── Square44x44Logo.altform-unplated_targetsize-44.png
│ ├── Square44x44Logo.altform-unplated_targetsize-48.png
│ ├── Square44x44Logo.altform-unplated_targetsize-60.png
│ ├── Square44x44Logo.altform-unplated_targetsize-64.png
│ ├── Square44x44Logo.altform-unplated_targetsize-72.png
│ ├── Square44x44Logo.altform-unplated_targetsize-80.png
│ ├── Square44x44Logo.altform-unplated_targetsize-96.png
│ ├── Square44x44Logo.scale-100.png
│ ├── Square44x44Logo.scale-125.png
│ ├── Square44x44Logo.scale-150.png
│ ├── Square44x44Logo.scale-200.png
│ ├── Square44x44Logo.scale-400.png
│ ├── Square44x44Logo.targetsize-16.png
│ ├── Square44x44Logo.targetsize-20.png
│ ├── Square44x44Logo.targetsize-24.png
│ ├── Square44x44Logo.targetsize-256.png
│ ├── Square44x44Logo.targetsize-30.png
│ ├── Square44x44Logo.targetsize-32.png
│ ├── Square44x44Logo.targetsize-36.png
│ ├── Square44x44Logo.targetsize-40.png
│ ├── Square44x44Logo.targetsize-44.png
│ ├── Square44x44Logo.targetsize-48.png
│ ├── Square44x44Logo.targetsize-60.png
│ ├── Square44x44Logo.targetsize-64.png
│ ├── Square44x44Logo.targetsize-72.png
│ ├── Square44x44Logo.targetsize-80.png
│ ├── Square44x44Logo.targetsize-96.png
│ ├── StoreLogo.scale-100.png
│ ├── StoreLogo.scale-125.png
│ ├── StoreLogo.scale-150.png
│ ├── StoreLogo.scale-200.png
│ ├── StoreLogo.scale-400.png
│ ├── Wide310x150Logo.scale-100.png
│ ├── Wide310x150Logo.scale-125.png
│ ├── Wide310x150Logo.scale-150.png
│ ├── Wide310x150Logo.scale-200.png
│ └── Wide310x150Logo.scale-400.png
├── styles
└── globals.css
├── tailwind.config.ts
├── tsconfig.json
├── types
└── index.ts
└── updateSessionInterval.ts
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: 'Bug: '
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Additional context**
27 | Add any other context about the problem here.
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: 'Feature: '
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/services/frontend" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 | labels:
13 | - "dependencies"
14 | - "frontend"
15 | target-branch: "develop"
16 |
17 | - package-ecosystem: "gomod" # See documentation for possible values
18 | directory: "/services/backend" # Location of package manifests
19 | schedule:
20 | interval: "weekly"
21 | labels:
22 | - "dependencies"
23 | - "backend"
24 | target-branch: "develop"
25 | groups:
26 | backend:
27 | patterns:
28 | - "*"
29 |
--------------------------------------------------------------------------------
/.github/workflows/check-image-build.yml:
--------------------------------------------------------------------------------
1 | name: Check Image Build
2 |
3 | on:
4 | pull_request:
5 | types: [opened, reopened, edited, synchronize]
6 | branches: [ "release/**", "develop" ]
7 | paths-ignore:
8 | - '.github/**'
9 | - '*.md'
10 | - 'deployment-examples/**'
11 |
12 | jobs:
13 | frontend:
14 | name: Check Standalone Frontend Build
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 |
20 | - name: Build Frontend Docker Image
21 | run: docker build . --file services/frontend/Dockerfile --tag justnz/exflow:frontend-test
22 | backend:
23 | name: Check Standalone Backend Build
24 | runs-on: ubuntu-latest
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v4
28 |
29 | - name: Build Backend Docker Image
30 | run: docker build . --file services/backend/Dockerfile --tag justnz/exflow:backend-test
31 | exflow:
32 | name: Check exFlow Build
33 | runs-on: ubuntu-latest
34 | steps:
35 | - name: Checkout code
36 | uses: actions/checkout@v4
37 |
38 | - name: Build exFlow Docker Image
39 | run: docker build . --file Dockerfile --tag justnz/exflow:test
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | services/frontend/node_modules
5 | services/frontend/.pnp
6 | services/frontend/.pnp.js
7 |
8 | # testing
9 | services/frontend/coverage
10 |
11 | # next.js
12 | services/frontend/.next/
13 | services/frontend/out/
14 |
15 | # production
16 | services/frontend/build
17 |
18 | # misc
19 | services/frontend/.DS_Store
20 | services/frontend/*.pem
21 |
22 | services/frontend/public/sw*
23 | services/frontend/public/swe-worker*
24 |
25 | # debug
26 | services/frontend/npm-debug.log*
27 | services/frontend/yarn-debug.log*
28 | services/frontend/yarn-error.log*
29 |
30 | # local env files
31 | .env*.local
32 |
33 | # typescript
34 | services/frontend/*.tsbuildinfo
35 | services/frontend/next-env.d.ts
36 |
37 | # If you prefer the allow list template instead of the deny list, see community template:
38 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
39 | #
40 | # Binaries for programs and plugins
41 | services/backend/*.exe
42 | services/backend/*.exe~
43 | services/backend/*.dll
44 | services/backend/*.so
45 | services/backend/*.dylib
46 |
47 | # Test binary, built with `go test -c`
48 | services/backend/*.test
49 |
50 | # Output of the go coverage tool, specifically when used with LiteIDE
51 | services/backend/*.out
52 |
53 | # Dependency directories (remove the comment below to include it)
54 | services/backend/vendor/
55 |
56 | # Go workspace file
57 | services/backend/go.work
58 |
59 | services/backend/.envrc
60 | services/backend/alertflow-backend
61 |
62 | services/backend/config.yaml
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/deployment-examples/config-with-runner/backend-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | log_level: info
3 | port: 8081
4 | database:
5 | server: db
6 | port: 5432
7 | name: postgres
8 | user: postgres
9 | password: postgres
10 | encryption:
11 | enabled: true
12 | key: 0AMutjk[Ga:a4.?0nLv|Sf[s?o~Q-RW>
13 | jwt:
14 | secret: TyO7,:`(!DcBF@tK!NU6l$AU+Dd`&FGtDS4Uj,)SDQ[rsEhB
15 | runner:
16 | shared_runner_secret: <`@8W!?vBl`3$O6@Po7rZ|G:U3=j}ek]
--------------------------------------------------------------------------------
/deployment-examples/config-with-runner/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | db:
5 | image: postgres:15
6 | environment:
7 | POSTGRES_DB: postgres
8 | POSTGRES_USER: postgres
9 | POSTGRES_PASSWORD: postgres
10 | volumes:
11 | - db_data:/var/lib/postgresql/data
12 | ports:
13 | - "5432:5432"
14 |
15 | exflow:
16 | image: justnz/exflow:latest
17 | depends_on:
18 | - db
19 | ports:
20 | - "3000:3000" # exFlow frontend
21 | - "8080:8080" # exFlow backend
22 | volumes:
23 | - ./backend-config.yaml:/etc/exflow/backend_config.yaml
24 | entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./exflow-backend --config /etc/exflow/backend_config.yaml & exec node /app/server.js"]
25 |
26 | runner:
27 | image: justnz/runner:latest
28 | depends_on:
29 | - exflow
30 | ports:
31 | - "8081:8081"
32 | volumes:
33 | - ./runner-config.yaml:/app/config/config.yaml
34 |
35 | volumes:
36 | db_data:
--------------------------------------------------------------------------------
/deployment-examples/config-with-runner/runner-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | log_level: info
4 | mode: master
5 |
6 | alertflow:
7 | enabled: false
8 |
9 | exflow:
10 | enabled: true
11 | url: http://exflow:8080
12 |
13 | plugins:
14 | - name: git
15 | version: v1.3.1
16 | - name: ansible
17 | version: v1.4.1
18 | - name: ssh
19 | version: v1.5.1
20 |
21 | api_endpoint:
22 | port: 8081
23 |
24 | runner:
25 | shared_runner_secret: <`@8W!?vBl`3$O6@Po7rZ|G:U3=j}ek]
26 |
--------------------------------------------------------------------------------
/deployment-examples/minimal-with-config/backend-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | log_level: info
3 | port: 8081
4 | database:
5 | server: db
6 | port: 5432
7 | name: postgres
8 | user: postgres
9 | password: postgres
10 | encryption:
11 | enabled: true
12 | key: 0AMutjk[Ga:a4.?0nLv|Sf[s?o~Q-RW>
13 | jwt:
14 | secret: TyO7,:`(!DcBF@tK!NU6l$AU+Dd`&FGtDS4Uj,)SDQ[rsEhB
--------------------------------------------------------------------------------
/deployment-examples/minimal-with-config/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | db:
5 | image: postgres:15
6 | environment:
7 | POSTGRES_DB: postgres
8 | POSTGRES_USER: postgres
9 | POSTGRES_PASSWORD: postgres
10 | volumes:
11 | - db_data:/var/lib/postgresql/data
12 | ports:
13 | - "5432:5432"
14 |
15 | exflow:
16 | image: justnz/exflow:latest
17 | depends_on:
18 | - db
19 | ports:
20 | - "3000:3000" # exFlow frontend
21 | - "8080:8080" # exFlow backend
22 | volumes:
23 | - ./backend-config.yaml:/etc/exflow/backend_config.yaml
24 | entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./exflow-backend --config /etc/exflow/backend_config.yaml & exec node /app/server.js"]
25 |
26 | volumes:
27 | db_data:
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | db:
5 | image: postgres:15
6 | environment:
7 | POSTGRES_DB: postgres
8 | POSTGRES_USER: postgres
9 | POSTGRES_PASSWORD: postgres
10 | volumes:
11 | - db_data:/var/lib/postgresql/data
12 | ports:
13 | - "5432:5432"
14 |
15 | exflow:
16 | image: justnz/exflow:latest
17 | depends_on:
18 | - db
19 | ports:
20 | - "3000:3000" # exFlow frontend
21 | - "8080:8080" # exFlow backend
22 | volumes:
23 | - ./config.yaml:/etc/exflow/backend_config.yaml
24 | # environment:
25 | # Adjust these if your config.yaml uses different DB settings
26 | # BACKEND_LOG_LEVEL: info
27 | # BACKEND_PORT: 8080
28 | # BACKEND_DATABASE_SERVER: db
29 | # BACKEND_DATABASE_PORT: 5432
30 | # BACKEND_DATABASE_NAME: postgres
31 | # BACKEND_DATABASE_USER: postgres
32 | # BACKEND_DATABASE_PASSWORD: postgres
33 | # BACKEND_ENCRYPTION_ENABLED: "true"
34 | # BACKEND_ENCRYPTION_KEY: "change-me"
35 | # BACKEND_JWT_SECRET: "change-me"
36 | entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./exflow-backend --config /etc/exflow/backend_config.yaml & exec node /app/server.js"]
37 |
38 | volumes:
39 | db_data:
--------------------------------------------------------------------------------
/release-notes.md:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 | ## [Version 1.3.1] - 2025-05-29
4 |
5 | ### ⚠️ Breaking Changes ⚠️
6 | With version 1.1.0 the config format has change to be compliant with the default yaml formatting.
7 | Please have a look at the [default config](https://github.com/v1Flows/exFlow/blob/develop/services/backend/config/config.yaml) and align to your current config accordingly.
8 |
9 | ### Added
10 | - nothing added
11 |
12 | ### Changed
13 | - nothing changes
14 |
15 | ### Fixed
16 | - dashboard and executions did not load when the amount of executions were greater than 500+. Implemented an limit, offset and status filter on the backend db select
17 |
18 | ### Known Issues
19 | - No known issues at this time.
20 |
21 | ---
22 |
23 | *Thank you for using exFlow!*
--------------------------------------------------------------------------------
/services/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.24-alpine as builder
2 |
3 | WORKDIR /backend
4 |
5 | COPY services/backend/go.mod services/backend/go.sum ./
6 | RUN go mod download
7 |
8 | COPY services/backend/ ./
9 |
10 | # Build
11 | RUN CGO_ENABLED=0 GOOS=linux go build -o /exflow-backend
12 |
13 | FROM alpine:3.12 as runner
14 | WORKDIR /app
15 |
16 | COPY --from=builder /exflow-backend /exflow-backend
17 |
18 | RUN mkdir /app/config
19 | COPY services/backend/config/config.yaml /etc/exflow/backend_config.yaml
20 |
21 | VOLUME [ "/etc/exflow" ]
22 |
23 | EXPOSE 8080
24 |
25 | CMD [ "/exflow-backend", "--config", "/etc/exflow/backend_config.yaml" ]
26 |
--------------------------------------------------------------------------------
/services/backend/config/config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | log_level: info
4 |
5 | port: 8080
6 |
7 | database:
8 | server: localhost
9 | port: 5432
10 | name: postgres
11 | user: postgres
12 | password: postgres
13 |
14 | encryption:
15 | enabled: true
16 | # maximum 32 characters
17 | key: null
18 |
19 | jwt:
20 | secret: null
21 |
22 | runner:
23 | shared_runner_secret: null
24 |
--------------------------------------------------------------------------------
/services/backend/database/create_settings.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 |
8 | functions_runner "github.com/v1Flows/exFlow/services/backend/functions/runner"
9 |
10 | log "github.com/sirupsen/logrus"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func createDefaultSettings(db *bun.DB) {
15 | ctx := context.Background()
16 |
17 | var settings models.Settings
18 | count, err := db.NewSelect().Model(&settings).Where("id = 1").ScanAndCount(ctx)
19 | if err != nil && count != 0 {
20 | panic(err)
21 | }
22 |
23 | if count == 0 {
24 | log.Info("No existing settings found. Creating default...")
25 | settings.ID = 1
26 | settings.SharedRunnerAutoJoinToken, err = functions_runner.GenerateExFlowAutoJoinToken(db)
27 | if err != nil {
28 | panic(err)
29 | }
30 |
31 | _, err := db.NewInsert().Model(&settings).Exec(ctx)
32 | if err != nil {
33 | panic(err)
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/services/backend/database/migrations/migrations.go:
--------------------------------------------------------------------------------
1 | package migrations
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/uptrace/bun"
7 | "github.com/uptrace/bun/migrate"
8 | )
9 |
10 | var Migrations = migrate.NewMigrations()
11 |
12 | func columnExists(ctx context.Context, db *bun.DB, table, column string) (bool, error) {
13 | exists, err := db.NewSelect().
14 | Table("information_schema.columns").
15 | Where("table_name = ? AND column_name = ?", table, column).
16 | Exists(ctx)
17 |
18 | return exists, err
19 | }
20 |
21 | func tableExists(ctx context.Context, db *bun.DB, table string) (bool, error) {
22 | exists, err := db.NewSelect().
23 | Table("information_schema.tables").
24 | Where("table_name = ?", table).
25 | Exists(ctx)
26 |
27 | return exists, err
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/functions/admin_stats/users_per_plan.go:
--------------------------------------------------------------------------------
1 | package admin_stats
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func UsersPerPlanStats(context *gin.Context, db *bun.DB) []models.PlanCountStats {
12 | var stats []models.PlanCountStats
13 | err := db.NewRaw("SELECT pl.name as plan, COUNT(us.*) as count FROM plans as pl LEFT JOIN users as us ON pl.id = us.plan GROUP BY pl.name, pl.price ORDER BY pl.price ASC").Scan(context, &stats)
14 | if err != nil {
15 | httperror.InternalServerError(context, "Error collecting user stats from db", err)
16 | return nil
17 | }
18 |
19 | return stats
20 | }
21 |
--------------------------------------------------------------------------------
/services/backend/functions/admin_stats/users_per_role.go:
--------------------------------------------------------------------------------
1 | package admin_stats
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func UsersPerRoleStats(context *gin.Context, db *bun.DB) []models.RoleCountStats {
12 | var stats []models.RoleCountStats
13 | err := db.NewRaw("SELECT role, COUNT(*) as count FROM users GROUP BY role ORDER BY role ASC").Scan(context, &stats)
14 | if err != nil {
15 | httperror.InternalServerError(context, "Error collecting user stats from db", err)
16 | return nil
17 | }
18 |
19 | return stats
20 | }
21 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/alertflowAutoRunnerToken.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func GenerateExFlowAutoRunnerJWT(id uuid.UUID) (tokenString string, expirationTime time.Time, err error) {
14 | var jwtKey = []byte(config.Config.JWT.Secret)
15 |
16 | expirationTime = time.Now().Add(50 * 365 * 24 * time.Hour) // 10 years
17 | claims := &models.JWTProjectRunnerClaim{
18 | ProjectID: "admin",
19 | ID: id,
20 | Type: "shared_auto_runner",
21 | RegisteredClaims: jwt.RegisteredClaims{
22 | ExpiresAt: jwt.NewNumericDate(expirationTime),
23 | },
24 | }
25 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
26 | tokenString, err = token.SignedString(jwtKey)
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/projectAutoRunnerToken.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func GenerateProjectAutoRunnerJWT(projectID string, id uuid.UUID) (tokenString string, expirationTime time.Time, err error) {
14 | var jwtKey = []byte(config.Config.JWT.Secret)
15 |
16 | expirationTime = time.Now().Add(50 * 365 * 24 * time.Hour) // 10 years
17 | claims := &models.JWTProjectRunnerClaim{
18 | ProjectID: projectID,
19 | ID: id,
20 | Type: "project_auto_runner",
21 | RegisteredClaims: jwt.RegisteredClaims{
22 | ExpiresAt: jwt.NewNumericDate(expirationTime),
23 | },
24 | }
25 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
26 | tokenString, err = token.SignedString(jwtKey)
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/projectToken.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func GenerateProjectJWT(projectID string, days int, id uuid.UUID) (tokenString string, expirationTime time.Time, err error) {
14 | var jwtKey = []byte(config.Config.JWT.Secret)
15 |
16 | // Set expiration time by adding days to current time
17 | expirationTime = time.Now().AddDate(0, 0, days)
18 | claims := &models.JWTProjectClaim{
19 | ProjectID: projectID,
20 | ID: id,
21 | Type: "project",
22 | RegisteredClaims: jwt.RegisteredClaims{
23 | ExpiresAt: jwt.NewNumericDate(expirationTime),
24 | },
25 | }
26 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
27 | tokenString, err = token.SignedString(jwtKey)
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/runnerToken.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func GenerateRunnerJWT(runnerID string, projectID string, id uuid.UUID) (tokenString string, expirationTime time.Time, err error) {
14 | var jwtKey = []byte(config.Config.JWT.Secret)
15 |
16 | expirationTime = time.Now().Add(50 * 365 * 24 * time.Hour) // 10 years
17 | claims := &models.JWTProjectRunnerClaim{
18 | RunnerID: runnerID,
19 | ProjectID: projectID,
20 | ID: id,
21 | Type: "runner",
22 | RegisteredClaims: jwt.RegisteredClaims{
23 | ExpiresAt: jwt.NewNumericDate(expirationTime),
24 | },
25 | }
26 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
27 | tokenString, err = token.SignedString(jwtKey)
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/serviceToken.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func GenerateServiceJWT(days int, id uuid.UUID) (tokenString string, expirationTime time.Time, err error) {
14 | var jwtKey = []byte(config.Config.JWT.Secret)
15 |
16 | // Set expiration time by adding days to current time
17 | expirationTime = time.Now().AddDate(0, 0, days)
18 | claims := &models.JWTClaim{
19 | ID: id,
20 | Type: "service",
21 | RegisteredClaims: jwt.RegisteredClaims{
22 | ExpiresAt: jwt.NewNumericDate(expirationTime),
23 | },
24 | }
25 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
26 | tokenString, err = token.SignedString(jwtKey)
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/userToken.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/golang-jwt/jwt/v5"
10 | "github.com/google/uuid"
11 | )
12 |
13 | func GenerateJWT(id uuid.UUID, rememberMe bool) (tokenString string, ExpiresAt int64, err error) {
14 | var jwtKey = []byte(config.Config.JWT.Secret)
15 | var expirationTime time.Time
16 |
17 | if rememberMe {
18 | expirationTime = time.Now().Add(7 * 24 * time.Hour)
19 | } else {
20 | expirationTime = time.Now().Add(12 * time.Hour)
21 | }
22 |
23 | claims := &models.JWTClaim{
24 | ID: id,
25 | Type: "user",
26 | RegisteredClaims: jwt.RegisteredClaims{
27 | ExpiresAt: jwt.NewNumericDate(expirationTime),
28 | },
29 | }
30 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
31 | tokenString, err = token.SignedString(jwtKey)
32 | ExpiresAt = expirationTime.Unix()
33 | return
34 | }
35 |
--------------------------------------------------------------------------------
/services/backend/functions/auth/validateTokenDB.go:
--------------------------------------------------------------------------------
1 | package auth
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/google/uuid"
8 | "github.com/uptrace/bun"
9 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
10 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
11 | )
12 |
13 | func ValidateTokenDBEntry(token string, db *bun.DB, ctx *gin.Context) (valid bool, err error) {
14 | var dbToken models.Tokens
15 | err = db.NewSelect().Model(&dbToken).Where("key = ?", token).Scan(ctx)
16 | if err != nil {
17 | httperror.InternalServerError(ctx, "Error receiving token from db", err)
18 | return false, err
19 | }
20 |
21 | if dbToken.ID == uuid.Nil {
22 | httperror.Unauthorized(ctx, "The provided token is not valid", errors.New("the provided token is not valid"))
23 | return false, err
24 | }
25 |
26 | if dbToken.Disabled {
27 | httperror.Unauthorized(ctx, "The provided token is disabled", errors.New("the provided token is disabled"))
28 | return false, err
29 | }
30 |
31 | return true, nil
32 | }
33 |
--------------------------------------------------------------------------------
/services/backend/functions/background_checks/checkDisconnectedAutoRunners.go:
--------------------------------------------------------------------------------
1 | package background_checks
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | log "github.com/sirupsen/logrus"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func checkDisconnectedAutoRunners(db *bun.DB) {
14 | context := context.Background()
15 |
16 | log.Info("Bot: Checking for disconnected runners")
17 |
18 | // get all executions that are not finished
19 | var runners []models.Runners
20 | err := db.NewSelect().Model(&runners).Where("last_heartbeat < NOW() - INTERVAL '5 minutes' and auto_runner = ?", true).Scan(context)
21 | if err != nil {
22 | log.Error("Bot: Error getting running runners. ", err)
23 | }
24 |
25 | for _, runner := range runners {
26 | log.Info(fmt.Sprintf("Bot: Runner %s seems to be not connected anymore. Removing it", runner.ID))
27 | _, err := db.NewDelete().Model(&runner).Where("id = ?", runner.ID).Exec(context)
28 | if err != nil {
29 | log.Error(fmt.Sprintf("Bot: Error removing runner %s. ", runner.ID), err)
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/services/backend/functions/background_checks/main.go:
--------------------------------------------------------------------------------
1 | package background_checks
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/uptrace/bun"
7 | )
8 |
9 | func Init(db *bun.DB) {
10 | ticker := time.NewTicker(1 * time.Minute)
11 | ticker2 := time.NewTicker(10 * time.Second)
12 | quit := make(chan struct{})
13 |
14 | go func() {
15 | for {
16 | select {
17 | case <-ticker.C:
18 | checkHangingExecutions(db)
19 | checkHangingExecutionSteps(db)
20 | checkDisconnectedAutoRunners(db)
21 | checkForFlowActionUpdates(db)
22 | scheduleFlowExecutions(db)
23 | case <-quit:
24 | ticker.Stop()
25 | return
26 | }
27 | }
28 | }()
29 |
30 | go func() {
31 | for {
32 | select {
33 | case <-ticker2.C:
34 | checkScheduledExecutions(db)
35 | case <-quit:
36 | ticker2.Stop()
37 | return
38 | }
39 | }
40 | }()
41 | }
42 |
--------------------------------------------------------------------------------
/services/backend/functions/flow/startExecution.go:
--------------------------------------------------------------------------------
1 | package functions
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 |
8 | "github.com/google/uuid"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func PreStartExecution(flowID string, flow models.Flows, db *bun.DB) error {
13 | context := context.Background()
14 |
15 | var execution models.Executions
16 |
17 | if flow.RunnerID != "" {
18 | execution.RunnerID = flow.RunnerID
19 | }
20 |
21 | execution.ID = uuid.New()
22 | execution.FlowID = flowID
23 | execution.Status = "pending"
24 | _, err := db.NewInsert().Model(&execution).Column("id", "flow_id", "status", "executed_at").Exec(context)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | return nil
30 | }
31 |
--------------------------------------------------------------------------------
/services/backend/functions/gatekeeper/checkAccountStatus.go:
--------------------------------------------------------------------------------
1 | package gatekeeper
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
5 | "context"
6 |
7 | _ "github.com/lib/pq"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func CheckAccountStatus(userId string, db *bun.DB) (bool, error) {
12 | ctx := context.Background()
13 | user := new(models.Users)
14 | err := db.NewSelect().Model(user).Where("id = ?", userId).Scan(ctx)
15 | if err != nil {
16 | return false, err
17 | }
18 |
19 | if user.Disabled {
20 | return true, nil
21 | } else {
22 | return false, nil
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/services/backend/functions/gatekeeper/checkAdmin.go:
--------------------------------------------------------------------------------
1 | package gatekeeper
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
5 | "context"
6 |
7 | "github.com/google/uuid"
8 | _ "github.com/lib/pq"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func CheckAdmin(userID uuid.UUID, db *bun.DB) (bool, error) {
13 | ctx := context.Background()
14 | user := new(models.Users)
15 | err := db.NewSelect().Model(user).Where("id = ?", userID).Scan(ctx)
16 | if err != nil {
17 | return false, err
18 | }
19 |
20 | if user.Role != "admin" {
21 | return false, nil
22 | } else {
23 | return true, nil
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/services/backend/functions/gatekeeper/checkRequestUserProjectModifyRole.go:
--------------------------------------------------------------------------------
1 | package gatekeeper
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func CheckRequestUserProjectModifyRole(projectID string, context *gin.Context, db *bun.DB) (bool, error) {
13 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
14 | if err != nil {
15 | httperror.InternalServerError(context, "Error receiving userID from token", err)
16 | return false, err
17 | }
18 |
19 | isAdmin, err := CheckAdmin(userID, db)
20 | if err != nil {
21 | httperror.InternalServerError(context, "Error checking if user is admin", err)
22 | return false, err
23 | }
24 | if isAdmin {
25 | return true, nil
26 | }
27 |
28 | var member models.ProjectMembers
29 | err = db.NewSelect().Model(&member).Where("project_id = ? AND user_id = ?", projectID, userID).Scan(context)
30 | if err != nil {
31 | return false, err
32 | }
33 |
34 | if member.Role == "Owner" || member.Role == "Editor" {
35 | return true, nil
36 | }
37 |
38 | return false, nil
39 | }
40 |
--------------------------------------------------------------------------------
/services/backend/functions/httperror/internalServerError.go:
--------------------------------------------------------------------------------
1 | package httperror
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func InternalServerError(context *gin.Context, message string, err error) {
10 | context.JSON(http.StatusInternalServerError, gin.H{"message": message, "error": err.Error()})
11 | context.Abort()
12 | }
13 |
--------------------------------------------------------------------------------
/services/backend/functions/httperror/statusBadRequest.go:
--------------------------------------------------------------------------------
1 | package httperror
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func StatusBadRequest(context *gin.Context, message string, err error) {
10 | context.JSON(http.StatusBadRequest, gin.H{"message": message, "error": err.Error()})
11 | context.Abort()
12 | }
13 |
--------------------------------------------------------------------------------
/services/backend/functions/httperror/statusConflict.go:
--------------------------------------------------------------------------------
1 | package httperror
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func StatusConflict(context *gin.Context, message string, err error) {
10 | context.JSON(http.StatusConflict, gin.H{"message": message, "error": err.Error()})
11 | context.Abort()
12 | }
13 |
--------------------------------------------------------------------------------
/services/backend/functions/httperror/statusNotFound.go:
--------------------------------------------------------------------------------
1 | package httperror
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func StatusNotFound(context *gin.Context, message string, err error) {
10 | context.JSON(http.StatusNotFound, gin.H{"message": message, "error": err.Error()})
11 | context.Abort()
12 | }
13 |
--------------------------------------------------------------------------------
/services/backend/functions/httperror/unauthorized.go:
--------------------------------------------------------------------------------
1 | package httperror
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gin-gonic/gin"
7 | )
8 |
9 | func Unauthorized(context *gin.Context, message string, err error) {
10 | context.JSON(http.StatusUnauthorized, gin.H{"message": message, "error": err.Error()})
11 | context.Abort()
12 | }
13 |
--------------------------------------------------------------------------------
/services/backend/functions/project/checkIfUserIsProjectMember.go:
--------------------------------------------------------------------------------
1 | package functions_project
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
5 | "context"
6 |
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | func CheckIfUserIsProjectMember(email string, projectID string, db *bun.DB) (bool, error) {
11 | ctx := context.Background()
12 | user := new(models.Users)
13 | err := db.NewSelect().Model(user).Where("email = ?", email).Scan(ctx)
14 | if err != nil {
15 | return false, err
16 | }
17 |
18 | var members []models.ProjectMembers
19 | count, err := db.NewSelect().Model(&members).Where("project_id = ?", projectID).Where("user_id = ?", user.ID).ScanAndCount(ctx)
20 | if err != nil {
21 | return false, err
22 | }
23 |
24 | if count > 0 {
25 | return true, nil
26 | }
27 | return false, nil
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/functions/project/createAuditEntry.go:
--------------------------------------------------------------------------------
1 | package functions_project
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func CreateAuditEntry(projectID string, operation string, details string, db *bun.DB, context *gin.Context) error {
12 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
13 | if err != nil {
14 | return err
15 | }
16 |
17 | audit := new(models.Audit)
18 | audit.ProjectID = projectID
19 | audit.UserID = userID.String()
20 | audit.Operation = operation
21 | audit.Details = details
22 | _, err = db.NewInsert().Model(audit).Exec(context)
23 | if err != nil {
24 | return err
25 | }
26 | return nil
27 | }
28 |
--------------------------------------------------------------------------------
/services/backend/functions/runner/generate_alertflow_auto_join_token.go:
--------------------------------------------------------------------------------
1 | package functions_runner
2 |
3 | import (
4 | "context"
5 | "time"
6 |
7 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/google/uuid"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GenerateExFlowAutoJoinToken(db *bun.DB) (token string, err error) {
15 | var key models.Tokens
16 |
17 | key.ID = uuid.New()
18 | key.CreatedAt = time.Now()
19 | key.ProjectID = "admin"
20 | key.Type = "shared_auto_runner"
21 | key.Description = "Token for Shared Auto Runner Join"
22 |
23 | key.Key, key.ExpiresAt, err = auth.GenerateExFlowAutoRunnerJWT(key.ID)
24 | if err != nil {
25 | return "", err
26 | }
27 |
28 | _, err = db.NewInsert().Model(&key).Exec(context.Background())
29 | if err != nil {
30 | return "", err
31 | }
32 |
33 | return key.Key, nil
34 | }
35 |
--------------------------------------------------------------------------------
/services/backend/functions/runner/generate_project_auto_join_token.go:
--------------------------------------------------------------------------------
1 | package functions_runner
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "context"
7 | "time"
8 |
9 | "github.com/google/uuid"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GenerateProjectAutoJoinToken(projectID string, db *bun.DB) (token string, err error) {
14 | var key models.Tokens
15 |
16 | key.ID = uuid.New()
17 | key.CreatedAt = time.Now()
18 | key.ProjectID = projectID
19 | key.Type = "project_auto_runner"
20 | key.Description = "Token for Project Auto Runner Join"
21 |
22 | key.Key, key.ExpiresAt, err = auth.GenerateProjectAutoRunnerJWT(projectID, key.ID)
23 | if err != nil {
24 | return "", err
25 | }
26 |
27 | _, err = db.NewInsert().Model(&key).Exec(context.Background())
28 | if err != nil {
29 | return "", err
30 | }
31 |
32 | return key.Key, nil
33 | }
34 |
--------------------------------------------------------------------------------
/services/backend/functions/runner/generate_runner_token.go:
--------------------------------------------------------------------------------
1 | package functions_runner
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "context"
7 | "time"
8 |
9 | "github.com/google/uuid"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GenerateRunnerToken(projectID string, runnerID string, db *bun.DB) (token string, err error) {
14 | var key models.Tokens
15 |
16 | key.ID = uuid.New()
17 | key.CreatedAt = time.Now()
18 | key.ProjectID = projectID
19 | key.Type = "runner"
20 | key.Description = "Token for runner " + runnerID
21 |
22 | key.Key, key.ExpiresAt, err = auth.GenerateRunnerJWT(runnerID, projectID, key.ID)
23 | if err != nil {
24 | return "", err
25 | }
26 |
27 | _, err = db.NewInsert().Model(&key).Exec(context.Background())
28 | if err != nil {
29 | return "", err
30 | }
31 |
32 | return key.Key, nil
33 | }
34 |
--------------------------------------------------------------------------------
/services/backend/functions/user/sendUserNotification.go:
--------------------------------------------------------------------------------
1 | package functions
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
5 | "context"
6 |
7 | _ "github.com/lib/pq"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func SendUserNotification(userID string, title string, body string, icon string, color string, link string, linxText string, db *bun.DB) error {
12 | ctx := context.Background()
13 |
14 | notification := models.Notifications{
15 | UserID: userID,
16 | Title: title,
17 | Body: body,
18 | Icon: icon,
19 | Color: color,
20 | Link: link,
21 | LinkText: linxText,
22 | }
23 | _, err := db.NewInsert().Model(¬ification).Exec(ctx)
24 | if err != nil {
25 | return err
26 | }
27 |
28 | return nil
29 | }
30 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/change_project_status.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | functions_project "github.com/v1Flows/exFlow/services/backend/functions/project"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | log "github.com/sirupsen/logrus"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func ChangeProjectStatus(context *gin.Context, db *bun.DB) {
15 | projectID := context.Param("projectID")
16 |
17 | var project models.Projects
18 | if err := context.ShouldBindJSON(&project); err != nil {
19 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
20 | return
21 | }
22 |
23 | _, err := db.NewUpdate().Model(&project).Column("disabled", "disabled_reason").Where("id = ?", projectID).Exec(context)
24 | if err != nil {
25 | httperror.InternalServerError(context, "Error updating project on db", err)
26 | return
27 | }
28 |
29 | // Audit
30 | if project.Disabled {
31 | err = functions_project.CreateAuditEntry(projectID, "update", "Project disabled: "+project.DisabledReason, db, context)
32 | if err != nil {
33 | log.Error(err)
34 | }
35 | } else {
36 | err = functions_project.CreateAuditEntry(projectID, "update", "Project enabled", db, context)
37 | if err != nil {
38 | log.Error(err)
39 | }
40 | }
41 |
42 | context.JSON(http.StatusOK, gin.H{"result": "success"})
43 | }
44 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/change_runner_status.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func ChangeRunnerStatus(context *gin.Context, db *bun.DB) {
13 | runnerID := context.Param("runnerID")
14 |
15 | var runner models.Runners
16 | if err := context.ShouldBindJSON(&runner); err != nil {
17 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
18 | return
19 | }
20 |
21 | _, err := db.NewUpdate().Model(&runner).Column("disabled", "disabled_reason").Where("id = ?", runnerID).Exec(context)
22 | if err != nil {
23 | httperror.InternalServerError(context, "Error updating runner on db", err)
24 | return
25 | }
26 |
27 | context.JSON(http.StatusOK, gin.H{"result": "success"})
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/create_user.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func CreateUser(context *gin.Context, db *bun.DB) {
16 | var user models.Users
17 | if err := context.ShouldBindJSON(&user); err != nil {
18 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
19 | return
20 | }
21 |
22 | // check if user exists
23 | firstCount, err := db.NewSelect().Model(&user).Where("email = ?", user.Email).Where("username = ?", user.Username).Count(context)
24 | if err != nil {
25 | httperror.InternalServerError(context, "Error checking for email and username on db", err)
26 | return
27 | }
28 | if firstCount > 0 {
29 | httperror.StatusConflict(context, "User already exists", nil)
30 | return
31 | }
32 |
33 | if err := user.HashPassword(user.Password); err != nil {
34 | httperror.InternalServerError(context, "Error encrypting user password", err)
35 | return
36 | }
37 |
38 | _, err = db.NewInsert().Model(&user).Column("email", "username", "password", "role").Exec(context)
39 | if err != nil {
40 | httperror.InternalServerError(context, "Error creating user on db", err)
41 | return
42 | }
43 |
44 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
45 | }
46 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/delete_token.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func DeleteToken(context *gin.Context, db *bun.DB) {
13 | tokenID := context.Param("tokenID")
14 |
15 | _, err := db.NewDelete().Model(&models.Tokens{}).Where("id = ?", tokenID).Exec(context)
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error deleting API Key on db", err)
18 | return
19 | }
20 |
21 | context.JSON(http.StatusOK, gin.H{"result": "success"})
22 | }
23 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/delete_user.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func DeleteUser(context *gin.Context, db *bun.DB) {
14 | userID := context.Param("userID")
15 |
16 | _, err := db.NewDelete().Model(&models.Users{}).Where("id = ?", userID).Exec(context)
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error deleting user on db", err)
19 | return
20 | }
21 |
22 | // remove user from project_members
23 | _, err = db.NewDelete().Model(&models.ProjectMembers{}).Where("user_id = ?", userID).Exec(context)
24 | if err != nil {
25 | httperror.InternalServerError(context, "Error deleting user from project memberships", err)
26 | return
27 | }
28 |
29 | context.JSON(http.StatusOK, gin.H{"result": "success"})
30 | }
31 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/disable_user.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func DisableUser(context *gin.Context, db *bun.DB) {
14 | userID := context.Param("userID")
15 |
16 | var user models.Users
17 | if err := context.ShouldBindJSON(&user); err != nil {
18 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
19 | return
20 | }
21 |
22 | user.UpdatedAt = time.Now()
23 |
24 | res, err := db.NewUpdate().Model(&user).Column("disabled", "disabled_reason", "updated_at").Where("id = ?", userID).Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error updating user on db", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusOK, gin.H{"result": "success", "response": res})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/generate_service_token.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/google/uuid"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GenerateServiceToken(context *gin.Context, db *bun.DB) {
15 | var expiresIn models.IncExpireTokenRequest
16 | if err := context.ShouldBindJSON(&expiresIn); err != nil {
17 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
18 | return
19 | }
20 |
21 | // save api key to tokens
22 | var token models.Tokens
23 | token.ID = uuid.New()
24 | token.Type = "service"
25 | token.Description = "Service API key. " + expiresIn.Description
26 | token.ProjectID = "admin"
27 |
28 | // generate api key
29 | tokenKey, expirationTime, err := auth.GenerateServiceJWT(expiresIn.ExpiresIn, token.ID)
30 | if err != nil {
31 | httperror.InternalServerError(context, "Error generating API key", err)
32 | return
33 | }
34 |
35 | token.Key = tokenKey
36 | token.ExpiresAt = expirationTime
37 |
38 | _, err = db.NewInsert().Model(&token).Exec(context)
39 | if err != nil {
40 | httperror.InternalServerError(context, "Error saving API key", err)
41 | return
42 | }
43 |
44 | context.JSON(http.StatusCreated, gin.H{
45 | "key": tokenKey,
46 | })
47 | }
48 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_executions.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetExecutions(context *gin.Context, db *bun.DB) {
14 | executions := make([]models.Executions, 0)
15 | err := db.NewSelect().Model(&executions).Order("created_at DESC").Scan(context)
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error collecting executions data on db", err)
18 | return
19 | }
20 |
21 | context.JSON(http.StatusOK, gin.H{"executions": executions})
22 | }
23 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_flows.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetFlows(context *gin.Context, db *bun.DB) {
14 | flows := make([]models.Flows, 0)
15 | err := db.NewSelect().Model(&flows).Scan(context)
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error collecting flow data on db", err)
18 | return
19 | }
20 |
21 | context.JSON(http.StatusOK, gin.H{"flows": flows})
22 | }
23 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_folders.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | _ "github.com/lib/pq"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetFolders(context *gin.Context, db *bun.DB) {
15 | folders := make([]models.Folders, 0)
16 | err := db.NewSelect().Model(&folders).Scan(context)
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error collecting folders data on db", err)
19 | return
20 | }
21 |
22 | context.JSON(http.StatusOK, gin.H{"folders": folders})
23 | }
24 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_projects.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | _ "github.com/lib/pq"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetProjects(context *gin.Context, db *bun.DB) {
15 | projects := make([]models.Projects, 0)
16 | err := db.NewSelect().Model(&projects).Scan(context)
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error collecting projects data on db", err)
19 | return
20 | }
21 |
22 | // Convert to ProjectsWithMembers and populate Members
23 | projectsWithMembers := make([]models.AdminProjectsWithMembers, len(projects))
24 | for i, project := range projects {
25 | projectsWithMembers[i].Projects = project
26 |
27 | members := make([]models.ProjectMembersWithUserData, 0)
28 | err = db.NewRaw("SELECT project_members.*, us.username, us.email FROM project_members JOIN users AS us ON us.id::uuid = project_members.user_id::uuid WHERE project_members.project_id = ? ORDER BY CASE WHEN project_members.role = 'Owner' THEN 1 WHEN project_members.role = 'Editor' THEN 2 ELSE 3 END", project.ID).
29 | Scan(context, &members)
30 | if err != nil {
31 | httperror.InternalServerError(context, "Error receiving project members from db", err)
32 | return
33 | }
34 |
35 | projectsWithMembers[i].Members = members
36 | }
37 |
38 | context.JSON(http.StatusOK, gin.H{"projects": projectsWithMembers})
39 | }
40 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_runners.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetRunners(context *gin.Context, db *bun.DB) {
14 | runners := make([]models.Runners, 0)
15 | err := db.NewSelect().Model(&runners).Scan(context)
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error collecting runners data on db", err)
18 | return
19 | }
20 |
21 | context.JSON(http.StatusOK, gin.H{"runners": runners})
22 | }
23 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_settings.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | functions_runner "github.com/v1Flows/exFlow/services/backend/functions/runner"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetSettings(context *gin.Context, db *bun.DB) {
15 | var settings models.Settings
16 | err := db.NewSelect().Model(&settings).Where("id = 1").Scan(context)
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error collecting settings data on db", err)
19 | return
20 | }
21 |
22 | // regenerate ExFlowRunnerAutoJoinToken if it got deleted or is not existing
23 | if settings.SharedRunnerAutoJoinToken == "" {
24 | settings.SharedRunnerAutoJoinToken, err = functions_runner.GenerateExFlowAutoJoinToken(db)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error generating ExFlowRunnerAutoJoinToken", err)
27 | return
28 | }
29 | _, err = db.NewUpdate().Model(&settings).Set("shared_runner_auto_join_token = ?", settings.SharedRunnerAutoJoinToken).Where("id = 1").Exec(context)
30 | if err != nil {
31 | httperror.InternalServerError(context, "Error updating ExFlowRunnerAutoJoinToken on db", err)
32 | return
33 | }
34 | }
35 |
36 | context.JSON(http.StatusOK, gin.H{"settings": settings})
37 | }
38 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_tokens.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetTokens(context *gin.Context, db *bun.DB) {
14 | tokens := make([]models.Tokens, 0)
15 | err := db.NewSelect().Model(&tokens).Order("created_at DESC").Scan(context)
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error collecting tokens on db", err)
18 | return
19 | }
20 |
21 | context.JSON(http.StatusOK, gin.H{"tokens": tokens})
22 | }
23 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/get_users.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | _ "github.com/lib/pq"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetUsers(context *gin.Context, db *bun.DB) {
15 | var users []models.Users
16 | err := db.NewSelect().Model(&users).ExcludeColumn("password").Scan(context)
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error collecting users on db", err)
19 | return
20 | }
21 |
22 | context.JSON(http.StatusOK, gin.H{"users": users})
23 | }
24 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/rotate-auto-join-token.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | functions_runner "github.com/v1Flows/exFlow/services/backend/functions/runner"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | _ "github.com/lib/pq"
12 | "github.com/uptrace/bun"
13 | )
14 |
15 | func RotateAutoJoinToken(context *gin.Context, db *bun.DB) {
16 | var settings models.Settings
17 | var err error
18 | settings.SharedRunnerAutoJoinToken, err = functions_runner.GenerateExFlowAutoJoinToken(db)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error rotating shared runner token", err)
21 | return
22 | }
23 |
24 | _, err = db.NewUpdate().Model(&settings).Column("shared_runner_auto_join_token").Where("id = 1").Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error rotating shared runner token", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/update_settings.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func UpdateSettings(context *gin.Context, db *bun.DB) {
13 | var settings models.Settings
14 | if err := context.ShouldBindJSON(&settings); err != nil {
15 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
16 | return
17 | }
18 |
19 | result, err := db.NewUpdate().Model(&settings).Where("id = 1").Exec(context)
20 | if err != nil {
21 | httperror.InternalServerError(context, "Error updating settings on db", err)
22 | return
23 | }
24 |
25 | context.JSON(http.StatusOK, gin.H{"settings": result, "result": "success"})
26 | }
27 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/update_token.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func UpdateToken(context *gin.Context, db *bun.DB) {
13 | tokenID := context.Param("tokenID")
14 |
15 | var token models.Tokens
16 | if err := context.ShouldBindJSON(&token); err != nil {
17 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
18 | return
19 | }
20 |
21 | _, err := db.NewUpdate().Model(&token).Column("description", "disabled", "disabled_reason").Where("id = ?", tokenID).Exec(context)
22 | if err != nil {
23 | httperror.InternalServerError(context, "Error updating token informations on db", err)
24 | }
25 |
26 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
27 | }
28 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/update_user.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "net/http"
5 | "strings"
6 | "time"
7 |
8 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
9 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
10 |
11 | "github.com/gin-gonic/gin"
12 | _ "github.com/lib/pq"
13 | "github.com/uptrace/bun"
14 | )
15 |
16 | func UpdateUser(context *gin.Context, db *bun.DB) {
17 | userID := context.Param("userID")
18 |
19 | var user models.Users
20 | if err := context.ShouldBindJSON(&user); err != nil {
21 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
22 | return
23 | }
24 |
25 | // get user db data
26 | var userDB models.Users
27 | err := db.NewSelect().Model(&userDB).Where("id = ?", userID).Scan(context)
28 | if err != nil {
29 | httperror.InternalServerError(context, "Error getting user from db", err)
30 | return
31 | }
32 |
33 | if user.Password != "" {
34 | // hash password
35 | if err := user.HashPassword(user.Password); err != nil {
36 | httperror.InternalServerError(context, "Error encrypting user password", err)
37 | return
38 | }
39 | } else {
40 | user.Password = userDB.Password
41 | }
42 |
43 | user.UpdatedAt = time.Now()
44 | user.Role = strings.ToLower(user.Role)
45 | _, err = db.NewUpdate().Model(&user).Column("username", "email", "role", "updated_at", "password").Where("id = ?", userID).Exec(context)
46 | if err != nil {
47 | httperror.InternalServerError(context, "Error updating user on db", err)
48 | return
49 | }
50 |
51 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
52 | }
53 |
--------------------------------------------------------------------------------
/services/backend/handlers/admins/user_send_admin_notify.go:
--------------------------------------------------------------------------------
1 | package admins
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | functions "github.com/v1Flows/exFlow/services/backend/functions/user"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func SendAdminToUserNotification(context *gin.Context, db *bun.DB) {
14 | userID := context.Param("userID")
15 |
16 | var notification models.Notifications
17 | if err := context.ShouldBindJSON(¬ification); err != nil {
18 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
19 | return
20 | }
21 |
22 | err := functions.SendUserNotification(userID, notification.Title, notification.Body, "solar:shield-up-broken", "danger", "", "", db)
23 | if err != nil {
24 | httperror.InternalServerError(context, "Error sending notification to user", err)
25 | return
26 | }
27 |
28 | context.JSON(http.StatusOK, gin.H{"result": "success"})
29 | }
30 |
--------------------------------------------------------------------------------
/services/backend/handlers/executions/get_execution.go:
--------------------------------------------------------------------------------
1 | package executions
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "errors"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | _ "github.com/lib/pq"
12 | "github.com/uptrace/bun"
13 | )
14 |
15 | func GetExecution(context *gin.Context, db *bun.DB) {
16 | executionID := context.Param("executionID")
17 |
18 | // get execution
19 | var execution models.Executions
20 | err := db.NewSelect().Model(&execution).Where("id = ?", executionID).Scan(context)
21 | if err != nil {
22 | httperror.InternalServerError(context, "Error collecting execution data from db", err)
23 | return
24 | }
25 |
26 | // get flow
27 | var flow models.Flows
28 | err = db.NewSelect().Model(&flow).Where("id = ?", execution.FlowID).Scan(context)
29 | if err != nil {
30 | httperror.InternalServerError(context, "Error collecting flow data from db", err)
31 | return
32 | }
33 |
34 | // check if user has access to project
35 | access, err := gatekeeper.CheckUserProjectAccess(flow.ProjectID, context, db)
36 | if err != nil {
37 | httperror.InternalServerError(context, "Error checking for flow access", err)
38 | return
39 | }
40 | if !access {
41 | httperror.Unauthorized(context, "You do not have access to this execution", errors.New("you do not have access to this execution"))
42 | return
43 | }
44 |
45 | context.JSON(http.StatusOK, gin.H{"execution": execution})
46 | }
47 |
--------------------------------------------------------------------------------
/services/backend/handlers/executions/get_step.go:
--------------------------------------------------------------------------------
1 | package executions
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/encryption"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetStep(context *gin.Context, db *bun.DB) {
14 | executionID := context.Param("executionID")
15 | stepID := context.Param("stepID")
16 |
17 | step := models.ExecutionSteps{}
18 | err := db.NewSelect().Model(&step).Where("execution_id = ? AND id = ?", executionID, stepID).Scan(context)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error collecting execution step from db", err)
21 | return
22 | }
23 |
24 | if step.Encrypted {
25 | step.Messages, err = encryption.DecryptExecutionStepActionMessage(step.Messages)
26 | if err != nil {
27 | httperror.InternalServerError(context, "Error decrypting execution step action messages", err)
28 | return
29 | }
30 | }
31 |
32 | context.JSON(http.StatusOK, gin.H{"step": step})
33 | }
34 |
--------------------------------------------------------------------------------
/services/backend/handlers/executions/get_steps.go:
--------------------------------------------------------------------------------
1 | package executions
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/encryption"
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetSteps(context *gin.Context, db *bun.DB) {
15 | executionID := context.Param("executionID")
16 |
17 | steps := make([]models.ExecutionSteps, 0)
18 | err := db.NewSelect().Model(&steps).Where("execution_id = ?", executionID).Order("created_at ASC").Order("started_at DESC").Scan(context)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error collecting execution steps from db", err)
21 | return
22 | }
23 |
24 | for i := range steps {
25 | if steps[i].Encrypted {
26 | steps[i].Messages, err = encryption.DecryptExecutionStepActionMessage(steps[i].Messages)
27 | if err != nil {
28 | httperror.InternalServerError(context, "Error decrypting execution step action messages", err)
29 | return
30 | }
31 | }
32 | }
33 |
34 | context.JSON(http.StatusOK, gin.H{"steps": steps})
35 | }
36 |
--------------------------------------------------------------------------------
/services/backend/handlers/executions/heartbeat.go:
--------------------------------------------------------------------------------
1 | package executions
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func Hearbeat(context *gin.Context, db *bun.DB) {
15 | executionID := context.Param("executionID")
16 |
17 | _, err := db.NewUpdate().Model((*models.Executions)(nil)).Where("id = ?", executionID).Set("last_heartbeat = ?", time.Now()).Exec(context)
18 | if err != nil {
19 | httperror.InternalServerError(context, "Error updating execution hearbeat on db", err)
20 | return
21 | }
22 |
23 | context.JSON(http.StatusOK, gin.H{"result": "success"})
24 | }
25 |
--------------------------------------------------------------------------------
/services/backend/handlers/executions/update.go:
--------------------------------------------------------------------------------
1 | package executions
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func Update(context *gin.Context, db *bun.DB) {
14 | executionID := context.Param("executionID")
15 |
16 | var execution models.Executions
17 | if err := context.ShouldBindJSON(&execution); err != nil {
18 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
19 | return
20 | }
21 |
22 | _, err := db.NewUpdate().Model(&execution).Where("id = ?", executionID).ExcludeColumn("scheduled_at", "last_heartbeat", "triggered_by").Exec(context)
23 | if err != nil {
24 | httperror.InternalServerError(context, "Error updating execution data on db", err)
25 | return
26 | }
27 |
28 | context.JSON(http.StatusOK, gin.H{"result": "success"})
29 | }
30 |
--------------------------------------------------------------------------------
/services/backend/handlers/flows/get_flows.go:
--------------------------------------------------------------------------------
1 | package flows
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | _ "github.com/lib/pq"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetFlows(context *gin.Context, db *bun.DB) {
15 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error receiving userID from token", err)
18 | return
19 | }
20 |
21 | flows := make([]models.Flows, 0)
22 | count, err := db.NewSelect().Model(&flows).Where("project_id::uuid IN (SELECT project_id::uuid FROM project_members WHERE user_id = ? AND invite_pending = false)", userID).ScanAndCount(context)
23 | if err != nil {
24 | httperror.InternalServerError(context, "Error collecting flows from db", err)
25 | return
26 | }
27 |
28 | context.JSON(http.StatusOK, gin.H{"flows": flows, "count": count})
29 | }
30 |
--------------------------------------------------------------------------------
/services/backend/handlers/flows/get_stats.go:
--------------------------------------------------------------------------------
1 | package flows
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/flow_stats"
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetStats(context *gin.Context, db *bun.DB) {
14 | flowID := context.Param("flowID")
15 | interval := context.DefaultQuery("interval", "24-hours")
16 |
17 | executionsStats := flow_stats.ExecutionsStats(interval, flowID, context, db)
18 | if executionsStats == nil {
19 | httperror.InternalServerError(context, "Error collecting stats", nil)
20 | return
21 | }
22 |
23 | executionTrends, err := flow_stats.ExecutionsTrends(interval, flowID, context, db)
24 | if err != nil {
25 | httperror.InternalServerError(context, "Error collecting trends", nil)
26 | return
27 | }
28 |
29 | // Return the stats
30 | context.JSON(http.StatusOK, gin.H{
31 | "executions_stats": executionsStats,
32 | "executions_trends": executionTrends,
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/services/backend/handlers/folders/get_folder.go:
--------------------------------------------------------------------------------
1 | package folders
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
8 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
9 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
10 |
11 | "github.com/gin-gonic/gin"
12 | _ "github.com/lib/pq"
13 | "github.com/uptrace/bun"
14 | )
15 |
16 | func GetFolder(context *gin.Context, db *bun.DB) {
17 | folderID := context.Param("folderID")
18 |
19 | var folder models.Folders
20 | err := db.NewSelect().Model(&folder).Where("id = ?", folderID).Scan(context)
21 | if err != nil {
22 | httperror.InternalServerError(context, "Error collecting folder data from db", err)
23 | return
24 | }
25 |
26 | // check if user has access to project
27 | access, err := gatekeeper.CheckUserProjectAccess(folder.ProjectID, context, db)
28 | if err != nil {
29 | httperror.InternalServerError(context, "Error checking for folder access", err)
30 | return
31 | }
32 | if !access {
33 | httperror.Unauthorized(context, "You do not have access to this folder", errors.New("you do not have access to this folder"))
34 | return
35 | }
36 |
37 | context.JSON(http.StatusOK, gin.H{"folder": folder})
38 | }
39 |
--------------------------------------------------------------------------------
/services/backend/handlers/folders/get_folders.go:
--------------------------------------------------------------------------------
1 | package folders
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | _ "github.com/lib/pq"
12 | "github.com/uptrace/bun"
13 | )
14 |
15 | func GetFolders(context *gin.Context, db *bun.DB) {
16 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error receiving userID from token", err)
19 | return
20 | }
21 |
22 | folders := make([]models.Folders, 0)
23 | count, err := db.NewSelect().Model(&folders).Where("project_id::uuid IN (SELECT project_id::uuid FROM project_members WHERE user_id = ? AND invite_pending = false)", userID).ScanAndCount(context)
24 | if err != nil {
25 | httperror.InternalServerError(context, "Error collecting folders from db", err)
26 | return
27 | }
28 |
29 | context.JSON(http.StatusOK, gin.H{"folders": folders, "count": count})
30 | }
31 |
--------------------------------------------------------------------------------
/services/backend/handlers/pages/get_settings.go:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func GetSettings(context *gin.Context, db *bun.DB) {
14 | var settings models.Settings
15 | err := db.NewSelect().Model(&settings).Column(
16 | "maintenance",
17 | "signup",
18 | "create_projects",
19 | "create_flows",
20 | "create_runners",
21 | "create_api_keys",
22 | "add_project_members",
23 | "add_flow_actions",
24 | "start_executions",
25 | ).Where("id = 1").Scan(context)
26 | if err != nil {
27 | httperror.InternalServerError(context, "Error collecting settings data on db", err)
28 | return
29 | }
30 |
31 | context.JSON(http.StatusOK, gin.H{"settings": settings})
32 | }
33 |
--------------------------------------------------------------------------------
/services/backend/handlers/projects/accept_invite.go:
--------------------------------------------------------------------------------
1 | package projects
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | functions_project "github.com/v1Flows/exFlow/services/backend/functions/project"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | log "github.com/sirupsen/logrus"
12 | "github.com/uptrace/bun"
13 | )
14 |
15 | func AcceptProjectInvite(context *gin.Context, db *bun.DB) {
16 | projectID := context.Param("projectID")
17 |
18 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error checking for userID in token", err)
21 | return
22 | }
23 |
24 | var member models.ProjectMembers
25 | member.InvitePending = false
26 | _, err = db.NewUpdate().Model(&member).Column("invite_pending").Where("user_id = ?", userID).Where("project_id = ?", projectID).Exec(context)
27 | if err != nil {
28 | httperror.InternalServerError(context, "Error receiving project member data from db", err)
29 | return
30 | }
31 |
32 | // Audit
33 | err = functions_project.CreateAuditEntry(projectID, "info", "User accepted project invite", db, context)
34 | if err != nil {
35 | log.Error(err)
36 | }
37 |
38 | context.JSON(http.StatusOK, gin.H{"result": "success"})
39 | }
40 |
--------------------------------------------------------------------------------
/services/backend/handlers/projects/decline_invite.go:
--------------------------------------------------------------------------------
1 | package projects
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | functions_project "github.com/v1Flows/exFlow/services/backend/functions/project"
7 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | log "github.com/sirupsen/logrus"
12 | "github.com/uptrace/bun"
13 | )
14 |
15 | func DeclineProjectInvite(context *gin.Context, db *bun.DB) {
16 | projectID := context.Param("projectID")
17 |
18 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error receiving userID from token", err)
21 | return
22 | }
23 |
24 | _, err = db.NewDelete().Model(&models.ProjectMembers{}).Where("user_id = ?", userID).Where("project_id = ?", projectID).Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error removing temporary membership from project", err)
27 | return
28 | }
29 |
30 | // Audit
31 | err = functions_project.CreateAuditEntry(projectID, "info", "User declined invite to project", db, context)
32 | if err != nil {
33 | log.Error(err)
34 | }
35 |
36 | context.JSON(http.StatusOK, gin.H{"result": "success"})
37 | }
38 |
--------------------------------------------------------------------------------
/services/backend/handlers/projects/get_audit_logs.go:
--------------------------------------------------------------------------------
1 | package projects
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "errors"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetProjectAuditLogs(context *gin.Context, db *bun.DB) {
15 | projectID := context.Param("projectID")
16 |
17 | // check if user has access to project
18 | access, err := gatekeeper.CheckUserProjectAccess(projectID, context, db)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error checking for project access", err)
21 | return
22 | }
23 | if !access {
24 | httperror.Unauthorized(context, "You do not have access to this project", errors.New("you do not have access to this project"))
25 | return
26 | }
27 |
28 | audit := make([]models.AuditWithUser, 0)
29 | err = db.NewRaw("SELECT audit.*, users.username, users.email, users.role FROM audit JOIN users ON audit.user_id::text = users.id::text WHERE audit.project_id = ? ORDER BY created_at DESC", projectID).Scan(context, &audit)
30 | if err != nil {
31 | httperror.InternalServerError(context, "Error receiving project audit logs from db", err)
32 | return
33 | }
34 |
35 | context.JSON(http.StatusOK, gin.H{"audit": audit})
36 | }
37 |
--------------------------------------------------------------------------------
/services/backend/handlers/projects/get_runners.go:
--------------------------------------------------------------------------------
1 | package projects
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
8 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
9 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
10 |
11 | "github.com/gin-gonic/gin"
12 | "github.com/uptrace/bun"
13 | )
14 |
15 | func GetProjectRunners(context *gin.Context, db *bun.DB) {
16 | projectID := context.Param("projectID")
17 |
18 | // check if user has access to project
19 | access, err := gatekeeper.CheckUserProjectAccess(projectID, context, db)
20 | if err != nil {
21 | httperror.InternalServerError(context, "Error checking for project access", err)
22 | return
23 | }
24 | if !access {
25 | httperror.Unauthorized(context, "You do not have access to this project", errors.New("you do not have access to this project"))
26 | return
27 | }
28 |
29 | runners := make([]models.Runners, 0)
30 | err = db.NewSelect().Model(&runners).Where("project_id = ? OR shared_runner = true", projectID).Order("name ASC").Order("last_heartbeat DESC").Scan(context)
31 | if err != nil {
32 | httperror.InternalServerError(context, "Error receiving project runners from db", err)
33 | return
34 | }
35 |
36 | context.JSON(http.StatusOK, gin.H{"runners": runners})
37 | }
38 |
--------------------------------------------------------------------------------
/services/backend/handlers/projects/get_tokens.go:
--------------------------------------------------------------------------------
1 | package projects
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "errors"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetProjectTokens(context *gin.Context, db *bun.DB) {
15 | projectID := context.Param("projectID")
16 |
17 | // check if user has access to project
18 | access, err := gatekeeper.CheckUserProjectAccess(projectID, context, db)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error checking for project access", err)
21 | return
22 | }
23 | if !access {
24 | httperror.Unauthorized(context, "You do not have access to this project", errors.New("you do not have access to this project"))
25 | return
26 | }
27 |
28 | tokens := make([]models.Tokens, 0)
29 | err = db.NewSelect().Model(&tokens).Where("project_id = ? and type != 'project_auto_runner'", projectID).Order("expires_at asc").Scan(context)
30 | if err != nil {
31 | httperror.InternalServerError(context, "Error receiving project tokens from db", err)
32 | return
33 | }
34 |
35 | context.JSON(http.StatusOK, gin.H{"tokens": tokens})
36 | }
37 |
--------------------------------------------------------------------------------
/services/backend/handlers/runners/busy.go:
--------------------------------------------------------------------------------
1 | package runners
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func Busy(context *gin.Context, db *bun.DB) {
15 | runnerID := context.Param("runnerID")
16 |
17 | var runner models.Runners
18 | if err := context.ShouldBindJSON(&runner); err != nil {
19 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
20 | return
21 | }
22 |
23 | // check if runner is disabled
24 | var runnerDB models.Runners
25 | err := db.NewSelect().Model(&runnerDB).Where("id = ?", runnerID).Scan(context)
26 | if err != nil {
27 | httperror.InternalServerError(context, "Error collecting runner data from db", err)
28 | return
29 | }
30 | if runnerDB.Disabled {
31 | httperror.StatusBadRequest(context, "Runner is disabled", errors.New("runner is disabled"))
32 | return
33 | }
34 |
35 | _, err = db.NewUpdate().Model(&runner).Column("executing_job").Where("id = ?", runnerID).Exec(context)
36 | if err != nil {
37 | httperror.InternalServerError(context, "Error updating runner informations on db", err)
38 | return
39 | }
40 |
41 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
42 | }
43 |
--------------------------------------------------------------------------------
/services/backend/handlers/runners/edit.go:
--------------------------------------------------------------------------------
1 | package runners
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "errors"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func EditRunner(context *gin.Context, db *bun.DB) {
15 | runnerID := context.Param("runnerID")
16 |
17 | var runner models.Runners
18 | if err := context.ShouldBindJSON(&runner); err != nil {
19 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
20 | return
21 | }
22 |
23 | // check the requestors role in project
24 | canModify, err := gatekeeper.CheckRequestUserProjectModifyRole(runner.ProjectID, context, db)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error checking your user permissions on project", err)
27 | return
28 | }
29 | if !canModify {
30 | httperror.Unauthorized(context, "You are not allowed to edit runners for this project", errors.New("unauthorized"))
31 | return
32 | }
33 |
34 | _, err = db.NewUpdate().Model(&runner).Column("name").Where("id = ?", runnerID).Exec(context)
35 | if err != nil {
36 | httperror.InternalServerError(context, "Error updating runner on db", err)
37 | return
38 | }
39 |
40 | context.JSON(http.StatusOK, gin.H{"result": "success"})
41 | }
42 |
--------------------------------------------------------------------------------
/services/backend/handlers/runners/get_links.go:
--------------------------------------------------------------------------------
1 | package runners
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func GetRunnerFlowLinks(context *gin.Context, db *bun.DB) {
13 | runnerID := context.Param("runnerID")
14 |
15 | flows := make([]models.Flows, 0)
16 | err := db.NewSelect().Model(&flows).Where("runner_id = ?", runnerID).Scan(context)
17 | if err != nil {
18 | httperror.InternalServerError(context, "Error collecting flows runner is assigned to", err)
19 | return
20 | }
21 |
22 | context.JSON(http.StatusOK, gin.H{"flows": flows})
23 | }
24 |
--------------------------------------------------------------------------------
/services/backend/handlers/runners/get_runners.go:
--------------------------------------------------------------------------------
1 | package runners
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func GetRunners(context *gin.Context, db *bun.DB) {
15 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
16 | if err != nil {
17 | httperror.InternalServerError(context, "Error receiving userID from token", err)
18 | return
19 | }
20 |
21 | projectRunners := make([]models.Runners, 0)
22 | err = db.NewSelect().Model(&projectRunners).Where("project_id::text IN (SELECT project_id::text FROM project_members WHERE user_id = ?)", userID).Where("shared_runner = false").Scan(context)
23 | if err != nil {
24 | httperror.InternalServerError(context, "Error collecting project runners from db", err)
25 | return
26 | }
27 |
28 | exflowRunners := make([]models.Runners, 0)
29 | err = db.NewSelect().Model(&exflowRunners).Where("shared_runner = true").Scan(context)
30 | if err != nil {
31 | httperror.InternalServerError(context, "Error collecting exflow runners from db", err)
32 | return
33 | }
34 |
35 | runners := append(projectRunners, exflowRunners...)
36 |
37 | context.JSON(http.StatusOK, gin.H{"runners": runners})
38 | }
39 |
--------------------------------------------------------------------------------
/services/backend/handlers/runners/heartbeat.go:
--------------------------------------------------------------------------------
1 | package runners
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "errors"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func Hearbeat(context *gin.Context, db *bun.DB) {
15 | runnerID := context.Param("runnerID")
16 |
17 | var runner models.Runners
18 | err := db.NewSelect().Model(&runner).Where("id = ?", runnerID).Scan(context)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error collecting runner data from db", err)
21 | return
22 | }
23 | if runner.Disabled {
24 | httperror.StatusBadRequest(context, "Runner is disabled", errors.New("runner is disabled"))
25 | return
26 | }
27 |
28 | _, err = db.NewUpdate().Model((*models.Runners)(nil)).Where("id = ?", runnerID).Set("last_heartbeat = ?", time.Now()).Exec(context)
29 | if err != nil {
30 | httperror.InternalServerError(context, "Error updating runner hearbeat on db", err)
31 | return
32 | }
33 |
34 | context.JSON(http.StatusOK, gin.H{"result": "success"})
35 | }
36 |
--------------------------------------------------------------------------------
/services/backend/handlers/runners/set_actions.go:
--------------------------------------------------------------------------------
1 | package runners
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
5 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func SetRunnerActions(context *gin.Context, db *bun.DB) {
13 | id := context.Param("id")
14 |
15 | var runner models.Runners
16 | if err := context.ShouldBindJSON(&runner); err != nil {
17 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
18 | return
19 | }
20 |
21 | _, err := db.NewUpdate().Model(&runner).Where("id = ?", id).Set("actions = ?", runner.Actions).Exec(context)
22 | if err != nil {
23 | httperror.InternalServerError(context, "Error updating runner actions on db", err)
24 | return
25 | }
26 |
27 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/handlers/tokens/delete_runner_token.go:
--------------------------------------------------------------------------------
1 | package tokens
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/gatekeeper"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "errors"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func DeleteRunnerToken(context *gin.Context, db *bun.DB) {
15 | tokenID := context.Param("apikey")
16 |
17 | // get token from db
18 | var key models.Tokens
19 | err := db.NewSelect().Model(&key).Where("id = ?", tokenID).Scan(context)
20 | if err != nil {
21 | httperror.InternalServerError(context, "Error getting token from db", err)
22 | return
23 | }
24 |
25 | // check the requestors role in project
26 | canModify, err := gatekeeper.CheckRequestUserProjectModifyRole(key.ProjectID, context, db)
27 | if err != nil {
28 | httperror.InternalServerError(context, "Error checking your user permissions on project", err)
29 | return
30 | }
31 | if !canModify {
32 | httperror.Unauthorized(context, "You are not allowed to make modifications on this project", errors.New("unauthorized"))
33 | return
34 | }
35 |
36 | _, err = db.NewDelete().Model(&key).Where("id = ?", tokenID).Exec(context)
37 | if err != nil {
38 | httperror.InternalServerError(context, "Error deleting token from db", err)
39 | return
40 | }
41 |
42 | context.JSON(http.StatusOK, gin.H{"result": "success"})
43 | }
44 |
--------------------------------------------------------------------------------
/services/backend/handlers/tokens/validate.go:
--------------------------------------------------------------------------------
1 | package tokens
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "net/http"
7 |
8 | "github.com/gin-gonic/gin"
9 | )
10 |
11 | func ValidateToken(context *gin.Context) {
12 | token := context.GetHeader("Authorization")
13 | err := auth.ValidateToken(token)
14 | if err != nil {
15 | httperror.Unauthorized(context, "Token is invalid", err)
16 | return
17 | }
18 | context.JSON(http.StatusOK, gin.H{"result": "success"})
19 | }
20 |
--------------------------------------------------------------------------------
/services/backend/handlers/tokens/validate_service_token.go:
--------------------------------------------------------------------------------
1 | package tokens
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "errors"
8 | "net/http"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func ValidateServiceToken(context *gin.Context, db *bun.DB) {
15 | token := context.GetHeader("Authorization")
16 |
17 | var key models.Tokens
18 | err := db.NewSelect().Model(&key).Where("key = ?", token).Scan(context)
19 | if err != nil {
20 | httperror.Unauthorized(context, "Token is invalid or expired", errors.New("token invalid or expired"))
21 | return
22 | }
23 |
24 | err = auth.ValidateToken(token)
25 | if err != nil {
26 | httperror.Unauthorized(context, "Token is invalid", err)
27 | return
28 | }
29 | context.JSON(http.StatusOK, gin.H{"result": "success"})
30 | }
31 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/archive_notification.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func ArchiveUserNotification(context *gin.Context, db *bun.DB) {
16 | notificationID := context.Param("notificationID")
17 |
18 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
19 | if err != nil {
20 | httperror.Unauthorized(context, "Error receiving userID from token", err)
21 | return
22 | }
23 |
24 | _, err = db.NewUpdate().Model(&models.Notifications{}).Set("is_archived = true").Where("id = ?", notificationID).Where("user_id = ?", userID).Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error archiving notification on db", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusOK, gin.H{"result": "success"})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/change_details.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func ChangeUserDetails(context *gin.Context, db *bun.DB) {
16 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
17 | if err != nil {
18 | httperror.Unauthorized(context, "Error receiving userID from token", err)
19 | return
20 | }
21 |
22 | var user models.Users
23 | if err := context.ShouldBindJSON(&user); err != nil {
24 | httperror.StatusBadRequest(context, "Error parsing incoming data", err)
25 | return
26 | }
27 |
28 | _, err = db.NewUpdate().Model(&user).Column("username", "email").Where("id = ?", userID).Exec(context)
29 | if err != nil {
30 | httperror.InternalServerError(context, "Error updating user on db", err)
31 | return
32 | }
33 |
34 | context.JSON(http.StatusCreated, gin.H{"result": "success"})
35 | }
36 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/delete.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | _ "github.com/lib/pq"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func DeleteUser(context *gin.Context, db *bun.DB) {
15 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
16 | if err != nil {
17 | httperror.Unauthorized(context, "Error receiving userID from token", err)
18 | return
19 | }
20 |
21 | _, err = db.NewDelete().Model(&models.Users{}).Where("id = ?", userID).Exec(context)
22 | if err != nil {
23 | httperror.InternalServerError(context, "Error deleting user on db", err)
24 | return
25 | }
26 |
27 | // remove user from project_members
28 | _, err = db.NewDelete().Model(&models.ProjectMembers{}).Where("user_id = ?", userID).Exec(context)
29 | if err != nil {
30 | httperror.InternalServerError(context, "Error deleting user from project memberships", err)
31 | return
32 | }
33 |
34 | context.JSON(http.StatusOK, gin.H{"result": "success"})
35 | }
36 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/details.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
7 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
8 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
9 |
10 | _ "github.com/lib/pq"
11 | "github.com/uptrace/bun"
12 |
13 | "github.com/gin-gonic/gin"
14 | )
15 |
16 | func GetUserDetails(context *gin.Context, db *bun.DB) {
17 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
18 | if err != nil {
19 | httperror.Unauthorized(context, "Error receiving userID from token", err)
20 | return
21 | }
22 |
23 | var user models.Users
24 | err = db.NewSelect().Model(&user).Column("id", "username", "email", "email_verified", "welcomed", "role", "created_at", "updated_at").Where("id = ?", userID).Scan(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error collecting user data from db", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusCreated, gin.H{"result": "success", "user": user})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/disable.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 | "time"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func DisableUser(context *gin.Context, db *bun.DB) {
15 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
16 | if err != nil {
17 | httperror.Unauthorized(context, "Error receiving userID from token", err)
18 | return
19 | }
20 |
21 | var user models.Users
22 |
23 | user.Disabled = true
24 | user.UpdatedAt = time.Now()
25 |
26 | _, err = db.NewUpdate().Model(&user).Column("disabled", "updated_at").Where("id = ?", userID).Exec(context)
27 | if err != nil {
28 | httperror.InternalServerError(context, "Error disable user on db", err)
29 | return
30 | }
31 |
32 | context.JSON(http.StatusOK, gin.H{"result": "success"})
33 | }
34 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/get_notifications.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func GetUserNotifications(context *gin.Context, db *bun.DB) {
16 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
17 | if err != nil {
18 | httperror.Unauthorized(context, "Error receiving userID from token", err)
19 | return
20 | }
21 |
22 | notifications := make([]models.Notifications, 0)
23 | err = db.NewSelect().Model(¬ifications).Where("user_id = ?", userID).Order("created_at DESC").Scan(context)
24 | if err != nil {
25 | httperror.InternalServerError(context, "Error collecting notifications from db", err)
26 | return
27 | }
28 |
29 | context.JSON(http.StatusOK, gin.H{"result": "success", "notifications": notifications})
30 | }
31 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/read_notification.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func ReadUserNotification(context *gin.Context, db *bun.DB) {
16 | notificationID := context.Param("notificationID")
17 |
18 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
19 | if err != nil {
20 | httperror.Unauthorized(context, "Error receiving userID from token", err)
21 | return
22 | }
23 |
24 | _, err = db.NewUpdate().Model(&models.Notifications{}).Set("is_read = true").Where("id = ?", notificationID).Where("user_id = ?", userID).Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error updating notification state on db", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusOK, gin.H{"result": "success"})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/unarchive_notification.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func UnarchiveUserNotification(context *gin.Context, db *bun.DB) {
16 | notificationID := context.Param("notificationID")
17 |
18 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
19 | if err != nil {
20 | httperror.Unauthorized(context, "Error receiving userID from token", err)
21 | return
22 | }
23 |
24 | _, err = db.NewUpdate().Model(&models.Notifications{}).Set("is_archived = false").Where("id = ?", notificationID).Where("user_id = ?", userID).Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error updating notification state on db", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusOK, gin.H{"result": "success"})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/unread_notification.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | _ "github.com/lib/pq"
10 | "github.com/uptrace/bun"
11 |
12 | "github.com/gin-gonic/gin"
13 | )
14 |
15 | func UnreadUserNotification(context *gin.Context, db *bun.DB) {
16 | notificationID := context.Param("notificationID")
17 |
18 | userID, err := auth.GetUserIDFromToken(context.GetHeader("Authorization"))
19 | if err != nil {
20 | httperror.Unauthorized(context, "Error receiving userID from token", err)
21 | return
22 | }
23 |
24 | _, err = db.NewUpdate().Model(&models.Notifications{}).Set("is_read = false").Where("id = ?", notificationID).Where("user_id = ?", userID).Exec(context)
25 | if err != nil {
26 | httperror.InternalServerError(context, "Error updating notification state on db", err)
27 | return
28 | }
29 |
30 | context.JSON(http.StatusOK, gin.H{"result": "success"})
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/handlers/users/welcomed.go:
--------------------------------------------------------------------------------
1 | package users
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
5 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
6 | "github.com/v1Flows/exFlow/services/backend/pkg/models"
7 | "net/http"
8 |
9 | "github.com/gin-gonic/gin"
10 | "github.com/uptrace/bun"
11 | )
12 |
13 | func WelcomedUser(context *gin.Context, db *bun.DB) {
14 | token := context.GetHeader("Authorization")
15 | userID, _ := auth.GetUserIDFromToken(token)
16 |
17 | var user models.Users
18 | _, err := db.NewUpdate().Model(&user).Set("welcomed = ?", true).Where("id = ?", userID).Exec(context)
19 | if err != nil {
20 | httperror.InternalServerError(context, "Error updating user data", err)
21 | return
22 | }
23 |
24 | context.JSON(http.StatusOK, gin.H{"result": "success"})
25 | }
26 |
--------------------------------------------------------------------------------
/services/backend/middlewares/runner.go:
--------------------------------------------------------------------------------
1 | package middlewares
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/v1Flows/exFlow/services/backend/config"
7 | "github.com/v1Flows/exFlow/services/backend/functions/auth"
8 | "github.com/v1Flows/exFlow/services/backend/functions/httperror"
9 |
10 | "github.com/gin-gonic/gin"
11 | "github.com/uptrace/bun"
12 | )
13 |
14 | func Runner(db *bun.DB) gin.HandlerFunc {
15 | return func(context *gin.Context) {
16 | tokenString := context.GetHeader("Authorization")
17 | if tokenString == "" {
18 | httperror.Unauthorized(context, "Request does not contain an access token", errors.New("request does not contain an access token"))
19 | return
20 | }
21 |
22 | err := auth.ValidateToken(tokenString)
23 | if err != nil {
24 | // if the token is not valid
25 | // check if the token matches the config.runner.shared_runner_secret
26 | if config.Config.Runner.SharedRunnerSecret != "" {
27 | if tokenString != config.Config.Runner.SharedRunnerSecret {
28 | httperror.Unauthorized(context, "The provided secret is not valid", err)
29 | return
30 | }
31 |
32 | context.Next()
33 | return
34 | } else {
35 | httperror.Unauthorized(context, "The provided token is not valid", err)
36 | return
37 | }
38 | }
39 |
40 | valid, err := auth.ValidateTokenDBEntry(tokenString, db, context)
41 | if err != nil {
42 | httperror.InternalServerError(context, "Error receiving token from db", err)
43 | return
44 | }
45 |
46 | if !valid {
47 | return
48 | }
49 |
50 | context.Next()
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/audit.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | type Audit struct {
11 | bun.BaseModel `bun:"table:audit"`
12 |
13 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"`
14 | ProjectID string `bun:"project_id,type:text,notnull" json:"project_id"`
15 | UserID string `bun:"user_id,type:text,notnull" json:"user_id"`
16 | Operation string `bun:"operation,type:text,notnull" json:"operation"`
17 | Details string `bun:"details,type:text,default:''" json:"details"`
18 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"`
19 | }
20 |
21 | type AuditWithUser struct {
22 | bun.BaseModel `bun:"table:audit"`
23 |
24 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"`
25 | ProjectID string `bun:"project_id,type:text,notnull" json:"project_id"`
26 | UserID string `bun:"user_id,type:text,notnull" json:"user_id"`
27 | Operation string `bun:"operation,type:text,notnull" json:"operation"`
28 | Details string `bun:"details,type:text,default:''" json:"details"`
29 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"`
30 | Username string `bun:"username,type:text" json:"username"`
31 | Email string `bun:"email,type:text" json:"email"`
32 | Role string `bun:"role,type:text" json:"role"`
33 | }
34 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/auth.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/golang-jwt/jwt/v5"
5 | "github.com/google/uuid"
6 | )
7 |
8 | type JWTClaim struct {
9 | ID uuid.UUID `json:"id"`
10 | Type string `json:"type"`
11 | jwt.RegisteredClaims
12 | }
13 |
14 | type JWTProjectRunnerClaim struct {
15 | RunnerID string `json:"runner_id"`
16 | ProjectID string `json:"project_id"`
17 | ID uuid.UUID `json:"id"`
18 | Type string `json:"type"`
19 | jwt.RegisteredClaims
20 | }
21 |
22 | type JWTProjectClaim struct {
23 | ProjectID string `json:"project_id"`
24 | ID uuid.UUID `json:"id"`
25 | Type string `json:"type"`
26 | jwt.RegisteredClaims
27 | }
28 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/execution_steps.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | shared_models "github.com/v1Flows/shared-library/pkg/models"
5 | )
6 |
7 | type ExecutionSteps struct {
8 | shared_models.ExecutionSteps
9 | }
10 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/executions.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | shared_models "github.com/v1Flows/shared-library/pkg/models"
7 | )
8 |
9 | type Executions struct {
10 | shared_models.Executions
11 |
12 | ScheduledAt time.Time `bun:"scheduled_at,type:timestamptz" json:"scheduled_at"`
13 | TriggeredBy string `bun:"triggered_by,type:text,default:'user'" json:"triggered_by"`
14 | }
15 |
16 | type ExecutionWithSteps struct {
17 | Executions
18 | Steps []ExecutionSteps `json:"steps"`
19 | }
20 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/flows.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | shared_models "github.com/v1Flows/shared-library/pkg/models"
5 | )
6 |
7 | type Flows struct {
8 | shared_models.Flows
9 |
10 | FolderID string `bun:"folder_id,type:text,default:''" json:"folder_id"`
11 | ScheduleEveryValue int `bun:"schedule_every_value,type:integer,default:0" json:"schedule_every_value"`
12 | ScheduleEveryUnit string `bun:"schedule_every_unit,type:text,default:''" json:"schedule_every_unit"`
13 | }
14 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/folders.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | type Folders struct {
11 | bun.BaseModel `bun:"table:folders"`
12 |
13 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"`
14 | Name string `bun:"name,type:text,notnull" json:"name"`
15 | Description string `bun:"description,type:text,default:''" json:"description"`
16 | ParentID string `bun:"parent_id,type:text,default:''" json:"parent_id"`
17 | ProjectID string `bun:"project_id,type:text,default:''" json:"project_id"`
18 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"`
19 | }
20 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/notifications.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | type Notifications struct {
11 | bun.BaseModel `bun:"table:notifications"`
12 |
13 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"`
14 | UserID string `bun:"user_id,type:text,notnull" json:"user_id"`
15 | Title string `bun:"title,type:text,notnull" json:"title"`
16 | Body string `bun:"body,type:text,default:''" json:"body"`
17 | IsRead bool `bun:"is_read,type:bool,default:false" json:"is_read"`
18 | IsArchived bool `bun:"is_archived,type:bool,default:false" json:"is_archived"`
19 | Icon string `bun:"icon,type:text,default:''" json:"icon"`
20 | Color string `bun:"color,type:text,default:'primary'" json:"color"`
21 | Link string `bun:"link,type:text,default:''" json:"link"`
22 | LinkText string `bun:"link_text,type:text,default:''" json:"link_text"`
23 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"`
24 | }
25 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/projects.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | type Projects struct {
11 | bun.BaseModel `bun:"table:projects"`
12 |
13 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"`
14 | Name string `bun:"name,type:text,notnull" json:"name"`
15 | Description string `bun:"description,type:text,default:''" json:"description"`
16 | SharedRunners bool `bun:"shared_runners,type:bool,default:false" json:"shared_runners"`
17 | Color string `bun:"color,type:text,default:''" json:"color"`
18 | Icon string `bun:"icon,type:text,default:''" json:"icon"`
19 | Disabled bool `bun:"disabled,type:bool,default:false" json:"disabled"`
20 | DisabledReason string `bun:"disabled_reason,type:text,default:''" json:"disabled_reason"`
21 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"`
22 | EnableAutoRunners bool `bun:"enable_auto_runners,type:bool,default:false" json:"enable_auto_runners"`
23 | DisableRunnerJoin bool `bun:"disable_runner_join,type:bool,default:false" json:"disable_runner_join"`
24 | RunnerAutoJoinToken string `bun:"runner_auto_join_token,type:text,notnull" json:"runner_auto_join_token"`
25 | }
26 |
27 | type ProjectsWithMembers struct {
28 | Projects
29 | Members []ProjectMembers `json:"members"`
30 | }
31 |
32 | type AdminProjectsWithMembers struct {
33 | Projects
34 | Members []ProjectMembersWithUserData `json:"members"`
35 | }
36 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/runners.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | shared_models "github.com/v1Flows/shared-library/pkg/models"
5 | )
6 |
7 | type Runners struct {
8 | shared_models.Runners
9 | }
10 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/settings.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/uptrace/bun"
5 | )
6 |
7 | type Settings struct {
8 | bun.BaseModel `bun:"table:settings"`
9 |
10 | ID int `bun:"id,type:integer,pk,default:1" json:"id"`
11 | Maintenance bool `bun:"maintenance,type:bool,default:false" json:"maintenance"`
12 | SignUp bool `bun:"signup,type:bool,default:true" json:"signup"`
13 | CreateProjects bool `bun:"create_projects,type:bool,default:true" json:"create_projects"`
14 | CreateFlows bool `bun:"create_flows,type:bool,default:true" json:"create_flows"`
15 | CreateRunners bool `bun:"create_runners,type:bool,default:true" json:"create_runners"`
16 | CreateApiKeys bool `bun:"create_api_keys,type:bool,default:true" json:"create_api_keys"`
17 | AddProjectMembers bool `bun:"add_project_members,type:bool,default:true" json:"add_project_members"`
18 | AddFlowActions bool `bun:"add_flow_actions,type:bool,default:true" json:"add_flow_actions"`
19 | StartExecutions bool `bun:"start_executions,type:bool,default:true" json:"start_executions"`
20 | AllowSharedRunnerAutoJoin bool `bun:"allow_shared_runner_auto_join,type:bool,default:true" json:"allow_shared_runner_auto_join"`
21 | AllowSharedRunnerJoin bool `bun:"allow_shared_runner_join,type:bool,default:true" json:"allow_shared_runner_join"`
22 | SharedRunnerAutoJoinToken string `bun:"shared_runner_auto_join_token,type:text,default:''" json:"shared_runner_auto_join_token"`
23 | }
24 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/stats.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | type StatsExecutions struct {
4 | Key string `json:"key"`
5 | Executions int `json:"executions"`
6 | }
7 |
8 | type StatsExecutionsTotals struct {
9 | ExecutionCount int `json:"total_executions"`
10 | ExecutionTrend Trend `json:"execution_trend"`
11 | }
12 |
13 | type Stats struct {
14 | Key string `json:"key"`
15 | Value int `json:"value"`
16 | }
17 |
18 | type PlanCountStats struct {
19 | Plan string `json:"plan"`
20 | Count int `json:"count"`
21 | }
22 |
23 | type RoleCountStats struct {
24 | Role string `json:"role"`
25 | Count int `json:"count"`
26 | }
27 |
28 | type Trend struct {
29 | Direction string `json:"direction"`
30 | Percentage float64 `json:"percentage"`
31 | }
32 |
--------------------------------------------------------------------------------
/services/backend/pkg/models/tokens.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | type Tokens struct {
11 | bun.BaseModel `bun:"table:tokens"`
12 |
13 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"`
14 | ProjectID string `bun:"project_id,type:text,default:''" json:"project_id"`
15 | Key string `bun:"key,type:text,notnull" json:"key"`
16 | Description string `bun:"description,type:text,default:''" json:"description"`
17 | Type string `bun:"type,type:text,notnull" json:"type"`
18 | Disabled bool `bun:"disabled,type:bool,default:false" json:"disabled"`
19 | DisabledReason string `bun:"disabled_reason,type:text,default:''" json:"disabled_reason"`
20 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"`
21 | ExpiresAt time.Time `bun:"expires_at,type:timestamptz" json:"expires_at"`
22 | UserID string `bun:"user_id,type:text,default:''" json:"user_id"`
23 | }
24 |
25 | type IncExpireTokenRequest struct {
26 | ExpiresIn int `json:"expires_in"`
27 | Description string `json:"description"`
28 | }
29 |
--------------------------------------------------------------------------------
/services/backend/router/auth.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/handlers/auths"
5 | "github.com/v1Flows/exFlow/services/backend/handlers/tokens"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func Auth(router *gin.RouterGroup, db *bun.DB) {
12 | auth := router.Group("/auth")
13 | {
14 | auth.POST("/login", func(c *gin.Context) {
15 | tokens.GenerateTokenUser(db, c)
16 | })
17 | auth.POST("/register", func(c *gin.Context) {
18 | auths.RegisterUser(c, db)
19 | })
20 | auth.POST("/user/taken", func(c *gin.Context) {
21 | auths.CheckUserTaken(c, db)
22 | })
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/services/backend/router/folders.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/handlers/folders"
5 | "github.com/v1Flows/exFlow/services/backend/middlewares"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/uptrace/bun"
9 | )
10 |
11 | func Folders(router *gin.RouterGroup, db *bun.DB) {
12 | folder := router.Group("/folders").Use(middlewares.Auth(db))
13 | {
14 | // folders
15 | folder.GET("/", func(c *gin.Context) {
16 | folders.GetFolders(c, db)
17 | })
18 | folder.POST("/", func(c *gin.Context) {
19 | folders.CreateFolder(c, db)
20 | })
21 |
22 | // folder
23 | folder.GET("/:folderID", func(c *gin.Context) {
24 | folders.GetFolder(c, db)
25 | })
26 | folder.PUT("/:folderID", func(c *gin.Context) {
27 | folders.UpdateFolder(c, db)
28 | })
29 | folder.DELETE("/:folderID", func(c *gin.Context) {
30 | folders.DeleteFolder(c, db)
31 | })
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/services/backend/router/health.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/gin-gonic/gin"
5 | )
6 |
7 | func Health(router *gin.RouterGroup) {
8 | router.GET("/health", func(c *gin.Context) {
9 | c.JSON(200, gin.H{"status": "ok"})
10 | })
11 | }
12 |
--------------------------------------------------------------------------------
/services/backend/router/main.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "strconv"
5 | "time"
6 |
7 | "github.com/gin-contrib/cors"
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 |
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | func StartRouter(db *bun.DB, port int) {
15 | gin.SetMode(gin.ReleaseMode)
16 | router := gin.Default()
17 |
18 | router.Use(cors.New(cors.Config{
19 | AllowOrigins: []string{"https://exflow.org", "http://localhost:3000"},
20 | AllowMethods: []string{"GET", "HEAD", "POST", "PUT", "OPTIONS", "DELETE"},
21 | AllowHeaders: []string{"Origin", "Authorization", "X-Requested-With", "Content-Type"},
22 | ExposeHeaders: []string{"Content-Length"},
23 | AllowCredentials: true,
24 | MaxAge: 12 * time.Hour,
25 | }))
26 |
27 | v1 := router.Group("/api/v1")
28 | {
29 | Admin(v1, db)
30 | Auth(v1, db)
31 | Folders(v1, db)
32 | Executions(v1, db)
33 | Flows(v1, db)
34 | Page(v1, db)
35 | Projects(v1, db)
36 | Runners(v1, db)
37 | Token(v1, db)
38 | User(v1, db)
39 | Health(v1)
40 | }
41 |
42 | log.Info("Starting Router on port ", strconv.Itoa(port))
43 | router.Run(":" + strconv.Itoa(port))
44 | }
45 |
--------------------------------------------------------------------------------
/services/backend/router/page.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/handlers/pages"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | func Page(router *gin.RouterGroup, db *bun.DB) {
11 | page := router.Group("/page")
12 | {
13 | page.GET("/settings", func(c *gin.Context) {
14 | pages.GetSettings(c, db)
15 | })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/services/backend/router/runners.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/handlers/executions"
5 | "github.com/v1Flows/exFlow/services/backend/handlers/runners"
6 | "github.com/v1Flows/exFlow/services/backend/middlewares"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/uptrace/bun"
10 | )
11 |
12 | func Runners(router *gin.RouterGroup, db *bun.DB) {
13 | runner := router.Group("/runners").Use(middlewares.Runner(db))
14 | {
15 | runner.GET("/", func(c *gin.Context) {
16 | runners.GetRunners(c, db)
17 | })
18 | runner.POST("/", func(c *gin.Context) {
19 | runners.CreateRunner(c, db)
20 | })
21 |
22 | runner.PUT("/:runnerID", func(c *gin.Context) {
23 | runners.EditRunner(c, db)
24 | })
25 | runner.DELETE("/:runnerID", func(c *gin.Context) {
26 | runners.DeleteRunner(c, db)
27 | })
28 |
29 | runner.GET("/:runnerID/flows/links", func(c *gin.Context) {
30 | runners.GetRunnerFlowLinks(c, db)
31 | })
32 |
33 | // Runner Access Endpoints
34 | runner.GET("/:runnerID/executions/pending", func(c *gin.Context) {
35 | executions.GetPendingExecutions(c, db)
36 | })
37 | runner.PUT("/register", func(c *gin.Context) {
38 | runners.RegisterRunner(c, db)
39 | })
40 | runner.PUT("/:runnerID/heartbeat", func(c *gin.Context) {
41 | runners.Hearbeat(c, db)
42 | })
43 | runner.PUT("/:runnerID/busy", func(c *gin.Context) {
44 | runners.Busy(c, db)
45 | })
46 | runner.PUT("/:runnerID/actions", func(c *gin.Context) {
47 | runners.SetRunnerActions(c, db)
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/services/backend/router/token.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/v1Flows/exFlow/services/backend/handlers/tokens"
5 |
6 | "github.com/gin-gonic/gin"
7 | "github.com/uptrace/bun"
8 | )
9 |
10 | func Token(router *gin.RouterGroup, db *bun.DB) {
11 | token := router.Group("/token")
12 | {
13 | token.GET("/validate", func(c *gin.Context) {
14 | tokens.ValidateToken(c)
15 | })
16 | token.POST("/refresh", func(c *gin.Context) {
17 | tokens.RefreshToken(c, db)
18 | })
19 | token.GET("/service/validate", func(c *gin.Context) {
20 | tokens.ValidateServiceToken(c, db)
21 | })
22 | token.PUT("/:id", func(c *gin.Context) {
23 | tokens.UpdateToken(c, db)
24 | })
25 | token.DELETE("/runner/:apikey", func(c *gin.Context) {
26 | tokens.DeleteRunnerToken(c, db)
27 | })
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/services/frontend/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_API_URL="http://localhost:8080"
--------------------------------------------------------------------------------
/services/frontend/.npmrc:
--------------------------------------------------------------------------------
1 | public-hoist-pattern[]=*@heroui/*
2 | package-lock=true
--------------------------------------------------------------------------------
/services/frontend/app/admin/executions/page.tsx:
--------------------------------------------------------------------------------
1 | import { Divider } from "@heroui/react";
2 |
3 | import ErrorCard from "@/components/error/ErrorCard";
4 | import AdminGetFlows from "@/lib/fetch/admin/flows";
5 | import AdminGetExecutions from "@/lib/fetch/admin/executions";
6 | import Executions from "@/components/executions/executionsTable";
7 | import AdminGetRunners from "@/lib/fetch/admin/runners";
8 |
9 | export default async function AdminExecutionsPage() {
10 | const flowsData = await AdminGetFlows();
11 | const executionsData = await AdminGetExecutions();
12 | const runnersData = await AdminGetRunners();
13 |
14 | const [flows, executions, runners] = (await Promise.all([
15 | flowsData,
16 | executionsData,
17 | runnersData,
18 | ])) as any;
19 |
20 | return (
21 | Executions
Projects
16 |Page Settings
11 |Users
16 |{name}
49 |{permission}
51 |Runners
10 |