diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 43e26b028..09f39ea27 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -2,11 +2,7 @@ "mcpServers": { "trigger": { "command": "npx", - "args": [ - "trigger.dev@latest", - "mcp", - "--dev-only" - ] + "args": ["trigger.dev@latest", "mcp", "--dev-only"] } } -} \ No newline at end of file +} diff --git a/.cursor/rules/package-manager.mdc b/.cursor/rules/package-manager.mdc index cea894441..e826f4428 100644 --- a/.cursor/rules/package-manager.mdc +++ b/.cursor/rules/package-manager.mdc @@ -1,10 +1,11 @@ --- -description: -globs: +description: +globs: alwaysApply: true --- + Always use bun as our package manager, example: bun install axios bun remove bun run dev -bunx prisma migrate dev \ No newline at end of file +pnpx prisma migrate dev diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0565c6294..d2e2741ed 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,9 +1,9 @@ --- name: Report a bug about: Any issues with the platform, unexpected behavior, etc. -title: '' -labels: ['🐛 bug'] -assignees: '' +title: "" +labels: ["🐛 bug"] +assignees: "" --- Found a bug? Please fill out the sections below. 👍 diff --git a/.github/TESTS_README.md b/.github/TESTS_README.md index 38bf1da9f..dc938d442 100644 --- a/.github/TESTS_README.md +++ b/.github/TESTS_README.md @@ -48,8 +48,8 @@ bun run test:e2e:report bun run test:e2e:debug # Run specific test file -bunx vitest specific-test.spec.ts -bunx playwright test specific-e2e.spec.ts +pnpx vitest specific-test.spec.ts +pnpx playwright test specific-e2e.spec.ts ``` ## Branch Protection @@ -88,7 +88,7 @@ Add these to your repository settings → Secrets and variables → Actions: ## Maintenance - Workflows use `ubuntu-latest-custom` runner -- Update Playwright browsers monthly: `bunx playwright install` +- Update Playwright browsers monthly: `pnpx playwright install` - Check for action updates quarterly - Review test performance weekly diff --git a/.github/actions/bun-install/action.yml b/.github/actions/bun-install/action.yml index 225268b9b..955e3c397 100644 --- a/.github/actions/bun-install/action.yml +++ b/.github/actions/bun-install/action.yml @@ -6,16 +6,16 @@ # - node_modules directory # ######################################################################################## -name: 'Bun install' -description: 'Run bun install with cache enabled' +name: "Bun install" +description: "Run bun install with cache enabled" inputs: node_version: - description: 'Node.js version to use' + description: "Node.js version to use" required: false default: v20.x runs: - using: 'composite' + using: "composite" steps: - name: Use Node ${{ inputs.node_version }} uses: buildjet/setup-node@v4 @@ -41,7 +41,7 @@ runs: id: bun-nm-cache uses: buildjet/cache@v4 with: - path: '**/node_modules/' + path: "**/node_modules/" key: ${{ runner.os }}-bun-nm-cache-${{ hashFiles('bun.lockb', 'package.json') }} - name: Install dependencies diff --git a/.github/actions/dangerous-git-checkout/action.yml b/.github/actions/dangerous-git-checkout/action.yml index 332402716..c2dccc4e2 100644 --- a/.github/actions/dangerous-git-checkout/action.yml +++ b/.github/actions/dangerous-git-checkout/action.yml @@ -1,7 +1,7 @@ name: Dangerous git Checkout -description: 'Git Checkout from PR code so we can run checks from forks' +description: "Git Checkout from PR code so we can run checks from forks" runs: - using: 'composite' + using: "composite" steps: - name: Checkout repo uses: actions/checkout@v4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ae26f7ff4..c646015a6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,8 +1,8 @@ version: 2 updates: - package-ecosystem: npm - directory: '/' + directory: "/" schedule: interval: daily - target-branch: 'main' + target-branch: "main" open-pull-requests-limit: 3 diff --git a/.github/workflows/auto-pr-to-release.yml b/.github/workflows/auto-pr-to-release.yml index 3e574a4ce..f9e06e61c 100644 --- a/.github/workflows/auto-pr-to-release.yml +++ b/.github/workflows/auto-pr-to-release.yml @@ -26,10 +26,10 @@ jobs: uses: repo-sync/pull-request@v2 continue-on-error: true with: - destination_branch: 'release' + destination_branch: "release" github_token: ${{ secrets.GITHUB_TOKEN }} - pr_label: 'prod-deploy,automated-pr' - pr_title: '[${{ github.event.repository.name }}] Production Deploy' + pr_label: "prod-deploy,automated-pr" + pr_title: "[${{ github.event.repository.name }}] Production Deploy" pr_body: | This is an automated pull request to release the candidate branch into production, which will trigger a deployment. It was created by the [Production PR] action. diff --git a/.github/workflows/database-migrations-main.yml b/.github/workflows/database-migrations-main.yml index 3fd87ba7d..b28d092d8 100644 --- a/.github/workflows/database-migrations-main.yml +++ b/.github/workflows/database-migrations-main.yml @@ -25,4 +25,4 @@ jobs: env: DATABASE_URL: ${{ secrets.DATABASE_URL_DEV }} working-directory: packages/db - run: bunx prisma migrate deploy + run: pnpx prisma migrate deploy diff --git a/.github/workflows/database-migrations-release.yml b/.github/workflows/database-migrations-release.yml index b64c4798d..d53f08bfd 100644 --- a/.github/workflows/database-migrations-release.yml +++ b/.github/workflows/database-migrations-release.yml @@ -25,4 +25,4 @@ jobs: env: DATABASE_URL: ${{ secrets.DATABASE_URL_PROD }} working-directory: packages/db - run: bunx prisma migrate deploy + run: pnpx prisma migrate deploy diff --git a/.github/workflows/trigger-tasks-deploy-main.yml b/.github/workflows/trigger-tasks-deploy-main.yml index 7d5a975c6..bc77b64a2 100644 --- a/.github/workflows/trigger-tasks-deploy-main.yml +++ b/.github/workflows/trigger-tasks-deploy-main.yml @@ -26,7 +26,7 @@ jobs: run: bun install --frozen-lockfile --ignore-scripts - name: Generate Prisma client working-directory: ./packages/db - run: bunx prisma generate + run: pnpx prisma generate - name: 🚀 Deploy Trigger.dev working-directory: ./apps/app timeout-minutes: 20 @@ -36,4 +36,4 @@ jobs: VERCEL_ACCESS_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} - run: bunx trigger.dev@4.0.6 deploy --env staging --log-level debug + run: pnpx trigger.dev@4.0.6 deploy --env staging --log-level debug diff --git a/.github/workflows/trigger-tasks-deploy-release.yml b/.github/workflows/trigger-tasks-deploy-release.yml index 28b78431b..b6be5667a 100644 --- a/.github/workflows/trigger-tasks-deploy-release.yml +++ b/.github/workflows/trigger-tasks-deploy-release.yml @@ -30,7 +30,7 @@ jobs: - name: Generate Prisma client working-directory: ./packages/db - run: bunx prisma generate + run: pnpx prisma generate - name: 🚀 Deploy Trigger.dev working-directory: ./apps/app @@ -40,4 +40,4 @@ jobs: VERCEL_ACCESS_TOKEN: ${{ secrets.VERCEL_ACCESS_TOKEN }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }} - run: bunx trigger.dev@4.0.6 deploy + run: pnpx trigger.dev@4.0.6 deploy diff --git a/.github/workflows_disabled/e2e-tests.yml b/.github/workflows_disabled/e2e-tests.yml index 91a0edb70..c6281fd95 100644 --- a/.github/workflows_disabled/e2e-tests.yml +++ b/.github/workflows_disabled/e2e-tests.yml @@ -6,10 +6,10 @@ on: - main # Feature branches -> main - release # main -> release (production) paths: - - 'apps/**' - - 'packages/**' - - '!**/*.md' - - '.github/workflows/e2e-tests.yml' + - "apps/**" + - "packages/**" + - "!**/*.md" + - ".github/workflows/e2e-tests.yml" push: # Also runs AFTER merge to main/release branches: - main @@ -93,13 +93,13 @@ jobs: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db run: | cd packages/db - bunx prisma migrate deploy - bunx prisma db seed || true # Allow seed to fail + pnpx prisma migrate deploy + pnpx prisma db seed || true # Allow seed to fail - name: Generate Prisma client run: | cd packages/db - bunx prisma generate + pnpx prisma generate - name: Get Playwright version id: playwright-version @@ -111,7 +111,7 @@ jobs: echo "version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT # Also get Playwright's internal browser versions for better cache key - BROWSER_VERSIONS=$(bunx playwright --version | head -1 || echo "unknown") + BROWSER_VERSIONS=$(pnpx playwright --version | head -1 || echo "unknown") echo "Browser versions: $BROWSER_VERSIONS" echo "browser_versions=$BROWSER_VERSIONS" >> $GITHUB_OUTPUT @@ -133,14 +133,14 @@ jobs: # Always run install - Playwright will skip downloading browsers that already exist # This is the safest approach to ensure browsers are properly installed echo "Installing Playwright browsers (will skip if already cached)..." - bunx playwright install chromium firefox webkit + pnpx playwright install chromium firefox webkit - name: Install Playwright system dependencies run: | cd apps/app # Install system dependencies separately - this is always needed on Ubuntu runners echo "Installing system dependencies..." - bunx playwright install-deps chromium firefox webkit + pnpx playwright install-deps chromium firefox webkit - name: Verify Playwright installation run: | @@ -163,7 +163,7 @@ jobs: # Check Playwright's own cache directory echo "Playwright cache home:" - cd apps/app && bunx playwright install --help | grep -i cache || true + cd apps/app && pnpx playwright install --help | grep -i cache || true # Show disk usage echo "Disk usage of cache directories:" @@ -179,13 +179,13 @@ jobs: ls -la ~/.cache/ms-playwright 2>/dev/null || echo "~/.cache/ms-playwright not found" du -sh ~/.cache/ms-playwright 2>/dev/null || echo "Cannot check size" echo "Playwright browsers installed:" - cd apps/app && bunx playwright --version + cd apps/app && pnpx playwright --version - name: Build app run: | cd apps/app export E2E_TEST_MODE=true - bunx next build + pnpx next build env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db NEXTAUTH_URL: http://localhost:3000 @@ -285,7 +285,7 @@ jobs: run: | cd apps/app echo "Starting E2E tests for ${{ matrix.project }} with 2 workers..." - bunx playwright test --project=${{ matrix.project }} + pnpx playwright test --project=${{ matrix.project }} env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db NEXTAUTH_URL: http://localhost:3000 @@ -366,7 +366,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "Or run locally:" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "bunx playwright show-trace path/to/trace.zip" >> $GITHUB_STEP_SUMMARY + echo "pnpx playwright show-trace path/to/trace.zip" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY fi diff --git a/.github/workflows_disabled/quick-tests.yml b/.github/workflows_disabled/quick-tests.yml index f22ab00bf..cf08115bd 100644 --- a/.github/workflows_disabled/quick-tests.yml +++ b/.github/workflows_disabled/quick-tests.yml @@ -48,19 +48,19 @@ jobs: - name: Generate Prisma client run: | cd packages/db - bunx prisma generate + pnpx prisma generate - name: Run database migrations run: | cd packages/db - bunx prisma migrate deploy + pnpx prisma migrate deploy env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db - name: Run quick tests run: | cd apps/app - bunx vitest run src/middleware.test.ts src/lib/__tests__/ + pnpx vitest run src/middleware.test.ts src/lib/__tests__/ env: CI: true MOCK_REDIS: true diff --git a/.github/workflows_disabled/test-quick.yml b/.github/workflows_disabled/test-quick.yml index e96c8e947..4407bd638 100644 --- a/.github/workflows_disabled/test-quick.yml +++ b/.github/workflows_disabled/test-quick.yml @@ -58,7 +58,7 @@ jobs: - name: Generate Prisma Client run: | cd packages/db - bunx prisma generate + pnpx prisma generate - name: Type check run: | @@ -73,7 +73,7 @@ jobs: - name: Run middleware tests (smoke test) run: | cd apps/app - bunx vitest middleware.test.ts --run + pnpx vitest middleware.test.ts --run env: CI: true AUTH_SECRET: test-auth-secret-for-ci @@ -83,7 +83,7 @@ jobs: run: | cd apps/app # Quick build check without full optimization - bunx next build --experimental-build-mode=compile + pnpx next build --experimental-build-mode=compile env: SKIP_ENV_VALIDATION: true DATABASE_URL: postgresql://dummy:dummy@localhost:5432/dummy diff --git a/.github/workflows_disabled/unit-tests.yml b/.github/workflows_disabled/unit-tests.yml index df30e34d6..984d54569 100644 --- a/.github/workflows_disabled/unit-tests.yml +++ b/.github/workflows_disabled/unit-tests.yml @@ -12,9 +12,9 @@ on: - main # Feature branches -> main - release # main -> release (production) paths: - - 'apps/**' - - 'packages/**' - - '!**/*.md' + - "apps/**" + - "packages/**" + - "!**/*.md" push: # Also runs AFTER merge to main/release branches: - main @@ -60,7 +60,7 @@ jobs: - name: Generate Prisma Client run: | cd packages/db - bunx prisma generate + pnpx prisma generate - name: Run unit tests for ${{ matrix.app }} run: | diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0e83b9991..000000000 --- a/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-organize-imports"], - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "useTabs": false, - "trailingComma": "all", - "printWidth": 100, - "bracketSpacing": true, - "arrowParens": "always", - "endOfLine": "lf", - "quoteProps": "as-needed", - "jsxSingleQuote": false, - "bracketSameLine": false, - "proseWrap": "preserve" -} diff --git a/.syncpackrc.json b/.syncpackrc.json deleted file mode 100644 index fa39a0481..000000000 --- a/.syncpackrc.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "source": ["package.json", "apps/*/package.json", "packages/*/package.json"], - "dependencyTypes": ["prod", "dev", "peer"], - "semverGroups": [ - { - "label": "Use exact versions for internal packages", - "packages": ["@comp/**"], - "dependencies": ["@comp/**"], - "range": "workspace:*" - } - ], - "versionGroups": [ - { - "label": "Ensure React is consistent", - "packages": ["**"], - "dependencies": ["react", "react-dom", "@types/react", "@types/react-dom", "react-is"], - "isIgnored": false - }, - { - "label": "Ensure Next.js is consistent", - "packages": ["**"], - "dependencies": ["next"], - "isIgnored": false - }, - { - "label": "Ensure TypeScript is consistent", - "packages": ["**"], - "dependencies": ["typescript"], - "isIgnored": false - }, - { - "label": "Ensure common build tools are consistent", - "packages": ["**"], - "dependencies": ["postcss", "tailwindcss", "@tailwindcss/**", "autoprefixer"], - "isIgnored": false - }, - { - "label": "Ensure testing tools are consistent", - "packages": ["**"], - "dependencies": ["@types/node", "prettier", "turbo"], - "isIgnored": false - }, - { - "label": "Ensure ESLint is consistent", - "packages": ["**"], - "dependencies": ["eslint", "eslint-config-next"], - "isIgnored": false - } - ], - "lintRules": { - "forbiddenDependencies": { - "dependencies": ["crypto", "buffer", "fs", "path", "os", "install", "npm"], - "message": "This is a Node.js built-in module or a mistakenly added dependency" - } - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 4450f981d..d89b3b1ff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,28 +1,28 @@ { - "prisma.fileWatcher": true, - "[prisma]": { - "editor.defaultFormatter": "Prisma.prisma" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.tabSize": 4, - "editor.detectIndentation": true, - "[yaml]": { - "editor.tabSize": 4 - }, - "[dockercompose]": { - "editor.tabSize": 4 - }, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit" + "[typescript,typescriptreact]": { + "editor.defaultFormatter": "dbaeumer.vscode-eslint" }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" + "eslint.rules.customizations": [{ "rule": "*", "severity": "warn" }], + "eslint.runtime": "node", + "eslint.workingDirectories": [ + { "pattern": "apps/*/" }, + { "pattern": "packages/*/" }, + { "pattern": "toolings/*/" }, + ], + "files.associations": { + "*.css": "tailwindcss" }, - "typescript.tsserver.experimental.enableProjectDiagnostics": true -} + "prettier.ignorePath": ".gitignore", + "tailwindCSS.classFunctions": ["cva", "cx", "cn"], + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.preferences.autoImportFileExcludePatterns": [ + "next/router.d.ts", + "next/dist/client/router.d.ts" + ], + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d633dc54..bddc696fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,426 +1,394 @@ ## [1.59.3](https://github.com/trycompai/comp/compare/v1.59.2...v1.59.3) (2025-11-18) - ### Bug Fixes -* **api:** update buildspec and Dockerfile to prepare workspace packag… ([#1775](https://github.com/trycompai/comp/issues/1775)) ([22fb0eb](https://github.com/trycompai/comp/commit/22fb0eb434250333ff7758d393f8d24bf13a0168)) -* **portal:** update module to download executable device agent file for windows ([#1766](https://github.com/trycompai/comp/issues/1766)) ([70ff9c7](https://github.com/trycompai/comp/commit/70ff9c7a6951c9558bc91a2ab25de886ec0c3fae)) +- **api:** update buildspec and Dockerfile to prepare workspace packag… ([#1775](https://github.com/trycompai/comp/issues/1775)) ([22fb0eb](https://github.com/trycompai/comp/commit/22fb0eb434250333ff7758d393f8d24bf13a0168)) +- **portal:** update module to download executable device agent file for windows ([#1766](https://github.com/trycompai/comp/issues/1766)) ([70ff9c7](https://github.com/trycompai/comp/commit/70ff9c7a6951c9558bc91a2ab25de886ec0c3fae)) ## [1.59.2](https://github.com/trycompai/comp/compare/v1.59.1...v1.59.2) (2025-11-18) - ### Bug Fixes -* **tasks:** include 'not_relevant' status in task completion checks ([#1770](https://github.com/trycompai/comp/issues/1770)) ([b5dd9c8](https://github.com/trycompai/comp/commit/b5dd9c8d4976facff39badf5dab19139b581ffde)) +- **tasks:** include 'not_relevant' status in task completion checks ([#1770](https://github.com/trycompai/comp/issues/1770)) ([b5dd9c8](https://github.com/trycompai/comp/commit/b5dd9c8d4976facff39badf5dab19139b581ffde)) ## [1.59.1](https://github.com/trycompai/comp/compare/v1.59.0...v1.59.1) (2025-11-17) - ### Bug Fixes -* **tasks:** increase maxAttempts for answer-question and vendor orchestrator tasks ([#1763](https://github.com/trycompai/comp/issues/1763)) ([ecaadd5](https://github.com/trycompai/comp/commit/ecaadd55ae01cf1c94aa33540aeab7686fff995b)) +- **tasks:** increase maxAttempts for answer-question and vendor orchestrator tasks ([#1763](https://github.com/trycompai/comp/issues/1763)) ([ecaadd5](https://github.com/trycompai/comp/commit/ecaadd55ae01cf1c94aa33540aeab7686fff995b)) # [1.59.0](https://github.com/trycompai/comp/compare/v1.58.0...v1.59.0) (2025-11-17) - ### Features -* **questionnaire:** add security questionnaire feature with AI parsing and auto-answering ([#1755](https://github.com/trycompai/comp/issues/1755)) ([dd4f86c](https://github.com/trycompai/comp/commit/dd4f86c512f40a03b175434877ba6f48059b1bc5)) -* **questionnaire:** enhance S3 client creation on parse action ([#1760](https://github.com/trycompai/comp/issues/1760)) ([4079b73](https://github.com/trycompai/comp/commit/4079b7315af6651c353d5f1d9dff9924a36269a6)) -* **security-questionnaire:** add AI-powered questionnaire parsing an… ([#1751](https://github.com/trycompai/comp/issues/1751)) ([e06bb15](https://github.com/trycompai/comp/commit/e06bb1522a0251b10bfdb00f6b7580c8dc46c6a0)) -* **security-questionnaire:** add support for questionnaire file uploads to S3 ([#1758](https://github.com/trycompai/comp/issues/1758)) ([1ba8866](https://github.com/trycompai/comp/commit/1ba886635839b3e3e9a60cdc34648bee2f60ec13)) -* **security-questionnaire:** add tooltip and disable CTA for unpublished policies ([#1761](https://github.com/trycompai/comp/issues/1761)) ([849966e](https://github.com/trycompai/comp/commit/849966eb4329a17771148add8e970f55c8a2ec95)) -* **tasks:** enhance task management with automation features and UI improvements ([#1752](https://github.com/trycompai/comp/issues/1752)) ([60dfb28](https://github.com/trycompai/comp/commit/60dfb28e6c1727c677571bc3736333fce57b7944)) -* **trust-access:** implement trust access request management system ([#1739](https://github.com/trycompai/comp/issues/1739)) ([2ba3d5d](https://github.com/trycompai/comp/commit/2ba3d5d64591a566c24cc443540bc4e853d9a350)) +- **questionnaire:** add security questionnaire feature with AI parsing and auto-answering ([#1755](https://github.com/trycompai/comp/issues/1755)) ([dd4f86c](https://github.com/trycompai/comp/commit/dd4f86c512f40a03b175434877ba6f48059b1bc5)) +- **questionnaire:** enhance S3 client creation on parse action ([#1760](https://github.com/trycompai/comp/issues/1760)) ([4079b73](https://github.com/trycompai/comp/commit/4079b7315af6651c353d5f1d9dff9924a36269a6)) +- **security-questionnaire:** add AI-powered questionnaire parsing an… ([#1751](https://github.com/trycompai/comp/issues/1751)) ([e06bb15](https://github.com/trycompai/comp/commit/e06bb1522a0251b10bfdb00f6b7580c8dc46c6a0)) +- **security-questionnaire:** add support for questionnaire file uploads to S3 ([#1758](https://github.com/trycompai/comp/issues/1758)) ([1ba8866](https://github.com/trycompai/comp/commit/1ba886635839b3e3e9a60cdc34648bee2f60ec13)) +- **security-questionnaire:** add tooltip and disable CTA for unpublished policies ([#1761](https://github.com/trycompai/comp/issues/1761)) ([849966e](https://github.com/trycompai/comp/commit/849966eb4329a17771148add8e970f55c8a2ec95)) +- **tasks:** enhance task management with automation features and UI improvements ([#1752](https://github.com/trycompai/comp/issues/1752)) ([60dfb28](https://github.com/trycompai/comp/commit/60dfb28e6c1727c677571bc3736333fce57b7944)) +- **trust-access:** implement trust access request management system ([#1739](https://github.com/trycompai/comp/issues/1739)) ([2ba3d5d](https://github.com/trycompai/comp/commit/2ba3d5d64591a566c24cc443540bc4e853d9a350)) # [1.58.0](https://github.com/trycompai/comp/compare/v1.57.1...v1.58.0) (2025-11-13) - ### Features -* **onboarding:** add individual tracking for vendors and risks with auto-expand ([#1748](https://github.com/trycompai/comp/issues/1748)) ([7a85be8](https://github.com/trycompai/comp/commit/7a85be80f4c27da0cef485ad09e7152222dc8c48)) +- **onboarding:** add individual tracking for vendors and risks with auto-expand ([#1748](https://github.com/trycompai/comp/issues/1748)) ([7a85be8](https://github.com/trycompai/comp/commit/7a85be80f4c27da0cef485ad09e7152222dc8c48)) ## [1.57.1](https://github.com/trycompai/comp/compare/v1.57.0...v1.57.1) (2025-11-13) - ### Bug Fixes -* **tasks:** show all task statuses and sort alphabetically ([#1743](https://github.com/trycompai/comp/issues/1743)) ([413b14c](https://github.com/trycompai/comp/commit/413b14ca95d5490a086aecb418a3305c2099534a)) +- **tasks:** show all task statuses and sort alphabetically ([#1743](https://github.com/trycompai/comp/issues/1743)) ([413b14c](https://github.com/trycompai/comp/commit/413b14ca95d5490a086aecb418a3305c2099534a)) # [1.57.0](https://github.com/trycompai/comp/compare/v1.56.7...v1.57.0) (2025-11-11) - ### Features -* add contractor role ([#1735](https://github.com/trycompai/comp/issues/1735)) ([2d87914](https://github.com/trycompai/comp/commit/2d87914a25ef5edad2cee3f033e9583dc30b04d2)) +- add contractor role ([#1735](https://github.com/trycompai/comp/issues/1735)) ([2d87914](https://github.com/trycompai/comp/commit/2d87914a25ef5edad2cee3f033e9583dc30b04d2)) ## [1.56.7](https://github.com/trycompai/comp/compare/v1.56.6...v1.56.7) (2025-11-07) - ### Bug Fixes -* **portal:** remove Ubuntu support help ([#1715](https://github.com/trycompai/comp/issues/1715)) ([b604b18](https://github.com/trycompai/comp/commit/b604b18a5c16db2f9b58be64f2262d70fca66d34)) -* **portal:** update macOS version requirements ([#1716](https://github.com/trycompai/comp/issues/1716)) ([21c259b](https://github.com/trycompai/comp/commit/21c259bb5c20fd3798df9c2bca3ef0883863b901)) +- **portal:** remove Ubuntu support help ([#1715](https://github.com/trycompai/comp/issues/1715)) ([b604b18](https://github.com/trycompai/comp/commit/b604b18a5c16db2f9b58be64f2262d70fca66d34)) +- **portal:** update macOS version requirements ([#1716](https://github.com/trycompai/comp/issues/1716)) ([21c259b](https://github.com/trycompai/comp/commit/21c259bb5c20fd3798df9c2bca3ef0883863b901)) ## [1.56.6](https://github.com/trycompai/comp/compare/v1.56.5...v1.56.6) (2025-10-31) - ### Bug Fixes -* **app:** send emails to employees when all policies are published ([#1707](https://github.com/trycompai/comp/issues/1707)) ([df7a461](https://github.com/trycompai/comp/commit/df7a4617e008174624c02bc89905af389f7c478d)) +- **app:** send emails to employees when all policies are published ([#1707](https://github.com/trycompai/comp/issues/1707)) ([df7a461](https://github.com/trycompai/comp/commit/df7a4617e008174624c02bc89905af389f7c478d)) ## [1.56.5](https://github.com/trycompai/comp/compare/v1.56.4...v1.56.5) (2025-10-30) - ### Bug Fixes -* **cloud-tests:** improve error messages and user feedback ([#1703](https://github.com/trycompai/comp/issues/1703)) ([9abfc4a](https://github.com/trycompai/comp/commit/9abfc4a2bd0b320d1e804f9b3cf4a714f52d1002)) +- **cloud-tests:** improve error messages and user feedback ([#1703](https://github.com/trycompai/comp/issues/1703)) ([9abfc4a](https://github.com/trycompai/comp/commit/9abfc4a2bd0b320d1e804f9b3cf4a714f52d1002)) ## [1.56.4](https://github.com/trycompai/comp/compare/v1.56.3...v1.56.4) (2025-10-29) - ### Bug Fixes -* **cloud-tests:** display integration scan errors in UI ([#1698](https://github.com/trycompai/comp/issues/1698)) ([a27b3ba](https://github.com/trycompai/comp/commit/a27b3bab8cb4d01c3516db583f9963955cb57164)), closes [#1695](https://github.com/trycompai/comp/issues/1695) +- **cloud-tests:** display integration scan errors in UI ([#1698](https://github.com/trycompai/comp/issues/1698)) ([a27b3ba](https://github.com/trycompai/comp/commit/a27b3bab8cb4d01c3516db583f9963955cb57164)), closes [#1695](https://github.com/trycompai/comp/issues/1695) ## [1.56.3](https://github.com/trycompai/comp/compare/v1.56.2...v1.56.3) (2025-10-23) - ### Bug Fixes -* **api:** allow uploading excel documents for task ([#1677](https://github.com/trycompai/comp/issues/1677)) ([8c7f955](https://github.com/trycompai/comp/commit/8c7f9550755090d8aa780fc8fbcd2ebadf1e7fb8)) +- **api:** allow uploading excel documents for task ([#1677](https://github.com/trycompai/comp/issues/1677)) ([8c7f955](https://github.com/trycompai/comp/commit/8c7f9550755090d8aa780fc8fbcd2ebadf1e7fb8)) ## [1.56.2](https://github.com/trycompai/comp/compare/v1.56.1...v1.56.2) (2025-10-21) - ### Bug Fixes -* **app:** auto-approve the org creation on staging ([#1676](https://github.com/trycompai/comp/issues/1676)) ([dbb149e](https://github.com/trycompai/comp/commit/dbb149ea2eecc32b9d470112fd293f72b91e6eaa)) +- **app:** auto-approve the org creation on staging ([#1676](https://github.com/trycompai/comp/issues/1676)) ([dbb149e](https://github.com/trycompai/comp/commit/dbb149ea2eecc32b9d470112fd293f72b91e6eaa)) ## [1.56.1](https://github.com/trycompai/comp/compare/v1.56.0...v1.56.1) (2025-10-17) - ### Bug Fixes -* **app:** show device list of only employees ([#1646](https://github.com/trycompai/comp/issues/1646)) ([f5fc56e](https://github.com/trycompai/comp/commit/f5fc56e696968110bfe66e5c533cbc7f3790f847)) -* **portal:** fixed the issue that the Posthog didn't identify people on the portal ([#1668](https://github.com/trycompai/comp/issues/1668)) ([5614d62](https://github.com/trycompai/comp/commit/5614d620135e86fa2c40862d439a67ebd856d747)) +- **app:** show device list of only employees ([#1646](https://github.com/trycompai/comp/issues/1646)) ([f5fc56e](https://github.com/trycompai/comp/commit/f5fc56e696968110bfe66e5c533cbc7f3790f847)) +- **portal:** fixed the issue that the Posthog didn't identify people on the portal ([#1668](https://github.com/trycompai/comp/issues/1668)) ([5614d62](https://github.com/trycompai/comp/commit/5614d620135e86fa2c40862d439a67ebd856d747)) # [1.56.0](https://github.com/trycompai/comp/compare/v1.55.2...v1.56.0) (2025-10-08) - ### Bug Fixes -* **app:** add ConditionalOnboardingTracker component and update layout ([#1618](https://github.com/trycompai/comp/issues/1618)) ([af4977c](https://github.com/trycompai/comp/commit/af4977c439794bb79f8f86f41c129c7599cd4284)) -* **app:** show the image on onboarding section by setting unoptimized ([#1619](https://github.com/trycompai/comp/issues/1619)) ([1f4639f](https://github.com/trycompai/comp/commit/1f4639ff0a3149e5ba446083c58391e616913789)) -* **app:** task dates should be creation date instead of defaults ([#1620](https://github.com/trycompai/comp/issues/1620)) ([12e5e15](https://github.com/trycompai/comp/commit/12e5e15b52cc85dfb007b026d762231c603a1a16)) - +- **app:** add ConditionalOnboardingTracker component and update layout ([#1618](https://github.com/trycompai/comp/issues/1618)) ([af4977c](https://github.com/trycompai/comp/commit/af4977c439794bb79f8f86f41c129c7599cd4284)) +- **app:** show the image on onboarding section by setting unoptimized ([#1619](https://github.com/trycompai/comp/issues/1619)) ([1f4639f](https://github.com/trycompai/comp/commit/1f4639ff0a3149e5ba446083c58391e616913789)) +- **app:** task dates should be creation date instead of defaults ([#1620](https://github.com/trycompai/comp/issues/1620)) ([12e5e15](https://github.com/trycompai/comp/commit/12e5e15b52cc85dfb007b026d762231c603a1a16)) ### Features -* **db:** add Fraud to RiskCategory ([#1615](https://github.com/trycompai/comp/issues/1615)) ([cc265e6](https://github.com/trycompai/comp/commit/cc265e65ddc171cbd5b09d125ae6a6933528896c)) +- **db:** add Fraud to RiskCategory ([#1615](https://github.com/trycompai/comp/issues/1615)) ([cc265e6](https://github.com/trycompai/comp/commit/cc265e65ddc171cbd5b09d125ae6a6933528896c)) ## [1.55.2](https://github.com/trycompai/comp/compare/v1.55.1...v1.55.2) (2025-10-07) - ### Bug Fixes -* **app:** handle potential null for integrationsUsed in UnifiedWorkflowCard ([#1612](https://github.com/trycompai/comp/issues/1612)) ([561d9b1](https://github.com/trycompai/comp/commit/561d9b18f60d280d8a6627d6ebbdc552ecc8efff)) -* **app:** set portal url to new-policy-email sent to employees ([#1614](https://github.com/trycompai/comp/issues/1614)) ([5264c76](https://github.com/trycompai/comp/commit/5264c76690ae3ec27bb7d66b13c497fda8dc6d5e)) +- **app:** handle potential null for integrationsUsed in UnifiedWorkflowCard ([#1612](https://github.com/trycompai/comp/issues/1612)) ([561d9b1](https://github.com/trycompai/comp/commit/561d9b18f60d280d8a6627d6ebbdc552ecc8efff)) +- **app:** set portal url to new-policy-email sent to employees ([#1614](https://github.com/trycompai/comp/issues/1614)) ([5264c76](https://github.com/trycompai/comp/commit/5264c76690ae3ec27bb7d66b13c497fda8dc6d5e)) ## [1.55.1](https://github.com/trycompai/comp/compare/v1.55.0...v1.55.1) (2025-10-06) - ### Bug Fixes -* **app:** fix AWS cloud test error caused by improper usage of batchTriggerAndWait ([#1608](https://github.com/trycompai/comp/issues/1608)) ([0cddf49](https://github.com/trycompai/comp/commit/0cddf49a7373a538c25eae13239b2eff5bbbdf55)) -* **app:** make reviewDate readonly and update it once policy is published ([#1603](https://github.com/trycompai/comp/issues/1603)) ([c966652](https://github.com/trycompai/comp/commit/c966652189c15506fc01caea1bdfdf3b175ce20d)) -* **app:** onboarding flow being overriden ([#1595](https://github.com/trycompai/comp/issues/1595)) ([572003f](https://github.com/trycompai/comp/commit/572003fba63662f7c92ef9efb69afbbda527f1d3)) -* **auth:** improve OTP error handling with specific user messages ([#1596](https://github.com/trycompai/comp/issues/1596)) ([0bb7b2b](https://github.com/trycompai/comp/commit/0bb7b2b8c5e6cb0f3809ffcd9574b121707ec5ae)) -* **portal:** fix build error to add AUTH_SECRET env to portal ([#1590](https://github.com/trycompai/comp/issues/1590)) ([3596376](https://github.com/trycompai/comp/commit/3596376014660ce688d5ee13efed9bae880a18ee)) +- **app:** fix AWS cloud test error caused by improper usage of batchTriggerAndWait ([#1608](https://github.com/trycompai/comp/issues/1608)) ([0cddf49](https://github.com/trycompai/comp/commit/0cddf49a7373a538c25eae13239b2eff5bbbdf55)) +- **app:** make reviewDate readonly and update it once policy is published ([#1603](https://github.com/trycompai/comp/issues/1603)) ([c966652](https://github.com/trycompai/comp/commit/c966652189c15506fc01caea1bdfdf3b175ce20d)) +- **app:** onboarding flow being overriden ([#1595](https://github.com/trycompai/comp/issues/1595)) ([572003f](https://github.com/trycompai/comp/commit/572003fba63662f7c92ef9efb69afbbda527f1d3)) +- **auth:** improve OTP error handling with specific user messages ([#1596](https://github.com/trycompai/comp/issues/1596)) ([0bb7b2b](https://github.com/trycompai/comp/commit/0bb7b2b8c5e6cb0f3809ffcd9574b121707ec5ae)) +- **portal:** fix build error to add AUTH_SECRET env to portal ([#1590](https://github.com/trycompai/comp/issues/1590)) ([3596376](https://github.com/trycompai/comp/commit/3596376014660ce688d5ee13efed9bae880a18ee)) # [1.55.0](https://github.com/trycompai/comp/compare/v1.54.0...v1.55.0) (2025-09-30) - ### Features -* **app:** implement automated tasks ([#1550](https://github.com/trycompai/comp/issues/1550)) ([56f019d](https://github.com/trycompai/comp/commit/56f019d7508c19f24fc3fd31113d9c0fa788fb81)) -* **portal:** add gmail signin ([#1587](https://github.com/trycompai/comp/issues/1587)) ([f8d6af4](https://github.com/trycompai/comp/commit/f8d6af4e8723b4c79a7ae7c508ad8ac34892db84)) -* **portal:** display policies with PDF format ([#1559](https://github.com/trycompai/comp/issues/1559)) ([3d7ee5f](https://github.com/trycompai/comp/commit/3d7ee5fd37f40258b11615a48d57e330567ae7fc)) -* **secrets:** fix secrets ([b3af710](https://github.com/trycompai/comp/commit/b3af710412ff76200eefb87ac8fc6ccd6774b455)) +- **app:** implement automated tasks ([#1550](https://github.com/trycompai/comp/issues/1550)) ([56f019d](https://github.com/trycompai/comp/commit/56f019d7508c19f24fc3fd31113d9c0fa788fb81)) +- **portal:** add gmail signin ([#1587](https://github.com/trycompai/comp/issues/1587)) ([f8d6af4](https://github.com/trycompai/comp/commit/f8d6af4e8723b4c79a7ae7c508ad8ac34892db84)) +- **portal:** display policies with PDF format ([#1559](https://github.com/trycompai/comp/issues/1559)) ([3d7ee5f](https://github.com/trycompai/comp/commit/3d7ee5fd37f40258b11615a48d57e330567ae7fc)) +- **secrets:** fix secrets ([b3af710](https://github.com/trycompai/comp/commit/b3af710412ff76200eefb87ac8fc6ccd6774b455)) # [1.54.0](https://github.com/trycompai/comp/compare/v1.53.0...v1.54.0) (2025-09-19) - ### Bug Fixes -* **app:** show only published policies on Employee Tasks ([#1547](https://github.com/trycompai/comp/issues/1547)) ([92b2dc9](https://github.com/trycompai/comp/commit/92b2dc9312c183e54c09e86e85706dbe01942196)) - +- **app:** show only published policies on Employee Tasks ([#1547](https://github.com/trycompai/comp/issues/1547)) ([92b2dc9](https://github.com/trycompai/comp/commit/92b2dc9312c183e54c09e86e85706dbe01942196)) ### Features -* **app:** Add support for ISO 42001 ([#1549](https://github.com/trycompai/comp/issues/1549)) ([1a9d57b](https://github.com/trycompai/comp/commit/1a9d57b8dc748016ab1a5389041017984455de5b)) -* **app:** Auto mark tasks as todo when review period starts ([#1546](https://github.com/trycompai/comp/issues/1546)) ([777c0db](https://github.com/trycompai/comp/commit/777c0db8473c464d909ebce5ea6e0c2c3f72e31f)) +- **app:** Add support for ISO 42001 ([#1549](https://github.com/trycompai/comp/issues/1549)) ([1a9d57b](https://github.com/trycompai/comp/commit/1a9d57b8dc748016ab1a5389041017984455de5b)) +- **app:** Auto mark tasks as todo when review period starts ([#1546](https://github.com/trycompai/comp/issues/1546)) ([777c0db](https://github.com/trycompai/comp/commit/777c0db8473c464d909ebce5ea6e0c2c3f72e31f)) # [1.53.0](https://github.com/trycompai/comp/compare/v1.52.1...v1.53.0) (2025-09-18) - ### Features -* **app:** Add reviewDate column to Task table ([#1541](https://github.com/trycompai/comp/issues/1541)) ([68e0e42](https://github.com/trycompai/comp/commit/68e0e42fe26ac5391afa696a1cd15ae3148aadd3)) -* **app:** Create a scheduled task for Recurring policy ([#1540](https://github.com/trycompai/comp/issues/1540)) ([1eb9623](https://github.com/trycompai/comp/commit/1eb9623c05a88b8c04b303ac505d32ae0c13baeb)) +- **app:** Add reviewDate column to Task table ([#1541](https://github.com/trycompai/comp/issues/1541)) ([68e0e42](https://github.com/trycompai/comp/commit/68e0e42fe26ac5391afa696a1cd15ae3148aadd3)) +- **app:** Create a scheduled task for Recurring policy ([#1540](https://github.com/trycompai/comp/issues/1540)) ([1eb9623](https://github.com/trycompai/comp/commit/1eb9623c05a88b8c04b303ac505d32ae0c13baeb)) ## [1.52.1](https://github.com/trycompai/comp/compare/v1.52.0...v1.52.1) (2025-09-17) - ### Bug Fixes -* force version bump past v1.52.0 ([d7c58c0](https://github.com/trycompai/comp/commit/d7c58c0411d0d467cdc99a4211d197c86cae1f06)) +- force version bump past v1.52.0 ([d7c58c0](https://github.com/trycompai/comp/commit/d7c58c0411d0d467cdc99a4211d197c86cae1f06)) # [1.52.0](https://github.com/trycompai/comp/compare/v1.51.0...v1.52.0) (2025-09-17) - ### Bug Fixes -* **app:** only show owner/admin users on Task UI ([#1524](https://github.com/trycompai/comp/issues/1524)) ([151ccc6](https://github.com/trycompai/comp/commit/151ccc6acc914ff86c304e14abc50b19c96d13af)) -* **portal:** show only published policies on portal ([#1520](https://github.com/trycompai/comp/issues/1520)) ([ceb99d7](https://github.com/trycompai/comp/commit/ceb99d786644bc86ed8968a71502e25a35ffca1f)) - +- **app:** only show owner/admin users on Task UI ([#1524](https://github.com/trycompai/comp/issues/1524)) ([151ccc6](https://github.com/trycompai/comp/commit/151ccc6acc914ff86c304e14abc50b19c96d13af)) +- **portal:** show only published policies on portal ([#1520](https://github.com/trycompai/comp/issues/1520)) ([ceb99d7](https://github.com/trycompai/comp/commit/ceb99d786644bc86ed8968a71502e25a35ffca1f)) ### Features -* **portal:** Whenever the policy is published, signedBy field should be cleared and send email to only previous singers to let them accept it again. ([#1532](https://github.com/trycompai/comp/issues/1532)) ([8c1b525](https://github.com/trycompai/comp/commit/8c1b525ead980181cd3e8f57e72961c1b82b7e4f)) +- **portal:** Whenever the policy is published, signedBy field should be cleared and send email to only previous singers to let them accept it again. ([#1532](https://github.com/trycompai/comp/issues/1532)) ([8c1b525](https://github.com/trycompai/comp/commit/8c1b525ead980181cd3e8f57e72961c1b82b7e4f)) # [1.52.0](https://github.com/trycompai/comp/compare/v1.51.0...v1.52.0) (2025-09-17) - ### Bug Fixes -* **app:** only show owner/admin users on Task UI ([#1524](https://github.com/trycompai/comp/issues/1524)) ([151ccc6](https://github.com/trycompai/comp/commit/151ccc6acc914ff86c304e14abc50b19c96d13af)) -* **portal:** show only published policies on portal ([#1520](https://github.com/trycompai/comp/issues/1520)) ([ceb99d7](https://github.com/trycompai/comp/commit/ceb99d786644bc86ed8968a71502e25a35ffca1f)) - +- **app:** only show owner/admin users on Task UI ([#1524](https://github.com/trycompai/comp/issues/1524)) ([151ccc6](https://github.com/trycompai/comp/commit/151ccc6acc914ff86c304e14abc50b19c96d13af)) +- **portal:** show only published policies on portal ([#1520](https://github.com/trycompai/comp/issues/1520)) ([ceb99d7](https://github.com/trycompai/comp/commit/ceb99d786644bc86ed8968a71502e25a35ffca1f)) ### Features -* **portal:** Whenever the policy is published, signedBy field should be cleared and send email to only previous singers to let them accept it again. ([#1532](https://github.com/trycompai/comp/issues/1532)) ([8c1b525](https://github.com/trycompai/comp/commit/8c1b525ead980181cd3e8f57e72961c1b82b7e4f)) +- **portal:** Whenever the policy is published, signedBy field should be cleared and send email to only previous singers to let them accept it again. ([#1532](https://github.com/trycompai/comp/issues/1532)) ([8c1b525](https://github.com/trycompai/comp/commit/8c1b525ead980181cd3e8f57e72961c1b82b7e4f)) ## [1.51.1](https://github.com/trycompai/comp/compare/v1.51.0...v1.51.1) (2025-09-16) - ### Bug Fixes -* **portal:** show only published policies on portal ([#1520](https://github.com/trycompai/comp/issues/1520)) ([ceb99d7](https://github.com/trycompai/comp/commit/ceb99d786644bc86ed8968a71502e25a35ffca1f)) +- **portal:** show only published policies on portal ([#1520](https://github.com/trycompai/comp/issues/1520)) ([ceb99d7](https://github.com/trycompai/comp/commit/ceb99d786644bc86ed8968a71502e25a35ffca1f)) # [1.51.0](https://github.com/trycompai/comp/compare/v1.50.0...v1.51.0) (2025-09-16) - ### Bug Fixes -* add DEQUEUED state handling in OnboardingTracker component ([74d7151](https://github.com/trycompai/comp/commit/74d71518a53e7de404eb321dd9c778d7ab1ce57a)) -* add missing newlines in package.json and index.ts files for consistency ([f33bb34](https://github.com/trycompai/comp/commit/f33bb348cfb5d8919468f397965417489824a6e1)) -* add missing port number to database connection string ([bafbb27](https://github.com/trycompai/comp/commit/bafbb27affac170251a68f0a79038bc9c97851a1)) -* add newline at end of package.json for proper formatting ([fb91918](https://github.com/trycompai/comp/commit/fb91918aac91b456b8b61eac01e542b95a5542e3)) -* add persistent cmd session support in Windows script ([bfad924](https://github.com/trycompai/comp/commit/bfad924a7ecf8149f978c4545524c98a93a77573)) -* add robust database connectivity rules for CodeBuild ([50c9a7e](https://github.com/trycompai/comp/commit/50c9a7eac5ea4994eb94d5cbd82b002bdb059246)) -* add verification for public directory in buildspec.yml ([b8e2064](https://github.com/trycompai/comp/commit/b8e20645333547842e7731deef05c0fc53c7732c)) -* Added Control property to task sidebar ([8cc503b](https://github.com/trycompai/comp/commit/8cc503b70f02e3a0bb448fa0b4179f8601811017)) -* Allow access to auditor role ([1553400](https://github.com/trycompai/comp/commit/155340017c62a09dcfd03709011e25df89a85863)) -* **auth:** rename email parameter for invite function to improve clarity ([95bb6c9](https://github.com/trycompai/comp/commit/95bb6c9c8c12f6543cfa4e4a3e4862c8471845e6)) -* **buildspec, Dockerfile:** update Node.js version and adjust Prisma client generation path ([2d677d0](https://github.com/trycompai/comp/commit/2d677d05f54d9cc84c0def4369c1e07ed46bd511)) -* **buildspec:** adjust working directory for dependency installation ([0774214](https://github.com/trycompai/comp/commit/0774214ef2aba48215acb24d4739884f578ff753)) -* **buildspec:** enhance type checking step with optional script handling ([f151692](https://github.com/trycompai/comp/commit/f151692672559c3ee2d9b00586befb0a3964b375)) -* comprehensive Prisma deployment fix for Vercel monorepo ([6ef5033](https://github.com/trycompai/comp/commit/6ef50339a5ef29931cc1991241e0ca777db21f7b)) -* **config:** update Vercel environment check for standalone output ([39f3710](https://github.com/trycompai/comp/commit/39f3710cda9cf6ba57f3aa0308ee5f19868acff9)) -* copy Prisma binary to locations where runtime is searching ([7c3af89](https://github.com/trycompai/comp/commit/7c3af894a75439a2df512e79d834e83cd7ca7a9f)) -* correct Docker entry point for monorepo structure ([bb3aecb](https://github.com/trycompai/comp/commit/bb3aecb58c407592a76afa6ca3c197ccd538d56b)) -* correct environment variable assignment for Prisma query engine in Next.js config ([1ccaae1](https://github.com/trycompai/comp/commit/1ccaae1279b80e9410283f52fb6d183c2121d92f)) -* correct indentation in package.json scripts section ([f3ed25a](https://github.com/trycompai/comp/commit/f3ed25a85ae44a585633f309ada466b896df1ae8)) -* correct static file paths for next.js standalone monorepo structure ([55442ab](https://github.com/trycompai/comp/commit/55442ab2f4f96b25be5d919be43580cd32bdb733)) -* correct static file paths for next.js standalone monorepo structure ([12adfa6](https://github.com/trycompai/comp/commit/12adfa621f5ed8b6ebae8227fb07fc4055a63ee5)) -* correct Windows path formatting in fleet label creation script ([f34610f](https://github.com/trycompai/comp/commit/f34610fcfcc46a157d80c36204be40147679da7f)) -* **deploy:** ensure pulumi update completes before starting build ([d47e9b5](https://github.com/trycompai/comp/commit/d47e9b5b5b3eff8e36c1006f12f4a108506575bb)) -* **docs:** Broken contributing guide link in PR template ([7c8f509](https://github.com/trycompai/comp/commit/7c8f509a594183c1bd41a187271f4aeba7065573)) -* Enforce role-based access control in app ([8a81f42](https://github.com/trycompai/comp/commit/8a81f42b22da29a7a39e2b4698e94421a6a329fd)) -* enhance directory and marker file handling in Windows script ([49e3ec4](https://github.com/trycompai/comp/commit/49e3ec4f51bdee8647aa3c1684abcef041d376df)) -* enhance elevation and error handling in Windows script ([1a67177](https://github.com/trycompai/comp/commit/1a67177a638a87576c662dafefaad39158ef0884)) -* enhance elevation process in Windows script for better user experience ([9b4aeee](https://github.com/trycompai/comp/commit/9b4aeee2cfca20766bc6d457ef83f465dc52f700)) -* enhance logging and error handling in Windows script ([9c54eb0](https://github.com/trycompai/comp/commit/9c54eb09fba40a63b4d5a641526f7cd4292f61e3)) -* enhance session token retrieval in middleware ([27a7254](https://github.com/trycompai/comp/commit/27a72549959ea8f367bf0cff86ae6faf75a5ed01)) -* enhance structure and logging in Windows script ([e27a003](https://github.com/trycompai/comp/commit/e27a0038e1bcb63cca12411cd3cfac0b6867a05b)) -* ensure consistent newline handling in Prisma client and exports ([b5ce5f1](https://github.com/trycompai/comp/commit/b5ce5f11d108cf41259c5125b5768e974daf551e)) -* ensure Prisma binaries are included in Vercel deployment ([fa43f5e](https://github.com/trycompai/comp/commit/fa43f5eb9c75aba50fa232bbc8db8fe84f823a82)) -* ensure Prisma binaries are included in Vercel deployment with custom output path ([e877714](https://github.com/trycompai/comp/commit/e877714c77c2cb3ce6a238518cfc4e0de162cd25)) -* ensure Prisma client is generated during Next.js build for deployment ([a9ee09a](https://github.com/trycompai/comp/commit/a9ee09aebc52ed2f8766e90c526a81909495228c)) -* ensure Prisma query engine binary is properly copied in Trigger.dev deployment ([8d2fe33](https://github.com/trycompai/comp/commit/8d2fe332c99b5b65505c728ea9ac5e307fa87bd3)) -* fixed search ([36e12f1](https://github.com/trycompai/comp/commit/36e12f12cf3aae097ded63c33dc803b9596bcddd)) -* improve directory creation logic in Windows script ([71858ec](https://github.com/trycompai/comp/commit/71858ec19bcbce3b8e624e84d71a9420e62b668c)) -* improve directory selection logic in Windows script ([b2da074](https://github.com/trycompai/comp/commit/b2da0741cf3148966bc5800f335023f8af43a3b9)) -* improve Docker entry point detection for Next.js standalone build ([4e11649](https://github.com/trycompai/comp/commit/4e11649476f44cbe425c6a5446557083d83d2f9c)) -* improve error handling in file upload processes ([af52289](https://github.com/trycompai/comp/commit/af52289e4bb71752f915d96f74f542ada14ea293)) -* improve error handling in ToDoOverview component ([d4dd686](https://github.com/trycompai/comp/commit/d4dd686f8997002d569cde201ca65f2f1a134ee1)) -* improve PowerShell command in Windows script for better execution ([a313739](https://github.com/trycompai/comp/commit/a313739b3ff8c6481342ef4872fec4b2698886c8)) -* improve variable handling and logging in Windows script ([042db43](https://github.com/trycompai/comp/commit/042db43cd4e45087aaa1e881c4ca59d88947050f)) -* **infra:** correct echo command syntax in buildspec for database URL check ([1140eba](https://github.com/trycompai/comp/commit/1140ebae1b1230eca4b6b3554f7a37cc388c92c1)) -* **infra:** correct secrets manager syntax for codebuild and container ([7ada508](https://github.com/trycompai/comp/commit/7ada5083c13a632a3ade97f3c7d453271adfa191)) -* **infra:** correctly resolve database connection string for codebuild ([093bcb3](https://github.com/trycompai/comp/commit/093bcb3f5ba310ac4fc17be6d1c751fe71c56827)) -* **infra:** update health check configuration in container module ([c9b41a6](https://github.com/trycompai/comp/commit/c9b41a6acf20a8caa94a2b01c4c0271acb638013)) -* **infra:** update health check endpoint in container configuration ([5f3f4a2](https://github.com/trycompai/comp/commit/5f3f4a23a3386374b7a8a389f3eb3da4a5af2757)) -* Keep the `getRowId` and `rowClickBasePath` optional ([544afdc](https://github.com/trycompai/comp/commit/544afdcbd4885c501d2a2d9985e409f053020c53)) -* Member edit only for owner/admin ([1a2b9d2](https://github.com/trycompai/comp/commit/1a2b9d28d476f030a55761e30f2b9143df70ca87)) -* Move role checks on org level ([a4056bf](https://github.com/trycompai/comp/commit/a4056bf4d904baf450fd07c4e7ce0f83100e3504)) -* prevent build failure when metadata service is inaccessible ([951f462](https://github.com/trycompai/comp/commit/951f4622985044669503ca60a21388cc5f9207f5)) -* prevent infinite loop in TrustPortalSwitch by updating useEffect dependencies ([5f28201](https://github.com/trycompai/comp/commit/5f282018f3c9bee323a92503ff6a20406306a5ec)) -* Prisma seed command in `packages/db` ([a9957cd](https://github.com/trycompai/comp/commit/a9957cd3daf9d15430d1c47c45ae4e6139e47b92)) -* properly separate app env vars from infra env vars ([55aa5d6](https://github.com/trycompai/comp/commit/55aa5d607a1f33acec59bf1008e8b7a2604e6957)) -* refine directory creation logic in Windows script ([bfbfca0](https://github.com/trycompai/comp/commit/bfbfca0466d37452bc3aa70116d4eb96adeaa03c)) -* refine directory existence checks and logging in Windows script ([37ea28b](https://github.com/trycompai/comp/commit/37ea28b5a3362a1e678e9d23105cb16a00e715c3)) -* refine directory handling and logging in Windows script ([3982150](https://github.com/trycompai/comp/commit/3982150f3ea620097a535d80c7c315b3d7df76c3)) -* remove duplicate dependsOn key ([#1426](https://github.com/trycompai/comp/issues/1426)) ([9035238](https://github.com/trycompai/comp/commit/90352385bf358b6e4d30f0345ab715652b3dc3f7)) -* remove duplicate import of HealthModule in app.module.ts ([e02a51c](https://github.com/trycompai/comp/commit/e02a51c6c6330fe3bbb4b0097bf20492c47787a0)) -* resolve CodeBuild database connectivity issues ([04b1555](https://github.com/trycompai/comp/commit/04b155568619dfcbdce12bf6aa234df1c77229a2)) -* resolve YAML syntax error in buildspec.yml ([a458015](https://github.com/trycompai/comp/commit/a458015cf0bb5656f383c6371987b0c88474027f)) -* restore import of environment variables in Next.js config ([1e01ae4](https://github.com/trycompai/comp/commit/1e01ae418cdca30e722b35e8c0a32791379aef04)) -* restore prebuild script in package.json for Prisma client generation ([4850e68](https://github.com/trycompai/comp/commit/4850e6863a31cda497de1ef01cb96c47c9e1a475)) -* Restrict member management actions to Owner/Admin roles ([260131d](https://github.com/trycompai/comp/commit/260131d8fd8fc51c9564ce78b92381218740b0a9)) -* set controller versioning to VERSION_NEUTRAL for API consistency ([9ede4e8](https://github.com/trycompai/comp/commit/9ede4e824bf28bb4332bcb38d9cbdf47e73f6433)) -* simplify Prisma extension to copy generated client to correct location ([556bd17](https://github.com/trycompai/comp/commit/556bd1721eada7513e50e46d6acd72e60b61b610)) -* streamline elevation process in Windows script for clarity ([2705540](https://github.com/trycompai/comp/commit/2705540a6c21cffc581c546ee1d4f7eab7e9e1ce)) -* streamline script structure and enhance logging in Windows script ([e9a5afd](https://github.com/trycompai/comp/commit/e9a5afd978b4c0473bfb5ad803aba61f0eba019f)) -* Type errors ([ec77b2f](https://github.com/trycompai/comp/commit/ec77b2fb4b605386001af047dc16404f8b3cbad3)) -* **ui:** Align Employee breadcrumb and highlight People tab ([97c353b](https://github.com/trycompai/comp/commit/97c353b0e9dd3849bf5b63a6f4a0054159adf450)) -* update assetPrefix configuration to ensure proper URL handling in production ([3487041](https://github.com/trycompai/comp/commit/348704157cda7822eb867811d885275806f43ae5)) -* update BINARY_TARGET for Prisma extension ([639ac5e](https://github.com/trycompai/comp/commit/639ac5e658383550e2a217733d97141b09a4435f)) -* update Dockerfile to remove unnecessary bunfig.toml copy ([05193c1](https://github.com/trycompai/comp/commit/05193c1409d7cead8e71f378844a29dbb277eca9)) -* update invitation email domain configuration ([04579b0](https://github.com/trycompai/comp/commit/04579b075ad2976a7a91f3de9891f9170c58a131)) -* update marker file handling in Windows script ([fe37a73](https://github.com/trycompai/comp/commit/fe37a738822d0ec16437652724e76280224e13ca)) -* Update member role check ([3848a65](https://github.com/trycompai/comp/commit/3848a656a2b9da5c990b37f3bc214e8524f687e8)) -* update OpenAI model in onboarding and policy helpers for consistency ([fcb070f](https://github.com/trycompai/comp/commit/fcb070f69e54d33d507d6019563f7f2d1a5cda2c)) -* update Prisma extension to properly handle generated client location in Trigger.dev deployment ([a53cb42](https://github.com/trycompai/comp/commit/a53cb42d5c6212df3931bd04f76afd9061f0a3eb)) -* update registry path formatting in Windows script ([d48ea60](https://github.com/trycompai/comp/commit/d48ea605084f4a4de1d45c59fb9e384167dda0d3)) -* update role query in onboard-organization to use 'contains' for owner ([af9f1c3](https://github.com/trycompai/comp/commit/af9f1c35303664bca65f0ecf5b84fec14e9b5c6d)) -* update role validation in InviteMembersModal to prevent admin and employee overlap ([#1485](https://github.com/trycompai/comp/issues/1485)) ([18f3440](https://github.com/trycompai/comp/commit/18f34409af196add1b501a0d1eb6e85f6be34d5c)) -* update S3 bucket name environment variable ([1cc4fb2](https://github.com/trycompai/comp/commit/1cc4fb283735f12b091f62938b36af07629535f6)) -* update S3 bucket name environment variable ([74b98bc](https://github.com/trycompai/comp/commit/74b98bc267fd2709f7132c59d2a51fe4292bcb4c)) -* update typecheck:ci filters to correct package names ([dc8f93b](https://github.com/trycompai/comp/commit/dc8f93b90bddff281bbdfc916910243560d8139c)) -* use dynamic port variable for database connection strings ([f1f6030](https://github.com/trycompai/comp/commit/f1f60304efe352a8865f762415544592a970dd5e)) -* Validate user role for adding employee ([194b462](https://github.com/trycompai/comp/commit/194b462fe1b46233206a52062187fa6ce5f5614a)) -* Validate user role when revoking invitation ([7263791](https://github.com/trycompai/comp/commit/7263791df25c020cd6e0c34c809c92e19bcf0d58)) - +- add DEQUEUED state handling in OnboardingTracker component ([74d7151](https://github.com/trycompai/comp/commit/74d71518a53e7de404eb321dd9c778d7ab1ce57a)) +- add missing newlines in package.json and index.ts files for consistency ([f33bb34](https://github.com/trycompai/comp/commit/f33bb348cfb5d8919468f397965417489824a6e1)) +- add missing port number to database connection string ([bafbb27](https://github.com/trycompai/comp/commit/bafbb27affac170251a68f0a79038bc9c97851a1)) +- add newline at end of package.json for proper formatting ([fb91918](https://github.com/trycompai/comp/commit/fb91918aac91b456b8b61eac01e542b95a5542e3)) +- add persistent cmd session support in Windows script ([bfad924](https://github.com/trycompai/comp/commit/bfad924a7ecf8149f978c4545524c98a93a77573)) +- add robust database connectivity rules for CodeBuild ([50c9a7e](https://github.com/trycompai/comp/commit/50c9a7eac5ea4994eb94d5cbd82b002bdb059246)) +- add verification for public directory in buildspec.yml ([b8e2064](https://github.com/trycompai/comp/commit/b8e20645333547842e7731deef05c0fc53c7732c)) +- Added Control property to task sidebar ([8cc503b](https://github.com/trycompai/comp/commit/8cc503b70f02e3a0bb448fa0b4179f8601811017)) +- Allow access to auditor role ([1553400](https://github.com/trycompai/comp/commit/155340017c62a09dcfd03709011e25df89a85863)) +- **auth:** rename email parameter for invite function to improve clarity ([95bb6c9](https://github.com/trycompai/comp/commit/95bb6c9c8c12f6543cfa4e4a3e4862c8471845e6)) +- **buildspec, Dockerfile:** update Node.js version and adjust Prisma client generation path ([2d677d0](https://github.com/trycompai/comp/commit/2d677d05f54d9cc84c0def4369c1e07ed46bd511)) +- **buildspec:** adjust working directory for dependency installation ([0774214](https://github.com/trycompai/comp/commit/0774214ef2aba48215acb24d4739884f578ff753)) +- **buildspec:** enhance type checking step with optional script handling ([f151692](https://github.com/trycompai/comp/commit/f151692672559c3ee2d9b00586befb0a3964b375)) +- comprehensive Prisma deployment fix for Vercel monorepo ([6ef5033](https://github.com/trycompai/comp/commit/6ef50339a5ef29931cc1991241e0ca777db21f7b)) +- **config:** update Vercel environment check for standalone output ([39f3710](https://github.com/trycompai/comp/commit/39f3710cda9cf6ba57f3aa0308ee5f19868acff9)) +- copy Prisma binary to locations where runtime is searching ([7c3af89](https://github.com/trycompai/comp/commit/7c3af894a75439a2df512e79d834e83cd7ca7a9f)) +- correct Docker entry point for monorepo structure ([bb3aecb](https://github.com/trycompai/comp/commit/bb3aecb58c407592a76afa6ca3c197ccd538d56b)) +- correct environment variable assignment for Prisma query engine in Next.js config ([1ccaae1](https://github.com/trycompai/comp/commit/1ccaae1279b80e9410283f52fb6d183c2121d92f)) +- correct indentation in package.json scripts section ([f3ed25a](https://github.com/trycompai/comp/commit/f3ed25a85ae44a585633f309ada466b896df1ae8)) +- correct static file paths for next.js standalone monorepo structure ([55442ab](https://github.com/trycompai/comp/commit/55442ab2f4f96b25be5d919be43580cd32bdb733)) +- correct static file paths for next.js standalone monorepo structure ([12adfa6](https://github.com/trycompai/comp/commit/12adfa621f5ed8b6ebae8227fb07fc4055a63ee5)) +- correct Windows path formatting in fleet label creation script ([f34610f](https://github.com/trycompai/comp/commit/f34610fcfcc46a157d80c36204be40147679da7f)) +- **deploy:** ensure pulumi update completes before starting build ([d47e9b5](https://github.com/trycompai/comp/commit/d47e9b5b5b3eff8e36c1006f12f4a108506575bb)) +- **docs:** Broken contributing guide link in PR template ([7c8f509](https://github.com/trycompai/comp/commit/7c8f509a594183c1bd41a187271f4aeba7065573)) +- Enforce role-based access control in app ([8a81f42](https://github.com/trycompai/comp/commit/8a81f42b22da29a7a39e2b4698e94421a6a329fd)) +- enhance directory and marker file handling in Windows script ([49e3ec4](https://github.com/trycompai/comp/commit/49e3ec4f51bdee8647aa3c1684abcef041d376df)) +- enhance elevation and error handling in Windows script ([1a67177](https://github.com/trycompai/comp/commit/1a67177a638a87576c662dafefaad39158ef0884)) +- enhance elevation process in Windows script for better user experience ([9b4aeee](https://github.com/trycompai/comp/commit/9b4aeee2cfca20766bc6d457ef83f465dc52f700)) +- enhance logging and error handling in Windows script ([9c54eb0](https://github.com/trycompai/comp/commit/9c54eb09fba40a63b4d5a641526f7cd4292f61e3)) +- enhance session token retrieval in middleware ([27a7254](https://github.com/trycompai/comp/commit/27a72549959ea8f367bf0cff86ae6faf75a5ed01)) +- enhance structure and logging in Windows script ([e27a003](https://github.com/trycompai/comp/commit/e27a0038e1bcb63cca12411cd3cfac0b6867a05b)) +- ensure consistent newline handling in Prisma client and exports ([b5ce5f1](https://github.com/trycompai/comp/commit/b5ce5f11d108cf41259c5125b5768e974daf551e)) +- ensure Prisma binaries are included in Vercel deployment ([fa43f5e](https://github.com/trycompai/comp/commit/fa43f5eb9c75aba50fa232bbc8db8fe84f823a82)) +- ensure Prisma binaries are included in Vercel deployment with custom output path ([e877714](https://github.com/trycompai/comp/commit/e877714c77c2cb3ce6a238518cfc4e0de162cd25)) +- ensure Prisma client is generated during Next.js build for deployment ([a9ee09a](https://github.com/trycompai/comp/commit/a9ee09aebc52ed2f8766e90c526a81909495228c)) +- ensure Prisma query engine binary is properly copied in Trigger.dev deployment ([8d2fe33](https://github.com/trycompai/comp/commit/8d2fe332c99b5b65505c728ea9ac5e307fa87bd3)) +- fixed search ([36e12f1](https://github.com/trycompai/comp/commit/36e12f12cf3aae097ded63c33dc803b9596bcddd)) +- improve directory creation logic in Windows script ([71858ec](https://github.com/trycompai/comp/commit/71858ec19bcbce3b8e624e84d71a9420e62b668c)) +- improve directory selection logic in Windows script ([b2da074](https://github.com/trycompai/comp/commit/b2da0741cf3148966bc5800f335023f8af43a3b9)) +- improve Docker entry point detection for Next.js standalone build ([4e11649](https://github.com/trycompai/comp/commit/4e11649476f44cbe425c6a5446557083d83d2f9c)) +- improve error handling in file upload processes ([af52289](https://github.com/trycompai/comp/commit/af52289e4bb71752f915d96f74f542ada14ea293)) +- improve error handling in ToDoOverview component ([d4dd686](https://github.com/trycompai/comp/commit/d4dd686f8997002d569cde201ca65f2f1a134ee1)) +- improve PowerShell command in Windows script for better execution ([a313739](https://github.com/trycompai/comp/commit/a313739b3ff8c6481342ef4872fec4b2698886c8)) +- improve variable handling and logging in Windows script ([042db43](https://github.com/trycompai/comp/commit/042db43cd4e45087aaa1e881c4ca59d88947050f)) +- **infra:** correct echo command syntax in buildspec for database URL check ([1140eba](https://github.com/trycompai/comp/commit/1140ebae1b1230eca4b6b3554f7a37cc388c92c1)) +- **infra:** correct secrets manager syntax for codebuild and container ([7ada508](https://github.com/trycompai/comp/commit/7ada5083c13a632a3ade97f3c7d453271adfa191)) +- **infra:** correctly resolve database connection string for codebuild ([093bcb3](https://github.com/trycompai/comp/commit/093bcb3f5ba310ac4fc17be6d1c751fe71c56827)) +- **infra:** update health check configuration in container module ([c9b41a6](https://github.com/trycompai/comp/commit/c9b41a6acf20a8caa94a2b01c4c0271acb638013)) +- **infra:** update health check endpoint in container configuration ([5f3f4a2](https://github.com/trycompai/comp/commit/5f3f4a23a3386374b7a8a389f3eb3da4a5af2757)) +- Keep the `getRowId` and `rowClickBasePath` optional ([544afdc](https://github.com/trycompai/comp/commit/544afdcbd4885c501d2a2d9985e409f053020c53)) +- Member edit only for owner/admin ([1a2b9d2](https://github.com/trycompai/comp/commit/1a2b9d28d476f030a55761e30f2b9143df70ca87)) +- Move role checks on org level ([a4056bf](https://github.com/trycompai/comp/commit/a4056bf4d904baf450fd07c4e7ce0f83100e3504)) +- prevent build failure when metadata service is inaccessible ([951f462](https://github.com/trycompai/comp/commit/951f4622985044669503ca60a21388cc5f9207f5)) +- prevent infinite loop in TrustPortalSwitch by updating useEffect dependencies ([5f28201](https://github.com/trycompai/comp/commit/5f282018f3c9bee323a92503ff6a20406306a5ec)) +- Prisma seed command in `packages/db` ([a9957cd](https://github.com/trycompai/comp/commit/a9957cd3daf9d15430d1c47c45ae4e6139e47b92)) +- properly separate app env vars from infra env vars ([55aa5d6](https://github.com/trycompai/comp/commit/55aa5d607a1f33acec59bf1008e8b7a2604e6957)) +- refine directory creation logic in Windows script ([bfbfca0](https://github.com/trycompai/comp/commit/bfbfca0466d37452bc3aa70116d4eb96adeaa03c)) +- refine directory existence checks and logging in Windows script ([37ea28b](https://github.com/trycompai/comp/commit/37ea28b5a3362a1e678e9d23105cb16a00e715c3)) +- refine directory handling and logging in Windows script ([3982150](https://github.com/trycompai/comp/commit/3982150f3ea620097a535d80c7c315b3d7df76c3)) +- remove duplicate dependsOn key ([#1426](https://github.com/trycompai/comp/issues/1426)) ([9035238](https://github.com/trycompai/comp/commit/90352385bf358b6e4d30f0345ab715652b3dc3f7)) +- remove duplicate import of HealthModule in app.module.ts ([e02a51c](https://github.com/trycompai/comp/commit/e02a51c6c6330fe3bbb4b0097bf20492c47787a0)) +- resolve CodeBuild database connectivity issues ([04b1555](https://github.com/trycompai/comp/commit/04b155568619dfcbdce12bf6aa234df1c77229a2)) +- resolve YAML syntax error in buildspec.yml ([a458015](https://github.com/trycompai/comp/commit/a458015cf0bb5656f383c6371987b0c88474027f)) +- restore import of environment variables in Next.js config ([1e01ae4](https://github.com/trycompai/comp/commit/1e01ae418cdca30e722b35e8c0a32791379aef04)) +- restore prebuild script in package.json for Prisma client generation ([4850e68](https://github.com/trycompai/comp/commit/4850e6863a31cda497de1ef01cb96c47c9e1a475)) +- Restrict member management actions to Owner/Admin roles ([260131d](https://github.com/trycompai/comp/commit/260131d8fd8fc51c9564ce78b92381218740b0a9)) +- set controller versioning to VERSION_NEUTRAL for API consistency ([9ede4e8](https://github.com/trycompai/comp/commit/9ede4e824bf28bb4332bcb38d9cbdf47e73f6433)) +- simplify Prisma extension to copy generated client to correct location ([556bd17](https://github.com/trycompai/comp/commit/556bd1721eada7513e50e46d6acd72e60b61b610)) +- streamline elevation process in Windows script for clarity ([2705540](https://github.com/trycompai/comp/commit/2705540a6c21cffc581c546ee1d4f7eab7e9e1ce)) +- streamline script structure and enhance logging in Windows script ([e9a5afd](https://github.com/trycompai/comp/commit/e9a5afd978b4c0473bfb5ad803aba61f0eba019f)) +- Type errors ([ec77b2f](https://github.com/trycompai/comp/commit/ec77b2fb4b605386001af047dc16404f8b3cbad3)) +- **ui:** Align Employee breadcrumb and highlight People tab ([97c353b](https://github.com/trycompai/comp/commit/97c353b0e9dd3849bf5b63a6f4a0054159adf450)) +- update assetPrefix configuration to ensure proper URL handling in production ([3487041](https://github.com/trycompai/comp/commit/348704157cda7822eb867811d885275806f43ae5)) +- update BINARY_TARGET for Prisma extension ([639ac5e](https://github.com/trycompai/comp/commit/639ac5e658383550e2a217733d97141b09a4435f)) +- update Dockerfile to remove unnecessary bunfig.toml copy ([05193c1](https://github.com/trycompai/comp/commit/05193c1409d7cead8e71f378844a29dbb277eca9)) +- update invitation email domain configuration ([04579b0](https://github.com/trycompai/comp/commit/04579b075ad2976a7a91f3de9891f9170c58a131)) +- update marker file handling in Windows script ([fe37a73](https://github.com/trycompai/comp/commit/fe37a738822d0ec16437652724e76280224e13ca)) +- Update member role check ([3848a65](https://github.com/trycompai/comp/commit/3848a656a2b9da5c990b37f3bc214e8524f687e8)) +- update OpenAI model in onboarding and policy helpers for consistency ([fcb070f](https://github.com/trycompai/comp/commit/fcb070f69e54d33d507d6019563f7f2d1a5cda2c)) +- update Prisma extension to properly handle generated client location in Trigger.dev deployment ([a53cb42](https://github.com/trycompai/comp/commit/a53cb42d5c6212df3931bd04f76afd9061f0a3eb)) +- update registry path formatting in Windows script ([d48ea60](https://github.com/trycompai/comp/commit/d48ea605084f4a4de1d45c59fb9e384167dda0d3)) +- update role query in onboard-organization to use 'contains' for owner ([af9f1c3](https://github.com/trycompai/comp/commit/af9f1c35303664bca65f0ecf5b84fec14e9b5c6d)) +- update role validation in InviteMembersModal to prevent admin and employee overlap ([#1485](https://github.com/trycompai/comp/issues/1485)) ([18f3440](https://github.com/trycompai/comp/commit/18f34409af196add1b501a0d1eb6e85f6be34d5c)) +- update S3 bucket name environment variable ([1cc4fb2](https://github.com/trycompai/comp/commit/1cc4fb283735f12b091f62938b36af07629535f6)) +- update S3 bucket name environment variable ([74b98bc](https://github.com/trycompai/comp/commit/74b98bc267fd2709f7132c59d2a51fe4292bcb4c)) +- update typecheck:ci filters to correct package names ([dc8f93b](https://github.com/trycompai/comp/commit/dc8f93b90bddff281bbdfc916910243560d8139c)) +- use dynamic port variable for database connection strings ([f1f6030](https://github.com/trycompai/comp/commit/f1f60304efe352a8865f762415544592a970dd5e)) +- Validate user role for adding employee ([194b462](https://github.com/trycompai/comp/commit/194b462fe1b46233206a52062187fa6ce5f5614a)) +- Validate user role when revoking invitation ([7263791](https://github.com/trycompai/comp/commit/7263791df25c020cd6e0c34c809c92e19bcf0d58)) ### Features -* add @ai-sdk/rsc package and update imports ([15d7d66](https://github.com/trycompai/comp/commit/15d7d667556a71a2f8702c0ff88a5c331b0224ff)) -* add advanced mode functionality for organizations ([#1503](https://github.com/trycompai/comp/issues/1503)) ([04a9e26](https://github.com/trycompai/comp/commit/04a9e26ad0693bae058175c04f37fcb77b145b32)) -* add API endpoint to approve organizations for QA team ([3b526b4](https://github.com/trycompai/comp/commit/3b526b4bd8e47817981f7ea8f8734a19b75e8f24)) -* add API endpoint to delete users for QA team ([89a6d16](https://github.com/trycompai/comp/commit/89a6d16daef5e7b67dcc9496c1d2d0e70f865661)) -* add BETTER_AUTH_URL to environment variables and buildspec validation ([a767884](https://github.com/trycompai/comp/commit/a76788479d7bb08504a95bae30edebec7595130a)) -* add buildspec and deployment scripts for application ([1722e2e](https://github.com/trycompai/comp/commit/1722e2eb260dc4eabf3917abc32bf4851d9dc64b)) -* add debug scripts for ECS and CodeBuild log retrieval ([02b7df5](https://github.com/trycompai/comp/commit/02b7df520866a210f4bbb612f3d9f003c1d2f6c3)) -* add delete confirmation dialog to CommentItem component ([a94ecd7](https://github.com/trycompai/comp/commit/a94ecd7abd8c3c046ea9840c94b433305d2b0684)) -* add department column to policies table ([#1499](https://github.com/trycompai/comp/issues/1499)) ([cff6bc9](https://github.com/trycompai/comp/commit/cff6bc96437c638adf0a7e4d07675a9f881252a6)) -* add deployment verification script for ECS ([eee55f6](https://github.com/trycompai/comp/commit/eee55f6e1fbfdf4c1afbf09b9c947ee8bd9dcd62)) -* add Dockerfiles for app and portal services ([98efb51](https://github.com/trycompai/comp/commit/98efb51dd67b26b77c32b1797600c3725347ede2)) -* add E2E and unit testing workflows ([e1f0f7b](https://github.com/trycompai/comp/commit/e1f0f7ba037909e7282b562f8a9fac8dec7e9387)) -* add Font components to email templates for improved typography ([e2059a6](https://github.com/trycompai/comp/commit/e2059a67bdb39459debc1e235874fef104ed67c5)) -* add geo field to onboarding process ([fd74000](https://github.com/trycompai/comp/commit/fd740003667abb4bcc4385f44045c3c140b750e2)) -* add header value sanitization for S3 metadata ([d582d40](https://github.com/trycompai/comp/commit/d582d40ab80d1af5063fd51401fcdb359ea34c92)) -* add Husky hooks for commit message validation and pre-push checks ([99fffad](https://github.com/trycompai/comp/commit/99fffad46c012b6967c12df6c0ddf35faefc7059)) -* add migration for JWKS table to support JWT authentication ([9cc1f88](https://github.com/trycompai/comp/commit/9cc1f8895bf096f3461d6665cafb8b95cbddd662)) -* add NEXT_PUBLIC_API_URL environment variable ([f31ee34](https://github.com/trycompai/comp/commit/f31ee344dd02061f30952915928c3af70b0f5125)) -* Add PDF view for policies ([#1451](https://github.com/trycompai/comp/issues/1451)) ([c4e52fc](https://github.com/trycompai/comp/commit/c4e52fc25642145142ef3d45f716e4fb3be468cf)) -* add Prisma client generation task and update build dependencies ([e0061c0](https://github.com/trycompai/comp/commit/e0061c02384a2a344e91ac09baec6f719d8d410e)) -* add progress bars to ComplianceOverview for improved visibility ([d84daeb](https://github.com/trycompai/comp/commit/d84daeba7cfedf50ba6f912f5b669282fd869607)) -* add required Next.js environment variables with proper validation ([3d10913](https://github.com/trycompai/comp/commit/3d10913a1469af86df9b633d11596bd06462fb2d)) -* add risk and vendor action components and regeneration functionality ([6b7d3a4](https://github.com/trycompai/comp/commit/6b7d3a4a2e3663c8e3a1bb0fb7655a7ee98cd2cb)) -* add shouldGenerateTypes option to PrismaExtensionOptions ([cbc1d36](https://github.com/trycompai/comp/commit/cbc1d3680b438f07de3b9725bb96456b35833a96)) -* Add sorting options for organization list ([bcf4434](https://github.com/trycompai/comp/commit/bcf4434ff0513e40554e7d548d68ccfded55826f)) -* add TCP connectivity debugging to buildspec ([804bcb3](https://github.com/trycompai/comp/commit/804bcb3e0f6b6a5d85c7dbd613caf7c65d4bc3a3)) -* add TipTap content validation utilities for AI-generated content ([9d0437e](https://github.com/trycompai/comp/commit/9d0437e073c85d830af0f43e45233a28d7354429)) -* enable cross-subdomain cookies for authentication ([e7d8521](https://github.com/trycompai/comp/commit/e7d8521e565a2a1104d0eec1dac737d6eb4c7357)) -* enable dynamic rendering and set revalidation for layout component ([27f2555](https://github.com/trycompai/comp/commit/27f255523d6ee0f5ccf4ff71f71e7570c7d3b2d7)) -* enhance content validation by removing empty text nodes ([ba735e4](https://github.com/trycompai/comp/commit/ba735e4dce27cf8a0fc5db4ba6e611b9c95085b2)) -* enhance control status logic and onboarding process ([4e00985](https://github.com/trycompai/comp/commit/4e00985999991de4a6354722b088598c722ed150)) -* enhance delete user API to require email for user deletion ([4548a0c](https://github.com/trycompai/comp/commit/4548a0c6694f6ab6f7ad0332a13da36de9797ba0)) -* enhance DeviceAgentAccordionItem for macOS support and update download handling ([ebff53e](https://github.com/trycompai/comp/commit/ebff53ec48ad07f7bc21ec220ee7c4bdfbf0b148)) -* enhance download agent with improved error handling and configuration ([900d99a](https://github.com/trycompai/comp/commit/900d99a7be390e19036c82a7ccd27ef71f5228bf)) -* enhance editor functionality with linkification and configurable extensions ([5bb1f94](https://github.com/trycompai/comp/commit/5bb1f94d3d8d6762d625eead0e423fe7bfb85807)) -* enhance Fleet label creation and error handling ([8f84460](https://github.com/trycompai/comp/commit/8f84460d8ea49e4b747523ccdacde175870e0908)) -* enhance framework compliance tracking and UI ([c566a01](https://github.com/trycompai/comp/commit/c566a01a86beb32b91e4ae3fd87dca57860d90a0)) -* enhance GitHub OIDC module to support existing provider usage ([9212589](https://github.com/trycompai/comp/commit/92125894b5ad1637ac2a137690382fe99f0175b2)) -* enhance HybridAuthGuard and update authentication interfaces ([3868a3e](https://github.com/trycompai/comp/commit/3868a3e7b287150068ef3500d62b5a0b03941a56)) -* enhance invitation handling and user redirection ([e3fb7d3](https://github.com/trycompai/comp/commit/e3fb7d3686b5338680995fc05afbb0dd7e5b25b9)) -* enhance MemberRow component and onboarding task updates ([41f8006](https://github.com/trycompai/comp/commit/41f80062b3f6be20d4fa85802852966ce56bd13f)) -* enhance onboarding process with local development support ([a7fab14](https://github.com/trycompai/comp/commit/a7fab14af21cb534778eed5f686e543e04633062)) -* enhance onboarding tasks with increased concurrency and retry logic ([362cef8](https://github.com/trycompai/comp/commit/362cef81c3757858e1ea989d092c7549195019cd)) -* Enhance PDF handling in policy viewer ([#1476](https://github.com/trycompai/comp/issues/1476)) ([c005b3e](https://github.com/trycompai/comp/commit/c005b3e54ce0381c7c9886e7683ea7460644407f)) -* enhance policy management with regeneration and UI improvements ([c4b4c0d](https://github.com/trycompai/comp/commit/c4b4c0dfe957116776a1e04b394540578d5df014)) -* enhance Prisma client generation and application secrets management ([d44cfa3](https://github.com/trycompai/comp/commit/d44cfa364b214fa1457a428123fa3a776526b3ca)) -* enhance Prisma client generation process with detailed logging ([a9210a8](https://github.com/trycompai/comp/commit/a9210a80a02826dbc8d08566a30567d0a3298a83)) -* enhance Prisma exports and update db:generate script ([2d6195d](https://github.com/trycompai/comp/commit/2d6195d8f776096e518f3982732e47d8685dcd18)) -* enhance PrismaExtension to resolve and copy schema from @trycompai/db package ([8bd0263](https://github.com/trycompai/comp/commit/8bd02634731ba32f1f933a84dfd204e85a73bdab)) -* enhance schema resolution in PrismaExtension for monorepo support ([c413f6f](https://github.com/trycompai/comp/commit/c413f6fc51332f9057653c1929034ac5823fc52d)) -* enhance ToDoOverview component with dynamic tab selection ([e03f402](https://github.com/trycompai/comp/commit/e03f4024753fe6e193f27e3f72200e4f838fd00d)) -* implement attachment download functionality and metadata retrieval ([dde6d48](https://github.com/trycompai/comp/commit/dde6d486cde5d91163d948f127200238e5e33f93)) -* implement auto-resizing for TaskBody textarea ([758cbb2](https://github.com/trycompai/comp/commit/758cbb2624aa6609586f747c1cf471e6b7f1d0db)) -* implement comments and attachments functionality in API ([51162ac](https://github.com/trycompai/comp/commit/51162ac03463ea66415e7a7c33e2e1421f04d64a)) -* implement control creation functionality with UI integration ([#1497](https://github.com/trycompai/comp/issues/1497)) ([943a7d3](https://github.com/trycompai/comp/commit/943a7d3a1e808ed8a5ec6e518c888a387b2cab8d)) -* implement CORS and security headers for API routes ([35aaeb1](https://github.com/trycompai/comp/commit/35aaeb1bd725c3af74444ea8651bd60402bbe2ee)) -* implement DynamicMinHeight component for responsive layout ([09f52cf](https://github.com/trycompai/comp/commit/09f52cffd704e0c1bbe26c9a2885f9e19e521154)) -* implement JWT authentication and update related components ([2786ef6](https://github.com/trycompai/comp/commit/2786ef6f6d6f677ea017dac812c9268bbdd986ce)) -* implement optimistic updates for comments and task attachments ([7fe5669](https://github.com/trycompai/comp/commit/7fe5669da23f5733a0de23889e9ac389eb5763b2)) -* implement publish all policies action and UI components ([52afe86](https://github.com/trycompai/comp/commit/52afe8665dd8a87e24cb9c7d1abfe52087c0ef3d)) -* implement risk and vendor mitigation tasks ([da94ecc](https://github.com/trycompai/comp/commit/da94ecccd22650a166f1353d90ef4a798e0ceb6d)) -* implement session-based JWT retrieval in ApiClient ([33af882](https://github.com/trycompai/comp/commit/33af8822c6251b789f0d0d8ec03ec6ec13662550)) -* implement TriggerTokenProvider for access token management ([6812f39](https://github.com/trycompai/comp/commit/6812f39f425023b07df19bb59919546b360dfc51)) -* improve invite page handling and user feedback ([eb9bcab](https://github.com/trycompai/comp/commit/eb9bcab5bdf84e670103544297438e1619b3b7f8)) -* **infra:** add NEXT_PUBLIC_PORTAL_URL to applications configuration ([bdd5f5f](https://github.com/trycompai/comp/commit/bdd5f5fdd5380a77d4b2e77932a38c73a8f2378e)) -* **infra:** add source version to build system configuration ([424f6ad](https://github.com/trycompai/comp/commit/424f6ad684cd41f5ce9a343c96b234ef6db0d79f)) -* **infra:** add STS VPC endpoint for IAM role assumption ([b5a70c4](https://github.com/trycompai/comp/commit/b5a70c4895a2d0a13e50899e13cfe5cd42b0f6b9)) -* **infra:** enhance application container to support dynamic secret management ([b5a7ee7](https://github.com/trycompai/comp/commit/b5a7ee714ae6d5fb4850d282a9c7656f56e705ab)) -* **infra:** enhance build and deployment configurations for applications ([67fc172](https://github.com/trycompai/comp/commit/67fc172794f65552ed3bde57c633e4a51e2520e7)) -* **infra:** enhance build system to include required secrets from AWS Secrets Manager ([7de1d6c](https://github.com/trycompai/comp/commit/7de1d6ca8000e020e44994317871ba8d1d06d9df)) -* **infra:** implement SSL certificate management and DNS validation ([da41e90](https://github.com/trycompai/comp/commit/da41e909d4beae6ce462dc63a6e3c3edb7058d79)) -* **infra:** refactor application secrets management to support individual secrets ([72b00d4](https://github.com/trycompai/comp/commit/72b00d4da3a9a10c3e9ac469fd1aaa923a8a89db)) -* **infra:** refactor build system to improve environment variable handling ([cc53704](https://github.com/trycompai/comp/commit/cc53704188f96af17076b0353b23d75564746a93)) -* initialize API module with NestJS structure and essential configurations ([2ea50d2](https://github.com/trycompai/comp/commit/2ea50d28525396a8aba2b3f50d6fd4f484241f74)) -* initialize Pulumi infrastructure for Pathfinder app ([3cc14fa](https://github.com/trycompai/comp/commit/3cc14fa681a8c1c4bc5d97dbe5ad73e59cf98a1c)) -* integrate Prisma client and update database schema handling ([81c4c3a](https://github.com/trycompai/comp/commit/81c4c3a68334200a31826735e4ed07d4665f2712)) -* integrate Prisma client generation and update imports for local usage ([e7cbc8f](https://github.com/trycompai/comp/commit/e7cbc8f7e71caf188a55e9437bf977ca7371ec90)) -* introduce custom PrismaExtension for enhanced schema handling ([856f869](https://github.com/trycompai/comp/commit/856f869804913ae0a6b6b72cda4d50feb04671e8)) -* New & improved dashboard ([38ce071](https://github.com/trycompai/comp/commit/38ce071d7cf5da9eb5bf94d5061fa6ed55608a1b)) -* optimize build process with parallel processing and enhanced caching ([7276586](https://github.com/trycompai/comp/commit/7276586d25dda1ed91e2d665213e921b2ea348b8)) -* optimize width calculation in ToDoOverview component ([6e55979](https://github.com/trycompai/comp/commit/6e55979c4c20980ee47516f8500b9ebde63743db)) -* read app environment variables from apps/app/.env ([b49fb1c](https://github.com/trycompai/comp/commit/b49fb1c352f20ad62dece108ad79308fbac6020e)) -* simplify Prisma exports and update db:generate script ([b6f9757](https://github.com/trycompai/comp/commit/b6f97571f437ba3c49d32ccbc00a2dd38381e299)) -* streamline policy generation by directly producing TipTap JSON ([1c799e2](https://github.com/trycompai/comp/commit/1c799e2a687c759aa23a2e6d6eda13c5d6a8cbb3)) -* Support opening policy in new tab for easier navigation ([42c2626](https://github.com/trycompai/comp/commit/42c2626b61d5e81d92fe032236b87da29f3a2475)) -* support protocol-less links in comments ([a5f124b](https://github.com/trycompai/comp/commit/a5f124b5b29032e371e86ed1b653d882fe4dcdc3)) -* update AddFrameworkModal to make organizationId optional ([d742463](https://github.com/trycompai/comp/commit/d742463c715548da5f8fad16198a3fc173967110)) -* update API client and auth utility to use Bearer token authentication ([1d95394](https://github.com/trycompai/comp/commit/1d95394c95078708b786a28b4d2e603f29c79cb2)) -* update auto-pr-to-main workflow to include new chas/* path ([861eb35](https://github.com/trycompai/comp/commit/861eb35f1243f01e49f3aa7dfad36f1561b15e50)) -* update dependencies and enhance onboarding process ([59db96d](https://github.com/trycompai/comp/commit/59db96df9c744b43c45558a7c6ae95acbc8aa05e)) -* update DeviceAgentAccordionItem to enhance user guidance for macOS and Windows ([ec1876c](https://github.com/trycompai/comp/commit/ec1876c9157f465bcba741f0e797aaffe1f1f4db)) -* update OpenAI model and enhance TipTap JSON schema for policy updates ([79c5f2a](https://github.com/trycompai/comp/commit/79c5f2a56985a9505bf0029862ed555f4d5d797a)) -* update policy signature requirement and dependencies ([bfdbb15](https://github.com/trycompai/comp/commit/bfdbb15a70d8f41ae1352733eb5fe214ca27c7e9)) -* update publishAllPoliciesAction to use parsedInput for organizationId ([67741b4](https://github.com/trycompai/comp/commit/67741b4bf9c5065e0d4c98efaa368eebf081fc6f)) -* update S3 key handling for fleet agent downloads ([9a5ab4f](https://github.com/trycompai/comp/commit/9a5ab4f62f263de183e0cdde3d6d7bb7aed08072)) -* update SingleControl component and enhance control status logic ([4287fec](https://github.com/trycompai/comp/commit/4287fecd47cbe4f2c232dc2cf9635ba6e13fc34e)) -* Update tasks on top level ([3cb06d6](https://github.com/trycompai/comp/commit/3cb06d670fb288300b18214737f39c412966fd19)) -* update Trust Portal to include HIPAA compliance framework ([bde32c4](https://github.com/trycompai/comp/commit/bde32c4ef4c39e0325b5bfa32d1d65ffacc3a1e2)) -* use PULUMI_PROJECT_NAME as environment identifier ([29fed1e](https://github.com/trycompai/comp/commit/29fed1eca43895f2bf85144df1ec98be531de4cd)) -* Vendor delete button ([e98926a](https://github.com/trycompai/comp/commit/e98926afd4dee2dd13e7f111b08e4bcd0288670a)) +- add @ai-sdk/rsc package and update imports ([15d7d66](https://github.com/trycompai/comp/commit/15d7d667556a71a2f8702c0ff88a5c331b0224ff)) +- add advanced mode functionality for organizations ([#1503](https://github.com/trycompai/comp/issues/1503)) ([04a9e26](https://github.com/trycompai/comp/commit/04a9e26ad0693bae058175c04f37fcb77b145b32)) +- add API endpoint to approve organizations for QA team ([3b526b4](https://github.com/trycompai/comp/commit/3b526b4bd8e47817981f7ea8f8734a19b75e8f24)) +- add API endpoint to delete users for QA team ([89a6d16](https://github.com/trycompai/comp/commit/89a6d16daef5e7b67dcc9496c1d2d0e70f865661)) +- add BETTER_AUTH_URL to environment variables and buildspec validation ([a767884](https://github.com/trycompai/comp/commit/a76788479d7bb08504a95bae30edebec7595130a)) +- add buildspec and deployment scripts for application ([1722e2e](https://github.com/trycompai/comp/commit/1722e2eb260dc4eabf3917abc32bf4851d9dc64b)) +- add debug scripts for ECS and CodeBuild log retrieval ([02b7df5](https://github.com/trycompai/comp/commit/02b7df520866a210f4bbb612f3d9f003c1d2f6c3)) +- add delete confirmation dialog to CommentItem component ([a94ecd7](https://github.com/trycompai/comp/commit/a94ecd7abd8c3c046ea9840c94b433305d2b0684)) +- add department column to policies table ([#1499](https://github.com/trycompai/comp/issues/1499)) ([cff6bc9](https://github.com/trycompai/comp/commit/cff6bc96437c638adf0a7e4d07675a9f881252a6)) +- add deployment verification script for ECS ([eee55f6](https://github.com/trycompai/comp/commit/eee55f6e1fbfdf4c1afbf09b9c947ee8bd9dcd62)) +- add Dockerfiles for app and portal services ([98efb51](https://github.com/trycompai/comp/commit/98efb51dd67b26b77c32b1797600c3725347ede2)) +- add E2E and unit testing workflows ([e1f0f7b](https://github.com/trycompai/comp/commit/e1f0f7ba037909e7282b562f8a9fac8dec7e9387)) +- add Font components to email templates for improved typography ([e2059a6](https://github.com/trycompai/comp/commit/e2059a67bdb39459debc1e235874fef104ed67c5)) +- add geo field to onboarding process ([fd74000](https://github.com/trycompai/comp/commit/fd740003667abb4bcc4385f44045c3c140b750e2)) +- add header value sanitization for S3 metadata ([d582d40](https://github.com/trycompai/comp/commit/d582d40ab80d1af5063fd51401fcdb359ea34c92)) +- add Husky hooks for commit message validation and pre-push checks ([99fffad](https://github.com/trycompai/comp/commit/99fffad46c012b6967c12df6c0ddf35faefc7059)) +- add migration for JWKS table to support JWT authentication ([9cc1f88](https://github.com/trycompai/comp/commit/9cc1f8895bf096f3461d6665cafb8b95cbddd662)) +- add NEXT_PUBLIC_API_URL environment variable ([f31ee34](https://github.com/trycompai/comp/commit/f31ee344dd02061f30952915928c3af70b0f5125)) +- Add PDF view for policies ([#1451](https://github.com/trycompai/comp/issues/1451)) ([c4e52fc](https://github.com/trycompai/comp/commit/c4e52fc25642145142ef3d45f716e4fb3be468cf)) +- add Prisma client generation task and update build dependencies ([e0061c0](https://github.com/trycompai/comp/commit/e0061c02384a2a344e91ac09baec6f719d8d410e)) +- add progress bars to ComplianceOverview for improved visibility ([d84daeb](https://github.com/trycompai/comp/commit/d84daeba7cfedf50ba6f912f5b669282fd869607)) +- add required Next.js environment variables with proper validation ([3d10913](https://github.com/trycompai/comp/commit/3d10913a1469af86df9b633d11596bd06462fb2d)) +- add risk and vendor action components and regeneration functionality ([6b7d3a4](https://github.com/trycompai/comp/commit/6b7d3a4a2e3663c8e3a1bb0fb7655a7ee98cd2cb)) +- add shouldGenerateTypes option to PrismaExtensionOptions ([cbc1d36](https://github.com/trycompai/comp/commit/cbc1d3680b438f07de3b9725bb96456b35833a96)) +- Add sorting options for organization list ([bcf4434](https://github.com/trycompai/comp/commit/bcf4434ff0513e40554e7d548d68ccfded55826f)) +- add TCP connectivity debugging to buildspec ([804bcb3](https://github.com/trycompai/comp/commit/804bcb3e0f6b6a5d85c7dbd613caf7c65d4bc3a3)) +- add TipTap content validation utilities for AI-generated content ([9d0437e](https://github.com/trycompai/comp/commit/9d0437e073c85d830af0f43e45233a28d7354429)) +- enable cross-subdomain cookies for authentication ([e7d8521](https://github.com/trycompai/comp/commit/e7d8521e565a2a1104d0eec1dac737d6eb4c7357)) +- enable dynamic rendering and set revalidation for layout component ([27f2555](https://github.com/trycompai/comp/commit/27f255523d6ee0f5ccf4ff71f71e7570c7d3b2d7)) +- enhance content validation by removing empty text nodes ([ba735e4](https://github.com/trycompai/comp/commit/ba735e4dce27cf8a0fc5db4ba6e611b9c95085b2)) +- enhance control status logic and onboarding process ([4e00985](https://github.com/trycompai/comp/commit/4e00985999991de4a6354722b088598c722ed150)) +- enhance delete user API to require email for user deletion ([4548a0c](https://github.com/trycompai/comp/commit/4548a0c6694f6ab6f7ad0332a13da36de9797ba0)) +- enhance DeviceAgentAccordionItem for macOS support and update download handling ([ebff53e](https://github.com/trycompai/comp/commit/ebff53ec48ad07f7bc21ec220ee7c4bdfbf0b148)) +- enhance download agent with improved error handling and configuration ([900d99a](https://github.com/trycompai/comp/commit/900d99a7be390e19036c82a7ccd27ef71f5228bf)) +- enhance editor functionality with linkification and configurable extensions ([5bb1f94](https://github.com/trycompai/comp/commit/5bb1f94d3d8d6762d625eead0e423fe7bfb85807)) +- enhance Fleet label creation and error handling ([8f84460](https://github.com/trycompai/comp/commit/8f84460d8ea49e4b747523ccdacde175870e0908)) +- enhance framework compliance tracking and UI ([c566a01](https://github.com/trycompai/comp/commit/c566a01a86beb32b91e4ae3fd87dca57860d90a0)) +- enhance GitHub OIDC module to support existing provider usage ([9212589](https://github.com/trycompai/comp/commit/92125894b5ad1637ac2a137690382fe99f0175b2)) +- enhance HybridAuthGuard and update authentication interfaces ([3868a3e](https://github.com/trycompai/comp/commit/3868a3e7b287150068ef3500d62b5a0b03941a56)) +- enhance invitation handling and user redirection ([e3fb7d3](https://github.com/trycompai/comp/commit/e3fb7d3686b5338680995fc05afbb0dd7e5b25b9)) +- enhance MemberRow component and onboarding task updates ([41f8006](https://github.com/trycompai/comp/commit/41f80062b3f6be20d4fa85802852966ce56bd13f)) +- enhance onboarding process with local development support ([a7fab14](https://github.com/trycompai/comp/commit/a7fab14af21cb534778eed5f686e543e04633062)) +- enhance onboarding tasks with increased concurrency and retry logic ([362cef8](https://github.com/trycompai/comp/commit/362cef81c3757858e1ea989d092c7549195019cd)) +- Enhance PDF handling in policy viewer ([#1476](https://github.com/trycompai/comp/issues/1476)) ([c005b3e](https://github.com/trycompai/comp/commit/c005b3e54ce0381c7c9886e7683ea7460644407f)) +- enhance policy management with regeneration and UI improvements ([c4b4c0d](https://github.com/trycompai/comp/commit/c4b4c0dfe957116776a1e04b394540578d5df014)) +- enhance Prisma client generation and application secrets management ([d44cfa3](https://github.com/trycompai/comp/commit/d44cfa364b214fa1457a428123fa3a776526b3ca)) +- enhance Prisma client generation process with detailed logging ([a9210a8](https://github.com/trycompai/comp/commit/a9210a80a02826dbc8d08566a30567d0a3298a83)) +- enhance Prisma exports and update db:generate script ([2d6195d](https://github.com/trycompai/comp/commit/2d6195d8f776096e518f3982732e47d8685dcd18)) +- enhance PrismaExtension to resolve and copy schema from @trycompai/db package ([8bd0263](https://github.com/trycompai/comp/commit/8bd02634731ba32f1f933a84dfd204e85a73bdab)) +- enhance schema resolution in PrismaExtension for monorepo support ([c413f6f](https://github.com/trycompai/comp/commit/c413f6fc51332f9057653c1929034ac5823fc52d)) +- enhance ToDoOverview component with dynamic tab selection ([e03f402](https://github.com/trycompai/comp/commit/e03f4024753fe6e193f27e3f72200e4f838fd00d)) +- implement attachment download functionality and metadata retrieval ([dde6d48](https://github.com/trycompai/comp/commit/dde6d486cde5d91163d948f127200238e5e33f93)) +- implement auto-resizing for TaskBody textarea ([758cbb2](https://github.com/trycompai/comp/commit/758cbb2624aa6609586f747c1cf471e6b7f1d0db)) +- implement comments and attachments functionality in API ([51162ac](https://github.com/trycompai/comp/commit/51162ac03463ea66415e7a7c33e2e1421f04d64a)) +- implement control creation functionality with UI integration ([#1497](https://github.com/trycompai/comp/issues/1497)) ([943a7d3](https://github.com/trycompai/comp/commit/943a7d3a1e808ed8a5ec6e518c888a387b2cab8d)) +- implement CORS and security headers for API routes ([35aaeb1](https://github.com/trycompai/comp/commit/35aaeb1bd725c3af74444ea8651bd60402bbe2ee)) +- implement DynamicMinHeight component for responsive layout ([09f52cf](https://github.com/trycompai/comp/commit/09f52cffd704e0c1bbe26c9a2885f9e19e521154)) +- implement JWT authentication and update related components ([2786ef6](https://github.com/trycompai/comp/commit/2786ef6f6d6f677ea017dac812c9268bbdd986ce)) +- implement optimistic updates for comments and task attachments ([7fe5669](https://github.com/trycompai/comp/commit/7fe5669da23f5733a0de23889e9ac389eb5763b2)) +- implement publish all policies action and UI components ([52afe86](https://github.com/trycompai/comp/commit/52afe8665dd8a87e24cb9c7d1abfe52087c0ef3d)) +- implement risk and vendor mitigation tasks ([da94ecc](https://github.com/trycompai/comp/commit/da94ecccd22650a166f1353d90ef4a798e0ceb6d)) +- implement session-based JWT retrieval in ApiClient ([33af882](https://github.com/trycompai/comp/commit/33af8822c6251b789f0d0d8ec03ec6ec13662550)) +- implement TriggerTokenProvider for access token management ([6812f39](https://github.com/trycompai/comp/commit/6812f39f425023b07df19bb59919546b360dfc51)) +- improve invite page handling and user feedback ([eb9bcab](https://github.com/trycompai/comp/commit/eb9bcab5bdf84e670103544297438e1619b3b7f8)) +- **infra:** add NEXT_PUBLIC_PORTAL_URL to applications configuration ([bdd5f5f](https://github.com/trycompai/comp/commit/bdd5f5fdd5380a77d4b2e77932a38c73a8f2378e)) +- **infra:** add source version to build system configuration ([424f6ad](https://github.com/trycompai/comp/commit/424f6ad684cd41f5ce9a343c96b234ef6db0d79f)) +- **infra:** add STS VPC endpoint for IAM role assumption ([b5a70c4](https://github.com/trycompai/comp/commit/b5a70c4895a2d0a13e50899e13cfe5cd42b0f6b9)) +- **infra:** enhance application container to support dynamic secret management ([b5a7ee7](https://github.com/trycompai/comp/commit/b5a7ee714ae6d5fb4850d282a9c7656f56e705ab)) +- **infra:** enhance build and deployment configurations for applications ([67fc172](https://github.com/trycompai/comp/commit/67fc172794f65552ed3bde57c633e4a51e2520e7)) +- **infra:** enhance build system to include required secrets from AWS Secrets Manager ([7de1d6c](https://github.com/trycompai/comp/commit/7de1d6ca8000e020e44994317871ba8d1d06d9df)) +- **infra:** implement SSL certificate management and DNS validation ([da41e90](https://github.com/trycompai/comp/commit/da41e909d4beae6ce462dc63a6e3c3edb7058d79)) +- **infra:** refactor application secrets management to support individual secrets ([72b00d4](https://github.com/trycompai/comp/commit/72b00d4da3a9a10c3e9ac469fd1aaa923a8a89db)) +- **infra:** refactor build system to improve environment variable handling ([cc53704](https://github.com/trycompai/comp/commit/cc53704188f96af17076b0353b23d75564746a93)) +- initialize API module with NestJS structure and essential configurations ([2ea50d2](https://github.com/trycompai/comp/commit/2ea50d28525396a8aba2b3f50d6fd4f484241f74)) +- initialize Pulumi infrastructure for Pathfinder app ([3cc14fa](https://github.com/trycompai/comp/commit/3cc14fa681a8c1c4bc5d97dbe5ad73e59cf98a1c)) +- integrate Prisma client and update database schema handling ([81c4c3a](https://github.com/trycompai/comp/commit/81c4c3a68334200a31826735e4ed07d4665f2712)) +- integrate Prisma client generation and update imports for local usage ([e7cbc8f](https://github.com/trycompai/comp/commit/e7cbc8f7e71caf188a55e9437bf977ca7371ec90)) +- introduce custom PrismaExtension for enhanced schema handling ([856f869](https://github.com/trycompai/comp/commit/856f869804913ae0a6b6b72cda4d50feb04671e8)) +- New & improved dashboard ([38ce071](https://github.com/trycompai/comp/commit/38ce071d7cf5da9eb5bf94d5061fa6ed55608a1b)) +- optimize build process with parallel processing and enhanced caching ([7276586](https://github.com/trycompai/comp/commit/7276586d25dda1ed91e2d665213e921b2ea348b8)) +- optimize width calculation in ToDoOverview component ([6e55979](https://github.com/trycompai/comp/commit/6e55979c4c20980ee47516f8500b9ebde63743db)) +- read app environment variables from apps/app/.env ([b49fb1c](https://github.com/trycompai/comp/commit/b49fb1c352f20ad62dece108ad79308fbac6020e)) +- simplify Prisma exports and update db:generate script ([b6f9757](https://github.com/trycompai/comp/commit/b6f97571f437ba3c49d32ccbc00a2dd38381e299)) +- streamline policy generation by directly producing TipTap JSON ([1c799e2](https://github.com/trycompai/comp/commit/1c799e2a687c759aa23a2e6d6eda13c5d6a8cbb3)) +- Support opening policy in new tab for easier navigation ([42c2626](https://github.com/trycompai/comp/commit/42c2626b61d5e81d92fe032236b87da29f3a2475)) +- support protocol-less links in comments ([a5f124b](https://github.com/trycompai/comp/commit/a5f124b5b29032e371e86ed1b653d882fe4dcdc3)) +- update AddFrameworkModal to make organizationId optional ([d742463](https://github.com/trycompai/comp/commit/d742463c715548da5f8fad16198a3fc173967110)) +- update API client and auth utility to use Bearer token authentication ([1d95394](https://github.com/trycompai/comp/commit/1d95394c95078708b786a28b4d2e603f29c79cb2)) +- update auto-pr-to-main workflow to include new chas/\* path ([861eb35](https://github.com/trycompai/comp/commit/861eb35f1243f01e49f3aa7dfad36f1561b15e50)) +- update dependencies and enhance onboarding process ([59db96d](https://github.com/trycompai/comp/commit/59db96df9c744b43c45558a7c6ae95acbc8aa05e)) +- update DeviceAgentAccordionItem to enhance user guidance for macOS and Windows ([ec1876c](https://github.com/trycompai/comp/commit/ec1876c9157f465bcba741f0e797aaffe1f1f4db)) +- update OpenAI model and enhance TipTap JSON schema for policy updates ([79c5f2a](https://github.com/trycompai/comp/commit/79c5f2a56985a9505bf0029862ed555f4d5d797a)) +- update policy signature requirement and dependencies ([bfdbb15](https://github.com/trycompai/comp/commit/bfdbb15a70d8f41ae1352733eb5fe214ca27c7e9)) +- update publishAllPoliciesAction to use parsedInput for organizationId ([67741b4](https://github.com/trycompai/comp/commit/67741b4bf9c5065e0d4c98efaa368eebf081fc6f)) +- update S3 key handling for fleet agent downloads ([9a5ab4f](https://github.com/trycompai/comp/commit/9a5ab4f62f263de183e0cdde3d6d7bb7aed08072)) +- update SingleControl component and enhance control status logic ([4287fec](https://github.com/trycompai/comp/commit/4287fecd47cbe4f2c232dc2cf9635ba6e13fc34e)) +- Update tasks on top level ([3cb06d6](https://github.com/trycompai/comp/commit/3cb06d670fb288300b18214737f39c412966fd19)) +- update Trust Portal to include HIPAA compliance framework ([bde32c4](https://github.com/trycompai/comp/commit/bde32c4ef4c39e0325b5bfa32d1d65ffacc3a1e2)) +- use PULUMI_PROJECT_NAME as environment identifier ([29fed1e](https://github.com/trycompai/comp/commit/29fed1eca43895f2bf85144df1ec98be531de4cd)) +- Vendor delete button ([e98926a](https://github.com/trycompai/comp/commit/e98926afd4dee2dd13e7f111b08e4bcd0288670a)) # [1.50.0](https://github.com/trycompai/comp/compare/v1.49.0...v1.50.0) (2025-07-11) - ### Bug Fixes -* standardize plan type casing in PricingCard component ([6a2b48f](https://github.com/trycompai/comp/commit/6a2b48f275a03203394d3cc68e7f206876f8c0b8)) -* update subscription handling in checkout hook ([3175ddd](https://github.com/trycompai/comp/commit/3175ddd52fb43f2dfec9f073da43ad86cdf212fe)) - +- standardize plan type casing in PricingCard component ([6a2b48f](https://github.com/trycompai/comp/commit/6a2b48f275a03203394d3cc68e7f206876f8c0b8)) +- update subscription handling in checkout hook ([3175ddd](https://github.com/trycompai/comp/commit/3175ddd52fb43f2dfec9f073da43ad86cdf212fe)) ### Features -* enhance upgrade flow with subscription type handling ([5650623](https://github.com/trycompai/comp/commit/56506235a37d9b795207f001766e14a859d390db)) -* integrate HubSpot API for user and company management ([0ab2560](https://github.com/trycompai/comp/commit/0ab2560c124e2b77c5b1bcc2d4969f7bce7079b8)) +- enhance upgrade flow with subscription type handling ([5650623](https://github.com/trycompai/comp/commit/56506235a37d9b795207f001766e14a859d390db)) +- integrate HubSpot API for user and company management ([0ab2560](https://github.com/trycompai/comp/commit/0ab2560c124e2b77c5b1bcc2d4969f7bce7079b8)) # [1.49.0](https://github.com/trycompai/comp/compare/v1.48.1...v1.49.0) (2025-07-03) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fb3bc3e3..9a6683bd9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ To develop locally: - Duplicate `.env.example` to `.env`. - Use `openssl rand -base64 32` to generate a key and add it under `SECRET_KEY` in the `.env` file. - Setup Trigger.dev - - CD into apps/app and run `bunx trigger.dev@latest login`, then `bunx trigger.dev@latest dev` + - CD into apps/app and run `pnpx trigger.dev@latest login`, then `pnpx trigger.dev@latest dev` - Use `openssl rand -base64 32` to generate a key and add it under `TRIGGER_SECRET_KEY` in the `.env` file. 6. Start developing and watch for code changes: diff --git a/Dockerfile b/Dockerfile index b22294546..1b1e58ed7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ RUN cp -R packages/db/prisma/migrations node_modules/@trycompai/db/dist/ # Run migrations against the combined schema published by @trycompai/db RUN echo "Running migrations against @trycompai/db combined schema" -CMD ["bunx", "prisma", "migrate", "deploy", "--schema=node_modules/@trycompai/db/dist/schema.prisma"] +CMD ["pnpx", "prisma", "migrate", "deploy", "--schema=node_modules/@trycompai/db/dist/schema.prisma"] # ============================================================================= # STAGE 3: App Builder diff --git a/README.md b/README.md index 1eb2f6bd5..ae92056aa 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Some environment variables may not load correctly from `.env` — in such cases, - Create a project and copy the Project ID - In `comp/apps/app/trigger.config.ts`, set: ```ts - project: 'proj_****az***ywb**ob*'; + project: "proj_****az***ywb**ob*"; ``` #### 2. Google OAuth @@ -387,7 +387,7 @@ bun run release:packages --dry-run ## Repo Activity -![Alt](https://repobeats.axiom.co/api/embed/1371c2fe20e274ff1e0e8d4ca225455dea609cb9.svg 'Repobeats analytics image') +![Alt](https://repobeats.axiom.co/api/embed/1371c2fe20e274ff1e0e8d4ca225455dea609cb9.svg "Repobeats analytics image") diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index c225133e9..2eb652bc8 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -83,7 +83,7 @@ services: target: migrator env_file: - .env - command: sh -lc "bunx prisma generate --schema=node_modules/@trycompai/db/dist/schema.prisma && bun packages/db/prisma/seed/seed.js" + command: sh -lc "pnpx prisma generate --schema=node_modules/@trycompai/db/dist/schema.prisma && bun packages/db/prisma/seed/seed.js" app: build: @@ -92,11 +92,11 @@ services: target: app args: NEXT_PUBLIC_BETTER_AUTH_URL: ${BETTER_AUTH_URL} - ports: ['3000:3000'] + ports: ["3000:3000"] env_file: [.env] restart: unless-stopped healthcheck: - test: ['CMD-SHELL', 'curl -f http://localhost:3000/api/health || exit 1'] + test: ["CMD-SHELL", "curl -f http://localhost:3000/api/health || exit 1"] interval: 30s timeout: 10s retries: 3 @@ -108,11 +108,11 @@ services: target: portal args: NEXT_PUBLIC_BETTER_AUTH_URL: ${BETTER_AUTH_URL_PORTAL} - ports: ['3002:3000'] + ports: ["3002:3000"] env_file: [.env] restart: unless-stopped healthcheck: - test: ['CMD-SHELL', 'curl -f http://localhost:3002/ || exit 1'] + test: ["CMD-SHELL", "curl -f http://localhost:3002/ || exit 1"] interval: 30s timeout: 10s retries: 3 @@ -195,8 +195,8 @@ Steps: 3. From your workstation (not inside Docker): ```bash cd apps/app - bunx trigger.dev@latest login - bunx trigger.dev@latest deploy + pnpx trigger.dev@latest login + pnpx trigger.dev@latest deploy ``` 4. Set `TRIGGER_SECRET_KEY` in the `app` service environment. diff --git a/apps/api/.prettierrc b/apps/api/.prettierrc deleted file mode 100644 index dcb72794f..000000000 --- a/apps/api/.prettierrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "singleQuote": true, - "trailingComma": "all" -} \ No newline at end of file diff --git a/apps/api/clean.sh b/apps/api/clean.sh new file mode 100755 index 000000000..135b79cc2 --- /dev/null +++ b/apps/api/clean.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TARGET_DIRS=( + "$ROOT_DIR/src" +) + +echo "Cleaning generated .js, .js.map, and .d.ts files in apps/api" + +for dir in "${TARGET_DIRS[@]}"; do + if [[ -d "$dir" ]]; then + echo "Processing $dir" + find "$dir" \( -name '*.js' -o -name '*.js.map' -o -name '*.d.ts' \) -type f -print -delete + fi +done + +echo "Done." + diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs deleted file mode 100644 index 212fd04b3..000000000 --- a/apps/api/eslint.config.mjs +++ /dev/null @@ -1,39 +0,0 @@ -// @ts-check -import eslint from '@eslint/js'; -import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { - ignores: ['eslint.config.mjs'], - }, - eslint.configs.recommended, - ...tseslint.configs.recommendedTypeChecked, - eslintPluginPrettierRecommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.jest, - }, - sourceType: 'commonjs', - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, - }, - { - rules: { - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'warn', - '@typescript-eslint/no-unsafe-argument': 'warn', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/restrict-template-expressions': 'off', - }, - }, -); diff --git a/apps/api/eslint.config.ts b/apps/api/eslint.config.ts new file mode 100644 index 000000000..96a04d970 --- /dev/null +++ b/apps/api/eslint.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'eslint/config'; + +import { baseConfig } from '@trycompai/eslint-config/base'; + +export default defineConfig( + { + ignores: ['dist/**'], + }, + baseConfig, +); diff --git a/apps/api/package.json b/apps/api/package.json index 4bed55244..9c275decf 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -4,59 +4,58 @@ "version": "0.0.1", "author": "", "dependencies": { - "@aws-sdk/client-s3": "^3.859.0", - "@aws-sdk/s3-request-presigner": "^3.859.0", - "@nestjs/common": "^11.0.1", + "@aws-sdk/client-s3": "^3.936.0", + "@aws-sdk/s3-request-presigner": "^3.936.0", + "@nestjs/common": "^11.1.9", "@nestjs/config": "^4.0.2", - "@nestjs/core": "^11.0.1", - "@nestjs/platform-express": "^11.1.5", - "@nestjs/swagger": "^11.2.0", - "@prisma/client": "^6.13.0", - "@trycompai/db": "^1.3.17", + "@nestjs/core": "^11.1.9", + "@nestjs/platform-express": "^11.1.9", + "@nestjs/swagger": "^11.2.3", + "@trycompai/db": "workspace:*", "@trycompai/email": "workspace:*", "archiver": "^7.0.1", - "axios": "^1.12.2", - "better-auth": "^1.3.27", + "axios": "^1.13.2", + "better-auth": "^1.3.34", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "dotenv": "^17.2.3", - "jose": "^6.0.12", - "jspdf": "^3.0.3", + "express": "^5.1.0", + "jose": "^6.1.2", + "jspdf": "^3.0.4", "nanoid": "^5.1.6", "pdf-lib": "^1.17.1", - "prisma": "^6.13.0", + "pg": "^8.16.3", "reflect-metadata": "^0.2.2", - "resend": "^6.4.2", - "rxjs": "^7.8.1", + "rxjs": "^7.8.2", "swagger-ui-express": "^5.0.1", - "zod": "^4.0.14" + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "zod": "^4.1.12" }, "devDependencies": { - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.18.0", - "@nestjs/cli": "^11.0.0", - "@nestjs/schematics": "^11.0.0", - "@nestjs/testing": "^11.0.1", - "@types/archiver": "^6.0.3", - "@types/express": "^5.0.0", + "@nestjs/cli": "^11.0.12", + "@nestjs/schematics": "^11.0.9", + "@nestjs/testing": "^11.1.9", + "@trycompai/db": "workspace:*", + "@trycompai/email": "workspace:*", + "@trycompai/eslint-config": "workspace:*", + "@trycompai/tsconfig": "workspace:*", + "@types/archiver": "^7.0.0", + "@types/express": "^5.0.5", "@types/jest": "^30.0.0", - "@types/node": "^24.0.3", - "@types/supertest": "^6.0.2", + "@types/node": "^24.10.1", + "@types/supertest": "^6.0.3", "@types/swagger-ui-express": "^4.1.8", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.2", - "globals": "^16.0.0", - "jest": "^30.0.0", - "prettier": "^3.5.3", + "eslint": "^9.39.1", + "globals": "^16.5.0", + "jest": "^30.2.0", "source-map-support": "^0.5.21", - "supertest": "^7.0.0", - "ts-jest": "^29.2.5", - "ts-loader": "^9.5.2", - "ts-node": "^10.9.2", + "supertest": "^7.1.4", + "ts-jest": "^29.4.5", + "ts-loader": "^9.5.4", "tsconfig-paths": "^4.2.0", - "typescript": "^5.8.3", - "typescript-eslint": "^8.20.0" + "typescript": "^5.9.3", + "typescript-eslint": "^8.47.0" }, "jest": { "moduleFileExtensions": [ @@ -79,18 +78,14 @@ "private": true, "scripts": { "build": "nest build", - "build:docker": "bunx prisma generate && nest build", - "db:generate": "bun run db:getschema && bunx prisma generate", - "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", - "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/api", - "dev": "nest start --watch", - "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "prebuild": "bun run db:generate", - "start": "nest start", - "start:debug": "nest start --debug --watch", - "start:dev": "nest start --watch", - "start:prod": "node dist/main", + "build:docker": "nest build", + "dev": "nest start --watch --exec \"node --enable-source-maps --experimental-specifier-resolution=node dist/main.js\"", + "format": "eslint . --fix", + "lint": "eslint .", + "start": "nest start --exec \"node --enable-source-maps --experimental-specifier-resolution=node dist/main.js\"", + "start:debug": "nest start --debug --watch --exec \"node --enable-source-maps --experimental-specifier-resolution=node dist/main.js\"", + "start:prod": "node --enable-source-maps --experimental-specifier-resolution=node dist/main.js", + "clean": "rm -rf dist .turbo node_modules", "test": "jest", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", diff --git a/apps/api/prisma/client.ts b/apps/api/prisma/client.ts deleted file mode 100644 index a696328be..000000000 --- a/apps/api/prisma/client.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const globalForPrisma = global as unknown as { prisma: PrismaClient }; - -export const db = globalForPrisma.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/apps/api/prisma/index.ts b/apps/api/prisma/index.ts deleted file mode 100644 index 54d1c4b9c..000000000 --- a/apps/api/prisma/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@prisma/client'; -export { db } from './client'; diff --git a/apps/api/src/attachments/attachments.service.ts b/apps/api/src/attachments/attachments.service.ts index a63c72038..6d7c4ccba 100644 --- a/apps/api/src/attachments/attachments.service.ts +++ b/apps/api/src/attachments/attachments.service.ts @@ -5,7 +5,7 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { AttachmentEntityType, AttachmentType, db } from '@db'; +import { AttachmentEntityType, AttachmentType } from '@trycompai/db'; import { BadRequestException, Injectable, @@ -14,6 +14,7 @@ import { import { randomBytes } from 'crypto'; import { AttachmentResponseDto } from '../tasks/dto/task-responses.dto'; import { UploadAttachmentDto } from './upload-attachment.dto'; +import { db } from '@trycompai/db'; @Injectable() export class AttachmentsService { diff --git a/apps/api/src/comments/comments.controller.ts b/apps/api/src/comments/comments.controller.ts index 2026055e3..79157952d 100644 --- a/apps/api/src/comments/comments.controller.ts +++ b/apps/api/src/comments/comments.controller.ts @@ -1,4 +1,4 @@ -import { CommentEntityType } from '@db'; +import { CommentEntityType } from '@trycompai/db'; import { BadRequestException, Body, diff --git a/apps/api/src/comments/comments.service.ts b/apps/api/src/comments/comments.service.ts index 7d245dd8b..66e1036a0 100644 --- a/apps/api/src/comments/comments.service.ts +++ b/apps/api/src/comments/comments.service.ts @@ -1,4 +1,4 @@ -import { AttachmentEntityType, CommentEntityType } from '@db'; +import { AttachmentEntityType, CommentEntityType } from '@trycompai/db'; import { BadRequestException, Injectable, diff --git a/apps/api/src/comments/dto/create-comment.dto.ts b/apps/api/src/comments/dto/create-comment.dto.ts index 6775f8832..02efc9a8d 100644 --- a/apps/api/src/comments/dto/create-comment.dto.ts +++ b/apps/api/src/comments/dto/create-comment.dto.ts @@ -1,4 +1,4 @@ -import { CommentEntityType } from '@db'; +import { CommentEntityType } from '@trycompai/db'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { diff --git a/apps/api/src/tasks/attachments.service.ts b/apps/api/src/tasks/attachments.service.ts index 876f9bce9..e580bdfd0 100644 --- a/apps/api/src/tasks/attachments.service.ts +++ b/apps/api/src/tasks/attachments.service.ts @@ -5,7 +5,7 @@ import { S3Client, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { AttachmentEntityType, AttachmentType } from '@db'; +import { AttachmentEntityType, AttachmentType } from '@trycompai/db'; import { BadRequestException, Injectable, diff --git a/apps/api/src/tasks/dto/task-responses.dto.ts b/apps/api/src/tasks/dto/task-responses.dto.ts index 3870fe307..acd0dc257 100644 --- a/apps/api/src/tasks/dto/task-responses.dto.ts +++ b/apps/api/src/tasks/dto/task-responses.dto.ts @@ -1,4 +1,3 @@ -import { MemberResponseDto } from '@/devices/dto/member-responses.dto'; import { ApiProperty } from '@nestjs/swagger'; export class AttachmentResponseDto { diff --git a/apps/api/src/tasks/tasks.controller.ts b/apps/api/src/tasks/tasks.controller.ts index cde966a7d..1e7250911 100644 --- a/apps/api/src/tasks/tasks.controller.ts +++ b/apps/api/src/tasks/tasks.controller.ts @@ -1,4 +1,4 @@ -import { AttachmentEntityType } from '@db'; +import { AttachmentEntityType } from '@trycompai/db'; import { BadRequestException, Body, diff --git a/apps/api/src/vendors/vendors.controller.ts b/apps/api/src/vendors/vendors.controller.ts index 1bcd8f1e8..c9072e0e2 100644 --- a/apps/api/src/vendors/vendors.controller.ts +++ b/apps/api/src/vendors/vendors.controller.ts @@ -1,11 +1,11 @@ import { + Body, Controller, - Get, - Post, - Patch, Delete, - Body, + Get, Param, + Patch, + Post, UseGuards, } from '@nestjs/common'; import { @@ -20,17 +20,17 @@ import { import { AuthContext, OrganizationId } from '../auth/auth-context.decorator'; import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; import type { AuthContext as AuthContextType } from '../auth/types'; -import { CreateVendorDto } from './dto/create-vendor.dto'; -import { UpdateVendorDto } from './dto/update-vendor.dto'; -import { VendorsService } from './vendors.service'; -import { VENDOR_OPERATIONS } from './schemas/vendor-operations'; -import { VENDOR_PARAMS } from './schemas/vendor-params'; -import { VENDOR_BODIES } from './schemas/vendor-bodies'; +import type { CreateVendorDto } from './dto/create-vendor.dto'; +import type { UpdateVendorDto } from './dto/update-vendor.dto'; +import { CREATE_VENDOR_RESPONSES } from './schemas/create-vendor.responses'; +import { DELETE_VENDOR_RESPONSES } from './schemas/delete-vendor.responses'; import { GET_ALL_VENDORS_RESPONSES } from './schemas/get-all-vendors.responses'; import { GET_VENDOR_BY_ID_RESPONSES } from './schemas/get-vendor-by-id.responses'; -import { CREATE_VENDOR_RESPONSES } from './schemas/create-vendor.responses'; import { UPDATE_VENDOR_RESPONSES } from './schemas/update-vendor.responses'; -import { DELETE_VENDOR_RESPONSES } from './schemas/delete-vendor.responses'; +import { VENDOR_BODIES } from './schemas/vendor-bodies'; +import { VENDOR_OPERATIONS } from './schemas/vendor-operations'; +import { VENDOR_PARAMS } from './schemas/vendor-params'; +import type { VendorsService } from './vendors.service'; @ApiTags('Vendors') @Controller({ path: 'vendors', version: '1' }) diff --git a/apps/api/src/vendors/vendors.service.ts b/apps/api/src/vendors/vendors.service.ts index 159848356..1021a80d5 100644 --- a/apps/api/src/vendors/vendors.service.ts +++ b/apps/api/src/vendors/vendors.service.ts @@ -1,7 +1,7 @@ -import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { db } from '@trycompai/db'; -import { CreateVendorDto } from './dto/create-vendor.dto'; -import { UpdateVendorDto } from './dto/update-vendor.dto'; +import type { CreateVendorDto } from './dto/create-vendor.dto'; +import type { UpdateVendorDto } from './dto/update-vendor.dto'; @Injectable() export class VendorsService { diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json index 64f86c6bd..cbe30027a 100644 --- a/apps/api/tsconfig.build.json +++ b/apps/api/tsconfig.build.json @@ -1,4 +1,20 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "compilerOptions": { + "outDir": "./dist", + "sourceMap": true, + "inlineSources": true, + "incremental": true, + "tsBuildInfoFile": "./dist/tsconfig.build.tsbuildinfo", + "declaration": false + }, + "include": ["src/**/*.ts"], + "exclude": [ + "dist", + "node_modules", + "test", + "**/*.spec.ts", + "**/*.e2e-spec.ts", + "eslint.config.ts" + ] } diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json index 7e903ec25..ca7bb5a7e 100644 --- a/apps/api/tsconfig.json +++ b/apps/api/tsconfig.json @@ -1,29 +1,25 @@ { + "extends": "@trycompai/tsconfig/base.json", "compilerOptions": { - "module": "nodenext", - "moduleResolution": "nodenext", - "resolvePackageJsonExports": true, - "esModuleInterop": true, - "isolatedModules": true, - "declaration": true, - "removeComments": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["node", "jest"], + "verbatimModuleSyntax": false, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "rootDir": "src", + "jsx": "react-jsx", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "esnext", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "forceConsistentCasingInFileNames": true, + "strictPropertyInitialization": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "strictNullChecks": false, "noImplicitAny": false, - "strictBindCallApply": false, - "noFallthroughCasesInSwitch": false, - "paths": { - "@/*": ["./src/*"], - "@db": ["./prisma/index"] - } - } + "useUnknownInCatchVariables": false, + "noUncheckedIndexedAccess": false + }, + "include": ["src"], + "exclude": ["dist", "node_modules"] } diff --git a/apps/app/.eslintrc.json b/apps/app/.eslintrc.json deleted file mode 100644 index ab0a70296..000000000 --- a/apps/app/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": ["next/core-web-vitals"], - "ignorePatterns": ["src/db/generated/**"], - "rules": { - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "react/no-unescaped-entities": "off", - "react/display-name": "off" - } -} diff --git a/apps/app/.prettierignore b/apps/app/.prettierignore deleted file mode 100644 index d31f44e6c..000000000 --- a/apps/app/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -# Playwright test artifacts -playwright-report/ -test-results/ -.playwright/ - -# Generated Prisma Client -src/db/generated/ \ No newline at end of file diff --git a/apps/app/customPrismaExtension.ts b/apps/app/customPrismaExtension.ts index 5afb75c50..72d858cc3 100644 --- a/apps/app/customPrismaExtension.ts +++ b/apps/app/customPrismaExtension.ts @@ -1,8 +1,13 @@ -import { binaryForRuntime, BuildContext, BuildExtension, BuildManifest } from '@trigger.dev/build'; -import assert from 'node:assert'; -import { existsSync } from 'node:fs'; -import { cp } from 'node:fs/promises'; -import { dirname, join, resolve } from 'node:path'; +import assert from "node:assert"; +import { existsSync } from "node:fs"; +import { cp } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { + binaryForRuntime, + BuildContext, + BuildExtension, + BuildManifest, +} from "@trigger.dev/build"; export type PrismaExtensionOptions = { version?: string; @@ -14,32 +19,34 @@ export type PrismaExtensionOptions = { dbPackageVersion?: string; }; -export function prismaExtension(options: PrismaExtensionOptions = {}): PrismaExtension { +export function prismaExtension( + options: PrismaExtensionOptions = {}, +): PrismaExtension { return new PrismaExtension(options); } export class PrismaExtension implements BuildExtension { moduleExternals: string[]; - public readonly name = 'PrismaExtension'; + public readonly name = "PrismaExtension"; private _resolvedSchemaPath?: string; constructor(private options: PrismaExtensionOptions) { this.moduleExternals = [ - '@prisma/client', - '@prisma/engines', - '@trycompai/db', // Add the published package to externals + "@prisma/client", + "@prisma/engines", + "@trycompai/db", // Add the published package to externals ]; } externalsForTarget(target: any) { - if (target === 'dev') { + if (target === "dev") { return []; } return this.moduleExternals; } async onBuildStart(context: BuildContext) { - if (context.target === 'dev') { + if (context.target === "dev") { return; } @@ -47,12 +54,21 @@ export class PrismaExtension implements BuildExtension { // In a monorepo, node_modules are typically hoisted to the workspace root // Walk up the directory tree to find the workspace root (where node_modules/@trycompai/db exists) let workspaceRoot = context.workingDir; - let dbPackagePath = resolve(workspaceRoot, 'node_modules/@trycompai/db/dist/schema.prisma'); + let dbPackagePath = resolve( + workspaceRoot, + "node_modules/@trycompai/db/dist/schema.prisma", + ); // If not found in working dir, try parent directories - while (!existsSync(dbPackagePath) && workspaceRoot !== dirname(workspaceRoot)) { + while ( + !existsSync(dbPackagePath) && + workspaceRoot !== dirname(workspaceRoot) + ) { workspaceRoot = dirname(workspaceRoot); - dbPackagePath = resolve(workspaceRoot, 'node_modules/@trycompai/db/dist/schema.prisma'); + dbPackagePath = resolve( + workspaceRoot, + "node_modules/@trycompai/db/dist/schema.prisma", + ); } this._resolvedSchemaPath = dbPackagePath; @@ -63,13 +79,19 @@ export class PrismaExtension implements BuildExtension { ); // Debug: List contents of the @trycompai/db package directory - const dbPackageDir = resolve(workspaceRoot, 'node_modules/@trycompai/db'); - const dbDistDir = resolve(workspaceRoot, 'node_modules/@trycompai/db/dist'); + const dbPackageDir = resolve(workspaceRoot, "node_modules/@trycompai/db"); + const dbDistDir = resolve(workspaceRoot, "node_modules/@trycompai/db/dist"); try { - const { readdirSync } = require('node:fs'); - context.logger.debug(`@trycompai/db package directory contents:`, readdirSync(dbPackageDir)); - context.logger.debug(`@trycompai/db/dist directory contents:`, readdirSync(dbDistDir)); + const { readdirSync } = require("node:fs"); + context.logger.debug( + `@trycompai/db package directory contents:`, + readdirSync(dbPackageDir), + ); + context.logger.debug( + `@trycompai/db/dist directory contents:`, + readdirSync(dbDistDir), + ); } catch (err) { context.logger.debug(`Failed to list directory contents:`, err); } @@ -77,24 +99,24 @@ export class PrismaExtension implements BuildExtension { // Check that the prisma schema exists in the published package if (!existsSync(this._resolvedSchemaPath)) { throw new Error( - `PrismaExtension could not find the prisma schema at ${this._resolvedSchemaPath}. Make sure @trycompai/db package is installed with version ${this.options.dbPackageVersion || 'latest'}`, + `PrismaExtension could not find the prisma schema at ${this._resolvedSchemaPath}. Make sure @trycompai/db package is installed with version ${this.options.dbPackageVersion || "latest"}`, ); } } async onBuildComplete(context: BuildContext, manifest: BuildManifest) { - if (context.target === 'dev') { + if (context.target === "dev") { return; } - assert(this._resolvedSchemaPath, 'Resolved schema path is not set'); + assert(this._resolvedSchemaPath, "Resolved schema path is not set"); - context.logger.debug('Looking for @prisma/client in the externals', { + context.logger.debug("Looking for @prisma/client in the externals", { externals: manifest.externals, }); const prismaExternal = manifest.externals?.find( - (external) => external.name === '@prisma/client', + (external) => external.name === "@prisma/client", ); const version = prismaExternal?.version ?? this.options.version; @@ -112,7 +134,11 @@ export class PrismaExtension implements BuildExtension { const env: Record = {}; // Copy the prisma schema from the published package to the build output path - const schemaDestinationPath = join(manifest.outputPath, 'prisma', 'schema.prisma'); + const schemaDestinationPath = join( + manifest.outputPath, + "prisma", + "schema.prisma", + ); context.logger.debug( `Copying the prisma schema from ${this._resolvedSchemaPath} to ${schemaDestinationPath}`, ); @@ -126,10 +152,10 @@ export class PrismaExtension implements BuildExtension { // Only handle migrations if requested if (this.options.migrate) { context.logger.debug( - 'Migration support not implemented for published package - please handle migrations separately', + "Migration support not implemented for published package - please handle migrations separately", ); // You could add migration commands here if needed - // commands.push(`${binaryForRuntime(manifest.runtime)} npx prisma migrate deploy`); + // commands.push(`${binaryForRuntime(manifest.runtime)} pnpx prisma migrate deploy`); } // Set up environment variables @@ -151,25 +177,28 @@ export class PrismaExtension implements BuildExtension { if (!env.DATABASE_URL) { context.logger.warn( - 'prismaExtension could not resolve the DATABASE_URL environment variable. Make sure you add it to your environment variables. See our docs for more info: https://trigger.dev/docs/deploy-environment-variables', + "prismaExtension could not resolve the DATABASE_URL environment variable. Make sure you add it to your environment variables. See our docs for more info: https://trigger.dev/docs/deploy-environment-variables", ); } - context.logger.debug('Adding the prisma layer with the following commands', { - commands, - env, - dependencies: { - prisma: version, - '@trycompai/db': this.options.dbPackageVersion || 'latest', + context.logger.debug( + "Adding the prisma layer with the following commands", + { + commands, + env, + dependencies: { + prisma: version, + "@trycompai/db": this.options.dbPackageVersion || "latest", + }, }, - }); + ); context.addLayer({ - id: 'prisma', + id: "prisma", commands, dependencies: { prisma: version, - '@trycompai/db': this.options.dbPackageVersion || 'latest', + "@trycompai/db": this.options.dbPackageVersion || "latest", }, build: { env, diff --git a/apps/app/e2e/README.md b/apps/app/e2e/README.md index b5e3bdc33..ffd8e2825 100644 --- a/apps/app/e2e/README.md +++ b/apps/app/e2e/README.md @@ -102,12 +102,12 @@ e2e/ ### Basic Test Structure ```typescript -import { test, expect } from '@playwright/test'; +import { expect, test } from "@playwright/test"; -test.describe('Feature Name', () => { - test('should do something', async ({ page }) => { - await page.goto('/'); - await expect(page).toHaveTitle('My App'); +test.describe("Feature Name", () => { + test("should do something", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle("My App"); }); }); ``` @@ -115,24 +115,24 @@ test.describe('Feature Name', () => { ### Using Helper Functions ```typescript -import { fillFormField, clickAndWait, expectToast } from '../utils/helpers'; +import { clickAndWait, expectToast, fillFormField } from "../utils/helpers"; -test('create organization', async ({ page }) => { - await fillFormField(page, '[name="name"]', 'My Organization'); +test("create organization", async ({ page }) => { + await fillFormField(page, '[name="name"]', "My Organization"); await clickAndWait(page, 'button[type="submit"]'); - await expectToast(page, 'Organization created successfully'); + await expectToast(page, "Organization created successfully"); }); ``` ### Using Authentication Fixture ```typescript -import { test, expect } from '../fixtures/auth'; +import { expect, test } from "../fixtures/auth"; -test('authenticated user flow', async ({ authenticatedPage }) => { +test("authenticated user flow", async ({ authenticatedPage }) => { // authenticatedPage is already logged in - await authenticatedPage.goto('/dashboard'); - await expect(authenticatedPage).toHaveURL('/dashboard'); + await authenticatedPage.goto("/dashboard"); + await expect(authenticatedPage).toHaveURL("/dashboard"); }); ``` @@ -180,7 +180,7 @@ DATABASE_URL=postgresql://user:pass@localhost:5432/test_db 1. **View trace**: Tests save traces on failure ```bash - bunx playwright show-trace trace.zip + pnpx playwright show-trace trace.zip ``` 2. **Debug mode**: Step through tests interactively @@ -230,7 +230,7 @@ The E2E workflow: ### Browser not installed - Run `bun run test:e2e:install` -- Or install specific browser: `bunx playwright install chromium` +- Or install specific browser: `pnpx playwright install chromium` ## 📊 Viewing Test Results @@ -244,4 +244,4 @@ See [TEST_RESULTS_VIEWING.md](./TEST_RESULTS_VIEWING.md) for detailed instructio ## 🔧 Debugging Tests -bunx playwright test --ui +pnpx playwright test --ui diff --git a/apps/app/e2e/TEST_RESULTS_VIEWING.md b/apps/app/e2e/TEST_RESULTS_VIEWING.md index 9e8500aa6..f636d70fd 100644 --- a/apps/app/e2e/TEST_RESULTS_VIEWING.md +++ b/apps/app/e2e/TEST_RESULTS_VIEWING.md @@ -47,7 +47,7 @@ Debug failed tests with step-by-step execution traces. **Local viewer:** ```bash -bunx playwright show-trace path/to/trace.zip +pnpx playwright show-trace path/to/trace.zip ``` ## 📊 What's Available diff --git a/apps/app/e2e/fixtures/auth.ts b/apps/app/e2e/fixtures/auth.ts index 8ff6f4f3a..1cb1598cd 100644 --- a/apps/app/e2e/fixtures/auth.ts +++ b/apps/app/e2e/fixtures/auth.ts @@ -1,6 +1,7 @@ -import { test as base, expect, type BrowserContext, type Page } from '@playwright/test'; -import fs from 'fs/promises'; -import path from 'path'; +import fs from "fs/promises"; +import path from "path"; +import type { BrowserContext, Page } from "@playwright/test"; +import { test as base, expect } from "@playwright/test"; // Define custom fixtures type AuthFixtures = { @@ -11,7 +12,7 @@ export const test = base.extend({ // This fixture provides an authenticated page authenticatedPage: async ({ browser }, use) => { // Check if we have saved auth state - const authFile = path.join(__dirname, '../auth/user.json'); + const authFile = path.join(__dirname, "../auth/user.json"); let context: BrowserContext; try { @@ -21,11 +22,11 @@ export const test = base.extend({ // Verify the auth state is still valid const page = await context.newPage(); - await page.goto('/'); + await page.goto("/"); // If we get redirected to auth, the session is invalid - if (page.url().includes('/auth')) { - throw new Error('Saved auth state is invalid'); + if (page.url().includes("/auth")) { + throw new Error("Saved auth state is invalid"); } await page.close(); @@ -35,37 +36,42 @@ export const test = base.extend({ const page = await context.newPage(); // Go to login page and authenticate - await page.goto('/auth'); + await page.goto("/auth"); // For Google OAuth with better-auth // Option 1: Use test account with real Google OAuth (recommended for E2E) - if (process.env.E2E_USE_REAL_AUTH === 'true') { + if (process.env.E2E_USE_REAL_AUTH === "true") { // Click Google sign-in button await page.click( 'button:has-text("Continue with Google"), button:has-text("Sign in with Google")', ); // Handle Google OAuth flow in popup - const popupPromise = page.waitForEvent('popup'); + const popupPromise = page.waitForEvent("popup"); const popup = await popupPromise; // Fill Google credentials await popup.fill('input[type="email"]', process.env.E2E_GOOGLE_EMAIL!); - await popup.click('#identifierNext'); + await popup.click("#identifierNext"); - await popup.waitForSelector('input[type="password"]', { state: 'visible' }); - await popup.fill('input[type="password"]', process.env.E2E_GOOGLE_PASSWORD!); - await popup.click('#passwordNext'); + await popup.waitForSelector('input[type="password"]', { + state: "visible", + }); + await popup.fill( + 'input[type="password"]', + process.env.E2E_GOOGLE_PASSWORD!, + ); + await popup.click("#passwordNext"); // Wait for OAuth callback - await page.waitForURL('**/setup', { timeout: 30000 }); + await page.waitForURL("**/setup", { timeout: 30000 }); } else { // Option 2: Mock auth for faster tests (requires test endpoint) // This assumes you have a test-only endpoint that creates a session - await page.request.post('/api/auth/test-login', { + await page.request.post("/api/auth/test-login", { data: { - email: process.env.E2E_TEST_EMAIL || 'test@example.com', - name: process.env.E2E_TEST_NAME || 'Test User', + email: process.env.E2E_TEST_EMAIL || "test@example.com", + name: process.env.E2E_TEST_NAME || "Test User", }, }); diff --git a/apps/app/e2e/simple-auth.spec.ts b/apps/app/e2e/simple-auth.spec.ts index 6448ae689..a2a8095cb 100644 --- a/apps/app/e2e/simple-auth.spec.ts +++ b/apps/app/e2e/simple-auth.spec.ts @@ -1,53 +1,60 @@ -import { expect, test } from '@playwright/test'; -import { authenticateTestUser } from './utils/auth-helpers'; +import { expect, test } from "@playwright/test"; -test('simple auth flow', async ({ page, context, browserName }) => { +import { authenticateTestUser } from "./utils/auth-helpers"; + +test("simple auth flow", async ({ page, context, browserName }) => { const testEmail = `test-${Date.now()}-${Math.random().toString(36).substring(7)}-${browserName}@example.com`; // Authenticate user await authenticateTestUser(page, { email: testEmail, - name: 'Test User', + name: "Test User", skipOrg: false, hasAccess: true, }); // Verify session cookie was set const cookies = await context.cookies(); - const sessionCookie = cookies.find((c) => c.name === 'better-auth.session_token'); + const sessionCookie = cookies.find( + (c) => c.name === "better-auth.session_token", + ); expect(sessionCookie).toBeDefined(); // Navigate to root first to let the user settle into their authenticated state - await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.goto("/", { waitUntil: "domcontentloaded" }); await page.waitForTimeout(3000); // Wait for all redirects to complete const afterRootUrl = page.url(); - console.log('URL after navigating to root:', afterRootUrl); + console.log("URL after navigating to root:", afterRootUrl); // Now navigate to auth page - should be redirected since we're authenticated - await page.goto('/auth', { waitUntil: 'domcontentloaded' }); + await page.goto("/auth", { waitUntil: "domcontentloaded" }); // Wait for redirect away from auth - await page.waitForURL((url) => !url.toString().includes('/auth'), { timeout: 5000 }); + await page.waitForURL((url) => !url.toString().includes("/auth"), { + timeout: 5000, + }); // If we're on root, wait for the subsequent redirect to final destination - if (new URL(page.url()).pathname === '/') { - console.log('On root route, waiting for final redirect...'); - await page.waitForURL((url) => new URL(url).pathname !== '/', { timeout: 5000 }); + if (new URL(page.url()).pathname === "/") { + console.log("On root route, waiting for final redirect..."); + await page.waitForURL((url) => new URL(url).pathname !== "/", { + timeout: 5000, + }); } const currentUrl = page.url(); - console.log('Final URL after auth redirect:', currentUrl); + console.log("Final URL after auth redirect:", currentUrl); - expect(currentUrl).not.toContain('/auth'); + expect(currentUrl).not.toContain("/auth"); // User should be on one of these meaningful authenticated routes const isAuthenticatedRoute = - currentUrl.includes('/setup') || // Setup flow + currentUrl.includes("/setup") || // Setup flow currentUrl.match(/\/org_[a-zA-Z0-9]+\//) !== null || // Organization pages - currentUrl.includes('/upgrade') || // Upgrade page - currentUrl.includes('/no-access') || // No access page - currentUrl.includes('/onboarding'); // Onboarding flow + currentUrl.includes("/upgrade") || // Upgrade page + currentUrl.includes("/no-access") || // No access page + currentUrl.includes("/onboarding"); // Onboarding flow expect(isAuthenticatedRoute).toBeTruthy(); }); diff --git a/apps/app/e2e/tests/middleware-onboarding.spec.ts b/apps/app/e2e/tests/middleware-onboarding.spec.ts index c44504a87..336f3213c 100644 --- a/apps/app/e2e/tests/middleware-onboarding.spec.ts +++ b/apps/app/e2e/tests/middleware-onboarding.spec.ts @@ -1,13 +1,16 @@ -import { expect, test } from '@playwright/test'; -import { authenticateTestUser, clearAuth } from '../utils/auth-helpers'; -import { generateTestData } from '../utils/helpers'; +import { expect, test } from "@playwright/test"; -test.describe('Middleware Onboarding Behavior', () => { +import { authenticateTestUser, clearAuth } from "../utils/auth-helpers"; +import { generateTestData } from "../utils/helpers"; + +test.describe("Middleware Onboarding Behavior", () => { test.beforeEach(async ({ page }) => { await clearAuth(page); }); - test('user with null onboardingCompleted is NOT redirected to onboarding', async ({ page }) => { + test("user with null onboardingCompleted is NOT redirected to onboarding", async ({ + page, + }) => { const testData = generateTestData(); // Create user with org (onboardingCompleted will be null by default) @@ -18,26 +21,26 @@ test.describe('Middleware Onboarding Behavior', () => { }); // Try to access organization page - await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.goto("/", { waitUntil: "domcontentloaded" }); // Wait for all redirects to complete await page.waitForTimeout(3000); // Just wait for redirects to settle const currentUrl = page.url(); - console.log('Current URL after navigation:', currentUrl); + console.log("Current URL after navigation:", currentUrl); - expect(currentUrl).not.toContain('/onboarding'); + expect(currentUrl).not.toContain("/onboarding"); // Should be on an authenticated page - could be org page or setup (if activeOrgId not set) const isOnValidPage = currentUrl.match(/\/org_[a-zA-Z0-9]+\//) !== null || // Organization routes currentUrl.match(/\/setup\/[a-zA-Z0-9]+/) !== null || // Dynamic setup URLs - currentUrl.includes('/upgrade'); // Upgrade page + currentUrl.includes("/upgrade"); // Upgrade page expect(isOnValidPage).toBeTruthy(); }); - test('user without org is redirected to setup', async ({ page }) => { + test("user without org is redirected to setup", async ({ page }) => { const testData = generateTestData(); // Create user without org @@ -48,22 +51,22 @@ test.describe('Middleware Onboarding Behavior', () => { }); // Navigate to root - await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.goto("/", { waitUntil: "domcontentloaded" }); // Wait for redirects await page.waitForTimeout(2000); const currentUrl = page.url(); - console.log('User without org redirected to:', currentUrl); + console.log("User without org redirected to:", currentUrl); // Should be redirected to setup (with dynamic ID) expect(currentUrl).toMatch(/\/setup\/[a-zA-Z0-9]+/); }); - test('unauthenticated user is redirected to auth', async ({ page }) => { + test("unauthenticated user is redirected to auth", async ({ page }) => { // Try to access protected route without auth, expecting redirect - const response = await page.goto('/org_123/frameworks', { - waitUntil: 'commit', // Just wait for navigation to start, not complete + const response = await page.goto("/org_123/frameworks", { + waitUntil: "commit", // Just wait for navigation to start, not complete }); // Wait a bit for redirect @@ -71,9 +74,9 @@ test.describe('Middleware Onboarding Behavior', () => { // Check final URL const currentUrl = page.url(); - console.log('Unauthenticated user redirected to:', currentUrl); + console.log("Unauthenticated user redirected to:", currentUrl); // Should be redirected to auth - expect(currentUrl).toContain('/auth'); + expect(currentUrl).toContain("/auth"); }); }); diff --git a/apps/app/e2e/tests/onboarding.spec.ts b/apps/app/e2e/tests/onboarding.spec.ts index 0b16149b2..340ba7b5d 100644 --- a/apps/app/e2e/tests/onboarding.spec.ts +++ b/apps/app/e2e/tests/onboarding.spec.ts @@ -1,19 +1,20 @@ -import { expect, test } from '@playwright/test'; -import { authenticateTestUser, clearAuth } from '../utils/auth-helpers'; -import { fillFormField, generateTestData, waitForURL } from '../utils/helpers'; +import { expect, test } from "@playwright/test"; + +import { authenticateTestUser, clearAuth } from "../utils/auth-helpers"; +import { fillFormField, generateTestData, waitForURL } from "../utils/helpers"; // Increase test timeout for complex flows test.describe.configure({ timeout: 60000 }); // DEPRECATED: This test file is for the old full onboarding flow. // The new split onboarding flow is tested in split-onboarding.spec.ts -test.describe.skip('Onboarding Flow (DEPRECATED)', () => { +test.describe.skip("Onboarding Flow (DEPRECATED)", () => { test.beforeEach(async ({ page }) => { // Clear any existing auth state await clearAuth(page); }); - test('new user can complete full onboarding flow', async ({ page }) => { + test("new user can complete full onboarding flow", async ({ page }) => { const testData = generateTestData(); // 1. Authenticate user without organization @@ -24,17 +25,21 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { }); // 2. Navigate to root - should redirect to setup - await page.goto('/'); + await page.goto("/"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); // 3. Wait for frameworks to fully load await page.waitForSelector('input[type="checkbox"]', { timeout: 10000 }); // Wait for at least one framework to be checked (auto-selected) - await page.waitForSelector('input[type="checkbox"]:checked', { timeout: 10000 }); + await page.waitForSelector('input[type="checkbox"]:checked', { + timeout: 10000, + }); // 4. Verify we're on the setup page with frameworks - await expect(page.locator('text=/compliance frameworks/i').first()).toBeVisible({ + await expect( + page.locator("text=/compliance frameworks/i").first(), + ).toBeVisible({ timeout: 10000, }); @@ -57,7 +62,7 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { const websiteInput = page.locator('input[name="website"]'); await expect(websiteInput).toBeVisible({ timeout: 5000 }); // Don't include https:// as the input strips it - const websiteUrl = `${testData.organizationName.toLowerCase().replace(/\s+/g, '')}.com`; + const websiteUrl = `${testData.organizationName.toLowerCase().replace(/\s+/g, "")}.com`; await websiteInput.fill(websiteUrl); // 10. Click Next to go to description @@ -76,7 +81,7 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { // That's enough - we've verified the basic flow works }); - test('existing user can create additional organization', async ({ page }) => { + test("existing user can create additional organization", async ({ page }) => { const testData = generateTestData(); // For existing user test, we need to ensure they already have an organization @@ -86,25 +91,25 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { try { await authenticateTestUser(page, { email: setupEmail, - name: 'Existing User', + name: "Existing User", // Don't skip org - let them have their first organization }); } catch (error) { - console.log('Auth error, retrying with different email'); + console.log("Auth error, retrying with different email"); // If auth fails, try with a different email const retryEmail = `existing-${Date.now()}-retry@example.com`; await authenticateTestUser(page, { email: retryEmail, - name: 'Existing User', + name: "Existing User", }); } // Navigate to root first - await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.goto("/", { waitUntil: "domcontentloaded" }); // Navigate to setup with intent to create additional organization - await page.goto('/setup?intent=create-additional', { - waitUntil: 'domcontentloaded', + await page.goto("/setup?intent=create-additional", { + waitUntil: "domcontentloaded", timeout: 30000, }); @@ -112,27 +117,33 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 15000 }); // Wait for content to load - await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}); + await page + .waitForLoadState("networkidle", { timeout: 10000 }) + .catch(() => {}); // Framework step - wait and click Next const frameworksLoaded = await page .waitForSelector('input[type="checkbox"]', { timeout: 10000 }) .catch(() => null); if (!frameworksLoaded) { - console.log('Frameworks not loaded, skipping test'); + console.log("Frameworks not loaded, skipping test"); return; } await page.locator('button:has-text("Next"):visible').click(); // Organization name - await page.locator('input[name="organizationName"]').fill(testData.organizationName); + await page + .locator('input[name="organizationName"]') + .fill(testData.organizationName); await page.locator('button:has-text("Next"):visible').click(); // Website await page .locator('input[name="website"]') - .fill(`${testData.organizationName.toLowerCase().replace(/\s+/g, '')}.com`); + .fill( + `${testData.organizationName.toLowerCase().replace(/\s+/g, "")}.com`, + ); await page.locator('button:has-text("Next"):visible').click(); // Description @@ -150,7 +161,7 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { // Check if we've completed the flow const currentUrl = page.url(); - if (!currentUrl.includes('/setup/')) { + if (!currentUrl.includes("/setup/")) { break; } @@ -158,7 +169,9 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await page.waitForTimeout(500); // Try to fill any visible selects - const selectTrigger = await page.locator('[role="combobox"]:visible').first(); + const selectTrigger = await page + .locator('[role="combobox"]:visible') + .first(); if ((await selectTrigger.count()) > 0) { await selectTrigger.click(); await page.waitForTimeout(300); @@ -169,9 +182,13 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { } // Try to select pills - const pills = await page.locator('[role="button"][data-value]:visible').count(); + const pills = await page + .locator('[role="button"][data-value]:visible') + .count(); if (pills > 0 && pills < 20) { - const pillButtons = await page.locator('[role="button"][data-value]:visible').all(); + const pillButtons = await page + .locator('[role="button"][data-value]:visible') + .all(); for (let i = 0; i < Math.min(2, pillButtons.length); i++) { await pillButtons[i].click().catch(() => {}); } @@ -179,7 +196,9 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { // Try Next or Finish const finishBtn = page.locator('button:has-text("Finish"):visible'); - const nextBtn = page.locator('button:has-text("Next"):visible:not([disabled])'); + const nextBtn = page.locator( + 'button:has-text("Next"):visible:not([disabled])', + ); if ((await finishBtn.count()) > 0) { await finishBtn.click(); @@ -197,46 +216,48 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { // Check if we're no longer on setup const finalUrl = page.url(); - if (finalUrl.includes('/setup/')) { + if (finalUrl.includes("/setup/")) { // If still on setup, try navigating away - await page.goto('/'); + await page.goto("/"); await page.waitForTimeout(1000); } // Final verification - we should not be on setup page - expect(page.url()).not.toContain('/setup/'); + expect(page.url()).not.toContain("/setup/"); }); - test('user without org is redirected to setup from dashboard', async ({ page }) => { + test("user without org is redirected to setup from dashboard", async ({ + page, + }) => { const testData = generateTestData(); // Authenticate user without organization await authenticateTestUser(page, { email: testData.email, // Use unique email - name: 'No Org User', + name: "No Org User", skipOrg: true, // User should not have an organization }); // Navigate to root - await page.goto('/'); + await page.goto("/"); // Should be redirected to setup with session ID await waitForURL(page, /\/setup\/[a-zA-Z0-9]+/); // Verify we're on the setup page with frameworks - await expect(page.locator('text=/compliance frameworks/i')).toBeVisible(); + await expect(page.locator("text=/compliance frameworks/i")).toBeVisible(); }); - test('setup page validates required fields', async ({ page }) => { + test("setup page validates required fields", async ({ page }) => { const testData = generateTestData(); // Authenticate first await authenticateTestUser(page, { email: testData.email, // Use unique email - name: 'Validation Test', + name: "Validation Test", skipOrg: true, // User needs to go through setup }); - await page.goto('/setup'); + await page.goto("/setup"); // Wait for redirect to setup with session ID await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); @@ -245,7 +266,9 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await page.waitForSelector('input[type="checkbox"]', { timeout: 10000 }); // Wait for at least one framework to be checked (auto-selected) - await page.waitForSelector('input[type="checkbox"]:checked', { timeout: 10000 }); + await page.waitForSelector('input[type="checkbox"]:checked', { + timeout: 10000, + }); // Framework is already selected, Next button should be enabled const firstNextButton = page.locator('button:has-text("Next")'); @@ -255,14 +278,20 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await firstNextButton.click(); // Wait for organization name step - await page.waitForSelector('input[name="organizationName"]', { timeout: 10000 }); + await page.waitForSelector('input[name="organizationName"]', { + timeout: 10000, + }); // Next button should be disabled when organization name is empty const secondNextButton = page.locator('button:has-text("Next")'); await expect(secondNextButton).toBeDisabled(); // Fill organization name - await fillFormField(page, 'input[name="organizationName"]', 'Test Organization'); + await fillFormField( + page, + 'input[name="organizationName"]', + "Test Organization", + ); // Next button should now be enabled await expect(secondNextButton).toBeEnabled(); @@ -275,7 +304,7 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await expect(nextButton).toBeDisabled(); // Fill website with simple text (will become valid with .com added) - await fillFormField(page, 'input[name="website"]', 'testwebsite'); + await fillFormField(page, 'input[name="website"]', "testwebsite"); // Blur the field to trigger .com addition await page.locator('input[name="website"]').blur(); @@ -285,7 +314,7 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { // Clear and enter a proper domain await page.locator('input[name="website"]').clear(); - await fillFormField(page, 'input[name="website"]', 'testorganization.com'); + await fillFormField(page, 'input[name="website"]', "testorganization.com"); // Next button should be enabled await expect(page.locator('button:has-text("Next")')).toBeEnabled(); @@ -300,41 +329,41 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await fillFormField( page, 'textarea[name="describe"]', - 'Test Organization is a company that provides testing services.', + "Test Organization is a company that provides testing services.", ); // Next button should now be enabled await expect(page.locator('button:has-text("Next")')).toBeEnabled(); }); - test('handles organization creation errors gracefully', async ({ page }) => { + test("handles organization creation errors gracefully", async ({ page }) => { const testData = generateTestData(); // Authenticate first await authenticateTestUser(page, { email: testData.email, // Use unique email - name: 'Error Test', + name: "Error Test", skipOrg: true, // User needs to go through setup }); // Mock API to return error - await page.route('**/api/**', async (route) => { + await page.route("**/api/**", async (route) => { const url = route.request().url(); // Only intercept organization/setup creation endpoints if ( - url.includes('/api/organization/create') || - url.includes('/api/setup/complete') || - url.includes('/setup/') + url.includes("/api/organization/create") || + url.includes("/api/setup/complete") || + url.includes("/setup/") ) { // For POST to setup endpoints, return error - if (route.request().method() === 'POST') { + if (route.request().method() === "POST") { await route.fulfill({ status: 400, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify({ - error: 'Organization name already exists', - message: 'An organization with this name already exists', + error: "Organization name already exists", + message: "An organization with this name already exists", }), }); return; @@ -345,7 +374,7 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await route.continue(); }); - await page.goto('/setup'); + await page.goto("/setup"); // Wait for redirect to setup with session ID await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); @@ -354,24 +383,30 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { await page.locator('button:has-text("Next")').click(); // Wait for organization name step - await page.waitForSelector('input[name="organizationName"]', { timeout: 10000 }); + await page.waitForSelector('input[name="organizationName"]', { + timeout: 10000, + }); // Fill organization name - await fillFormField(page, 'input[name="organizationName"]', 'Existing Org'); + await fillFormField(page, 'input[name="organizationName"]', "Existing Org"); // Click Next to go to website await page.locator('button:has-text("Next")').click(); await page.waitForTimeout(500); // Fill website - await fillFormField(page, 'input[name="website"]', 'existingorg.com'); + await fillFormField(page, 'input[name="website"]', "existingorg.com"); // Click Next to go to description await page.locator('button:has-text("Next")').click(); await page.waitForTimeout(500); // Fill description - await fillFormField(page, 'textarea[name="describe"]', 'Existing Org is a test company.'); + await fillFormField( + page, + 'textarea[name="describe"]', + "Existing Org is a test company.", + ); // Try to continue - this should trigger the error await page.locator('button:has-text("Next")').click(); @@ -390,33 +425,33 @@ test.describe.skip('Onboarding Flow (DEPRECATED)', () => { }); }); -test.describe.skip('Setup Page Components (DEPRECATED)', () => { +test.describe.skip("Setup Page Components (DEPRECATED)", () => { test.beforeEach(async ({ page }) => { const testData = generateTestData(); // Authenticate before each test await authenticateTestUser(page, { email: testData.email, // Use unique email for each test - name: 'Component Test', + name: "Component Test", skipOrg: true, // Need to test setup page components }); }); - test('displays progress indicator during setup', async ({ page }) => { - await page.goto('/setup'); + test("displays progress indicator during setup", async ({ page }) => { + await page.goto("/setup"); // Wait for redirect to setup with session ID await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); // Check for step indicator - await expect(page.locator('text=/Step 1 of/i')).toBeVisible(); + await expect(page.locator("text=/Step 1 of/i")).toBeVisible(); // Check that we're on the setup page (framework step) - await expect(page.locator('text=/compliance frameworks/i')).toBeVisible(); + await expect(page.locator("text=/compliance frameworks/i")).toBeVisible(); }); - test('multi-step navigation works correctly', async ({ page }) => { - await page.goto('/setup'); + test("multi-step navigation works correctly", async ({ page }) => { + await page.goto("/setup"); // Wait for redirect to setup with session ID await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); @@ -426,48 +461,48 @@ test.describe.skip('Setup Page Components (DEPRECATED)', () => { // Step 2: Organization name await page.waitForTimeout(500); - await expect(page.locator('text=/Step 2 of/i')).toBeVisible(); + await expect(page.locator("text=/Step 2 of/i")).toBeVisible(); const orgNameInput = page.locator('input[name="organizationName"]'); await expect(orgNameInput).toBeVisible(); // Check back button works await page.locator('button:has-text("Back")').click(); await page.waitForTimeout(500); - await expect(page.locator('text=/Step 1 of/i')).toBeVisible(); + await expect(page.locator("text=/Step 1 of/i")).toBeVisible(); }); - test('responsive design works on mobile', async ({ page }) => { + test("responsive design works on mobile", async ({ page }) => { // Set mobile viewport await page.setViewportSize({ width: 375, height: 667 }); - await page.goto('/setup'); + await page.goto("/setup"); // Wait for redirect to setup with session ID await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); // Check that framework selection is accessible on mobile - await expect(page.locator('text=/compliance frameworks/i')).toBeVisible(); + await expect(page.locator("text=/compliance frameworks/i")).toBeVisible(); const submitButton = page.locator('button[type="submit"]'); await expect(submitButton).toBeVisible(); // Form should be vertically stacked on mobile - const form = page.locator('form').first(); + const form = page.locator("form").first(); const formWidth = await form.boundingBox().then((box) => box?.width); expect(formWidth).toBeLessThan(400); }); - test('framework selection enables/disables Next button', async ({ page }) => { + test("framework selection enables/disables Next button", async ({ page }) => { const testData = generateTestData(); // Authenticate first await authenticateTestUser(page, { email: testData.email, - name: 'Framework Test', + name: "Framework Test", skipOrg: true, }); - await page.goto('/setup'); + await page.goto("/setup"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 }); // Wait for frameworks to load @@ -481,11 +516,15 @@ test.describe.skip('Setup Page Components (DEPRECATED)', () => { // So we need to test the behavior differently // First, verify at least one framework is selected - const initialCheckedCount = await page.locator('input[type="checkbox"]:checked').count(); + const initialCheckedCount = await page + .locator('input[type="checkbox"]:checked') + .count(); expect(initialCheckedCount).toBeGreaterThan(0); // If multiple frameworks are available, try selecting additional ones - const allFrameworkLabels = await page.locator('label[for^="framework-"]').all(); + const allFrameworkLabels = await page + .locator('label[for^="framework-"]') + .all(); if (allFrameworkLabels.length > 1) { // Click on a non-selected framework const uncheckedFramework = await page @@ -498,7 +537,9 @@ test.describe.skip('Setup Page Components (DEPRECATED)', () => { await page.waitForTimeout(200); // Verify we now have more selected - const newCheckedCount = await page.locator('input[type="checkbox"]:checked').count(); + const newCheckedCount = await page + .locator('input[type="checkbox"]:checked') + .count(); expect(newCheckedCount).toBeGreaterThan(initialCheckedCount); } } diff --git a/apps/app/e2e/tests/split-onboarding.spec.ts b/apps/app/e2e/tests/split-onboarding.spec.ts index eda5130c6..e09b25cf6 100644 --- a/apps/app/e2e/tests/split-onboarding.spec.ts +++ b/apps/app/e2e/tests/split-onboarding.spec.ts @@ -1,15 +1,20 @@ -import { expect, test } from '@playwright/test'; -import { authenticateTestUser, clearAuth, grantAccess } from '../utils/auth-helpers'; -import { generateTestData } from '../utils/helpers'; +import { expect, test } from "@playwright/test"; -test.describe('Split Onboarding Flow', () => { +import { + authenticateTestUser, + clearAuth, + grantAccess, +} from "../utils/auth-helpers"; +import { generateTestData } from "../utils/helpers"; + +test.describe("Split Onboarding Flow", () => { test.setTimeout(60000); test.beforeEach(async ({ page }) => { await clearAuth(page); }); - test('new user without access: 3 steps → book call', async ({ page }) => { + test("new user without access: 3 steps → book call", async ({ page }) => { const testData = generateTestData(); const website = `example${Date.now()}.com`; @@ -22,44 +27,54 @@ test.describe('Split Onboarding Flow', () => { }); // Go to setup - await page.goto('/setup'); + await page.goto("/setup"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/); // Step 1: Framework selection - wait for it to be auto-selected - await expect(page.getByText('Step 1 of 3')).toBeVisible(); - await expect(page.getByText('Which compliance frameworks do you need?')).toBeVisible(); + await expect(page.getByText("Step 1 of 3")).toBeVisible(); + await expect( + page.getByText("Which compliance frameworks do you need?"), + ).toBeVisible(); // Wait for frameworks to load and one to be auto-selected await page.waitForSelector('input[type="checkbox"]:checked'); // Click Next - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); // Step 2: Organization name - await expect(page.getByText('Step 2 of 3')).toBeVisible(); - await expect(page.getByText('What is your company name?')).toBeVisible(); + await expect(page.getByText("Step 2 of 3")).toBeVisible(); + await expect(page.getByText("What is your company name?")).toBeVisible(); - await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName); - await page.getByTestId('setup-next-button').click(); + await page + .getByPlaceholder("e.g., Acme Inc.") + .fill(testData.organizationName); + await page.getByTestId("setup-next-button").click(); // Step 3: Website - await expect(page.getByText('Step 3 of 3')).toBeVisible(); + await expect(page.getByText("Step 3 of 3")).toBeVisible(); await expect(page.getByText("What's your company website?")).toBeVisible(); - await page.getByPlaceholder('example.com').fill(website); + await page.getByPlaceholder("example.com").fill(website); // Click Finish and wait for redirect await Promise.all([ page.waitForURL(/\/upgrade\/org_/), - page.getByTestId('setup-finish-button').click(), + page.getByTestId("setup-finish-button").click(), ]); // Should see book a call page - await expect(page.getByText(`Let's get ${testData.organizationName} approved`)).toBeVisible(); - await expect(page.getByText('A quick 20-minute call with our team')).toBeVisible(); + await expect( + page.getByText(`Let's get ${testData.organizationName} approved`), + ).toBeVisible(); + await expect( + page.getByText("A quick 20-minute call with our team"), + ).toBeVisible(); }); - test('user creates org → book call → gets access → onboarding', async ({ page }) => { + test("user creates org → book call → gets access → onboarding", async ({ + page, + }) => { const testData = generateTestData(); const website = `example${Date.now()}.com`; @@ -72,32 +87,38 @@ test.describe('Split Onboarding Flow', () => { }); // Go to setup - await page.goto('/setup'); + await page.goto("/setup"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/); // Step 1: Framework selection - await expect(page.getByText('Step 1 of 3')).toBeVisible(); + await expect(page.getByText("Step 1 of 3")).toBeVisible(); await page.waitForSelector('input[type="checkbox"]:checked'); - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); // Step 2: Organization name - await expect(page.getByText('Step 2 of 3')).toBeVisible(); - await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName); - await page.getByTestId('setup-next-button').click(); + await expect(page.getByText("Step 2 of 3")).toBeVisible(); + await page + .getByPlaceholder("e.g., Acme Inc.") + .fill(testData.organizationName); + await page.getByTestId("setup-next-button").click(); // Step 3: Website - await expect(page.getByText('Step 3 of 3')).toBeVisible(); - await page.getByPlaceholder('example.com').fill(website); + await expect(page.getByText("Step 3 of 3")).toBeVisible(); + await page.getByPlaceholder("example.com").fill(website); // Click Finish and wait for redirect - NEW ORGS ALWAYS GO TO BOOK CALL FIRST await Promise.all([ page.waitForURL(/\/upgrade\/org_/), - page.getByTestId('setup-finish-button').click(), + page.getByTestId("setup-finish-button").click(), ]); // Even users with access get redirected to book call for NEW organizations - await expect(page.getByText(`Let's get ${testData.organizationName} approved`)).toBeVisible(); - await expect(page.getByText('A quick 20-minute call with our team')).toBeVisible(); + await expect( + page.getByText(`Let's get ${testData.organizationName} approved`), + ).toBeVisible(); + await expect( + page.getByText("A quick 20-minute call with our team"), + ).toBeVisible(); // Extract orgId to grant access and test the onboarding flow const orgId = page.url().match(/org_[a-zA-Z0-9]+/)?.[0]; @@ -109,11 +130,15 @@ test.describe('Split Onboarding Flow', () => { // Should now redirect to onboarding await expect(page).toHaveURL(`/onboarding/${orgId!}`); - await expect(page.getByText('Step 1 of 9')).toBeVisible(); - await expect(page.getByText('Describe your company in a few sentences')).toBeVisible(); + await expect(page.getByText("Step 1 of 9")).toBeVisible(); + await expect( + page.getByText("Describe your company in a few sentences"), + ).toBeVisible(); }); - test('user with approved org: redirected from product to onboarding', async ({ page }) => { + test("user with approved org: redirected from product to onboarding", async ({ + page, + }) => { const testData = generateTestData(); const website = `example${Date.now()}.com`; @@ -126,7 +151,7 @@ test.describe('Split Onboarding Flow', () => { }); // Complete setup to create org - await page.goto('/setup'); + await page.goto("/setup"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/); // Go through 3 steps quickly @@ -142,9 +167,11 @@ test.describe('Split Onboarding Flow', () => { { timeout: 10000 }, ); await page.waitForTimeout(500); // Brief pause for stability - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); - await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName); + await page + .getByPlaceholder("e.g., Acme Inc.") + .fill(testData.organizationName); // Wait for the button to be enabled after filling the organization name await page.waitForFunction( () => { @@ -156,12 +183,12 @@ test.describe('Split Onboarding Flow', () => { { timeout: 5000 }, ); await page.waitForTimeout(300); // Brief pause for stability - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); - await page.getByPlaceholder('example.com').fill(website); + await page.getByPlaceholder("example.com").fill(website); await Promise.all([ page.waitForURL(/\/upgrade\/org_/), - page.getByTestId('setup-finish-button').click(), + page.getByTestId("setup-finish-button").click(), ]); // Extract orgId from current URL @@ -183,7 +210,7 @@ test.describe('Split Onboarding Flow', () => { await expect(page).toHaveURL(/\/onboarding\/org_/); }); - test('completed user redirected from setup to app', async ({ page }) => { + test("completed user redirected from setup to app", async ({ page }) => { const testData = generateTestData(); const website = `example${Date.now()}.com`; @@ -196,7 +223,7 @@ test.describe('Split Onboarding Flow', () => { }); // Complete setup - await page.goto('/setup'); + await page.goto("/setup"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/); await page.waitForSelector('input[type="checkbox"]:checked'); @@ -211,9 +238,11 @@ test.describe('Split Onboarding Flow', () => { { timeout: 10000 }, ); await page.waitForTimeout(500); // Brief pause for stability - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); - await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName); + await page + .getByPlaceholder("e.g., Acme Inc.") + .fill(testData.organizationName); // Wait for the button to be enabled after filling the organization name await page.waitForFunction( () => { @@ -225,12 +254,12 @@ test.describe('Split Onboarding Flow', () => { { timeout: 5000 }, ); await page.waitForTimeout(300); // Brief pause for stability - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); - await page.getByPlaceholder('example.com').fill(website); + await page.getByPlaceholder("example.com").fill(website); await Promise.all([ page.waitForURL(/\/upgrade\/org_/), - page.getByTestId('setup-finish-button').click(), + page.getByTestId("setup-finish-button").click(), ]); const orgId = page.url().match(/org_[a-zA-Z0-9]+/)?.[0]; @@ -247,92 +276,114 @@ test.describe('Split Onboarding Flow', () => { // Step 1: Describe company await page - .getByTestId('onboarding-input-describe') - .fill('We are a test company that provides compliance solutions.'); - await page.getByTestId('onboarding-next-button').click(); + .getByTestId("onboarding-input-describe") + .fill("We are a test company that provides compliance solutions."); + await page.getByTestId("onboarding-next-button").click(); // Step 2: Industry (dropdown) - await page.getByTestId('onboarding-input-industry').click(); - await page.waitForSelector('[data-testid="onboarding-option-saas"]', { state: 'visible' }); - await page.getByTestId('onboarding-option-saas').click(); - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-input-industry").click(); + await page.waitForSelector('[data-testid="onboarding-option-saas"]', { + state: "visible", + }); + await page.getByTestId("onboarding-option-saas").click(); + await page.getByTestId("onboarding-next-button").click(); // Step 3: Team size (dropdown) - await page.getByTestId('onboarding-input-teamSize').click(); - await page.waitForSelector('[data-testid="onboarding-option-11-50"]', { state: 'visible' }); - await page.getByTestId('onboarding-option-11-50').click(); - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-input-teamSize").click(); + await page.waitForSelector('[data-testid="onboarding-option-11-50"]', { + state: "visible", + }); + await page.getByTestId("onboarding-option-11-50").click(); + await page.getByTestId("onboarding-next-button").click(); // Step 4: Devices (multi-select) - await page.getByTestId('onboarding-input-devices-search').click(); + await page.getByTestId("onboarding-input-devices-search").click(); await page.waitForSelector( '[data-testid="onboarding-input-devices-search-option-company-provided-laptops"]', - { state: 'visible' }, + { state: "visible" }, ); await page - .getByTestId('onboarding-input-devices-search-option-company-provided-laptops') + .getByTestId( + "onboarding-input-devices-search-option-company-provided-laptops", + ) .click(); - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-next-button").click(); // Step 5: Authentication (multi-select) - await page.getByTestId('onboarding-input-authentication-search').click(); + await page.getByTestId("onboarding-input-authentication-search").click(); await page.waitForSelector( '[data-testid="onboarding-input-authentication-search-option-google-workspace"]', - { state: 'visible' }, + { state: "visible" }, ); await page - .getByTestId('onboarding-input-authentication-search-option-google-workspace') + .getByTestId( + "onboarding-input-authentication-search-option-google-workspace", + ) .click(); - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-next-button").click(); // Step 6: Software (multi-select) - await page.getByTestId('onboarding-input-software-search').click(); - await page.waitForSelector('[data-testid="onboarding-input-software-search-option-github"]', { - state: 'visible', - }); - await page.getByTestId('onboarding-input-software-search-option-github').click(); - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-input-software-search").click(); + await page.waitForSelector( + '[data-testid="onboarding-input-software-search-option-github"]', + { + state: "visible", + }, + ); + await page + .getByTestId("onboarding-input-software-search-option-github") + .click(); + await page.getByTestId("onboarding-next-button").click(); // Step 7: Work location (dropdown) - await page.getByTestId('onboarding-input-workLocation').click(); - await page.waitForSelector('[data-testid="onboarding-option-fully-remote"]', { - state: 'visible', - }); - await page.getByTestId('onboarding-option-fully-remote').click(); - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-input-workLocation").click(); + await page.waitForSelector( + '[data-testid="onboarding-option-fully-remote"]', + { + state: "visible", + }, + ); + await page.getByTestId("onboarding-option-fully-remote").click(); + await page.getByTestId("onboarding-next-button").click(); // Step 8: Infrastructure (multi-select) - await page.getByTestId('onboarding-input-infrastructure-search').click(); + await page.getByTestId("onboarding-input-infrastructure-search").click(); await page.waitForSelector( '[data-testid="onboarding-input-infrastructure-search-option-aws"]', - { state: 'visible' }, + { state: "visible" }, ); - await page.getByTestId('onboarding-input-infrastructure-search-option-aws').click(); - await page.getByTestId('onboarding-next-button').click(); + await page + .getByTestId("onboarding-input-infrastructure-search-option-aws") + .click(); + await page.getByTestId("onboarding-next-button").click(); // Step 9: Data types (multi-select) - Final step - await page.getByTestId('onboarding-input-dataTypes-search').click(); + await page.getByTestId("onboarding-input-dataTypes-search").click(); await page.waitForSelector( '[data-testid="onboarding-input-dataTypes-search-option-customer-pii"]', - { state: 'visible' }, + { state: "visible" }, ); - await page.getByTestId('onboarding-input-dataTypes-search-option-customer-pii').click(); + await page + .getByTestId("onboarding-input-dataTypes-search-option-customer-pii") + .click(); // Final step - Complete Setup button - await page.getByTestId('onboarding-next-button').click(); + await page.getByTestId("onboarding-next-button").click(); // Wait for the redirect chain: /onboarding/org_xxx → /org_xxx/ → /org_xxx/frameworks await page.waitForURL(`/${orgId!}/frameworks`, { timeout: 10000 }); // Now test redirects for completed user - await page.goto('/setup'); + await page.goto("/setup"); await expect(page).toHaveURL(`/${orgId!}/frameworks`); await page.goto(`/upgrade/${orgId!}`); await expect(page).toHaveURL(`/${orgId!}/frameworks`); }); - test('user without access gets blocked then granted access', async ({ page }) => { + test("user without access gets blocked then granted access", async ({ + page, + }) => { const testData = generateTestData(); const website = `example${Date.now()}.com`; @@ -345,7 +396,7 @@ test.describe('Split Onboarding Flow', () => { }); // Complete setup to get to book call page - await page.goto('/setup'); + await page.goto("/setup"); await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/); await page.waitForSelector('input[type="checkbox"]:checked'); @@ -360,9 +411,11 @@ test.describe('Split Onboarding Flow', () => { { timeout: 10000 }, ); await page.waitForTimeout(500); // Brief pause for stability - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); - await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName); + await page + .getByPlaceholder("e.g., Acme Inc.") + .fill(testData.organizationName); // Wait for the button to be enabled after filling the organization name await page.waitForFunction( () => { @@ -374,19 +427,21 @@ test.describe('Split Onboarding Flow', () => { { timeout: 5000 }, ); await page.waitForTimeout(300); // Brief pause for stability - await page.getByTestId('setup-next-button').click(); + await page.getByTestId("setup-next-button").click(); - await page.getByPlaceholder('example.com').fill(website); + await page.getByPlaceholder("example.com").fill(website); await Promise.all([ page.waitForURL(/\/upgrade\/org_/), - page.getByTestId('setup-finish-button').click(), + page.getByTestId("setup-finish-button").click(), ]); const orgId = page.url().match(/org_[a-zA-Z0-9]+/)?.[0]; expect(orgId).toBeTruthy(); // Verify blocked - await expect(page.getByText(`Let's get ${testData.organizationName} approved`)).toBeVisible(); + await expect( + page.getByText(`Let's get ${testData.organizationName} approved`), + ).toBeVisible(); // Try accessing onboarding - should be blocked await page.goto(`/onboarding/${orgId!}`); @@ -398,6 +453,6 @@ test.describe('Split Onboarding Flow', () => { // Should now redirect to onboarding await expect(page).toHaveURL(`/onboarding/${orgId!}`); - await expect(page.getByText('Step 1 of 9')).toBeVisible(); + await expect(page.getByText("Step 1 of 9")).toBeVisible(); }); }); diff --git a/apps/app/e2e/utils/auth-helpers.ts b/apps/app/e2e/utils/auth-helpers.ts index f9e0a94c0..60260ad19 100644 --- a/apps/app/e2e/utils/auth-helpers.ts +++ b/apps/app/e2e/utils/auth-helpers.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { type Page } from "@playwright/test"; interface AuthOptions { email?: string; @@ -11,22 +11,32 @@ interface AuthOptions { * Authenticate a test user by calling the test-login endpoint * This properly handles cookies in the browser context */ -export async function authenticateTestUser(page: Page, options: AuthOptions = {}): Promise { - console.log('Attempting to authenticate test user:', options.email || 'auto-generated'); +export async function authenticateTestUser( + page: Page, + options: AuthOptions = {}, +): Promise { + console.log( + "Attempting to authenticate test user:", + options.email || "auto-generated", + ); let lastError: Error | null = null; const maxRetries = 3; for (let attempt = 0; attempt < maxRetries; attempt++) { try { - const response = await page.context().request.post('/api/auth/test-login', { - data: options, - timeout: 30000, // Increase timeout to 30 seconds - }); + const response = await page + .context() + .request.post("/api/auth/test-login", { + data: options, + timeout: 30000, // Increase timeout to 30 seconds + }); if (!response.ok()) { const errorBody = await response.text(); - throw new Error(`Authentication failed: ${response.status()} - ${errorBody}`); + throw new Error( + `Authentication failed: ${response.status()} - ${errorBody}`, + ); } // Wait a bit for cookies to be set @@ -34,7 +44,7 @@ export async function authenticateTestUser(page: Page, options: AuthOptions = {} // Verify authentication by checking we're no longer on /auth const currentUrl = page.url(); - if (currentUrl.includes('/auth') && !currentUrl.includes('/api/auth')) { + if (currentUrl.includes("/auth") && !currentUrl.includes("/api/auth")) { // We're still on auth page, wait a bit more await page.waitForTimeout(2000); } @@ -42,11 +52,15 @@ export async function authenticateTestUser(page: Page, options: AuthOptions = {} return; // Success, exit the function } catch (error: any) { lastError = error; - console.error(`Authentication attempt ${attempt + 1} failed:`, error.message); + console.error( + `Authentication attempt ${attempt + 1} failed:`, + error.message, + ); // If it's a connection error and not the last attempt, retry if ( - (error.message.includes('ECONNRESET') || error.message.includes('ECONNREFUSED')) && + (error.message.includes("ECONNRESET") || + error.message.includes("ECONNREFUSED")) && attempt < maxRetries - 1 ) { console.log(`Retrying authentication in 2 seconds...`); @@ -60,7 +74,7 @@ export async function authenticateTestUser(page: Page, options: AuthOptions = {} } // If we get here, all retries failed - throw lastError || new Error('Authentication failed after all retries'); + throw lastError || new Error("Authentication failed after all retries"); } /** @@ -79,18 +93,24 @@ export async function grantAccess( orgId: string, hasAccess: boolean = true, ): Promise { - console.log(`Granting access to organization: ${orgId}, hasAccess: ${hasAccess}`); + console.log( + `Granting access to organization: ${orgId}, hasAccess: ${hasAccess}`, + ); - const response = await page.context().request.post('/api/auth/test-grant-access', { - data: { orgId, hasAccess }, - timeout: 10000, - }); + const response = await page + .context() + .request.post("/api/auth/test-grant-access", { + data: { orgId, hasAccess }, + timeout: 10000, + }); if (!response.ok()) { const errorBody = await response.text(); - throw new Error(`Failed to grant access: ${response.status()} - ${errorBody}`); + throw new Error( + `Failed to grant access: ${response.status()} - ${errorBody}`, + ); } const result = await response.json(); - console.log('Access granted successfully:', result); + console.log("Access granted successfully:", result); } diff --git a/apps/app/e2e/utils/helpers.ts b/apps/app/e2e/utils/helpers.ts index 29bfd9a0e..30b194b49 100644 --- a/apps/app/e2e/utils/helpers.ts +++ b/apps/app/e2e/utils/helpers.ts @@ -1,4 +1,5 @@ -import { type Page, expect } from '@playwright/test'; +import type { Page } from "@playwright/test"; +import { expect } from "@playwright/test"; /** * Helper functions for E2E tests @@ -10,17 +11,21 @@ export async function waitForURL(page: Page, urlPattern: string | RegExp) { } // Fill a form field with retry logic -export async function fillFormField(page: Page, selector: string, value: string) { +export async function fillFormField( + page: Page, + selector: string, + value: string, +) { const field = page.locator(selector); await expect(field).toBeVisible({ timeout: 10000 }); await field.clear(); await field.fill(value); // Special handling for website inputs that strip protocols - if (selector.includes('website')) { + if (selector.includes("website")) { // The WebsiteInput component strips https:// from display // so we just verify the field has some value - await expect(field).not.toHaveValue(''); + await expect(field).not.toHaveValue(""); } else { await expect(field).toHaveValue(value); } @@ -33,7 +38,7 @@ export async function clickAndWait( options?: { waitForNavigation?: boolean }, ) { const element = page.locator(selector); - await element.waitFor({ state: 'visible' }); + await element.waitFor({ state: "visible" }); if (options?.waitForNavigation) { await Promise.all([page.waitForNavigation(), element.click()]); @@ -44,7 +49,7 @@ export async function clickAndWait( // Wait for network idle (useful after data mutations) export async function waitForNetworkIdle(page: Page) { - await page.waitForLoadState('networkidle'); + await page.waitForLoadState("networkidle"); } // Generate unique test data @@ -59,7 +64,10 @@ export function generateTestData() { } // Check if element exists -export async function elementExists(page: Page, selector: string): Promise { +export async function elementExists( + page: Page, + selector: string, +): Promise { return (await page.locator(selector).count()) > 0; } @@ -81,11 +89,15 @@ export async function expectToast(page: Page, expectedText: string) { } // Mock API responses -export async function mockAPIResponse(page: Page, url: string | RegExp, response: any) { +export async function mockAPIResponse( + page: Page, + url: string | RegExp, + response: any, +) { await page.route(url, (route) => { route.fulfill({ status: 200, - contentType: 'application/json', + contentType: "application/json", body: JSON.stringify(response), }); }); diff --git a/apps/app/eslint.config.ts b/apps/app/eslint.config.ts new file mode 100644 index 000000000..c7643f235 --- /dev/null +++ b/apps/app/eslint.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "eslint/config"; + +import { baseConfig } from "@trycompai/eslint-config/base"; +import { nextjsConfig } from "@trycompai/eslint-config/nextjs"; +import { reactConfig } from "@trycompai/eslint-config/react"; + +export default defineConfig( + { + ignores: [".next/**"], + }, + baseConfig, + reactConfig, + nextjsConfig +); diff --git a/apps/app/instrumentation-client.ts b/apps/app/instrumentation-client.ts index e945c0bf3..affd41e78 100644 --- a/apps/app/instrumentation-client.ts +++ b/apps/app/instrumentation-client.ts @@ -1,15 +1,15 @@ -import { initBotId } from 'botid/client/core'; +import { initBotId } from "botid/client/core"; initBotId({ protect: [ - { path: '/api/chat', method: 'POST' }, + { path: "/api/chat", method: "POST" }, { path: `${process.env.NEXT_PUBLIC_ENTERPRISE_API_URL}/api/tasks-automations/chat`, - method: 'POST', + method: "POST", }, { path: `${process.env.NEXT_PUBLIC_ENTERPRISE_API_URL}/api/tasks-automations/errors`, - method: 'POST', + method: "POST", }, ], }); diff --git a/apps/app/markdown.d.ts b/apps/app/markdown.d.ts index 43d00fea7..c94d67b1a 100644 --- a/apps/app/markdown.d.ts +++ b/apps/app/markdown.d.ts @@ -1,4 +1,4 @@ -declare module '*.md' { - const content: string - export default content +declare module "*.md" { + const content: string; + export default content; } diff --git a/apps/app/next.config.ts b/apps/app/next.config.ts index affee2e36..c73402058 100644 --- a/apps/app/next.config.ts +++ b/apps/app/next.config.ts @@ -1,71 +1,66 @@ -import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin'; -import { withBotId } from 'botid/next/config'; -import type { NextConfig } from 'next'; -import path from 'path'; +import type { NextConfig } from "next"; +import path from "path"; -import './src/env.mjs'; +import { withBotId } from "botid/next/config"; -const isStandalone = process.env.NEXT_OUTPUT_STANDALONE === 'true'; +import "./src/env.mjs"; + +const isStandalone = process.env.NEXT_OUTPUT_STANDALONE === "true"; const config: NextConfig = { // Ensure Turbopack can import .md files as raw strings during dev turbopack: { - root: path.join(__dirname, '..', '..'), + root: path.join(__dirname, "..", ".."), rules: { - '*.md': { - loaders: ['raw-loader'], - as: '*.js', + "*.md": { + loaders: ["raw-loader"], + as: "*.js", }, }, }, - webpack: (config, { isServer }) => { - if (isServer) { - // Very important, DO NOT REMOVE, it's needed for Prisma to work in the server bundle - config.plugins = [...config.plugins, new PrismaPlugin()]; - } - + webpack: (config) => { // Enable importing .md files as raw strings during webpack builds config.module = config.module || { rules: [] }; config.module.rules = config.module.rules || []; config.module.rules.push({ test: /\.md$/, - type: 'asset/source', + type: "asset/source", }); return config; }, // Use S3 bucket for static assets with app-specific path assetPrefix: - process.env.NODE_ENV === 'production' && process.env.STATIC_ASSETS_URL + process.env.NODE_ENV === "production" && process.env.STATIC_ASSETS_URL ? `${process.env.STATIC_ASSETS_URL}/app` - : '', + : "", reactStrictMode: false, - transpilePackages: ['@trycompai/db', '@prisma/client'], + transpilePackages: ["@trycompai/ui", "@trycompai/email"], images: { remotePatterns: [ { - protocol: 'https', - hostname: '**', + protocol: "https", + hostname: "**", }, ], }, experimental: { serverActions: { - bodySizeLimit: '15mb', + bodySizeLimit: "15mb", allowedOrigins: - process.env.NODE_ENV === 'production' - ? ([process.env.NEXT_PUBLIC_PORTAL_URL, 'https://app.trycomp.ai'].filter( - Boolean, - ) as string[]) + process.env.NODE_ENV === "production" + ? ([ + process.env.NEXT_PUBLIC_PORTAL_URL, + "https://app.trycomp.ai", + ].filter(Boolean) as string[]) : undefined, }, authInterrupts: true, - optimizePackageImports: ['@trycompai/db', '@trycompai/ui'], // Reduce build peak memory webpackMemoryOptimizations: true, }, - outputFileTracingRoot: path.join(__dirname, '../../'), + outputFileTracingRoot: path.join(__dirname, "../../"), // Reduce memory usage during production build productionBrowserSourceMaps: false, @@ -73,7 +68,7 @@ const config: NextConfig = { // swcMinify: false, ...(isStandalone ? { - output: 'standalone' as const, + output: "standalone" as const, } : {}), @@ -81,16 +76,16 @@ const config: NextConfig = { async rewrites() { return [ { - source: '/ingest/static/:path*', - destination: 'https://us-assets.i.posthog.com/static/:path*', + source: "/ingest/static/:path*", + destination: "https://us-assets.i.posthog.com/static/:path*", }, { - source: '/ingest/:path*', - destination: 'https://us.i.posthog.com/:path*', + source: "/ingest/:path*", + destination: "https://us.i.posthog.com/:path*", }, { - source: '/ingest/decide', - destination: 'https://us.i.posthog.com/decide', + source: "/ingest/decide", + destination: "https://us.i.posthog.com/decide", }, ]; }, diff --git a/apps/app/package.json b/apps/app/package.json index 6de9953a8..b1671bfcd 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -1,182 +1,174 @@ { "name": "@comp/app", "version": "0.1.0", + "private": true, + "scripts": { + "analyze-locale-usage": "pnpx tsx src/locales/analyze-locale-usage.ts", + "build": "next build --webpack", + "build:docker": "next build", + "clean": "rm -rf .next .turbo .trigger node_modules", + "deploy:trigger-prod": "pnpx trigger.dev@4.0.6 deploy", + "dev": "pnpx concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"next dev --webpack -p 3000\" \"pnpx trigger.dev@4.0.6 dev\"", + "lint": "eslint .", + "start": "next start", + "test": "vitest", + "test:all": "./scripts/test-all.sh", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test", + "test:e2e:debug": "playwright test --debug", + "test:e2e:headed": "playwright test --headed", + "test:e2e:install": "playwright install --with-deps", + "test:e2e:report": "playwright show-report", + "test:e2e:setup": "./scripts/setup-e2e.sh", + "test:e2e:ui": "playwright test --ui", + "test:ui": "vitest --ui", + "test:watch": "vitest --watch", + "typecheck": "tsc --noEmit" + }, "dependencies": { - "@ai-sdk/anthropic": "^2.0.0", - "@ai-sdk/groq": "^2.0.0", - "@ai-sdk/openai": "^2.0.65", + "@ai-sdk/anthropic": "^2.0.45", + "@ai-sdk/gateway": "^2.0.12", + "@ai-sdk/groq": "^2.0.29", + "@ai-sdk/openai": "^2.0.69", "@ai-sdk/provider": "^2.0.0", - "@ai-sdk/react": "^2.0.60", - "@ai-sdk/rsc": "^1.0.0", - "@aws-sdk/client-ec2": "^3.911.0", - "@aws-sdk/client-lambda": "^3.891.0", - "@aws-sdk/client-s3": "^3.859.0", - "@aws-sdk/client-sts": "^3.808.0", - "@aws-sdk/s3-request-presigner": "^3.859.0", - "@azure/core-rest-pipeline": "^1.21.0", - "@browserbasehq/sdk": "^2.5.0", - "@calcom/atoms": "^1.0.102-framer", + "@ai-sdk/react": "^2.0.97", + "@ai-sdk/rsc": "^1.0.98", + "@aws-sdk/client-ec2": "^3.936.0", + "@aws-sdk/client-lambda": "^3.936.0", + "@aws-sdk/client-s3": "^3.936.0", + "@aws-sdk/client-sts": "^3.936.0", + "@aws-sdk/s3-request-presigner": "^3.936.0", + "@azure/core-rest-pipeline": "^1.22.2", + "@browserbasehq/sdk": "^2.6.0", + "@calcom/atoms": "^1.12.1", "@calcom/embed-react": "^1.5.3", - "@date-fns/tz": "^1.2.0", + "@date-fns/tz": "^1.4.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", - "@dub/analytics": "^0.0.27", + "@dub/analytics": "^0.0.32", "@dub/better-auth": "^0.0.6", "@dub/embed-react": "^0.0.16", - "@hookform/resolvers": "^5.1.1", - "@mendable/firecrawl-js": "^1.24.0", + "@hookform/resolvers": "^5.2.2", + "@mendable/firecrawl-js": "^4.6.1", "@monaco-editor/react": "^4.7.0", - "@nangohq/frontend": "^0.53.2", - "@next/third-parties": "^15.3.1", - "@novu/api": "^1.6.0", - "@novu/nextjs": "^3.10.1", - "@number-flow/react": "^0.5.9", - "@prisma/client": "^6.13.0", - "@prisma/instrumentation": "6.6.0", - "@prisma/nextjs-monorepo-workaround-plugin": "^6.13.0", - "@radix-ui/react-slot": "^1.2.3", - "@react-email/components": "^0.0.41", - "@react-email/render": "^1.1.2", - "@react-three/drei": "^10.3.0", - "@react-three/fiber": "^9.1.2", + "@nangohq/frontend": "^0.69.13", + "@next/third-parties": "^16.0.3", + "@novu/api": "^3.11.0", + "@novu/nextjs": "^3.11.0", + "@number-flow/react": "^0.5.10", + "@prisma/client": "^7.0.0", + "@prisma/instrumentation": "^7.0.0", + "@radix-ui/react-slot": "^1.2.4", + "@react-email/components": "^1.0.1", + "@react-email/render": "^2.0.0", + "@react-three/drei": "^10.7.7", + "@react-three/fiber": "^9.4.0", "@react-three/postprocessing": "^3.0.4", "@t3-oss/env-nextjs": "^0.13.8", - "@tanstack/react-form": "^1.23.8", - "@tanstack/react-query": "^5.90.7", + "@tailwindcss/typography": "^0.5.19", + "@tanstack/react-form": "^1.25.0", + "@tanstack/react-query": "^5.90.10", "@tanstack/react-table": "^8.21.3", - "@tiptap/extension-table": "^3.4.4", - "@tiptap/extension-table-cell": "^3.4.4", - "@tiptap/extension-table-header": "^3.4.4", - "@tiptap/extension-table-row": "^3.4.4", + "@tiptap/extension-table": "^3.11.0", + "@tiptap/extension-table-cell": "^3.11.0", + "@tiptap/extension-table-header": "^3.11.0", + "@tiptap/extension-table-row": "^3.11.0", + "@tiptap/react": "^3.11.0", "@trigger.dev/react-hooks": "4.0.6", "@trigger.dev/sdk": "4.0.6", - "@trycompai/db": "^1.3.17", + "@trycompai/analytics": "workspace:*", + "@trycompai/db": "workspace:*", "@trycompai/email": "workspace:*", - "@types/canvas-confetti": "^1.9.0", - "@types/react-syntax-highlighter": "^15.5.13", - "@types/three": "^0.180.0", - "@uploadthing/react": "^7.3.0", - "@upstash/ratelimit": "^2.0.5", + "@trycompai/integrations": "workspace:*", + "@trycompai/kv": "workspace:*", + "@trycompai/ui": "workspace:*", + "@trycompai/utils": "workspace:*", + "@uploadthing/react": "^7.3.3", + "@upstash/ratelimit": "^2.0.7", + "@upstash/vector": "^1.2.2", "@vercel/analytics": "^1.5.0", - "@vercel/sandbox": "^0.0.21", - "@vercel/sdk": "^1.7.1", - "ai": "^5.0.60", - "ai-elements": "^1.6.1", - "axios": "^1.9.0", - "better-auth": "^1.3.27", - "botid": "^1.5.5", - "canvas-confetti": "^1.9.3", + "@vercel/sandbox": "^1.0.2", + "@vercel/sdk": "^1.17.3", + "ai": "^5.0.97", + "ai-elements": "^1.6.3", + "axios": "^1.13.2", + "better-auth": "^1.3.34", + "botid": "^1.5.10", + "canvas-confetti": "^1.9.4", "d3": "^7.9.0", "date-fns": "^4.1.0", - "dub": "^0.66.1", - "framer-motion": "^12.18.1", - "geist": "^1.3.1", - "jspdf": "^3.0.2", - "lucide-react": "^0.544.0", - "motion": "^12.9.2", - "next": "^15.4.6", - "next-safe-action": "^8.0.3", - "next-themes": "^0.4.4", - "nuqs": "^2.4.3", + "dub": "^0.68.0", + "framer-motion": "^12.23.24", + "geist": "^1.5.1", + "jspdf": "^3.0.4", + "lucide-react": "^0.554.0", + "motion": "^12.23.24", + "next": "^16.0.3", + "next-safe-action": "^8.0.11", + "next-themes": "^0.4.6", + "nuqs": "^2.8.0", "pdf-parse": "^2.4.5", - "playwright-core": "^1.52.0", - "posthog-js": "^1.236.6", - "posthog-node": "^5.8.2", - "puppeteer-core": "^24.7.2", - "react": "^19.1.1", - "react-dom": "^19.1.0", - "react-email": "^4.0.15", - "react-hook-form": "^7.61.1", - "react-hotkeys-hook": "^5.1.0", - "react-intersection-observer": "^9.16.0", + "playwright-core": "^1.56.1", + "posthog-js": "^1.297.1", + "posthog-node": "^5.13.1", + "puppeteer-core": "^24.30.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.66.1", + "react-hotkeys-hook": "^5.2.1", + "react-intersection-observer": "^10.0.0", "react-markdown": "10.1.0", "react-spinners": "^0.17.0", - "react-syntax-highlighter": "^15.6.6", + "react-syntax-highlighter": "^16.1.0", "react-textarea-autosize": "^8.5.9", "react-use-draggable-scroll": "^0.4.7", "react-wrap-balancer": "^1.1.1", + "recharts": "2.15.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", - "resend": "^4.4.1", - "sonner": "^2.0.5", - "swr": "^2.3.4", - "three": "^0.177.0", - "ts-pattern": "^5.7.0", - "use-debounce": "^10.0.4", + "sonner": "^2.0.7", + "superjson": "^2.2.5", + "swr": "^2.3.6", + "three": "^0.181.2", + "ts-pattern": "^5.9.0", + "use-debounce": "^10.0.6", "use-long-press": "^3.3.0", "use-stick-to-bottom": "^1.1.1", "xlsx": "^0.18.5", "xml2js": "^0.6.2", "zaraz-ts": "^1.2.0", - "zod": "^3.25.76", - "zustand": "^5.0.3" + "zod": "^4.1.12", + "zustand": "^5.0.8" }, "devDependencies": { - "@playwright/experimental-ct-react": "^1.53.1", - "@playwright/test": "^1.53.1", - "@tailwindcss/postcss": "^4.1.10", - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", + "@playwright/experimental-ct-react": "^1.56.1", + "@playwright/test": "^1.56.1", + "@tailwindcss/postcss": "^4.1.17", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", "@trigger.dev/build": "4.0.6", + "@trycompai/eslint-config": "workspace:*", + "@types/canvas-confetti": "^1.9.0", "@types/d3": "^7.4.3", - "@types/jspdf": "^2.0.0", - "@types/node": "^24.0.3", - "@vitejs/plugin-react": "^4.6.0", - "@vitest/ui": "^3.2.4", - "eslint": "^9.18.0", - "eslint-config-next": "15.5.2", - "fleetctl": "^4.68.1", - "glob": "^11.0.3", - "jsdom": "^26.1.0", - "postcss": "^8.5.4", - "prisma": "^6.13.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.6", + "@types/react-dom": "^19.2.3", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/three": "^0.181.0", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/ui": "^4.0.10", + "eslint": "^9.39.1", + "fleetctl": "^4.76.1", + "glob": "^13.0.0", + "postcss": "^8.5.6", "raw-loader": "^4.0.2", - "tailwindcss": "^4.1.8", - "typescript": "^5.8.3", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.2.4" - }, - "exports": { - "./src/lib/encryption": "./src/lib/encryption.ts" - }, - "peerDependencies": { - "react": "^19.1.1", - "react-dom": "^19.1.0" - }, - "pnpm": { - "overrides": { - "tiptap-extension-global-drag-handle": "^0.1.18" - } - }, - "private": true, - "scripts": { - "analyze-locale-usage": "bunx tsx src/locales/analyze-locale-usage.ts", - "build": "next build", - "build:docker": "prisma generate && next build", - "db:generate": "bun run db:getschema && prisma generate", - "db:getschema": "node ../../packages/db/scripts/combine-schemas.js && cp ../../packages/db/dist/schema.prisma prisma/schema.prisma", - "db:migrate": "cd ../../packages/db && bunx prisma migrate dev && cd ../../apps/app", - "deploy:trigger-prod": "npx trigger.dev@4.0.6 deploy", - "dev": "bun i && bunx concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"next dev --turbo -p 3000\" \"bunx trigger.dev@4.0.6 dev\"", - "lint": "next lint && prettier --check .", - "prebuild": "bun run db:generate", - "start": "next start", - "test": "vitest", - "test:all": "./scripts/test-all.sh", - "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test", - "test:e2e:debug": "playwright test --debug", - "test:e2e:headed": "playwright test --headed", - "test:e2e:install": "playwright install --with-deps", - "test:e2e:report": "playwright show-report", - "test:e2e:setup": "./scripts/setup-e2e.sh", - "test:e2e:ui": "playwright test --ui", - "test:ui": "vitest --ui", - "test:watch": "vitest --watch", - "typecheck": "tsc --noEmit" + "vitest": "^4.0.10" } -} \ No newline at end of file +} diff --git a/apps/app/playwright.config.ts b/apps/app/playwright.config.ts index 343ac499a..cd1785b88 100644 --- a/apps/app/playwright.config.ts +++ b/apps/app/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -10,7 +10,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './e2e', + testDir: "./e2e", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -26,31 +26,31 @@ export default defineConfig({ /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? [ - ['list'], + ["list"], [ - 'html', + "html", { - open: 'never', - outputFolder: 'playwright-report', + open: "never", + outputFolder: "playwright-report", }, ], - ['json', { outputFile: 'test-results/results.json' }], - ['junit', { outputFile: 'test-results/junit.xml' }], + ["json", { outputFile: "test-results/results.json" }], + ["junit", { outputFile: "test-results/junit.xml" }], ] - : [['list'], ['html', { open: 'on-failure' }]], + : [["list"], ["html", { open: "on-failure" }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000', + baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", /* Take screenshot on failure */ - screenshot: 'only-on-failure', + screenshot: "only-on-failure", /* Record video on failure */ - video: 'retain-on-failure', + video: "retain-on-failure", /* Reduce default navigation timeout */ navigationTimeout: 15000, // 15 seconds instead of 30 @@ -60,28 +60,28 @@ export default defineConfig({ /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ { - name: 'Mobile Chrome', - use: { ...devices['Pixel 7'] }, + name: "Mobile Chrome", + use: { ...devices["Pixel 7"] }, }, { - name: 'Mobile Safari', - use: { ...devices['iPhone 14'] }, + name: "Mobile Safari", + use: { ...devices["iPhone 14"] }, }, /* Test against branded browsers. */ @@ -99,12 +99,12 @@ export default defineConfig({ webServer: process.env.CI ? undefined : { - command: 'E2E_TEST_MODE=true bun run dev', - url: 'http://localhost:3000', + command: "E2E_TEST_MODE=true bun run dev", + url: "http://localhost:3000", reuseExistingServer: !process.env.CI, timeout: 120 * 1000, env: { - E2E_TEST_MODE: 'true', + E2E_TEST_MODE: "true", }, }, }); diff --git a/apps/app/postcss.config.mjs b/apps/app/postcss.config.mjs index 77e8d2406..7059fe95a 100644 --- a/apps/app/postcss.config.mjs +++ b/apps/app/postcss.config.mjs @@ -1,8 +1,6 @@ const config = { plugins: { - '@tailwindcss/postcss': { - config: '../../packages/ui/tailwind.config.ts', - }, + "@tailwindcss/postcss": {}, }, }; export default config; diff --git a/apps/app/prisma/client.ts b/apps/app/prisma/client.ts deleted file mode 100644 index a696328be..000000000 --- a/apps/app/prisma/client.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -const globalForPrisma = global as unknown as { prisma: PrismaClient }; - -export const db = globalForPrisma.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db; diff --git a/apps/app/prisma/index.ts b/apps/app/prisma/index.ts deleted file mode 100644 index 54d1c4b9c..000000000 --- a/apps/app/prisma/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from '@prisma/client'; -export { db } from './client'; diff --git a/apps/app/scripts/backfill-training-videos.ts b/apps/app/scripts/backfill-training-videos.ts index ee1960403..d64eafb61 100644 --- a/apps/app/scripts/backfill-training-videos.ts +++ b/apps/app/scripts/backfill-training-videos.ts @@ -15,36 +15,43 @@ * - Testing the backfill process * - Running on-demand backfills for specific organizations */ - -import { backfillTrainingVideosForAllOrgs } from '@/jobs/tasks/onboarding/backfill-training-videos-for-all-orgs'; -import { backfillTrainingVideosForOrg } from '@/jobs/tasks/onboarding/backfill-training-videos-for-org'; +import { backfillTrainingVideosForAllOrgs } from "@/jobs/tasks/onboarding/backfill-training-videos-for-all-orgs"; +import { backfillTrainingVideosForOrg } from "@/jobs/tasks/onboarding/backfill-training-videos-for-org"; async function main() { const args = process.argv.slice(2); - const orgIndex = args.indexOf('--org'); + const orgIndex = args.indexOf("--org"); const organizationId = orgIndex !== -1 ? args[orgIndex + 1] : null; try { if (organizationId) { - console.log(`🚀 Triggering training video backfill for organization: ${organizationId}`); + console.log( + `🚀 Triggering training video backfill for organization: ${organizationId}`, + ); const handle = await backfillTrainingVideosForOrg.trigger({ organizationId: organizationId, }); console.log(`✅ Successfully triggered job with ID: ${handle.id}`); - console.log(`📊 You can monitor the progress in the Trigger.dev dashboard`); + console.log( + `📊 You can monitor the progress in the Trigger.dev dashboard`, + ); } else { - console.log('🚀 Triggering training video backfill for ALL organizations'); + console.log( + "🚀 Triggering training video backfill for ALL organizations", + ); const handle = await backfillTrainingVideosForAllOrgs.trigger(); console.log(`✅ Successfully triggered batch job with ID: ${handle.id}`); - console.log(`📊 You can monitor the progress in the Trigger.dev dashboard`); + console.log( + `📊 You can monitor the progress in the Trigger.dev dashboard`, + ); console.log(`⚠️ This will process ALL organizations and their members`); } } catch (error) { - console.error('❌ Error triggering backfill job:', error); + console.error("❌ Error triggering backfill job:", error); process.exit(1); } } @@ -52,7 +59,7 @@ async function main() { // Only run if this script is executed directly if (require.main === module) { main().catch((error) => { - console.error('❌ Script failed:', error); + console.error("❌ Script failed:", error); process.exit(1); }); } diff --git a/apps/app/scripts/setup-e2e.sh b/apps/app/scripts/setup-e2e.sh index 8aae600df..e3ea9d1d5 100755 --- a/apps/app/scripts/setup-e2e.sh +++ b/apps/app/scripts/setup-e2e.sh @@ -75,7 +75,7 @@ if [ -f "../../apps/app/.env.test.local" ]; then export $(grep DATABASE_URL ../../apps/app/.env.test.local | xargs) fi # Create database and push schema (will create DB if it doesn't exist) -bunx prisma db push --skip-generate --accept-data-loss +pnpx prisma db push --skip-generate --accept-data-loss cd ../../apps/app echo -e "${GREEN}✓ Migrations complete${NC}" @@ -84,7 +84,7 @@ echo "" echo -e "${YELLOW}4. Checking Playwright browsers...${NC}" if [ ! -d "$HOME/.cache/ms-playwright" ]; then echo "Installing Playwright browsers..." - bunx playwright install --with-deps chromium + pnpx playwright install --with-deps chromium echo -e "${GREEN}✓ Installed Playwright browsers${NC}" else echo -e "${GREEN}✓ Playwright browsers already installed${NC}" diff --git a/apps/app/scripts/test-all.sh b/apps/app/scripts/test-all.sh index 69ad686df..ddce4381e 100755 --- a/apps/app/scripts/test-all.sh +++ b/apps/app/scripts/test-all.sh @@ -72,7 +72,7 @@ if ! curl -s http://localhost:3000 > /dev/null; then fi # Run E2E tests -if bunx playwright test --project=chromium; then +if pnpx playwright test --project=chromium; then echo -e "${GREEN}✓ E2E tests passed${NC}" else echo -e "${RED}✗ E2E tests failed${NC}" diff --git a/apps/app/scripts/update-imports.js b/apps/app/scripts/update-imports.js index 6ab96d825..eeb8874f2 100755 --- a/apps/app/scripts/update-imports.js +++ b/apps/app/scripts/update-imports.js @@ -1,12 +1,12 @@ #!/usr/bin/env node -const fs = require('fs'); -const glob = require('glob'); +const fs = require("fs"); +const glob = require("glob"); -console.log('🔄 Updating database imports to use local client...'); +console.log("🔄 Updating database imports to use local client..."); // Find all TypeScript/TSX files in src directory -const files = glob.sync('src/**/*.{ts,tsx}', { +const files = glob.sync("src/**/*.{ts,tsx}", { cwd: process.cwd(), absolute: true, }); @@ -14,22 +14,31 @@ const files = glob.sync('src/**/*.{ts,tsx}', { let filesUpdated = 0; files.forEach((filePath) => { - const content = fs.readFileSync(filePath, 'utf8'); + const content = fs.readFileSync(filePath, "utf8"); let newContent = content; let hasChanges = false; // Pattern 1: Replace db imports to use local client - const dbImportPattern = /import\s*{\s*db\s*}\s*from\s*['"]@trycompai\/db['"];?/g; + const dbImportPattern = + /import\s*{\s*db\s*}\s*from\s*['"]@trycompai\/db['"];?/g; if (dbImportPattern.test(newContent)) { - newContent = newContent.replace(dbImportPattern, "import { db } from '@/lib/db';"); + newContent = newContent.replace( + dbImportPattern, + "import { db } from '@/lib/db';", + ); hasChanges = true; } // Pattern 2: Replace type-only imports to use @prisma/client - const typeImportPattern = /import\s*(?:type\s*)?\s*{([^}]+)}\s*from\s*['"]@trycompai\/db['"];?/g; + const typeImportPattern = + /import\s*(?:type\s*)?\s*{([^}]+)}\s*from\s*['"]@trycompai\/db['"];?/g; newContent = newContent.replace(typeImportPattern, (match, types) => { // Skip if it includes 'db' (non-type import) - if (types.includes(' db') || types.startsWith('db') || types.endsWith('db ')) { + if ( + types.includes(" db") || + types.startsWith("db") || + types.endsWith("db ") + ) { return match; // Don't change mixed imports, they'll be handled separately } hasChanges = true; @@ -37,18 +46,19 @@ files.forEach((filePath) => { }); // Pattern 3: Handle mixed imports (db + types) - const mixedImportPattern = /import\s*{\s*([^}]*db[^}]*)\s*}\s*from\s*['"]@trycompai\/db['"];?/g; + const mixedImportPattern = + /import\s*{\s*([^}]*db[^}]*)\s*}\s*from\s*['"]@trycompai\/db['"];?/g; newContent = newContent.replace(mixedImportPattern, (match, imports) => { - const parts = imports.split(',').map((part) => part.trim()); - const dbImports = parts.filter((part) => part === 'db'); - const typeImports = parts.filter((part) => part !== 'db' && part !== ''); + const parts = imports.split(",").map((part) => part.trim()); + const dbImports = parts.filter((part) => part === "db"); + const typeImports = parts.filter((part) => part !== "db" && part !== ""); - let replacement = ''; + let replacement = ""; if (dbImports.length > 0) { replacement += "import { db } from '@/lib/db';\n"; } if (typeImports.length > 0) { - replacement += `import type { ${typeImports.join(', ')} } from '@prisma/client';`; + replacement += `import type { ${typeImports.join(", ")} } from '@prisma/client';`; } hasChanges = true; @@ -57,10 +67,10 @@ files.forEach((filePath) => { if (hasChanges) { fs.writeFileSync(filePath, newContent); - console.log(`✅ Updated: ${filePath.replace(process.cwd() + '/', '')}`); + console.log(`✅ Updated: ${filePath.replace(process.cwd() + "/", "")}`); filesUpdated++; } }); console.log(`🎉 Updated ${filesUpdated} files!`); -console.log('📝 All imports now use local Prisma client'); +console.log("📝 All imports now use local Prisma client"); diff --git a/apps/app/src/actions/add-comment.ts b/apps/app/src/actions/add-comment.ts index 40a92dbf7..1e12a4bab 100644 --- a/apps/app/src/actions/add-comment.ts +++ b/apps/app/src/actions/add-comment.ts @@ -1,11 +1,13 @@ -'use server'; +"use server"; -import { AppError, appErrors } from '@/lib/errors'; -import { CommentEntityType, db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; -import { authActionClient } from './safe-action'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { AppError, appErrors } from "@/lib/errors"; +import { z } from "zod"; + +import { CommentEntityType, db } from "@trycompai/db"; + +import { authActionClient } from "./safe-action"; export const addCommentAction = authActionClient .inputSchema( @@ -16,10 +18,10 @@ export const addCommentAction = authActionClient }), ) .metadata({ - name: 'add-comment', + name: "add-comment", track: { - event: 'add-comment', - channel: 'server', + event: "add-comment", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -59,8 +61,9 @@ export const addCommentAction = authActionClient }); const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); diff --git a/apps/app/src/actions/change-organization.ts b/apps/app/src/actions/change-organization.ts index 32e768bb0..5109b45d9 100644 --- a/apps/app/src/actions/change-organization.ts +++ b/apps/app/src/actions/change-organization.ts @@ -1,11 +1,13 @@ -'use server'; +"use server"; -import { auth } from '@/utils/auth'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; -import { authActionClient } from './safe-action'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { auth } from "@/utils/auth"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "./safe-action"; export const changeOrganizationAction = authActionClient .inputSchema( @@ -14,10 +16,10 @@ export const changeOrganizationAction = authActionClient }), ) .metadata({ - name: 'change-organization', + name: "change-organization", track: { - event: 'create-employee', - channel: 'server', + event: "create-employee", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -34,7 +36,7 @@ export const changeOrganizationAction = authActionClient if (!organizationMember) { return { success: false, - error: 'Unauthorized', + error: "Unauthorized", }; } @@ -48,7 +50,7 @@ export const changeOrganizationAction = authActionClient if (!organization) { return { success: false, - error: 'Organization not found', + error: "Organization not found", }; } @@ -66,11 +68,11 @@ export const changeOrganizationAction = authActionClient data: organization, }; } catch (error) { - console.error('Error changing organization:', error); + console.error("Error changing organization:", error); return { success: false, - error: 'Failed to change organization', + error: "Failed to change organization", }; } }); diff --git a/apps/app/src/actions/context-hub/create-context-entry-action.ts b/apps/app/src/actions/context-hub/create-context-entry-action.ts index 261ed96de..8fe50071e 100644 --- a/apps/app/src/actions/context-hub/create-context-entry-action.ts +++ b/apps/app/src/actions/context-hub/create-context-entry-action.ts @@ -1,18 +1,20 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { authActionClient } from '../safe-action'; -import { createContextEntrySchema } from '../schema'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { createContextEntrySchema } from "../schema"; export const createContextEntryAction = authActionClient .inputSchema(createContextEntrySchema) - .metadata({ name: 'create-context-entry' }) + .metadata({ name: "create-context-entry" }) .action(async ({ parsedInput, ctx }) => { const { question, answer, tags } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error('No active organization'); + if (!organizationId) throw new Error("No active organization"); await db.context.create({ data: { @@ -20,7 +22,7 @@ export const createContextEntryAction = authActionClient answer, tags: tags ? tags - .split(',') + .split(",") .map((t) => t.trim()) .filter(Boolean) : [], @@ -29,8 +31,9 @@ export const createContextEntryAction = authActionClient }); const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); diff --git a/apps/app/src/actions/context-hub/delete-context-entry-action.ts b/apps/app/src/actions/context-hub/delete-context-entry-action.ts index a04488d35..fd3bc64bf 100644 --- a/apps/app/src/actions/context-hub/delete-context-entry-action.ts +++ b/apps/app/src/actions/context-hub/delete-context-entry-action.ts @@ -1,26 +1,29 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { authActionClient } from '../safe-action'; -import { deleteContextEntrySchema } from '../schema'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { deleteContextEntrySchema } from "../schema"; export const deleteContextEntryAction = authActionClient .inputSchema(deleteContextEntrySchema) - .metadata({ name: 'delete-context-entry' }) + .metadata({ name: "delete-context-entry" }) .action(async ({ parsedInput, ctx }) => { const { id } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error('No active organization'); + if (!organizationId) throw new Error("No active organization"); await db.context.delete({ where: { id, organizationId }, }); const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); return { success: true }; diff --git a/apps/app/src/actions/context-hub/update-context-entry-action.ts b/apps/app/src/actions/context-hub/update-context-entry-action.ts index a7c103b88..4e0c092da 100644 --- a/apps/app/src/actions/context-hub/update-context-entry-action.ts +++ b/apps/app/src/actions/context-hub/update-context-entry-action.ts @@ -1,18 +1,20 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { authActionClient } from '../safe-action'; -import { updateContextEntrySchema } from '../schema'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updateContextEntrySchema } from "../schema"; export const updateContextEntryAction = authActionClient .inputSchema(updateContextEntrySchema) - .metadata({ name: 'update-context-entry' }) + .metadata({ name: "update-context-entry" }) .action(async ({ parsedInput, ctx }) => { const { id, question, answer, tags } = parsedInput; const organizationId = ctx.session.activeOrganizationId; - if (!organizationId) throw new Error('No active organization'); + if (!organizationId) throw new Error("No active organization"); await db.context.update({ where: { id, organizationId }, @@ -21,7 +23,7 @@ export const updateContextEntryAction = authActionClient answer, tags: tags ? tags - .split(',') + .split(",") .map((t) => t.trim()) .filter(Boolean) : [], @@ -29,8 +31,9 @@ export const updateContextEntryAction = authActionClient }); const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); diff --git a/apps/app/src/actions/controls/create-control-action.ts b/apps/app/src/actions/controls/create-control-action.ts index 72cd1af66..e0ae84efe 100644 --- a/apps/app/src/actions/controls/create-control-action.ts +++ b/apps/app/src/actions/controls/create-control-action.ts @@ -1,17 +1,18 @@ -'use server'; +"use server"; -import { authActionClient } from '@/actions/safe-action'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { authActionClient } from "@/actions/safe-action"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; const createControlSchema = z.object({ name: z.string().min(1, { - message: 'Name is required', + message: "Name is required", }), description: z.string().min(1, { - message: 'Description is required', + message: "Description is required", }), policyIds: z.array(z.string()).optional(), taskIds: z.array(z.string()).optional(), @@ -28,21 +29,22 @@ const createControlSchema = z.object({ export const createControlAction = authActionClient .inputSchema(createControlSchema) .metadata({ - name: 'create-control', + name: "create-control", track: { - event: 'create-control', - channel: 'server', + event: "create-control", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { - const { name, description, policyIds, taskIds, requirementMappings } = parsedInput; + const { name, description, policyIds, taskIds, requirementMappings } = + parsedInput; const { session: { activeOrganizationId }, user, } = ctx; if (!user.id || !activeOrganizationId) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } try { @@ -84,8 +86,9 @@ export const createControlAction = authActionClient // Revalidate the path based on the header const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); return { @@ -93,10 +96,10 @@ export const createControlAction = authActionClient control, }; } catch (error) { - console.error('Failed to create control:', error); + console.error("Failed to create control:", error); return { success: false, - error: 'Failed to create control', + error: "Failed to create control", }; } }); diff --git a/apps/app/src/actions/files/upload-file.ts b/apps/app/src/actions/files/upload-file.ts index e4f90ff1e..ae01c46fd 100644 --- a/apps/app/src/actions/files/upload-file.ts +++ b/apps/app/src/actions/files/upload-file.ts @@ -1,39 +1,41 @@ -'use server'; +"use server"; -console.log('[uploadFile] Upload action module is being loaded...'); +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { BUCKET_NAME, s3Client } from "@/app/s3"; +import { auth } from "@/utils/auth"; +import { logger } from "@/utils/logger"; +import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; +import { z } from "zod"; -console.log('[uploadFile] Importing auth and logger...'); -import { BUCKET_NAME, s3Client } from '@/app/s3'; -import { auth } from '@/utils/auth'; -import { logger } from '@/utils/logger'; -import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { AttachmentEntityType, AttachmentType, db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; +import { AttachmentEntityType, AttachmentType, db } from "@trycompai/db"; -console.log('[uploadFile] Importing S3 client...'); +console.log("[uploadFile] Upload action module is being loaded..."); -console.log('[uploadFile] Importing AWS SDK...'); +console.log("[uploadFile] Importing auth and logger..."); -console.log('[uploadFile] Importing database...'); +console.log("[uploadFile] Importing S3 client..."); -console.log('[uploadFile] All imports successful'); +console.log("[uploadFile] Importing AWS SDK..."); + +console.log("[uploadFile] Importing database..."); + +console.log("[uploadFile] All imports successful"); // This log will run as soon as the module is loaded. -logger.info('[uploadFile] Module loaded.'); +logger.info("[uploadFile] Module loaded."); function mapFileTypeToAttachmentType(fileType: string): AttachmentType { - const type = fileType.split('/')[0]; + const type = fileType.split("/")[0]; switch (type) { - case 'image': + case "image": return AttachmentType.image; - case 'video': + case "video": return AttachmentType.video; - case 'audio': + case "audio": return AttachmentType.audio; - case 'application': + case "application": return AttachmentType.document; default: return AttachmentType.other; @@ -49,49 +51,62 @@ const uploadAttachmentSchema = z.object({ pathToRevalidate: z.string().optional(), }); -export const uploadFile = async (input: z.infer) => { - console.log('[uploadFile] Function called - starting execution'); +export const uploadFile = async ( + input: z.infer, +) => { + console.log("[uploadFile] Function called - starting execution"); logger.info(`[uploadFile] Starting upload for ${input.fileName}`); - console.log('[uploadFile] Checking S3 client availability'); + console.log("[uploadFile] Checking S3 client availability"); try { // Check if S3 client is available if (!s3Client) { - logger.error('[uploadFile] S3 client not initialized - check environment variables'); + logger.error( + "[uploadFile] S3 client not initialized - check environment variables", + ); return { success: false, - error: 'File upload service is currently unavailable. Please contact support.', + error: + "File upload service is currently unavailable. Please contact support.", } as const; } if (!BUCKET_NAME) { - logger.error('[uploadFile] S3 bucket name not configured'); + logger.error("[uploadFile] S3 bucket name not configured"); return { success: false, - error: 'File upload service is not properly configured.', + error: "File upload service is not properly configured.", } as const; } - console.log('[uploadFile] Parsing input schema'); - const { fileName, fileType, fileData, entityId, entityType, pathToRevalidate } = - uploadAttachmentSchema.parse(input); - - console.log('[uploadFile] Getting user session'); + console.log("[uploadFile] Parsing input schema"); + const { + fileName, + fileType, + fileData, + entityId, + entityType, + pathToRevalidate, + } = uploadAttachmentSchema.parse(input); + + console.log("[uploadFile] Getting user session"); const session = await auth.api.getSession({ headers: await headers() }); const organizationId = session?.session.activeOrganizationId; if (!organizationId) { - logger.error('[uploadFile] Not authorized - no organization found'); + logger.error("[uploadFile] Not authorized - no organization found"); return { success: false, - error: 'Not authorized - no organization found', + error: "Not authorized - no organization found", } as const; } - logger.info(`[uploadFile] Starting upload for ${fileName} in org ${organizationId}`); + logger.info( + `[uploadFile] Starting upload for ${fileName} in org ${organizationId}`, + ); - console.log('[uploadFile] Converting file data to buffer'); - const fileBuffer = Buffer.from(fileData, 'base64'); + console.log("[uploadFile] Converting file data to buffer"); + const fileBuffer = Buffer.from(fileData, "base64"); const MAX_FILE_SIZE_MB = 10; const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024; @@ -106,7 +121,7 @@ export const uploadFile = async (input: z.infer) } const timestamp = Date.now(); - const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_'); + const sanitizedFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, "_"); const key = `${organizationId}/attachments/${entityType}/${entityId}/${timestamp}-${sanitizedFileName}`; logger.info(`[uploadFile] Uploading to S3 with key: ${key}`); @@ -119,7 +134,9 @@ export const uploadFile = async (input: z.infer) await s3Client.send(putCommand); logger.info(`[uploadFile] S3 upload successful for key: ${key}`); - logger.info(`[uploadFile] Creating attachment record in DB for key: ${key}`); + logger.info( + `[uploadFile] Creating attachment record in DB for key: ${key}`, + ); const attachment = await db.attachment.create({ data: { name: fileName, @@ -157,7 +174,8 @@ export const uploadFile = async (input: z.infer) logger.error(`[uploadFile] Error during upload process:`, error); return { success: false, - error: error instanceof Error ? error.message : 'An unknown error occurred.', + error: + error instanceof Error ? error.message : "An unknown error occurred.", } as const; } }; diff --git a/apps/app/src/actions/floating.ts b/apps/app/src/actions/floating.ts index 6f4746973..c6f3356b8 100644 --- a/apps/app/src/actions/floating.ts +++ b/apps/app/src/actions/floating.ts @@ -1,9 +1,9 @@ -'use server'; +"use server"; -import { addYears } from 'date-fns'; -import { createSafeActionClient } from 'next-safe-action'; -import { cookies } from 'next/headers'; -import { z } from 'zod'; +import { cookies } from "next/headers"; +import { addYears } from "date-fns"; +import { createSafeActionClient } from "next-safe-action"; +import { z } from "zod"; const schema = z.object({ floatingOpen: z.boolean(), @@ -15,7 +15,7 @@ export const updateFloatingState = createSafeActionClient() const cookieStore = await cookies(); cookieStore.set({ - name: 'floating-onboarding-checklist', + name: "floating-onboarding-checklist", value: JSON.stringify(parsedInput.floatingOpen), expires: addYears(new Date(), 1), }); diff --git a/apps/app/src/actions/integrations/delete-integration-connection.ts b/apps/app/src/actions/integrations/delete-integration-connection.ts index d47db08c9..a3a4f9fa1 100644 --- a/apps/app/src/actions/integrations/delete-integration-connection.ts +++ b/apps/app/src/actions/integrations/delete-integration-connection.ts @@ -1,19 +1,21 @@ // delete-integration-connection.ts -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { deleteIntegrationConnectionSchema } from '../schema'; +import { revalidatePath } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { deleteIntegrationConnectionSchema } from "../schema"; export const deleteIntegrationConnectionAction = authActionClient .inputSchema(deleteIntegrationConnectionSchema) .metadata({ - name: 'delete-integration-connection', + name: "delete-integration-connection", track: { - event: 'delete-integration-connection', - channel: 'server', + event: "delete-integration-connection", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -23,7 +25,7 @@ export const deleteIntegrationConnectionAction = authActionClient if (!session.activeOrganizationId) { return { success: false, - error: 'Unauthorized', + error: "Unauthorized", }; } @@ -37,7 +39,7 @@ export const deleteIntegrationConnectionAction = authActionClient if (!integration) { return { success: false, - error: 'Integration not found', + error: "Integration not found", }; } @@ -47,7 +49,7 @@ export const deleteIntegrationConnectionAction = authActionClient }, }); - revalidatePath('/integrations'); + revalidatePath("/integrations"); return { success: true, diff --git a/apps/app/src/actions/integrations/retrieve-integration-session-token.ts b/apps/app/src/actions/integrations/retrieve-integration-session-token.ts index 3eaa60a2f..aec280248 100644 --- a/apps/app/src/actions/integrations/retrieve-integration-session-token.ts +++ b/apps/app/src/actions/integrations/retrieve-integration-session-token.ts @@ -1,17 +1,17 @@ // retrieve-integration-session-token.ts -'use server'; +"use server"; -import { authActionClient } from '../safe-action'; -import { createIntegrationSchema } from '../schema'; +import { authActionClient } from "../safe-action"; +import { createIntegrationSchema } from "../schema"; export const retrieveIntegrationSessionTokenAction = authActionClient .inputSchema(createIntegrationSchema) .metadata({ - name: 'retrieve-integration-session-token', + name: "retrieve-integration-session-token", track: { - event: 'retrieve-integration-session-token', - channel: 'server', + event: "retrieve-integration-session-token", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -20,6 +20,6 @@ export const retrieveIntegrationSessionTokenAction = authActionClient return { success: true, - sessionToken: '123', + sessionToken: "123", }; }); diff --git a/apps/app/src/actions/integrations/update-integration-settings-action.ts b/apps/app/src/actions/integrations/update-integration-settings-action.ts index e4e2a6d2b..ab2f63d88 100644 --- a/apps/app/src/actions/integrations/update-integration-settings-action.ts +++ b/apps/app/src/actions/integrations/update-integration-settings-action.ts @@ -1,10 +1,12 @@ -'use server'; +"use server"; -import { encrypt } from '@/lib/encryption'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; +import { encrypt } from "@trycompai/utils/encryption"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; export const updateIntegrationSettingsAction = authActionClient .inputSchema( @@ -17,77 +19,84 @@ export const updateIntegrationSettingsAction = authActionClient }), ) .metadata({ - name: 'update-integration-settings', + name: "update-integration-settings", track: { - event: 'update-integration-settings', - channel: 'update-integration-settings', + event: "update-integration-settings", + channel: "update-integration-settings", }, }) - .action(async ({ parsedInput: { integration_id, option }, ctx: { session } }) => { - try { - if (!session.activeOrganizationId) { - throw new Error('User organization not found'); - } - - let existingIntegration = await db.integration.findFirst({ - where: { - name: integration_id, - organizationId: session.activeOrganizationId, - }, - }); + .action( + async ({ parsedInput: { integration_id, option }, ctx: { session } }) => { + try { + if (!session.activeOrganizationId) { + throw new Error("User organization not found"); + } - if (!existingIntegration) { - existingIntegration = await db.integration.create({ - data: { + let existingIntegration = await db.integration.findFirst({ + where: { name: integration_id, organizationId: session.activeOrganizationId, - userSettings: {}, - integrationId: integration_id, - settings: {}, }, }); - } - const userSettings = existingIntegration.userSettings; + if (!existingIntegration) { + existingIntegration = await db.integration.create({ + data: { + name: integration_id, + organizationId: session.activeOrganizationId, + userSettings: {}, + integrationId: integration_id, + settings: {}, + }, + }); + } - if (!userSettings) { - throw new Error('User settings not found'); - } + const userSettings = existingIntegration.userSettings; - const updatedUserSettings = { - ...(userSettings as Record), - [option.id]: option.value, - }; + if (!userSettings) { + throw new Error("User settings not found"); + } - const parsedUserSettings = JSON.parse(JSON.stringify(updatedUserSettings)); + const updatedUserSettings = { + ...(userSettings as Record), + [option.id]: option.value, + }; - const encryptedSettings = await Promise.all( - Object.entries(parsedUserSettings).map(async ([key, value]) => { - if (typeof value === 'string') { - const encrypted = await encrypt(value); - return [key, encrypted]; - } - return [key, value]; - }), - ).then(Object.fromEntries); + const parsedUserSettings = JSON.parse( + JSON.stringify(updatedUserSettings), + ); - await db.integration.update({ - where: { - id: existingIntegration.id, - }, - data: { - userSettings: encryptedSettings, - }, - }); + const encryptedSettings = await Promise.all( + Object.entries(parsedUserSettings).map(async ([key, value]) => { + if (typeof value === "string") { + const encrypted = await encrypt(value); + return [key, encrypted]; + } + return [key, value]; + }), + ).then(Object.fromEntries); - revalidatePath('/integrations'); + await db.integration.update({ + where: { + id: existingIntegration.id, + }, + data: { + userSettings: encryptedSettings, + }, + }); - return { success: true }; - } catch (error) { - console.error('Failed to update integration settings:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update integration settings', - }; - } - }); + revalidatePath("/integrations"); + + return { success: true }; + } catch (error) { + console.error("Failed to update integration settings:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to update integration settings", + }; + } + }, + ); diff --git a/apps/app/src/actions/organization/accept-invitation.ts b/apps/app/src/actions/organization/accept-invitation.ts index 78e0bf7ac..636d2b834 100644 --- a/apps/app/src/actions/organization/accept-invitation.ts +++ b/apps/app/src/actions/organization/accept-invitation.ts @@ -1,17 +1,19 @@ -'use server'; +"use server"; -import { createTrainingVideoEntries } from '@/lib/db/employee'; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { redirect } from 'next/navigation'; -import { z } from 'zod'; -import { authActionClientWithoutOrg } from '../safe-action'; -import type { ActionResponse } from '../types'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { redirect } from "next/navigation"; +import { createTrainingVideoEntries } from "@/lib/db/employee"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import type { ActionResponse } from "../types"; +import { authActionClientWithoutOrg } from "../safe-action"; async function validateInviteCode(inviteCode: string, invitedEmail: string) { const pendingInvitation = await db.invitation.findFirst({ where: { - status: 'pending', + status: "pending", email: invitedEmail, id: inviteCode, }, @@ -34,10 +36,10 @@ const completeInvitationSchema = z.object({ export const completeInvitation = authActionClientWithoutOrg .metadata({ - name: 'complete-invitation', + name: "complete-invitation", track: { - event: 'complete_invitation', - channel: 'organization', + event: "complete_invitation", + channel: "organization", }, }) .inputSchema(completeInvitationSchema) @@ -55,14 +57,14 @@ export const completeInvitation = authActionClientWithoutOrg const user = ctx.user; if (!user || !user.email) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } try { const invitation = await validateInviteCode(inviteCode, user.email); if (!invitation) { - throw new Error('Invitation either used or expired'); + throw new Error("Invitation either used or expired"); } const existingMembership = await db.member.findFirst({ @@ -85,7 +87,7 @@ export const completeInvitation = authActionClientWithoutOrg await db.invitation.update({ where: { id: invitation.id }, data: { - status: 'accepted', + status: "accepted", }, }); @@ -94,7 +96,7 @@ export const completeInvitation = authActionClientWithoutOrg } if (!invitation.role) { - throw new Error('Invitation role is required'); + throw new Error("Invitation role is required"); } const newMember = await db.member.create({ @@ -102,7 +104,7 @@ export const completeInvitation = authActionClientWithoutOrg userId: user.id, organizationId: invitation.organizationId, role: invitation.role, - department: 'none', + department: "none", }, }); @@ -114,7 +116,7 @@ export const completeInvitation = authActionClientWithoutOrg id: invitation.id, }, data: { - status: 'accepted', + status: "accepted", }, }); @@ -129,12 +131,12 @@ export const completeInvitation = authActionClientWithoutOrg revalidatePath(`/${invitation.organization.id}`); revalidatePath(`/${invitation.organization.id}/settings/users`); - revalidateTag(`user_${user.id}`); + revalidateTag(`user_${user.id}`, { expire: 0 }); // Server redirect to the organization's root redirect(`/${invitation.organizationId}/`); } catch (error) { - console.error('Error accepting invitation:', error); + console.error("Error accepting invitation:", error); throw new Error(error as string); } }, diff --git a/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts b/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts index 37068764e..ffb22197f 100644 --- a/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts +++ b/apps/app/src/actions/organization/add-frameworks-to-organization-action.ts @@ -1,9 +1,11 @@ -'use server'; +"use server"; -import { addFrameworksSchema } from '@/actions/schema'; -import { db, Prisma } from '@db'; -import { authWithOrgAccessClient } from '../safe-action'; -import { _upsertOrgFrameworkStructureCore } from './lib/initialize-organization'; +import { addFrameworksSchema } from "@/actions/schema"; + +import { db, Prisma } from "@trycompai/db"; + +import { authWithOrgAccessClient } from "../safe-action"; +import { _upsertOrgFrameworkStructureCore } from "./lib/initialize-organization"; /** * Adds specified frameworks and their related entities (controls, policies, tasks) @@ -13,11 +15,11 @@ import { _upsertOrgFrameworkStructureCore } from './lib/initialize-organization' export const addFrameworksToOrganizationAction = authWithOrgAccessClient .inputSchema(addFrameworksSchema) .metadata({ - name: 'add-frameworks-to-organization', + name: "add-frameworks-to-organization", track: { - event: 'add-frameworks', - description: 'Add frameworks to organization', - channel: 'server', + event: "add-frameworks", + description: "Add frameworks to organization", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -26,21 +28,26 @@ export const addFrameworksToOrganizationAction = authWithOrgAccessClient await db.$transaction(async (tx) => { // 1. Fetch FrameworkEditorFrameworks and their requirements for the given frameworkIds, filtering by visible: true - const frameworksAndRequirements = await tx.frameworkEditorFramework.findMany({ - where: { - id: { in: frameworkIds }, - visible: true, - }, - include: { - requirements: true, - }, - }); + const frameworksAndRequirements = + await tx.frameworkEditorFramework.findMany({ + where: { + id: { in: frameworkIds }, + visible: true, + }, + include: { + requirements: true, + }, + }); if (frameworksAndRequirements.length === 0) { - throw new Error('No valid or visible frameworks found for the provided IDs.'); + throw new Error( + "No valid or visible frameworks found for the provided IDs.", + ); } - const finalFrameworkEditorIds = frameworksAndRequirements.map((f) => f.id); + const finalFrameworkEditorIds = frameworksAndRequirements.map( + (f) => f.id, + ); // 2. Call the renamed core function await _upsertOrgFrameworkStructureCore({ diff --git a/apps/app/src/actions/organization/bulk-invite-employees.ts b/apps/app/src/actions/organization/bulk-invite-employees.ts index cbc4e67db..6c12397c1 100644 --- a/apps/app/src/actions/organization/bulk-invite-employees.ts +++ b/apps/app/src/actions/organization/bulk-invite-employees.ts @@ -1,16 +1,18 @@ -'use server'; +"use server"; -import { auth } from '@/utils/auth'; -import { authClient } from '@/utils/auth-client'; -import { createSafeActionClient } from 'next-safe-action'; -import { headers } from 'next/headers'; -import { z } from 'zod'; +import { headers } from "next/headers"; +import { auth } from "@/utils/auth"; +import { authClient } from "@/utils/auth-client"; +import { createSafeActionClient } from "next-safe-action"; +import { z } from "zod"; -const emailSchema = z.string().email({ message: 'Invalid email format' }); +const emailSchema = z.string().email({ message: "Invalid email format" }); const schema = z.object({ organizationId: z.string(), - emails: z.array(emailSchema).min(1, { message: 'At least one email is required.' }), + emails: z + .array(emailSchema) + .min(1, { message: "At least one email is required." }), }); interface InviteResult { @@ -28,7 +30,7 @@ export const bulkInviteEmployees = createSafeActionClient() if (session?.session.activeOrganizationId !== organizationId) { return { success: false, - error: 'Unauthorized or invalid organization.', + error: "Unauthorized or invalid organization.", }; } @@ -39,13 +41,14 @@ export const bulkInviteEmployees = createSafeActionClient() try { await authClient.organization.inviteMember({ email: email, - role: 'employee', + role: "employee", }); results.push({ email, success: true }); } catch (error) { allSuccess = false; console.error(`Failed to invite ${email}:`, error); - const errorMessage = error instanceof Error ? error.message : 'Invitation failed'; + const errorMessage = + error instanceof Error ? error.message : "Invitation failed"; results.push({ email, success: false, error: errorMessage }); } } diff --git a/apps/app/src/actions/organization/create-api-key-action.ts b/apps/app/src/actions/organization/create-api-key-action.ts index 3b9389368..14403115e 100644 --- a/apps/app/src/actions/organization/create-api-key-action.ts +++ b/apps/app/src/actions/organization/create-api-key-action.ts @@ -1,18 +1,19 @@ -'use server'; +"use server"; -import { authActionClient } from '@/actions/safe-action'; -import { apiKeySchema } from '@/actions/schema'; -import { generateApiKey, generateSalt, hashApiKey } from '@/lib/api-key'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; +import { revalidatePath } from "next/cache"; +import { authActionClient } from "@/actions/safe-action"; +import { apiKeySchema } from "@/actions/schema"; +import { generateApiKey, generateSalt, hashApiKey } from "@/lib/api-key"; + +import { db } from "@trycompai/db"; export const createApiKeyAction = authActionClient .inputSchema(apiKeySchema) .metadata({ - name: 'createApiKey', + name: "createApiKey", track: { - event: 'createApiKey', - channel: 'server', + event: "createApiKey", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -24,26 +25,28 @@ export const createApiKeyAction = authActionClient const apiKey = generateApiKey(); const salt = generateSalt(); const hashedKey = hashApiKey(apiKey, salt); - console.log(`Generated new API key for organization: ${ctx.session.activeOrganizationId}`); + console.log( + `Generated new API key for organization: ${ctx.session.activeOrganizationId}`, + ); // Parse the expiration date let expirationDate: Date | null = null; - if (expiresAt && expiresAt !== 'never') { + if (expiresAt && expiresAt !== "never") { const now = new Date(); switch (expiresAt) { - case '30days': + case "30days": expirationDate = new Date(now.setDate(now.getDate() + 30)); break; - case '90days': + case "90days": expirationDate = new Date(now.setDate(now.getDate() + 90)); break; - case '1year': + case "1year": expirationDate = new Date(now.setFullYear(now.getFullYear() + 1)); break; } console.log(`Set expiration date to: ${expirationDate?.toISOString()}`); } else { - console.log('No expiration date set for API key'); + console.log("No expiration date set for API key"); } // Create the API key in the database @@ -72,32 +75,35 @@ export const createApiKeyAction = authActionClient ...apiKeyRecord, key: apiKey, createdAt: apiKeyRecord.createdAt.toISOString(), - expiresAt: apiKeyRecord.expiresAt ? apiKeyRecord.expiresAt.toISOString() : null, + expiresAt: apiKeyRecord.expiresAt + ? apiKeyRecord.expiresAt.toISOString() + : null, }, }; } catch (error) { - console.error('Error creating API key:', error); + console.error("Error creating API key:", error); // Provide more specific error messages based on error type if (error instanceof Error) { console.error(`Error details: ${error.message}`); - if (error.message.includes('Unique constraint')) { + if (error.message.includes("Unique constraint")) { return { success: false, error: { - code: 'DUPLICATE_NAME', - message: 'An API key with this name already exists', + code: "DUPLICATE_NAME", + message: "An API key with this name already exists", }, }; } - if (error.message.includes('Foreign key constraint')) { + if (error.message.includes("Foreign key constraint")) { return { success: false, error: { - code: 'INVALID_ORGANIZATION', - message: "The organization does not exist or you don't have access", + code: "INVALID_ORGANIZATION", + message: + "The organization does not exist or you don't have access", }, }; } @@ -106,8 +112,8 @@ export const createApiKeyAction = authActionClient return { success: false, error: { - code: 'INTERNAL_ERROR', - message: 'An unexpected error occurred while creating the API key', + code: "INTERNAL_ERROR", + message: "An unexpected error occurred while creating the API key", }, }; } diff --git a/apps/app/src/actions/organization/delete-organization-action.ts b/apps/app/src/actions/organization/delete-organization-action.ts index 232f301d4..dedcc9c1b 100644 --- a/apps/app/src/actions/organization/delete-organization-action.ts +++ b/apps/app/src/actions/organization/delete-organization-action.ts @@ -1,11 +1,13 @@ // delete-organization-action.ts -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { deleteOrganizationSchema } from '../schema'; +import { revalidatePath } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { deleteOrganizationSchema } from "../schema"; type DeleteOrganizationResult = { success: boolean; @@ -15,10 +17,10 @@ type DeleteOrganizationResult = { export const deleteOrganizationAction = authActionClient .inputSchema(deleteOrganizationSchema) .metadata({ - name: 'delete-organization', + name: "delete-organization", track: { - event: 'delete-organization', - channel: 'server', + event: "delete-organization", + channel: "server", }, }) .action(async ({ parsedInput, ctx }): Promise => { @@ -26,17 +28,17 @@ export const deleteOrganizationAction = authActionClient const { session } = ctx; if (!id) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } if (!session.activeOrganizationId) { - throw new Error('Invalid organization input'); + throw new Error("Invalid organization input"); } try { await db.$transaction(async () => { await db.organization.delete({ - where: { id: session.activeOrganizationId ?? '' }, + where: { id: session.activeOrganizationId ?? "" }, }); }); diff --git a/apps/app/src/actions/organization/get-api-keys-action.ts b/apps/app/src/actions/organization/get-api-keys-action.ts index 85149c4e7..7537d58a2 100644 --- a/apps/app/src/actions/organization/get-api-keys-action.ts +++ b/apps/app/src/actions/organization/get-api-keys-action.ts @@ -1,9 +1,10 @@ -'use server'; +"use server"; -import type { ActionResponse } from '@/actions/types'; -import { auth } from '@/utils/auth'; -import { db } from '@db'; -import { headers } from 'next/headers'; +import type { ActionResponse } from "@/actions/types"; +import { headers } from "next/headers"; +import { auth } from "@/utils/auth"; + +import { db } from "@trycompai/db"; export const getApiKeysAction = async (): Promise< ActionResponse< @@ -26,8 +27,8 @@ export const getApiKeysAction = async (): Promise< return { success: false, error: { - code: 'UNAUTHORIZED', - message: 'You must be logged in to perform this action', + code: "UNAUTHORIZED", + message: "You must be logged in to perform this action", }, }; } @@ -46,7 +47,7 @@ export const getApiKeysAction = async (): Promise< isActive: true, }, orderBy: { - createdAt: 'desc', + createdAt: "desc", }, }); @@ -60,12 +61,12 @@ export const getApiKeysAction = async (): Promise< })), }; } catch (error) { - console.error('Error fetching API keys:', error); + console.error("Error fetching API keys:", error); return { success: false, error: { - code: 'INTERNAL_ERROR', - message: 'An error occurred while fetching API keys', + code: "INTERNAL_ERROR", + message: "An error occurred while fetching API keys", }, }; } diff --git a/apps/app/src/actions/organization/get-organization-users-action.ts b/apps/app/src/actions/organization/get-organization-users-action.ts index dcd33e6a8..2fbfe8f9f 100644 --- a/apps/app/src/actions/organization/get-organization-users-action.ts +++ b/apps/app/src/actions/organization/get-organization-users-action.ts @@ -1,7 +1,8 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { authActionClient } from '../safe-action'; +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; interface User { id: string; @@ -11,14 +12,17 @@ interface User { export const getOrganizationUsersAction = authActionClient .metadata({ - name: 'get-organization-users', + name: "get-organization-users", }) .action( - async ({ parsedInput, ctx }): Promise<{ success: boolean; error?: string; data?: User[] }> => { + async ({ + parsedInput, + ctx, + }): Promise<{ success: boolean; error?: string; data?: User[] }> => { if (!ctx.session.activeOrganizationId) { return { success: false, - error: 'User does not have an organization', + error: "User does not have an organization", }; } @@ -38,7 +42,7 @@ export const getOrganizationUsersAction = authActionClient }, orderBy: { user: { - name: 'asc', + name: "asc", }, }, }); @@ -47,14 +51,14 @@ export const getOrganizationUsersAction = authActionClient success: true, data: users.map((user) => ({ id: user.user.id, - name: user.user.name || '', - image: user.user.image || '', + name: user.user.name || "", + image: user.user.image || "", })), }; } catch (error) { return { success: false, - error: 'Failed to fetch organization users', + error: "Failed to fetch organization users", }; } }, diff --git a/apps/app/src/actions/organization/invite-employee.ts b/apps/app/src/actions/organization/invite-employee.ts index 241fa7d24..c14783d2e 100644 --- a/apps/app/src/actions/organization/invite-employee.ts +++ b/apps/app/src/actions/organization/invite-employee.ts @@ -1,10 +1,11 @@ -'use server'; +"use server"; -import { authClient } from '@/utils/auth-client'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; -import type { ActionResponse } from '../types'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { authClient } from "@/utils/auth-client"; +import { z } from "zod"; + +import type { ActionResponse } from "../types"; +import { authActionClient } from "../safe-action"; // Schema only needs email now const inviteEmployeeSchema = z.object({ @@ -13,45 +14,51 @@ const inviteEmployeeSchema = z.object({ export const inviteEmployee = authActionClient .metadata({ - name: 'invite-employee', // Updated name + name: "invite-employee", // Updated name track: { - event: 'invite_employee', // Updated event name - channel: 'organization', + event: "invite_employee", // Updated event name + channel: "organization", }, }) .inputSchema(inviteEmployeeSchema) - .action(async ({ parsedInput, ctx }): Promise> => { - const organizationId = ctx.session.activeOrganizationId; - - if (!organizationId) { - return { - success: false, - error: 'Organization not found', - }; - } - - const { email } = parsedInput; // Role is removed from input - - try { - await authClient.organization.inviteMember({ - email, - role: 'employee', // Hardcoded role - }); - - // Revalidate the employees list page - revalidatePath(`/${organizationId}/people/all`); - revalidateTag(`user_${ctx.user.id}`); // Keep user tag revalidation - - return { - success: true, - data: { invited: true }, - }; - } catch (error) { - console.error('Error inviting employee:', error); - const errorMessage = error instanceof Error ? error.message : 'Failed to invite employee'; - return { - success: false, - error: errorMessage, - }; - } - }); + .action( + async ({ + parsedInput, + ctx, + }): Promise> => { + const organizationId = ctx.session.activeOrganizationId; + + if (!organizationId) { + return { + success: false, + error: "Organization not found", + }; + } + + const { email } = parsedInput; // Role is removed from input + + try { + await authClient.organization.inviteMember({ + email, + role: "employee", // Hardcoded role + }); + + // Revalidate the employees list page + revalidatePath(`/${organizationId}/people/all`); + revalidateTag(`user_${ctx.user.id}`, { expire: 0 }); // Keep user tag revalidation + + return { + success: true, + data: { invited: true }, + }; + } catch (error) { + console.error("Error inviting employee:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to invite employee"; + return { + success: false, + error: errorMessage, + }; + } + }, + ); diff --git a/apps/app/src/actions/organization/invite-member.ts b/apps/app/src/actions/organization/invite-member.ts index ea826671d..18443d242 100644 --- a/apps/app/src/actions/organization/invite-member.ts +++ b/apps/app/src/actions/organization/invite-member.ts @@ -1,56 +1,63 @@ -'use server'; +"use server"; -import { authClient } from '@/utils/auth-client'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; -import type { ActionResponse } from '../types'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { authClient } from "@/utils/auth-client"; +import { z } from "zod"; + +import type { ActionResponse } from "../types"; +import { authActionClient } from "../safe-action"; const inviteMemberSchema = z.object({ email: z.string().email(), - role: z.enum(['owner', 'admin', 'auditor', 'employee', 'contractor']), + role: z.enum(["owner", "admin", "auditor", "employee", "contractor"]), }); export const inviteMember = authActionClient .metadata({ - name: 'invite-member', + name: "invite-member", track: { - event: 'invite_member', - channel: 'organization', + event: "invite_member", + channel: "organization", }, }) .inputSchema(inviteMemberSchema) - .action(async ({ parsedInput, ctx }): Promise> => { - const organizationId = ctx.session.activeOrganizationId; - - if (!organizationId) { - return { - success: false, - error: 'Organization not found', - }; - } - - const { email, role } = parsedInput; - - try { - await authClient.organization.inviteMember({ - email, - role, - }); - - revalidatePath(`/${organizationId}/settings/users`); - revalidateTag(`user_${ctx.user.id}`); - - return { - success: true, - data: { invited: true }, - }; - } catch (error) { - console.error('Error inviting member:', error); - const errorMessage = error instanceof Error ? error.message : 'Failed to invite member'; - return { - success: false, - error: errorMessage, - }; - } - }); + .action( + async ({ + parsedInput, + ctx, + }): Promise> => { + const organizationId = ctx.session.activeOrganizationId; + + if (!organizationId) { + return { + success: false, + error: "Organization not found", + }; + } + + const { email, role } = parsedInput; + + try { + await authClient.organization.inviteMember({ + email, + role, + }); + + revalidatePath(`/${organizationId}/settings/users`); + revalidateTag(`user_${ctx.user.id}`, { expire: 0 }); + + return { + success: true, + data: { invited: true }, + }; + } catch (error) { + console.error("Error inviting member:", error); + const errorMessage = + error instanceof Error ? error.message : "Failed to invite member"; + return { + success: false, + error: errorMessage, + }; + } + }, + ); diff --git a/apps/app/src/actions/organization/lib/get-framework-names.ts b/apps/app/src/actions/organization/lib/get-framework-names.ts index 5e6b8eab8..05689ae75 100644 --- a/apps/app/src/actions/organization/lib/get-framework-names.ts +++ b/apps/app/src/actions/organization/lib/get-framework-names.ts @@ -1,13 +1,15 @@ -'use server'; +"use server"; -import { db } from '@db'; +import { db } from "@trycompai/db"; /** * Fetch framework names by IDs and convert them to lowercase with no spaces * @param frameworkIds - Array of framework IDs * @returns Array of framework names in lowercase with no spaces */ -export async function getFrameworkNames(frameworkIds: string[]): Promise { +export async function getFrameworkNames( + frameworkIds: string[], +): Promise { if (!frameworkIds || frameworkIds.length === 0) { return []; } @@ -27,8 +29,8 @@ export async function getFrameworkNames(frameworkIds: string[]): Promise "soc2", "ISO 27001" -> "iso27001", "GDPR" -> "gdpr" return framework.name .toLowerCase() - .replace(/\s+/g, '') // Remove all spaces - .replace(/[^a-z0-9]/g, ''); // Remove special characters + .replace(/\s+/g, "") // Remove all spaces + .replace(/[^a-z0-9]/g, ""); // Remove special characters }) .filter(Boolean); // Remove any empty strings } diff --git a/apps/app/src/actions/organization/lib/initialize-organization.ts b/apps/app/src/actions/organization/lib/initialize-organization.ts index 6eff295be..efa90b412 100644 --- a/apps/app/src/actions/organization/lib/initialize-organization.ts +++ b/apps/app/src/actions/organization/lib/initialize-organization.ts @@ -1,11 +1,12 @@ -import { db, Prisma } from '@db'; +import { db, Prisma } from "@trycompai/db"; // Define a type for FrameworkEditorFramework with requirements included // This assumes FrameworkEditorFramework and FrameworkEditorRequirement are valid Prisma types. // Adjust if your Prisma client exposes these differently (e.g., via Prisma.FrameworkEditorFrameworkGetPayload). -type FrameworkEditorFrameworkWithRequirements = Prisma.FrameworkEditorFrameworkGetPayload<{ - include: { requirements: true }; -}>; +type FrameworkEditorFrameworkWithRequirements = + Prisma.FrameworkEditorFrameworkGetPayload<{ + include: { requirements: true }; + }>; export type InitializeOrganizationInput = { frameworkIds: string[]; @@ -91,12 +92,16 @@ export const _upsertOrgFrameworkStructureCore = async ({ }, }); - const groupedControlTemplateRelations = controlRelations.map((controlTemplate) => ({ - controlTemplateId: controlTemplate.id, - requirementTemplateIds: controlTemplate.requirements.map((req) => req.id), - policyTemplateIds: controlTemplate.policyTemplates.map((policy) => policy.id), - taskTemplateIds: controlTemplate.taskTemplates.map((task) => task.id), - })); + const groupedControlTemplateRelations = controlRelations.map( + (controlTemplate) => ({ + controlTemplateId: controlTemplate.id, + requirementTemplateIds: controlTemplate.requirements.map((req) => req.id), + policyTemplateIds: controlTemplate.policyTemplates.map( + (policy) => policy.id, + ), + taskTemplateIds: controlTemplate.taskTemplates.map((task) => task.id), + }), + ); /** |-------------------------------------------------- @@ -120,7 +125,8 @@ export const _upsertOrgFrameworkStructureCore = async ({ const frameworkInstancesToCreateData = frameworkEditorFrameworks .filter( (f) => - targetFrameworkEditorIds.includes(f.id) && !existingFrameworkInstanceFrameworkIds.has(f.id), + targetFrameworkEditorIds.includes(f.id) && + !existingFrameworkInstanceFrameworkIds.has(f.id), ) .map((framework) => ({ organizationId: organizationId, @@ -157,7 +163,9 @@ export const _upsertOrgFrameworkStructureCore = async ({ select: { controlTemplateId: true }, }); const existingControlTemplateIdsSet = new Set( - existingControlsQuery.map((c) => c.controlTemplateId).filter((id) => id !== null) as string[], + existingControlsQuery + .map((c) => c.controlTemplateId) + .filter((id) => id !== null) as string[], ); const controlTemplatesForCreation = controlTemplates.filter( @@ -188,7 +196,9 @@ export const _upsertOrgFrameworkStructureCore = async ({ select: { policyTemplateId: true }, }); const existingPolicyTemplateIdsSet = new Set( - existingPoliciesQuery.map((p) => p.policyTemplateId).filter((id) => id !== null) as string[], + existingPoliciesQuery + .map((p) => p.policyTemplateId) + .filter((id) => id !== null) as string[], ); const policyTemplatesForCreation = policyTemplates.filter( @@ -202,7 +212,7 @@ export const _upsertOrgFrameworkStructureCore = async ({ description: policyTemplate.description, department: policyTemplate.department, frequency: policyTemplate.frequency, - content: policyTemplate.content as Prisma.PolicyCreateInput['content'], + content: policyTemplate.content as Prisma.PolicyCreateInput["content"], organizationId: organizationId, policyTemplateId: policyTemplate.id, })), @@ -222,7 +232,9 @@ export const _upsertOrgFrameworkStructureCore = async ({ select: { taskTemplateId: true }, }); const existingTaskTemplateIdsSet = new Set( - existingTasksQuery.map((t) => t.taskTemplateId).filter((id) => id !== null) as string[], + existingTasksQuery + .map((t) => t.taskTemplateId) + .filter((id) => id !== null) as string[], ); const taskTemplatesForCreation = taskTemplates.filter( @@ -281,10 +293,13 @@ export const _upsertOrgFrameworkStructureCore = async ({ .map((p) => [p.policyTemplateId!, p.id]), ); const taskTemplateIdToInstanceIdMap = new Map( - allRelevantTasks.filter((t) => t.taskTemplateId != null).map((t) => [t.taskTemplateId!, t.id]), + allRelevantTasks + .filter((t) => t.taskTemplateId != null) + .map((t) => [t.taskTemplateId!, t.id]), ); - const requirementMapEntriesToCreate: Prisma.RequirementMapCreateManyInput[] = []; + const requirementMapEntriesToCreate: Prisma.RequirementMapCreateManyInput[] = + []; for (const controlTemplateRelation of groupedControlTemplateRelations) { const newControlId = controlTemplateIdToInstanceIdMap.get( @@ -312,7 +327,9 @@ export const _upsertOrgFrameworkStructureCore = async ({ } } const frameworkInstanceId = frameworkEditorFrameworkIdForReq - ? editorFrameworkIdToInstanceIdMap.get(frameworkEditorFrameworkIdForReq) + ? editorFrameworkIdToInstanceIdMap.get( + frameworkEditorFrameworkIdForReq, + ) : undefined; if (frameworkInstanceId) { @@ -333,7 +350,8 @@ export const _upsertOrgFrameworkStructureCore = async ({ if (controlTemplateRelation.policyTemplateIds.length > 0) { const policiesToConnect = []; for (const policyTemplateId of controlTemplateRelation.policyTemplateIds) { - const newPolicyId = policyTemplateIdToInstanceIdMap.get(policyTemplateId); + const newPolicyId = + policyTemplateIdToInstanceIdMap.get(policyTemplateId); if (newPolicyId) { policiesToConnect.push({ id: newPolicyId }); } else { @@ -395,18 +413,20 @@ export const initializeOrganization = async ({ frameworkIds, organizationId, }: InitializeOrganizationInput) => { - const frameworksAndReqsToProcess = await db.frameworkEditorFramework.findMany({ - where: { - id: { in: frameworkIds }, - }, - include: { - requirements: true, + const frameworksAndReqsToProcess = await db.frameworkEditorFramework.findMany( + { + where: { + id: { in: frameworkIds }, + }, + include: { + requirements: true, + }, }, - }); + ); if (frameworksAndReqsToProcess.length === 0 && frameworkIds.length > 0) { console.warn( - `InitializeOrganization: No FrameworkEditorFrameworks found for IDs: ${frameworkIds.join(', ')}`, + `InitializeOrganization: No FrameworkEditorFrameworks found for IDs: ${frameworkIds.join(", ")}`, ); } diff --git a/apps/app/src/actions/organization/remove-employee.ts b/apps/app/src/actions/organization/remove-employee.ts index 602648a11..feb03810a 100644 --- a/apps/app/src/actions/organization/remove-employee.ts +++ b/apps/app/src/actions/organization/remove-employee.ts @@ -1,10 +1,12 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; -import type { ActionResponse } from '../types'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import type { ActionResponse } from "../types"; +import { authActionClient } from "../safe-action"; const removeEmployeeSchema = z.object({ memberId: z.string(), @@ -12,10 +14,10 @@ const removeEmployeeSchema = z.object({ export const removeEmployeeRoleOrMember = authActionClient .metadata({ - name: 'remove-employee-role-or-member', + name: "remove-employee-role-or-member", track: { - event: 'remove_employee', // Changed event name - channel: 'organization', + event: "remove_employee", // Changed event name + channel: "organization", }, }) .inputSchema(removeEmployeeSchema) @@ -23,14 +25,16 @@ export const removeEmployeeRoleOrMember = authActionClient async ({ parsedInput, ctx, - }): Promise> => { + }): Promise< + ActionResponse<{ removed: boolean; roleUpdated?: boolean }> + > => { const organizationId = ctx.session.activeOrganizationId; const currentUserId = ctx.user.id; if (!organizationId) { return { success: false, - error: 'Organization not found', + error: "Organization not found", }; } @@ -45,10 +49,14 @@ export const removeEmployeeRoleOrMember = authActionClient }, }); - if (!currentUserMember || !['admin', 'owner'].includes(currentUserMember.role)) { + if ( + !currentUserMember || + !["admin", "owner"].includes(currentUserMember.role) + ) { return { success: false, - error: 'Permission denied: Only admins or owners can remove employees.', + error: + "Permission denied: Only admins or owners can remove employees.", }; } @@ -63,35 +71,39 @@ export const removeEmployeeRoleOrMember = authActionClient if (!targetMember) { return { success: false, - error: 'Target employee not found in this organization.', + error: "Target employee not found in this organization.", }; } // 3. Check if target has 'employee' or 'contractor' role - const roles = targetMember.role.split(',').filter(Boolean); // Handle empty strings/commas - if (!roles.includes('employee') && !roles.includes('contractor')) { + const roles = targetMember.role.split(",").filter(Boolean); // Handle empty strings/commas + if (!roles.includes("employee") && !roles.includes("contractor")) { return { success: false, - error: 'Target member does not have the employee or contractor role.', + error: + "Target member does not have the employee or contractor role.", }; } // 4. Logic: Remove role or delete member - if (roles.length === 1 && (roles[0] === 'employee' || roles[0] === 'contractor')) { + if ( + roles.length === 1 && + (roles[0] === "employee" || roles[0] === "contractor") + ) { // Only has employee or contractor role - delete member fully // Cannot remove owner (shouldn't happen if only role is employee, but safety check) - if (targetMember.role === 'owner') { + if (targetMember.role === "owner") { return { success: false, - error: 'Cannot remove the organization owner.', + error: "Cannot remove the organization owner.", }; } // Cannot remove self if (targetMember.userId === currentUserId) { return { success: false, - error: 'You cannot remove yourself.', + error: "You cannot remove yourself.", }; } @@ -104,12 +116,14 @@ export const removeEmployeeRoleOrMember = authActionClient // Revalidate revalidatePath(`/${organizationId}/people/all`); - revalidateTag(`user_${currentUserId}`); + revalidateTag(`user_${currentUserId}`, { expire: 0 }); return { success: true, data: { removed: true } }; } else { // Has other roles - just remove 'employee' role - const updatedRoles = roles.filter((role) => role !== 'employee').join(','); + const updatedRoles = roles + .filter((role) => role !== "employee") + .join(","); await db.member.update({ where: { id: memberId }, @@ -118,7 +132,7 @@ export const removeEmployeeRoleOrMember = authActionClient // Revalidate revalidatePath(`/${organizationId}/people/all`); - revalidateTag(`user_${currentUserId}`); + revalidateTag(`user_${currentUserId}`, { expire: 0 }); return { success: true, @@ -126,9 +140,11 @@ export const removeEmployeeRoleOrMember = authActionClient }; } } catch (error) { - console.error('Error removing employee role/member:', error); + console.error("Error removing employee role/member:", error); const errorMessage = - error instanceof Error ? error.message : 'Failed to remove employee role or member.'; + error instanceof Error + ? error.message + : "Failed to remove employee role or member."; return { success: false, error: errorMessage, diff --git a/apps/app/src/actions/organization/revoke-api-key-action.ts b/apps/app/src/actions/organization/revoke-api-key-action.ts index f3a4d6e6c..7419339e3 100644 --- a/apps/app/src/actions/organization/revoke-api-key-action.ts +++ b/apps/app/src/actions/organization/revoke-api-key-action.ts @@ -1,9 +1,10 @@ -'use server'; +"use server"; -import { authActionClient } from '@/actions/safe-action'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; +import { revalidatePath } from "next/cache"; +import { authActionClient } from "@/actions/safe-action"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; const revokeApiKeySchema = z.object({ id: z.string().min(1), @@ -12,10 +13,10 @@ const revokeApiKeySchema = z.object({ export const revokeApiKeyAction = authActionClient .inputSchema(revokeApiKeySchema) .metadata({ - name: 'revokeApiKey', + name: "revokeApiKey", track: { - event: 'revokeApiKey', - channel: 'server', + event: "revokeApiKey", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -35,7 +36,7 @@ export const revokeApiKeyAction = authActionClient if (result.count === 0) { return { success: false, - error: 'API key not found or not authorized to revoke', + error: "API key not found or not authorized to revoke", }; } @@ -43,13 +44,13 @@ export const revokeApiKeyAction = authActionClient return { success: true, - message: 'API key revoked successfully', + message: "API key revoked successfully", }; } catch (error) { - console.error('Error revoking API key:', error); + console.error("Error revoking API key:", error); return { success: false, - error: 'An error occurred while revoking the API key', + error: "An error occurred while revoking the API key", }; } }); diff --git a/apps/app/src/actions/organization/update-organization-advanced-mode-action.ts b/apps/app/src/actions/organization/update-organization-advanced-mode-action.ts index 0044f1a30..e008aab7d 100644 --- a/apps/app/src/actions/organization/update-organization-advanced-mode-action.ts +++ b/apps/app/src/actions/organization/update-organization-advanced-mode-action.ts @@ -1,18 +1,20 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { headers } from 'next/headers'; -import { authActionClient } from '../safe-action'; -import { organizationAdvancedModeSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { headers } from "next/headers"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { organizationAdvancedModeSchema } from "../schema"; export const updateOrganizationAdvancedModeAction = authActionClient .inputSchema(organizationAdvancedModeSchema) .metadata({ - name: 'update-organization-advanced-mode', + name: "update-organization-advanced-mode", track: { - event: 'update-organization-advanced-mode', - channel: 'server', + event: "update-organization-advanced-mode", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -20,7 +22,7 @@ export const updateOrganizationAdvancedModeAction = authActionClient const { activeOrganizationId } = ctx.session; if (!activeOrganizationId) { - throw new Error('No active organization'); + throw new Error("No active organization"); } try { @@ -32,17 +34,18 @@ export const updateOrganizationAdvancedModeAction = authActionClient }); const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); - revalidateTag(`organization_${activeOrganizationId}`); + revalidateTag(`organization_${activeOrganizationId}`, { expire: 0 }); return { success: true, }; } catch (error) { console.error(error); - throw new Error('Failed to update advanced mode setting'); + throw new Error("Failed to update advanced mode setting"); } }); diff --git a/apps/app/src/actions/organization/update-organization-name-action.ts b/apps/app/src/actions/organization/update-organization-name-action.ts index 24b402f0c..98da5152c 100644 --- a/apps/app/src/actions/organization/update-organization-name-action.ts +++ b/apps/app/src/actions/organization/update-organization-name-action.ts @@ -1,19 +1,21 @@ // update-organization-name-action.ts -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { organizationNameSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { organizationNameSchema } from "../schema"; export const updateOrganizationNameAction = authActionClient .inputSchema(organizationNameSchema) .metadata({ - name: 'update-organization-name', + name: "update-organization-name", track: { - event: 'update-organization-name', - channel: 'server', + event: "update-organization-name", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -21,29 +23,29 @@ export const updateOrganizationNameAction = authActionClient const { activeOrganizationId } = ctx.session; if (!name) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } if (!activeOrganizationId) { - throw new Error('No active organization'); + throw new Error("No active organization"); } try { await db.$transaction(async () => { await db.organization.update({ - where: { id: activeOrganizationId ?? '' }, + where: { id: activeOrganizationId ?? "" }, data: { name }, }); }); - revalidatePath('/settings'); - revalidateTag(`organization_${activeOrganizationId}`); + revalidatePath("/settings"); + revalidateTag(`organization_${activeOrganizationId}`, { expire: 0 }); return { success: true, }; } catch (error) { console.error(error); - throw new Error('Failed to update organization name'); + throw new Error("Failed to update organization name"); } }); diff --git a/apps/app/src/actions/organization/update-organization-website-action.ts b/apps/app/src/actions/organization/update-organization-website-action.ts index fbcf37869..686c7631c 100644 --- a/apps/app/src/actions/organization/update-organization-website-action.ts +++ b/apps/app/src/actions/organization/update-organization-website-action.ts @@ -1,19 +1,21 @@ // update-organization-name-action.ts -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { organizationWebsiteSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { organizationWebsiteSchema } from "../schema"; export const updateOrganizationWebsiteAction = authActionClient .inputSchema(organizationWebsiteSchema) .metadata({ - name: 'update-organization-website', + name: "update-organization-website", track: { - event: 'update-organization-website', - channel: 'server', + event: "update-organization-website", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -21,29 +23,29 @@ export const updateOrganizationWebsiteAction = authActionClient const { activeOrganizationId } = ctx.session; if (!website) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } if (!activeOrganizationId) { - throw new Error('No active organization'); + throw new Error("No active organization"); } try { await db.$transaction(async () => { await db.organization.update({ - where: { id: activeOrganizationId ?? '' }, + where: { id: activeOrganizationId ?? "" }, data: { website }, }); }); - revalidatePath('/settings'); - revalidateTag(`organization_${activeOrganizationId}`); + revalidatePath("/settings"); + revalidateTag(`organization_${activeOrganizationId}`, { expire: 0 }); return { success: true, }; } catch (error) { console.error(error); - throw new Error('Failed to update organization website'); + throw new Error("Failed to update organization website"); } }); diff --git a/apps/app/src/actions/people/create-employee-action.ts b/apps/app/src/actions/people/create-employee-action.ts index 2717cfdef..78c2a34db 100644 --- a/apps/app/src/actions/people/create-employee-action.ts +++ b/apps/app/src/actions/people/create-employee-action.ts @@ -1,18 +1,19 @@ -'use server'; +"use server"; -import { completeEmployeeCreation } from '@/lib/db/employee'; -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'; -import { authActionClient } from '../safe-action'; -import { createEmployeeSchema } from '../schema'; -import type { ActionResponse } from '../types'; +import { completeEmployeeCreation } from "@/lib/db/employee"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; + +import type { ActionResponse } from "../types"; +import { authActionClient } from "../safe-action"; +import { createEmployeeSchema } from "../schema"; export const createEmployeeAction = authActionClient .inputSchema(createEmployeeSchema) .metadata({ - name: 'create-employee', + name: "create-employee", track: { - event: 'create-employee', - channel: 'server', + event: "create-employee", + channel: "server", }, }) .action(async ({ parsedInput, ctx }): Promise => { @@ -22,7 +23,7 @@ export const createEmployeeAction = authActionClient if (!session.activeOrganizationId) { return { success: false, - error: 'Not authorized - no organization found', + error: "Not authorized - no organization found", }; } @@ -40,18 +41,23 @@ export const createEmployeeAction = authActionClient data: employee, }; } catch (error) { - console.error('Error creating employee:', error); + console.error("Error creating employee:", error); - if (error instanceof PrismaClientKnownRequestError && error.code === 'P2002') { + if ( + error instanceof PrismaClientKnownRequestError && + error.code === "P2002" + ) { return { success: false, - error: 'An employee with this email already exists in your organization', + error: + "An employee with this email already exists in your organization", }; } return { success: false, - error: error instanceof Error ? error.message : 'Failed to create employee', + error: + error instanceof Error ? error.message : "Failed to create employee", }; } }); diff --git a/apps/app/src/actions/policies/accept-requested-policy-changes.ts b/apps/app/src/actions/policies/accept-requested-policy-changes.ts index f60e30acc..6f08f6fc5 100644 --- a/apps/app/src/actions/policies/accept-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/accept-requested-policy-changes.ts @@ -1,11 +1,13 @@ -'use server'; +"use server"; -import { sendNewPolicyEmail } from '@/jobs/tasks/email/new-policy-email'; -import { db, PolicyStatus } from '@db'; -import { tasks } from '@trigger.dev/sdk'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { sendNewPolicyEmail } from "@/jobs/tasks/email/new-policy-email"; +import { tasks } from "@trigger.dev/sdk"; +import { z } from "zod"; + +import { db, PolicyStatus } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; const acceptRequestedPolicyChangesSchema = z.object({ id: z.string(), @@ -17,11 +19,11 @@ const acceptRequestedPolicyChangesSchema = z.object({ export const acceptRequestedPolicyChangesAction = authActionClient .inputSchema(acceptRequestedPolicyChangesSchema) .metadata({ - name: 'accept-requested-policy-changes', + name: "accept-requested-policy-changes", track: { - event: 'accept-requested-policy-changes', - description: 'Accept Policy Changes', - channel: 'server', + event: "accept-requested-policy-changes", + description: "Accept Policy Changes", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -29,11 +31,11 @@ export const acceptRequestedPolicyChangesAction = authActionClient const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } if (!approverId) { - throw new Error('Approver is required'); + throw new Error("Approver is required"); } try { @@ -52,11 +54,11 @@ export const acceptRequestedPolicyChangesAction = authActionClient }); if (!policy) { - throw new Error('Policy not found'); + throw new Error("Policy not found"); } if (policy.approverId !== approverId) { - throw new Error('Approver is not the same'); + throw new Error("Approver is not the same"); } // Check if there were previous signers to determine notification type @@ -90,29 +92,31 @@ export const acceptRequestedPolicyChangesAction = authActionClient // Filter to get only employees and contractors const employeeMembers = employees.filter((member) => { - const roles = member.role.includes(',') ? member.role.split(',') : [member.role]; - return roles.includes('employee') || roles.includes('contractor'); + const roles = member.role.includes(",") + ? member.role.split(",") + : [member.role]; + return roles.includes("employee") || roles.includes("contractor"); }); // Prepare the events array for the API const events = employeeMembers .filter((employee) => employee.user.email) .map((employee) => { - let notificationType: 'new' | 're-acceptance' | 'updated'; + let notificationType: "new" | "re-acceptance" | "updated"; const wasAlreadySigned = policy.signedBy.includes(employee.id); if (isNewPolicy) { - notificationType = 'new'; + notificationType = "new"; } else if (wasAlreadySigned) { - notificationType = 're-acceptance'; + notificationType = "re-acceptance"; } else { - notificationType = 'updated'; + notificationType = "updated"; } return { email: employee.user.email, - userName: employee.user.name || employee.user.email || 'Employee', + userName: employee.user.name || employee.user.email || "Employee", policyName: policy.name, - organizationId: session.activeOrganizationId || '', + organizationId: session.activeOrganizationId || "", organizationName: policy.organization.name, notificationType, }; @@ -121,12 +125,15 @@ export const acceptRequestedPolicyChangesAction = authActionClient // Call the API route to send the emails await Promise.all( events.map((event) => - tasks.trigger('send-new-policy-email', event), + tasks.trigger( + "send-new-policy-email", + event, + ), ), ); // If a comment was provided, create a comment - if (comment && comment.trim() !== '') { + if (comment && comment.trim() !== "") { const member = await db.member.findFirst({ where: { userId: user.id, @@ -139,7 +146,7 @@ export const acceptRequestedPolicyChangesAction = authActionClient data: { content: `Policy changes accepted: ${comment}`, entityId: id, - entityType: 'policy', + entityType: "policy", organizationId: session.activeOrganizationId, authorId: member.id, }, @@ -149,13 +156,13 @@ export const acceptRequestedPolicyChangesAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/policies`); revalidatePath(`/${session.activeOrganizationId}/policies/${id}`); - revalidateTag('policies'); + revalidateTag("policies", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error submitting policy for approval:', error); + console.error("Error submitting policy for approval:", error); return { success: false, diff --git a/apps/app/src/actions/policies/archive-policy.ts b/apps/app/src/actions/policies/archive-policy.ts index 48dd1bf26..af9dc533f 100644 --- a/apps/app/src/actions/policies/archive-policy.ts +++ b/apps/app/src/actions/policies/archive-policy.ts @@ -1,24 +1,26 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; const archivePolicySchema = z.object({ id: z.string(), - action: z.enum(['archive', 'restore']).optional(), + action: z.enum(["archive", "restore"]).optional(), entityId: z.string(), }); export const archivePolicyAction = authActionClient .inputSchema(archivePolicySchema) .metadata({ - name: 'archive-policy', + name: "archive-policy", track: { - event: 'archive-policy', - description: 'Archive Policy', - channel: 'server', + event: "archive-policy", + description: "Archive Policy", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -28,7 +30,7 @@ export const archivePolicyAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -43,12 +45,13 @@ export const archivePolicyAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: "Policy not found", }; } // Determine if we should archive or restore based on action or current state - const shouldArchive = action === 'archive' || (action === undefined && !policy.isArchived); + const shouldArchive = + action === "archive" || (action === undefined && !policy.isArchived); await db.policy.update({ where: { id }, @@ -60,7 +63,7 @@ export const archivePolicyAction = authActionClient revalidatePath(`/${activeOrganizationId}/policies/${id}`); revalidatePath(`/${activeOrganizationId}/policies/all`); revalidatePath(`/${activeOrganizationId}/policies`); - revalidateTag('policies'); + revalidateTag("policies", { expire: 0 }); return { success: true, @@ -70,7 +73,7 @@ export const archivePolicyAction = authActionClient console.error(error); return { success: false, - error: 'Failed to update policy archive status', + error: "Failed to update policy archive status", }; } }); diff --git a/apps/app/src/actions/policies/create-new-policy.ts b/apps/app/src/actions/policies/create-new-policy.ts index 9f7e75a82..92654fd31 100644 --- a/apps/app/src/actions/policies/create-new-policy.ts +++ b/apps/app/src/actions/policies/create-new-policy.ts @@ -1,18 +1,20 @@ -'use server'; +"use server"; -import { db, Departments, Frequency } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { createPolicySchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db, Departments, Frequency } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { createPolicySchema } from "../schema"; export const createPolicyAction = authActionClient .inputSchema(createPolicySchema) .metadata({ - name: 'create-policy', + name: "create-policy", track: { - event: 'create-policy', - description: 'Create New Policy', - channel: 'server', + event: "create-policy", + description: "Create New Policy", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -23,14 +25,14 @@ export const createPolicyAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } if (!user) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -45,7 +47,7 @@ export const createPolicyAction = authActionClient if (!member) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -61,8 +63,8 @@ export const createPolicyAction = authActionClient frequency: Frequency.monthly, content: [ { - type: 'paragraph', - content: [{ type: 'text', text: '' }], + type: "paragraph", + content: [{ type: "text", text: "" }], }, ], ...(controlIds && @@ -105,7 +107,7 @@ export const createPolicyAction = authActionClient revalidatePath(`/${activeOrganizationId}/policies/all`); revalidatePath(`/${activeOrganizationId}/policies`); - revalidateTag('policies'); + revalidateTag("policies", { expire: 0 }); return { success: true, @@ -116,7 +118,7 @@ export const createPolicyAction = authActionClient return { success: false, - error: 'Failed to create policy', + error: "Failed to create policy", }; } }); diff --git a/apps/app/src/actions/policies/delete-policy.ts b/apps/app/src/actions/policies/delete-policy.ts index b836aa26f..23ae8659a 100644 --- a/apps/app/src/actions/policies/delete-policy.ts +++ b/apps/app/src/actions/policies/delete-policy.ts @@ -1,9 +1,11 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; const deletePolicySchema = z.object({ id: z.string(), @@ -13,11 +15,11 @@ const deletePolicySchema = z.object({ export const deletePolicyAction = authActionClient .inputSchema(deletePolicySchema) .metadata({ - name: 'delete-policy', + name: "delete-policy", track: { - event: 'delete-policy', - description: 'Delete Policy', - channel: 'server', + event: "delete-policy", + description: "Delete Policy", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -27,7 +29,7 @@ export const deletePolicyAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -42,7 +44,7 @@ export const deletePolicyAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: "Policy not found", }; } @@ -53,12 +55,12 @@ export const deletePolicyAction = authActionClient // Revalidate paths to update UI revalidatePath(`/${activeOrganizationId}/policies/all`); - revalidateTag('policies'); + revalidateTag("policies", { expire: 0 }); } catch (error) { console.error(error); return { success: false, - error: 'Failed to delete policy', + error: "Failed to delete policy", }; } }); diff --git a/apps/app/src/actions/policies/deny-requested-policy-changes.ts b/apps/app/src/actions/policies/deny-requested-policy-changes.ts index 5c937edbe..e217bc142 100644 --- a/apps/app/src/actions/policies/deny-requested-policy-changes.ts +++ b/apps/app/src/actions/policies/deny-requested-policy-changes.ts @@ -1,9 +1,11 @@ -'use server'; +"use server"; -import { db, PolicyStatus } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { z } from "zod"; + +import { db, PolicyStatus } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; const denyRequestedPolicyChangesSchema = z.object({ id: z.string(), @@ -15,11 +17,11 @@ const denyRequestedPolicyChangesSchema = z.object({ export const denyRequestedPolicyChangesAction = authActionClient .inputSchema(denyRequestedPolicyChangesSchema) .metadata({ - name: 'deny-requested-policy-changes', + name: "deny-requested-policy-changes", track: { - event: 'deny-requested-policy-changes', - description: 'Deny Policy Changes', - channel: 'server', + event: "deny-requested-policy-changes", + description: "Deny Policy Changes", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -27,11 +29,11 @@ export const denyRequestedPolicyChangesAction = authActionClient const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } if (!approverId) { - throw new Error('Approver is required'); + throw new Error("Approver is required"); } try { @@ -43,11 +45,11 @@ export const denyRequestedPolicyChangesAction = authActionClient }); if (!policy) { - throw new Error('Policy not found'); + throw new Error("Policy not found"); } if (policy.approverId !== approverId) { - throw new Error('Approver is not the same'); + throw new Error("Approver is not the same"); } // Update policy status @@ -63,7 +65,7 @@ export const denyRequestedPolicyChangesAction = authActionClient }); // If a comment was provided, create a comment - if (comment && comment.trim() !== '') { + if (comment && comment.trim() !== "") { const member = await db.member.findFirst({ where: { userId: user.id, @@ -76,7 +78,7 @@ export const denyRequestedPolicyChangesAction = authActionClient data: { content: `Policy changes denied: ${comment}`, entityId: id, - entityType: 'policy', + entityType: "policy", organizationId: session.activeOrganizationId, authorId: member.id, }, @@ -86,13 +88,13 @@ export const denyRequestedPolicyChangesAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/policies`); revalidatePath(`/${session.activeOrganizationId}/policies/${id}`); - revalidateTag('policies'); + revalidateTag("policies", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error submitting policy for approval:', error); + console.error("Error submitting policy for approval:", error); return { success: false, diff --git a/apps/app/src/actions/policies/publish-all.ts b/apps/app/src/actions/policies/publish-all.ts index 8ce0cdf79..c1f8e5bf6 100644 --- a/apps/app/src/actions/policies/publish-all.ts +++ b/apps/app/src/actions/policies/publish-all.ts @@ -1,10 +1,12 @@ -'use server'; +"use server"; -import { sendPublishAllPoliciesEmail } from '@/jobs/tasks/email/publish-all-policies-email'; -import { db, PolicyStatus, Role } from '@db'; -import { revalidatePath } from 'next/cache'; -import { z } from 'zod'; -import { authActionClient } from '../safe-action'; +import { revalidatePath } from "next/cache"; +import { sendPublishAllPoliciesEmail } from "@/jobs/tasks/email/publish-all-policies-email"; +import { z } from "zod"; + +import { db, PolicyStatus, Role } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; const publishAllPoliciesSchema = z.object({ organizationId: z.string(), @@ -13,11 +15,11 @@ const publishAllPoliciesSchema = z.object({ export const publishAllPoliciesAction = authActionClient .inputSchema(publishAllPoliciesSchema) .metadata({ - name: 'publish-all-policies', + name: "publish-all-policies", track: { - event: 'publish-all-policies', - description: 'Publish All Policies', - channel: 'server', + event: "publish-all-policies", + description: "Publish All Policies", + channel: "server", }, }) .action(async ({ ctx, parsedInput }) => { @@ -26,14 +28,14 @@ export const publishAllPoliciesAction = authActionClient if (!user) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } if (!session.activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -47,28 +49,31 @@ export const publishAllPoliciesAction = authActionClient if (!member) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } // Check if user is an owner - if (!member.role.includes('owner')) { - console.log('[publish-all-policies] User is not an owner'); + if (!member.role.includes("owner")) { + console.log("[publish-all-policies] User is not an owner"); return { success: false, - error: 'Only organization owners can publish all policies', + error: "Only organization owners can publish all policies", }; } try { const policies = await db.policy.findMany({ - where: { organizationId: parsedInput.organizationId, status: PolicyStatus.draft }, + where: { + organizationId: parsedInput.organizationId, + status: PolicyStatus.draft, + }, }); if (!policies || policies.length === 0) { return { success: false, - error: 'No policies found', + error: "No policies found", }; } @@ -79,17 +84,22 @@ export const publishAllPoliciesAction = authActionClient data: { status: PolicyStatus.published, assigneeId: member.id, - reviewDate: new Date(new Date().setDate(new Date().getDate() + 90)), + reviewDate: new Date( + new Date().setDate(new Date().getDate() + 90), + ), }, }); } catch (policyError) { - console.error(`[publish-all-policies] Failed to update policy ${policy.id}:`, { - error: policyError, - policyId: policy.id, - policyName: policy.name, - memberId: member.id, - organizationId: parsedInput.organizationId, - }); + console.error( + `[publish-all-policies] Failed to update policy ${policy.id}:`, + { + error: policyError, + policyId: policy.id, + policyName: policy.name, + memberId: member.id, + organizationId: parsedInput.organizationId, + }, + ); throw policyError; // Re-throw to be caught by outer catch block } } @@ -125,8 +135,8 @@ export const publishAllPoliciesAction = authActionClient .map((orgMember) => ({ payload: { email: orgMember.user.email, - userName: orgMember.user.name || 'there', - organizationName: organization?.name || 'Your organization', + userName: orgMember.user.name || "there", + organizationName: organization?.name || "Your organization", organizationId: parsedInput.organizationId, }, })); @@ -135,7 +145,10 @@ export const publishAllPoliciesAction = authActionClient try { await sendPublishAllPoliciesEmail.batchTrigger(emailPayloads); } catch (emailError) { - console.error('[publish-all-policies] Failed to trigger bulk emails:', emailError); + console.error( + "[publish-all-policies] Failed to trigger bulk emails:", + emailError, + ); // Don't throw - the policies are published successfully } } @@ -146,18 +159,22 @@ export const publishAllPoliciesAction = authActionClient success: true, }; } catch (error) { - console.error('[publish-all-policies] Error in publish all policies action:', { - error, - errorMessage: error instanceof Error ? error.message : 'Unknown error', - errorStack: error instanceof Error ? error.stack : undefined, - userId: user?.id, - memberId: member?.id, - organizationId: parsedInput.organizationId, - }); + console.error( + "[publish-all-policies] Error in publish all policies action:", + { + error, + errorMessage: + error instanceof Error ? error.message : "Unknown error", + errorStack: error instanceof Error ? error.stack : undefined, + userId: user?.id, + memberId: member?.id, + organizationId: parsedInput.organizationId, + }, + ); return { success: false, - error: 'Failed to publish all policies', + error: "Failed to publish all policies", }; } }); diff --git a/apps/app/src/actions/policies/submit-policy-for-approval-action.ts b/apps/app/src/actions/policies/submit-policy-for-approval-action.ts index ad25e71ca..164cf6ce7 100644 --- a/apps/app/src/actions/policies/submit-policy-for-approval-action.ts +++ b/apps/app/src/actions/policies/submit-policy-for-approval-action.ts @@ -1,30 +1,39 @@ -'use server'; +"use server"; -import { db, PolicyStatus } from '@db'; -import { revalidatePath } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updatePolicyFormSchema } from '../schema'; +import { revalidatePath } from "next/cache"; + +import { db, PolicyStatus } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updatePolicyFormSchema } from "../schema"; export const submitPolicyForApprovalAction = authActionClient .inputSchema(updatePolicyFormSchema) .metadata({ - name: 'submit-policy-for-approval', + name: "submit-policy-for-approval", track: { - event: 'submit-policy-for-approval', - description: 'Submit Policy for Approval', - channel: 'server', + event: "submit-policy-for-approval", + description: "Submit Policy for Approval", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { - const { id, assigneeId, department, review_frequency, review_date, approverId } = parsedInput; + const { + id, + assigneeId, + department, + review_frequency, + review_date, + approverId, + } = parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } if (!approverId) { - throw new Error('Approver is required'); + throw new Error("Approver is required"); } try { @@ -51,7 +60,7 @@ export const submitPolicyForApprovalAction = authActionClient success: true, }; } catch (error) { - console.error('Error submitting policy for approval:', error); + console.error("Error submitting policy for approval:", error); return { success: false, diff --git a/apps/app/src/actions/policies/update-policy-action.ts b/apps/app/src/actions/policies/update-policy-action.ts index 41deb66a0..65b98a73b 100644 --- a/apps/app/src/actions/policies/update-policy-action.ts +++ b/apps/app/src/actions/policies/update-policy-action.ts @@ -1,10 +1,12 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { logger } from '@trigger.dev/sdk'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updatePolicySchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { logger } from "@trigger.dev/sdk"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updatePolicySchema } from "../schema"; interface ContentNode { type: string; @@ -16,7 +18,9 @@ interface ContentNode { } // Simplified content processor that creates a new plain object -function processContent(content: ContentNode | ContentNode[]): ContentNode | ContentNode[] { +function processContent( + content: ContentNode | ContentNode[], +): ContentNode | ContentNode[] { if (!content) return content; // Handle arrays @@ -54,11 +58,11 @@ function processContent(content: ContentNode | ContentNode[]): ContentNode | Con export const updatePolicyAction = authActionClient .inputSchema(updatePolicySchema) .metadata({ - name: 'update-policy', + name: "update-policy", track: { - event: 'update-policy', - description: 'Update Policy', - channel: 'server', + event: "update-policy", + description: "Update Policy", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -69,14 +73,14 @@ export const updatePolicyAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } if (!user) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -88,12 +92,14 @@ export const updatePolicyAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: "Policy not found", }; } // Create a new plain object from the content - const processedContent = JSON.parse(JSON.stringify(processContent(content as ContentNode))); + const processedContent = JSON.parse( + JSON.stringify(processContent(content as ContentNode)), + ); await db.policy.update({ where: { id }, @@ -102,20 +108,21 @@ export const updatePolicyAction = authActionClient revalidatePath(`/${activeOrganizationId}/policies/${id}`); revalidatePath(`/${activeOrganizationId}/policies`); - revalidateTag(`user_${user.id}`); + revalidateTag(`user_${user.id}`, { expire: 0 }); return { success: true, }; } catch (error) { - logger.error('Error updating policy:', { + logger.error("Error updating policy:", { error, - errorMessage: error instanceof Error ? error.message : 'Unknown error', + errorMessage: error instanceof Error ? error.message : "Unknown error", errorStack: error instanceof Error ? error.stack : undefined, }); return { success: false, - error: error instanceof Error ? error.message : 'Failed to update policy', + error: + error instanceof Error ? error.message : "Failed to update policy", }; } }); diff --git a/apps/app/src/actions/policies/update-policy-form-action.ts b/apps/app/src/actions/policies/update-policy-form-action.ts index 3edac74d0..add373694 100644 --- a/apps/app/src/actions/policies/update-policy-form-action.ts +++ b/apps/app/src/actions/policies/update-policy-form-action.ts @@ -1,24 +1,29 @@ // update-policy-form-action.ts -'use server'; +"use server"; -import { db, PolicyStatus } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updatePolicyFormSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db, PolicyStatus } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updatePolicyFormSchema } from "../schema"; // Helper function to calculate next review date based on frequency -function calculateNextReviewDate(frequency: string, baseDate: Date = new Date()): Date { +function calculateNextReviewDate( + frequency: string, + baseDate: Date = new Date(), +): Date { const nextDate = new Date(baseDate); switch (frequency) { - case 'monthly': + case "monthly": nextDate.setMonth(nextDate.getMonth() + 1); break; - case 'quarterly': + case "quarterly": nextDate.setMonth(nextDate.getMonth() + 3); break; - case 'yearly': + case "yearly": nextDate.setFullYear(nextDate.getFullYear() + 1); break; default: @@ -32,19 +37,26 @@ function calculateNextReviewDate(frequency: string, baseDate: Date = new Date()) export const updatePolicyFormAction = authActionClient .inputSchema(updatePolicyFormSchema) .metadata({ - name: 'update-policy-form', + name: "update-policy-form", track: { - event: 'update-policy-form', - description: 'Update Policy', - channel: 'server', + event: "update-policy-form", + description: "Update Policy", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { - const { id, status, assigneeId, department, review_frequency, review_date } = parsedInput; + const { + id, + status, + assigneeId, + department, + review_frequency, + review_date, + } = parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } try { @@ -64,7 +76,10 @@ export const updatePolicyFormAction = authActionClient let lastPublishedAt = undefined; // If status is changing to 'published', calculate next review date based on frequency - if (status === PolicyStatus.published && currentPolicy?.status !== PolicyStatus.published) { + if ( + status === PolicyStatus.published && + currentPolicy?.status !== PolicyStatus.published + ) { reviewDate = calculateNextReviewDate(review_frequency); lastPublishedAt = new Date(); // Set lastPublishedAt to now when publishing } @@ -86,13 +101,13 @@ export const updatePolicyFormAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/policies`); revalidatePath(`/${session.activeOrganizationId}/policies/${id}`); - revalidateTag('policies'); + revalidateTag("policies", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error updating policy:', error); + console.error("Error updating policy:", error); return { success: false, diff --git a/apps/app/src/actions/policies/update-policy-overview-action.ts b/apps/app/src/actions/policies/update-policy-overview-action.ts index 9445d40c1..98055e996 100644 --- a/apps/app/src/actions/policies/update-policy-overview-action.ts +++ b/apps/app/src/actions/policies/update-policy-overview-action.ts @@ -1,20 +1,22 @@ // update-policy-overview-action.ts -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updatePolicyOverviewSchema } from '../schema'; +import { revalidatePath } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updatePolicyOverviewSchema } from "../schema"; export const updatePolicyOverviewAction = authActionClient .inputSchema(updatePolicyOverviewSchema) .metadata({ - name: 'update-policy-overview', + name: "update-policy-overview", track: { - event: 'update-policy-overview', - description: 'Update Policy', - channel: 'server', + event: "update-policy-overview", + description: "Update Policy", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -24,14 +26,14 @@ export const updatePolicyOverviewAction = authActionClient if (!user) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } if (!session.activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } @@ -43,7 +45,7 @@ export const updatePolicyOverviewAction = authActionClient if (!policy) { return { success: false, - error: 'Policy not found', + error: "Policy not found", }; } @@ -65,7 +67,7 @@ export const updatePolicyOverviewAction = authActionClient } catch (error) { return { success: false, - error: 'Failed to update policy overview', + error: "Failed to update policy overview", }; } }); diff --git a/apps/app/src/actions/research-vendor.ts b/apps/app/src/actions/research-vendor.ts index 8ac04c87c..5cc7401dc 100644 --- a/apps/app/src/actions/research-vendor.ts +++ b/apps/app/src/actions/research-vendor.ts @@ -1,18 +1,19 @@ -'use server'; +"use server"; -import { researchVendor } from '@/jobs/tasks/scrape/research'; -import { tasks } from '@trigger.dev/sdk'; -import { z } from 'zod'; -import { authActionClient } from './safe-action'; +import { researchVendor } from "@/jobs/tasks/scrape/research"; +import { tasks } from "@trigger.dev/sdk"; +import { z } from "zod"; + +import { authActionClient } from "./safe-action"; export const researchVendorAction = authActionClient .inputSchema( z.object({ - website: z.string().url({ message: 'Invalid URL format' }), + website: z.string().url({ message: "Invalid URL format" }), }), ) .metadata({ - name: 'research-vendor', + name: "research-vendor", }) .action(async ({ parsedInput: { website }, ctx: { session } }) => { try { @@ -21,25 +22,31 @@ export const researchVendorAction = authActionClient if (!activeOrganizationId) { return { success: false, - error: 'Not authorized', + error: "Not authorized", }; } - const handle = await tasks.trigger('research-vendor', { - website, - }); + const handle = await tasks.trigger( + "research-vendor", + { + website, + }, + ); return { success: true, handle, }; } catch (error) { - console.error('Error in researchVendorAction:', error); + console.error("Error in researchVendorAction:", error); return { success: false, error: { - message: error instanceof Error ? error.message : 'An unexpected error occurred.', + message: + error instanceof Error + ? error.message + : "An unexpected error occurred.", }, }; } diff --git a/apps/app/src/actions/risk/create-risk-action.ts b/apps/app/src/actions/risk/create-risk-action.ts index 29eb5a03c..43614769d 100644 --- a/apps/app/src/actions/risk/create-risk-action.ts +++ b/apps/app/src/actions/risk/create-risk-action.ts @@ -1,27 +1,30 @@ // create-risk-action.ts -'use server'; +"use server"; -import { db, Impact, Likelihood } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { createRiskSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db, Impact, Likelihood } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { createRiskSchema } from "../schema"; export const createRiskAction = authActionClient .inputSchema(createRiskSchema) .metadata({ - name: 'create-risk', + name: "create-risk", track: { - event: 'create-risk', - channel: 'server', + event: "create-risk", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { - const { title, description, category, department, assigneeId } = parsedInput; + const { title, description, category, department, assigneeId } = + parsedInput; const { user, session } = ctx; if (!user.id || !session.activeOrganizationId) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } try { @@ -40,7 +43,7 @@ export const createRiskAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/risk`); revalidatePath(`/${session.activeOrganizationId}/risk/register`); - revalidateTag(`risk_${session.activeOrganizationId}`); + revalidateTag(`risk_${session.activeOrganizationId}`, { expire: 0 }); return { success: true, diff --git a/apps/app/src/actions/risk/task/revalidate-upload.ts b/apps/app/src/actions/risk/task/revalidate-upload.ts index f25f3a5fc..29bf16fa4 100644 --- a/apps/app/src/actions/risk/task/revalidate-upload.ts +++ b/apps/app/src/actions/risk/task/revalidate-upload.ts @@ -1,16 +1,16 @@ -'use server'; +"use server"; -import { authActionClient } from '@/actions/safe-action'; -import { uploadTaskFileSchema } from '@/actions/schema'; -import { revalidatePath, revalidateTag } from 'next/cache'; +import { revalidatePath, revalidateTag } from "next/cache"; +import { authActionClient } from "@/actions/safe-action"; +import { uploadTaskFileSchema } from "@/actions/schema"; export const revalidateUpload = authActionClient .inputSchema(uploadTaskFileSchema) .metadata({ - name: 'upload-task-file', + name: "upload-task-file", track: { - event: 'upload-task-file', - channel: 'server', + event: "upload-task-file", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -18,12 +18,14 @@ export const revalidateUpload = authActionClient const { session } = ctx; if (!session.activeOrganizationId) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } revalidatePath(`/${session.activeOrganizationId}/risk/${riskId}`); - revalidatePath(`/${session.activeOrganizationId}/risk/${riskId}/tasks/${taskId}`); - revalidateTag('risk-cache'); + revalidatePath( + `/${session.activeOrganizationId}/risk/${riskId}/tasks/${taskId}`, + ); + revalidateTag("risk-cache", { expire: 0 }); return { riskId, diff --git a/apps/app/src/actions/risk/task/update-task-action.ts b/apps/app/src/actions/risk/task/update-task-action.ts index dd185312a..80c5fe34a 100644 --- a/apps/app/src/actions/risk/task/update-task-action.ts +++ b/apps/app/src/actions/risk/task/update-task-action.ts @@ -1,20 +1,22 @@ // update-task-action.ts -'use server'; +"use server"; -import type { TaskStatus } from '@db'; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../../safe-action'; -import { updateTaskSchema } from '../../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import type { TaskStatus } from "@trycompai/db"; +import { db } from "@trycompai/db"; + +import { authActionClient } from "../../safe-action"; +import { updateTaskSchema } from "../../schema"; export const updateTaskAction = authActionClient .inputSchema(updateTaskSchema) .metadata({ - name: 'update-task', + name: "update-task", track: { - event: 'update-task', - channel: 'server', + event: "update-task", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -22,7 +24,7 @@ export const updateTaskAction = authActionClient const { session } = ctx; if (!session.activeOrganizationId) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } try { @@ -33,7 +35,7 @@ export const updateTaskAction = authActionClient }); if (!task) { - throw new Error('Task not found'); + throw new Error("Task not found"); } await db.task.update({ @@ -53,7 +55,7 @@ export const updateTaskAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/risk`); revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); revalidatePath(`/${session.activeOrganizationId}/risk/${id}/tasks/${id}`); - revalidateTag('risks'); + revalidateTag("risks", { expire: 0 }); return { success: true }; } catch (error) { diff --git a/apps/app/src/actions/risk/update-inherent-risk-action.ts b/apps/app/src/actions/risk/update-inherent-risk-action.ts index b21f5f27e..616ebf6e0 100644 --- a/apps/app/src/actions/risk/update-inherent-risk-action.ts +++ b/apps/app/src/actions/risk/update-inherent-risk-action.ts @@ -1,17 +1,19 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updateInherentRiskSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updateInherentRiskSchema } from "../schema"; export const updateInherentRiskAction = authActionClient .inputSchema(updateInherentRiskSchema) .metadata({ - name: 'update-inherent-risk', + name: "update-inherent-risk", track: { - event: 'update-inherent-risk', - channel: 'server', + event: "update-inherent-risk", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -19,7 +21,7 @@ export const updateInherentRiskAction = authActionClient const { session } = ctx; if (!session.activeOrganizationId) { - throw new Error('Invalid organization'); + throw new Error("Invalid organization"); } try { @@ -37,13 +39,13 @@ export const updateInherentRiskAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/risk`); revalidatePath(`/${session.activeOrganizationId}/risk/register`); revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); - revalidateTag('risks'); + revalidateTag("risks", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error updating inherent risk:', error); + console.error("Error updating inherent risk:", error); return { success: false, }; diff --git a/apps/app/src/actions/risk/update-residual-risk-action.ts b/apps/app/src/actions/risk/update-residual-risk-action.ts index d7b8101db..7d4eb3264 100644 --- a/apps/app/src/actions/risk/update-residual-risk-action.ts +++ b/apps/app/src/actions/risk/update-residual-risk-action.ts @@ -1,9 +1,11 @@ -'use server'; +"use server"; -import { db, Impact, Likelihood } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updateResidualRiskSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db, Impact, Likelihood } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updateResidualRiskSchema } from "../schema"; function mapNumericToImpact(value: number): Impact { if (value <= 2) return Impact.insignificant; @@ -24,10 +26,10 @@ function mapNumericToLikelihood(value: number): Likelihood { export const updateResidualRiskAction = authActionClient .inputSchema(updateResidualRiskSchema) .metadata({ - name: 'update-residual-risk', + name: "update-residual-risk", track: { - event: 'update-residual-risk', - channel: 'server', + event: "update-residual-risk", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -35,7 +37,7 @@ export const updateResidualRiskAction = authActionClient const { session } = ctx; if (!session.activeOrganizationId) { - throw new Error('Invalid organization'); + throw new Error("Invalid organization"); } try { @@ -53,13 +55,13 @@ export const updateResidualRiskAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/risk`); revalidatePath(`/${session.activeOrganizationId}/risk/register`); revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); - revalidateTag('risks'); + revalidateTag("risks", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error updating residual risk:', error); + console.error("Error updating residual risk:", error); return { success: false, }; diff --git a/apps/app/src/actions/risk/update-residual-risk-enum-action.ts b/apps/app/src/actions/risk/update-residual-risk-enum-action.ts index 12adc136d..46d770bae 100644 --- a/apps/app/src/actions/risk/update-residual-risk-enum-action.ts +++ b/apps/app/src/actions/risk/update-residual-risk-enum-action.ts @@ -1,17 +1,19 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updateResidualRiskEnumSchema } from '../schema'; // Use the new enum schema +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updateResidualRiskEnumSchema } from "../schema"; // Use the new enum schema export const updateResidualRiskEnumAction = authActionClient .inputSchema(updateResidualRiskEnumSchema) // Use the new enum schema .metadata({ - name: 'update-residual-risk-enum', // New name + name: "update-residual-risk-enum", // New name track: { - event: 'update-residual-risk', // Keep original event if desired - channel: 'server', + event: "update-residual-risk", // Keep original event if desired + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -19,7 +21,7 @@ export const updateResidualRiskEnumAction = authActionClient const { session } = ctx; if (!session.activeOrganizationId) { - throw new Error('Invalid organization'); + throw new Error("Invalid organization"); } try { @@ -37,13 +39,13 @@ export const updateResidualRiskEnumAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/risk`); revalidatePath(`/${session.activeOrganizationId}/risk/register`); revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); - revalidateTag('risks'); + revalidateTag("risks", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error updating residual risk (enum):', error); + console.error("Error updating residual risk (enum):", error); return { success: false, }; diff --git a/apps/app/src/actions/risk/update-risk-action.ts b/apps/app/src/actions/risk/update-risk-action.ts index 69809efef..9860623d7 100644 --- a/apps/app/src/actions/risk/update-risk-action.ts +++ b/apps/app/src/actions/risk/update-risk-action.ts @@ -1,27 +1,30 @@ // update-risk-action.ts -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath, revalidateTag } from 'next/cache'; -import { authActionClient } from '../safe-action'; -import { updateRiskSchema } from '../schema'; +import { revalidatePath, revalidateTag } from "next/cache"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../safe-action"; +import { updateRiskSchema } from "../schema"; export const updateRiskAction = authActionClient .inputSchema(updateRiskSchema) .metadata({ - name: 'update-risk', + name: "update-risk", track: { - event: 'update-risk', - channel: 'server', + event: "update-risk", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { - const { id, title, description, category, department, assigneeId, status } = parsedInput; + const { id, title, description, category, department, assigneeId, status } = + parsedInput; const { session } = ctx; if (!session.activeOrganizationId) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } try { @@ -43,13 +46,13 @@ export const updateRiskAction = authActionClient revalidatePath(`/${session.activeOrganizationId}/risk`); revalidatePath(`/${session.activeOrganizationId}/risk/register`); revalidatePath(`/${session.activeOrganizationId}/risk/${id}`); - revalidateTag('risks'); + revalidateTag("risks", { expire: 0 }); return { success: true, }; } catch (error) { - console.error('Error updating risk:', error); + console.error("Error updating risk:", error); return { success: false, diff --git a/apps/app/src/actions/runtime-config.ts b/apps/app/src/actions/runtime-config.ts index 15bdd85c8..77ba33e5d 100644 --- a/apps/app/src/actions/runtime-config.ts +++ b/apps/app/src/actions/runtime-config.ts @@ -1,2 +1,2 @@ // This file contains shared configuration for server actions -export const runtime = 'nodejs'; +export const runtime = "nodejs"; diff --git a/apps/app/src/actions/safe-action.ts b/apps/app/src/actions/safe-action.ts index 3814b4f65..d3dc54233 100644 --- a/apps/app/src/actions/safe-action.ts +++ b/apps/app/src/actions/safe-action.ts @@ -1,20 +1,24 @@ -import { track } from '@/app/posthog'; -import { env } from '@/env.mjs'; -import { auth } from '@/utils/auth'; -import { logger } from '@/utils/logger'; -import { client } from '@comp/kv'; -import { AuditLogEntityType, db } from '@db'; -import { Ratelimit } from '@upstash/ratelimit'; -import { DEFAULT_SERVER_ERROR_MESSAGE, createSafeActionClient } from 'next-safe-action'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { track } from "@/app/posthog"; +import { env } from "@/env.mjs"; +import { auth } from "@/utils/auth"; +import { logger } from "@/utils/logger"; +import { Ratelimit } from "@upstash/ratelimit"; +import { + createSafeActionClient, + DEFAULT_SERVER_ERROR_MESSAGE, +} from "next-safe-action"; +import { z } from "zod"; + +import { AuditLogEntityType, db } from "@trycompai/db"; +import { client } from "@trycompai/kv"; let ratelimit: Ratelimit | undefined; if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) { ratelimit = new Ratelimit({ - limiter: Ratelimit.fixedWindow(10, '10s'), + limiter: Ratelimit.fixedWindow(10, "10s"), redis: client, }); } @@ -22,7 +26,7 @@ if (env.UPSTASH_REDIS_REST_URL && env.UPSTASH_REDIS_REST_TOKEN) { export const actionClientWithMeta = createSafeActionClient({ handleServerError(e) { // Log the error for debugging - logger.error('Server error:', e); + logger.error("Server error:", e); // Throw the error instead of returning it if (e instanceof Error) { @@ -58,7 +62,7 @@ export const authActionClient = actionClientWithMeta const { session, user } = response ?? {}; if (!session) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } const result = await next({ @@ -69,12 +73,15 @@ export const authActionClient = actionClientWithMeta }); const { fileData: _, ...inputForLog } = clientInput as any; - logger.info('Input ->', JSON.stringify(inputForLog, null, 2)); - logger.info('Result ->', JSON.stringify(result.data, null, 2)); + logger.info("Input ->", JSON.stringify(inputForLog, null, 2)); + logger.info("Result ->", JSON.stringify(result.data, null, 2)); // Also log validation errors if they exist if (result.validationErrors) { - logger.warn('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2)); + logger.warn( + "Validation Errors ->", + JSON.stringify(result.validationErrors, null, 2), + ); } return result; @@ -85,11 +92,11 @@ export const authActionClient = actionClientWithMeta if (ratelimit) { const { success, remaining: rateLimitRemaining } = await ratelimit.limit( - `${headersList.get('x-forwarded-for')}-${metadata.name}`, + `${headersList.get("x-forwarded-for")}-${metadata.name}`, ); if (!success) { - throw new Error('Too many requests'); + throw new Error("Too many requests"); } remaining = rateLimitRemaining; @@ -97,8 +104,8 @@ export const authActionClient = actionClientWithMeta return next({ ctx: { - ip: headersList.get('x-forwarded-for'), - userAgent: headersList.get('user-agent'), + ip: headersList.get("x-forwarded-for"), + userAgent: headersList.get("user-agent"), ratelimit: { remaining: remaining ?? 0, }, @@ -111,7 +118,7 @@ export const authActionClient = actionClientWithMeta }); if (!session) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } if (metadata.track) { @@ -135,20 +142,23 @@ export const authActionClient = actionClientWithMeta headers: headersList, }); - const member = await auth.api.getActiveMember({ - headers: headersList, - }); - if (!session) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } if (!session.session.activeOrganizationId) { - throw new Error('Organization not found'); + throw new Error("Organization not found"); } + const member = await db.member.findFirst({ + where: { + userId: session.user.id, + organizationId: session.session.activeOrganizationId, + }, + }); + if (!member) { - throw new Error('Member not found'); + throw new Error("Member not found"); } const { fileData: _, ...inputForAuditLog } = clientInput as any; @@ -160,8 +170,8 @@ export const authActionClient = actionClientWithMeta organizationId: session.session.activeOrganizationId, action: metadata.name, input: inputForAuditLog, - ipAddress: headersList.get('x-forwarded-for') || null, - userAgent: headersList.get('user-agent') || null, + ipAddress: headersList.get("x-forwarded-for") || null, + userAgent: headersList.get("user-agent") || null, }; const entityId = (clientInput as { entityId: string })?.entityId || null; @@ -186,13 +196,14 @@ export const authActionClient = actionClientWithMeta }; if (entityId) { - const parts = entityId.split('_'); + const parts = entityId.split("_"); const prefix = `${parts[0]}_`; // Handle special case prefixes with multiple parts if (parts.length > 2) { const complexPrefix = `${prefix}${parts[1]}_`; - entityType = mapEntityType[complexPrefix] || mapEntityType[prefix] || null; + entityType = + mapEntityType[complexPrefix] || mapEntityType[prefix] || null; } else { entityType = mapEntityType[prefix] || null; } @@ -211,12 +222,13 @@ export const authActionClient = actionClientWithMeta }, }); } catch (error) { - logger.error('Audit log error:', error); + logger.error("Audit log error:", error); } // Add revalidation logic based on the cursor rules - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); @@ -224,33 +236,36 @@ export const authActionClient = actionClientWithMeta }); // New action client that includes organization access check -export const authWithOrgAccessClient = authActionClient.use(async ({ next, clientInput, ctx }) => { - // Extract organizationId from the input - const organizationId = (clientInput as { organizationId?: string })?.organizationId; - - if (!organizationId) { - throw new Error('Organization ID is required'); - } - - // Check if user is a member of the organization - const member = await db.member.findFirst({ - where: { - userId: ctx.user.id, - organizationId, - }, - }); +export const authWithOrgAccessClient = authActionClient.use( + async ({ next, clientInput, ctx }) => { + // Extract organizationId from the input + const organizationId = (clientInput as { organizationId?: string }) + ?.organizationId; + + if (!organizationId) { + throw new Error("Organization ID is required"); + } - if (!member) { - throw new Error('You do not have access to this organization'); - } + // Check if user is a member of the organization + const member = await db.member.findFirst({ + where: { + userId: ctx.user.id, + organizationId, + }, + }); - return next({ - ctx: { - member, - organizationId, - }, - }); -}); + if (!member) { + throw new Error("You do not have access to this organization"); + } + + return next({ + ctx: { + member, + organizationId, + }, + }); + }, +); // New action client that requires auth but not an active organization export const authActionClientWithoutOrg = actionClientWithMeta @@ -262,7 +277,7 @@ export const authActionClientWithoutOrg = actionClientWithMeta const { session, user } = response ?? {}; if (!session) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } const result = await next({ @@ -273,12 +288,15 @@ export const authActionClientWithoutOrg = actionClientWithMeta }); const { fileData: _, ...inputForLog } = clientInput as any; - logger.info('Input ->', JSON.stringify(inputForLog, null, 2)); - logger.info('Result ->', JSON.stringify(result.data, null, 2)); + logger.info("Input ->", JSON.stringify(inputForLog, null, 2)); + logger.info("Result ->", JSON.stringify(result.data, null, 2)); // Also log validation errors if they exist if (result.validationErrors) { - logger.warn('Validation Errors ->', JSON.stringify(result.validationErrors, null, 2)); + logger.warn( + "Validation Errors ->", + JSON.stringify(result.validationErrors, null, 2), + ); } return result; @@ -289,11 +307,11 @@ export const authActionClientWithoutOrg = actionClientWithMeta if (ratelimit) { const { success, remaining: rateLimitRemaining } = await ratelimit.limit( - `${headersList.get('x-forwarded-for')}-${metadata.name}`, + `${headersList.get("x-forwarded-for")}-${metadata.name}`, ); if (!success) { - throw new Error('Too many requests'); + throw new Error("Too many requests"); } remaining = rateLimitRemaining; @@ -301,8 +319,8 @@ export const authActionClientWithoutOrg = actionClientWithMeta return next({ ctx: { - ip: headersList.get('x-forwarded-for'), - userAgent: headersList.get('user-agent'), + ip: headersList.get("x-forwarded-for"), + userAgent: headersList.get("user-agent"), ratelimit: { remaining: remaining ?? 0, }, @@ -315,7 +333,7 @@ export const authActionClientWithoutOrg = actionClientWithMeta }); if (!session) { - throw new Error('Unauthorized'); + throw new Error("Unauthorized"); } if (metadata.track) { diff --git a/apps/app/src/actions/schema.ts b/apps/app/src/actions/schema.ts index 6dc15fe11..341702a94 100644 --- a/apps/app/src/actions/schema.ts +++ b/apps/app/src/actions/schema.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + import { CommentEntityType, Departments, @@ -8,13 +10,12 @@ import { RiskCategory, RiskStatus, TaskStatus, -} from '@db'; -import { z } from 'zod'; +} from "@trycompai/db"; export const organizationSchema = z.object({ frameworkIds: z .array(z.string()) - .min(1, 'Please select at least one framework to get started with'), + .min(1, "Please select at least one framework to get started with"), }); export type OrganizationSchema = z.infer; @@ -22,17 +23,18 @@ export type OrganizationSchema = z.infer; export const organizationNameSchema = z.object({ name: z .string() - .min(1, 'Organization name is required') - .max(255, 'Organization name cannot exceed 255 characters'), + .min(1, "Organization name is required") + .max(255, "Organization name cannot exceed 255 characters"), }); export const subdomainAvailabilitySchema = z.object({ subdomain: z .string() - .min(1, 'Subdomain is required') - .max(255, 'Subdomain cannot exceed 255 characters') + .min(1, "Subdomain is required") + .max(255, "Subdomain cannot exceed 255 characters") .regex(/^[a-z0-9-]+$/, { - message: 'Subdomain can only contain lowercase letters, numbers, and hyphens', + message: + "Subdomain can only contain lowercase letters, numbers, and hyphens", }), }); @@ -56,9 +58,9 @@ export const organizationWebsiteSchema = z.object({ website: z .string() .url({ - message: 'Please enter a valid website that starts with https://', + message: "Please enter a valid website that starts with https://", }) - .max(255, 'Website cannot exceed 255 characters'), + .max(255, "Website cannot exceed 255 characters"), }); export const organizationAdvancedModeSchema = z.object({ @@ -69,66 +71,66 @@ export const organizationAdvancedModeSchema = z.object({ export const createRiskSchema = z.object({ title: z .string({ - required_error: 'Risk name is required', + error: "Risk name is required", }) .min(1, { - message: 'Risk name should be at least 1 character', + message: "Risk name should be at least 1 character", }) .max(100, { - message: 'Risk name should be at most 100 characters', + message: "Risk name should be at most 100 characters", }), description: z .string({ - required_error: 'Risk description is required', + error: "Risk description is required", }) .min(1, { - message: 'Risk description should be at least 1 character', + message: "Risk description should be at least 1 character", }) .max(255, { - message: 'Risk description should be at most 255 characters', + message: "Risk description should be at most 255 characters", }), - category: z.nativeEnum(RiskCategory, { - required_error: 'Risk category is required', + category: z.enum(RiskCategory, { + error: "Risk category is required", }), - department: z.nativeEnum(Departments, { - required_error: 'Risk department is required', + department: z.enum(Departments, { + error: "Risk department is required", }), assigneeId: z.string().optional().nullable(), }); export const updateRiskSchema = z.object({ id: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), title: z.string().min(1, { - message: 'Risk title is required', + message: "Risk title is required", }), description: z.string().min(1, { - message: 'Risk description is required', + message: "Risk description is required", }), - category: z.nativeEnum(RiskCategory, { - required_error: 'Risk category is required', + category: z.enum(RiskCategory, { + error: "Risk category is required", }), - department: z.nativeEnum(Departments, { - required_error: 'Risk department is required', + department: z.enum(Departments, { + error: "Risk department is required", }), assigneeId: z.string().optional().nullable(), - status: z.nativeEnum(RiskStatus, { - required_error: 'Risk status is required', + status: z.enum(RiskStatus, { + error: "Risk status is required", }), }); export const createRiskCommentSchema = z.object({ riskId: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), content: z .string() .min(1, { - message: 'Comment content is required', + message: "Comment content is required", }) .max(1000, { - message: 'Comment content should be at most 1000 characters', + message: "Comment content should be at most 1000 characters", }) .transform((val) => { // Remove any HTML tags by applying the replacement repeatedly until no changes occur @@ -137,7 +139,7 @@ export const createRiskCommentSchema = z.object({ do { previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); + sanitized = sanitized.replace(/<[^>]*>/g, ""); } while (sanitized !== previousValue); return sanitized; @@ -146,13 +148,13 @@ export const createRiskCommentSchema = z.object({ export const createTaskSchema = z.object({ riskId: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), title: z.string().min(1, { - message: 'Task title is required', + message: "Task title is required", }), description: z.string().min(1, { - message: 'Task description is required', + message: "Task description is required", }), dueDate: z.date().optional(), assigneeId: z.string().optional().nullable(), @@ -160,31 +162,31 @@ export const createTaskSchema = z.object({ export const updateTaskSchema = z.object({ id: z.string().min(1, { - message: 'Task ID is required', + message: "Task ID is required", }), title: z.string().optional(), description: z.string().optional(), dueDate: z.date().optional(), - status: z.nativeEnum(TaskStatus, { - required_error: 'Task status is required', + status: z.enum(TaskStatus, { + error: "Task status is required", }), assigneeId: z.string().optional().nullable(), }); export const createTaskCommentSchema = z.object({ riskId: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), taskId: z.string().min(1, { - message: 'Task ID is required', + message: "Task ID is required", }), content: z .string() .min(1, { - message: 'Comment content is required', + message: "Comment content is required", }) .max(1000, { - message: 'Comment content should be at most 1000 characters', + message: "Comment content should be at most 1000 characters", }) .transform((val) => { // Remove any HTML tags by applying the replacement repeatedly until no changes occur @@ -193,7 +195,7 @@ export const createTaskCommentSchema = z.object({ do { previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); + sanitized = sanitized.replace(/<[^>]*>/g, ""); } while (sanitized !== previousValue); return sanitized; @@ -202,23 +204,23 @@ export const createTaskCommentSchema = z.object({ export const uploadTaskFileSchema = z.object({ riskId: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), taskId: z.string().min(1, { - message: 'Task ID is required', + message: "Task ID is required", }), }); // Integrations export const deleteIntegrationConnectionSchema = z.object({ integrationName: z.string().min(1, { - message: 'Integration name is required', + message: "Integration name is required", }), }); export const createIntegrationSchema = z.object({ integrationId: z.string().min(1, { - message: 'Integration ID is required', + message: "Integration ID is required", }), }); @@ -229,15 +231,15 @@ export const seedDataSchema = z.object({ export const updateInherentRiskSchema = z.object({ id: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), - probability: z.nativeEnum(Likelihood), - impact: z.nativeEnum(Impact), + probability: z.enum(Likelihood), + impact: z.enum(Impact), }); export const updateResidualRiskSchema = z.object({ id: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), probability: z.number().min(1).max(10), impact: z.number().min(1).max(10), @@ -246,19 +248,19 @@ export const updateResidualRiskSchema = z.object({ // ADD START: Schema for enum-based residual risk update export const updateResidualRiskEnumSchema = z.object({ id: z.string().min(1, { - message: 'Risk ID is required', + message: "Risk ID is required", }), - probability: z.nativeEnum(Likelihood), - impact: z.nativeEnum(Impact), + probability: z.enum(Likelihood), + impact: z.enum(Impact), }); // ADD END // Policies export const createPolicySchema = z.object({ - title: z.string({ required_error: 'Title is required' }).min(1, 'Title is required'), + title: z.string({ error: "Title is required" }).min(1, "Title is required"), description: z - .string({ required_error: 'Description is required' }) - .min(1, 'Description is required'), + .string({ error: "Description is required" }) + .min(1, "Description is required"), frameworkIds: z.array(z.string()).optional(), controlIds: z.array(z.string()).optional(), entityId: z.string().optional(), @@ -273,8 +275,10 @@ export const updatePolicySchema = z.object({ }); export const addFrameworksSchema = z.object({ - organizationId: z.string().min(1, 'Organization ID is required'), - frameworkIds: z.array(z.string()).min(1, 'Please select at least one framework to add'), + organizationId: z.string().min(1, "Organization ID is required"), + frameworkIds: z + .array(z.string()) + .min(1, "Please select at least one framework to add"), }); export const assistantSettingsSchema = z.object({ @@ -282,10 +286,10 @@ export const assistantSettingsSchema = z.object({ }); export const createEmployeeSchema = z.object({ - name: z.string().min(1, 'Name is required'), - email: z.string().email('Invalid email address'), - department: z.nativeEnum(Departments, { - required_error: 'Department is required', + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email address"), + department: z.enum(Departments, { + error: "Department is required", }), externalEmployeeId: z.string().optional(), isActive: z.boolean().default(true), @@ -300,10 +304,10 @@ export const updatePolicyOverviewSchema = z.object({ export const updatePolicyFormSchema = z.object({ id: z.string(), - status: z.nativeEnum(PolicyStatus), + status: z.enum(PolicyStatus), assigneeId: z.string().optional().nullable(), - department: z.nativeEnum(Departments), - review_frequency: z.nativeEnum(Frequency), + department: z.enum(Departments), + review_frequency: z.enum(Frequency), review_date: z.date(), approverId: z.string().optional().nullable(), // Added for selecting an approver entityId: z.string(), @@ -312,22 +316,22 @@ export const updatePolicyFormSchema = z.object({ export const apiKeySchema = z.object({ name: z .string() - .min(1, { message: 'Name is required' }) - .max(64, { message: 'Name must be less than 64 characters' }), - expiresAt: z.enum(['30days', '90days', '1year', 'never']), + .min(1, { message: "Name is required" }) + .max(64, { message: "Name must be less than 64 characters" }), + expiresAt: z.enum(["30days", "90days", "1year", "never"]), }); export const createPolicyCommentSchema = z.object({ policyId: z.string().min(1, { - message: 'Policy ID is required', + message: "Policy ID is required", }), content: z .string() .min(1, { - message: 'Comment content is required', + message: "Comment content is required", }) .max(1000, { - message: 'Comment content should be at most 1000 characters', + message: "Comment content should be at most 1000 characters", }) .transform((val) => { // Remove any HTML tags by applying the replacement repeatedly until no changes occur @@ -336,7 +340,7 @@ export const createPolicyCommentSchema = z.object({ do { previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); + sanitized = sanitized.replace(/<[^>]*>/g, ""); } while (sanitized !== previousValue); return sanitized; @@ -346,8 +350,8 @@ export const createPolicyCommentSchema = z.object({ export const addCommentSchema = z.object({ content: z .string() - .min(1, 'Comment content is required') - .max(1000, 'Comment content should be at most 1000 characters') + .min(1, "Comment content is required") + .max(1000, "Comment content should be at most 1000 characters") .transform((val) => { // Remove any HTML tags by applying the replacement repeatedly until no changes occur let sanitized = val; @@ -355,37 +359,37 @@ export const addCommentSchema = z.object({ do { previousValue = sanitized; - sanitized = sanitized.replace(/<[^>]*>/g, ''); + sanitized = sanitized.replace(/<[^>]*>/g, ""); } while (sanitized !== previousValue); return sanitized; }), - entityId: z.string().min(1, 'Entity ID is required'), - entityType: z.nativeEnum(CommentEntityType), + entityId: z.string().min(1, "Entity ID is required"), + entityType: z.enum(CommentEntityType), }); export const createContextEntrySchema = z.object({ - question: z.string().min(1, 'Question is required'), - answer: z.string().min(1, 'Answer is required'), + question: z.string().min(1, "Question is required"), + answer: z.string().min(1, "Answer is required"), tags: z.string().optional(), // comma separated }); export const updateContextEntrySchema = z.object({ - id: z.string().min(1, 'ID is required'), - question: z.string().min(1, 'Question is required'), - answer: z.string().min(1, 'Answer is required'), + id: z.string().min(1, "ID is required"), + question: z.string().min(1, "Question is required"), + answer: z.string().min(1, "Answer is required"), tags: z.string().optional(), }); export const deleteContextEntrySchema = z.object({ - id: z.string().min(1, 'ID is required'), + id: z.string().min(1, "ID is required"), }); // Comment schemas for the new generic comments API export const createCommentSchema = z.object({ - content: z.string().min(1, 'Comment content is required'), + content: z.string().min(1, "Comment content is required"), entityId: z.string(), - entityType: z.nativeEnum(CommentEntityType), + entityType: z.enum(CommentEntityType), attachments: z .array( z.object({ @@ -401,7 +405,7 @@ export type CreateCommentSchema = z.infer; export const updateCommentSchema = z.object({ commentId: z.string(), - content: z.string().min(1, 'Comment content is required'), + content: z.string().min(1, "Comment content is required"), }); export type UpdateCommentSchema = z.infer; diff --git a/apps/app/src/actions/sidebar.ts b/apps/app/src/actions/sidebar.ts index 16d12e8e8..ef2b8cb35 100644 --- a/apps/app/src/actions/sidebar.ts +++ b/apps/app/src/actions/sidebar.ts @@ -1,9 +1,9 @@ -'use server'; +"use server"; -import { addYears } from 'date-fns'; -import { createSafeActionClient } from 'next-safe-action'; -import { cookies } from 'next/headers'; -import { z } from 'zod'; +import { cookies } from "next/headers"; +import { addYears } from "date-fns"; +import { createSafeActionClient } from "next-safe-action"; +import { z } from "zod"; const schema = z.object({ isCollapsed: z.boolean(), @@ -15,7 +15,7 @@ export const updateSidebarState = createSafeActionClient() const cookieStore = await cookies(); cookieStore.set({ - name: 'sidebar-collapsed', + name: "sidebar-collapsed", value: JSON.stringify(parsedInput.isCollapsed), expires: addYears(new Date(), 1), }); diff --git a/apps/app/src/actions/tasks/create-task-action.ts b/apps/app/src/actions/tasks/create-task-action.ts index db77e88e9..b5f6d4363 100644 --- a/apps/app/src/actions/tasks/create-task-action.ts +++ b/apps/app/src/actions/tasks/create-task-action.ts @@ -1,17 +1,18 @@ -'use server'; +"use server"; -import { authActionClient } from '@/actions/safe-action'; -import { db, Departments, TaskFrequency } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { authActionClient } from "@/actions/safe-action"; +import { z } from "zod"; + +import { db, Departments, TaskFrequency } from "@trycompai/db"; const createTaskSchema = z.object({ title: z.string().min(1, { - message: 'Title is required', + message: "Title is required", }), description: z.string().min(1, { - message: 'Description is required', + message: "Description is required", }), assigneeId: z.string().nullable().optional(), frequency: z.nativeEnum(TaskFrequency).nullable().optional(), @@ -23,22 +24,29 @@ const createTaskSchema = z.object({ export const createTaskAction = authActionClient .inputSchema(createTaskSchema) .metadata({ - name: 'create-task', + name: "create-task", track: { - event: 'create-task', - channel: 'server', + event: "create-task", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { - const { title, description, assigneeId, frequency, department, controlIds, taskTemplateId } = - parsedInput; + const { + title, + description, + assigneeId, + frequency, + department, + controlIds, + taskTemplateId, + } = parsedInput; const { session: { activeOrganizationId }, user, } = ctx; if (!user.id || !activeOrganizationId) { - throw new Error('Invalid user input'); + throw new Error("Invalid user input"); } try { @@ -48,7 +56,7 @@ export const createTaskAction = authActionClient description, assigneeId: assigneeId || null, organizationId: activeOrganizationId, - status: 'todo', + status: "todo", order: 0, frequency: frequency || null, department: department || null, @@ -64,8 +72,9 @@ export const createTaskAction = authActionClient // Revalidate the path based on the header const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); return { @@ -73,10 +82,10 @@ export const createTaskAction = authActionClient task, }; } catch (error) { - console.error('Failed to create task:', error); + console.error("Failed to create task:", error); return { success: false, - error: 'Failed to create task', + error: "Failed to create task", }; } }); diff --git a/apps/app/src/actions/tasks/regenerate-task-action.ts b/apps/app/src/actions/tasks/regenerate-task-action.ts index afce4aa54..a92db26a9 100644 --- a/apps/app/src/actions/tasks/regenerate-task-action.ts +++ b/apps/app/src/actions/tasks/regenerate-task-action.ts @@ -1,10 +1,11 @@ -'use server'; +"use server"; -import { authActionClient } from '@/actions/safe-action'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { authActionClient } from "@/actions/safe-action"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; export const regenerateTaskAction = authActionClient .inputSchema( @@ -13,10 +14,10 @@ export const regenerateTaskAction = authActionClient }), ) .metadata({ - name: 'regenerate-task', + name: "regenerate-task", track: { - event: 'regenerate-task', - channel: 'server', + event: "regenerate-task", + channel: "server", }, }) .action(async ({ parsedInput, ctx }) => { @@ -24,7 +25,7 @@ export const regenerateTaskAction = authActionClient const { session } = ctx; if (!session?.activeOrganizationId) { - throw new Error('No active organization'); + throw new Error("No active organization"); } // Get the task with its template @@ -39,11 +40,11 @@ export const regenerateTaskAction = authActionClient }); if (!task) { - throw new Error('Task not found'); + throw new Error("Task not found"); } if (!task.taskTemplate) { - throw new Error('Task has no associated template to regenerate from'); + throw new Error("Task has no associated template to regenerate from"); } // Update the task with the template's current title and description @@ -57,8 +58,9 @@ export const regenerateTaskAction = authActionClient // Revalidate the path based on the header const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); return { success: true }; diff --git a/apps/app/src/actions/trigger/heal-access-token.ts b/apps/app/src/actions/trigger/heal-access-token.ts index 08dfd70bf..5ce8db5fd 100644 --- a/apps/app/src/actions/trigger/heal-access-token.ts +++ b/apps/app/src/actions/trigger/heal-access-token.ts @@ -1,10 +1,12 @@ -'use server'; +"use server"; -import { auth } from '@trigger.dev/sdk'; -import { cookies } from 'next/headers'; +import { cookies } from "next/headers"; +import { auth } from "@trigger.dev/sdk"; // Server action that can set cookies (called from client components or forms) -export async function healAndSetAccessToken(triggerJobId: string): Promise { +export async function healAndSetAccessToken( + triggerJobId: string, +): Promise { try { const cookieStore = await cookies(); @@ -16,17 +18,19 @@ export async function healAndSetAccessToken(triggerJobId: string): Promise { +export async function createAccessToken( + triggerJobId: string, +): Promise { try { const token = await auth.createPublicToken({ scopes: { @@ -38,7 +42,7 @@ export async function createAccessToken(triggerJobId: string): Promise = }; export type DomainVerificationStatusProps = - | 'Valid Configuration' - | 'Invalid Configuration' - | 'Pending Verification' - | 'Domain Not Found' - | 'Unknown Error'; + | "Valid Configuration" + | "Invalid Configuration" + | "Pending Verification" + | "Domain Not Found" + | "Unknown Error"; // From https://vercel.com/docs/rest-api/endpoints#get-a-project-domain export interface DomainResponse { @@ -45,9 +45,9 @@ export interface DomainResponse { // From https://vercel.com/docs/rest-api/endpoints#get-a-domain-s-configuration export interface DomainConfigResponse { /** How we see the domain's configuration. - `CNAME`: Domain has a CNAME pointing to Vercel. - `A`: Domain's A record is resolving to Vercel. - `http`: Domain is resolving to Vercel but may be behind a Proxy. - `null`: Domain is not resolving to Vercel. */ - configuredBy?: ('CNAME' | 'A' | 'http') | null; + configuredBy?: ("CNAME" | "A" | "http") | null; /** Which challenge types the domain can use for issuing certs. */ - acceptedChallenges?: ('dns-01' | 'http-01')[]; + acceptedChallenges?: ("dns-01" | "http-01")[]; /** Whether or not the domain is configured AND we can automatically generate a TLS certificate. */ misconfigured: boolean; } @@ -74,7 +74,7 @@ export interface DomainVerificationResponse { } export const UPLOAD_TYPE = { - riskTask: 'risk-task', - vendorTask: 'vendor-task', - policy: 'policy', + riskTask: "risk-task", + vendorTask: "vendor-task", + policy: "policy", } as const; diff --git a/apps/app/src/actions/update-menu-action.ts b/apps/app/src/actions/update-menu-action.ts index e43ca2c96..66a4919e3 100644 --- a/apps/app/src/actions/update-menu-action.ts +++ b/apps/app/src/actions/update-menu-action.ts @@ -1,15 +1,16 @@ -'use server'; +"use server"; -import { Cookies } from '@/utils/constants'; -import { addYears } from 'date-fns'; -import { cookies } from 'next/headers'; -import { authActionClient } from './safe-action'; -import { updaterMenuSchema } from './schema'; +import { cookies } from "next/headers"; +import { Cookies } from "@/utils/constants"; +import { addYears } from "date-fns"; + +import { authActionClient } from "./safe-action"; +import { updaterMenuSchema } from "./schema"; export const updateMenuAction = authActionClient .inputSchema(updaterMenuSchema) .metadata({ - name: 'update-menu', + name: "update-menu", }) .action(async ({ parsedInput: value }) => { const cookieStore = await cookies(); diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts index 0174f9ace..e5058056b 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/connect-cloud.ts @@ -1,132 +1,148 @@ -'use server'; +"use server"; -import { encrypt } from '@/lib/encryption'; -import { getIntegrationHandler } from '@comp/integrations'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { cookies, headers } from 'next/headers'; -import { z } from 'zod'; -import { authActionClient } from '../../../../../actions/safe-action'; -import { runTests } from './run-tests'; +import { encrypt } from "@trycompai/utils/encryption"; +import { revalidatePath } from "next/cache"; +import { cookies, headers } from "next/headers"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; +import { getIntegrationHandler } from "@trycompai/integrations"; + +import { authActionClient } from "../../../../../actions/safe-action"; +import { runTests } from "./run-tests"; const connectCloudSchema = z.object({ - cloudProvider: z.enum(['aws', 'gcp', 'azure']), + cloudProvider: z.enum(["aws", "gcp", "azure"]), credentials: z.record(z.string(), z.string()), }); export const connectCloudAction = authActionClient .inputSchema(connectCloudSchema) .metadata({ - name: 'connect-cloud', + name: "connect-cloud", track: { - event: 'connect-cloud', - channel: 'cloud-tests', + event: "connect-cloud", + channel: "cloud-tests", }, }) - .action(async ({ parsedInput: { cloudProvider, credentials }, ctx: { session } }) => { - try { - if (!session.activeOrganizationId) { - return { - success: false, - error: 'No active organization found', - }; - } - - // Validate credentials before storing + .action( + async ({ + parsedInput: { cloudProvider, credentials }, + ctx: { session }, + }) => { try { - const integrationHandler = getIntegrationHandler(cloudProvider); - if (!integrationHandler) { + if (!session.activeOrganizationId) { return { success: false, - error: 'Integration handler not found', + error: "No active organization found", }; } - // Process credentials to the format expected by the handler - const typedCredentials = await integrationHandler.processCredentials( - credentials, - async (data: any) => data, // Pass through without encryption for validation - ); + // Validate credentials before storing + try { + const integrationHandler = getIntegrationHandler(cloudProvider); + if (!integrationHandler) { + return { + success: false, + error: "Integration handler not found", + }; + } - // Validate by attempting to fetch (this will throw if credentials are invalid) - await integrationHandler.fetch(typedCredentials); - } catch (error) { - console.error('Credential validation failed:', error); - return { - success: false, - error: - error instanceof Error - ? `Invalid credentials: ${error.message}` - : 'Failed to validate credentials. Please check your credentials and try again.', - }; - } + // Process credentials to the format expected by the handler + const typedCredentials = await integrationHandler.processCredentials( + credentials, + async (data: any) => data // Pass through without encryption for validation + ); - // Encrypt all credential fields after validation - const encryptedCredentials: Record = {}; - for (const [key, value] of Object.entries(credentials)) { - if (value) { - encryptedCredentials[key] = await encrypt(value); + // Validate by attempting to fetch (this will throw if credentials are invalid) + await integrationHandler.fetch(typedCredentials); + } catch (error) { + console.error("Credential validation failed:", error); + return { + success: false, + error: + error instanceof Error + ? `Invalid credentials: ${error.message}` + : "Failed to validate credentials. Please check your credentials and try again.", + }; } - } - // Check if integration already exists - const existingIntegration = await db.integration.findFirst({ - where: { - integrationId: cloudProvider, - organizationId: session.activeOrganizationId, - }, - }); + // Encrypt all credential fields after validation + const encryptedCredentials: Record = {}; + for (const [key, value] of Object.entries(credentials)) { + if (value) { + encryptedCredentials[key] = await encrypt(value); + } + } - if (existingIntegration) { - // Update existing integration - await db.integration.update({ - where: { id: existingIntegration.id }, - data: { - userSettings: encryptedCredentials as any, - lastRunAt: null, // Reset to trigger new scan - }, - }); - } else { - // Create new integration - await db.integration.create({ - data: { - name: cloudProvider.toUpperCase(), + // Check if integration already exists + const existingIntegration = await db.integration.findFirst({ + where: { integrationId: cloudProvider, organizationId: session.activeOrganizationId, - userSettings: encryptedCredentials as any, - settings: {}, }, }); - } - // Trigger immediate scan - const runResult = await runTests(); + if (existingIntegration) { + // Update existing integration + await db.integration.update({ + where: { id: existingIntegration.id }, + data: { + userSettings: encryptedCredentials as any, + lastRunAt: null, // Reset to trigger new scan + }, + }); + } else { + // Create new integration + await db.integration.create({ + data: { + name: cloudProvider.toUpperCase(), + integrationId: cloudProvider, + organizationId: session.activeOrganizationId, + userSettings: encryptedCredentials as any, + settings: {}, + }, + }); + } - if (runResult.success && runResult.publicAccessToken) { - (await cookies()).set('publicAccessToken', runResult.publicAccessToken); - } + // Trigger immediate scan + const runResult = await runTests(); + + if (runResult.success && runResult.publicAccessToken) { + (await cookies()).set( + "publicAccessToken", + runResult.publicAccessToken + ); + } - // Revalidate the path - const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); - revalidatePath(path); + // Revalidate the path + const headersList = await headers(); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); + revalidatePath(path); - return { - success: true, - trigger: runResult.success - ? { - taskId: runResult.taskId ?? undefined, - publicAccessToken: runResult.publicAccessToken ?? undefined, - } - : undefined, - runErrors: runResult.success ? undefined : (runResult.errors ?? undefined), - }; - } catch (error) { - console.error('Failed to connect cloud provider:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to connect cloud provider', - }; + return { + success: true, + trigger: runResult.success + ? { + taskId: runResult.taskId ?? undefined, + publicAccessToken: runResult.publicAccessToken ?? undefined, + } + : undefined, + runErrors: runResult.success + ? undefined + : (runResult.errors ?? undefined), + }; + } catch (error) { + console.error("Failed to connect cloud provider:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to connect cloud provider", + }; + } } - }); + ); diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/create-trigger-token.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/create-trigger-token.ts index 6e1dc60db..1a6c589a6 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/create-trigger-token.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/create-trigger-token.ts @@ -1,8 +1,8 @@ -'use server'; +"use server"; -import { auth as betterAuth } from '@/utils/auth'; -import { auth } from '@trigger.dev/sdk'; -import { headers } from 'next/headers'; +import { headers } from "next/headers"; +import { auth as betterAuth } from "@/utils/auth"; +import { auth } from "@trigger.dev/sdk"; export const createTriggerToken = async () => { const session = await betterAuth.api.getSession({ @@ -12,7 +12,7 @@ export const createTriggerToken = async () => { if (!session) { return { success: false, - error: 'Unauthorized', + error: "Unauthorized", }; } @@ -20,14 +20,14 @@ export const createTriggerToken = async () => { if (!orgId) { return { success: false, - error: 'No active organization', + error: "No active organization", }; } try { - const token = await auth.createTriggerPublicToken('run-integration-tests', { + const token = await auth.createTriggerPublicToken("run-integration-tests", { multipleUse: true, - expirationTime: '1hr', + expirationTime: "1hr", }); return { @@ -35,10 +35,13 @@ export const createTriggerToken = async () => { token, }; } catch (error) { - console.error('Error creating trigger token:', error); + console.error("Error creating trigger token:", error); return { success: false, - error: error instanceof Error ? error.message : 'Failed to create trigger token', + error: + error instanceof Error + ? error.message + : "Failed to create trigger token", }; } }; diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/disconnect-cloud.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/disconnect-cloud.ts index 5e5aa7c75..76cee1cc8 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/disconnect-cloud.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/disconnect-cloud.ts @@ -1,22 +1,24 @@ -'use server'; +"use server"; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; -import { authActionClient } from '../../../../../actions/safe-action'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../../../../../actions/safe-action"; const disconnectCloudSchema = z.object({ - cloudProvider: z.enum(['aws', 'gcp', 'azure']), + cloudProvider: z.enum(["aws", "gcp", "azure"]), }); export const disconnectCloudAction = authActionClient .inputSchema(disconnectCloudSchema) .metadata({ - name: 'disconnect-cloud', + name: "disconnect-cloud", track: { - event: 'disconnect-cloud', - channel: 'cloud-tests', + event: "disconnect-cloud", + channel: "cloud-tests", }, }) .action(async ({ parsedInput: { cloudProvider }, ctx: { session } }) => { @@ -24,7 +26,7 @@ export const disconnectCloudAction = authActionClient if (!session.activeOrganizationId) { return { success: false, - error: 'No active organization found', + error: "No active organization found", }; } @@ -39,7 +41,7 @@ export const disconnectCloudAction = authActionClient if (!integration) { return { success: false, - error: 'Cloud provider not found', + error: "Cloud provider not found", }; } @@ -50,18 +52,22 @@ export const disconnectCloudAction = authActionClient // Revalidate the path const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); return { success: true, }; } catch (error) { - console.error('Failed to disconnect cloud provider:', error); + console.error("Failed to disconnect cloud provider:", error); return { success: false, - error: error instanceof Error ? error.message : 'Failed to disconnect cloud provider', + error: + error instanceof Error + ? error.message + : "Failed to disconnect cloud provider", }; } }); diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/run-tests.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/run-tests.ts index 6d73e7095..9fb35eb96 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/run-tests.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/run-tests.ts @@ -1,10 +1,10 @@ -'use server'; +"use server"; -import { runIntegrationTests } from '@/jobs/tasks/integration/run-integration-tests'; -import { auth } from '@/utils/auth'; -import { tasks } from '@trigger.dev/sdk'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { runIntegrationTests } from "@/jobs/tasks/integration/run-integration-tests"; +import { auth } from "@/utils/auth"; +import { tasks } from "@trigger.dev/sdk"; export const runTests = async () => { const session = await auth.api.getSession({ @@ -14,7 +14,7 @@ export const runTests = async () => { if (!session) { return { success: false, - errors: ['Unauthorized'], + errors: ["Unauthorized"], }; } @@ -22,18 +22,22 @@ export const runTests = async () => { if (!orgId) { return { success: false, - errors: ['No active organization'], + errors: ["No active organization"], }; } try { - const handle = await tasks.trigger('run-integration-tests', { - organizationId: orgId, - }); + const handle = await tasks.trigger( + "run-integration-tests", + { + organizationId: orgId, + }, + ); const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); revalidatePath(path); @@ -44,11 +48,15 @@ export const runTests = async () => { publicAccessToken: handle.publicAccessToken, }; } catch (error) { - console.error('Error triggering integration tests:', error); + console.error("Error triggering integration tests:", error); return { success: false, - errors: [error instanceof Error ? error.message : 'Failed to trigger integration tests'], + errors: [ + error instanceof Error + ? error.message + : "Failed to trigger integration tests", + ], }; } }; diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/update-cloud-credentials.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/update-cloud-credentials.ts index 7550d7099..334efff85 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/update-cloud-credentials.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/update-cloud-credentials.ts @@ -1,80 +1,91 @@ -'use server'; +"use server"; -import { encrypt } from '@/lib/encryption'; -import { db } from '@db'; -import { revalidatePath } from 'next/cache'; -import { headers } from 'next/headers'; -import { z } from 'zod'; -import { authActionClient } from '../../../../../actions/safe-action'; +import { encrypt } from "@trycompai/utils/encryption"; +import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { z } from "zod"; + +import { db } from "@trycompai/db"; + +import { authActionClient } from "../../../../../actions/safe-action"; const updateCloudCredentialsSchema = z.object({ - cloudProvider: z.enum(['aws', 'gcp', 'azure']), + cloudProvider: z.enum(["aws", "gcp", "azure"]), credentials: z.record(z.string(), z.string()), }); export const updateCloudCredentialsAction = authActionClient .inputSchema(updateCloudCredentialsSchema) .metadata({ - name: 'update-cloud-credentials', + name: "update-cloud-credentials", track: { - event: 'update-cloud-credentials', - channel: 'cloud-tests', + event: "update-cloud-credentials", + channel: "cloud-tests", }, }) - .action(async ({ parsedInput: { cloudProvider, credentials }, ctx: { session } }) => { - try { - if (!session.activeOrganizationId) { - return { - success: false, - error: 'No active organization found', - }; - } + .action( + async ({ + parsedInput: { cloudProvider, credentials }, + ctx: { session }, + }) => { + try { + if (!session.activeOrganizationId) { + return { + success: false, + error: "No active organization found", + }; + } - // Find the integration - const integration = await db.integration.findFirst({ - where: { - integrationId: cloudProvider, - organizationId: session.activeOrganizationId, - }, - }); + // Find the integration + const integration = await db.integration.findFirst({ + where: { + integrationId: cloudProvider, + organizationId: session.activeOrganizationId, + }, + }); - if (!integration) { - return { - success: false, - error: 'Cloud provider not found', - }; - } + if (!integration) { + return { + success: false, + error: "Cloud provider not found", + }; + } - // Encrypt all credential fields - const encryptedCredentials: Record = {}; - for (const [key, value] of Object.entries(credentials)) { - if (value) { - encryptedCredentials[key] = await encrypt(value); + // Encrypt all credential fields + const encryptedCredentials: Record = {}; + for (const [key, value] of Object.entries(credentials)) { + if (value) { + encryptedCredentials[key] = await encrypt(value); + } } - } - // Update the integration - await db.integration.update({ - where: { id: integration.id }, - data: { - userSettings: encryptedCredentials as any, - }, - }); + // Update the integration + await db.integration.update({ + where: { id: integration.id }, + data: { + userSettings: encryptedCredentials as any, + }, + }); - // Revalidate the path - const headersList = await headers(); - let path = headersList.get('x-pathname') || headersList.get('referer') || ''; - path = path.replace(/\/[a-z]{2}\//, '/'); - revalidatePath(path); + // Revalidate the path + const headersList = await headers(); + let path = + headersList.get("x-pathname") || headersList.get("referer") || ""; + path = path.replace(/\/[a-z]{2}\//, "/"); + revalidatePath(path); - return { - success: true, - }; - } catch (error) { - console.error('Failed to update cloud credentials:', error); - return { - success: false, - error: error instanceof Error ? error.message : 'Failed to update cloud credentials', - }; + return { + success: true, + }; + } catch (error) { + console.error("Failed to update cloud credentials:", error); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Failed to update cloud credentials", + }; + } } - }); + ); diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/validate-aws-credentials.ts b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/validate-aws-credentials.ts index 6eb90739f..65725adbc 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/validate-aws-credentials.ts +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/actions/validate-aws-credentials.ts @@ -1,9 +1,10 @@ -'use server'; +"use server"; -import { DescribeRegionsCommand, EC2Client } from '@aws-sdk/client-ec2'; -import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; -import { z } from 'zod'; -import { authActionClient } from '../../../../../actions/safe-action'; +import { DescribeRegionsCommand, EC2Client } from "@aws-sdk/client-ec2"; +import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts"; +import { z } from "zod"; + +import { authActionClient } from "../../../../../actions/safe-action"; const validateAwsCredentialsSchema = z.object({ accessKeyId: z.string(), @@ -13,17 +14,17 @@ const validateAwsCredentialsSchema = z.object({ export const validateAwsCredentialsAction = authActionClient .inputSchema(validateAwsCredentialsSchema) .metadata({ - name: 'validate-aws-credentials', + name: "validate-aws-credentials", track: { - event: 'validate-aws-credentials', - channel: 'cloud-tests', + event: "validate-aws-credentials", + channel: "cloud-tests", }, }) .action(async ({ parsedInput: { accessKeyId, secretAccessKey } }) => { try { // First, validate credentials using STS const stsClient = new STSClient({ - region: 'us-east-1', // Default region for validation + region: "us-east-1", // Default region for validation credentials: { accessKeyId, secretAccessKey, @@ -34,38 +35,40 @@ export const validateAwsCredentialsAction = authActionClient // Get available regions const ec2Client = new EC2Client({ - region: 'us-east-1', + region: "us-east-1", credentials: { accessKeyId, secretAccessKey, }, }); - const regionsResponse = await ec2Client.send(new DescribeRegionsCommand({})); + const regionsResponse = await ec2Client.send( + new DescribeRegionsCommand({}), + ); // Map of common region codes to friendly names const regionNames: Record = { - 'us-east-1': 'US East (N. Virginia)', - 'us-east-2': 'US East (Ohio)', - 'us-west-1': 'US West (N. California)', - 'us-west-2': 'US West (Oregon)', - 'eu-west-1': 'Europe (Ireland)', - 'eu-west-2': 'Europe (London)', - 'eu-west-3': 'Europe (Paris)', - 'eu-central-1': 'Europe (Frankfurt)', - 'eu-north-1': 'Europe (Stockholm)', - 'eu-south-1': 'Europe (Milan)', - 'ap-southeast-1': 'Asia Pacific (Singapore)', - 'ap-southeast-2': 'Asia Pacific (Sydney)', - 'ap-northeast-1': 'Asia Pacific (Tokyo)', - 'ap-northeast-2': 'Asia Pacific (Seoul)', - 'ap-northeast-3': 'Asia Pacific (Osaka)', - 'ap-south-1': 'Asia Pacific (Mumbai)', - 'ap-east-1': 'Asia Pacific (Hong Kong)', - 'ca-central-1': 'Canada (Central)', - 'sa-east-1': 'South America (São Paulo)', - 'me-south-1': 'Middle East (Bahrain)', - 'af-south-1': 'Africa (Cape Town)', + "us-east-1": "US East (N. Virginia)", + "us-east-2": "US East (Ohio)", + "us-west-1": "US West (N. California)", + "us-west-2": "US West (Oregon)", + "eu-west-1": "Europe (Ireland)", + "eu-west-2": "Europe (London)", + "eu-west-3": "Europe (Paris)", + "eu-central-1": "Europe (Frankfurt)", + "eu-north-1": "Europe (Stockholm)", + "eu-south-1": "Europe (Milan)", + "ap-southeast-1": "Asia Pacific (Singapore)", + "ap-southeast-2": "Asia Pacific (Sydney)", + "ap-northeast-1": "Asia Pacific (Tokyo)", + "ap-northeast-2": "Asia Pacific (Seoul)", + "ap-northeast-3": "Asia Pacific (Osaka)", + "ap-south-1": "Asia Pacific (Mumbai)", + "ap-east-1": "Asia Pacific (Hong Kong)", + "ca-central-1": "Canada (Central)", + "sa-east-1": "South America (São Paulo)", + "me-south-1": "Middle East (Bahrain)", + "af-south-1": "Africa (Cape Town)", }; const regions = (regionsResponse.Regions || []) @@ -86,13 +89,13 @@ export const validateAwsCredentialsAction = authActionClient regions, }; } catch (error) { - console.error('AWS credential validation failed:', error); + console.error("AWS credential validation failed:", error); return { success: false, error: error instanceof Error ? error.message - : 'Failed to validate AWS credentials. Please check your access key and secret.', + : "Failed to validate AWS credentials. Please check your access key and secret.", }; } }); diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ChatPlaceholder.tsx b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ChatPlaceholder.tsx index 33fb1691f..9cd037100 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ChatPlaceholder.tsx +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/ChatPlaceholder.tsx @@ -1,7 +1,14 @@ -'use client'; +"use client"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp/ui/card'; -import { MessageSquare } from 'lucide-react'; +import { MessageSquare } from "lucide-react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@trycompai/ui/card"; export function ChatPlaceholder() { return ( diff --git a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudConnectionCard.tsx b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudConnectionCard.tsx index 479770906..9b6d66e29 100644 --- a/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudConnectionCard.tsx +++ b/apps/app/src/app/(app)/[orgId]/cloud-tests/components/CloudConnectionCard.tsx @@ -1,13 +1,21 @@ -'use client'; +"use client"; -import { Button } from '@comp/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@comp/ui/card'; -import { Input } from '@comp/ui/input'; -import { Label } from '@comp/ui/label'; -import { ExternalLink, Loader2 } from 'lucide-react'; -import { useState } from 'react'; -import { toast } from 'sonner'; -import { connectCloudAction } from '../actions/connect-cloud'; +import { useState } from "react"; +import { ExternalLink, Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +import { Button } from "@trycompai/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@trycompai/ui/card"; +import { Input } from "@trycompai/ui/input"; +import { Label } from "@trycompai/ui/label"; + +import { connectCloudAction } from "../actions/connect-cloud"; interface CloudField { id: string; @@ -24,7 +32,7 @@ type TriggerInfo = { }; interface CloudConnectionCardProps { - cloudProvider: 'aws' | 'gcp' | 'azure'; + cloudProvider: "aws" | "gcp" | "azure"; name: string; shortName: string; description: string; @@ -42,7 +50,7 @@ export function CloudConnectionCard({ description, fields, guideUrl, - color = 'from-primary to-primary', + color = "from-primary to-primary", logoUrl, onSuccess, }: CloudConnectionCardProps) { @@ -65,7 +73,7 @@ export function CloudConnectionCard({ const newErrors: Record = {}; fields.forEach((field) => { if (!credentials[field.id]?.trim()) { - newErrors[field.id] = 'Required'; + newErrors[field.id] = "Required"; } }); setErrors(newErrors); @@ -74,7 +82,7 @@ export function CloudConnectionCard({ const handleConnect = async () => { if (!validateFields()) { - toast.error('Please fill in all required fields'); + toast.error("Please fill in all required fields"); return; } @@ -91,14 +99,16 @@ export function CloudConnectionCard({ onSuccess?.(result.data?.trigger); if (result.data?.runErrors && result.data.runErrors.length > 0) { - toast.error(result.data.runErrors[0] || 'Initial scan reported an issue'); + toast.error( + result.data.runErrors[0] || "Initial scan reported an issue", + ); } } else { - toast.error(result?.data?.error || 'Failed to connect'); + toast.error(result?.data?.error || "Failed to connect"); } } catch (error) { - console.error('Connection error:', error); - toast.error('An unexpected error occurred'); + console.error("Connection error:", error); + toast.error("An unexpected error occurred"); } finally { setIsConnecting(false); } @@ -112,7 +122,11 @@ export function CloudConnectionCard({ className={`bg-gradient-to-br ${color} flex items-center justify-center rounded-lg p-2`} > {logoUrl && ( - {`${shortName} + {`${shortName} )}
@@ -125,7 +139,7 @@ export function CloudConnectionCard({ href={guideUrl} target="_blank" rel="noopener noreferrer" - className="text-primary hover:underline flex items-center gap-1 text-xs" + className="text-primary flex items-center gap-1 text-xs hover:underline" > Setup guide @@ -139,39 +153,45 @@ export function CloudConnectionCard({ {field.label} * - {field.type === 'textarea' ? ( + {field.type === "textarea" ? (