├── .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 |
22 | {flows.success ? ( 23 | <> 24 |

Executions

25 | 26 | 32 | 33 | ) : ( 34 | 35 | )} 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /services/frontend/app/admin/page-settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from "@heroui/react"; 2 | 3 | import AdminSettingsHeading from "@/components/admin/settings/heading"; 4 | import AdminGetPageSettings from "@/lib/fetch/admin/settings"; 5 | import { AdminSettings } from "@/components/admin/settings/list"; 6 | 7 | export default async function AdminSettingsPage() { 8 | const settingsData = AdminGetPageSettings(); 9 | 10 | const [settings] = (await Promise.all([settingsData])) as any; 11 | 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /services/frontend/app/admin/projects/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from "@heroui/react"; 2 | 3 | import ErrorCard from "@/components/error/ErrorCard"; 4 | import GetUserDetails from "@/lib/fetch/user/getDetails"; 5 | import AdminGetProjects from "@/lib/fetch/admin/projects"; 6 | import AdminGetPageSettings from "@/lib/fetch/admin/settings"; 7 | import AdminProjectsHeading from "@/components/admin/projects/heading"; 8 | import { AdminProjectList } from "@/components/admin/projects/list"; 9 | 10 | export default async function AdminProjectsPage() { 11 | const projectsData = AdminGetProjects(); 12 | const settingsData = AdminGetPageSettings(); 13 | const userDetailsData = GetUserDetails(); 14 | 15 | const [projects, settings, userDetails] = (await Promise.all([ 16 | projectsData, 17 | settingsData, 18 | userDetailsData, 19 | ])) as any; 20 | 21 | return ( 22 |
23 | {projects.success && settings.success && userDetails.success ? ( 24 | <> 25 | 26 | 27 | 28 | 29 | ) : ( 30 | 34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /services/frontend/app/admin/runners/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from "@heroui/react"; 2 | 3 | import AdminRunnersHeading from "@/components/admin/runners/heading"; 4 | import RunnersList from "@/components/runners/list"; 5 | import AdminGetProjects from "@/lib/fetch/admin/projects"; 6 | import AdminGetRunners from "@/lib/fetch/admin/runners"; 7 | import GetUserDetails from "@/lib/fetch/user/getDetails"; 8 | import AdminGetPageSettings from "@/lib/fetch/admin/settings"; 9 | 10 | export default async function AdminRunnersPage() { 11 | const projectsData = AdminGetProjects(); 12 | const runnersData = AdminGetRunners(); 13 | const userDetailsData = GetUserDetails(); 14 | const settingsData = AdminGetPageSettings(); 15 | 16 | const [projects, runners, userDetails, settings] = (await Promise.all([ 17 | projectsData, 18 | runnersData, 19 | userDetailsData, 20 | settingsData, 21 | ])) as any; 22 | 23 | return ( 24 |
25 | 26 | 27 | {projects.success && runners.success && userDetails.success && ( 28 | 34 | )} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /services/frontend/app/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from "@heroui/react"; 2 | 3 | import AdminUsersHeading from "@/components/admin/users/heading"; 4 | import AdminGetUsers from "@/lib/fetch/admin/users"; 5 | import { AdminUsersList } from "@/components/admin/users/list"; 6 | import ErrorCard from "@/components/error/ErrorCard"; 7 | 8 | export default async function AdminUsersPage() { 9 | const usersData = AdminGetUsers(); 10 | 11 | const [users] = (await Promise.all([usersData])) as any; 12 | 13 | return ( 14 |
15 | {users.success ? ( 16 | <> 17 | 18 | 19 | 20 | 21 | ) : ( 22 | 23 | )} 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /services/frontend/app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import LoginPageComponent from "@/components/auth/loginPage"; 2 | import PageGetSettings from "@/lib/fetch/page/settings"; 3 | 4 | export default async function LoginPage() { 5 | const settingsData = PageGetSettings(); 6 | 7 | const [settings] = (await Promise.all([settingsData])) as any; 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /services/frontend/app/auth/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import SignUpPage from "@/components/auth/signupPage"; 2 | import PageGetSettings from "@/lib/fetch/page/settings"; 3 | 4 | export default async function SignupPage() { 5 | const settingsData = PageGetSettings(); 6 | 7 | const [settings] = (await Promise.all([settingsData])) as any; 8 | 9 | return ; 10 | } 11 | -------------------------------------------------------------------------------- /services/frontend/app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | /* eslint-disable no-console */ 15 | console.error(error); 16 | }, [error]); 17 | 18 | return ( 19 |
20 |

Something went wrong!

21 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /services/frontend/app/maintenance/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function DashboardHomeLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode; 7 | }) { 8 | return ( 9 | <> 10 |
11 | {children} 12 |
13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /services/frontend/app/maintenance/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | 3 | import SparklesText from "@/components/magicui/sparkles-text"; 4 | import { subtitle } from "@/components/primitives"; 5 | import Login from "@/components/auth/login"; 6 | 7 | export default async function MaintenancePage() { 8 | const cookieStore = await cookies(); 9 | const user = JSON.parse(cookieStore.get("user")?.value || "{}"); 10 | const session = cookieStore.get("session")?.value; 11 | 12 | return ( 13 |
14 |
15 | 16 |

17 | We are currently in maintenance mode. Please come back later. 18 | Apologies for the inconvenience. 19 |

20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /services/frontend/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | 3 | import ErrorCard from "@/components/error/ErrorCard"; 4 | import PageGetSettings from "@/lib/fetch/page/settings"; 5 | import GetUserDetails from "@/lib/fetch/user/getDetails"; 6 | import { UserProfile } from "@/components/user/profile"; 7 | 8 | export default async function ProfilePage() { 9 | const c = await cookies(); 10 | 11 | const settingsData = PageGetSettings(); 12 | const userDetailsData = GetUserDetails(); 13 | const session = c.get("session")?.value; 14 | 15 | const [settings, userDetails] = (await Promise.all([ 16 | settingsData, 17 | userDetailsData, 18 | ])) as any; 19 | 20 | return ( 21 | <> 22 | {settings.success && userDetails.success ? ( 23 | 28 | ) : ( 29 | 33 | )} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /services/frontend/app/projects/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from "@heroui/react"; 2 | 3 | import ErrorCard from "@/components/error/ErrorCard"; 4 | import { ProjectsList } from "@/components/projects/list"; 5 | import PageGetSettings from "@/lib/fetch/page/settings"; 6 | import GetProjects from "@/lib/fetch/project/all"; 7 | import GetUserDetails from "@/lib/fetch/user/getDetails"; 8 | import ProjectsHeading from "@/components/projects/heading"; 9 | 10 | export default async function ProjectsPage() { 11 | const projectsData = GetProjects(); 12 | const settingsData = PageGetSettings(); 13 | const userDetailsData = GetUserDetails(); 14 | 15 | const [projects, settings, userDetails] = (await Promise.all([ 16 | projectsData, 17 | settingsData, 18 | userDetailsData, 19 | ])) as any; 20 | 21 | return ( 22 |
23 | {projects.success && settings.success && userDetails.success ? ( 24 | <> 25 | 29 | 30 | 36 | 37 | ) : ( 38 | 42 | )} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /services/frontend/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { HeroUIProvider } from "@heroui/system"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { useRouter } from "next/navigation"; 6 | import * as React from "react"; 7 | import { ToastProvider } from "@heroui/react"; 8 | 9 | type ThemeProviderProps = React.ComponentProps; 10 | 11 | export type ProvidersProps = { 12 | children: React.ReactNode; 13 | themeProps?: ThemeProviderProps; 14 | }; 15 | 16 | export function Providers({ children, themeProps }: ProvidersProps) { 17 | const router = useRouter(); 18 | 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /services/frontend/app/runners/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from "@heroui/react"; 2 | 3 | import RunnersList from "@/components/runners/list"; 4 | import GetRunners from "@/lib/fetch/runner/get"; 5 | import GetProjects from "@/lib/fetch/project/all"; 6 | import RunnersHeading from "@/components/runners/heading"; 7 | import GetUserDetails from "@/lib/fetch/user/getDetails"; 8 | import ErrorCard from "@/components/error/ErrorCard"; 9 | 10 | export default async function RunnersPage() { 11 | const projectsData = GetProjects(); 12 | const runnersData = GetRunners(); 13 | const userDetailsData = GetUserDetails(); 14 | 15 | const [projects, runners, userDetails] = (await Promise.all([ 16 | projectsData, 17 | runnersData, 18 | userDetailsData, 19 | ])) as any; 20 | 21 | return ( 22 |
23 | {projects.success && runners.success && userDetails.success ? ( 24 | <> 25 | 26 | 27 | 28 | 34 | 35 | ) : ( 36 | 40 | )} 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /services/frontend/app/sw.ts: -------------------------------------------------------------------------------- 1 | import type { PrecacheEntry, SerwistGlobalConfig } from "serwist"; 2 | 3 | import { defaultCache } from "@serwist/next/worker"; 4 | import { Serwist } from "serwist"; 5 | 6 | // This declares the value of `injectionPoint` to TypeScript. 7 | // `injectionPoint` is the string that will be replaced by the 8 | // actual precache manifest. By default, this string is set to 9 | // `"self.__SW_MANIFEST"`. 10 | declare global { 11 | interface WorkerGlobalScope extends SerwistGlobalConfig { 12 | __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; 13 | } 14 | } 15 | 16 | // eslint-disable-next-line no-undef 17 | declare const self: ServiceWorkerGlobalScope; 18 | 19 | const serwist = new Serwist({ 20 | precacheEntries: self.__SW_MANIFEST, 21 | skipWaiting: true, 22 | clientsClaim: true, 23 | navigationPreload: true, 24 | runtimeCaching: defaultCache, 25 | }); 26 | 27 | serwist.addEventListeners(); 28 | -------------------------------------------------------------------------------- /services/frontend/components/admin/projects/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, useDisclosure } from "@heroui/react"; 4 | import { Icon } from "@iconify/react"; 5 | 6 | import CreateProjectModal from "@/components/modals/projects/create"; 7 | 8 | export default function AdminProjectsHeading() { 9 | const newProjectModal = useDisclosure(); 10 | 11 | return ( 12 |
13 |
14 |
15 |

Projects

16 |
17 |
18 |
19 | 26 |
27 | 28 |
29 | 32 |
33 |
34 |
35 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /services/frontend/components/admin/settings/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Reloader from "@/components/reloader/Reloader"; 4 | 5 | export default function AdminSettingsHeading() { 6 | return ( 7 |
8 |
9 |
10 |

Page Settings

11 |
12 |
13 | 14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /services/frontend/components/admin/users/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, useDisclosure } from "@heroui/react"; 4 | import { Icon } from "@iconify/react"; 5 | 6 | import AdminCreateUserModal from "@/components/modals/admin/createUser"; 7 | 8 | export default function AdminUsersHeading() { 9 | const createUserModal = useDisclosure(); 10 | 11 | return ( 12 |
13 |
14 |
15 |

Users

16 |
17 |
18 |
19 | 27 |
28 | 29 |
30 | 33 |
34 |
35 |
36 | 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /services/frontend/components/cn/cn.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | 3 | import clsx from "clsx"; 4 | import { extendTailwindMerge } from "tailwind-merge"; 5 | 6 | const COMMON_UNITS = ["small", "medium", "large"]; 7 | 8 | /** 9 | * We need to extend the tailwind merge to include NextUI's custom classes. 10 | * 11 | * So we can use classes like `text-small` or `text-default-500` and override them. 12 | */ 13 | const twMerge = extendTailwindMerge({ 14 | extend: { 15 | theme: { 16 | opacity: ["disabled"], 17 | spacing: ["divider"], 18 | borderWidth: COMMON_UNITS, 19 | borderRadius: COMMON_UNITS, 20 | }, 21 | classGroups: { 22 | shadow: [{ shadow: COMMON_UNITS }], 23 | "font-size": [{ text: ["tiny", ...COMMON_UNITS] }], 24 | "bg-image": ["bg-stripe-gradient"], 25 | }, 26 | }, 27 | }); 28 | 29 | export function cn(...inputs: ClassValue[]) { 30 | return twMerge(clsx(inputs)); 31 | } 32 | -------------------------------------------------------------------------------- /services/frontend/components/error/ErrorCard.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from "@heroui/react"; 2 | 3 | export default function ErrorCard({ error, message }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/components/modals/projects/cell-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cn } from "./cn"; 4 | 5 | const CellWrapper = ({ 6 | ref, 7 | children, 8 | className, 9 | ...props 10 | // eslint-disable-next-line no-undef 11 | }: React.HTMLAttributes & { 12 | // eslint-disable-next-line no-undef 13 | ref: React.RefObject; 14 | }) => ( 15 |
23 | {children} 24 |
25 | ); 26 | 27 | CellWrapper.displayName = "CellWrapper"; 28 | 29 | export default CellWrapper; 30 | -------------------------------------------------------------------------------- /services/frontend/components/modals/projects/cn.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | 3 | import clsx from "clsx"; 4 | import { extendTailwindMerge } from "tailwind-merge"; 5 | 6 | const COMMON_UNITS = ["small", "medium", "large"]; 7 | 8 | /** 9 | * We need to extend the tailwind merge to include NextUI's custom classes. 10 | * 11 | * So we can use classes like `text-small` or `text-default-500` and override them. 12 | */ 13 | const twMerge = extendTailwindMerge({ 14 | extend: { 15 | theme: { 16 | opacity: ["disabled"], 17 | spacing: ["divider"], 18 | borderWidth: COMMON_UNITS, 19 | borderRadius: COMMON_UNITS, 20 | }, 21 | classGroups: { 22 | shadow: [{ shadow: COMMON_UNITS }], 23 | "font-size": [{ text: ["tiny", ...COMMON_UNITS] }], 24 | "bg-image": ["bg-stripe-gradient"], 25 | }, 26 | }, 27 | }); 28 | 29 | export function cn(...inputs: ClassValue[]) { 30 | return twMerge(clsx(inputs)); 31 | } 32 | -------------------------------------------------------------------------------- /services/frontend/components/modals/projects/user-cell.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Avatar, cn } from "@heroui/react"; 4 | import React from "react"; 5 | 6 | import CellWrapper from "./cell-wrapper"; 7 | 8 | // eslint-disable-next-line no-undef 9 | export type UserCellProps = React.HTMLAttributes & { 10 | avatar: string; 11 | name: string; 12 | permission: string; 13 | color: string; 14 | }; 15 | 16 | const UserCell = ({ 17 | ref, 18 | avatar, 19 | name, 20 | permission, 21 | color, 22 | className, 23 | ...props 24 | // eslint-disable-next-line no-undef 25 | }: UserCellProps & { ref: React.RefObject }) => ( 26 | 31 |
32 | 48 |

{name}

49 |
50 |

{permission}

51 |
52 | ); 53 | 54 | UserCell.displayName = "UserCell"; 55 | 56 | export default UserCell; 57 | -------------------------------------------------------------------------------- /services/frontend/components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from "tailwind-variants"; 2 | 3 | export const title = tv({ 4 | base: "tracking-tight inline font-semibold", 5 | variants: { 6 | color: { 7 | violet: "from-[#FF1CF7] to-[#b249f8]", 8 | yellow: "from-[#FF705B] to-[#FFB457]", 9 | blue: "from-[#5EA2EF] to-[#0072F5]", 10 | cyan: "from-[#00b7fa] to-[#01cfea]", 11 | green: "from-[#6FEE8D] to-[#17c964]", 12 | pink: "from-[#FF72E1] to-[#F54C7A]", 13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", 14 | }, 15 | size: { 16 | sm: "text-3xl lg:text-4xl", 17 | md: "text-[2.3rem] lg:text-5xl leading-9", 18 | lg: "text-4xl lg:text-6xl", 19 | }, 20 | fullWidth: { 21 | true: "w-full block", 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: "md", 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | "violet", 31 | "yellow", 32 | "blue", 33 | "cyan", 34 | "green", 35 | "pink", 36 | "foreground", 37 | ], 38 | class: "bg-clip-text text-transparent bg-gradient-to-b", 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", 45 | variants: { 46 | fullWidth: { 47 | true: "!w-full", 48 | }, 49 | }, 50 | defaultVariants: { 51 | fullWidth: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /services/frontend/components/reloader/Reloader.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CircularProgress, Progress } from "@heroui/react"; 4 | import { useRouter } from "next/navigation"; 5 | import React from "react"; 6 | 7 | export default function Reloader({ 8 | circle = false, 9 | refresh = 50, 10 | }: { 11 | circle?: boolean; 12 | refresh?: number; 13 | }) { 14 | const [value, setValue] = React.useState(0); 15 | const router = useRouter(); 16 | 17 | React.useEffect(() => { 18 | const interval = setInterval(() => { 19 | setValue((v) => (v >= 100 ? 0 : v + refresh)); 20 | if (value === 100) { 21 | clearInterval(interval); 22 | router.refresh(); 23 | } 24 | }, 1000); 25 | 26 | return () => clearInterval(interval); 27 | }, [value]); 28 | 29 | return circle ? ( 30 | 31 | ) : ( 32 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /services/frontend/components/runners/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Reloader from "../reloader/Reloader"; 4 | 5 | export default function RunnersHeading() { 6 | return ( 7 |
8 |
9 |

Runners

10 |
11 | 12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /services/frontend/components/search/new-chip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Chip, cn } from "@heroui/react"; 4 | import { startsWith } from "lodash"; 5 | 6 | // eslint-disable-next-line no-undef 7 | type NewChipProps = React.HTMLAttributes; 8 | 9 | // eslint-disable-next-line no-undef 10 | export const NewChip: React.FC< 11 | NewChipProps & { 12 | background?: string; 13 | isBorderGradient?: boolean; 14 | } 15 | > = ({ isBorderGradient, background = "#050713", className }) => { 16 | let style = {}; 17 | const linearGradientBg = startsWith(background, "--") 18 | ? `hsl(var(${background}))` 19 | : background; 20 | 21 | if (isBorderGradient) { 22 | style = { 23 | border: "solid 1px transparent", 24 | backgroundImage: `linear-gradient(${linearGradientBg}, ${linearGradientBg}), linear-gradient(to right, #F54180, #338EF7)`, 25 | backgroundOrigin: "border-box", 26 | backgroundClip: "padding-box, border-box", 27 | }; 28 | } 29 | 30 | return ( 31 | 42 | New 43 | 44 | ); 45 | }; 46 | 47 | NewChip.displayName = "NewChip"; 48 | -------------------------------------------------------------------------------- /services/frontend/components/search/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@heroui/react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | import * as React from "react"; 6 | 7 | const Popover = PopoverPrimitive.Root; 8 | 9 | const PopoverTrigger = PopoverPrimitive.Trigger; 10 | 11 | const PopoverAnchor = PopoverPrimitive.Anchor; 12 | 13 | const PopoverContent = ({ 14 | ref, 15 | className, 16 | align = "center", 17 | sideOffset = 4, 18 | ...props 19 | }: React.ComponentPropsWithoutRef & { 20 | ref: React.RefObject>; 21 | }) => ( 22 | 23 | 33 | 34 | ); 35 | 36 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 37 | 38 | export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }; 39 | -------------------------------------------------------------------------------- /services/frontend/components/search/regex-constants.ts: -------------------------------------------------------------------------------- 1 | const IMPORT_REGEX = 2 | /import(?:\s+\S.*(?:[\n\r\u2028\u2029]\s*|[\t\v\f \xA0\u1680\u2000-\u200A\u202F\u205F\u3000\uFEFF])|\s{2,})from\s+['"](@\/|..\/)(.*)['"];/g; 3 | const IMPORT_PATH_MATCH_REGEX = 4 | /^\S*import(?:\s[\w*\s{},]+from\s)?\s*['"](.+)['"];?/gm; 5 | 6 | export { IMPORT_PATH_MATCH_REGEX, IMPORT_REGEX }; 7 | -------------------------------------------------------------------------------- /services/frontend/components/search/sort.ts: -------------------------------------------------------------------------------- 1 | import type { SearchResultItem } from "./data"; 2 | 3 | import { sortBy } from "lodash"; 4 | 5 | import { getPriorityValue } from "./component-files"; 6 | 7 | export const sortSearchCategoryItems = (items: SearchResultItem[]) => { 8 | return sortBy(items, [ 9 | // Sort by 'isNew' values first 10 | (item: any) => item?.component?.attributes?.isNew, 11 | // Sort by 'featured' property: true values first 12 | (item: any) => item?.component?.attributes?.featured, 13 | // Sort by 'sortPriority' 14 | (item: any) => getPriorityValue(item?.component.attributes?.sortPriority), 15 | // Sort by 'groupOrder' in ascending order (lower values first) 16 | (item: any) => item?.component?.attributes?.groupOrder || 9999, 17 | // Then sort by 'name' 18 | "name", 19 | ]); 20 | }; 21 | -------------------------------------------------------------------------------- /services/frontend/components/search/use-update-effect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | /** 4 | * React effect hook that invokes only on update. 5 | * It doesn't invoke on mount 6 | */ 7 | export const useUpdateEffect: typeof useEffect = (effect, deps) => { 8 | const renderCycleRef = useRef(false); 9 | const effectCycleRef = useRef(false); 10 | 11 | useEffect(() => { 12 | const isMounted = renderCycleRef.current; 13 | const shouldRun = isMounted && effectCycleRef.current; 14 | 15 | if (shouldRun) { 16 | return effect(); 17 | } 18 | effectCycleRef.current = true; 19 | }, deps); 20 | 21 | useEffect(() => { 22 | renderCycleRef.current = true; 23 | 24 | return () => { 25 | renderCycleRef.current = false; 26 | }; 27 | }, []); 28 | }; 29 | -------------------------------------------------------------------------------- /services/frontend/components/user/cell-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { cn } from "@/components/cn/cn"; 4 | 5 | const CellWrapper = ({ 6 | ref, 7 | children, 8 | className, 9 | ...props 10 | // eslint-disable-next-line no-undef 11 | }: React.HTMLAttributes & { 12 | // eslint-disable-next-line no-undef 13 | ref: React.RefObject; 14 | }) => ( 15 |
23 | {children} 24 |
25 | ); 26 | 27 | CellWrapper.displayName = "CellWrapper"; 28 | 29 | export default CellWrapper; 30 | -------------------------------------------------------------------------------- /services/frontend/config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google"; 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-sans", 6 | }); 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ["latin"], 10 | variable: "--font-mono", 11 | }); 12 | -------------------------------------------------------------------------------- /services/frontend/config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig; 2 | 3 | export const siteConfig = { 4 | name: "exFlow", 5 | description: "exFlow is an workflow automation tool", 6 | version: "1.3.1", 7 | navItems: [ 8 | { 9 | label: "Dashboard", 10 | href: "/", 11 | }, 12 | { 13 | label: "Projects", 14 | href: "/projects", 15 | }, 16 | { 17 | label: "Flows", 18 | href: "/flows", 19 | }, 20 | { 21 | label: "Runners", 22 | href: "/runners", 23 | }, 24 | ], 25 | navMenuItems: [ 26 | { 27 | label: "Dashboard", 28 | href: "/", 29 | }, 30 | { 31 | label: "Projects", 32 | href: "/projects", 33 | }, 34 | { 35 | label: "Flows", 36 | href: "/flows", 37 | }, 38 | { 39 | label: "Runners", 40 | href: "/runners", 41 | }, 42 | ], 43 | links: { 44 | github: "https://github.com/v1Flows/exFlow", 45 | docs: "https://exflow.org", 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /services/frontend/lib/IconWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@heroui/react"; 2 | import React from "react"; 3 | 4 | export const IconWrapper = ({ children, className }: any) => ( 5 |
11 | {children} 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/checkTaken.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export default async function CheckUserTaken( 4 | id: string, 5 | email: string, 6 | username: string, 7 | ) { 8 | "use client"; 9 | try { 10 | const headers = new Headers(); 11 | 12 | headers.append("Content-Type", "application/json"); 13 | const res = await fetch( 14 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/user/taken`, 15 | { 16 | method: "POST", 17 | headers, 18 | body: JSON.stringify({ 19 | id, 20 | email, 21 | username, 22 | }), 23 | }, 24 | ); 25 | const data = await res.json(); 26 | 27 | return data; 28 | } catch { 29 | return { error: "Failed to fetch data" }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/deleteSession.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cookies } from "next/headers"; 3 | import { redirect } from "next/navigation"; 4 | 5 | export async function deleteSession() { 6 | const cookieStore = await cookies(); 7 | 8 | cookieStore.delete("session"); 9 | cookieStore.delete("user"); 10 | 11 | redirect("/"); 12 | } 13 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/login.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export default async function LoginAPI( 4 | email: string, 5 | password: string, 6 | remember_me: boolean, 7 | ) { 8 | "use client"; 9 | try { 10 | const headers = new Headers(); 11 | 12 | headers.append("Content-Type", "application/json"); 13 | const res = await fetch( 14 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/login`, 15 | { 16 | method: "POST", 17 | headers, 18 | body: JSON.stringify({ 19 | email, 20 | password, 21 | remember_me, 22 | }), 23 | }, 24 | ); 25 | const data = await res.json(); 26 | 27 | return data; 28 | } catch { 29 | return { error: "Failed to login" }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/signup.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | export default async function SignUpAPI( 4 | email: string, 5 | username: string, 6 | password: string, 7 | ) { 8 | "use client"; 9 | try { 10 | const headers = new Headers(); 11 | 12 | headers.append("Content-Type", "application/json"); 13 | const res = await fetch( 14 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/register`, 15 | { 16 | method: "POST", 17 | headers, 18 | body: JSON.stringify({ 19 | email, 20 | username, 21 | password, 22 | }), 23 | }, 24 | ); 25 | const data = await res.json(); 26 | 27 | return data; 28 | } catch { 29 | return { error: "Failed to fetch data" }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/updateSession.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cookies } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function updateSession() { 6 | "use client"; 7 | const cookieStore = await cookies(); 8 | const session = cookieStore.get("session")?.value; 9 | 10 | try { 11 | const headers = new Headers(); 12 | 13 | headers.append("Content-Type", "application/json"); 14 | if (session) { 15 | headers.append("Authorization", session); 16 | } 17 | 18 | const response = await fetch( 19 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/token/refresh`, 20 | { 21 | method: "POST", 22 | headers, 23 | }, 24 | ); 25 | const data = await response.json(); 26 | 27 | const res = NextResponse.next(); 28 | 29 | res.cookies.set({ 30 | name: "session", 31 | value: data.token, 32 | expires: new Date(data.expires_at * 1000), 33 | httpOnly: true, 34 | }); 35 | res.cookies.set({ 36 | name: "user", 37 | value: JSON.stringify(data.user), 38 | expires: new Date(data.expires_at * 1000), 39 | httpOnly: true, 40 | }); 41 | 42 | return true; 43 | } catch { 44 | return false; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/admin/POST/CreateUser.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | export default async function AdminCreateUser( 6 | email: string, 7 | username: string, 8 | password: string, 9 | role: string, 10 | ) { 11 | "use client"; 12 | try { 13 | const cookieStore = await cookies(); 14 | const token = cookieStore.get("session"); 15 | 16 | if (!token) { 17 | return { 18 | success: false, 19 | error: "Authentication token not found", 20 | message: "User is not authenticated", 21 | }; 22 | } 23 | 24 | const res = await fetch( 25 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/admin/users`, 26 | { 27 | method: "POST", 28 | headers: { 29 | "Content-Type": "application/json", 30 | Authorization: token.value, 31 | }, 32 | body: JSON.stringify({ 33 | email, 34 | username, 35 | password, 36 | role, 37 | }), 38 | }, 39 | ); 40 | 41 | if (!res.ok) { 42 | const errorData = await res.json(); 43 | 44 | return { 45 | success: false, 46 | error: `API error: ${res.status} ${res.statusText}`, 47 | message: errorData.message || "An error occurred", 48 | }; 49 | } 50 | 51 | const data = await res.json(); 52 | 53 | return data; 54 | } catch { 55 | return { error: "Failed to create user" }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/flow/flow.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | type Flow = { 6 | flow: object; 7 | }; 8 | 9 | type ErrorResponse = { 10 | success: false; 11 | error: string; 12 | message: string; 13 | }; 14 | 15 | type SuccessResponse = { 16 | success: true; 17 | data: Flow; 18 | }; 19 | 20 | export async function GetFlow( 21 | flowID: any, 22 | ): Promise { 23 | try { 24 | const cookieStore = await cookies(); 25 | const token = cookieStore.get("session"); 26 | 27 | const res = await fetch( 28 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/flows/${flowID}`, 29 | { 30 | method: "GET", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: token.value, 34 | }, 35 | }, 36 | ); 37 | 38 | if (!res.ok) { 39 | const errorData = await res.json(); 40 | 41 | return { 42 | success: false, 43 | error: `API error: ${res.status} ${res.statusText}`, 44 | message: errorData.message || "An error occurred", 45 | }; 46 | } 47 | 48 | const data = await res.json(); 49 | 50 | return { 51 | success: true, 52 | data, 53 | }; 54 | } catch (error) { 55 | return { 56 | success: false, 57 | error: error instanceof Error ? error.message : "Unknown error occurred", 58 | message: "Failed to fetch flow", 59 | }; 60 | } 61 | } 62 | 63 | export default GetFlow; 64 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/folder/single.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | type Folder = { 6 | folder: object; 7 | }; 8 | 9 | type ErrorResponse = { 10 | success: false; 11 | error: string; 12 | message: string; 13 | }; 14 | 15 | type SuccessResponse = { 16 | success: true; 17 | data: Folder; 18 | }; 19 | 20 | export async function GetFolder( 21 | folderID: any, 22 | ): Promise { 23 | try { 24 | const cookieStore = await cookies(); 25 | const token = cookieStore.get("session"); 26 | 27 | const res = await fetch( 28 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/folders/${folderID}`, 29 | { 30 | method: "GET", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: token.value, 34 | }, 35 | }, 36 | ); 37 | 38 | if (!res.ok) { 39 | const errorData = await res.json(); 40 | 41 | return { 42 | success: false, 43 | error: `API error: ${res.status} ${res.statusText}`, 44 | message: errorData.message || "An error occurred", 45 | }; 46 | } 47 | 48 | const data = await res.json(); 49 | 50 | return { 51 | success: true, 52 | data, 53 | }; 54 | } catch (error) { 55 | return { 56 | success: false, 57 | error: error instanceof Error ? error.message : "Unknown error occurred", 58 | message: "Failed to fetch folder", 59 | }; 60 | } 61 | } 62 | 63 | export default GetFolder; 64 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/page/settings.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | type Settings = { 4 | settings: any; 5 | }; 6 | 7 | type ErrorResponse = { 8 | success: false; 9 | error: string; 10 | message: string; 11 | }; 12 | 13 | type SuccessResponse = { 14 | success: true; 15 | data: Settings; 16 | }; 17 | 18 | export async function PageGetSettings(): Promise< 19 | SuccessResponse | ErrorResponse 20 | > { 21 | try { 22 | const res = await fetch( 23 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/page/settings`, 24 | { 25 | method: "GET", 26 | headers: { 27 | "Content-Type": "application/json", 28 | }, 29 | }, 30 | ); 31 | 32 | if (!res.ok) { 33 | const errorData = await res.json(); 34 | 35 | return { 36 | success: false, 37 | error: `API error: ${res.status} ${res.statusText}`, 38 | message: errorData.message || "An error occurred", 39 | }; 40 | } 41 | 42 | const data = await res.json(); 43 | 44 | return { 45 | success: true, 46 | data, 47 | }; 48 | } catch (error) { 49 | return { 50 | success: false, 51 | error: error instanceof Error ? error.message : "Unknown error occurred", 52 | message: "Failed to fetch settings", 53 | }; 54 | } 55 | } 56 | 57 | export default PageGetSettings; 58 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/runner/get.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | type Runners = { 6 | runners: []; 7 | }; 8 | 9 | type ErrorResponse = { 10 | success: false; 11 | error: string; 12 | message: string; 13 | }; 14 | 15 | type SuccessResponse = { 16 | success: true; 17 | data: Runners; 18 | }; 19 | 20 | export async function GetRunners(): Promise { 21 | try { 22 | const cookieStore = await cookies(); 23 | const token = cookieStore.get("session"); 24 | 25 | const res = await fetch( 26 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/runners/`, 27 | { 28 | method: "GET", 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: token.value, 32 | }, 33 | }, 34 | ); 35 | 36 | if (!res.ok) { 37 | const errorData = await res.json(); 38 | 39 | return { 40 | success: false, 41 | error: `API error: ${res.status} ${res.statusText}`, 42 | message: errorData.message || "An error occurred", 43 | }; 44 | } 45 | 46 | const data = await res.json(); 47 | 48 | return { 49 | success: true, 50 | data, 51 | }; 52 | } catch (error) { 53 | return { 54 | success: false, 55 | error: error instanceof Error ? error.message : "Unknown error occurred", 56 | message: "Failed to fetch runners", 57 | }; 58 | } 59 | } 60 | 61 | export default GetRunners; 62 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/user/DELETE/delete.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | type Result = { 6 | result: string; 7 | }; 8 | 9 | type ErrorResponse = { 10 | success: false; 11 | error: string; 12 | message: string; 13 | }; 14 | 15 | type SuccessResponse = { 16 | success: true; 17 | data: Result; 18 | }; 19 | 20 | export default async function DeleteUser(): Promise< 21 | SuccessResponse | ErrorResponse 22 | > { 23 | try { 24 | const cookieStore = await cookies(); 25 | const token = cookieStore.get("session"); 26 | 27 | if (!token) { 28 | return { 29 | success: false, 30 | error: "Authentication token not found", 31 | message: "User is not authenticated", 32 | }; 33 | } 34 | 35 | const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/user/`, { 36 | method: "DELETE", 37 | headers: { 38 | "Content-Type": "application/json", 39 | Authorization: token.value, 40 | }, 41 | }); 42 | 43 | if (!res.ok) { 44 | const errorData = await res.json(); 45 | 46 | return { 47 | success: false, 48 | error: `API error: ${res.status} ${res.statusText}`, 49 | message: errorData.message || "An error occurred", 50 | }; 51 | } 52 | 53 | const data = await res.json(); 54 | 55 | return { 56 | success: true, 57 | data, 58 | }; 59 | } catch (error) { 60 | return { 61 | success: false, 62 | error: error instanceof Error ? error.message : "Unknown error occurred", 63 | message: "Failed to delete user", 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/user/stats.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | type Stats = { 6 | stats: object; 7 | }; 8 | 9 | type ErrorResponse = { 10 | success: false; 11 | error: string; 12 | message: string; 13 | }; 14 | 15 | type SuccessResponse = { 16 | success: true; 17 | data: Stats; 18 | }; 19 | 20 | export async function GetUserStats(): Promise { 21 | try { 22 | const cookieStore = await cookies(); 23 | const token = cookieStore.get("session"); 24 | 25 | if (!token) { 26 | return { 27 | success: false, 28 | error: "Authentication token not found", 29 | message: "User is not authenticated", 30 | }; 31 | } 32 | 33 | const res = await fetch( 34 | `${process.env.NEXT_PUBLIC_API_URL}/api/v1/user/stats`, 35 | { 36 | method: "GET", 37 | headers: { 38 | "Content-Type": "application/json", 39 | Authorization: token.value, 40 | }, 41 | }, 42 | ); 43 | const data = await res.json(); 44 | 45 | return { 46 | success: true, 47 | data, 48 | }; 49 | } catch (error) { 50 | return { 51 | success: false, 52 | error: error instanceof Error ? error.message : "Unknown error occurred", 53 | message: "Failed to fetch user stats", 54 | }; 55 | } 56 | } 57 | 58 | export default GetUserStats; 59 | -------------------------------------------------------------------------------- /services/frontend/lib/functions/canEditProject.tsx: -------------------------------------------------------------------------------- 1 | export default function canEditProject( 2 | userId: string, 3 | projectMembers: { user_id: string; role: string }[], 4 | ): boolean { 5 | const isProjectOwner = projectMembers.some( 6 | (member) => member.user_id === userId && member.role === "Owner", 7 | ); 8 | const isProjectEditor = projectMembers.some( 9 | (member) => member.user_id === userId && member.role === "Editor", 10 | ); 11 | const isProjectMember = projectMembers.some( 12 | (member) => member.user_id === userId && member.role === "Viewer", 13 | ); 14 | 15 | if (isProjectOwner) { 16 | return true; 17 | } else if (isProjectEditor) { 18 | return true; 19 | } 20 | if (isProjectMember) { 21 | return false; 22 | } 23 | 24 | return false; 25 | } 26 | -------------------------------------------------------------------------------- /services/frontend/lib/functions/userExecutionStepStyle.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | type DisplayStyle = "accordion" | "table"; 5 | 6 | interface ExecutionStepStyleStore { 7 | displayStyle: DisplayStyle; 8 | setDisplayStyle: (style: DisplayStyle) => void; 9 | } 10 | 11 | export const useExecutionStepStyleStore = create()( 12 | persist( 13 | (set) => ({ 14 | displayStyle: "accordion", 15 | setDisplayStyle: (style) => set({ displayStyle: style }), 16 | }), 17 | { 18 | name: "executionStepDisplayStyle", // key in localStorage 19 | }, 20 | ), 21 | ); 22 | 23 | // Usage example in a component: 24 | // const { displayStyle, setDisplayStyle } = useExecutionsStyleStore(); 25 | -------------------------------------------------------------------------------- /services/frontend/lib/functions/userExecutionsStyle.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | type DisplayStyle = "compact" | "list" | "table"; 5 | 6 | interface ExecutionsStyleStore { 7 | displayStyle: DisplayStyle; 8 | setDisplayStyle: (style: DisplayStyle) => void; 9 | } 10 | 11 | export const useExecutionsStyleStore = create()( 12 | persist( 13 | (set) => ({ 14 | displayStyle: "list", 15 | setDisplayStyle: (style) => set({ displayStyle: style }), 16 | }), 17 | { 18 | name: "executionsDisplayStyle", // key in localStorage 19 | }, 20 | ), 21 | ); 22 | 23 | // Usage example in a component: 24 | // const { displayStyle, setDisplayStyle } = useExecutionsStyleStore(); 25 | -------------------------------------------------------------------------------- /services/frontend/lib/logout.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cookies } from "next/headers"; 3 | import { redirect } from "next/navigation"; 4 | 5 | export async function Logout() { 6 | const c = await cookies(); 7 | 8 | c.delete("session"); 9 | c.delete("user"); 10 | 11 | redirect("/"); 12 | } 13 | -------------------------------------------------------------------------------- /services/frontend/lib/setSession.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | export async function setSession(token: string, user: any, expires_at: number) { 6 | const expires = new Date(expires_at * 1000); 7 | const c = await cookies(); 8 | 9 | c.set({ 10 | name: "session", 11 | value: token, 12 | expires, 13 | httpOnly: true, 14 | }); 15 | 16 | c.set({ 17 | name: "user", 18 | value: JSON.stringify(user), 19 | expires, 20 | httpOnly: true, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /services/frontend/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ClassValue } from "clsx"; 2 | 3 | import { clsx } from "clsx"; 4 | import { twMerge } from "tailwind-merge"; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | -------------------------------------------------------------------------------- /services/frontend/next.config.js: -------------------------------------------------------------------------------- 1 | const { 2 | PHASE_DEVELOPMENT_SERVER, 3 | PHASE_PRODUCTION_BUILD, 4 | } = require('next/constants'); 5 | const dotenv = require('dotenv'); 6 | 7 | // Load environment variables from .env file 8 | dotenv.config(); 9 | 10 | /** @type {(phase: string, defaultConfig: import("next").NextConfig) => Promise} */ 11 | module.exports = async (phase) => { 12 | /** @type {import("next").NextConfig} */ 13 | const nextConfig = { 14 | output: 'standalone', 15 | trailingSlash: false, 16 | env: { 17 | NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, 18 | }, 19 | reactStrictMode: true, 20 | images: { 21 | unoptimized: true, 22 | domains: ['localhost', 'exflow.org'], 23 | }, 24 | }; 25 | if (phase === PHASE_DEVELOPMENT_SERVER || phase === PHASE_PRODUCTION_BUILD) { 26 | const withSerwist = (await import('@serwist/next')).default({ 27 | // Note: This is only an example. If you use Pages Router, 28 | // use something else that works, such as "service-worker/index.ts". 29 | swSrc: 'app/sw.ts', 30 | swDest: 'public/sw.js', 31 | }); 32 | return withSerwist(nextConfig); 33 | } 34 | 35 | return nextConfig; 36 | }; 37 | -------------------------------------------------------------------------------- /services/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-144-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/android/android-launchericon-144-144.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-192-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/android/android-launchericon-192-192.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-48-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/android/android-launchericon-48-48.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-512-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/android/android-launchericon-512-512.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-72-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/android/android-launchericon-72-72.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-96-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/android/android-launchericon-96-96.png -------------------------------------------------------------------------------- /services/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/favicon.ico -------------------------------------------------------------------------------- /services/frontend/public/images/bg-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/images/bg-gradient.png -------------------------------------------------------------------------------- /services/frontend/public/images/ef_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/images/ef_logo.png -------------------------------------------------------------------------------- /services/frontend/public/images/ef_logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/images/ef_logo_512.png -------------------------------------------------------------------------------- /services/frontend/public/images/full_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/images/full_dashboard.png -------------------------------------------------------------------------------- /services/frontend/public/ios/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/100.png -------------------------------------------------------------------------------- /services/frontend/public/ios/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/1024.png -------------------------------------------------------------------------------- /services/frontend/public/ios/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/114.png -------------------------------------------------------------------------------- /services/frontend/public/ios/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/120.png -------------------------------------------------------------------------------- /services/frontend/public/ios/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/128.png -------------------------------------------------------------------------------- /services/frontend/public/ios/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/144.png -------------------------------------------------------------------------------- /services/frontend/public/ios/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/152.png -------------------------------------------------------------------------------- /services/frontend/public/ios/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/16.png -------------------------------------------------------------------------------- /services/frontend/public/ios/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/167.png -------------------------------------------------------------------------------- /services/frontend/public/ios/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/180.png -------------------------------------------------------------------------------- /services/frontend/public/ios/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/192.png -------------------------------------------------------------------------------- /services/frontend/public/ios/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/20.png -------------------------------------------------------------------------------- /services/frontend/public/ios/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/256.png -------------------------------------------------------------------------------- /services/frontend/public/ios/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/29.png -------------------------------------------------------------------------------- /services/frontend/public/ios/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/32.png -------------------------------------------------------------------------------- /services/frontend/public/ios/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/40.png -------------------------------------------------------------------------------- /services/frontend/public/ios/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/50.png -------------------------------------------------------------------------------- /services/frontend/public/ios/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/512.png -------------------------------------------------------------------------------- /services/frontend/public/ios/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/57.png -------------------------------------------------------------------------------- /services/frontend/public/ios/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/58.png -------------------------------------------------------------------------------- /services/frontend/public/ios/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/60.png -------------------------------------------------------------------------------- /services/frontend/public/ios/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/64.png -------------------------------------------------------------------------------- /services/frontend/public/ios/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/72.png -------------------------------------------------------------------------------- /services/frontend/public/ios/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/76.png -------------------------------------------------------------------------------- /services/frontend/public/ios/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/80.png -------------------------------------------------------------------------------- /services/frontend/public/ios/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/ios/87.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/LargeTile.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/LargeTile.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/LargeTile.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/LargeTile.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/LargeTile.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SmallTile.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SmallTile.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SmallTile.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SmallTile.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SmallTile.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-20.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-30.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-36.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-40.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-44.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-60.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-64.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-72.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-80.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Square44x44Logo.targetsize-96.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/v1Flows/exFlow/357aa9133e13f4a25a154046a5825a9907b4cf01/services/frontend/public/windows11/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /services/frontend/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { heroui } from "@heroui/theme"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | sans: ["var(--font-sans)"], 14 | mono: ["var(--font-geist-mono)"], 15 | }, 16 | animation: { 17 | "border-beam": "border-beam calc(var(--duration)*1s) infinite linear", 18 | gradient: "gradient 8s linear infinite", 19 | }, 20 | keyframes: { 21 | "border-beam": { 22 | "100%": { 23 | "offset-distance": "100%", 24 | }, 25 | }, 26 | gradient: { 27 | to: { 28 | backgroundPosition: "var(--bg-size) 0", 29 | }, 30 | }, 31 | "shine-pulse": { 32 | "0%": { 33 | "background-position": "0% 0%", 34 | }, 35 | "50%": { 36 | "background-position": "100% 100%", 37 | }, 38 | to: { 39 | "background-position": "0% 0%", 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | darkMode: "class", 46 | plugins: [heroui()], 47 | }; 48 | -------------------------------------------------------------------------------- /services/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es5", 5 | "jsx": "preserve", 6 | "lib": ["dom", "dom.iterable", "esnext", "webworker"], 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "paths": { 10 | "@/*": ["./*"] 11 | }, 12 | "resolveJsonModule": true, 13 | "types": [ 14 | // This allows Serwist to type `window.serwist`. 15 | "@serwist/next/typings" 16 | ], 17 | "allowJs": true, 18 | "strict": false, 19 | "noEmit": true, 20 | "esModuleInterop": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "isolatedModules": true, 23 | "skipLibCheck": true, 24 | "plugins": [ 25 | { 26 | "name": "next" 27 | } 28 | ] 29 | }, 30 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 31 | "exclude": ["node_modules", "public/sw.js"] 32 | } 33 | -------------------------------------------------------------------------------- /services/frontend/types/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | // eslint-disable-next-line no-undef 4 | export type IconSvgProps = SVGProps & { 5 | size?: number; 6 | }; 7 | -------------------------------------------------------------------------------- /services/frontend/updateSessionInterval.ts: -------------------------------------------------------------------------------- 1 | import { updateSession } from "@/lib/auth/updateSession"; 2 | 3 | const TEN_MINUTES = 600000; 4 | 5 | setInterval(async () => { 6 | await updateSession(); 7 | }, TEN_MINUTES); 8 | --------------------------------------------------------------------------------