├── .vscode └── settings.json ├── services ├── frontend │ ├── .npmrc │ ├── public │ │ ├── favicon.ico │ │ ├── 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 │ │ ├── images │ │ │ ├── ef_logo.png │ │ │ ├── bg-gradient.png │ │ │ ├── ef_logo_512.png │ │ │ ├── full_dashboard.png │ │ │ ├── justlab_logo_minimal_transparent.png │ │ │ ├── justflow_logo_full_transparent_dark.png │ │ │ └── justflow_logo_full_transpartent_white.png │ │ ├── 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 │ │ │ ├── StoreLogo.scale-100.png │ │ │ ├── StoreLogo.scale-125.png │ │ │ ├── StoreLogo.scale-150.png │ │ │ ├── StoreLogo.scale-200.png │ │ │ ├── StoreLogo.scale-400.png │ │ │ ├── SplashScreen.scale-100.png │ │ │ ├── SplashScreen.scale-125.png │ │ │ ├── SplashScreen.scale-150.png │ │ │ ├── SplashScreen.scale-200.png │ │ │ ├── SplashScreen.scale-400.png │ │ │ ├── Square44x44Logo.scale-100.png │ │ │ ├── Square44x44Logo.scale-125.png │ │ │ ├── Square44x44Logo.scale-150.png │ │ │ ├── Square44x44Logo.scale-200.png │ │ │ ├── Square44x44Logo.scale-400.png │ │ │ ├── Wide310x150Logo.scale-100.png │ │ │ ├── Wide310x150Logo.scale-125.png │ │ │ ├── Wide310x150Logo.scale-150.png │ │ │ ├── Wide310x150Logo.scale-200.png │ │ │ ├── Wide310x150Logo.scale-400.png │ │ │ ├── Square150x150Logo.scale-100.png │ │ │ ├── Square150x150Logo.scale-125.png │ │ │ ├── Square150x150Logo.scale-150.png │ │ │ ├── Square150x150Logo.scale-200.png │ │ │ ├── Square150x150Logo.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 │ │ │ ├── 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.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 │ │ └── android │ │ │ ├── android-launchericon-48-48.png │ │ │ ├── android-launchericon-72-72.png │ │ │ ├── android-launchericon-96-96.png │ │ │ ├── android-launchericon-144-144.png │ │ │ ├── android-launchericon-192-192.png │ │ │ └── android-launchericon-512-512.png │ ├── postcss.config.mjs │ ├── instrumentation.ts │ ├── app │ │ ├── flows │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ ├── page.tsx │ │ │ │ └── execution │ │ │ │ └── [executionID] │ │ │ │ └── page.tsx │ │ ├── setup │ │ │ └── page.tsx │ │ ├── alerts │ │ │ └── page.tsx │ │ ├── auth │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ │ └── page.tsx │ │ ├── runners │ │ │ └── page.tsx │ │ ├── projects │ │ │ ├── page.tsx │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── page.tsx │ │ ├── maintenance │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── profile │ │ │ └── page.tsx │ │ ├── error.tsx │ │ ├── admin │ │ │ ├── system │ │ │ │ └── page.tsx │ │ │ ├── users │ │ │ │ └── page.tsx │ │ │ ├── runners │ │ │ │ └── page.tsx │ │ │ ├── executions │ │ │ │ └── page.tsx │ │ │ └── projects │ │ │ │ └── page.tsx │ │ ├── sw.ts │ │ └── providers.tsx │ ├── types │ │ └── index.ts │ ├── updateSessionInterval.ts │ ├── components │ │ ├── error │ │ │ └── ErrorCard.tsx │ │ ├── alerts │ │ │ ├── heading.tsx │ │ │ └── page-client.tsx │ │ ├── runners │ │ │ └── heading.tsx │ │ ├── app-content.tsx │ │ ├── search │ │ │ ├── regex-constants.ts │ │ │ └── search.tsx │ │ ├── admin │ │ │ ├── system │ │ │ │ └── heading.tsx │ │ │ ├── projects │ │ │ │ └── heading.tsx │ │ │ └── users │ │ │ │ └── heading.tsx │ │ ├── auth │ │ │ └── login-page-client.tsx │ │ ├── modals │ │ │ └── projects │ │ │ │ ├── cell-wrapper.tsx │ │ │ │ ├── cn.ts │ │ │ │ └── user-cell.tsx │ │ ├── user │ │ │ ├── cell-wrapper.tsx │ │ │ └── profile-page-client.tsx │ │ ├── cn │ │ │ └── cn.ts │ │ ├── primitives.ts │ │ └── ui │ │ │ └── marquee.tsx │ ├── hero.ts │ ├── lib │ │ ├── utils.ts │ │ ├── logout.ts │ │ ├── auth │ │ │ ├── deleteSession.ts │ │ │ ├── checkTaken.ts │ │ │ ├── login.ts │ │ │ ├── signup.ts │ │ │ └── updateSession.ts │ │ ├── IconWrapper.tsx │ │ ├── setSession.ts │ │ ├── functions │ │ │ ├── userAlertsStyle.tsx │ │ │ ├── userExecutionStepStyle.tsx │ │ │ ├── canEditProject.tsx │ │ │ └── userExecutionsStyle.tsx │ │ ├── swr │ │ │ ├── api │ │ │ │ └── executions.ts │ │ │ └── provider.tsx │ │ ├── setup.tsx │ │ └── fetch │ │ │ ├── alert │ │ │ └── POST │ │ │ │ └── send.ts │ │ │ ├── user │ │ │ └── stats.ts │ │ │ ├── page │ │ │ └── settings.ts │ │ │ ├── admin │ │ │ └── POST │ │ │ │ └── CreateUser.ts │ │ │ ├── runner │ │ │ └── get.ts │ │ │ ├── flow │ │ │ └── flow.ts │ │ │ └── folder │ │ │ └── single.ts │ ├── restart-helper │ │ └── package.json │ ├── config │ │ ├── fonts.ts │ │ └── site.ts │ ├── styles │ │ └── globals.css │ ├── instrumentation.node.ts │ ├── tsconfig.json │ ├── next.config.js │ └── tailwind.config.ts └── backend │ ├── router │ ├── health.go │ ├── page.go │ ├── setup.go │ ├── auth.go │ ├── token.go │ ├── folders.go │ ├── alerts.go │ └── runners.go │ ├── functions │ ├── httperror │ │ ├── statusConflict.go │ │ ├── statusNotFound.go │ │ ├── unauthorized.go │ │ ├── statusBadRequest.go │ │ └── internalServerError.go │ ├── gatekeeper │ │ ├── checkAccountStatus.go │ │ ├── checkAdmin.go │ │ └── checkRequestUserProjectModifyRole.go │ ├── admin_stats │ │ ├── users_per_role.go │ │ └── users_per_plan.go │ ├── user │ │ └── sendUserNotification.go │ ├── project │ │ ├── checkIfUserIsProjectMember.go │ │ └── createAuditEntry.go │ ├── auth │ │ ├── serviceToken.go │ │ ├── alertflowAutoRunnerToken.go │ │ ├── projectAutoRunnerToken.go │ │ ├── projectToken.go │ │ ├── runnerToken.go │ │ ├── validateTokenDB.go │ │ └── userToken.go │ ├── runner │ │ ├── generate_alertflow_auto_join_token.go │ │ ├── generate_runner_token.go │ │ └── generate_project_auto_join_token.go │ └── background_checks │ │ ├── checkDisconnectedAutoRunners.go │ │ └── main.go │ ├── pkg │ ├── telemetry │ │ ├── flow_metrics.go │ │ ├── business_metrics.go │ │ └── db_metrics.go │ └── models │ │ ├── auth.go │ │ ├── folders.go │ │ ├── stats.go │ │ ├── notifications.go │ │ ├── tokens.go │ │ ├── executions.go │ │ └── audit.go │ ├── config │ └── config.yaml │ ├── handlers │ ├── tokens │ │ ├── validate.go │ │ ├── validate_service_token.go │ │ └── delete_runner_token.go │ ├── admins │ │ ├── get_runners.go │ │ ├── get_flows.go │ │ ├── get_users.go │ │ ├── delete_token.go │ │ ├── get_folders.go │ │ ├── get_tokens.go │ │ ├── get_executions.go │ │ ├── update_settings.go │ │ ├── update_token.go │ │ ├── change_runner_status.go │ │ ├── disable_user.go │ │ ├── delete_user.go │ │ ├── user_send_admin_notify.go │ │ ├── rotate-auto-join-token.go │ │ ├── get_settings.go │ │ ├── change_project_status.go │ │ ├── create_user.go │ │ ├── generate_service_token.go │ │ └── get_projects.go │ ├── runners │ │ ├── get_links.go │ │ ├── set_actions.go │ │ ├── heartbeat.go │ │ ├── busy.go │ │ ├── edit.go │ │ └── get_runners.go │ ├── executions │ │ ├── heartbeat.go │ │ └── get_execution.go │ ├── users │ │ ├── welcomed.go │ │ ├── disable.go │ │ ├── get_notifications.go │ │ ├── details.go │ │ ├── read_notification.go │ │ ├── archive_notification.go │ │ ├── unread_notification.go │ │ ├── unarchive_notification.go │ │ ├── change_details.go │ │ └── delete.go │ ├── pages │ │ └── get_settings.go │ ├── flows │ │ └── get_stats.go │ ├── folders │ │ ├── get_folders.go │ │ └── get_folder.go │ └── projects │ │ ├── get_tokens.go │ │ ├── get_runners.go │ │ ├── decline_invite.go │ │ ├── get_audit_logs.go │ │ └── accept_invite.go │ ├── database │ ├── migrations │ │ ├── migrations.go │ │ └── 11_create_alerts_table.go │ └── create_settings.go │ ├── Dockerfile │ └── middlewares │ └── runner.go ├── deployment-examples ├── minimal-with-config │ ├── backend-config.yaml │ └── docker-compose.yaml └── config-with-runner │ ├── backend-config.yaml │ ├── runner-config.yaml │ └── docker-compose.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── dependabot.yml ├── docker-compose.yaml ├── entrypoint.sh └── release-notes.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /services/frontend/.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@heroui/* 2 | package-lock=true -------------------------------------------------------------------------------- /services/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/favicon.ico -------------------------------------------------------------------------------- /services/frontend/public/ios/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/100.png -------------------------------------------------------------------------------- /services/frontend/public/ios/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/1024.png -------------------------------------------------------------------------------- /services/frontend/public/ios/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/114.png -------------------------------------------------------------------------------- /services/frontend/public/ios/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/120.png -------------------------------------------------------------------------------- /services/frontend/public/ios/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/128.png -------------------------------------------------------------------------------- /services/frontend/public/ios/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/144.png -------------------------------------------------------------------------------- /services/frontend/public/ios/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/152.png -------------------------------------------------------------------------------- /services/frontend/public/ios/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/16.png -------------------------------------------------------------------------------- /services/frontend/public/ios/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/167.png -------------------------------------------------------------------------------- /services/frontend/public/ios/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/180.png -------------------------------------------------------------------------------- /services/frontend/public/ios/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/192.png -------------------------------------------------------------------------------- /services/frontend/public/ios/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/20.png -------------------------------------------------------------------------------- /services/frontend/public/ios/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/256.png -------------------------------------------------------------------------------- /services/frontend/public/ios/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/29.png -------------------------------------------------------------------------------- /services/frontend/public/ios/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/32.png -------------------------------------------------------------------------------- /services/frontend/public/ios/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/40.png -------------------------------------------------------------------------------- /services/frontend/public/ios/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/50.png -------------------------------------------------------------------------------- /services/frontend/public/ios/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/512.png -------------------------------------------------------------------------------- /services/frontend/public/ios/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/57.png -------------------------------------------------------------------------------- /services/frontend/public/ios/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/58.png -------------------------------------------------------------------------------- /services/frontend/public/ios/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/60.png -------------------------------------------------------------------------------- /services/frontend/public/ios/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/64.png -------------------------------------------------------------------------------- /services/frontend/public/ios/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/72.png -------------------------------------------------------------------------------- /services/frontend/public/ios/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/76.png -------------------------------------------------------------------------------- /services/frontend/public/ios/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/80.png -------------------------------------------------------------------------------- /services/frontend/public/ios/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/ios/87.png -------------------------------------------------------------------------------- /services/frontend/public/images/ef_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/ef_logo.png -------------------------------------------------------------------------------- /services/frontend/public/images/bg-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/bg-gradient.png -------------------------------------------------------------------------------- /services/frontend/public/images/ef_logo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/ef_logo_512.png -------------------------------------------------------------------------------- /services/frontend/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /services/frontend/public/images/full_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/full_dashboard.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/LargeTile.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/LargeTile.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/LargeTile.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/LargeTile.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/LargeTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/LargeTile.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SmallTile.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SmallTile.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SmallTile.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SmallTile.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SmallTile.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SmallTile.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/StoreLogo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/StoreLogo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/StoreLogo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/StoreLogo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/StoreLogo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/StoreLogo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SplashScreen.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SplashScreen.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SplashScreen.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/SplashScreen.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/SplashScreen.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-48-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/android/android-launchericon-48-48.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-72-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/android/android-launchericon-72-72.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-96-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/android/android-launchericon-96-96.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Wide310x150Logo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Wide310x150Logo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Wide310x150Logo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Wide310x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Wide310x150Logo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === "nodejs") { 3 | await import("./instrumentation.node"); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-144-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/android/android-launchericon-144-144.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-192-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/android/android-launchericon-192-192.png -------------------------------------------------------------------------------- /services/frontend/public/android/android-launchericon-512-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/android/android-launchericon-512-512.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square150x150Logo.scale-100.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square150x150Logo.scale-125.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square150x150Logo.scale-150.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square150x150Logo.scale-400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square150x150Logo.scale-400.png -------------------------------------------------------------------------------- /services/frontend/app/flows/page.tsx: -------------------------------------------------------------------------------- 1 | import FlowsPageClient from "@/components/flows/page-client"; 2 | 3 | export default function FlowsPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/app/setup/page.tsx: -------------------------------------------------------------------------------- 1 | import SetupPageClient from "@/components/setup/page-client"; 2 | 3 | export default function RunnersPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/public/images/justlab_logo_minimal_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/justlab_logo_minimal_transparent.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-16.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-20.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-24.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-256.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-30.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-32.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-36.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-40.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-44.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-48.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-60.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-64.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-72.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-80.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.targetsize-96.png -------------------------------------------------------------------------------- /services/frontend/app/alerts/page.tsx: -------------------------------------------------------------------------------- 1 | import AlertsPageClient from "@/components/alerts/page-client"; 2 | 3 | export default function RunnersPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/public/images/justflow_logo_full_transparent_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/justflow_logo_full_transparent_dark.png -------------------------------------------------------------------------------- /services/frontend/app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import LoginPageClient from "@/components/auth/login-page-client"; 2 | 3 | export default function LoginPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/app/runners/page.tsx: -------------------------------------------------------------------------------- 1 | import RunnersPageClient from "@/components/runners/page-client"; 2 | 3 | export default function RunnersPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/public/images/justflow_logo_full_transpartent_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/images/justflow_logo_full_transpartent_white.png -------------------------------------------------------------------------------- /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/app/projects/page.tsx: -------------------------------------------------------------------------------- 1 | import ProjectsPageClient from "@/components/projects/page-client"; 2 | 3 | export default function ProjectsPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/app/page.tsx: -------------------------------------------------------------------------------- 1 | import DashboardHomePageClient from "@/components/dashboard/home-page-client"; 2 | 3 | export default function DashboardHomePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-16.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-20.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-24.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-256.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-30.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-32.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-36.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-40.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-44.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-48.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-60.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-64.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-72.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-80.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-unplated_targetsize-96.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/hero.ts: -------------------------------------------------------------------------------- 1 | // hero.ts 2 | import { heroui } from "@heroui/react"; 3 | // or import from theme package if you are using individual packages. 4 | // import { heroui } from "@heroui/theme"; 5 | export default heroui(); 6 | -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png -------------------------------------------------------------------------------- /services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustLABv1/justflow/HEAD/services/frontend/public/windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png -------------------------------------------------------------------------------- /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/restart-helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "restart-frontend", 3 | "version": "1.0.0", 4 | "description": "Helper script to restart frontend after setup", 5 | "main": "restart.js", 6 | "scripts": { 7 | "restart": "node restart.js" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /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/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", "message": "Service is healthy", "service": "backend"}) 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /services/frontend/app/flows/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import FlowPageClient from "@/components/flows/flow/page-client"; 2 | 3 | export default async function FlowPage({ 4 | params, 5 | }: { 6 | params: Promise<{ id: string }>; 7 | }) { 8 | const { id } = await params; 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /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/app/projects/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import ProjectPageClient from "@/components/projects/project-page-client"; 2 | 3 | export default async function ProjectPage({ 4 | params, 5 | }: { 6 | params: Promise<{ id: string }>; 7 | }) { 8 | const { id } = await params; 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /services/frontend/components/alerts/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default function AlertsHeading() { 4 | return ( 5 |
6 |
7 |

Alerts

8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /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/frontend/components/runners/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export default function RunnersHeading() { 4 | return ( 5 |
6 |
7 |

Runners

8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /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/frontend/components/app-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ReactNode } from "react"; 4 | 5 | import { SetupGuard } from "@/lib/setup"; 6 | 7 | interface AppContentProps { 8 | children: ReactNode; 9 | } 10 | 11 | export function AppContent({ children }: AppContentProps) { 12 | return {children}; 13 | } 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/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/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/app/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers"; 2 | 3 | import ProfilePageClient from "@/components/user/profile-page-client"; 4 | 5 | export default async function ProfilePage() { 6 | const cookieStore = await cookies(); 7 | const session = cookieStore.get("session")?.value; 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/backend/router/page.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/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 | -------------------------------------------------------------------------------- /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] -------------------------------------------------------------------------------- /services/frontend/app/flows/[id]/execution/[executionID]/page.tsx: -------------------------------------------------------------------------------- 1 | import ExecutionPageClient from "@/components/executions/execution-page-client"; 2 | 3 | export default async function DashboardExecutionPage({ 4 | params, 5 | }: { 6 | params: Promise<{ id: string; executionID: string }>; 7 | }) { 8 | const { id, executionID } = await params; 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /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 | errorMessage := "Unknown error" 11 | if err != nil { 12 | errorMessage = err.Error() 13 | } 14 | context.JSON(http.StatusInternalServerError, gin.H{"message": message, "error": errorMessage}) 15 | } 16 | -------------------------------------------------------------------------------- /services/backend/pkg/telemetry/flow_metrics.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | FlowExecutionsTotal = promauto.NewCounterVec( 10 | prometheus.CounterOpts{ 11 | Name: "flow_executions_total", 12 | Help: "Total number of flow executions triggered", 13 | }, 14 | []string{"status", "flow_id"}, 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /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 | justflow: 10 | enabled: true 11 | url: http://justflow: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 | -------------------------------------------------------------------------------- /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/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 | # Minimum 32 characters, recommended 64+ characters 16 | master_secret: "your-very-long-and-secure-master-secret-here" 17 | # Fallback key for legacy data (optional) 18 | key: "legacy-key-for-backward-compatibility" 19 | 20 | jwt: 21 | secret: null 22 | 23 | runner: 24 | shared_runner_secret: null 25 | -------------------------------------------------------------------------------- /services/frontend/components/admin/system/heading.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Icon } from "@iconify/react"; 4 | 5 | export default function AdminSystemHeading() { 6 | return ( 7 |
8 |
9 |
10 | 11 |

System

12 |
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /services/backend/handlers/tokens/validate.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/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/functions/gatekeeper/checkAccountStatus.go: -------------------------------------------------------------------------------- 1 | package gatekeeper 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/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/JustLABv1/justflow/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/handlers/admins/get_runners.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | "github.com/JustLABv1/justflow/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/router/setup.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/handlers/setup" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func Setup(rg *gin.RouterGroup, configFile string, frontendEnv string) { 9 | setupGroup := rg.Group("/setup") 10 | { 11 | setupGroup.POST("/configure", func(c *gin.Context) { 12 | setup.SetupSystem(c, configFile, frontendEnv) 13 | }) 14 | setupGroup.GET("/status", func(c *gin.Context) { 15 | setup.CheckSetupStatus(c, configFile, frontendEnv) 16 | }) 17 | setupGroup.POST("/validate", setup.ValidateSetupData) 18 | setupGroup.POST("/restart", setup.RestartApplication) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/get_flows.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/router/auth.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/handlers/auths" 5 | "github.com/JustLABv1/justflow/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/get_users.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | "github.com/JustLABv1/justflow/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/delete_token.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/get_folders.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | "github.com/JustLABv1/justflow/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_tokens.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/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/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/backend/functions/admin_stats/users_per_role.go: -------------------------------------------------------------------------------- 1 | package admin_stats 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/frontend/lib/auth/checkTaken.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { serverFetch } from "../fetch/serverFetch"; 4 | 5 | export default async function CheckUserTaken( 6 | id: string, 7 | email: string, 8 | username: string, 9 | ) { 10 | try { 11 | const res = await serverFetch(`/api/v1/auth/user/taken`, { 12 | method: "POST", 13 | headers: { "Content-Type": "application/json" }, 14 | body: JSON.stringify({ 15 | id, 16 | email, 17 | username, 18 | }), 19 | timeout: 8000, 20 | retries: 1, 21 | }); 22 | const data = await res.json(); 23 | 24 | return data; 25 | } catch { 26 | return { error: "Failed to fetch data" }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/login.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { serverFetch } from "../fetch/serverFetch"; 4 | 5 | export default async function LoginAPI( 6 | email: string, 7 | password: string, 8 | remember_me: boolean, 9 | ) { 10 | try { 11 | const res = await serverFetch(`/api/v1/auth/login`, { 12 | method: "POST", 13 | headers: { "Content-Type": "application/json" }, 14 | body: JSON.stringify({ 15 | email, 16 | password, 17 | remember_me, 18 | }), 19 | timeout: 8000, 20 | retries: 1, 21 | }); 22 | const data = await res.json(); 23 | 24 | return data; 25 | } catch { 26 | return { error: "Failed to login" }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services/frontend/lib/auth/signup.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { serverFetch } from "../fetch/serverFetch"; 4 | 5 | export default async function SignUpAPI( 6 | email: string, 7 | username: string, 8 | password: string, 9 | ) { 10 | try { 11 | const res = await serverFetch(`/api/v1/auth/register`, { 12 | method: "POST", 13 | headers: { "Content-Type": "application/json" }, 14 | body: JSON.stringify({ 15 | email, 16 | username, 17 | password, 18 | }), 19 | timeout: 8000, 20 | retries: 1, 21 | }); 22 | const data = await res.json(); 23 | 24 | return data; 25 | } catch { 26 | return { error: "Failed to fetch data" }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /services/frontend/components/auth/login-page-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import LoginPageComponent from "@/components/auth/loginPage"; 4 | import { PageSkeleton } from "@/components/loading/page-skeleton"; 5 | import { usePageSettings } from "@/lib/swr/hooks/flows"; 6 | 7 | export default function LoginPageClient() { 8 | const { settings, isLoading, isError } = usePageSettings(); 9 | 10 | if (isLoading) { 11 | return ; 12 | } 13 | 14 | if (isError || !settings) { 15 | // For login page, we can fall back to some default settings or show a basic login form 16 | return ; 17 | } 18 | 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /services/frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @config "../tailwind.config.ts"; 3 | 4 | @theme inline { 5 | --animate-shine: shine var(--duration) infinite linear; 6 | --animate-ripple: ripple var(--duration, 2s) ease calc(var(--i, 0) * 0.2s) 7 | infinite; 8 | 9 | @keyframes shine { 10 | 0% { 11 | background-position: 0% 0%; 12 | } 13 | 50% { 14 | background-position: 100% 100%; 15 | } 16 | to { 17 | background-position: 0% 0%; 18 | } 19 | } 20 | 21 | @keyframes ripple { 22 | 0%, 23 | 100% { 24 | transform: translate(-50%, -50%) scale(1); 25 | } 26 | 50% { 27 | transform: translate(-50%, -50%) scale(0.9); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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/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/instrumentation.node.ts: -------------------------------------------------------------------------------- 1 | import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc"; 2 | import { resourceFromAttributes } from "@opentelemetry/resources"; 3 | import { NodeSDK } from "@opentelemetry/sdk-node"; 4 | import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node"; 5 | import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions"; 6 | 7 | const sdk = new NodeSDK({ 8 | resource: resourceFromAttributes({ 9 | [ATTR_SERVICE_NAME]: "justflow-frontend", 10 | }), 11 | spanProcessor: new SimpleSpanProcessor( 12 | new OTLPTraceExporter({ 13 | url: process.env.JUSTFLOW_OTEL_COLLECTOR || "http://localhost:4317", 14 | }), 15 | ), 16 | }); 17 | 18 | sdk.start(); 19 | -------------------------------------------------------------------------------- /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/frontend/lib/functions/userAlertsStyle.tsx: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | import { persist } from "zustand/middleware"; 3 | 4 | type DisplayStyle = "list"; 5 | 6 | interface AlertsStyleStore { 7 | displayStyle: DisplayStyle; 8 | setDisplayStyle: (style: DisplayStyle) => void; 9 | } 10 | 11 | export const useAlertsStyleStore = create()( 12 | persist( 13 | (set) => ({ 14 | displayStyle: "list", 15 | setDisplayStyle: (style) => set({ displayStyle: style }), 16 | }), 17 | { 18 | name: "alertsDisplayStyle", // key in localStorage 19 | }, 20 | ), 21 | ); 22 | 23 | // Usage example in a component: 24 | // const { displayStyle, setDisplayStyle } = useAlertsStyleStore(); 25 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/get_executions.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/runners/get_links.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/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/frontend/lib/swr/api/executions.ts: -------------------------------------------------------------------------------- 1 | import APIStartExecution from "@/lib/fetch/executions/start"; 2 | 3 | // Client-side API helpers for mutations 4 | export async function startExecution( 5 | flowId: string, 6 | ): Promise<{ success: boolean; error?: string }> { 7 | try { 8 | const result = await APIStartExecution(flowId); 9 | 10 | if (result.success) { 11 | return { success: true }; 12 | } else { 13 | return { 14 | success: false, 15 | error: 16 | "message" in result ? result.message : "Failed to start execution", 17 | }; 18 | } 19 | } catch (error) { 20 | return { 21 | success: false, 22 | error: error instanceof Error ? error.message : "Unknown error occurred", 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /services/backend/functions/user/sendUserNotification.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/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/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/handlers/executions/heartbeat.go: -------------------------------------------------------------------------------- 1 | package executions 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 8 | "github.com/JustLABv1/justflow/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/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/backend/functions/admin_stats/users_per_plan.go: -------------------------------------------------------------------------------- 1 | package admin_stats 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/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 | -------------------------------------------------------------------------------- /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 | justflow: 16 | image: justnz/justflow:latest 17 | depends_on: 18 | - db 19 | ports: 20 | - "3000:3000" # JustFlow frontend 21 | - "8080:8080" # JustFlow backend 22 | volumes: 23 | - justflow_data:/etc/justflow 24 | entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./justflow-backend --config /etc/justflow/config.yaml & exec node /app/server.js"] 25 | 26 | volumes: 27 | db_data: 28 | justflow_data: -------------------------------------------------------------------------------- /services/backend/router/token.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/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/backend/functions/project/checkIfUserIsProjectMember.go: -------------------------------------------------------------------------------- 1 | package functions_project 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/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/frontend/app/admin/system/page.tsx: -------------------------------------------------------------------------------- 1 | import { Divider, Spacer } from "@heroui/react"; 2 | 3 | import AdminSystemHeading from "@/components/admin/system/heading"; 4 | import AdminGetPageSettings from "@/lib/fetch/admin/settings"; 5 | import { AdminSystemSettings } from "@/components/admin/system/settings"; 6 | import { AdminSystemStatus } from "@/components/admin/system/status"; 7 | 8 | export default async function AdminSettingsPage() { 9 | const settingsData = AdminGetPageSettings(); 10 | 11 | const [settings] = (await Promise.all([settingsData])) as any; 12 | 13 | return ( 14 |
15 | 16 | 17 | 18 | 19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /services/backend/functions/project/createAuditEntry.go: -------------------------------------------------------------------------------- 1 | package functions_project 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/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 | -------------------------------------------------------------------------------- /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 | justflow: 16 | image: justnz/justflow:latest 17 | depends_on: 18 | - db 19 | ports: 20 | - "3000:3000" # JustFlow frontend 21 | - "8080:8080" # JustFlow backend 22 | volumes: 23 | - ./backend-config.yaml:/etc/justflow/backend_config.yaml 24 | entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./justflow-backend --config /etc/justflow/backend_config.yaml & exec node /app/server.js"] 25 | 26 | volumes: 27 | db_data: -------------------------------------------------------------------------------- /services/backend/handlers/users/welcomed.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine as builder 2 | LABEL org.opencontainers.image.source = "https://github.com/JustLabV1/justflow" 3 | 4 | WORKDIR /app/backend 5 | 6 | COPY go.mod go.sum ./ 7 | RUN go mod download 8 | 9 | COPY . . 10 | 11 | # Build 12 | RUN go build -o justflow-backend 13 | 14 | FROM alpine:3.22 as runner 15 | LABEL org.opencontainers.image.source = "https://github.com/JustLabV1/justflow" 16 | WORKDIR /app 17 | 18 | COPY --from=builder /app/backend/justflow-backend /app/ 19 | 20 | RUN addgroup --system --gid 1001 justflow 21 | RUN adduser --system --uid 1001 justflow 22 | 23 | RUN mkdir -p /etc/justflow \ 24 | && chown -R justflow:justflow /etc/justflow 25 | 26 | RUN chown -R justflow:justflow /app 27 | 28 | VOLUME [ "/etc/justflow" ] 29 | 30 | EXPOSE 8080 31 | 32 | CMD [ "/justflow-backend", "--config", "/etc/justflow/config.yaml" ] 33 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/update_settings.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/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 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Colors for output 5 | GREEN='\033[0;32m' 6 | BLUE='\033[0;34m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | echo "${BLUE}Starting JustFlow...${NC}" 11 | 12 | # Start backend in background 13 | echo "${YELLOW}Starting backend...${NC}" 14 | ./justflow-backend --config /etc/justflow/config.yaml & 15 | BACKEND_PID=$! 16 | 17 | # Give backend a moment to start 18 | sleep 2 19 | 20 | # Start frontend in background 21 | echo "${YELLOW}Starting frontend...${NC}" 22 | node /app/server.js & 23 | FRONTEND_PID=$! 24 | 25 | echo "${GREEN}✓ JustFlow is running${NC}" 26 | echo " Backend: http://localhost:8080" 27 | echo " Frontend: http://localhost:3000" 28 | echo " Setup page: http://localhost:3000/setup" 29 | 30 | # Wait for both processes 31 | wait $BACKEND_PID $FRONTEND_PID 32 | 33 | echo "${YELLOW}JustFlow shutting down...${NC}" 34 | -------------------------------------------------------------------------------- /services/backend/functions/auth/serviceToken.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/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/handlers/pages/get_settings.go: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | "github.com/JustLABv1/justflow/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/runners/set_actions.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/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/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/backend/router/folders.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/handlers/folders" 5 | "github.com/JustLABv1/justflow/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/functions/auth/alertflowAutoRunnerToken.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/services/backend/pkg/models" 8 | 9 | "github.com/golang-jwt/jwt/v5" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | func GenerateJustFlowAutoRunnerJWT(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/runner/generate_alertflow_auto_join_token.go: -------------------------------------------------------------------------------- 1 | package functions_runner 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 8 | "github.com/JustLABv1/justflow/services/backend/pkg/models" 9 | 10 | "github.com/google/uuid" 11 | "github.com/uptrace/bun" 12 | ) 13 | 14 | func GenerateJustFlowAutoJoinToken(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.GenerateJustFlowAutoRunnerJWT(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_runner_token.go: -------------------------------------------------------------------------------- 1 | package functions_runner 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/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/handlers/admins/update_token.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/change_runner_status.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/frontend/lib/setup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { setupApi } from "./api"; 4 | 5 | /** 6 | * Component to automatically redirect to setup if not configured 7 | * NOTE: Setup routing is now primarily handled by middleware. 8 | * This component is kept minimal to avoid client-side setup checks. 9 | */ 10 | export function SetupGuard({ children }: { children: React.ReactNode }) { 11 | // Simply render children - middleware handles setup page routing 12 | return <>{children}; 13 | } 14 | 15 | /** 16 | * Simple function to check setup status without hooks (for use in middleware, etc.) 17 | */ 18 | export async function checkSetupStatus(): Promise<{ 19 | isSetup: boolean; 20 | error?: string; 21 | }> { 22 | try { 23 | const status = await setupApi.checkStatus(); 24 | 25 | return { isSetup: status.is_setup }; 26 | } catch (error: any) { 27 | return { isSetup: false, error: error.message }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /services/backend/functions/auth/projectAutoRunnerToken.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/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/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/backend/functions/auth/projectToken.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/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/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/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/runner/generate_project_auto_join_token.go: -------------------------------------------------------------------------------- 1 | package functions_runner 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/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/router/alerts.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/handlers/alerts" 5 | "github.com/JustLABv1/justflow/services/backend/middlewares" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/uptrace/bun" 9 | ) 10 | 11 | func Alerts(router *gin.RouterGroup, db *bun.DB) { 12 | alert := router.Group("/alerts").Use(middlewares.Mixed(db)) 13 | { 14 | alert.GET("/", func(c *gin.Context) { 15 | alerts.GetMultiple(c, db) 16 | }) 17 | alert.GET("/grouped", func(c *gin.Context) { 18 | alerts.GetGrouped(c, db) 19 | }) 20 | alert.GET("/:alertID", func(c *gin.Context) { 21 | alerts.GetSingle(c, db) 22 | }) 23 | 24 | alert.POST("/", func(c *gin.Context) { 25 | alerts.CreateAlert(c, db) 26 | }) 27 | alert.PUT("/:alertID", func(c *gin.Context) { 28 | alerts.Update(c, db) 29 | }) 30 | alert.DELETE("/:alertID", func(c *gin.Context) { 31 | alerts.Delete(c, db) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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/backend/database/create_settings.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/pkg/models" 7 | 8 | functions_runner "github.com/JustLABv1/justflow/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.GenerateJustFlowAutoJoinToken(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/handlers/tokens/validate_service_token.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/admins/disable_user.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/delete_user.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/users/disable.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/admins/user_send_admin_notify.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | functions "github.com/JustLABv1/justflow/services/backend/functions/user" 6 | "github.com/JustLABv1/justflow/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | justflow: 16 | image: justnz/justflow:latest 17 | depends_on: 18 | - db 19 | ports: 20 | - "3000:3000" # JustFlow frontend 21 | - "8080:8080" # JustFlow backend 22 | volumes: 23 | - ./backend-config.yaml:/etc/justflow/backend_config.yaml 24 | entrypoint: ["/bin/sh", "-c", "until pg_isready -h db -p 5432 -U postgres; do sleep 1; done; exec ./justflow-backend --config /etc/justflow/backend_config.yaml & exec node /app/server.js"] 25 | 26 | runner: 27 | image: justnz/runner:latest 28 | depends_on: 29 | - justflow 30 | ports: 31 | - "8081:8081" 32 | volumes: 33 | - ./runner-config.yaml:/app/config/config.yaml 34 | 35 | volumes: 36 | db_data: -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## [Version 2.0.0-beta.15] - 2025-12-02 4 | 5 | 🚧 Beta Release 🚧 6 | Please use with caution! 7 | 8 | ### Added 9 | - Project Based Encryption 10 | - SWR for data fetching and caching 11 | - Tailwind v4 12 | - Merge AlertFlow into this Project 13 | - Initial Setup. No manual config or env file required anymore 14 | - Predefined Project Actions 15 | - Flow Setting: Always Cleanup Workspace 16 | - Set log level and format for backend via setup ui 17 | - Show shared_runner_secret after setup is completed 18 | - OTel Tracing Support for frontend and backend 19 | 20 | ### Changed 21 | - Major UI improvements (Introducing UI v2) 22 | - New Welcome Style 23 | - New Name: JustFlow 24 | - Refactored Admin Settings Page and System Management 25 | - New Logo 26 | - Updated ReadMe 27 | - Reduce Configuration overhead 28 | 29 | ### Fixed 30 | - Runner Workspace Cleanup 31 | - Execution Cancel 32 | - Improved Middleware for Frontend 33 | 34 | ### Known Issues 35 | - No known issues at this time. 36 | 37 | --- 38 | 39 | *Thank you for using JustFlow!* -------------------------------------------------------------------------------- /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/JustLABv1/justflow/services/backend/functions/httperror" 10 | "github.com/JustLABv1/justflow/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/handlers/flows/get_stats.go: -------------------------------------------------------------------------------- 1 | package flows 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/flow_stats" 7 | "github.com/JustLABv1/justflow/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/users/get_notifications.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/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/backend/handlers/users/details.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 7 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 8 | "github.com/JustLABv1/justflow/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/admins/rotate-auto-join-token.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | functions_runner "github.com/JustLABv1/justflow/services/backend/functions/runner" 8 | "github.com/JustLABv1/justflow/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.GenerateJustFlowAutoJoinToken(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/users/read_notification.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/archive_notification.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/unread_notification.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/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/functions/background_checks/checkDisconnectedAutoRunners.go: -------------------------------------------------------------------------------- 1 | package background_checks 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/JustLABv1/justflow/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/handlers/users/unarchive_notification.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/folders/get_folders.go: -------------------------------------------------------------------------------- 1 | package folders 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 7 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 8 | "github.com/JustLABv1/justflow/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/frontend/lib/fetch/alert/POST/send.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | type ErrorResponse = { 4 | success: false; 5 | error: string; 6 | message: string; 7 | }; 8 | 9 | type SuccessResponse = { 10 | success: true; 11 | }; 12 | 13 | export default async function SimulateAlert( 14 | target: any, 15 | payload: any, 16 | ): Promise { 17 | try { 18 | const res = await fetch(target, { 19 | method: "POST", 20 | headers: { 21 | "Content-Type": "application/json", 22 | }, 23 | body: payload, 24 | }); 25 | 26 | if (!res.ok) { 27 | const errorData = await res.json(); 28 | 29 | return { 30 | success: false, 31 | error: `API error: ${res.status} ${res.statusText}`, 32 | message: errorData.message || "An error occurred", 33 | }; 34 | } 35 | 36 | return { 37 | success: true, 38 | }; 39 | } catch (error) { 40 | return { 41 | success: false, 42 | error: error instanceof Error ? error.message : "Unknown error occurred", 43 | message: "Failed to send alert", 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /services/frontend/components/search/search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, Kbd } from "@heroui/react"; 4 | import { Icon } from "@iconify/react"; 5 | import { useEffect } from "react"; 6 | 7 | import { useSearch } from "./search-context"; 8 | 9 | export default function Search({ 10 | projects, 11 | flows, 12 | folders, 13 | trigger, 14 | }: { 15 | projects: any; 16 | flows: any; 17 | folders: any; 18 | // eslint-disable-next-line no-undef 19 | trigger?: React.ReactNode; 20 | }) { 21 | const { onOpen, setContextData } = useSearch(); 22 | 23 | useEffect(() => { 24 | setContextData({ projects, flows, folders }); 25 | }, [projects, flows, folders, setContextData]); 26 | 27 | if (trigger) { 28 | return <>{trigger}; 29 | } 30 | 31 | return ( 32 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /services/backend/functions/auth/userToken.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/services/backend/pkg/models" 8 | "github.com/JustLABv1/justflow/services/backend/pkg/telemetry" 9 | 10 | "github.com/golang-jwt/jwt/v5" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | func GenerateJWT(id uuid.UUID, rememberMe bool) (tokenString string, ExpiresAt int64, err error) { 15 | var jwtKey = []byte(config.Config.JWT.Secret) 16 | var expirationTime time.Time 17 | 18 | if rememberMe { 19 | expirationTime = time.Now().Add(7 * 24 * time.Hour) 20 | } else { 21 | expirationTime = time.Now().Add(12 * time.Hour) 22 | } 23 | 24 | claims := &models.JWTClaim{ 25 | ID: id, 26 | Type: "user", 27 | RegisteredClaims: jwt.RegisteredClaims{ 28 | ExpiresAt: jwt.NewNumericDate(expirationTime), 29 | }, 30 | } 31 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 32 | tokenString, err = token.SignedString(jwtKey) 33 | ExpiresAt = expirationTime.Unix() 34 | 35 | if err == nil { 36 | telemetry.UserLoginTotal.Inc() 37 | } 38 | 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /services/backend/handlers/runners/heartbeat.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | "github.com/JustLABv1/justflow/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/users/change_details.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/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 | import SWRProvider from "@/lib/swr/provider"; 10 | import { SearchProvider } from "@/components/search/search-context"; 11 | import SearchModal from "@/components/search/search-modal"; 12 | 13 | type ThemeProviderProps = React.ComponentProps; 14 | 15 | export type ProvidersProps = { 16 | children: React.ReactNode; 17 | themeProps?: ThemeProviderProps; 18 | }; 19 | 20 | export function Providers({ children, themeProps }: ProvidersProps) { 21 | const router = useRouter(); 22 | 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {children} 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /services/frontend/lib/swr/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SWRConfig } from "swr"; 4 | import { ReactNode } from "react"; 5 | 6 | interface SWRProviderProps { 7 | children: ReactNode; 8 | } 9 | 10 | export default function SWRProvider({ children }: SWRProviderProps) { 11 | return ( 12 | { 26 | // Basic error logging; adapt to your telemetry if needed 27 | // Keep minimal to avoid noisy logs for expected client errors 28 | // eslint-disable-next-line no-console 29 | console.error("SWR error", { key, err }); 30 | }, 31 | }} 32 | > 33 | {children} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /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/handlers/users/delete.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/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 | interface AlertsStyleStore { 12 | displayStyle: DisplayStyle; 13 | setDisplayStyle: (style: DisplayStyle) => void; 14 | } 15 | 16 | export const useExecutionsStyleStore = create()( 17 | persist( 18 | (set) => ({ 19 | displayStyle: "list", 20 | setDisplayStyle: (style) => set({ displayStyle: style }), 21 | }), 22 | { 23 | name: "executionsDisplayStyle", // key in localStorage 24 | }, 25 | ), 26 | ); 27 | 28 | export const useAlertsStyleStore = create()( 29 | persist( 30 | (set) => ({ 31 | displayStyle: "list", 32 | setDisplayStyle: (style) => set({ displayStyle: style }), 33 | }), 34 | { 35 | name: "alertsDisplayStyle", // key in localStorage 36 | }, 37 | ), 38 | ); 39 | 40 | // Usage example in a component: 41 | // const { displayStyle, setDisplayStyle } = useExecutionsStyleStore(); 42 | -------------------------------------------------------------------------------- /services/backend/database/migrations/11_create_alerts_table.go: -------------------------------------------------------------------------------- 1 | package migrations 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/JustLABv1/justflow/services/backend/pkg/models" 8 | 9 | "github.com/uptrace/bun" 10 | ) 11 | 12 | func init() { 13 | Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { 14 | return createAlertsSchema(ctx, db) 15 | }, func(ctx context.Context, db *bun.DB) error { 16 | return dropAlertsSchema(ctx, db) 17 | }) 18 | } 19 | 20 | func createAlertsSchema(ctx context.Context, db *bun.DB) error { 21 | models := []interface{}{ 22 | (*models.Alerts)(nil), 23 | } 24 | 25 | for _, model := range models { 26 | _, err := db.NewCreateTable().Model(model).IfNotExists().Exec(ctx) 27 | if err != nil { 28 | return fmt.Errorf("failed to create table: %v", err) 29 | } 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func dropAlertsSchema(ctx context.Context, db *bun.DB) error { 36 | models := []interface{}{ 37 | (*models.Alerts)(nil), 38 | } 39 | 40 | for _, model := range models { 41 | _, err := db.NewDropTable().Model(model).IfExists().Cascade().Exec(ctx) 42 | if err != nil { 43 | return fmt.Errorf("failed to drop table: %v", err) 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /services/backend/handlers/folders/get_folder.go: -------------------------------------------------------------------------------- 1 | package folders 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 8 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 9 | "github.com/JustLABv1/justflow/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/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 | path: '/etc/justflow/.env', 10 | }); 11 | 12 | /** @type {(phase: string, defaultConfig: import("next").NextConfig) => Promise} */ 13 | module.exports = async (phase) => { 14 | /** @type {import("next").NextConfig} */ 15 | const nextConfig = { 16 | output: 'standalone', 17 | trailingSlash: false, 18 | env: { 19 | NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, 20 | }, 21 | reactStrictMode: true, 22 | images: { 23 | unoptimized: true, 24 | domains: ['localhost', 'justlab.app'], 25 | }, 26 | }; 27 | if (phase === PHASE_DEVELOPMENT_SERVER || phase === PHASE_PRODUCTION_BUILD) { 28 | const withSerwist = (await import('@serwist/next')).default({ 29 | // Note: This is only an example. If you use Pages Router, 30 | // use something else that works, such as "service-worker/index.ts". 31 | swSrc: 'app/sw.ts', 32 | swDest: 'public/sw.js', 33 | }); 34 | return withSerwist(nextConfig); 35 | } 36 | 37 | return nextConfig; 38 | }; 39 | -------------------------------------------------------------------------------- /services/backend/functions/gatekeeper/checkRequestUserProjectModifyRole.go: -------------------------------------------------------------------------------- 1 | package gatekeeper 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/pkg/models/executions.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "github.com/uptrace/bun" 8 | ) 9 | 10 | type Executions struct { 11 | bun.BaseModel `bun:"table:executions"` 12 | 13 | ID uuid.UUID `bun:",pk,type:uuid,default:gen_random_uuid()" json:"id"` 14 | FlowID string `bun:"flow_id,type:text,default:''" json:"flow_id"` 15 | RunnerID string `bun:"runner_id,type:text,default:''" json:"runner_id"` 16 | Status string `bun:"status,type:text,default:''" json:"status"` 17 | CreatedAt time.Time `bun:"created_at,type:timestamptz,default:now()" json:"created_at"` 18 | ExecutedAt time.Time `bun:"executed_at,type:timestamptz" json:"executed_at"` 19 | FinishedAt time.Time `bun:"finished_at,type:timestamptz" json:"finished_at"` 20 | LastHeartbeat time.Time `bun:"last_heartbeat,type:timestamptz" json:"last_heartbeat"` 21 | ScheduledAt time.Time `bun:"scheduled_at,type:timestamptz" json:"scheduled_at"` 22 | TriggeredBy string `bun:"triggered_by,type:text,default:'user'" json:"triggered_by"` 23 | AlertID string `bun:"alert_id,type:text,default:''" json:"alert_id"` 24 | } 25 | 26 | type ExecutionWithSteps struct { 27 | Executions 28 | Steps []ExecutionSteps `json:"steps"` 29 | } 30 | -------------------------------------------------------------------------------- /services/backend/pkg/telemetry/business_metrics.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | ) 7 | 8 | var ( 9 | // Auth metrics 10 | UserLoginTotal = promauto.NewCounter(prometheus.CounterOpts{ 11 | Name: "justflow_user_login_total", 12 | Help: "Total number of user logins (JWT generation)", 13 | }) 14 | 15 | // Execution metrics 16 | ExecutionFinishedTotal = promauto.NewCounterVec(prometheus.CounterOpts{ 17 | Name: "justflow_execution_finished_total", 18 | Help: "Total number of finished executions", 19 | }, []string{"status", "flow_id"}) 20 | 21 | ExecutionDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{ 22 | Name: "justflow_execution_duration_seconds", 23 | Help: "Duration of flow executions in seconds", 24 | Buckets: prometheus.ExponentialBuckets(1, 2, 10), // 1s, 2s, 4s, ... 512s 25 | }, []string{"status", "flow_id"}) 26 | 27 | // Background check metrics 28 | BackgroundCheckDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{ 29 | Name: "justflow_background_check_duration_seconds", 30 | Help: "Duration of background checks in seconds", 31 | Buckets: prometheus.DefBuckets, 32 | }, []string{"check_name"}) 33 | ) 34 | -------------------------------------------------------------------------------- /services/backend/handlers/projects/get_tokens.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/frontend/lib/auth/updateSession.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { cookies } from "next/headers"; 3 | import { NextResponse } from "next/server"; 4 | 5 | import { serverFetch } from "../fetch/serverFetch"; 6 | 7 | export async function updateSession() { 8 | "use client"; 9 | const cookieStore = await cookies(); 10 | const session = cookieStore.get("session")?.value; 11 | 12 | try { 13 | const headers = new Headers(); 14 | 15 | headers.append("Content-Type", "application/json"); 16 | if (session) { 17 | headers.append("Authorization", session); 18 | } 19 | 20 | const response = await serverFetch(`/api/v1/token/refresh`, { 21 | method: "POST", 22 | headers: Object.fromEntries(headers.entries()), 23 | timeout: 8000, 24 | retries: 1, 25 | }); 26 | const data = await response.json(); 27 | 28 | const res = NextResponse.next(); 29 | 30 | res.cookies.set({ 31 | name: "session", 32 | value: data.token, 33 | expires: new Date(data.expires_at * 1000), 34 | httpOnly: true, 35 | }); 36 | res.cookies.set({ 37 | name: "user", 38 | value: JSON.stringify(data.user), 39 | expires: new Date(data.expires_at * 1000), 40 | httpOnly: true, 41 | }); 42 | 43 | return true; 44 | } catch { 45 | return false; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /services/backend/handlers/projects/get_runners.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 8 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 9 | "github.com/JustLABv1/justflow/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/decline_invite.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | functions_project "github.com/JustLABv1/justflow/services/backend/functions/project" 7 | "github.com/JustLABv1/justflow/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/runners/busy.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | 7 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 8 | "github.com/JustLABv1/justflow/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/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/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 |

25 | Admin | Executions 26 |

27 | 28 | 34 | 35 | ) : ( 36 | 37 | )} 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /services/frontend/components/user/profile-page-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ErrorCard from "@/components/error/ErrorCard"; 4 | import { UserProfile } from "@/components/user/profile"; 5 | import { PageSkeleton } from "@/components/loading/page-skeleton"; 6 | import { usePageSettings, useUserDetails } from "@/lib/swr/hooks/flows"; 7 | 8 | interface ProfilePageClientProps { 9 | session?: string; 10 | } 11 | 12 | export default function ProfilePageClient({ session }: ProfilePageClientProps) { 13 | const { 14 | settings, 15 | isLoading: settingsLoading, 16 | isError: settingsError, 17 | } = usePageSettings(); 18 | const { user, isLoading: userLoading, isError: userError } = useUserDetails(); 19 | 20 | // Check if any essential data is still loading or missing 21 | const isLoading = settingsLoading || userLoading || !settings || !user; 22 | 23 | // Show loading state if essential data is still loading 24 | if (isLoading) { 25 | return ; 26 | } 27 | 28 | // Show error state 29 | const hasError = settingsError || userError; 30 | 31 | if (hasError) { 32 | return ( 33 | 37 | ); 38 | } 39 | 40 | return ; 41 | } 42 | -------------------------------------------------------------------------------- /services/frontend/config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig; 2 | 3 | export const siteConfig = { 4 | name: "JustFlow", 5 | description: "JustFlow is an workflow automation tool", 6 | version: "2.0.0-beta.16", 7 | navItems: [ 8 | { 9 | label: "Dashboard", 10 | href: "/", 11 | icon: "hugeicons:home-01", 12 | }, 13 | { 14 | label: "Projects", 15 | href: "/projects", 16 | icon: "hugeicons:ai-folder-01", 17 | }, 18 | { 19 | label: "Flows", 20 | href: "/flows", 21 | icon: "hugeicons:workflow-square-10", 22 | }, 23 | { 24 | label: "Alerts", 25 | href: "/alerts", 26 | icon: "hugeicons:alert-01", 27 | }, 28 | { 29 | label: "Runners", 30 | href: "/runners", 31 | icon: "hugeicons:ai-brain-04", 32 | }, 33 | ], 34 | navMenuItems: [ 35 | { 36 | label: "Dashboard", 37 | href: "/", 38 | }, 39 | { 40 | label: "Projects", 41 | href: "/projects", 42 | }, 43 | { 44 | label: "Flows", 45 | href: "/flows", 46 | }, 47 | { 48 | label: "Alerts", 49 | href: "/alerts", 50 | }, 51 | { 52 | label: "Runners", 53 | href: "/runners", 54 | }, 55 | ], 56 | links: { 57 | github: "https://github.com/JustLABv1/justflow", 58 | docs: "https://justlab.xyz", 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /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/backend/handlers/projects/get_audit_logs.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/runners/edit.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/projects/accept_invite.go: -------------------------------------------------------------------------------- 1 | package projects 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | functions_project "github.com/JustLABv1/justflow/services/backend/functions/project" 7 | "github.com/JustLABv1/justflow/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/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-linear-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/backend/handlers/runners/get_runners.go: -------------------------------------------------------------------------------- 1 | package runners 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 7 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 8 | "github.com/JustLABv1/justflow/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 | justflowRunners := make([]models.Runners, 0) 29 | err = db.NewSelect().Model(&justflowRunners).Where("shared_runner = true").Scan(context) 30 | if err != nil { 31 | httperror.InternalServerError(context, "Error collecting justflow runners from db", err) 32 | return 33 | } 34 | 35 | runners := append(projectRunners, justflowRunners...) 36 | 37 | context.JSON(http.StatusOK, gin.H{"runners": runners}) 38 | } 39 | -------------------------------------------------------------------------------- /services/frontend/components/ui/marquee.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | interface MarqueeProps { 4 | className?: string; 5 | reverse?: boolean; 6 | pauseOnHover?: boolean; 7 | // eslint-disable-next-line no-undef 8 | children?: React.ReactNode; 9 | vertical?: boolean; 10 | repeat?: number; 11 | [key: string]: any; 12 | } 13 | 14 | export function Marquee({ 15 | className, 16 | reverse, 17 | pauseOnHover = false, 18 | children, 19 | vertical = false, 20 | repeat = 4, 21 | ...props 22 | }: MarqueeProps) { 23 | return ( 24 |
35 | {Array(repeat) 36 | .fill(0) 37 | .map((_, i) => ( 38 |
47 | {children} 48 |
49 | ))} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /services/backend/functions/background_checks/main.go: -------------------------------------------------------------------------------- 1 | package background_checks 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/pkg/telemetry" 7 | "github.com/uptrace/bun" 8 | ) 9 | 10 | func Init(db *bun.DB) { 11 | ticker := time.NewTicker(1 * time.Minute) 12 | ticker2 := time.NewTicker(10 * time.Second) 13 | quit := make(chan struct{}) 14 | 15 | go func() { 16 | for { 17 | select { 18 | case <-ticker.C: 19 | runCheck("checkHangingExecutions", func() { checkHangingExecutions(db) }) 20 | runCheck("checkHangingExecutionSteps", func() { checkHangingExecutionSteps(db) }) 21 | runCheck("checkDisconnectedAutoRunners", func() { checkDisconnectedAutoRunners(db) }) 22 | runCheck("checkForFlowActionUpdates", func() { checkForFlowActionUpdates(db) }) 23 | runCheck("scheduleFlowExecutions", func() { scheduleFlowExecutions(db) }) 24 | case <-quit: 25 | ticker.Stop() 26 | return 27 | } 28 | } 29 | }() 30 | 31 | go func() { 32 | for { 33 | select { 34 | case <-ticker2.C: 35 | runCheck("checkScheduledExecutions", func() { checkScheduledExecutions(db) }) 36 | case <-quit: 37 | ticker2.Stop() 38 | return 39 | } 40 | } 41 | }() 42 | } 43 | 44 | func runCheck(name string, check func()) { 45 | start := time.Now() 46 | check() 47 | duration := time.Since(start).Seconds() 48 | telemetry.BackgroundCheckDurationSeconds.WithLabelValues(name).Observe(duration) 49 | } 50 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/get_settings.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | functions_runner "github.com/JustLABv1/justflow/services/backend/functions/runner" 8 | "github.com/JustLABv1/justflow/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 JustFlowRunnerAutoJoinToken if it got deleted or is not existing 23 | if settings.SharedRunnerAutoJoinToken == "" { 24 | settings.SharedRunnerAutoJoinToken, err = functions_runner.GenerateJustFlowAutoJoinToken(db) 25 | if err != nil { 26 | httperror.InternalServerError(context, "Error generating JustFlowRunnerAutoJoinToken", 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 JustFlowRunnerAutoJoinToken on db", err) 32 | return 33 | } 34 | } 35 | 36 | context.JSON(http.StatusOK, gin.H{"settings": settings}) 37 | } 38 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/user/stats.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | import { serverFetch } from "../serverFetch"; 6 | 7 | type Stats = { 8 | stats: object; 9 | }; 10 | 11 | type ErrorResponse = { 12 | success: false; 13 | error: string; 14 | message: string; 15 | }; 16 | 17 | type SuccessResponse = { 18 | success: true; 19 | data: Stats; 20 | }; 21 | 22 | export async function GetUserStats(): Promise { 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 serverFetch(`/api/v1/user/stats`, { 36 | method: "GET", 37 | headers: { 38 | "Content-Type": "application/json", 39 | Authorization: token.value, 40 | }, 41 | timeout: 8000, 42 | retries: 1, 43 | }); 44 | const data = await res.json(); 45 | 46 | return { 47 | success: true, 48 | data, 49 | }; 50 | } catch (error) { 51 | return { 52 | success: false, 53 | error: error instanceof Error ? error.message : "Unknown error occurred", 54 | message: "Failed to fetch user stats", 55 | }; 56 | } 57 | } 58 | 59 | export default GetUserStats; 60 | -------------------------------------------------------------------------------- /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/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 |

16 | Admin | Projects 17 |

18 |
19 |
20 |
21 | 28 |
29 | 30 |
31 | 34 |
35 |
36 |
37 | 38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/change_project_status.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 5 | functions_project "github.com/JustLABv1/justflow/services/backend/functions/project" 6 | "github.com/JustLABv1/justflow/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/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 | // Page settings are public server-side; still use serverFetch to get timeout/retries 23 | const { serverFetch } = await import("../serverFetch"); 24 | const res = await serverFetch(`/api/v1/page/settings`, { 25 | method: "GET", 26 | headers: { 27 | "Content-Type": "application/json", 28 | }, 29 | timeout: 8000, 30 | retries: 1, 31 | }); 32 | 33 | if (!res.ok) { 34 | const errorData = await res.json(); 35 | 36 | return { 37 | success: false, 38 | error: `API error: ${res.status} ${res.statusText}`, 39 | message: errorData.message || "An error occurred", 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 settings", 54 | }; 55 | } 56 | } 57 | 58 | export default PageGetSettings; 59 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/create_user.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | "github.com/JustLABv1/justflow/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/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/backend/handlers/admins/generate_service_token.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/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 |

16 | Admin | Users 17 |

18 |
19 |
20 |
21 | 29 |
30 | 31 |
32 | 35 |
36 |
37 |
38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/admin/POST/CreateUser.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | import { serverFetch } from "../../serverFetch"; 6 | 7 | export default async function AdminCreateUser( 8 | email: string, 9 | username: string, 10 | password: string, 11 | role: string, 12 | ) { 13 | "use client"; 14 | try { 15 | const cookieStore = await cookies(); 16 | const token = cookieStore.get("session"); 17 | 18 | if (!token) { 19 | return { 20 | success: false, 21 | error: "Authentication token not found", 22 | message: "User is not authenticated", 23 | }; 24 | } 25 | 26 | const res = await serverFetch(`/api/v1/admin/users`, { 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 | timeout: 8000, 39 | retries: 1, 40 | }); 41 | 42 | if (!res.ok) { 43 | const errorData = await res.json(); 44 | 45 | return { 46 | success: false, 47 | error: `API error: ${res.status} ${res.statusText}`, 48 | message: errorData.message || "An error occurred", 49 | }; 50 | } 51 | 52 | const data = await res.json(); 53 | 54 | return data; 55 | } catch { 56 | return { error: "Failed to create user" }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /services/backend/handlers/tokens/delete_runner_token.go: -------------------------------------------------------------------------------- 1 | package tokens 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/frontend/lib/fetch/runner/get.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | import { serverFetch } from "../serverFetch"; 6 | 7 | type Runners = { 8 | runners: []; 9 | }; 10 | 11 | type ErrorResponse = { 12 | success: false; 13 | error: string; 14 | message: string; 15 | }; 16 | 17 | type SuccessResponse = { 18 | success: true; 19 | data: Runners; 20 | }; 21 | 22 | export async function GetRunners(): Promise { 23 | try { 24 | const cookieStore = await cookies(); 25 | const token = cookieStore.get("session"); 26 | 27 | const res = await serverFetch(`/api/v1/runners/`, { 28 | method: "GET", 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: token.value, 32 | }, 33 | timeout: 8000, 34 | retries: 1, 35 | }); 36 | 37 | if (!res.ok) { 38 | const errorData = await res.json(); 39 | 40 | return { 41 | success: false, 42 | error: `API error: ${res.status} ${res.statusText}`, 43 | message: errorData.message || "An error occurred", 44 | }; 45 | } 46 | 47 | const data = await res.json(); 48 | 49 | return { 50 | success: true, 51 | data, 52 | }; 53 | } catch (error) { 54 | return { 55 | success: false, 56 | error: error instanceof Error ? error.message : "Unknown error occurred", 57 | message: "Failed to fetch runners", 58 | }; 59 | } 60 | } 61 | 62 | export default GetRunners; 63 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/flow/flow.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | import { serverFetch } from "../serverFetch"; 6 | 7 | type Flow = { 8 | flow: object; 9 | }; 10 | 11 | type ErrorResponse = { 12 | success: false; 13 | error: string; 14 | message: string; 15 | }; 16 | 17 | type SuccessResponse = { 18 | success: true; 19 | data: Flow; 20 | }; 21 | 22 | export async function GetFlow( 23 | flowID: any, 24 | ): Promise { 25 | try { 26 | const cookieStore = await cookies(); 27 | const token = cookieStore.get("session"); 28 | 29 | const res = await serverFetch(`/api/v1/flows/${flowID}`, { 30 | method: "GET", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: token.value, 34 | }, 35 | timeout: 8000, 36 | retries: 1, 37 | }); 38 | 39 | if (!res.ok) { 40 | const errorData = await res.json(); 41 | 42 | return { 43 | success: false, 44 | error: `API error: ${res.status} ${res.statusText}`, 45 | message: errorData.message || "An error occurred", 46 | }; 47 | } 48 | 49 | const data = await res.json(); 50 | 51 | return { 52 | success: true, 53 | data, 54 | }; 55 | } catch (error) { 56 | return { 57 | success: false, 58 | error: error instanceof Error ? error.message : "Unknown error occurred", 59 | message: "Failed to fetch flow", 60 | }; 61 | } 62 | } 63 | 64 | export default GetFlow; 65 | -------------------------------------------------------------------------------- /services/backend/handlers/admins/get_projects.go: -------------------------------------------------------------------------------- 1 | package admins 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 7 | "github.com/JustLABv1/justflow/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/pkg/telemetry/db_metrics.go: -------------------------------------------------------------------------------- 1 | package telemetry 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "github.com/prometheus/client_golang/prometheus/promauto" 6 | "github.com/uptrace/bun" 7 | ) 8 | 9 | // RegisterDBMetrics registers database metrics 10 | func RegisterDBMetrics(db *bun.DB) { 11 | promauto.NewGaugeFunc( 12 | prometheus.GaugeOpts{ 13 | Name: "db_open_connections", 14 | Help: "Number of open connections to the database", 15 | }, 16 | func() float64 { 17 | return float64(db.Stats().OpenConnections) 18 | }, 19 | ) 20 | 21 | promauto.NewGaugeFunc( 22 | prometheus.GaugeOpts{ 23 | Name: "db_in_use_connections", 24 | Help: "Number of connections currently in use", 25 | }, 26 | func() float64 { 27 | return float64(db.Stats().InUse) 28 | }, 29 | ) 30 | 31 | promauto.NewGaugeFunc( 32 | prometheus.GaugeOpts{ 33 | Name: "db_idle_connections", 34 | Help: "Number of idle connections", 35 | }, 36 | func() float64 { 37 | return float64(db.Stats().Idle) 38 | }, 39 | ) 40 | 41 | promauto.NewGaugeFunc( 42 | prometheus.GaugeOpts{ 43 | Name: "db_wait_count", 44 | Help: "Total number of connections waited for", 45 | }, 46 | func() float64 { 47 | return float64(db.Stats().WaitCount) 48 | }, 49 | ) 50 | 51 | promauto.NewGaugeFunc( 52 | prometheus.GaugeOpts{ 53 | Name: "db_wait_duration_seconds", 54 | Help: "Total time blocked waiting for a new connection", 55 | }, 56 | func() float64 { 57 | return db.Stats().WaitDuration.Seconds() 58 | }, 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /services/frontend/lib/fetch/folder/single.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { cookies } from "next/headers"; 4 | 5 | import { serverFetch } from "../serverFetch"; 6 | 7 | type Folder = { 8 | folder: object; 9 | }; 10 | 11 | type ErrorResponse = { 12 | success: false; 13 | error: string; 14 | message: string; 15 | }; 16 | 17 | type SuccessResponse = { 18 | success: true; 19 | data: Folder; 20 | }; 21 | 22 | export async function GetFolder( 23 | folderID: any, 24 | ): Promise { 25 | try { 26 | const cookieStore = await cookies(); 27 | const token = cookieStore.get("session"); 28 | 29 | const res = await serverFetch(`/api/v1/folders/${folderID}`, { 30 | method: "GET", 31 | headers: { 32 | "Content-Type": "application/json", 33 | Authorization: token.value, 34 | }, 35 | timeout: 8000, 36 | retries: 1, 37 | }); 38 | 39 | if (!res.ok) { 40 | const errorData = await res.json(); 41 | 42 | return { 43 | success: false, 44 | error: `API error: ${res.status} ${res.statusText}`, 45 | message: errorData.message || "An error occurred", 46 | }; 47 | } 48 | 49 | const data = await res.json(); 50 | 51 | return { 52 | success: true, 53 | data, 54 | }; 55 | } catch (error) { 56 | return { 57 | success: false, 58 | error: error instanceof Error ? error.message : "Unknown error occurred", 59 | message: "Failed to fetch folder", 60 | }; 61 | } 62 | } 63 | 64 | export default GetFolder; 65 | -------------------------------------------------------------------------------- /services/backend/router/runners.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/handlers/executions" 5 | "github.com/JustLABv1/justflow/services/backend/handlers/runners" 6 | "github.com/JustLABv1/justflow/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/middlewares/runner.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/JustLABv1/justflow/services/backend/config" 7 | "github.com/JustLABv1/justflow/services/backend/functions/auth" 8 | "github.com/JustLABv1/justflow/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/handlers/executions/get_execution.go: -------------------------------------------------------------------------------- 1 | package executions 2 | 3 | import ( 4 | "github.com/JustLABv1/justflow/services/backend/functions/gatekeeper" 5 | "github.com/JustLABv1/justflow/services/backend/functions/httperror" 6 | "github.com/JustLABv1/justflow/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/frontend/components/alerts/page-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Divider } from "@heroui/react"; 4 | 5 | import ErrorCard from "@/components/error/ErrorCard"; 6 | import { PageSkeleton } from "@/components/loading/page-skeleton"; 7 | import { useUserDetails, useRunners, useFlows } from "@/lib/swr/hooks/flows"; 8 | 9 | import Alerts from "./alerts"; 10 | import AlertsHeading from "./heading"; 11 | 12 | export default function AlertsPageClient() { 13 | const { 14 | runners, 15 | isLoading: runnersLoading, 16 | isError: runnersError, 17 | } = useRunners(); 18 | const { flows, isLoading: flowsLoading, isError: flowsError } = useFlows(); 19 | const { user, isLoading: userLoading, isError: userError } = useUserDetails(); 20 | 21 | // Check if any essential data is still loading or missing 22 | const isLoading = 23 | runnersLoading || flowsLoading || userLoading || !runners || !user; 24 | 25 | // Show loading state if essential data is still loading 26 | if (isLoading) { 27 | return ; 28 | } 29 | 30 | // Show error state 31 | const hasError = runnersError || flowsError || userError; 32 | 33 | if (hasError) { 34 | return ( 35 |
36 | 40 |
41 | ); 42 | } 43 | 44 | return ( 45 |
46 | 47 | 48 | 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /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 | gradient: "gradient 8s linear infinite", 18 | marquee: "marquee var(--duration) linear infinite", 19 | "marquee-vertical": "marquee-vertical var(--duration) linear infinite", 20 | }, 21 | keyframes: { 22 | marquee: { 23 | from: { transform: "translateX(0)" }, 24 | to: { transform: "translateX(calc(-100% - var(--gap)))" }, 25 | }, 26 | "marquee-vertical": { 27 | from: { transform: "translateY(0)" }, 28 | to: { transform: "translateY(calc(-100% - var(--gap)))" }, 29 | }, 30 | gradient: { 31 | to: { 32 | backgroundPosition: "var(--bg-size) 0", 33 | }, 34 | }, 35 | "shine-pulse": { 36 | "0%": { 37 | "background-position": "0% 0%", 38 | }, 39 | "50%": { 40 | "background-position": "100% 100%", 41 | }, 42 | to: { 43 | "background-position": "0% 0%", 44 | }, 45 | }, 46 | }, 47 | }, 48 | }, 49 | darkMode: "class", 50 | plugins: [heroui()], 51 | }; 52 | --------------------------------------------------------------------------------