diff --git a/.env.example b/.env.example index c316a3eea..62e7a5d09 100644 --- a/.env.example +++ b/.env.example @@ -51,4 +51,4 @@ MONGO_MIGRATION_URL= OPENAI_API_KEY= ZENVIA_API_URL= -ZENVIA_API_TOKEN= \ No newline at end of file +ZENVIA_API_TOKEN= diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 48436b3aa..000000000 --- a/.eslintignore +++ /dev/null @@ -1,18 +0,0 @@ -# /node_modules/* in the project root is ignored by default -# build artefacts -dist/* -coverage/* -# data definition files -**/*.d.ts -# custom definition files -/src/types/ -**/bak/** -**/bin/** -**/config/** -**/dist/** -**/doc/** -**/node_modules/** -**/public/** -**/report/** -**/webpack.config*.js -**/docs/** diff --git a/.eslintrc.yml b/.eslintrc.yml deleted file mode 100644 index 99e7fa105..000000000 --- a/.eslintrc.yml +++ /dev/null @@ -1,33 +0,0 @@ -extends: ["react-app", "plugin:@next/next/recommended"] -rules: - { - "array-bracket-spacing": 0, - "arrow-parens": 0, - "camelcase": 0, - "comma-dangle": 0, - "comma-spacing": 0, - "computed-property-spacing": 0, - "indent": 0, - "key-spacing": 0, - "max-statements-per-line": 0, - "no-multi-spaces": 0, - "no-underscore-dangle": 0, - "no-unused-expressions": 0, - "no-restricted-syntax": 0, - "no-unused-vars": 0, - "one-var": 0, - "operator-linebreak": 0, - "space-before-function-paren": 0, - "space-in-parens": 0, - "valid-jsdoc": 0, - "jsx-a11y/anchor-is-valid": 0, - } -settings: - react: - version: "^17.0.2" -parser: "@typescript-eslint/parser" -parserOptions: - ecmaVersion: 2020 - sourceType: "module" -env: - jest: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 234009df9..39780c629 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,10 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 20.18.0 uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 20.18.0 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index fa5ba301a..18a54d3b6 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.19.1] + node-version: [20.18.0] steps: - name: Checkout uses: actions/checkout@v2 @@ -43,7 +43,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.19.1] + node-version: [20.18.0] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18.19.1] + node-version: [20.18.0] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/track-issues.yml b/.github/workflows/track-issues.yml new file mode 100644 index 000000000..8a1d86ab8 --- /dev/null +++ b/.github/workflows/track-issues.yml @@ -0,0 +1,204 @@ +name: Track Issues on Project Board + +on: + pull_request: + types: [opened, edited, reopened, ready_for_review] + branches: [develop, stage] + push: + branches: [stage, master] + workflow_dispatch: + inputs: + issue_number: + description: "Issue number to manually update" + required: false + target_status: + description: "Target status (In Progress, In Review, QA, Deployed)" + required: false + default: "In Review" + +concurrency: + group: track-issues-${{ github.ref }} + cancel-in-progress: false + +env: + PROJECT_NUMBER: 1 + +jobs: + update-project-board: + runs-on: ubuntu-latest + steps: + - name: Update issue status on project board + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.PROJECT_TOKEN }} + script: | + const projectNumber = parseInt('${{ env.PROJECT_NUMBER }}'); + const owner = context.repo.owner; + const repo = context.repo.repo; + const eventName = context.eventName; + const ref = context.ref; + + // --- Step 1: Determine target status and extract issue numbers --- + + let targetStatusKey; + const issueNumbers = new Set(); + + if (eventName === 'pull_request') { + const isDraft = context.payload.pull_request.draft; + targetStatusKey = isDraft ? 'progress' : 'review'; + + const text = `${context.payload.pull_request.body || ''} ${context.payload.pull_request.title || ''}`; + const pattern = /related\s+ticket\s*:?\s*#(\d+)/gi; + let match; + while ((match = pattern.exec(text)) !== null) { + issueNumbers.add(match[1]); + } + + console.log(`Event: pull_request (${isDraft ? 'draft' : 'ready'})`); + + } else if (eventName === 'push') { + const branch = ref.replace('refs/heads/', ''); + const statusMap = { stage: 'qa', master: 'deployed' }; + targetStatusKey = statusMap[branch]; + + if (!targetStatusKey) { + console.log(`Push to unhandled branch: ${branch}`); + return; + } + + // Find the merged PR for this push + const { data: prs } = await github.rest.pulls.list({ + owner, repo, + state: 'closed', + sort: 'updated', + direction: 'desc', + per_page: 20 + }); + + const mergedPR = prs.find(pr => + pr.merge_commit_sha === context.sha && pr.merged_at + ); + + if (!mergedPR) { + console.log('No merged PR found for commit:', context.sha); + return; + } + + console.log(`Event: push to ${branch} (merged PR #${mergedPR.number}: ${mergedPR.title})`); + + const text = `${mergedPR.body || ''} ${mergedPR.title}`; + const pattern = /related\s+ticket\s*:?\s*#(\d+)/gi; + let match; + while ((match = pattern.exec(text)) !== null) { + issueNumbers.add(match[1]); + } + + } else if (eventName === 'workflow_dispatch') { + const inputIssue = '${{ github.event.inputs.issue_number }}'; + const inputStatus = '${{ github.event.inputs.target_status }}'.toLowerCase(); + + if (!inputIssue) { + console.log('No issue number provided'); + return; + } + + targetStatusKey = inputStatus; + issueNumbers.add(inputIssue); + + console.log(`Event: manual trigger (issue #${inputIssue} → "${inputStatus}")`); + + } else { + console.log(`Unhandled event: ${eventName}`); + return; + } + + // --- Step 2: Validate we have issues to process --- + + if (issueNumbers.size === 0) { + console.log('No linked issues found'); + return; + } + + console.log('Issues to update:', Array.from(issueNumbers).map(n => `#${n}`).join(', ')); + console.log('Target status:', targetStatusKey); + + // --- Step 3: Get project and status field info --- + + const projectData = await github.graphql(` + query($owner: String!, $number: Int!) { + organization(login: $owner) { + projectV2(number: $number) { + id + field(name: "Status") { + ... on ProjectV2SingleSelectField { + id + options { id name } + } + } + } + } + } + `, { owner, number: projectNumber }); + + const project = projectData.organization.projectV2; + const statusField = project.field; + + const targetOption = statusField.options.find(o => + o.name.toLowerCase().includes(targetStatusKey) + ); + + if (!targetOption) { + console.log(`Status option matching "${targetStatusKey}" not found`); + console.log('Available options:', statusField.options.map(o => o.name).join(', ')); + return; + } + + // --- Step 4: Update each issue on the project board --- + + let successCount = 0; + let errorCount = 0; + + for (const issueNumber of issueNumbers) { + try { + const issue = await github.rest.issues.get({ + owner, repo, + issue_number: parseInt(issueNumber) + }); + + const addResult = await github.graphql(` + mutation($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { + item { id } + } + } + `, { projectId: project.id, contentId: issue.data.node_id }); + + const itemId = addResult.addProjectV2ItemById.item.id; + + await github.graphql(` + mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId + itemId: $itemId + fieldId: $fieldId + value: {singleSelectOptionId: $value} + }) { + projectV2Item { id } + } + } + `, { + projectId: project.id, + itemId: itemId, + fieldId: statusField.id, + value: targetOption.id + }); + + console.log(`Issue #${issueNumber} → "${targetOption.name}"`); + successCount++; + } catch (error) { + console.log(`Error on issue #${issueNumber}: ${error.message}`); + errorCount++; + } + } + + console.log(`Done: ${successCount} updated, ${errorCount} failed`); diff --git a/.node-version b/.node-version index 3c5535cf6..2a393af59 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.19.1 +20.18.0 diff --git a/.yarn/cache/@ai-sdk-gateway-npm-1.0.29-b61adc9e58-3bd41160ab.zip b/.yarn/cache/@ai-sdk-gateway-npm-1.0.29-b61adc9e58-3bd41160ab.zip new file mode 100644 index 000000000..d74553acb Binary files /dev/null and b/.yarn/cache/@ai-sdk-gateway-npm-1.0.29-b61adc9e58-3bd41160ab.zip differ diff --git a/.yarn/cache/@ai-sdk-provider-npm-0.0.26-3aa33cbf4c-9f6281b90d.zip b/.yarn/cache/@ai-sdk-provider-npm-0.0.26-3aa33cbf4c-9f6281b90d.zip deleted file mode 100644 index b5c96d2b5..000000000 Binary files a/.yarn/cache/@ai-sdk-provider-npm-0.0.26-3aa33cbf4c-9f6281b90d.zip and /dev/null differ diff --git a/.yarn/cache/@ai-sdk-provider-npm-2.0.0-3c34d402f4-6068b327d4.zip b/.yarn/cache/@ai-sdk-provider-npm-2.0.0-3c34d402f4-6068b327d4.zip new file mode 100644 index 000000000..9cae7f1af Binary files /dev/null and b/.yarn/cache/@ai-sdk-provider-npm-2.0.0-3c34d402f4-6068b327d4.zip differ diff --git a/.yarn/cache/@ai-sdk-provider-utils-npm-1.0.22-11020b0f1e-0657ea6b78.zip b/.yarn/cache/@ai-sdk-provider-utils-npm-1.0.22-11020b0f1e-0657ea6b78.zip deleted file mode 100644 index eca0dfd43..000000000 Binary files a/.yarn/cache/@ai-sdk-provider-utils-npm-1.0.22-11020b0f1e-0657ea6b78.zip and /dev/null differ diff --git a/.yarn/cache/@ai-sdk-provider-utils-npm-3.0.9-a1fcf189ea-b1c7a3ef43.zip b/.yarn/cache/@ai-sdk-provider-utils-npm-3.0.9-a1fcf189ea-b1c7a3ef43.zip new file mode 100644 index 000000000..2f5a65ef2 Binary files /dev/null and b/.yarn/cache/@ai-sdk-provider-utils-npm-3.0.9-a1fcf189ea-b1c7a3ef43.zip differ diff --git a/.yarn/cache/@ai-sdk-react-npm-0.0.70-76e40e15da-8a28523ba3.zip b/.yarn/cache/@ai-sdk-react-npm-0.0.70-76e40e15da-8a28523ba3.zip deleted file mode 100644 index 65339eaa8..000000000 Binary files a/.yarn/cache/@ai-sdk-react-npm-0.0.70-76e40e15da-8a28523ba3.zip and /dev/null differ diff --git a/.yarn/cache/@ai-sdk-solid-npm-0.0.54-d4e9e494b8-c9d9e6284d.zip b/.yarn/cache/@ai-sdk-solid-npm-0.0.54-d4e9e494b8-c9d9e6284d.zip deleted file mode 100644 index 4510ff709..000000000 Binary files a/.yarn/cache/@ai-sdk-solid-npm-0.0.54-d4e9e494b8-c9d9e6284d.zip and /dev/null differ diff --git a/.yarn/cache/@ai-sdk-svelte-npm-0.0.57-a9240f8e5b-7b8db6add3.zip b/.yarn/cache/@ai-sdk-svelte-npm-0.0.57-a9240f8e5b-7b8db6add3.zip deleted file mode 100644 index e1f260063..000000000 Binary files a/.yarn/cache/@ai-sdk-svelte-npm-0.0.57-a9240f8e5b-7b8db6add3.zip and /dev/null differ diff --git a/.yarn/cache/@ai-sdk-ui-utils-npm-0.0.50-c83534d1f8-6610b8e82e.zip b/.yarn/cache/@ai-sdk-ui-utils-npm-0.0.50-c83534d1f8-6610b8e82e.zip deleted file mode 100644 index 5c08ba039..000000000 Binary files a/.yarn/cache/@ai-sdk-ui-utils-npm-0.0.50-c83534d1f8-6610b8e82e.zip and /dev/null differ diff --git a/.yarn/cache/@ai-sdk-vue-npm-0.0.59-3483e7ac21-f489da3d1d.zip b/.yarn/cache/@ai-sdk-vue-npm-0.0.59-3483e7ac21-f489da3d1d.zip deleted file mode 100644 index 7f99679b6..000000000 Binary files a/.yarn/cache/@ai-sdk-vue-npm-0.0.59-3483e7ac21-f489da3d1d.zip and /dev/null differ diff --git a/.yarn/cache/@anthropic-ai-sdk-npm-0.9.1-ed6a7af5a8-0ec50abc0f.zip b/.yarn/cache/@anthropic-ai-sdk-npm-0.9.1-ed6a7af5a8-0ec50abc0f.zip deleted file mode 100644 index 79c3e1615..000000000 Binary files a/.yarn/cache/@anthropic-ai-sdk-npm-0.9.1-ed6a7af5a8-0ec50abc0f.zip and /dev/null differ diff --git a/.yarn/cache/@borewit-text-codec-npm-0.2.1-d8c2610020-72cbd41913.zip b/.yarn/cache/@borewit-text-codec-npm-0.2.1-d8c2610020-72cbd41913.zip new file mode 100644 index 000000000..9bdf81e08 Binary files /dev/null and b/.yarn/cache/@borewit-text-codec-npm-0.2.1-d8c2610020-72cbd41913.zip differ diff --git a/.yarn/cache/@casl-ability-npm-6.7.3-4a91eca645-9905944ed6.zip b/.yarn/cache/@casl-ability-npm-6.7.3-4a91eca645-9905944ed6.zip deleted file mode 100644 index fa33e88f1..000000000 Binary files a/.yarn/cache/@casl-ability-npm-6.7.3-4a91eca645-9905944ed6.zip and /dev/null differ diff --git a/.yarn/cache/@casl-ability-npm-6.8.0-1d6f8b738c-a0ccee805e.zip b/.yarn/cache/@casl-ability-npm-6.8.0-1d6f8b738c-a0ccee805e.zip new file mode 100644 index 000000000..5e02d5543 Binary files /dev/null and b/.yarn/cache/@casl-ability-npm-6.8.0-1d6f8b738c-a0ccee805e.zip differ diff --git a/.yarn/cache/@cfworker-json-schema-npm-4.1.1-87f64b1127-35b5b246ef.zip b/.yarn/cache/@cfworker-json-schema-npm-4.1.1-87f64b1127-35b5b246ef.zip new file mode 100644 index 000000000..63f3ea6d8 Binary files /dev/null and b/.yarn/cache/@cfworker-json-schema-npm-4.1.1-87f64b1127-35b5b246ef.zip differ diff --git a/.yarn/cache/@eslint-community-eslint-utils-npm-4.9.1-30ad3d49de-0a27c2d676.zip b/.yarn/cache/@eslint-community-eslint-utils-npm-4.9.1-30ad3d49de-0a27c2d676.zip new file mode 100644 index 000000000..928fd6302 Binary files /dev/null and b/.yarn/cache/@eslint-community-eslint-utils-npm-4.9.1-30ad3d49de-0a27c2d676.zip differ diff --git a/.yarn/cache/@eslint-community-regexpp-npm-4.12.2-3d54624470-1770bc81f6.zip b/.yarn/cache/@eslint-community-regexpp-npm-4.12.2-3d54624470-1770bc81f6.zip new file mode 100644 index 000000000..cc59c7cdc Binary files /dev/null and b/.yarn/cache/@eslint-community-regexpp-npm-4.12.2-3d54624470-1770bc81f6.zip differ diff --git a/.yarn/cache/@eslint-compat-npm-2.0.2-c914a79492-c2445b59ea.zip b/.yarn/cache/@eslint-compat-npm-2.0.2-c914a79492-c2445b59ea.zip new file mode 100644 index 000000000..93fe917a7 Binary files /dev/null and b/.yarn/cache/@eslint-compat-npm-2.0.2-c914a79492-c2445b59ea.zip differ diff --git a/.yarn/cache/@eslint-config-array-npm-0.21.1-c33ed9ec91-fc5b57803b.zip b/.yarn/cache/@eslint-config-array-npm-0.21.1-c33ed9ec91-fc5b57803b.zip new file mode 100644 index 000000000..021394cea Binary files /dev/null and b/.yarn/cache/@eslint-config-array-npm-0.21.1-c33ed9ec91-fc5b57803b.zip differ diff --git a/.yarn/cache/@eslint-config-helpers-npm-0.4.2-a55655f805-63ff6a0730.zip b/.yarn/cache/@eslint-config-helpers-npm-0.4.2-a55655f805-63ff6a0730.zip new file mode 100644 index 000000000..2e8c143cf Binary files /dev/null and b/.yarn/cache/@eslint-config-helpers-npm-0.4.2-a55655f805-63ff6a0730.zip differ diff --git a/.yarn/cache/@eslint-core-npm-0.17.0-8579df04c4-ff9b5b4987.zip b/.yarn/cache/@eslint-core-npm-0.17.0-8579df04c4-ff9b5b4987.zip new file mode 100644 index 000000000..6fcab27ff Binary files /dev/null and b/.yarn/cache/@eslint-core-npm-0.17.0-8579df04c4-ff9b5b4987.zip differ diff --git a/.yarn/cache/@eslint-core-npm-1.1.0-4eb5580cb4-8f0f11540c.zip b/.yarn/cache/@eslint-core-npm-1.1.0-4eb5580cb4-8f0f11540c.zip new file mode 100644 index 000000000..606c3502e Binary files /dev/null and b/.yarn/cache/@eslint-core-npm-1.1.0-4eb5580cb4-8f0f11540c.zip differ diff --git a/.yarn/cache/@eslint-eslintrc-npm-3.3.3-8ccf6281a3-d1e16e47f1.zip b/.yarn/cache/@eslint-eslintrc-npm-3.3.3-8ccf6281a3-d1e16e47f1.zip new file mode 100644 index 000000000..5d6ca22ef Binary files /dev/null and b/.yarn/cache/@eslint-eslintrc-npm-3.3.3-8ccf6281a3-d1e16e47f1.zip differ diff --git a/.yarn/cache/@eslint-js-npm-9.39.2-c8e5f9bf73-362aa44726.zip b/.yarn/cache/@eslint-js-npm-9.39.2-c8e5f9bf73-362aa44726.zip new file mode 100644 index 000000000..bd69a5133 Binary files /dev/null and b/.yarn/cache/@eslint-js-npm-9.39.2-c8e5f9bf73-362aa44726.zip differ diff --git a/.yarn/cache/@eslint-object-schema-npm-2.1.7-cb962a5b9b-fc5708f192.zip b/.yarn/cache/@eslint-object-schema-npm-2.1.7-cb962a5b9b-fc5708f192.zip new file mode 100644 index 000000000..b4b967254 Binary files /dev/null and b/.yarn/cache/@eslint-object-schema-npm-2.1.7-cb962a5b9b-fc5708f192.zip differ diff --git a/.yarn/cache/@eslint-plugin-kit-npm-0.4.1-3df70dd079-3f4492e02a.zip b/.yarn/cache/@eslint-plugin-kit-npm-0.4.1-3df70dd079-3f4492e02a.zip new file mode 100644 index 000000000..28ad54445 Binary files /dev/null and b/.yarn/cache/@eslint-plugin-kit-npm-0.4.1-3df70dd079-3f4492e02a.zip differ diff --git a/.yarn/cache/@hapi-address-npm-5.1.1-2944e6ed9b-678790d86d.zip b/.yarn/cache/@hapi-address-npm-5.1.1-2944e6ed9b-678790d86d.zip new file mode 100644 index 000000000..02e435362 Binary files /dev/null and b/.yarn/cache/@hapi-address-npm-5.1.1-2944e6ed9b-678790d86d.zip differ diff --git a/.yarn/cache/@sideway-formula-npm-3.0.1-ee371b2ddf-e4beeebc9d.zip b/.yarn/cache/@hapi-formula-npm-3.0.2-70d8fffc4e-a774d8133e.zip similarity index 67% rename from .yarn/cache/@sideway-formula-npm-3.0.1-ee371b2ddf-e4beeebc9d.zip rename to .yarn/cache/@hapi-formula-npm-3.0.2-70d8fffc4e-a774d8133e.zip index c4835760e..83759c826 100644 Binary files a/.yarn/cache/@sideway-formula-npm-3.0.1-ee371b2ddf-e4beeebc9d.zip and b/.yarn/cache/@hapi-formula-npm-3.0.2-70d8fffc4e-a774d8133e.zip differ diff --git a/.yarn/cache/@hapi-hoek-npm-11.0.7-67a33297f6-3da5546649.zip b/.yarn/cache/@hapi-hoek-npm-11.0.7-67a33297f6-3da5546649.zip new file mode 100644 index 000000000..9437ae6ba Binary files /dev/null and b/.yarn/cache/@hapi-hoek-npm-11.0.7-67a33297f6-3da5546649.zip differ diff --git a/.yarn/cache/@hapi-hoek-npm-9.3.0-447eb8d274-4771c7a776.zip b/.yarn/cache/@hapi-hoek-npm-9.3.0-447eb8d274-4771c7a776.zip deleted file mode 100644 index ff8a0ee9c..000000000 Binary files a/.yarn/cache/@hapi-hoek-npm-9.3.0-447eb8d274-4771c7a776.zip and /dev/null differ diff --git a/.yarn/cache/@hapi-pinpoint-npm-2.0.1-4da9d8fcbc-8a1bb399f7.zip b/.yarn/cache/@hapi-pinpoint-npm-2.0.1-4da9d8fcbc-8a1bb399f7.zip new file mode 100644 index 000000000..bc67ff2fb Binary files /dev/null and b/.yarn/cache/@hapi-pinpoint-npm-2.0.1-4da9d8fcbc-8a1bb399f7.zip differ diff --git a/.yarn/cache/@hapi-tlds-npm-1.1.4-443bc2e9a1-2bd39d5198.zip b/.yarn/cache/@hapi-tlds-npm-1.1.4-443bc2e9a1-2bd39d5198.zip new file mode 100644 index 000000000..b1c71d188 Binary files /dev/null and b/.yarn/cache/@hapi-tlds-npm-1.1.4-443bc2e9a1-2bd39d5198.zip differ diff --git a/.yarn/cache/@hapi-topo-npm-5.1.0-5e0b776809-604dfd5dde.zip b/.yarn/cache/@hapi-topo-npm-5.1.0-5e0b776809-604dfd5dde.zip deleted file mode 100644 index de25bd9e8..000000000 Binary files a/.yarn/cache/@hapi-topo-npm-5.1.0-5e0b776809-604dfd5dde.zip and /dev/null differ diff --git a/.yarn/cache/@hapi-topo-npm-6.0.2-2ca029706c-c11da8a995.zip b/.yarn/cache/@hapi-topo-npm-6.0.2-2ca029706c-c11da8a995.zip new file mode 100644 index 000000000..89e246f79 Binary files /dev/null and b/.yarn/cache/@hapi-topo-npm-6.0.2-2ca029706c-c11da8a995.zip differ diff --git a/.yarn/cache/@humanfs-core-npm-0.19.1-e2e7aaeb6e-611e054514.zip b/.yarn/cache/@humanfs-core-npm-0.19.1-e2e7aaeb6e-611e054514.zip new file mode 100644 index 000000000..a14c26465 Binary files /dev/null and b/.yarn/cache/@humanfs-core-npm-0.19.1-e2e7aaeb6e-611e054514.zip differ diff --git a/.yarn/cache/@humanfs-node-npm-0.16.7-fa16bdb590-7d2a396a94.zip b/.yarn/cache/@humanfs-node-npm-0.16.7-fa16bdb590-7d2a396a94.zip new file mode 100644 index 000000000..c219dfdc7 Binary files /dev/null and b/.yarn/cache/@humanfs-node-npm-0.16.7-fa16bdb590-7d2a396a94.zip differ diff --git a/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip b/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip new file mode 100644 index 000000000..7adb1e9f2 Binary files /dev/null and b/.yarn/cache/@humanwhocodes-module-importer-npm-1.0.1-9d07ed2e4a-0fd22007db.zip differ diff --git a/.yarn/cache/@humanwhocodes-retry-npm-0.4.3-a8d7ca1663-d423455b9d.zip b/.yarn/cache/@humanwhocodes-retry-npm-0.4.3-a8d7ca1663-d423455b9d.zip new file mode 100644 index 000000000..6d0c37020 Binary files /dev/null and b/.yarn/cache/@humanwhocodes-retry-npm-0.4.3-a8d7ca1663-d423455b9d.zip differ diff --git a/.yarn/cache/@langchain-classic-npm-1.0.18-4635892e2a-0141934d90.zip b/.yarn/cache/@langchain-classic-npm-1.0.18-4635892e2a-0141934d90.zip new file mode 100644 index 000000000..ded3c261f Binary files /dev/null and b/.yarn/cache/@langchain-classic-npm-1.0.18-4635892e2a-0141934d90.zip differ diff --git a/.yarn/cache/@langchain-community-npm-0.0.54-1440fe79c8-bfc01f4dc0.zip b/.yarn/cache/@langchain-community-npm-0.0.54-1440fe79c8-bfc01f4dc0.zip deleted file mode 100644 index 9f58071c7..000000000 Binary files a/.yarn/cache/@langchain-community-npm-0.0.54-1440fe79c8-bfc01f4dc0.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-community-npm-0.0.57-4ba16b61c4-9a9f8396af.zip b/.yarn/cache/@langchain-community-npm-0.0.57-4ba16b61c4-9a9f8396af.zip deleted file mode 100644 index be05726d8..000000000 Binary files a/.yarn/cache/@langchain-community-npm-0.0.57-4ba16b61c4-9a9f8396af.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-community-npm-1.1.16-0f324b8893-de64f5f81c.zip b/.yarn/cache/@langchain-community-npm-1.1.16-0f324b8893-de64f5f81c.zip new file mode 100644 index 000000000..064b133b1 Binary files /dev/null and b/.yarn/cache/@langchain-community-npm-1.1.16-0f324b8893-de64f5f81c.zip differ diff --git a/.yarn/cache/@langchain-core-npm-0.1.63-551e581297-0aa18f55e5.zip b/.yarn/cache/@langchain-core-npm-0.1.63-551e581297-0aa18f55e5.zip deleted file mode 100644 index 964a58a6f..000000000 Binary files a/.yarn/cache/@langchain-core-npm-0.1.63-551e581297-0aa18f55e5.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-core-npm-0.2.36-6e77c597ed-963aca44a8.zip b/.yarn/cache/@langchain-core-npm-0.2.36-6e77c597ed-963aca44a8.zip deleted file mode 100644 index 864a58f0b..000000000 Binary files a/.yarn/cache/@langchain-core-npm-0.2.36-6e77c597ed-963aca44a8.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-core-npm-1.1.26-c10d54b47a-ff4a294e26.zip b/.yarn/cache/@langchain-core-npm-1.1.26-c10d54b47a-ff4a294e26.zip new file mode 100644 index 000000000..c375dddcf Binary files /dev/null and b/.yarn/cache/@langchain-core-npm-1.1.26-c10d54b47a-ff4a294e26.zip differ diff --git a/.yarn/cache/@langchain-langgraph-checkpoint-npm-1.0.0-dbc155d41b-0e85633bd7.zip b/.yarn/cache/@langchain-langgraph-checkpoint-npm-1.0.0-dbc155d41b-0e85633bd7.zip new file mode 100644 index 000000000..abdc5685c Binary files /dev/null and b/.yarn/cache/@langchain-langgraph-checkpoint-npm-1.0.0-dbc155d41b-0e85633bd7.zip differ diff --git a/.yarn/cache/@langchain-langgraph-npm-1.1.5-83139dadb8-b1b9c848d7.zip b/.yarn/cache/@langchain-langgraph-npm-1.1.5-83139dadb8-b1b9c848d7.zip new file mode 100644 index 000000000..ea352456d Binary files /dev/null and b/.yarn/cache/@langchain-langgraph-npm-1.1.5-83139dadb8-b1b9c848d7.zip differ diff --git a/.yarn/cache/@langchain-langgraph-sdk-npm-2.0.0-d64052591c-17b73bdb25.zip b/.yarn/cache/@langchain-langgraph-sdk-npm-2.0.0-d64052591c-17b73bdb25.zip new file mode 100644 index 000000000..b4aab8cb2 Binary files /dev/null and b/.yarn/cache/@langchain-langgraph-sdk-npm-2.0.0-d64052591c-17b73bdb25.zip differ diff --git a/.yarn/cache/@langchain-openai-npm-0.0.28-9e023e2c57-ba7c8e4e57.zip b/.yarn/cache/@langchain-openai-npm-0.0.28-9e023e2c57-ba7c8e4e57.zip deleted file mode 100644 index b55006f94..000000000 Binary files a/.yarn/cache/@langchain-openai-npm-0.0.28-9e023e2c57-ba7c8e4e57.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-openai-npm-0.0.34-1381fec0b4-d2a5568b7f.zip b/.yarn/cache/@langchain-openai-npm-0.0.34-1381fec0b4-d2a5568b7f.zip deleted file mode 100644 index a7d224602..000000000 Binary files a/.yarn/cache/@langchain-openai-npm-0.0.34-1381fec0b4-d2a5568b7f.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-openai-npm-1.2.8-8789b38cec-0f5de0f6a7.zip b/.yarn/cache/@langchain-openai-npm-1.2.8-8789b38cec-0f5de0f6a7.zip new file mode 100644 index 000000000..7b58d8933 Binary files /dev/null and b/.yarn/cache/@langchain-openai-npm-1.2.8-8789b38cec-0f5de0f6a7.zip differ diff --git a/.yarn/cache/@langchain-textsplitters-npm-0.0.3-ee8c3b89b5-f0b32d65c8.zip b/.yarn/cache/@langchain-textsplitters-npm-0.0.3-ee8c3b89b5-f0b32d65c8.zip deleted file mode 100644 index 83f6ee4a1..000000000 Binary files a/.yarn/cache/@langchain-textsplitters-npm-0.0.3-ee8c3b89b5-f0b32d65c8.zip and /dev/null differ diff --git a/.yarn/cache/@langchain-textsplitters-npm-1.0.1-450fed13e1-1a553ee321.zip b/.yarn/cache/@langchain-textsplitters-npm-1.0.1-450fed13e1-1a553ee321.zip new file mode 100644 index 000000000..fa8dd0c1e Binary files /dev/null and b/.yarn/cache/@langchain-textsplitters-npm-1.0.1-450fed13e1-1a553ee321.zip differ diff --git a/.yarn/cache/@nestjs-common-npm-10.4.22-982ebf7b77-f78006a582.zip b/.yarn/cache/@nestjs-common-npm-10.4.22-982ebf7b77-f78006a582.zip new file mode 100644 index 000000000..dda8448d7 Binary files /dev/null and b/.yarn/cache/@nestjs-common-npm-10.4.22-982ebf7b77-f78006a582.zip differ diff --git a/.yarn/cache/@nestjs-common-npm-9.4.3-1b6d11580a-45ccb5acac.zip b/.yarn/cache/@nestjs-common-npm-9.4.3-1b6d11580a-45ccb5acac.zip deleted file mode 100644 index e0145fc4c..000000000 Binary files a/.yarn/cache/@nestjs-common-npm-9.4.3-1b6d11580a-45ccb5acac.zip and /dev/null differ diff --git a/.yarn/cache/@sideway-address-npm-4.1.5-a3852745c8-3e3ea0f00b.zip b/.yarn/cache/@sideway-address-npm-4.1.5-a3852745c8-3e3ea0f00b.zip deleted file mode 100644 index 5f8f3d403..000000000 Binary files a/.yarn/cache/@sideway-address-npm-4.1.5-a3852745c8-3e3ea0f00b.zip and /dev/null differ diff --git a/.yarn/cache/@sideway-pinpoint-npm-2.0.0-66d94e687e-0f4491e589.zip b/.yarn/cache/@sideway-pinpoint-npm-2.0.0-66d94e687e-0f4491e589.zip deleted file mode 100644 index dec4ec260..000000000 Binary files a/.yarn/cache/@sideway-pinpoint-npm-2.0.0-66d94e687e-0f4491e589.zip and /dev/null differ diff --git a/.yarn/cache/@standard-schema-spec-npm-1.1.0-d3e5ccd2e2-6245ebef5e.zip b/.yarn/cache/@standard-schema-spec-npm-1.1.0-d3e5ccd2e2-6245ebef5e.zip new file mode 100644 index 000000000..960a30dc7 Binary files /dev/null and b/.yarn/cache/@standard-schema-spec-npm-1.1.0-d3e5ccd2e2-6245ebef5e.zip differ diff --git a/.yarn/cache/@storybook-builder-manager-npm-7.6.20-46d2068fd1-3a0129e8e2.zip b/.yarn/cache/@storybook-builder-manager-npm-7.6.21-1784fa2be6-07bc0afc6a.zip similarity index 96% rename from .yarn/cache/@storybook-builder-manager-npm-7.6.20-46d2068fd1-3a0129e8e2.zip rename to .yarn/cache/@storybook-builder-manager-npm-7.6.21-1784fa2be6-07bc0afc6a.zip index 2e864e60f..fff75f6aa 100644 Binary files a/.yarn/cache/@storybook-builder-manager-npm-7.6.20-46d2068fd1-3a0129e8e2.zip and b/.yarn/cache/@storybook-builder-manager-npm-7.6.21-1784fa2be6-07bc0afc6a.zip differ diff --git a/.yarn/cache/@storybook-channels-npm-7.6.21-1ead97d3d9-0d08c340b3.zip b/.yarn/cache/@storybook-channels-npm-7.6.21-1ead97d3d9-0d08c340b3.zip new file mode 100644 index 000000000..bb14844ba Binary files /dev/null and b/.yarn/cache/@storybook-channels-npm-7.6.21-1ead97d3d9-0d08c340b3.zip differ diff --git a/.yarn/cache/@storybook-cli-npm-7.6.20-e80cdc4ae1-a3e0334c95.zip b/.yarn/cache/@storybook-cli-npm-7.6.21-9dac7231d1-2e06c6edb1.zip similarity index 89% rename from .yarn/cache/@storybook-cli-npm-7.6.20-e80cdc4ae1-a3e0334c95.zip rename to .yarn/cache/@storybook-cli-npm-7.6.21-9dac7231d1-2e06c6edb1.zip index 19b34c774..ec0072242 100644 Binary files a/.yarn/cache/@storybook-cli-npm-7.6.20-e80cdc4ae1-a3e0334c95.zip and b/.yarn/cache/@storybook-cli-npm-7.6.21-9dac7231d1-2e06c6edb1.zip differ diff --git a/.yarn/cache/@storybook-client-logger-npm-7.6.21-4b01088606-0b2cf1fd27.zip b/.yarn/cache/@storybook-client-logger-npm-7.6.21-4b01088606-0b2cf1fd27.zip new file mode 100644 index 000000000..2a826e9bb Binary files /dev/null and b/.yarn/cache/@storybook-client-logger-npm-7.6.21-4b01088606-0b2cf1fd27.zip differ diff --git a/.yarn/cache/@storybook-codemod-npm-7.6.20-47e412aa1b-e8a507ea29.zip b/.yarn/cache/@storybook-codemod-npm-7.6.21-4e627d2766-768d447872.zip similarity index 99% rename from .yarn/cache/@storybook-codemod-npm-7.6.20-47e412aa1b-e8a507ea29.zip rename to .yarn/cache/@storybook-codemod-npm-7.6.21-4e627d2766-768d447872.zip index cad2d6b52..9710889f7 100644 Binary files a/.yarn/cache/@storybook-codemod-npm-7.6.20-47e412aa1b-e8a507ea29.zip and b/.yarn/cache/@storybook-codemod-npm-7.6.21-4e627d2766-768d447872.zip differ diff --git a/.yarn/cache/@storybook-core-common-npm-7.6.21-2f8a0e2e5f-4de8f501cd.zip b/.yarn/cache/@storybook-core-common-npm-7.6.21-2f8a0e2e5f-4de8f501cd.zip new file mode 100644 index 000000000..722234dee Binary files /dev/null and b/.yarn/cache/@storybook-core-common-npm-7.6.21-2f8a0e2e5f-4de8f501cd.zip differ diff --git a/.yarn/cache/@storybook-core-events-npm-7.6.21-f937b33add-65db88aff2.zip b/.yarn/cache/@storybook-core-events-npm-7.6.21-f937b33add-65db88aff2.zip new file mode 100644 index 000000000..eb8a44084 Binary files /dev/null and b/.yarn/cache/@storybook-core-events-npm-7.6.21-f937b33add-65db88aff2.zip differ diff --git a/.yarn/cache/@storybook-core-server-npm-7.6.20-aa5644a55a-85ca3c14f0.zip b/.yarn/cache/@storybook-core-server-npm-7.6.21-8d41d74feb-e19862e6da.zip similarity index 92% rename from .yarn/cache/@storybook-core-server-npm-7.6.20-aa5644a55a-85ca3c14f0.zip rename to .yarn/cache/@storybook-core-server-npm-7.6.21-8d41d74feb-e19862e6da.zip index 92a9c7a25..43df1f86d 100644 Binary files a/.yarn/cache/@storybook-core-server-npm-7.6.20-aa5644a55a-85ca3c14f0.zip and b/.yarn/cache/@storybook-core-server-npm-7.6.21-8d41d74feb-e19862e6da.zip differ diff --git a/.yarn/cache/@storybook-csf-tools-npm-7.6.21-82dffd4457-3c7d619a4c.zip b/.yarn/cache/@storybook-csf-tools-npm-7.6.21-82dffd4457-3c7d619a4c.zip new file mode 100644 index 000000000..d2d027945 Binary files /dev/null and b/.yarn/cache/@storybook-csf-tools-npm-7.6.21-82dffd4457-3c7d619a4c.zip differ diff --git a/.yarn/cache/@storybook-manager-npm-7.6.20-c27c893506-4596149367.zip b/.yarn/cache/@storybook-manager-npm-7.6.21-7d6e54ab57-b2f171c3ca.zip similarity index 99% rename from .yarn/cache/@storybook-manager-npm-7.6.20-c27c893506-4596149367.zip rename to .yarn/cache/@storybook-manager-npm-7.6.21-7d6e54ab57-b2f171c3ca.zip index 1139dd4b4..2f90de831 100644 Binary files a/.yarn/cache/@storybook-manager-npm-7.6.20-c27c893506-4596149367.zip and b/.yarn/cache/@storybook-manager-npm-7.6.21-7d6e54ab57-b2f171c3ca.zip differ diff --git a/.yarn/cache/@storybook-node-logger-npm-7.6.21-7c0e5cc8ab-52685ccc58.zip b/.yarn/cache/@storybook-node-logger-npm-7.6.21-7c0e5cc8ab-52685ccc58.zip new file mode 100644 index 000000000..b17867268 Binary files /dev/null and b/.yarn/cache/@storybook-node-logger-npm-7.6.21-7c0e5cc8ab-52685ccc58.zip differ diff --git a/.yarn/cache/@storybook-preview-api-npm-7.6.21-634c10d6d8-5e81f22f9d.zip b/.yarn/cache/@storybook-preview-api-npm-7.6.21-634c10d6d8-5e81f22f9d.zip new file mode 100644 index 000000000..39dc16fe7 Binary files /dev/null and b/.yarn/cache/@storybook-preview-api-npm-7.6.21-634c10d6d8-5e81f22f9d.zip differ diff --git a/.yarn/cache/@storybook-telemetry-npm-7.6.20-94649f125d-546f2da998.zip b/.yarn/cache/@storybook-telemetry-npm-7.6.21-885b2fc1b7-b51aebeca1.zip similarity index 98% rename from .yarn/cache/@storybook-telemetry-npm-7.6.20-94649f125d-546f2da998.zip rename to .yarn/cache/@storybook-telemetry-npm-7.6.21-885b2fc1b7-b51aebeca1.zip index 1f198b9ee..d1be28995 100644 Binary files a/.yarn/cache/@storybook-telemetry-npm-7.6.20-94649f125d-546f2da998.zip and b/.yarn/cache/@storybook-telemetry-npm-7.6.21-885b2fc1b7-b51aebeca1.zip differ diff --git a/.yarn/cache/@storybook-types-npm-7.6.21-81f9e070cb-95b7d9cc9b.zip b/.yarn/cache/@storybook-types-npm-7.6.21-81f9e070cb-95b7d9cc9b.zip new file mode 100644 index 000000000..3ee31478a Binary files /dev/null and b/.yarn/cache/@storybook-types-npm-7.6.21-81f9e070cb-95b7d9cc9b.zip differ diff --git a/.yarn/cache/@tokenizer-inflate-npm-0.2.7-1d126e1d4f-0c77759d45.zip b/.yarn/cache/@tokenizer-inflate-npm-0.2.7-1d126e1d4f-0c77759d45.zip new file mode 100644 index 000000000..ba9f44d56 Binary files /dev/null and b/.yarn/cache/@tokenizer-inflate-npm-0.2.7-1d126e1d4f-0c77759d45.zip differ diff --git a/.yarn/cache/@tokenizer-token-npm-0.3.0-4441352cc5-1d575d02d2.zip b/.yarn/cache/@tokenizer-token-npm-0.3.0-4441352cc5-1d575d02d2.zip new file mode 100644 index 000000000..e4b734d5e Binary files /dev/null and b/.yarn/cache/@tokenizer-token-npm-0.3.0-4441352cc5-1d575d02d2.zip differ diff --git a/.yarn/cache/@types-diff-match-patch-npm-1.0.36-f65ece6691-7d7ce03422.zip b/.yarn/cache/@types-diff-match-patch-npm-1.0.36-f65ece6691-7d7ce03422.zip deleted file mode 100644 index 8407c9e2e..000000000 Binary files a/.yarn/cache/@types-diff-match-patch-npm-1.0.36-f65ece6691-7d7ce03422.zip and /dev/null differ diff --git a/.yarn/cache/@types-retry-npm-0.12.0-e4e6294a2c-61a072c763.zip b/.yarn/cache/@types-retry-npm-0.12.0-e4e6294a2c-61a072c763.zip deleted file mode 100644 index f7c0ed21e..000000000 Binary files a/.yarn/cache/@types-retry-npm-0.12.0-e4e6294a2c-61a072c763.zip and /dev/null differ diff --git a/.yarn/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip b/.yarn/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip deleted file mode 100644 index 2e52119d4..000000000 Binary files a/.yarn/cache/@typescript-eslint-parser-npm-4.33.0-799c6ce8d5-102457eae1.zip and /dev/null differ diff --git a/.yarn/cache/@typescript-eslint-parser-npm-8.54.0-9b917cc1cb-1a4c8c6edd.zip b/.yarn/cache/@typescript-eslint-parser-npm-8.54.0-9b917cc1cb-1a4c8c6edd.zip new file mode 100644 index 000000000..c2a793b22 Binary files /dev/null and b/.yarn/cache/@typescript-eslint-parser-npm-8.54.0-9b917cc1cb-1a4c8c6edd.zip differ diff --git a/.yarn/cache/@typescript-eslint-project-service-npm-8.54.0-df3e89508b-3c2a5c758a.zip b/.yarn/cache/@typescript-eslint-project-service-npm-8.54.0-df3e89508b-3c2a5c758a.zip new file mode 100644 index 000000000..29980a281 Binary files /dev/null and b/.yarn/cache/@typescript-eslint-project-service-npm-8.54.0-df3e89508b-3c2a5c758a.zip differ diff --git a/.yarn/cache/@typescript-eslint-scope-manager-npm-8.54.0-4f2ea67471-9a6bbdf019.zip b/.yarn/cache/@typescript-eslint-scope-manager-npm-8.54.0-4f2ea67471-9a6bbdf019.zip new file mode 100644 index 000000000..b2ef5d16e Binary files /dev/null and b/.yarn/cache/@typescript-eslint-scope-manager-npm-8.54.0-4f2ea67471-9a6bbdf019.zip differ diff --git a/.yarn/cache/@typescript-eslint-tsconfig-utils-npm-8.54.0-73efc09e2a-f8907f6e80.zip b/.yarn/cache/@typescript-eslint-tsconfig-utils-npm-8.54.0-73efc09e2a-f8907f6e80.zip new file mode 100644 index 000000000..9b3d07f9d Binary files /dev/null and b/.yarn/cache/@typescript-eslint-tsconfig-utils-npm-8.54.0-73efc09e2a-f8907f6e80.zip differ diff --git a/.yarn/cache/@typescript-eslint-types-npm-8.54.0-75d29a589d-53ee5c5ef8.zip b/.yarn/cache/@typescript-eslint-types-npm-8.54.0-75d29a589d-53ee5c5ef8.zip new file mode 100644 index 000000000..b7e432840 Binary files /dev/null and b/.yarn/cache/@typescript-eslint-types-npm-8.54.0-75d29a589d-53ee5c5ef8.zip differ diff --git a/.yarn/cache/@typescript-eslint-typescript-estree-npm-8.54.0-643fee1e58-0a4cf84abb.zip b/.yarn/cache/@typescript-eslint-typescript-estree-npm-8.54.0-643fee1e58-0a4cf84abb.zip new file mode 100644 index 000000000..ae8b995c7 Binary files /dev/null and b/.yarn/cache/@typescript-eslint-typescript-estree-npm-8.54.0-643fee1e58-0a4cf84abb.zip differ diff --git a/.yarn/cache/@typescript-eslint-visitor-keys-npm-8.54.0-b1e3d0cbc5-36aafcffee.zip b/.yarn/cache/@typescript-eslint-visitor-keys-npm-8.54.0-b1e3d0cbc5-36aafcffee.zip new file mode 100644 index 000000000..8a93bee56 Binary files /dev/null and b/.yarn/cache/@typescript-eslint-visitor-keys-npm-8.54.0-b1e3d0cbc5-36aafcffee.zip differ diff --git a/.yarn/cache/agentkeepalive-npm-4.6.0-6b61ca2a37-b3cdd10efc.zip b/.yarn/cache/agentkeepalive-npm-4.6.0-6b61ca2a37-b3cdd10efc.zip deleted file mode 100644 index c0e0cb910..000000000 Binary files a/.yarn/cache/agentkeepalive-npm-4.6.0-6b61ca2a37-b3cdd10efc.zip and /dev/null differ diff --git a/.yarn/cache/ai-npm-3.4.33-540588214d-e46ea18f20.zip b/.yarn/cache/ai-npm-3.4.33-540588214d-e46ea18f20.zip deleted file mode 100644 index ae497d7cc..000000000 Binary files a/.yarn/cache/ai-npm-3.4.33-540588214d-e46ea18f20.zip and /dev/null differ diff --git a/.yarn/cache/ai-npm-5.0.52-d57b539c0b-496f07b394.zip b/.yarn/cache/ai-npm-5.0.52-d57b539c0b-496f07b394.zip new file mode 100644 index 000000000..4db9596de Binary files /dev/null and b/.yarn/cache/ai-npm-5.0.52-d57b539c0b-496f07b394.zip differ diff --git a/.yarn/cache/ansi-regex-npm-4.1.1-af0a582bb9-b1a6ee44cb.zip b/.yarn/cache/ansi-regex-npm-4.1.1-af0a582bb9-b1a6ee44cb.zip deleted file mode 100644 index e56881103..000000000 Binary files a/.yarn/cache/ansi-regex-npm-4.1.1-af0a582bb9-b1a6ee44cb.zip and /dev/null differ diff --git a/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip b/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip deleted file mode 100644 index 4ffdcc494..000000000 Binary files a/.yarn/cache/ansi-styles-npm-3.2.1-8cb8107983-d85ade01c1.zip and /dev/null differ diff --git a/.yarn/cache/astral-regex-npm-1.0.0-2df7c41332-93417fc087.zip b/.yarn/cache/astral-regex-npm-1.0.0-2df7c41332-93417fc087.zip deleted file mode 100644 index d8a1b724e..000000000 Binary files a/.yarn/cache/astral-regex-npm-1.0.0-2df7c41332-93417fc087.zip and /dev/null differ diff --git a/.yarn/cache/axios-npm-0.25.0-a1c287d287-2a8a3787c0.zip b/.yarn/cache/axios-npm-0.25.0-a1c287d287-2a8a3787c0.zip deleted file mode 100644 index bdd1f0fa8..000000000 Binary files a/.yarn/cache/axios-npm-0.25.0-a1c287d287-2a8a3787c0.zip and /dev/null differ diff --git a/.yarn/cache/axios-npm-1.13.4-7c78039beb-1d1f360cf5.zip b/.yarn/cache/axios-npm-1.13.4-7c78039beb-1d1f360cf5.zip new file mode 100644 index 000000000..29fc922e1 Binary files /dev/null and b/.yarn/cache/axios-npm-1.13.4-7c78039beb-1d1f360cf5.zip differ diff --git a/.yarn/cache/base-64-npm-0.1.0-41e6da6777-5a42938f82.zip b/.yarn/cache/base-64-npm-0.1.0-41e6da6777-5a42938f82.zip deleted file mode 100644 index 55635d228..000000000 Binary files a/.yarn/cache/base-64-npm-0.1.0-41e6da6777-5a42938f82.zip and /dev/null differ diff --git a/.yarn/cache/binary-search-npm-1.3.6-b150a83e72-2e6b3459a9.zip b/.yarn/cache/binary-search-npm-1.3.6-b150a83e72-2e6b3459a9.zip deleted file mode 100644 index fdf4e4157..000000000 Binary files a/.yarn/cache/binary-search-npm-1.3.6-b150a83e72-2e6b3459a9.zip and /dev/null differ diff --git a/.yarn/cache/chalk-npm-5.6.2-ecbd482482-4ee2d47a62.zip b/.yarn/cache/chalk-npm-5.6.2-ecbd482482-4ee2d47a62.zip deleted file mode 100644 index 4dcea74de..000000000 Binary files a/.yarn/cache/chalk-npm-5.6.2-ecbd482482-4ee2d47a62.zip and /dev/null differ diff --git a/.yarn/cache/confusing-browser-globals-npm-1.0.11-b3ff8e9483-3afc635abd.zip b/.yarn/cache/confusing-browser-globals-npm-1.0.11-b3ff8e9483-3afc635abd.zip deleted file mode 100644 index 19db1b915..000000000 Binary files a/.yarn/cache/confusing-browser-globals-npm-1.0.11-b3ff8e9483-3afc635abd.zip and /dev/null differ diff --git a/.yarn/cache/console-table-printer-npm-2.15.0-1b2717088e-a878e44630.zip b/.yarn/cache/console-table-printer-npm-2.15.0-1b2717088e-a878e44630.zip new file mode 100644 index 000000000..b59c19bee Binary files /dev/null and b/.yarn/cache/console-table-printer-npm-2.15.0-1b2717088e-a878e44630.zip differ diff --git a/.yarn/cache/cookie-npm-0.6.0-362d6a2e45-f56a7d32a0.zip b/.yarn/cache/cookie-npm-0.6.0-362d6a2e45-f56a7d32a0.zip new file mode 100644 index 000000000..8bec72811 Binary files /dev/null and b/.yarn/cache/cookie-npm-0.6.0-362d6a2e45-f56a7d32a0.zip differ diff --git a/.yarn/cache/debug-npm-4.4.3-0105c6123a-4805abd570.zip b/.yarn/cache/debug-npm-4.4.3-0105c6123a-4805abd570.zip new file mode 100644 index 000000000..d2c0c4247 Binary files /dev/null and b/.yarn/cache/debug-npm-4.4.3-0105c6123a-4805abd570.zip differ diff --git a/.yarn/cache/diff-match-patch-npm-1.0.5-f715ad1381-841522d01b.zip b/.yarn/cache/diff-match-patch-npm-1.0.5-f715ad1381-841522d01b.zip deleted file mode 100644 index ccb35d84d..000000000 Binary files a/.yarn/cache/diff-match-patch-npm-1.0.5-f715ad1381-841522d01b.zip and /dev/null differ diff --git a/.yarn/cache/digest-fetch-npm-1.3.0-00876b1fae-8ebdb4b9ef.zip b/.yarn/cache/digest-fetch-npm-1.3.0-00876b1fae-8ebdb4b9ef.zip deleted file mode 100644 index f3302e37f..000000000 Binary files a/.yarn/cache/digest-fetch-npm-1.3.0-00876b1fae-8ebdb4b9ef.zip and /dev/null differ diff --git a/.yarn/cache/emoji-regex-npm-7.0.3-cfe9479bb3-9159b2228b.zip b/.yarn/cache/emoji-regex-npm-7.0.3-cfe9479bb3-9159b2228b.zip deleted file mode 100644 index 22e27d234..000000000 Binary files a/.yarn/cache/emoji-regex-npm-7.0.3-cfe9479bb3-9159b2228b.zip and /dev/null differ diff --git a/.yarn/cache/eslint-config-react-app-npm-6.0.0-c5908e735c-b265852455.zip b/.yarn/cache/eslint-config-react-app-npm-6.0.0-c5908e735c-b265852455.zip deleted file mode 100644 index 268b47656..000000000 Binary files a/.yarn/cache/eslint-config-react-app-npm-6.0.0-c5908e735c-b265852455.zip and /dev/null differ diff --git a/.yarn/cache/eslint-npm-7.0.0-5056483dc5-dd5066d9f8.zip b/.yarn/cache/eslint-npm-7.0.0-5056483dc5-dd5066d9f8.zip deleted file mode 100644 index 82926077c..000000000 Binary files a/.yarn/cache/eslint-npm-7.0.0-5056483dc5-dd5066d9f8.zip and /dev/null differ diff --git a/.yarn/cache/eslint-npm-9.39.2-af6e824e47-bfa288fe6b.zip b/.yarn/cache/eslint-npm-9.39.2-af6e824e47-bfa288fe6b.zip new file mode 100644 index 000000000..adabb535f Binary files /dev/null and b/.yarn/cache/eslint-npm-9.39.2-af6e824e47-bfa288fe6b.zip differ diff --git a/.yarn/cache/eslint-plugin-flowtype-npm-5.10.0-dee4499afc-791cd53c88.zip b/.yarn/cache/eslint-plugin-flowtype-npm-5.10.0-dee4499afc-791cd53c88.zip deleted file mode 100644 index f2777bc64..000000000 Binary files a/.yarn/cache/eslint-plugin-flowtype-npm-5.10.0-dee4499afc-791cd53c88.zip and /dev/null differ diff --git a/.yarn/cache/eslint-scope-npm-8.4.0-8ed12feb40-cf88f42cd5.zip b/.yarn/cache/eslint-scope-npm-8.4.0-8ed12feb40-cf88f42cd5.zip new file mode 100644 index 000000000..2be49b985 Binary files /dev/null and b/.yarn/cache/eslint-scope-npm-8.4.0-8ed12feb40-cf88f42cd5.zip differ diff --git a/.yarn/cache/eslint-utils-npm-2.1.0-a3a7ebf4fa-27500938f3.zip b/.yarn/cache/eslint-utils-npm-2.1.0-a3a7ebf4fa-27500938f3.zip deleted file mode 100644 index 1dadeb5d0..000000000 Binary files a/.yarn/cache/eslint-utils-npm-2.1.0-a3a7ebf4fa-27500938f3.zip and /dev/null differ diff --git a/.yarn/cache/eslint-visitor-keys-npm-4.2.1-435d5be22a-3a77e3f99a.zip b/.yarn/cache/eslint-visitor-keys-npm-4.2.1-435d5be22a-3a77e3f99a.zip new file mode 100644 index 000000000..fa8bb69e9 Binary files /dev/null and b/.yarn/cache/eslint-visitor-keys-npm-4.2.1-435d5be22a-3a77e3f99a.zip differ diff --git a/.yarn/cache/espree-npm-10.4.0-9633b00e55-5f9d0d7c81.zip b/.yarn/cache/espree-npm-10.4.0-9633b00e55-5f9d0d7c81.zip new file mode 100644 index 000000000..3bb0d0b3d Binary files /dev/null and b/.yarn/cache/espree-npm-10.4.0-9633b00e55-5f9d0d7c81.zip differ diff --git a/.yarn/cache/espree-npm-7.3.1-8d8ea5d1e3-aa9b50dcce.zip b/.yarn/cache/espree-npm-7.3.1-8d8ea5d1e3-aa9b50dcce.zip deleted file mode 100644 index be256f025..000000000 Binary files a/.yarn/cache/espree-npm-7.3.1-8d8ea5d1e3-aa9b50dcce.zip and /dev/null differ diff --git a/.yarn/cache/esquery-npm-1.6.0-16fee31531-08ec4fe446.zip b/.yarn/cache/esquery-npm-1.6.0-16fee31531-08ec4fe446.zip deleted file mode 100644 index 90baf4cfb..000000000 Binary files a/.yarn/cache/esquery-npm-1.6.0-16fee31531-08ec4fe446.zip and /dev/null differ diff --git a/.yarn/cache/esquery-npm-1.7.0-c1e8da438a-3239792b68.zip b/.yarn/cache/esquery-npm-1.7.0-c1e8da438a-3239792b68.zip new file mode 100644 index 000000000..3ef2d3f56 Binary files /dev/null and b/.yarn/cache/esquery-npm-1.7.0-c1e8da438a-3239792b68.zip differ diff --git a/.yarn/cache/eventsource-parser-npm-1.1.2-5a5b47ad45-01896eea72.zip b/.yarn/cache/eventsource-parser-npm-1.1.2-5a5b47ad45-01896eea72.zip deleted file mode 100644 index 675741300..000000000 Binary files a/.yarn/cache/eventsource-parser-npm-1.1.2-5a5b47ad45-01896eea72.zip and /dev/null differ diff --git a/.yarn/cache/eventsource-parser-npm-3.0.6-17d4172da2-b90ec27f8d.zip b/.yarn/cache/eventsource-parser-npm-3.0.6-17d4172da2-b90ec27f8d.zip new file mode 100644 index 000000000..c368796b9 Binary files /dev/null and b/.yarn/cache/eventsource-parser-npm-3.0.6-17d4172da2-b90ec27f8d.zip differ diff --git a/.yarn/cache/expr-eval-npm-2.0.2-20b6d1f745-01862f09b5.zip b/.yarn/cache/expr-eval-npm-2.0.2-20b6d1f745-01862f09b5.zip deleted file mode 100644 index 8da3c8f90..000000000 Binary files a/.yarn/cache/expr-eval-npm-2.0.2-20b6d1f745-01862f09b5.zip and /dev/null differ diff --git a/.yarn/cache/express-npm-4.19.2-f81334a22a-212dbd6c2c.zip b/.yarn/cache/express-npm-4.19.2-f81334a22a-212dbd6c2c.zip new file mode 100644 index 000000000..0d2218030 Binary files /dev/null and b/.yarn/cache/express-npm-4.19.2-f81334a22a-212dbd6c2c.zip differ diff --git a/.yarn/cache/fflate-npm-0.8.2-5129f303f0-29470337b8.zip b/.yarn/cache/fflate-npm-0.8.2-5129f303f0-29470337b8.zip new file mode 100644 index 000000000..67ef36976 Binary files /dev/null and b/.yarn/cache/fflate-npm-0.8.2-5129f303f0-29470337b8.zip differ diff --git a/.yarn/cache/file-entry-cache-npm-5.0.1-7212af17f3-9014b17766.zip b/.yarn/cache/file-entry-cache-npm-5.0.1-7212af17f3-9014b17766.zip deleted file mode 100644 index 7a48922c9..000000000 Binary files a/.yarn/cache/file-entry-cache-npm-5.0.1-7212af17f3-9014b17766.zip and /dev/null differ diff --git a/.yarn/cache/file-entry-cache-npm-8.0.0-5b09d19a83-f67802d333.zip b/.yarn/cache/file-entry-cache-npm-8.0.0-5b09d19a83-f67802d333.zip new file mode 100644 index 000000000..97687a842 Binary files /dev/null and b/.yarn/cache/file-entry-cache-npm-8.0.0-5b09d19a83-f67802d333.zip differ diff --git a/.yarn/cache/file-type-npm-20.4.1-569bfcb42b-672504dff2.zip b/.yarn/cache/file-type-npm-20.4.1-569bfcb42b-672504dff2.zip new file mode 100644 index 000000000..66554199a Binary files /dev/null and b/.yarn/cache/file-type-npm-20.4.1-569bfcb42b-672504dff2.zip differ diff --git a/.yarn/cache/flat-cache-npm-2.0.1-abf037b0b9-0f5e664676.zip b/.yarn/cache/flat-cache-npm-2.0.1-abf037b0b9-0f5e664676.zip deleted file mode 100644 index d23a3828d..000000000 Binary files a/.yarn/cache/flat-cache-npm-2.0.1-abf037b0b9-0f5e664676.zip and /dev/null differ diff --git a/.yarn/cache/flat-cache-npm-4.0.1-12bf2455f7-899fc86bf6.zip b/.yarn/cache/flat-cache-npm-4.0.1-12bf2455f7-899fc86bf6.zip new file mode 100644 index 000000000..3b159ec31 Binary files /dev/null and b/.yarn/cache/flat-cache-npm-4.0.1-12bf2455f7-899fc86bf6.zip differ diff --git a/.yarn/cache/flatted-npm-2.0.2-ccb06e14ff-473c754db7.zip b/.yarn/cache/flatted-npm-2.0.2-ccb06e14ff-473c754db7.zip deleted file mode 100644 index ee141710f..000000000 Binary files a/.yarn/cache/flatted-npm-2.0.2-ccb06e14ff-473c754db7.zip and /dev/null differ diff --git a/.yarn/cache/form-data-encoder-npm-1.7.2-e6028ef027-aeebd87a1c.zip b/.yarn/cache/form-data-encoder-npm-1.7.2-e6028ef027-aeebd87a1c.zip deleted file mode 100644 index 6c1b9e1df..000000000 Binary files a/.yarn/cache/form-data-encoder-npm-1.7.2-e6028ef027-aeebd87a1c.zip and /dev/null differ diff --git a/.yarn/cache/formdata-node-npm-4.4.1-1fb15d9b89-d91d4f667c.zip b/.yarn/cache/formdata-node-npm-4.4.1-1fb15d9b89-d91d4f667c.zip deleted file mode 100644 index b8a0b8263..000000000 Binary files a/.yarn/cache/formdata-node-npm-4.4.1-1fb15d9b89-d91d4f667c.zip and /dev/null differ diff --git a/.yarn/cache/glob-parent-npm-6.0.2-2cbef12738-c13ee97978.zip b/.yarn/cache/glob-parent-npm-6.0.2-2cbef12738-c13ee97978.zip new file mode 100644 index 000000000..2a4d60d72 Binary files /dev/null and b/.yarn/cache/glob-parent-npm-6.0.2-2cbef12738-c13ee97978.zip differ diff --git a/.yarn/cache/globals-npm-12.4.0-02b5a6ba9c-7ae5ee16a9.zip b/.yarn/cache/globals-npm-12.4.0-02b5a6ba9c-7ae5ee16a9.zip deleted file mode 100644 index 227f31c7c..000000000 Binary files a/.yarn/cache/globals-npm-12.4.0-02b5a6ba9c-7ae5ee16a9.zip and /dev/null differ diff --git a/.yarn/cache/globals-npm-14.0.0-5fc3d8d5da-534b821673.zip b/.yarn/cache/globals-npm-14.0.0-5fc3d8d5da-534b821673.zip new file mode 100644 index 000000000..15200cd2b Binary files /dev/null and b/.yarn/cache/globals-npm-14.0.0-5fc3d8d5da-534b821673.zip differ diff --git a/.yarn/cache/globals-npm-17.3.0-a102428cc6-4316ace3d0.zip b/.yarn/cache/globals-npm-17.3.0-a102428cc6-4316ace3d0.zip new file mode 100644 index 000000000..fe5ff37fa Binary files /dev/null and b/.yarn/cache/globals-npm-17.3.0-a102428cc6-4316ace3d0.zip differ diff --git a/.yarn/cache/humanize-ms-npm-1.2.1-e942bd7329-9c7a74a282.zip b/.yarn/cache/humanize-ms-npm-1.2.1-e942bd7329-9c7a74a282.zip deleted file mode 100644 index c09856b33..000000000 Binary files a/.yarn/cache/humanize-ms-npm-1.2.1-e942bd7329-9c7a74a282.zip and /dev/null differ diff --git a/.yarn/cache/ignore-npm-4.0.6-66c0d6543e-248f82e50a.zip b/.yarn/cache/ignore-npm-4.0.6-66c0d6543e-248f82e50a.zip deleted file mode 100644 index f5bcbcf28..000000000 Binary files a/.yarn/cache/ignore-npm-4.0.6-66c0d6543e-248f82e50a.zip and /dev/null differ diff --git a/.yarn/cache/is-any-array-npm-2.0.1-922fa2803c-472ed80e17.zip b/.yarn/cache/is-any-array-npm-2.0.1-922fa2803c-472ed80e17.zip deleted file mode 100644 index 8ea2eab84..000000000 Binary files a/.yarn/cache/is-any-array-npm-2.0.1-922fa2803c-472ed80e17.zip and /dev/null differ diff --git a/.yarn/cache/is-fullwidth-code-point-npm-2.0.0-507f56ec71-eef9c6e15f.zip b/.yarn/cache/is-fullwidth-code-point-npm-2.0.0-507f56ec71-eef9c6e15f.zip deleted file mode 100644 index 56f17d398..000000000 Binary files a/.yarn/cache/is-fullwidth-code-point-npm-2.0.0-507f56ec71-eef9c6e15f.zip and /dev/null differ diff --git a/.yarn/cache/is-network-error-npm-1.3.0-9efc893bc4-56dc0b8ed9.zip b/.yarn/cache/is-network-error-npm-1.3.0-9efc893bc4-56dc0b8ed9.zip new file mode 100644 index 000000000..6cadf935d Binary files /dev/null and b/.yarn/cache/is-network-error-npm-1.3.0-9efc893bc4-56dc0b8ed9.zip differ diff --git a/.yarn/cache/joi-npm-17.13.3-866dad5bc8-66ed454fee.zip b/.yarn/cache/joi-npm-17.13.3-866dad5bc8-66ed454fee.zip deleted file mode 100644 index b05a25151..000000000 Binary files a/.yarn/cache/joi-npm-17.13.3-866dad5bc8-66ed454fee.zip and /dev/null differ diff --git a/.yarn/cache/joi-npm-18.0.2-79a3fbcace-39d9e35845.zip b/.yarn/cache/joi-npm-18.0.2-79a3fbcace-39d9e35845.zip new file mode 100644 index 000000000..70593091b Binary files /dev/null and b/.yarn/cache/joi-npm-18.0.2-79a3fbcace-39d9e35845.zip differ diff --git a/.yarn/cache/js-yaml-npm-4.1.1-86ec786790-ea2339c693.zip b/.yarn/cache/js-yaml-npm-4.1.1-86ec786790-ea2339c693.zip new file mode 100644 index 000000000..01deabad3 Binary files /dev/null and b/.yarn/cache/js-yaml-npm-4.1.1-86ec786790-ea2339c693.zip differ diff --git a/.yarn/cache/jsondiffpatch-npm-0.6.0-8a8e017f57-27d7aa42c3.zip b/.yarn/cache/jsondiffpatch-npm-0.6.0-8a8e017f57-27d7aa42c3.zip deleted file mode 100644 index 28d618e36..000000000 Binary files a/.yarn/cache/jsondiffpatch-npm-0.6.0-8a8e017f57-27d7aa42c3.zip and /dev/null differ diff --git a/.yarn/cache/langchain-npm-0.1.37-0374861c0c-6c1106d02c.zip b/.yarn/cache/langchain-npm-0.1.37-0374861c0c-6c1106d02c.zip deleted file mode 100644 index 0681fe9cd..000000000 Binary files a/.yarn/cache/langchain-npm-0.1.37-0374861c0c-6c1106d02c.zip and /dev/null differ diff --git a/.yarn/cache/langchain-npm-1.2.25-39600e5a52-2336ca4ec4.zip b/.yarn/cache/langchain-npm-1.2.25-39600e5a52-2336ca4ec4.zip new file mode 100644 index 000000000..87b90db5c Binary files /dev/null and b/.yarn/cache/langchain-npm-1.2.25-39600e5a52-2336ca4ec4.zip differ diff --git a/.yarn/cache/langchainhub-npm-0.0.11-f5dd7ed32c-511371a6d9.zip b/.yarn/cache/langchainhub-npm-0.0.11-f5dd7ed32c-511371a6d9.zip deleted file mode 100644 index 209873dff..000000000 Binary files a/.yarn/cache/langchainhub-npm-0.0.11-f5dd7ed32c-511371a6d9.zip and /dev/null differ diff --git a/.yarn/cache/langsmith-npm-0.1.68-f735c37596-b5422f9d52.zip b/.yarn/cache/langsmith-npm-0.1.68-f735c37596-b5422f9d52.zip deleted file mode 100644 index 718a88cb2..000000000 Binary files a/.yarn/cache/langsmith-npm-0.1.68-f735c37596-b5422f9d52.zip and /dev/null differ diff --git a/.yarn/cache/langsmith-npm-0.5.4-8130c18e5f-11b68ced85.zip b/.yarn/cache/langsmith-npm-0.5.4-8130c18e5f-11b68ced85.zip new file mode 100644 index 000000000..b96eadebc Binary files /dev/null and b/.yarn/cache/langsmith-npm-0.5.4-8130c18e5f-11b68ced85.zip differ diff --git a/.yarn/cache/math-expression-evaluator-npm-2.0.7-de3af51f4d-af644ba331.zip b/.yarn/cache/math-expression-evaluator-npm-2.0.7-de3af51f4d-af644ba331.zip new file mode 100644 index 000000000..d8dec9ccb Binary files /dev/null and b/.yarn/cache/math-expression-evaluator-npm-2.0.7-de3af51f4d-af644ba331.zip differ diff --git a/.yarn/cache/ml-array-mean-npm-1.1.6-df75cbf3dd-81999dac8b.zip b/.yarn/cache/ml-array-mean-npm-1.1.6-df75cbf3dd-81999dac8b.zip deleted file mode 100644 index dca71eddb..000000000 Binary files a/.yarn/cache/ml-array-mean-npm-1.1.6-df75cbf3dd-81999dac8b.zip and /dev/null differ diff --git a/.yarn/cache/ml-array-sum-npm-1.1.6-64a901dff6-369dbb3681.zip b/.yarn/cache/ml-array-sum-npm-1.1.6-64a901dff6-369dbb3681.zip deleted file mode 100644 index 077fc5964..000000000 Binary files a/.yarn/cache/ml-array-sum-npm-1.1.6-64a901dff6-369dbb3681.zip and /dev/null differ diff --git a/.yarn/cache/ml-distance-euclidean-npm-2.0.0-6a442f7f40-e31f98a947.zip b/.yarn/cache/ml-distance-euclidean-npm-2.0.0-6a442f7f40-e31f98a947.zip deleted file mode 100644 index 28c584c92..000000000 Binary files a/.yarn/cache/ml-distance-euclidean-npm-2.0.0-6a442f7f40-e31f98a947.zip and /dev/null differ diff --git a/.yarn/cache/ml-distance-npm-4.0.1-9653973c44-21ea014064.zip b/.yarn/cache/ml-distance-npm-4.0.1-9653973c44-21ea014064.zip deleted file mode 100644 index efafc09d1..000000000 Binary files a/.yarn/cache/ml-distance-npm-4.0.1-9653973c44-21ea014064.zip and /dev/null differ diff --git a/.yarn/cache/ml-tree-similarity-npm-1.0.0-a387b90b6c-f99e217dc9.zip b/.yarn/cache/ml-tree-similarity-npm-1.0.0-a387b90b6c-f99e217dc9.zip deleted file mode 100644 index 603343caf..000000000 Binary files a/.yarn/cache/ml-tree-similarity-npm-1.0.0-a387b90b6c-f99e217dc9.zip and /dev/null differ diff --git a/.yarn/cache/node-domexception-npm-1.0.0-e1e813b76f-ee1d37dd2a.zip b/.yarn/cache/node-domexception-npm-1.0.0-e1e813b76f-ee1d37dd2a.zip deleted file mode 100644 index d58ba924f..000000000 Binary files a/.yarn/cache/node-domexception-npm-1.0.0-e1e813b76f-ee1d37dd2a.zip and /dev/null differ diff --git a/.yarn/cache/nodemailer-npm-6.10.1-a82e2575bf-39e9208e13.zip b/.yarn/cache/nodemailer-npm-6.10.1-a82e2575bf-39e9208e13.zip deleted file mode 100644 index a517eb3a9..000000000 Binary files a/.yarn/cache/nodemailer-npm-6.10.1-a82e2575bf-39e9208e13.zip and /dev/null differ diff --git a/.yarn/cache/nodemailer-npm-7.0.11-4dcad3ba5a-02940a5a62.zip b/.yarn/cache/nodemailer-npm-7.0.11-4dcad3ba5a-02940a5a62.zip new file mode 100644 index 000000000..8c08d2211 Binary files /dev/null and b/.yarn/cache/nodemailer-npm-7.0.11-4dcad3ba5a-02940a5a62.zip differ diff --git a/.yarn/cache/num-sort-npm-2.1.0-e0725952ee-5a80cd0456.zip b/.yarn/cache/num-sort-npm-2.1.0-e0725952ee-5a80cd0456.zip deleted file mode 100644 index 6d0445b94..000000000 Binary files a/.yarn/cache/num-sort-npm-2.1.0-e0725952ee-5a80cd0456.zip and /dev/null differ diff --git a/.yarn/cache/openai-npm-4.104.0-d842e71d7c-2bd3ba14a3.zip b/.yarn/cache/openai-npm-4.104.0-d842e71d7c-2bd3ba14a3.zip deleted file mode 100644 index bfff1820f..000000000 Binary files a/.yarn/cache/openai-npm-4.104.0-d842e71d7c-2bd3ba14a3.zip and /dev/null differ diff --git a/.yarn/cache/openai-npm-6.22.0-353b05aee4-f4781328eb.zip b/.yarn/cache/openai-npm-6.22.0-353b05aee4-f4781328eb.zip new file mode 100644 index 000000000..1e63fcc65 Binary files /dev/null and b/.yarn/cache/openai-npm-6.22.0-353b05aee4-f4781328eb.zip differ diff --git a/.yarn/cache/p-queue-npm-9.1.0-360e92eaa3-30b9b4a36a.zip b/.yarn/cache/p-queue-npm-9.1.0-360e92eaa3-30b9b4a36a.zip new file mode 100644 index 000000000..d8c90a65c Binary files /dev/null and b/.yarn/cache/p-queue-npm-9.1.0-360e92eaa3-30b9b4a36a.zip differ diff --git a/.yarn/cache/p-retry-npm-4.6.2-9f871cfc9b-45c270bfdd.zip b/.yarn/cache/p-retry-npm-4.6.2-9f871cfc9b-45c270bfdd.zip deleted file mode 100644 index 17581af83..000000000 Binary files a/.yarn/cache/p-retry-npm-4.6.2-9f871cfc9b-45c270bfdd.zip and /dev/null differ diff --git a/.yarn/cache/p-retry-npm-7.1.1-222df74ea6-ae5ac18118.zip b/.yarn/cache/p-retry-npm-7.1.1-222df74ea6-ae5ac18118.zip new file mode 100644 index 000000000..11a47dd04 Binary files /dev/null and b/.yarn/cache/p-retry-npm-7.1.1-222df74ea6-ae5ac18118.zip differ diff --git a/.yarn/cache/p-timeout-npm-7.0.1-3a2b4a11cc-696d5e3b46.zip b/.yarn/cache/p-timeout-npm-7.0.1-3a2b4a11cc-696d5e3b46.zip new file mode 100644 index 000000000..1d1287ade Binary files /dev/null and b/.yarn/cache/p-timeout-npm-7.0.1-3a2b4a11cc-696d5e3b46.zip differ diff --git a/.yarn/cache/retry-npm-0.13.1-89eb100ab6-47c4d5be67.zip b/.yarn/cache/retry-npm-0.13.1-89eb100ab6-47c4d5be67.zip deleted file mode 100644 index 9a38721ed..000000000 Binary files a/.yarn/cache/retry-npm-0.13.1-89eb100ab6-47c4d5be67.zip and /dev/null differ diff --git a/.yarn/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-d9d7d5a01f.zip b/.yarn/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-d9d7d5a01f.zip deleted file mode 100644 index 6609a2efc..000000000 Binary files a/.yarn/cache/secure-json-parse-npm-2.7.0-d5b89b0a3e-d9d7d5a01f.zip and /dev/null differ diff --git a/.yarn/cache/semver-npm-7.7.3-9cf7b3b46c-f013a3ee46.zip b/.yarn/cache/semver-npm-7.7.3-9cf7b3b46c-f013a3ee46.zip new file mode 100644 index 000000000..16de37ee9 Binary files /dev/null and b/.yarn/cache/semver-npm-7.7.3-9cf7b3b46c-f013a3ee46.zip differ diff --git a/.yarn/cache/simple-wcswidth-npm-1.1.2-71630fa07c-210eea36d2.zip b/.yarn/cache/simple-wcswidth-npm-1.1.2-71630fa07c-210eea36d2.zip new file mode 100644 index 000000000..437544cd5 Binary files /dev/null and b/.yarn/cache/simple-wcswidth-npm-1.1.2-71630fa07c-210eea36d2.zip differ diff --git a/.yarn/cache/slice-ansi-npm-2.1.0-02505ccc06-4e82995aa5.zip b/.yarn/cache/slice-ansi-npm-2.1.0-02505ccc06-4e82995aa5.zip deleted file mode 100644 index 23b558a26..000000000 Binary files a/.yarn/cache/slice-ansi-npm-2.1.0-02505ccc06-4e82995aa5.zip and /dev/null differ diff --git a/.yarn/cache/sswr-npm-2.2.0-4a201739ba-680b7c4353.zip b/.yarn/cache/sswr-npm-2.2.0-4a201739ba-680b7c4353.zip deleted file mode 100644 index 1a4cc43f1..000000000 Binary files a/.yarn/cache/sswr-npm-2.2.0-4a201739ba-680b7c4353.zip and /dev/null differ diff --git a/.yarn/cache/storybook-npm-7.6.20-ef59c34d68-7442d0bf40.zip b/.yarn/cache/storybook-npm-7.6.21-443ce105f7-a58bfff2a0.zip similarity index 69% rename from .yarn/cache/storybook-npm-7.6.20-ef59c34d68-7442d0bf40.zip rename to .yarn/cache/storybook-npm-7.6.21-443ce105f7-a58bfff2a0.zip index a8b106cd2..c88158308 100644 Binary files a/.yarn/cache/storybook-npm-7.6.20-ef59c34d68-7442d0bf40.zip and b/.yarn/cache/storybook-npm-7.6.21-443ce105f7-a58bfff2a0.zip differ diff --git a/.yarn/cache/string-natural-compare-npm-3.0.1-f6d0be6457-65910d9995.zip b/.yarn/cache/string-natural-compare-npm-3.0.1-f6d0be6457-65910d9995.zip deleted file mode 100644 index c4f9aa885..000000000 Binary files a/.yarn/cache/string-natural-compare-npm-3.0.1-f6d0be6457-65910d9995.zip and /dev/null differ diff --git a/.yarn/cache/string-width-npm-3.1.0-e031bfa4e0-57f7ca73d2.zip b/.yarn/cache/string-width-npm-3.1.0-e031bfa4e0-57f7ca73d2.zip deleted file mode 100644 index 706d03c8c..000000000 Binary files a/.yarn/cache/string-width-npm-3.1.0-e031bfa4e0-57f7ca73d2.zip and /dev/null differ diff --git a/.yarn/cache/strip-ansi-npm-5.2.0-275214c316-bdb5f76ade.zip b/.yarn/cache/strip-ansi-npm-5.2.0-275214c316-bdb5f76ade.zip deleted file mode 100644 index 2231cf589..000000000 Binary files a/.yarn/cache/strip-ansi-npm-5.2.0-275214c316-bdb5f76ade.zip and /dev/null differ diff --git a/.yarn/cache/strtok3-npm-10.3.4-bd6e987a57-7faf008cdf.zip b/.yarn/cache/strtok3-npm-10.3.4-bd6e987a57-7faf008cdf.zip new file mode 100644 index 000000000..3e919da3e Binary files /dev/null and b/.yarn/cache/strtok3-npm-10.3.4-bd6e987a57-7faf008cdf.zip differ diff --git a/.yarn/cache/swr-npm-2.3.6-1a69322beb-c7cc7dfb73.zip b/.yarn/cache/swr-npm-2.3.6-1a69322beb-c7cc7dfb73.zip deleted file mode 100644 index bc101db7f..000000000 Binary files a/.yarn/cache/swr-npm-2.3.6-1a69322beb-c7cc7dfb73.zip and /dev/null differ diff --git a/.yarn/cache/swrev-npm-4.0.0-a93e59e6ce-454aed0e03.zip b/.yarn/cache/swrev-npm-4.0.0-a93e59e6ce-454aed0e03.zip deleted file mode 100644 index 210009b46..000000000 Binary files a/.yarn/cache/swrev-npm-4.0.0-a93e59e6ce-454aed0e03.zip and /dev/null differ diff --git a/.yarn/cache/swrv-npm-1.1.0-6e8cbe2450-997fcd9769.zip b/.yarn/cache/swrv-npm-1.1.0-6e8cbe2450-997fcd9769.zip deleted file mode 100644 index 106fbcf8c..000000000 Binary files a/.yarn/cache/swrv-npm-1.1.0-6e8cbe2450-997fcd9769.zip and /dev/null differ diff --git a/.yarn/cache/table-npm-5.4.6-190b118384-9e35d3efa7.zip b/.yarn/cache/table-npm-5.4.6-190b118384-9e35d3efa7.zip deleted file mode 100644 index 386d1baae..000000000 Binary files a/.yarn/cache/table-npm-5.4.6-190b118384-9e35d3efa7.zip and /dev/null differ diff --git a/.yarn/cache/text-table-npm-0.2.0-d92a778b59-b6937a38c8.zip b/.yarn/cache/text-table-npm-0.2.0-d92a778b59-b6937a38c8.zip deleted file mode 100644 index 08df4834d..000000000 Binary files a/.yarn/cache/text-table-npm-0.2.0-d92a778b59-b6937a38c8.zip and /dev/null differ diff --git a/.yarn/cache/throttleit-npm-2.1.0-7064896991-a2003947aa.zip b/.yarn/cache/throttleit-npm-2.1.0-7064896991-a2003947aa.zip deleted file mode 100644 index 075d3a454..000000000 Binary files a/.yarn/cache/throttleit-npm-2.1.0-7064896991-a2003947aa.zip and /dev/null differ diff --git a/.yarn/cache/token-types-npm-6.1.2-1f6e70d865-ddade9c99f.zip b/.yarn/cache/token-types-npm-6.1.2-1f6e70d865-ddade9c99f.zip new file mode 100644 index 000000000..1e5702f85 Binary files /dev/null and b/.yarn/cache/token-types-npm-6.1.2-1f6e70d865-ddade9c99f.zip differ diff --git a/.yarn/cache/ts-api-utils-npm-2.4.0-1179124e9a-beae72a4fa.zip b/.yarn/cache/ts-api-utils-npm-2.4.0-1179124e9a-beae72a4fa.zip new file mode 100644 index 000000000..79061f81e Binary files /dev/null and b/.yarn/cache/ts-api-utils-npm-2.4.0-1179124e9a-beae72a4fa.zip differ diff --git a/.yarn/cache/typescript-npm-4.9.5-6427b65ee6-ee000bc268.zip b/.yarn/cache/typescript-npm-4.9.5-6427b65ee6-ee000bc268.zip deleted file mode 100644 index 5434f6e63..000000000 Binary files a/.yarn/cache/typescript-npm-4.9.5-6427b65ee6-ee000bc268.zip and /dev/null differ diff --git a/.yarn/cache/typescript-npm-5.9.3-48715be868-0d0ffb84f2.zip b/.yarn/cache/typescript-npm-5.9.3-48715be868-0d0ffb84f2.zip new file mode 100644 index 000000000..14df9208e Binary files /dev/null and b/.yarn/cache/typescript-npm-5.9.3-48715be868-0d0ffb84f2.zip differ diff --git a/.yarn/cache/typescript-patch-9cbb41b1b0-8bb8d86819.zip b/.yarn/cache/typescript-patch-9cbb41b1b0-8bb8d86819.zip new file mode 100644 index 000000000..5018a2832 Binary files /dev/null and b/.yarn/cache/typescript-patch-9cbb41b1b0-8bb8d86819.zip differ diff --git a/.yarn/cache/typescript-patch-f8edcd7439-1f8f3b6aae.zip b/.yarn/cache/typescript-patch-f8edcd7439-1f8f3b6aae.zip deleted file mode 100644 index f3332e3fe..000000000 Binary files a/.yarn/cache/typescript-patch-f8edcd7439-1f8f3b6aae.zip and /dev/null differ diff --git a/.yarn/cache/uint8array-extras-npm-1.5.0-30fc87691c-e7fb42b57e.zip b/.yarn/cache/uint8array-extras-npm-1.5.0-30fc87691c-e7fb42b57e.zip new file mode 100644 index 000000000..a2c146564 Binary files /dev/null and b/.yarn/cache/uint8array-extras-npm-1.5.0-30fc87691c-e7fb42b57e.zip differ diff --git a/.yarn/cache/uuid-npm-13.0.0-29831a4f1f-7510ee1ab3.zip b/.yarn/cache/uuid-npm-13.0.0-29831a4f1f-7510ee1ab3.zip new file mode 100644 index 000000000..2ee43d5da Binary files /dev/null and b/.yarn/cache/uuid-npm-13.0.0-29831a4f1f-7510ee1ab3.zip differ diff --git a/.yarn/cache/v8-compile-cache-npm-2.4.0-5979f8e405-8eb6ddb59d.zip b/.yarn/cache/v8-compile-cache-npm-2.4.0-5979f8e405-8eb6ddb59d.zip deleted file mode 100644 index 1c8df1904..000000000 Binary files a/.yarn/cache/v8-compile-cache-npm-2.4.0-5979f8e405-8eb6ddb59d.zip and /dev/null differ diff --git a/.yarn/cache/wait-on-npm-6.0.1-9e03b09170-e4d62aa414.zip b/.yarn/cache/wait-on-npm-6.0.1-9e03b09170-e4d62aa414.zip deleted file mode 100644 index 78b6cd21c..000000000 Binary files a/.yarn/cache/wait-on-npm-6.0.1-9e03b09170-e4d62aa414.zip and /dev/null differ diff --git a/.yarn/cache/wait-on-npm-9.0.3-cc34d3560e-088472eb3c.zip b/.yarn/cache/wait-on-npm-9.0.3-cc34d3560e-088472eb3c.zip new file mode 100644 index 000000000..b08754ea0 Binary files /dev/null and b/.yarn/cache/wait-on-npm-9.0.3-cc34d3560e-088472eb3c.zip differ diff --git a/.yarn/cache/web-streams-polyfill-npm-3.3.3-f24b9f8c34-21ab5ea08a.zip b/.yarn/cache/web-streams-polyfill-npm-3.3.3-f24b9f8c34-21ab5ea08a.zip deleted file mode 100644 index 2b72fbddb..000000000 Binary files a/.yarn/cache/web-streams-polyfill-npm-3.3.3-f24b9f8c34-21ab5ea08a.zip and /dev/null differ diff --git a/.yarn/cache/web-streams-polyfill-npm-4.0.0-beta.3-0dc6d160ed-dfec1fbf52.zip b/.yarn/cache/web-streams-polyfill-npm-4.0.0-beta.3-0dc6d160ed-dfec1fbf52.zip deleted file mode 100644 index 39b640afc..000000000 Binary files a/.yarn/cache/web-streams-polyfill-npm-4.0.0-beta.3-0dc6d160ed-dfec1fbf52.zip and /dev/null differ diff --git a/.yarn/cache/write-npm-1.0.3-1bac756049-6496197ceb.zip b/.yarn/cache/write-npm-1.0.3-1bac756049-6496197ceb.zip deleted file mode 100644 index b789c936a..000000000 Binary files a/.yarn/cache/write-npm-1.0.3-1bac756049-6496197ceb.zip and /dev/null differ diff --git a/.yarn/cache/zod-npm-4.3.6-a096e305e6-19cec761b4.zip b/.yarn/cache/zod-npm-4.3.6-a096e305e6-19cec761b4.zip new file mode 100644 index 000000000..35d4ddd40 Binary files /dev/null and b/.yarn/cache/zod-npm-4.3.6-a096e305e6-19cec761b4.zip differ diff --git a/.yarn/cache/zod-to-json-schema-npm-3.24.6-1ea8d4e085-5f4d29597c.zip b/.yarn/cache/zod-to-json-schema-npm-3.24.6-1ea8d4e085-5f4d29597c.zip deleted file mode 100644 index ed907d0d3..000000000 Binary files a/.yarn/cache/zod-to-json-schema-npm-3.24.6-1ea8d4e085-5f4d29597c.zip and /dev/null differ diff --git a/Dockerfile b/Dockerfile index 69f5a977f..7d81a9dcb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.19.1-alpine AS package +FROM node:20.18.0-alpine AS package ARG NEXT_PUBLIC_UMAMI_SITE_ID ARG NEXT_PUBLIC_RECAPTCHA_SITEKEY @@ -37,8 +37,7 @@ COPY config.websocket.$ENVIRONMENT.yaml ./config.websocket.yaml COPY config.seed.example.yaml ./config.seed.yaml COPY migrate-mongo-config-example.ts ./migrate-mongo-config.ts COPY ./migrations ./migrations -COPY ./.eslintignore ./ -COPY ./.eslintrc.yml ./ +COPY ./eslint.config.mjs ./ COPY server/jest.config.json ./jest.config.json COPY ./next.config.js ./ COPY ./tsconfig.json ./ @@ -62,7 +61,7 @@ RUN NEXT_PUBLIC_UMAMI_SITE_ID=$NEXT_PUBLIC_UMAMI_SITE_ID \ NEXT_PUBLIC_ENABLE_BANNER_DONATION=$NEXT_PUBLIC_ENABLE_BANNER_DONATION \ yarn build -FROM node:18.19.1-alpine +FROM node:20.18.0-alpine LABEL maintainer="Giovanni Rossini " diff --git a/cypress/e2e/tests/auth/login.cy.ts b/cypress/e2e/tests/auth/login.cy.ts new file mode 100644 index 000000000..044b566cc --- /dev/null +++ b/cypress/e2e/tests/auth/login.cy.ts @@ -0,0 +1,44 @@ +import user from "../../../fixtures/user"; +import locators from "../../../support/locators"; + +describe("Login tests", () => { + const submitLoginForm = (email, password) => { + cy.get(locators.login.USER).type(email); + cy.get(locators.login.PASSWORD).type(password); + cy.get(locators.login.BTN_LOGIN).click(); + }; + + it("Should login with valid credentials", () => { + cy.login(); + }); + + it("Should not login with invalid password", () => { + cy.intercept("POST", "/api/.ory/self-service/login**").as( + "loginRequest" + ); + cy.goToLoginPage(); + submitLoginForm(user.email, "invalidPassword123"); + cy.wait("@loginRequest").its("response.statusCode").should("eq", 400); + cy.contains("Erro ao fazer login").should("be.visible"); + }); + + it("Should not login with invalid email", () => { + cy.intercept("POST", "/api/.ory/self-service/login**").as( + "loginRequest" + ); + cy.goToLoginPage(); + submitLoginForm("invalidEmail@alethieiafact.org", user.password); + cy.wait("@loginRequest").its("response.statusCode").should("eq", 400); + cy.contains("Erro ao fazer login").should("be.visible"); + }); + + it("Should logout successfully", () => { + cy.login(); + cy.intercept("/api/.ory/sessions/whoami").as("confirmLogout"); + cy.get(locators.menu.USER_ICON).click(); + cy.get(locators.menu.LOGOUT_MENU).click(); + cy.wait("@confirmLogout", { timeout: 30000 }); + cy.get(locators.menu.USER_ICON).click(); + cy.get(locators.menu.LOGIN_MENU).should("be.visible"); + }); +}); diff --git a/cypress/e2e/tests/auth/signup.cy.ts b/cypress/e2e/tests/auth/signup.cy.ts new file mode 100644 index 000000000..b12ced2c0 --- /dev/null +++ b/cypress/e2e/tests/auth/signup.cy.ts @@ -0,0 +1,149 @@ +/// + +import locators from "../../../support/locators"; + +describe("Sign Up tests", () => { + const TIMEOUT = { + DEFAULT: 10000, + API: 30000, + }; + + const testUser = { + name: "Test User", + email: `test${Date.now()}@aletheiafact.org`, + password: "TestPassword123!", + }; + + beforeEach(() => { + cy.clearCookies(); + cy.clearLocalStorage(); + + cy.goToSignUpPage(); + + cy.get('iframe[title="reCAPTCHA"]', { timeout: TIMEOUT.DEFAULT }) + .should("be.visible") + .its("0.contentDocument.body") + .should("not.be.empty"); + }); + + it("Should display sign up form", () => { + cy.get(locators.signup.NAME).should("be.visible"); + cy.get(locators.signup.EMAIL).should("be.visible"); + cy.get(locators.signup.PASSWORD).should("be.visible"); + cy.get(locators.signup.REPEATED_PASSWORD).should("be.visible"); + cy.get('iframe[title="reCAPTCHA"]').should("be.visible"); + cy.get(locators.signup.BTN_SUBMIT).should("be.visible"); + }); + + it("Should show validation error when name is empty", () => { + cy.get(locators.signup.EMAIL).clear().type(testUser.email); + cy.get(locators.signup.PASSWORD).clear().type(testUser.password); + cy.get(locators.signup.REPEATED_PASSWORD) + .clear() + .type(testUser.password); + cy.checkRecaptcha(); + cy.get(locators.signup.BTN_SUBMIT).click(); + + cy.get(locators.signup.ERROR_NAME, { timeout: TIMEOUT.DEFAULT }).should( + "be.visible" + ); + }); + + it("Should show validation error when email is invalid", () => { + cy.get(locators.signup.NAME).clear().type(testUser.name); + cy.get(locators.signup.EMAIL).clear().type("invalid-email"); + cy.get(locators.signup.PASSWORD).clear().type(testUser.password); + cy.get(locators.signup.REPEATED_PASSWORD) + .clear() + .type(testUser.password); + cy.checkRecaptcha(); + cy.get(locators.signup.BTN_SUBMIT).click(); + + cy.get(locators.signup.ERROR_EMAIL, { + timeout: TIMEOUT.DEFAULT, + }).should("be.visible"); + }); + + it("Should show validation error when passwords don't match", () => { + cy.get(locators.signup.NAME).clear().type(testUser.name); + cy.get(locators.signup.EMAIL).clear().type(testUser.email); + cy.get(locators.signup.PASSWORD).clear().type(testUser.password); + cy.get(locators.signup.REPEATED_PASSWORD) + .clear() + .type("DifferentPassword123!"); + cy.checkRecaptcha(); + cy.get(locators.signup.BTN_SUBMIT).click(); + + cy.get(locators.signup.ERROR_REPEATED_PASSWORD, { + timeout: TIMEOUT.DEFAULT, + }).should("be.visible"); + }); + + it("Should show error when CAPTCHA is not completed", () => { + cy.get(locators.signup.NAME).clear().type(testUser.name); + cy.get(locators.signup.EMAIL).clear().type(testUser.email); + cy.get(locators.signup.PASSWORD).clear().type(testUser.password); + cy.get(locators.signup.REPEATED_PASSWORD) + .clear() + .type(testUser.password); + cy.get(locators.signup.BTN_SUBMIT).click(); + + cy.get(".ant-message-error, .MuiAlert-standardError, [role='alert']", { + timeout: TIMEOUT.DEFAULT, + }).should("be.visible"); + }); + + it("Should successfully create account with valid data and CAPTCHA", () => { + cy.get(locators.signup.NAME).clear().type(testUser.name); + cy.get(locators.signup.EMAIL).clear().type(testUser.email); + cy.get(locators.signup.PASSWORD).clear().type(testUser.password); + cy.get(locators.signup.REPEATED_PASSWORD) + .clear() + .type(testUser.password); + cy.checkRecaptcha(); + + cy.intercept("POST", "/api/user/register", (req) => { + req.continue((res) => { + if (res.statusCode >= 400) { + cy.log( + `Registration failed: ${ + res.statusCode + } - ${JSON.stringify(res.body)}` + ); + } + }); + }).as("registerUser"); + cy.intercept("/api/.ory/sessions/whoami").as("confirmLogin"); + + cy.get(locators.signup.BTN_SUBMIT).click(); + + cy.wait("@registerUser", { timeout: TIMEOUT.API }).then( + (interception) => { + const isSuccess = [200, 201].includes( + interception.response.statusCode + ); + if (!isSuccess) { + cy.log( + `Registration API Error: ${interception.response.statusCode}` + ); + cy.log( + `Error body: ${JSON.stringify( + interception.response.body + )}` + ); + expect(interception.response.statusCode).to.be.oneOf([ + 200, 201, + ]); + } + } + ); + + cy.wait("@confirmLogin", { timeout: TIMEOUT.API }); + + cy.url({ timeout: TIMEOUT.API }).should("eq", "http://localhost:3000/"); + cy.get( + ".ant-message-success, .MuiAlert-standardSuccess, [role='alert']", + { timeout: TIMEOUT.DEFAULT } + ).should("exist"); + }); +}); diff --git a/cypress/e2e/tests/header.cy.ts b/cypress/e2e/tests/header.cy.ts index b7351b02f..70125a791 100644 --- a/cypress/e2e/tests/header.cy.ts +++ b/cypress/e2e/tests/header.cy.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ /// import user from "../../fixtures/user"; diff --git a/cypress/e2e/tests/image.cy.ts b/cypress/e2e/tests/image.cy.ts index f11aeb7f5..7865b2a7d 100644 --- a/cypress/e2e/tests/image.cy.ts +++ b/cypress/e2e/tests/image.cy.ts @@ -1,8 +1,8 @@ -/* eslint-disable no-undef */ /// import locators from "../../support/locators"; import claim from "../../fixtures/claim"; +import { today } from "../../utils/dateUtils"; describe("Create image claim", () => { beforeEach("login", () => cy.login()); @@ -16,7 +16,7 @@ describe("Create image claim", () => { .type(claim.imageTitle); cy.get(locators.claim.INPUT_DATA).should("be.visible").click(); - cy.get(locators.claim.INPUT_DATA_TODAY).should("be.visible").click(); + cy.contains('[role="gridcell"]', today.format("D")).click(); cy.get(locators.claim.INPUT_SOURCE) .should("be.visible") diff --git a/cypress/e2e/tests/personality.cy.ts b/cypress/e2e/tests/personality.cy.ts index 462705226..084de10e4 100644 --- a/cypress/e2e/tests/personality.cy.ts +++ b/cypress/e2e/tests/personality.cy.ts @@ -1,9 +1,9 @@ -/* eslint-disable no-undef */ /// import locators from "../../support/locators"; import claim from "../../fixtures/claim"; import personality from "../../fixtures/personality"; +import { today } from "../../utils/dateUtils"; describe("Create personality and claim", () => { beforeEach("login", () => cy.login()); @@ -48,7 +48,7 @@ describe("Create personality and claim", () => { .type(claim.content); cy.get(locators.claim.INPUT_DATA).should("be.visible").click(); - cy.get(locators.claim.INPUT_DATA_TODAY).should("be.visible").click(); + cy.contains('[role="gridcell"]', today.format("D")).click(); cy.get(locators.claim.INPUT_SOURCE) .should("be.visible") @@ -79,7 +79,7 @@ describe("Create personality and claim", () => { .should("be.visible") .type(claim.imageTitle); cy.get(locators.claim.INPUT_DATA).should("be.visible").click(); - cy.get(locators.claim.INPUT_DATA_TODAY).should("be.visible").click(); + cy.contains('[role="gridcell"]', today.format("D")).click(); cy.get(locators.claim.INPUT_SOURCE) .should("be.visible") .type(claim.source); diff --git a/cypress/e2e/tests/review.cy.ts b/cypress/e2e/tests/review.cy.ts index a80e22c49..f26492bca 100644 --- a/cypress/e2e/tests/review.cy.ts +++ b/cypress/e2e/tests/review.cy.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ /// import claim from "../../fixtures/claim"; @@ -28,7 +27,7 @@ const assignUser = () => { cy.get(locators.claimReview.BTN_START_CLAIM_REVIEW).should("exist").click(); cy.get(locators.claimReview.INPUT_USER) .should("exist") - .type(`${review.username}{downarrow}{enter}`, { delay: 200 }) + .type(`${review.username}{downarrow}{enter}`, { delay: 200 }); cy.get('[title="reCAPTCHA"]').should("exist"); cy.get(locators.claimReview.BTN_ASSIGN_USER).should("be.disabled"); cy.checkRecaptcha(); @@ -37,11 +36,21 @@ const assignUser = () => { }; const blockAssignedUserReview = () => { + // Wait for the form to be fully loaded with review data (including classification) + // before interacting. The classification field has a validation rule, and if its value + // hasn't been restored via react-hook-form's reset(reviewData) before the submit button + // is clicked, validation fails silently and the state transition never happens. + cy.get(locators.claimReview.INPUT_CLASSIFICATION).should("exist"); cy.checkRecaptcha(); - cy.get(locators.claimReview.BTN_SELECTED_REVIEW).should("exist").click(); + cy.get(locators.claimReview.BTN_SELECTED_REVIEW) + .should("be.visible") + .and("be.enabled") + .click(); cy.get(locators.claimReview.INPUT_REVIEWER) .should("exist") - .type(`${review.username}{downarrow}{downarrow}{enter}`, { delay: 200 }) + .type(`${review.username}{downarrow}{downarrow}{enter}`, { + delay: 200, + }); cy.checkRecaptcha(); cy.get(locators.claimReview.BTN_SUBMIT).should("be.enabled").click(); cy.get(locators.claimReview.TEXT_REVIEWER_ERROR).should("exist"); @@ -113,8 +122,10 @@ describe("Test claim review", () => { }); it("should not be able submit after choosing assigned user as reviewer", () => { + cy.intercept("GET", "/api/reviewtask/hash/*").as("getReviewTask"); cy.login(); goToClaimReviewPage(); + cy.wait("@getReviewTask"); blockAssignedUserReview(); }); }); diff --git a/cypress/e2e/tests/source.cy.ts b/cypress/e2e/tests/source.cy.ts index 35aa3dd32..4278464c6 100644 --- a/cypress/e2e/tests/source.cy.ts +++ b/cypress/e2e/tests/source.cy.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ /// import locators from "../../support/locators"; @@ -34,7 +33,7 @@ describe("Create source and source review", () => { .click(); cy.get(locators.claimReview.INPUT_USER) .should("exist") - .type(`${review.username}{downarrow}{enter}`, { delay: 200 }) + .type(`${review.username}{downarrow}{enter}`, { delay: 200 }); cy.get('[title="reCAPTCHA"]').should("exist"); cy.get(locators.claimReview.BTN_ASSIGN_USER).should("be.disabled"); cy.checkRecaptcha(); @@ -72,8 +71,8 @@ describe("Create source and source review", () => { .click(); cy.get(locators.claimReview.INPUT_REVIEWER) .should("exist") - .type(`${review.username}{downarrow}{enter}`, { delay: 200 }) - cy.checkRecaptcha(); + .type(`${review.username}{downarrow}{enter}`, { delay: 200 }); + cy.checkRecaptcha(); cy.get(locators.claimReview.BTN_SUBMIT).should("be.enabled").click(); cy.get(locators.claimReview.TEXT_REVIEWER_ERROR).should("exist"); }); diff --git a/cypress/e2e/tests/verificationRequest.cy.ts b/cypress/e2e/tests/verificationRequest.cy.ts new file mode 100644 index 000000000..9444078ee --- /dev/null +++ b/cypress/e2e/tests/verificationRequest.cy.ts @@ -0,0 +1,182 @@ + +/// + +import { fullVerificationRequest, regexVerificationRequestPage, updatedSource, minimumContent } from "../../fixtures/verificationRequest"; +import locators from "../../support/locators"; +import { Dayjs } from "dayjs" +import { getPastDay, today } from "../../utils/dateUtils"; + +describe("Test verification request", () => { + const getHashFromUrl = (interception) => interception.request.url.split("/").pop(); + + const openCreateVerificationRequestForm = () => { + cy.get(locators.floatButton.FLOAT_BUTTON).click(); + cy.get(locators.floatButton.ADD_VERIFICATION_REQUEST).click(); + cy.url().should("contain", "/verification-request/create"); + }; + + const selectPublicationDate = (date: Dayjs) => { + cy.get(locators.verificationRequest.FORM_PUBLICATION_DATE).click(); + cy.contains('[role="gridcell"]', date.format("D")).click(); + }; + + const saveVerificationRequest = () => { + cy.checkRecaptcha(); + cy.get(locators.verificationRequest.SAVE_BUTTON).click(); + }; + + const assertDetailFields = (fields: Array<[string, string]>) => { + fields.forEach(([selector, expected]) => { + cy.get(selector).should("be.visible").and("contain", expected); + }); + }; + + const goToVerificationRequest = (data_hash: string) => { + cy.intercept("GET", "**/verification-request/**").as("getVerification"); + cy.visit(`/verification-request/${data_hash}`); + cy.wait("@getVerification"); + } + + describe("lifecycle verification request", () => { + beforeEach("login", () => cy.login()); + + it("should prevent submission when required fields are missing", () => { + openCreateVerificationRequestForm(); + saveVerificationRequest(); + cy.get(locators.verificationRequest.ERROR_VALIDATION_CONTENT).should("be.visible") + cy.get(locators.verificationRequest.ERROR_VALIDATION_PUBLICATION_DATE).should("be.visible") + cy.url().should("contain", "/verification-request/create"); + }); + + describe("Full verification request flow", () => { + let fullRequestHash: string; + + it("should create a verification request with all optional and mandatory fields", () => { + openCreateVerificationRequestForm(); + cy.intercept("GET", "**/verification-request/**").as("getVerification"); + + cy.get(locators.verificationRequest.FORM_CONTENT).type(fullVerificationRequest.content); + selectPublicationDate(today); + cy.get(locators.verificationRequest.FORM_REPORT_TYPE).click(); + cy.contains(fullVerificationRequest.reportType).click(); + cy.get(locators.verificationRequest.FORM_IMPACT_AREA).type(fullVerificationRequest.impactArea, { delay: 200 }); + cy.contains(fullVerificationRequest.impactArea).click(); + + cy.get(locators.verificationRequest.FORM_HEARD_FROM).type(fullVerificationRequest.heardFrom); + cy.get(locators.verificationRequest.FORM_SOURCE).type(`https://${fullVerificationRequest.source}`); + cy.get(locators.verificationRequest.FORM_EMAIL).type(fullVerificationRequest.email); + saveVerificationRequest(); + + cy.wait("@getVerification").then((interception) => { + getHashFromUrl(interception) + fullRequestHash = getHashFromUrl(interception); + cy.log("Hash capturado:", fullRequestHash); + }); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_CONTENT, fullVerificationRequest.content], + [locators.verificationRequest.DETAIL_REPORT_TYPE, fullVerificationRequest.reportType], + [locators.verificationRequest.DETAIL_IMPACT_AREA, fullVerificationRequest.impactArea], + [locators.verificationRequest.DETAIL_SOURCE_CHANNEL, "Web"], + [locators.verificationRequest.DETAIL_HEARD_FROM, fullVerificationRequest.heardFrom], + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, today.format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_DATE, today.format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_SOURCE_0, fullVerificationRequest.source], + ]); + + it("should update an existing request by adding additional sources and modifying the publication date", () => { + goToVerificationRequest(fullRequestHash) + + cy.get(locators.verificationRequest.EDIT_BUTTON).should("be.visible").click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ADD).click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ITEM_1).type(`https://${updatedSource}`); + selectPublicationDate(getPastDay(1)); + saveVerificationRequest(); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, getPastDay(1).format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_SOURCE_1, updatedSource], + ]) + }) + }) + + it("should manage topic tags by adding and removing them", () => { + goToVerificationRequest(fullRequestHash) + cy.intercept("PUT", "**/verification-request/*/topics").as("updateTopics"); + + cy.get(locators.verificationRequest.ADD_TOPIC_ICON).click(); + cy.get(locators.verificationRequest.TYPE_TOPIC_INPUT).type(fullVerificationRequest.topic, { delay: 200 }); + cy.contains(fullVerificationRequest.topic).click(); + cy.get(locators.verificationRequest.ADD_TOPIC_SUBMIT).click(); + + cy.wait("@updateTopics").then((interception) => { + expect(interception.response.statusCode).to.be.oneOf([200, 201]); + }); + + cy.contains(locators.verificationRequest.DETAIL_TOPIC_TAG, fullVerificationRequest.topic.toUpperCase()) + .should("be.visible") + .within(() => { + cy.get(locators.verificationRequest.REMOVE_TOPIC_ICON).click(); + }); + + + cy.contains(locators.verificationRequest.DETAIL_TOPIC_TAG, fullVerificationRequest.topic.toUpperCase()).should("not.exist"); + }) + + it("should discard unsaved changes when the edition form is canceled", () => { + goToVerificationRequest(fullRequestHash) + + cy.get(locators.verificationRequest.EDIT_BUTTON).should("be.visible").click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ADD).click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ITEM_1).type(`https://${updatedSource}`); + selectPublicationDate(getPastDay(10)); + cy.get(locators.verificationRequest.CANCEL_BUTTON).click(); + + cy.get(locators.verificationRequest.DETAIL_PUBLICATION_DATE).should("be.visible").and("not.contain", getPastDay(10).format("DD/MM/YYYY")); + cy.get(locators.verificationRequest.DETAIL_SOURCE_1).should("not.exist"); + }) + }); + + describe("Minimum verification request flow", () => { + let minRequestHash: string; + + it("should allow request creation using only the minimum mandatory information", () => { + openCreateVerificationRequestForm(); + cy.intercept("GET", "**/verification-request/**").as("getVerification"); + + cy.get(locators.verificationRequest.FORM_CONTENT).type(minimumContent); + selectPublicationDate(today); + saveVerificationRequest(); + cy.wait("@getVerification").then((interception) => { + getHashFromUrl(interception) + minRequestHash = getHashFromUrl(interception); + + cy.log("Hash capturado:", minRequestHash); + }); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_CONTENT, minimumContent], + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, today.format("DD/MM/YYYY")], + ]) + }); + + it("should supplement a minimalist request by adding its first source during edition", () => { + goToVerificationRequest(minRequestHash) + + cy.get(locators.verificationRequest.EDIT_BUTTON).should("be.visible").click(); + cy.get(locators.verificationRequest.FORM_SOURCE_ITEM_0).type(`https://${fullVerificationRequest.source}`); + selectPublicationDate(getPastDay(1)); + saveVerificationRequest(); + cy.url().should("match", regexVerificationRequestPage); + + assertDetailFields([ + [locators.verificationRequest.DETAIL_PUBLICATION_DATE, getPastDay(1).format("DD/MM/YYYY")], + [locators.verificationRequest.DETAIL_SOURCE_0, fullVerificationRequest.source], + ]) + }) + }) + }); +}); diff --git a/cypress/fixtures/verificationRequest.ts b/cypress/fixtures/verificationRequest.ts new file mode 100644 index 000000000..83e9e3470 --- /dev/null +++ b/cypress/fixtures/verificationRequest.ts @@ -0,0 +1,16 @@ +const fullVerificationRequest = { + content: "Verification Request Content", + reportType: "Discurso", + impactArea: "Ambientalismo", + topic: "Socialismo", + heardFrom: "Verification Request heardFrom", + source: "wikimedia.org", + email: "test-cypress@aletheiafact.org", +}; + +const minimumContent = "Verification Request Content minimium" + +const regexVerificationRequestPage = /\/verification-request\/[\w-]+$/ +const updatedSource = "www.wikidata.org" + +export { fullVerificationRequest, regexVerificationRequestPage, updatedSource, minimumContent } diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 152418d0b..15efef0f0 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,12 +1,16 @@ import user from "../fixtures/user"; import locators from "./locators"; -Cypress.Commands.add("login", () => { +Cypress.Commands.add("goToLoginPage", () => { cy.visit("http://localhost:3000"); cy.title().should("contain", "AletheiaFact.org"); cy.get(locators.menu.USER_ICON).click(); cy.get(locators.menu.LOGIN_MENU).click(); cy.url().should("contains", "login"); +}); + +Cypress.Commands.add("login", () => { + cy.goToLoginPage(); cy.get(locators.login.USER).type(user.email); cy.get(locators.login.PASSWORD).type(user.password); cy.get(locators.login.BTN_LOGIN).click(); @@ -21,20 +25,50 @@ Cypress.Commands.add("checkRecaptcha", () => { // get the iframe > document > body // and retries until the body is not empty or fails with timeout return cy - .get('iframe[title="reCAPTCHA"]') + .get('iframe[title="reCAPTCHA"]', { timeout: 10000 }) .its("0.contentDocument.body") .should("not.be.empty") .then(cy.wrap); }; - getIframeBody().find("#recaptcha-anchor").click(); + getIframeBody() + .find("#recaptcha-anchor", { timeout: 10000 }) + .should("be.visible") + .click({ force: true }); + + cy.wait(500); }); +Cypress.Commands.add("goToSignUpPage", () => { + cy.visit("http://localhost:3000/sign-up"); + cy.url().should("contains", "sign-up"); +}); + +Cypress.Commands.add( + "signup", + (name: string, email: string, password: string) => { + cy.goToSignUpPage(); + cy.get(locators.signup.NAME).type(name); + cy.get(locators.signup.EMAIL).type(email); + cy.get(locators.signup.PASSWORD).type(password); + cy.get(locators.signup.REPEATED_PASSWORD).type(password); + cy.checkRecaptcha(); + cy.get(locators.signup.BTN_SUBMIT).click(); + } +); + declare global { namespace Cypress { interface Chainable { login(): Chainable; checkRecaptcha(): Chainable; + goToLoginPage(): Chainable; + goToSignUpPage(): Chainable; + signup( + name: string, + email: string, + password: string + ): Chainable; } } } diff --git a/cypress/support/locators.ts b/cypress/support/locators.ts index d836c6549..3b0c82899 100644 --- a/cypress/support/locators.ts +++ b/cypress/support/locators.ts @@ -7,6 +7,18 @@ const locators = { BTN_LOGIN: "[data-cy=loginButton]", }, + signup: { + NAME: "[data-cy=nameInputCreateAccount]", + EMAIL: "[data-cy=emailInputCreateAccount]", + PASSWORD: "[data-cy=passwordInputCreateAccount]", + REPEATED_PASSWORD: "[data-cy=repeatedPasswordInputCreateAccount]", + BTN_SUBMIT: "[data-cy=loginButton]", + ERROR_NAME: "[data-cy=nameError]", + ERROR_EMAIL: "[data-cy=emailError]", + ERROR_PASSWORD: "[data-cy=passwordError]", + ERROR_REPEATED_PASSWORD: "[data-cy=repeatedPasswordError]", + }, + personality: { BTN_SEE_MORE_PERSONALITY: "[data-cy=testSeeMorePersonality]", BTN_ADD_PERSONALITY: "[data-cy=testButtonCreatePersonality]", @@ -26,7 +38,6 @@ const locators = { BTN_SUBMIT_CLAIM: "[data-cy=testSaveButton]", INPUT_TITLE: "[data-cy=testTitleClaimForm]", INPUT_DATA: "[data-cy=testSelectDate]", - INPUT_DATA_TODAY: ".MuiPickersDay-today", INPUT_SOURCE: "[data-cy=testSource1]", }, @@ -40,6 +51,7 @@ const locators = { ADD_CLAIM: "[data-cy=testFloatButtonAddClaim]", ADD_PERSONALITY: "[data-cy=testFloatButtonAddPersonality]", ADD_SOURCE: "[data-cy=testFloatButtonAddSources]", + ADD_VERIFICATION_REQUEST: "[data-cy=testFloatButtonAddVerificationRequest]" }, claimReview: { @@ -71,6 +83,49 @@ const locators = { "[data-cy=testClaimReviewSourcesButton]", }, + verificationRequest: { + FORM_CONTENT: "[data-cy=testClaimReviewcontent]", + FORM_REPORT_TYPE: "[data-cy=testClaimReviewreportType]", + FORM_IMPACT_AREA: "[data-cy=testClaimReviewimpactArea]", + FORM_HEARD_FROM: "[data-cy=testClaimReviewheardFrom]", + FORM_PUBLICATION_DATE: "[data-cy=testSelectDate]", + FORM_SOURCE: "[data-cy=testClaimReviewsource]", + FORM_EMAIL: "[data-cy=testClaimReviewemail]", + + FORM_SOURCE_ITEM_0: "[data-cy=testClaimReviewsourceEdit-0]", + FORM_SOURCE_ITEM_1: "[data-cy=testClaimReviewsourceEdit-1]", + FORM_SOURCE_ADD: "[data-cy=testClaimReviewsource-addSources]", + FORM_SOURCE_REMOVE_2: "[data-cy=testClaimReviewsourceRemove-2]", + + DETAIL_CONTENT: "[data-cy=testVerificationRequestContent]", + DETAIL_REPORT_TYPE: "[data-cy=testVerificationRequestReportType]", + DETAIL_IMPACT_AREA: "[data-cy=testVerificationRequestImpactArea]", + DETAIL_SOURCE_CHANNEL: "[data-cy=testVerificationRequestSourceChannel]", + DETAIL_SEVERITY: "[data-cy=testVerificationRequestSeverity]", + DETAIL_HEARD_FROM: "[data-cy=testVerificationRequestHeardFrom]", + DETAIL_PUBLICATION_DATE: "[data-cy=testVerificationRequestPublicationDate]", + DETAIL_DATE: "[data-cy=testVerificationRequestDate]", + DETAIL_SOURCE_0: "[data-cy=testVerificationRequestSource0]", + DETAIL_SOURCE_1: "[data-cy=testVerificationRequestSource1]", + + ERROR_VALIDATION_CONTENT: "[data-cy=testClaimReviewErrorcontent]", + ERROR_VALIDATION_PUBLICATION_DATE: "[data-cy=testClaimReviewErrorpublicationDate]", + + DETAIL_CARD_CONTENT: "[data-cy=testVerificationRequestCardContent0]", + DETAIL_CARD_CONTENT_1: "[data-cy=testVerificationRequestCardContent1]", + + ADD_TOPIC_ICON: "[data-cy=testVerificationRequestTopicsToggle]", + TYPE_TOPIC_INPUT: "[data-cy=testVerificationRequestTopicsInput]", + ADD_TOPIC_SUBMIT: "[data-cy=testVerificationRequestAddTopicButton]", + DETAIL_TOPIC_TAG: "[data-cy=testVerificationRequestTopicChip0]", + REMOVE_TOPIC_ICON: "[data-cy=testVerificationRequestTopicRemoveButton0]", + + SEE_FULL_BUTTON: "[data-cy=testSeeFullVerificationRequest]", + EDIT_BUTTON: "[data-cy=testVerificationRequestEditButton]", + SAVE_BUTTON: "[data-cy=testSaveButton]", + CANCEL_BUTTON: "[data-cy=testCancelButton]", + }, + menu: { SIDE_MENU: "[data-cy=testOpenSideMenu]", USER_ICON: "[data-cy=testUserIcon]", diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 15353f241..68f11a035 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -1,8 +1,10 @@ { "compilerOptions": { - "target": "es5", - "lib": ["es5", "dom"], - "types": ["cypress"] + "target": "es2017", + "lib": ["es2017", "dom"], + "types": ["cypress"], + "moduleResolution": "node", + "esModuleInterop": true }, "include": ["**/*.ts"] } \ No newline at end of file diff --git a/cypress/utils/dateUtils.ts b/cypress/utils/dateUtils.ts new file mode 100644 index 000000000..96bb020d4 --- /dev/null +++ b/cypress/utils/dateUtils.ts @@ -0,0 +1,6 @@ +import dayjs from "dayjs"; + +export const today = dayjs(); + +export const getPastDay = (daysAgo: number) => + dayjs().subtract(daysAgo, "day"); diff --git a/dashboard/all-actions-in-fact-checking.js b/dashboard/all-actions-in-fact-checking.js index fb025c0f4..890e57a80 100644 --- a/dashboard/all-actions-in-fact-checking.js +++ b/dashboard/all-actions-in-fact-checking.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - //Querying all action taken in the fact-checking workflow db.getCollection("stateevents").aggregate([ { $match: { taskId: ObjectId("62eaaaf819007c3f2bfe2a80") } }, diff --git a/dashboard/average-fact-check-time.js b/dashboard/average-fact-check-time.js index 05110adfa..faa26bd58 100644 --- a/dashboard/average-fact-check-time.js +++ b/dashboard/average-fact-check-time.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - //Querying average fact-checking time db.getCollection("stateevents").aggregate([ { $match: { taskId: ObjectId("62e99af928bdbf2c8f1de4cc") } }, diff --git a/dashboard/between-states.js b/dashboard/between-states.js index 3b674cb15..e83addabc 100644 --- a/dashboard/between-states.js +++ b/dashboard/between-states.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - //Querying how long it takes to move between specifics states db.getCollection("stateevents").aggregate([ { diff --git a/dashboard/claim-started.js b/dashboard/claim-started.js index 44f5afb09..2d517be0b 100644 --- a/dashboard/claim-started.js +++ b/dashboard/claim-started.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - //Querying how long it took from start to finish considering //the day the claim was registered db.getCollection("stateevents").aggregate([ diff --git a/dashboard/fact-checking-started.js b/dashboard/fact-checking-started.js index 76acfc622..fb522c105 100644 --- a/dashboard/fact-checking-started.js +++ b/dashboard/fact-checking-started.js @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ - //Querying how long it took from start to finish considering //the day the fact-checking started db.getCollection("stateevents").aggregate([ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..c3a322b1d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,76 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import { fixupConfigRules } from "@eslint/compat"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default defineConfig([globalIgnores([ + "dist/*", + "coverage/*", + "**/*.d.ts", + "src/types/", + "**/bak/**/*", + "**/bin/**/*", + "**/config/**/*", + "**/dist/**/*", + "**/doc/**/*", + "**/node_modules/**/*", + "**/public/**/*", + "**/report/**/*", + "**/webpack.config*.js", + "**/docs/**/*", + "migrations/**/*", + "**/migrate-mongo-config.ts", +]), { + extends: fixupConfigRules(compat.extends("plugin:@next/next/recommended")), + + languageOptions: { + globals: { + ...globals.jest, + }, + + parser: tsParser, + ecmaVersion: 2020, + sourceType: "module", + }, + + settings: { + react: { + version: "detect", + }, + }, + + rules: { + "array-bracket-spacing": 0, + "arrow-parens": 0, + camelcase: 0, + "comma-dangle": 0, + "comma-spacing": 0, + "computed-property-spacing": 0, + indent: 0, + "key-spacing": 0, + "max-statements-per-line": 0, + "no-multi-spaces": 0, + "no-underscore-dangle": 0, + "no-unused-expressions": 0, + "no-restricted-syntax": 0, + "no-unused-vars": 0, + "one-var": 0, + "operator-linebreak": 0, + "space-before-function-paren": 0, + "space-in-parens": 0, + "valid-jsdoc": 0, + "jsx-a11y/anchor-is-valid": 0, + }, +}]); \ No newline at end of file diff --git a/lib/editor-parser.ts b/lib/editor-parser.ts index 197636432..5ace1fdc2 100644 --- a/lib/editor-parser.ts +++ b/lib/editor-parser.ts @@ -96,10 +96,10 @@ export class EditorParser { */ private convertToParagraphElements(content: MultiParagraphContent): string { const lines = content.split('\n'); - + // Check if there are any empty lines (for visual spacing) const hasEmptyLines = lines.some(line => line.trim() === ''); - + if (hasEmptyLines) { // Use
approach for content with intentional empty lines/spacing return this.convertLineBreaksToHtml(content); @@ -157,7 +157,7 @@ export class EditorParser { if (this.isMultiParagraphContent(contentStr)) { const lines = contentStr.split('\n'); const hasEmptyLines = lines.some(line => line.trim() === ''); - + if (hasEmptyLines) { // Use
approach for content with intentional empty lines/spacing const htmlContent = this.convertLineBreaksToHtml(contentStr); @@ -215,7 +215,7 @@ export class EditorParser { if (this.isMultiParagraphContent(joinedContent)) { const lines = joinedContent.split('\n'); const hasEmptyLines = lines.some(line => line.trim() === ''); - + if (hasEmptyLines) { // Use
approach for content with intentional empty lines/spacing const finalContent = this.convertLineBreaksToHtml(joinedContent); @@ -355,10 +355,10 @@ export class EditorParser { { type, content: cardContent }: RemirrorJSON ): MultiParagraphContent { const paragraphContents: ParagraphContent[] = []; - + for (const { content } of cardContent) { const textFragments: TextFragment[] = []; - + if (content) { for (const { text, marks } of content) { if (marks) { @@ -376,12 +376,12 @@ export class EditorParser { } } } - + // Combine all fragments within a paragraph into a single paragraph content const paragraphContent: ParagraphContent = textFragments.join(""); paragraphContents.push(paragraphContent); } - + // Join paragraphs with newlines to create multi-paragraph content return paragraphContents.join("\n") as MultiParagraphContent; } @@ -684,4 +684,22 @@ export class EditorParser { const match = fragmentText.match(MarkupCleanerRegex); return match ? match[1] : ""; } + + removeTrailingParagraph(editorJSON: RemirrorJSON): RemirrorJSON { + if (!editorJSON?.content || !Array.isArray(editorJSON.content)) { + return editorJSON; + } + + const content = [...editorJSON.content]; + const lastItem = content[content.length - 1]; + + if (lastItem?.type === "paragraph") { + content.pop(); + } + + return { + ...editorJSON, + content, + }; + } } diff --git a/migrations/20230131211511-claimCollectionToDebate.ts b/migrations/20230131211511-claimCollectionToDebate.ts index 07518e794..8301bf713 100644 --- a/migrations/20230131211511-claimCollectionToDebate.ts +++ b/migrations/20230131211511-claimCollectionToDebate.ts @@ -3,7 +3,6 @@ import { Db } from "mongodb"; const ObjectId = require("mongodb").ObjectID; export async function up(db: Db) { - return; // migrations not needed try { const claimCollectionCursor = await db @@ -16,7 +15,7 @@ export async function up(db: Db) { type: "info", }); while (await claimCollectionCursor.hasNext()) { - const doc = await claimCollectionCursor.next(); + const doc: any = await claimCollectionCursor.next(); // create debate, claim and claim revision with claim collection basic data let hashString = doc.personalities.join(" "); hashString += ` ${doc.title} ${doc.date.toString()}`; @@ -69,7 +68,7 @@ export async function up(db: Db) { editorContentObject.content[i].attrs.claimId ), }); - const speechId = ObjectId(oldClaimRevision.contentId); + const speechId = ObjectId(oldClaimRevision?.contentId); delete editorContentObject.content[i].attrs.claimId; editorContentObject.content[i].attrs.speechId = @@ -80,8 +79,8 @@ export async function up(db: Db) { { $set: { personality: - oldClaimRevision.personalities?.[0] || - oldClaimRevision.personality || + oldClaimRevision?.personalities?.[0] || + oldClaimRevision?.personality || "", }, } @@ -97,22 +96,22 @@ export async function up(db: Db) { await db .collection("claimreviews") .updateMany( - { claim: oldClaimRevision.claimId }, + { claim: oldClaimRevision?.claimId }, { $set: { claim: claim.insertedId } } ); await db .collection("claimreviewtasks") .updateMany( - { claim: oldClaimRevision.claimId.toString() }, + { claim: oldClaimRevision?.claimId.toString() }, { $set: { claim: claim.insertedId.toString() } } ); await db .collection("claims") - .deleteOne({ _id: oldClaimRevision.claimId }); + .deleteOne({ _id: oldClaimRevision?.claimId }); await db .collection("claimrevisions") - .deleteOne({ _id: oldClaimRevision._id }); + .deleteOne({ _id: oldClaimRevision?._id }); } } @@ -127,13 +126,13 @@ export async function up(db: Db) { .find({ targetId: doc._id }); while (await sourceCursor.hasNext()) { const source = await sourceCursor.next(); - const targetId = source.targetId.map((target) => { + const targetId = source?.targetId.map((target) => { return target.toString() === doc._id.toString() ? claim.insertedId : target; }); await db.collection("sources").updateOne( - { _id: source._id }, + { _id: source?._id }, { $set: { targetId }, } @@ -163,7 +162,7 @@ export async function down(db: Db) { }); const debateCursor = await db.collection("debates").find(); while (await debateCursor.hasNext()) { - const debate = await debateCursor.next(); + const debate: any = await debateCursor.next(); const editor = await db .collection("editors") .findOne({ reference: debate._id }); @@ -175,10 +174,10 @@ export async function down(db: Db) { }); const claim = await db.collection("claims").findOne({ - latestRevision: claimRevision._id, + latestRevision: claimRevision?._id, }); - const editorContentObject = editor.editorContentObject; + const editorContentObject = editor?.editorContentObject; if (editorContentObject?.content) { for (let i = 0; i < editorContentObject.content.length; i++) { if ( @@ -196,7 +195,7 @@ export async function down(db: Db) { const newClaim = await db .collection("claims") .insertOne({ - slug: claimRevision.slug, + slug: claimRevision?.slug, personalities: [ObjectId(personalityId)], isDeleted: false, deletedAt: null, @@ -207,10 +206,10 @@ export async function down(db: Db) { claimId: newClaim.insertedId, contentModel: "Speech", personalities: [ObjectId(personalityId)], - title: claimRevision.title, - date: claimRevision.date, - slug: claimRevision.slug, - contentId: speech._id, + title: claimRevision?.title, + date: claimRevision?.date, + slug: claimRevision?.slug, + contentId: speech?._id, }); await db.collection("claims").updateOne( { _id: newClaim.insertedId }, @@ -233,17 +232,18 @@ export async function down(db: Db) { .insertOne({ isHidden: false, isLive: debate.isLive, - personalities: claim.personalities, - title: claimRevision.title, - date: claimRevision.date, - slug: claimRevision.slug, - editorContentObject: editor.editorContentObject, + personalities: claim?.personalities, + title: claimRevision?.title, + date: claimRevision?.date, + slug: claimRevision?.slug, + editorContentObject: editor?.editorContentObject, }); const reviews = await db .collection("claimreviews") - .find({ claim: claim._id }); - reviews.forEach(async (review) => { + .find({ claim: claim?._id }) + .toArray(); + for (const review of reviews) { db.collection("sentences") .findOne({ data_hash: review.data_hash }) .then(async (sentence) => { @@ -270,14 +270,14 @@ export async function down(db: Db) { .collection("claimreviews") .updateOne( { _id: review._id }, - { $set: { claim: revision.claimId } } + { $set: { claim: revision?.claimId } } ); await db.collection("claimreviewTasks").updateOne( { data_hash: review.data_hash }, { $set: { "machine.context.claimReview.claim": - revision.claimId.toString(), + revision?.claimId.toString(), }, } ); @@ -290,28 +290,28 @@ export async function down(db: Db) { type: "error", }); }); - }); + }; - await db.collection("editors").deleteOne({ _id: editor._id }); + await db.collection("editors").deleteOne({ _id: editor?._id }); await db.collection("debates").deleteOne({ _id: debate._id }); - await db.collection("claims").deleteOne({ _id: claim._id }); + await db.collection("claims").deleteOne({ _id: claim?._id }); await db.collection("claimrevisions").deleteOne({ - _id: claimRevision._id, + _id: claimRevision?._id, }); //update sources const sourceCursor = await db .collection("sources") - .find({ targetId: claim._id }); + .find({ targetId: claim?._id }); while (await sourceCursor.hasNext()) { const source = await sourceCursor.next(); - const targetId = source.targetId.map((target) => { - return target.toString() === claim._id.toString() + const targetId = source?.targetId.map((target) => { + return target.toString() === claim?._id.toString() ? claimCollection.insertedId : target; }); await db.collection("sources").updateOne( - { _id: source._id }, + { _id: source?._id }, { $set: { targetId }, } diff --git a/migrations/20231001191440-create-novu-subscribers.ts b/migrations/20231001191440-create-novu-subscribers.ts index 35312e9b4..42fcfc410 100644 --- a/migrations/20231001191440-create-novu-subscribers.ts +++ b/migrations/20231001191440-create-novu-subscribers.ts @@ -3,7 +3,6 @@ import { Novu } from "@novu/node"; import config from "../migrate-mongo-config"; export async function up(db: Db) { - return; // migrations not needed /* Create novu subscribers */ const novuApiKey = await config.novu_api_key; @@ -11,9 +10,13 @@ export async function up(db: Db) { const usersCursor = await db.collection("users").find(); while (await usersCursor.hasNext()) { - const { _id, email, name } = await usersCursor.next(); + const user = await usersCursor.next(); + if (!user) { + continue; + } + const { _id, email, name } = user; - await novu.subscribers.identify(_id, { + await novu.subscribers.identify(_id.toString(), { email, firstName: name, }); @@ -29,6 +32,6 @@ export async function down(db: Db) { while (await usersCursor.hasNext()) { const user = await usersCursor.next(); - await novu.subscribers.delete(user.id); + await novu.subscribers.delete(user?.id); } } diff --git a/migrations/20231013183353-move-source-extra-properties.ts b/migrations/20231013183353-move-source-extra-properties.ts index 9be3dbaef..d001991c8 100644 --- a/migrations/20231013183353-move-source-extra-properties.ts +++ b/migrations/20231013183353-move-source-extra-properties.ts @@ -1,12 +1,14 @@ import { Db } from "mongodb"; export async function up(db: Db) { - return; // migrations not needed const sourcesCursor = await db.collection("sources").find(); while (await sourcesCursor.hasNext()) { const source = await sourcesCursor.next(); + if (!source) { + continue; + } const { _id, user, href, targetId } = source; const updateData: any = { @@ -27,6 +29,6 @@ export async function up(db: Db) { copyPropsIfExist("targetText"); copyPropsIfExist("textRange"); - await db.collection("sources").update({ _id: source._id }, updateData); + await db.collection("sources").updateOne({ _id: source?._id }, { $set: updateData }); } } diff --git a/migrations/20251111165920-add-aliases-to-topics.ts b/migrations/20251111165920-add-aliases-to-topics.ts new file mode 100644 index 000000000..d5dcc6def --- /dev/null +++ b/migrations/20251111165920-add-aliases-to-topics.ts @@ -0,0 +1,40 @@ +import { Db } from "mongodb"; + +export async function up(db: Db) { + try { + await db + .collection("topics") + .updateMany( + { aliases: { $exists: false } }, + { $set: { aliases: [] } } + ); + + // Special case: Add "COP30" alias to the specific Wikidata entity + // Q115323194 = Conferência das Nações Unidas sobre as Mudanças Climáticas de 2025 + await db + .collection("topics") + .updateOne( + { wikidataId: "Q115323194" }, + { $set: { aliases: ["COP30"] } } + ); + console.log("Successfully added aliases field to topics."); + } catch (error) { + console.error("Error during migration:", error); + throw error; + } +} + +export async function down(db: Db) { + try { + await db + .collection("topics") + .updateMany( + { aliases: { $exists: true } }, + { $unset: { aliases: "" } } + ); + console.log("Removed aliases field from all topics."); + } catch (error) { + console.error("Error during migration rollback:", error); + throw error; + } +} diff --git a/migrations/20251111180309-add-timestamps-to-topics.ts b/migrations/20251111180309-add-timestamps-to-topics.ts new file mode 100644 index 000000000..d79972afd --- /dev/null +++ b/migrations/20251111180309-add-timestamps-to-topics.ts @@ -0,0 +1,53 @@ +import { Db, ObjectId } from "mongodb"; + +export async function up(db: Db) { + try { + const topics = await db + .collection("topics") + .find({ + createdAt: { $exists: false }, + }) + .toArray(); + + console.log(`Migrating ${topics.length} topics to add timestamps...`); + + if (topics.length === 0) { + console.log("No topics to migrate."); + return; + } + + const bulkOps = topics.map((topic) => { + const createdAt = (topic._id as ObjectId).getTimestamp(); + + return { + updateOne: { + filter: { _id: topic._id }, + update: { + $set: { + createdAt: createdAt, + updatedAt: createdAt, + }, + }, + }, + }; + }); + + const result = await db.collection("topics").bulkWrite(bulkOps); + console.log(`Successfully migrated ${result.modifiedCount} topics.`); + } catch (error) { + console.error("Error during migration:", error); + throw error; + } +} + +export async function down(db: Db) { + try { + await db + .collection("topics") + .updateMany({}, { $unset: { createdAt: "", updatedAt: "" } }); + console.log("Removed timestamps from all topics."); + } catch (error) { + console.error("Error during migration rollback:", error); + throw error; + } +} diff --git a/migrations/20251111191701-addRightSourceChannelForSomeVerificationRequests.ts b/migrations/20251111191701-addRightSourceChannelForSomeVerificationRequests.ts new file mode 100644 index 000000000..c0e53e728 --- /dev/null +++ b/migrations/20251111191701-addRightSourceChannelForSomeVerificationRequests.ts @@ -0,0 +1,27 @@ +import { Db } from "mongodb"; + +export async function up(db: Db) { + const filter = { + heardFrom: { $regex: /^Automated Monitoring -/i }, + sourceChannel: { $ne: "automated_monitoring" }, + }; + + const update = { + $set: { sourceChannel: "automated_monitoring" }, + }; + + const result = await db + .collection("verificationrequests") + .updateMany(filter, update); + + console.log(`✅ Updated ${result.modifiedCount} verification requests`); +} + +export async function down(db: Db) { + /** + * NO-OP: This migration is irreversible via script. + * Rolling back would incorrectly force all sourceChannel values to "instagram", + * potentially overwriting original historical data. + */ + console.warn('⚠️ Down migration: No action taken (Data correction is irreversible via script).'); +} diff --git a/migrations/20251112113748-add-timestamps-to-collections.ts b/migrations/20251112113748-add-timestamps-to-collections.ts new file mode 100644 index 000000000..90029aae0 --- /dev/null +++ b/migrations/20251112113748-add-timestamps-to-collections.ts @@ -0,0 +1,338 @@ +import { Db, ObjectId } from "mongodb"; + +/** + * Migration to add timestamps (createdAt and updatedAt) to collections + * that are missing them. + * + * For existing documents: + * - If the document has an ObjectId _id, we extract the creation date from it + * - If the collection has a 'date' field, we use it as createdAt + * - Otherwise, we use the current date + * - updatedAt is set to createdAt for existing documents + * + * Note: We keep the original 'date' fields for now to track all date parameters + * being used on the platform before fully migrating to the new createdAt values. + */ +export async function up(db: Db) { + const now = new Date(); + const BATCH_SIZE = 1000; + + // Helper function to extract date from ObjectId + const getCreationDateFromObjectId = (id: any): Date => { + if (id instanceof ObjectId) { + return id.getTimestamp(); + } + return now; + }; + + // Helper function to validate date + const isValidDate = (date: any): boolean => { + if (!date) return false; + const dateObj = new Date(date); + return !isNaN(dateObj.getTime()); + }; + + // Helper function to process collection in batches using cursor + const processCollectionInBatches = async ( + collectionName: string, + processBatch: (docs: any[]) => any[] + ) => { + try { + const collection = db.collection(collectionName); + const cursor = collection.find({}); + let batch: any[] = []; + let totalProcessed = 0; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (doc) { + batch.push(doc); + } + + if (batch.length >= BATCH_SIZE) { + const bulkOps = processBatch(batch); + if (bulkOps.length > 0) { + await collection.bulkWrite(bulkOps); + totalProcessed += bulkOps.length; + } + batch = []; + } + } + + // Process remaining documents + if (batch.length > 0) { + const bulkOps = processBatch(batch); + if (bulkOps.length > 0) { + await collection.bulkWrite(bulkOps); + totalProcessed += bulkOps.length; + } + } + + if (totalProcessed > 0) { + console.log(` - ${collectionName}: Updated ${totalProcessed} documents`); + } else { + console.log(` - ${collectionName}: No documents found, skipping`); + } + } catch (error) { + console.log(` - ${collectionName}: Collection not found or error occurred, skipping`); + } + }; + + // Collections with no timestamps at all + const collectionsWithoutTimestamps = [ + "namespaces", + "images", + "paragraphs", + "sentences", + "unattributeds", + "dailyreports", + "editors", + "groups", + "reviewtasks", + "sources", + "users", + "claims", + "speeches", + "personalities", + "reports", + "chatbotstates", + ]; + + // Collections that have a 'date' field that can be used as createdAt + const collectionsWithDateField = [ + { name: "stateevents", dateField: "date" }, + { name: "claimreviews", dateField: "date" }, + { name: "claimrevisions", dateField: "date" }, + { name: "histories", dateField: "date" }, + { name: "verificationrequests", dateField: "date" }, + ]; + + // Add timestamps to collections without any timestamp fields + console.log("Adding timestamps to collections without timestamp fields..."); + for (const collectionName of collectionsWithoutTimestamps) { + await processCollectionInBatches(collectionName, (docs) => + docs.map((doc) => { + const createdAt = getCreationDateFromObjectId(doc._id); + return { + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + createdAt: createdAt, + updatedAt: createdAt, + }, + }, + }, + }; + }) + ); + } + + // Add timestamps to collections with date field + console.log("\nAdding timestamps to collections with date field..."); + for (const { name: collectionName, dateField } of collectionsWithDateField) { + await processCollectionInBatches(collectionName, (docs) => + docs.map((doc) => { + // Use the date field if it exists and is valid, otherwise extract from ObjectId + const createdAt = + doc[dateField] && isValidDate(doc[dateField]) + ? new Date(doc[dateField]) + : getCreationDateFromObjectId(doc._id); + + return { + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + createdAt: createdAt, + updatedAt: createdAt, + }, + }, + }, + }; + }) + ); + } + + // Special case: badges collection (has created_at as string, need to add updatedAt) + console.log("\nUpdating badges collection..."); + await processCollectionInBatches("badges", (docs) => + docs.map((doc) => { + const createdAt = + doc.created_at && isValidDate(doc.created_at) + ? new Date(doc.created_at) + : getCreationDateFromObjectId(doc._id); + + return { + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + createdAt: createdAt, + updatedAt: createdAt, + }, + $unset: { + created_at: "", // Remove old string field + }, + }, + }, + }; + }) + ); + + // Special case: comments collection (has createdAt but missing updatedAt) + console.log("\nUpdating comments collection..."); + await processCollectionInBatches("comments", (docs) => + docs.map((doc) => { + const createdAt = + doc.createdAt && isValidDate(doc.createdAt) + ? new Date(doc.createdAt) + : getCreationDateFromObjectId(doc._id); + + return { + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + createdAt: createdAt, + updatedAt: createdAt, + }, + }, + }, + }; + }) + ); + + // Special case: wikidatacaches collection (has createdAt but missing updatedAt) + console.log("\nUpdating wikidatacaches collection..."); + await processCollectionInBatches("wikidatacaches", (docs) => + docs.map((doc) => { + const createdAt = + doc.createdAt && isValidDate(doc.createdAt) + ? new Date(doc.createdAt) + : getCreationDateFromObjectId(doc._id); + + return { + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + createdAt: createdAt, + updatedAt: createdAt, + }, + }, + }, + }; + }) + ); + + console.log("\nTimestamp migration completed!"); +} + +export async function down(db: Db) { + // Remove createdAt and updatedAt from all collections + const allCollections = [ + "namespaces", + "badges", + "images", + "paragraphs", + "sentences", + "unattributeds", + "dailyreports", + "editors", + "groups", + "reviewtasks", + "sources", + "stateevents", + "topics", + "users", + "claimreviews", + "claimrevisions", + "claims", + "speeches", + "personalities", + "reports", + "histories", + "verificationrequests", + "chatbotstates", + "comments", + "wikidatacaches", + ]; + + console.log("Removing timestamps from collections..."); + for (const collectionName of allCollections) { + try { + const collection = db.collection(collectionName); + await collection.updateMany( + {}, + { + $unset: { + createdAt: "", + updatedAt: "", + }, + } + ); + console.log(` - ${collectionName}: Removed timestamps`); + } catch (error) { + console.log(` - ${collectionName}: Collection not found or error occurred, skipping`); + } + } + + // Restore the old created_at field for badges + console.log("\nRestoring badges created_at field..."); + try { + const badgesCollection = db.collection("badges"); + const cursor = badgesCollection.find({}); + const BATCH_SIZE = 1000; + let batch: any[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (doc) { + batch.push(doc); + } + + if (batch.length >= BATCH_SIZE) { + const bulkOps = batch.map((doc) => ({ + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + created_at: doc.createdAt ? doc.createdAt.toISOString() : new Date().toISOString(), + }, + }, + }, + })); + + if (bulkOps.length > 0) { + await badgesCollection.bulkWrite(bulkOps); + } + batch = []; + } + } + + // Process remaining documents + if (batch.length > 0) { + const bulkOps = batch.map((doc) => ({ + updateOne: { + filter: { _id: doc._id }, + update: { + $set: { + created_at: doc.createdAt ? doc.createdAt.toISOString() : new Date().toISOString(), + }, + }, + }, + })); + + if (bulkOps.length > 0) { + await badgesCollection.bulkWrite(bulkOps); + } + } + + console.log(` - badges: Restored created_at field`); + } catch (error) { + console.log(` - badges: Collection not found or error occurred, skipping`); + } + + console.log("\nTimestamp rollback completed!"); +} diff --git a/migrations/20260117122748-change-history-user-strings-to-objectid.ts b/migrations/20260117122748-change-history-user-strings-to-objectid.ts new file mode 100644 index 000000000..f84374df3 --- /dev/null +++ b/migrations/20260117122748-change-history-user-strings-to-objectid.ts @@ -0,0 +1,64 @@ +import { Db, ObjectId } from "mongodb"; + +const HEX24 = /^[0-9a-fA-F]{24}$/; + +export async function up(db: Db) { + try { + const historiesToChange = await db + .collection("histories") + .find({ user: { $type: "string", $regex: HEX24 } }) + .toArray(); + + if (historiesToChange.length === 0) { + console.log("No histories with string users to change."); + } else { + const bulkOps = historiesToChange.map((history) => ({ + updateOne: { + filter: { _id: history._id }, + update: { + $set: { + user: new ObjectId(history.user as string), + migration_revert_flag: true, + }, + }, + }, + })); + + const result = await db.collection("histories").bulkWrite(bulkOps); + console.log(`Converted ${result.modifiedCount} history.user fields to ObjectId.`); + } + } catch (error) { + console.error("Migration UP failed:", error); + throw error; + } +} + +export async function down(db: Db) { + try { + const historiesToRedefine = await db + .collection("histories") + .find({ migration_revert_flag: true }) + .toArray(); + + if (historiesToRedefine.length === 0) return; + + const bulkOps = historiesToRedefine.map((history) => { + const userIdString = history.user ? String(history.user) : null; + + return { + updateOne: { + filter: { _id: history._id }, + update: { $set: { user: userIdString } }, + $unset: { migration_revert_flag: "" }, + }, + }; + }) + + const result = await db.collection("histories").bulkWrite(bulkOps); + console.log(`Reverted ${result.modifiedCount} fields back to strings.`); + + } catch (error) { + console.error("Migration DOWN failed:", error); + throw error; + } +} \ No newline at end of file diff --git a/migrations/20260124140000-migrate-history-user-strings-to-m2m-object.ts b/migrations/20260124140000-migrate-history-user-strings-to-m2m-object.ts new file mode 100644 index 000000000..227d455c6 --- /dev/null +++ b/migrations/20260124140000-migrate-history-user-strings-to-m2m-object.ts @@ -0,0 +1,62 @@ +import { Db } from "mongodb"; + +export async function up(db: Db) { + const historyCollection = db.collection("histories"); + + const historiesFound = historyCollection.find({ + user: { $type: "string", $regex: "-" }, + }); + + let count = 0; + + while (await historiesFound.hasNext()) { + const history = await historiesFound.next(); + if (!history) continue; + const oldUserValue = history.user as string; + + const newUser = { + isM2M: true, + clientId: oldUserValue, + subject: "chatbot-service", + scopes: ["read", "write"], + role: { main: "integration" }, + namespace: "main", + }; + + await historyCollection.updateOne( + { _id: history._id }, + { $set: { user: newUser } } + ); + + count++; + } + + console.log(`Migration complete. Updated ${count} documents.`); +} + +export async function down(db: Db) { + const historyCollection = db.collection("histories"); + + const historiesFound = historyCollection.find({ + "user.isM2M": true, + "user.subject": "chatbot-service", + "user.clientId": { $exists: true } + }); + + let count = 0; + + while (await historiesFound.hasNext()) { + const history = await historiesFound.next(); + if (!history) continue; + const clientId = history.user.clientId; + + await historyCollection.updateOne( + { _id: history._id }, + { $set: { user: clientId } } + ); + + count++; + } + + console.log(`Rollback complete. Reverted ${count} documents.`); +} diff --git a/migrations/20260225140000-flatten-nested-source-arrays.ts b/migrations/20260225140000-flatten-nested-source-arrays.ts new file mode 100644 index 000000000..a091a345a --- /dev/null +++ b/migrations/20260225140000-flatten-nested-source-arrays.ts @@ -0,0 +1,23 @@ +import { Db } from "mongodb"; + +export async function up(db: Db) { + const vrCollection = db.collection("verificationrequests"); + + const docsWithNestedSource = await vrCollection + .find({ "source.0": { $type: "array" } }) + .toArray(); + + for (const doc of docsWithNestedSource) { + const flattened = doc.source.flat(); + await vrCollection.updateOne( + { _id: doc._id }, + { + $set: { source: flattened }, + } + ); + } + + console.log( + `Flattened nested source arrays in ${docsWithNestedSource.length} verification requests` + ); +} diff --git a/migrations/20260226140000-flatten-nested-topics-arrays.ts b/migrations/20260226140000-flatten-nested-topics-arrays.ts new file mode 100644 index 000000000..7ac6e14af --- /dev/null +++ b/migrations/20260226140000-flatten-nested-topics-arrays.ts @@ -0,0 +1,23 @@ +import { Db } from "mongodb"; + +export async function up(db: Db) { + const vrCollection = db.collection("verificationrequests"); + + const docsWithNestedTopics = await vrCollection + .find({ "topics.0": { $type: "array" } }) + .toArray(); + + for (const doc of docsWithNestedTopics) { + const flattened = doc.topics.flat(); + await vrCollection.updateOne( + { _id: doc._id }, + { + $set: { topics: flattened }, + } + ); + } + + console.log( + `Flattened nested topics arrays in ${docsWithNestedTopics.length} verification requests` + ); +} diff --git a/package.json b/package.json index 995930741..822efb117 100644 --- a/package.json +++ b/package.json @@ -67,15 +67,16 @@ "@ant-design/icons": "^5.4.0", "@ant-design/icons-svg": "^4.1.0", "@babel/polyfill": "^7.12.1", - "@casl/ability": "^6.0.0", + "@casl/ability": "6.8.0", "@compodoc/compodoc": "1.1.21", "@cypress/react": "^9.0.0", "@dhaiwat10/react-link-preview": "^1.9.1", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", - "@langchain/community": "^0.0.54", - "@langchain/core": "^0.1.63", - "@langchain/openai": "^0.0.28", + "@langchain/community": "1.1.16", + "@langchain/core": "1.1.26", + "@langchain/langgraph": "^1.1.5", + "@langchain/openai": "1.2.8", "@mui/icons-material": "^5.10.9", "@mui/joy": "^5.0.0-beta.48", "@mui/lab": "^6.0.0-beta.30", @@ -84,7 +85,7 @@ "@mui/x-date-pickers": "5.0.20", "@nestjs/axios": "^3.0.0", "@nestjs/cli": "9.1.5", - "@nestjs/common": "^9.2.0", + "@nestjs/common": "10.4.22", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.2.0", "@nestjs/jwt": "^10.2.0", @@ -111,12 +112,12 @@ "@typescript-eslint/eslint-plugin": "^4.29.0", "@xstate/react": "^3.0.0", "accept-language-parser": "^1.5.0", - "ai": "^3.1.1", + "ai": "5.0.52", "aws-sdk": "^2.1154.0", "axios": "^1.12.0", "babel-jest": "^29.7.0", "babel-plugin-styled-components": "^1.13.2", - "body-parser": "^1.20.2", + "body-parser": "1.20.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "compromise": "^13.11.4", @@ -127,7 +128,7 @@ "dayjs": "^1.11.13", "domino": "^2.1.6", "dompurify": "^3.0.5", - "express": "^4.19.2", + "express": "4.19.2", "express-session": "^1.17.2", "handlebars": "^4.7.7", "helmet": "^6.0.0", @@ -136,7 +137,7 @@ "jotai-xstate": "^0.3.0", "js-cookie": "^3.0.1", "jsonwebtoken": "^9.0.2", - "langchain": "^0.1.36", + "langchain": "1.2.25", "lottie-web": "^5.10.1", "md5": "^2.3.0", "migrate-mongo-ts": "^1.1.4", @@ -150,7 +151,7 @@ "next": "^12.1.0", "next-i18next": "^10.2.0", "next-seo": "^5.4.0", - "nodemailer": "^6.9.9", + "nodemailer": "7.0.11", "react": "^18.3.1", "react-cookie-consent": "^6.4.1", "react-countdown": "^2.3.2", @@ -171,7 +172,7 @@ "sitemap": "5", "slugify": "^1.6.1", "socket.io": "^4.7.2", - "storybook": "^7.4.5", + "storybook": "7.6.21", "styled-components": "^5.3.0", "ts-node": "^10.2.0", "winston": "^3.13.0", @@ -187,6 +188,9 @@ "@babel/core": "^7.15.0", "@babel/preset-env": "^7.22.20", "@babel/preset-react": "^7.14.5", + "@eslint/compat": "^2.0.2", + "@eslint/eslintrc": "^3.3.3", + "@eslint/js": "^9.39.2", "@nestjs/schematics": "9.0.3", "@nestjs/testing": "^9.3.12", "@next/eslint-plugin-next": "^12.0.10", @@ -205,24 +209,23 @@ "@types/node": "^16.4.13", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", - "@typescript-eslint/parser": "^4.29.0", + "@typescript-eslint/parser": "^8.54.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.5", "concurrently": "^8.2.1", "cypress": "13.17.0", "dotenv": "^16.4.5", "env-cmd": "^10.1.0", - "eslint": "7.0.0", + "eslint": "^9.39.2", "eslint-config-next": "^12.0.10", "eslint-config-prettier": "8.3.0", - "eslint-config-react-app": "^6.0.0", - "eslint-plugin-flowtype": "^5.9.0", "eslint-plugin-import": "^2.18.0", "eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-prettier": "3.4.0", "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.0.8", "eslint-plugin-storybook": "^0.6.14", + "globals": "^17.3.0", "husky": "^8.0.1", "jest": "^29.7.0", "lint-staged": "^13.0.3", @@ -231,8 +234,8 @@ "react-i18next": "^11.16.7", "supertest": "^6.2.4", "ts-jest": "^29.1.1", - "typescript": "^4.3.5", - "wait-on": "^6.0.1" + "typescript": "^5.3.0", + "wait-on": "9.0.3" }, "browserslist": [ ">0.2%", @@ -280,7 +283,11 @@ "glob": "^9.0.0", "tldjs": "2.3.1", "@compodoc/compodoc": "1.1.21", - "cheerio": "1.0.0-rc.12" + "cheerio": "1.0.0-rc.12", + "@langchain/core/ansi-styles": "5.2.0" }, - "packageManager": "yarn@3.6.3" + "packageManager": "yarn@3.6.3", + "engines": { + "node": ">=20.18.0" + } } diff --git a/public/locales/en/badges.json b/public/locales/en/badges.json index 51bfb8f9c..92596e260 100644 --- a/public/locales/en/badges.json +++ b/public/locales/en/badges.json @@ -1,7 +1,9 @@ { - "nameColumn": "Name", - "descriptionColumn": "Description", - "imageColumn": "Image", + "nameLabel": "Name", + "namePlaceholder": "Enter badge name", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Enter description", + "imageFieldLabel": "Image", "title": "Badges area", "addBadge": "Add badge", "editBadge": "Edit badge", diff --git a/public/locales/en/claim.json b/public/locales/en/claim.json index 5518ab17f..3b1cdb70b 100644 --- a/public/locales/en/claim.json +++ b/public/locales/en/claim.json @@ -38,5 +38,7 @@ "hideSuccess": "Claim hidden succesfully", "hideError": "Error while hidden claim", "unhideSuccess": "Claim unhide succesfully", - "unhideError": "Error while unhide claim" + "unhideError": "Error while unhide claim", + "reviewImageButton": "Review Image", + "openReportButton": "View Report" } diff --git a/public/locales/en/claimForm.json b/public/locales/en/claimForm.json index 1479170dd..e2c2150b2 100644 --- a/public/locales/en/claimForm.json +++ b/public/locales/en/claimForm.json @@ -45,5 +45,6 @@ "fileInputButton": "Choose file", "fileTypeError": "You can only upload {{types}} files", "fileSizeError": "File must smaller than {{size}}", - "undefined": "Undefined" + "undefined": "Undefined", + "noAnswer": "N/A" } diff --git a/public/locales/en/common.json b/public/locales/en/common.json index ddaa71654..d94f13d6e 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -12,6 +12,7 @@ "supportEmail": "support@aletheifact.org", "contactEmail": "contact@aletheiafact.org", "captchaError": "There was an error validating the captcha", + "captchaLabel": "Verification", "change": "Change", "approve": "Approve", "reject": "Reject" diff --git a/public/locales/en/cop30.json b/public/locales/en/cop30.json new file mode 100644 index 000000000..83d381ba7 --- /dev/null +++ b/public/locales/en/cop30.json @@ -0,0 +1,26 @@ +{ + "bannerLocation": "Belém, Brazil • November 2025", + "bannerTitle": "Fact-Checking on COP30", + "bannerDescription": "We verify statements from authorities, politicians and experts about the United Nations Climate Change Conference (COP30). Follow fact-checks in real-time and contribute to an informed public debate on climate and environment.", + "statisticsTotalChecks": "COP30 Checks", + "statisticsReliable": "Reliable", + "statisticsMisleading": "Misleading", + "statisticsUnderReview": "Under Review", + "sectionLatestChecks": "Latest Checks", + "claimDeclaredOn": "Declared on", + "claimInOfficialSpeech": "in an Official Speech", + "claimStatusReliable": "RELIABLE, BUT...", + "claimStatusMisleading": "MISLEADING", + "claimSeeFullText": "see full text →", + "claimViewProfile": "View profile", + "claimOpen": "Open", + "all": "All", + "deforestation": "Deforestation", + "climateFinancing": "Climate Financing", + "emissions": "Emissions", + "blueAmazon": "Blue Amazon", + "energy": "Energy", + "environment": "Environment", + "infrastructure": "Infrastructure", + "cop30Conference": "COP30" +} diff --git a/public/locales/en/history.json b/public/locales/en/history.json index 385935d09..f6ccb3ddd 100644 --- a/public/locales/en/history.json +++ b/public/locales/en/history.json @@ -1,5 +1,7 @@ { "anonymousUserName": "Anonymous", + "automatedMonitoring": "Automated Monitoring", + "virtualAssistant": "Virtual Assistant", "create": "created", "update": "updated", "delete": "deleted", @@ -8,4 +10,4 @@ "personality": "profile", "claim": "claim", "historyItem": "{{username}} {{type}} {{title}} {{targetModel}}" -} +} \ No newline at end of file diff --git a/public/locales/en/landingPage.json b/public/locales/en/landingPage.json index 368f0264d..e743b10d6 100644 --- a/public/locales/en/landingPage.json +++ b/public/locales/en/landingPage.json @@ -1,5 +1,5 @@ { "socialNetworksCTA": "Follow us on our social media profiles", - "description": "AletheiaFact.org is a crowd-sourced fact-checking movement and platform that imagines a society where everyone can have free access and engage with truthful and credible information with autonomy.", + "description": "AletheiaFact.org is a movement and a collective fact-checking platform that strengthens free access to true and reliable information, promoting autonomy and citizen participation.", "learnMoreButton": "Learn More" } diff --git a/public/locales/en/login.json b/public/locales/en/login.json index 6ebecabb2..7c94e85a7 100644 --- a/public/locales/en/login.json +++ b/public/locales/en/login.json @@ -7,6 +7,7 @@ "nameLabel": "Name", "submitButton": "Submit", "emailErrorMessage": "Please, insert your e-mail", + "invalidEmailErrorMessage": "Invalid e-mail", "nameErrorMessage": "Please, insert your name", "passwordErrorMessage": "Please, insert your password", "loginFailedMessage": "Error while logging in", diff --git a/public/locales/en/namespaces.json b/public/locales/en/namespaces.json index fe537fb27..3a3ac9cc2 100644 --- a/public/locales/en/namespaces.json +++ b/public/locales/en/namespaces.json @@ -1,5 +1,6 @@ { - "nameColumn": "Name", + "nameLabel": "Name", + "namePlaceholder":"Namespace name", "title": "Namespaces area", "addNameSpace": "Add namespace", "editNameSpace": "Edit namespace", diff --git a/public/locales/en/timeAgo.json b/public/locales/en/timeAgo.json new file mode 100644 index 000000000..121bf1797 --- /dev/null +++ b/public/locales/en/timeAgo.json @@ -0,0 +1,8 @@ +{ + "minutesAgo_one": "{{count}} minute ago", + "minutesAgo_other": "{{count}} minutes ago", + "hoursAgo_one": "{{count}} hour ago", + "hoursAgo_other": "{{count}} hours ago", + "daysAgo_one": "{{count}} day ago", + "daysAgo_other": "{{count}} days ago" +} \ No newline at end of file diff --git a/public/locales/en/tracking.json b/public/locales/en/tracking.json new file mode 100644 index 000000000..fb4c78cd5 --- /dev/null +++ b/public/locales/en/tracking.json @@ -0,0 +1,10 @@ +{ + "verificationProgress": "Verification Request progress", + "description_PRE_TRIAGE": "Verification request received and entered into the automated triage system. Initial analysis and severity assessment in progress.", + "description_IN_TRIAGE": "Under review by the Aletheia team.", + "description_POSTED": "Verification request complete and reviewed. Results posted.", + "description_DECLINED": "Verification request declined following detailed analysis. The veracity of the information provided could not be confirmed.", + "errorInvalidId": "Invalid verification request ID format.", + "errorFetchData": "Failed to load tracking information. Please try again later.", + "noDateFound": "No date history found" +} diff --git a/public/locales/en/verificationRequest.json b/public/locales/en/verificationRequest.json index 45f7147ec..5b8625c6b 100644 --- a/public/locales/en/verificationRequest.json +++ b/public/locales/en/verificationRequest.json @@ -43,7 +43,7 @@ "impactAreaPlaceholder": "Select the impact area...", "publicationDatePlaceholder": "Select the date you saw this...", "heardFromPlaceholder": "Specify where you saw or heard this...", - "sourcePlaceholder": "Provide the source of the information...", + "sourcePlaceholder": "Provide the source of the information...", "emailPlaceholder": "Enter your email address here...", "verificationRequestCreateSuccess": "Verification Request created successfully", "verificationRequestCreateError": "Error while creating the Verification Request", @@ -63,30 +63,61 @@ "filterByContentLabel": "Filter by Content", "applyButtonLabel": "Apply", "showText": "Show", - "statusPreTriage": "Pre Triage", - "statusInTriage": "In Triage", - "statusPosted": "Posted", - "statusDeclined": "Declined", + "startDate": "Start Date", + "endDate": "End Date", "allPriorities": "All Priorities", "allSourceChannels": "All Source Channels", - "priorityCritical": "Critical", - "priorityHigh": "High", - "priorityMedium": "Medium", - "priorityLow": "Low", + "priority": { + "critical": "Critical", + "high": "High ({{level}})", + "medium": "Medium ({{level}})", + "low": "Low ({{level}})", + "filter_high": "High", + "filter_medium": "Medium", + "filter_low": "Low" + }, "filterByPriority": "Filter by Priority", "noRequestsInStatus": "No requests in this status", - "statusFilterOption": "Status", - "statusFilterLabel": "Status:", "impactAreaFilterLabel": "Impact Area:", - "reportedContent": "Reported Content", - "contentTypeLabel": "Content Type", - "receptionChannelLabel": "Reception Channel", - "severityLabel": "Severity", "editVerificationRequestSuccess": "Verification Request edited successfully", "editVerificationRequestError": "Error while editing the Verification Request", "titleEditVerificationRequest": "Verification Request Edition", "viewFullPage": "View full page", "automated_monitoring": "Automated Monitoring", "filterBySourceChannel": "Filter by Source Channel", - "errorUpdatingStatus": "Error updating status:" + "errorUpdatingStatus": "Error updating status:", + "PRE_TRIAGE": "Pre Triage", + "IN_TRIAGE": "In Triage", + "POSTED": "Posted", + "DECLINED": "Declined", + "activity": { + "Posted": "Verification request #{{hash}} verified and forwarded", + "In Triage": "Verification request #{{hash}} in the verification process", + "Pre Triage": "New verification request received by {{source}}", + "Declined": "Verification request #{{hash}} checked and archived" + }, + "dashboard": { + "title": "Reports Dashboard", + "subtitle": "Transparency and public monitoring", + "totalReports": "Total Reports", + "verified": "Verified", + "inAnalysis": "In Analysis", + "pending": "Pending", + "receivedThisMonth": "Received this month", + "ofTotal": "of total", + "sourcesTitle": "Report Sources", + "sourcesSubtitle": "Distribution by reception channel", + "statusTitle": "Report Status", + "statusSubtitle": "Distribution by verification status", + "activityTitle": "Recent Activity", + "activitySubtitle": "Latest updates from the system", + "errorLoading": "Error fetching statistics" + }, + "identifiedPersonalities": "People Mentioned", + "loadingPersonalities": "Loading personalities...", + "personalitiesList": "List of identified personalities", + "noPersonalitiesFound": "No personalities identified in this request", + "errorLoadingPersonalities": "Error loading personalities. Please try again.", + "retryLoadingPersonalities": "Retry", + "viewPersonalityPage": "View personality page for {{name}}" } diff --git a/public/locales/pt/badges.json b/public/locales/pt/badges.json index 44a5f1bdb..857c1acb8 100644 --- a/public/locales/pt/badges.json +++ b/public/locales/pt/badges.json @@ -1,7 +1,9 @@ { - "nameColumn": "Nome", - "descriptionColumn": "Descrição", - "imageColumn": "Imagem", + "nameLabel": "Nome", + "namePlaceholder": "Insira o nome da insígnia", + "descriptionLabel": "Descrição", + "descriptionPlaceholder": "Insira a descrição", + "imageFieldLabel": "Imagem", "title": "Área de Insígnias", "addBadge": "Adicionar Insígnia", "editBadge": "Editar Insígnia", diff --git a/public/locales/pt/claim.json b/public/locales/pt/claim.json index ac988f4cf..63659565e 100644 --- a/public/locales/pt/claim.json +++ b/public/locales/pt/claim.json @@ -38,5 +38,7 @@ "hideSuccess": "Afirmação escondida com sucesso", "hideError": "Erro ao esconder a afirmação", "unhideSuccess": "Afirmação pública com sucesso", - "unhideError": "Erro ao mostrar a afirmação" + "unhideError": "Erro ao mostrar a afirmação", + "reviewImageButton": "Revisar Imagem", + "openReportButton": "Ver Relatório" } diff --git a/public/locales/pt/claimForm.json b/public/locales/pt/claimForm.json index a303094ea..c230efeb2 100644 --- a/public/locales/pt/claimForm.json +++ b/public/locales/pt/claimForm.json @@ -24,7 +24,7 @@ "checkboxAcceptTerms": "Eu atesto pessoalmente que toda informação adicionada é confiável.", "cancelButton": "Cancelar", "updateButton": "Atualizar", - "saveButton": "Adicionar", + "saveButton": "Salvar", "contentModelTitle": "Formato", "selectContentModel": "Que formato de afirmação você quer incluir?", "Image": "Imagem", @@ -45,5 +45,6 @@ "fileInputButton": "Escolha o arquivo", "fileTypeError": "Voce só pode enviar arquivos nos formatos {{types}}", "fileSizeError": "O arquivo deve ter menos de {{size}}", - "undefined": "Indefinido" + "undefined": "Indefinido", + "noAnswer": "N/A" } diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json index b610af36e..cd67f5176 100644 --- a/public/locales/pt/common.json +++ b/public/locales/pt/common.json @@ -12,6 +12,7 @@ "supportEmail": "support@aletheifact.org", "contactEmail": "contato@aletheiafact.org", "captchaError": "Erro na validação do captcha", + "captchaLabel": "Verificação", "change": "Mudar", "approve": "Aprovar", "reject": "Rejeitar" diff --git a/public/locales/pt/cop30.json b/public/locales/pt/cop30.json new file mode 100644 index 000000000..8d4789d3a --- /dev/null +++ b/public/locales/pt/cop30.json @@ -0,0 +1,26 @@ +{ + "bannerLocation": "Belém, Brasil • Novembro 2025", + "bannerTitle": "Checagem de Fatos sobre a COP30", + "bannerDescription": "Verificamos afirmações de autoridades, políticos e especialistas sobre a Conferência das Nações Unidas sobre Mudanças Climáticas (COP30). Acompanhe as checagens em tempo real e contribua para um debate público informado sobre clima e meio ambiente.", + "statisticsTotalChecks": "Checagens COP30", + "statisticsReliable": "Confiáveis", + "statisticsMisleading": "Enganosas", + "statisticsUnderReview": "Em Análise", + "sectionLatestChecks": "Últimas Checagens", + "claimDeclaredOn": "Declarou em", + "claimInOfficialSpeech": "em um Discurso oficial", + "claimStatusReliable": "CONFIÁVEL, MAS...", + "claimStatusMisleading": "ENGANOSO", + "claimSeeFullText": "veja o texto completo →", + "claimViewProfile": "Veja o perfil", + "claimOpen": "Abrir", + "all": "Todos", + "deforestation": "Desmatamento", + "climateFinancing": "Financiamento Climático", + "emissions": "Emissões", + "blueAmazon": "Amazônia Azul", + "energy": "Energia", + "environment": "Meio-ambiente", + "infrastructure": "Infraestrutura", + "cop30Conference": "COP30" +} diff --git a/public/locales/pt/history.json b/public/locales/pt/history.json index 8468c1813..ec3529c2e 100644 --- a/public/locales/pt/history.json +++ b/public/locales/pt/history.json @@ -1,5 +1,7 @@ { "anonymousUserName": "Anônimo", + "automatedMonitoring": "Monitoramento Automatizado", + "virtualAssistant": "Assistente Virtual", "create": "criou", "update": "atualizou", "delete": "deletou", @@ -8,4 +10,4 @@ "personality": "o perfil de", "claim": "a afirmação", "historyItem": "{{username}} {{type}} {{targetModel}} {{title}}" -} +} \ No newline at end of file diff --git a/public/locales/pt/landingPage.json b/public/locales/pt/landingPage.json index ad614aa21..cd986bac2 100644 --- a/public/locales/pt/landingPage.json +++ b/public/locales/pt/landingPage.json @@ -1,5 +1,5 @@ { "socialNetworksCTA": "Acompanhe os nossos conteúdos nas redes sociais", - "description": "AletheiaFact.org é um movimento e plataforma coletivo de verificação de fatos que imagina uma sociedade onde todos podem ter acesso livre e se envolver com informações verdadeiras e confiáveis com autonomia.", + "description": "AletheiaFact.org é um movimento e uma plataforma coletiva de verificação de fatos que fortalece o acesso livre a informações verdadeiras e confiáveis, promovendo autonomia e participação cidadã.", "learnMoreButton": "Saiba mais" } diff --git a/public/locales/pt/namespaces.json b/public/locales/pt/namespaces.json index cde39126f..4b5069288 100644 --- a/public/locales/pt/namespaces.json +++ b/public/locales/pt/namespaces.json @@ -1,5 +1,6 @@ { - "nameColumn": "Nome", + "nameLabel": "Nome", + "namePlaceholder":"Nome do namespace", "title": "Área de namespaces", "addNameSpace": "Adicionar namespace", "editNameSpace": "Editar namespace", diff --git a/public/locales/pt/timeAgo.json b/public/locales/pt/timeAgo.json new file mode 100644 index 000000000..b26363e44 --- /dev/null +++ b/public/locales/pt/timeAgo.json @@ -0,0 +1,8 @@ +{ + "minutesAgo_one": "Há {{count}} minuto", + "minutesAgo_other": "Há {{count}} minutos", + "hoursAgo_one": "Há {{count}} hora", + "hoursAgo_other": "Há {{count}} horas", + "daysAgo_one": "Há {{count}} dia", + "daysAgo_other": "Há {{count}} dias" +} \ No newline at end of file diff --git a/public/locales/pt/tracking.json b/public/locales/pt/tracking.json new file mode 100644 index 000000000..d095b88ac --- /dev/null +++ b/public/locales/pt/tracking.json @@ -0,0 +1,10 @@ +{ + "verificationProgress": "Progresso da Denúncia", + "description_PRE_TRIAGE": "Denúncia recebida e inserida no sistema de triagem automatizada. Análise inicial e definição de severidade em progresso.", + "description_IN_TRIAGE": "Sob revisão da equipe da Aletheia.", + "description_POSTED": "Denúncia completa e revisada. Resultados publicados.", + "description_DECLINED": "Denúncia negada após análise detalhada. Não foi possível confirmar a veracidade dos dados fornecidos.", + "errorInvalidId": "O identificador da denúncia é inválido.", + "errorFetchData": "Não foi possível carregar as informações de rastreamento. Tente novamente.", + "noDateFound": "Nenhum histórico de data encontrado" +} diff --git a/public/locales/pt/verificationRequest.json b/public/locales/pt/verificationRequest.json index b2e8cea0e..28bf99251 100644 --- a/public/locales/pt/verificationRequest.json +++ b/public/locales/pt/verificationRequest.json @@ -43,7 +43,7 @@ "impactAreaPlaceholder": "Selecione a área de impacto...", "publicationDatePlaceholder": "Selecione a data em que você viu isso...", "heardFromPlaceholder": "Especifique onde você viu ou ouviu isso...", - "sourcePlaceholder": "Forneça a fonte da informação...", + "sourcePlaceholder": "Forneça a fonte da informação...", "emailPlaceholder": "Insira seu endereço de email aqui...", "verificationRequestCreateSuccess": "Denúncia criada com sucesso", "verificationRequestCreateError": "Erro ao criar a denúncia", @@ -63,30 +63,61 @@ "filterByContentLabel": "Filtrar por Conteudo", "applyButtonLabel": "Aplicar", "showText": "Mostrar", - "statusPreTriage": "Pré-Triagem", - "statusInTriage": "Em Triagem", - "statusPosted": "Publicado", - "statusDeclined": "Recusado", + "startDate": "Data inicial", + "endDate": "Data final", "allPriorities": "Todas as Prioridades", "allSourceChannels": "Todos os Canais de Origem", - "priorityCritical": "Crítica", - "priorityHigh": "Alta", - "priorityMedium": "Média", - "priorityLow": "Baixa", + "priority": { + "critical": "Crítico", + "high": "Alta ({{level}})", + "medium": "Média ({{level}})", + "low": "Baixa ({{level}})", + "filter_high": "Alta", + "filter_medium": "Média", + "filter_low": "Baixa" + }, "filterByPriority": "Filtrar por Prioridade", "noRequestsInStatus": "Nenhuma solicitação neste status", - "statusFilterOption": "Status", - "statusFilterLabel": "Status:", "impactAreaFilterLabel": "Área de Impacto:", - "reportedContent": "Conteúdo Reportado", - "contentTypeLabel": "Tipo de Conteúdo", - "receptionChannelLabel": "Canal de Recepção", - "severityLabel": "Prioridade", "editVerificationRequestSuccess": "Denúncia editada com sucesso", "editVerificationRequestError": "Erro ao editar a denúncia", "titleEditVerificationRequest": "Edição de Denúncia", "viewFullPage": "Ver página completa", "automated_monitoring": "Monitoramento Automatizado", "filterBySourceChannel": "Filtrar por Canal de Origem", - "errorUpdatingStatus": "Erro ao atualizar status:" + "PRE_TRIAGE": "Pré Triagem", + "IN_TRIAGE": "Em Triagem", + "POSTED": "Postado", + "DECLINED": "Recusado", + "activity": { + "Posted": "Denúncia #{{hash}} verificada e encaminhada", + "In Triage": "Denúncia #{{hash}} em processo de verificação", + "Pre Triage": "Nova denúncia recebida via {{source}}", + "Declined": "Denúncia #{{hash}} verificada e arquivada" + }, + "dashboard": { + "title": "Painel de Denúncias", + "subtitle": "Transparência e acompanhamento público", + "totalReports": "Total de Denúncias", + "verified": "Verificadas", + "inAnalysis": "Em Análise", + "pending": "Pendentes", + "receivedThisMonth": "Recebidas este mês", + "ofTotal": "do total", + "sourcesTitle": "Fontes de Denúncias", + "sourcesSubtitle": "Distribuição por canal de recebimento", + "statusTitle": "Status das Denúncias", + "statusSubtitle": "Distribuição por estado de verificação", + "activityTitle": "Atividade Recente", + "activitySubtitle": "Últimas atualizações do sistema", + "errorLoading": "Erro ao buscar as estatísticas" + }, + "errorUpdatingStatus": "Erro ao atualizar status:", + "identifiedPersonalities": "Pessoas Mencionadas", + "loadingPersonalities": "Carregando personalidades...", + "personalitiesList": "Lista de personalidades identificadas", + "noPersonalitiesFound": "Nenhuma personalidade identificada nesta denúncia", + "errorLoadingPersonalities": "Erro ao carregar personalidades. Por favor, tente novamente.", + "retryLoadingPersonalities": "Tentar novamente", + "viewPersonalityPage": "Ver página da personalidade {{name}}" } diff --git a/server/ai-task/ai-task.controller.ts b/server/ai-task/ai-task.controller.ts index b8bc79291..b7d5a4db8 100644 --- a/server/ai-task/ai-task.controller.ts +++ b/server/ai-task/ai-task.controller.ts @@ -14,29 +14,28 @@ import type { UpdateAiTaskDto } from "./dto/update-ai-task.dto"; import { AiTask } from "./schemas/ai-task.schema"; import { AiTaskState } from "./constants/ai-task.constants"; import { ObjectIdValidationPipe } from "./pipes/objectid-validation.pipe"; -import { M2MOrAbilities } from "../auth/decorators/m2m-or-abilities.decorator"; -import { ADMIN_USER_ABILITY } from "../auth/ability/abilities.constants"; +import { AdminOnly } from "../auth/decorators/auth.decorator"; @ApiTags("ai-tasks") @Controller("api/ai-tasks") export class AiTaskController { constructor(private readonly aiTaskService: AiTaskService) {} - @M2MOrAbilities(ADMIN_USER_ABILITY) + @AdminOnly({ allowM2M: true }) @ApiOperation({ summary: "Enqueue a new AI task" }) @Post() create(@Body() createDto: CreateAiTaskDto) { return this.aiTaskService.create(createDto); } - @M2MOrAbilities(ADMIN_USER_ABILITY) + @AdminOnly({ allowM2M: true }) @ApiOperation({ summary: "Get all pending AI tasks" }) @Get("pending") getPending() { return this.aiTaskService.findAll(AiTaskState.PENDING); } - @M2MOrAbilities(ADMIN_USER_ABILITY) + @AdminOnly({ allowM2M: true }) @ApiOperation({ summary: "Get AI tasks by state" }) @Get() findAll(@Query("state") state?: string) { @@ -44,7 +43,7 @@ export class AiTaskController { return this.aiTaskService.findAll(typedState); } - @M2MOrAbilities(ADMIN_USER_ABILITY) + @AdminOnly({ allowM2M: true }) @ApiOperation({ summary: "Update AI task state and optionally dispatch result", }) diff --git a/server/app.module.ts b/server/app.module.ts index 74b5479a4..8650510c8 100644 --- a/server/app.module.ts +++ b/server/app.module.ts @@ -64,6 +64,7 @@ import { SessionOrM2MGuard } from "./auth/m2m-or-session.guard"; import { M2MGuard } from "./auth/m2m.guard"; import { CallbackDispatcherModule } from "./callback-dispatcher/callback-dispatcher.module"; import { AiTaskModule } from "./ai-task/ai-task.module"; +import { TrackingModule } from "./tracking/tracking.module"; @Module({}) export class AppModule implements NestModule { @@ -120,6 +121,7 @@ export class AppModule implements NestModule { ReviewTaskModule, ClaimRevisionModule, HistoryModule, + TrackingModule, StateEventModule, SourceModule, SpeechModule, diff --git a/server/auth/name-space/name-space.controller.ts b/server/auth/name-space/name-space.controller.ts index e51c021d6..697062929 100644 --- a/server/auth/name-space/name-space.controller.ts +++ b/server/auth/name-space/name-space.controller.ts @@ -8,7 +8,6 @@ import { Query, Req, Res, - UseGuards, } from "@nestjs/common"; import { NameSpaceService } from "./name-space.service"; import type { Request, Response } from "express"; @@ -18,15 +17,12 @@ import { parse } from "url"; import { ViewService } from "../../view/view.service"; import { CreateNameSpaceDTO } from "./dto/create-namespace.dto"; import { UpdateNameSpaceDTO } from "./dto/update-name-space.dto"; -import { - AdminUserAbility, - CheckAbilities, -} from "../../auth/ability/ability.decorator"; -import { AbilitiesGuard } from "../../auth/ability/abilities.guard"; +import { AdminOnly } from "../decorators/auth.decorator"; import { Types } from "mongoose"; import { Roles } from "../../auth/ability/ability.factory"; import { NotificationService } from "../../notifications/notifications.service"; import slugify from "slugify"; +import { ConfigService } from "@nestjs/config"; @Controller() export class NameSpaceController { @@ -34,13 +30,13 @@ export class NameSpaceController { private nameSpaceService: NameSpaceService, private usersService: UsersService, private viewService: ViewService, - private notificationService: NotificationService + private notificationService: NotificationService, + private configService: ConfigService, ) {} + @AdminOnly() @ApiTags("name-space") @Post("api/name-space") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async create(@Body() namespace: CreateNameSpaceDTO) { namespace.slug = slugify(namespace.name, { lower: true, @@ -55,10 +51,9 @@ export class NameSpaceController { return await this.nameSpaceService.create(namespace); } + @AdminOnly() @ApiTags("name-space") @Put("api/name-space/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async update(@Param("id") id, @Body() namespaceBody: UpdateNameSpaceDTO) { const nameSpace = await this.nameSpaceService.getById(id); const newNameSpace = { @@ -66,13 +61,14 @@ export class NameSpaceController { ...namespaceBody, }; - newNameSpace.slug = slugify(nameSpace.name, { + newNameSpace.slug = slugify(newNameSpace.name, { lower: true, strict: true, }); newNameSpace.users = await this.updateNameSpaceUsers( newNameSpace.users, + newNameSpace.slug, nameSpace.slug ); @@ -86,9 +82,8 @@ export class NameSpaceController { return await this.nameSpaceService.update(id, newNameSpace); } + @AdminOnly() @ApiTags("name-space") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) @Get("api/name-space") async findAllOrFiltered( @Query("userId") userId?: string, @@ -100,6 +95,7 @@ export class NameSpaceController { return this.nameSpaceService.listAll(); } + @AdminOnly() @ApiTags("name-space") @Get("admin/name-spaces") public async adminNameSpaces(@Req() req: Request, @Res() res: Response) { @@ -107,25 +103,35 @@ export class NameSpaceController { const users = await this.usersService.findAll({}); const parsedUrl = parse(req.url, true); - const query = Object.assign(parsedUrl.query, { nameSpaces, users }); + const query = Object.assign( + parsedUrl.query, + { + sitekey: this.configService.get("recaptcha_sitekey"), + nameSpaces, + users + } + ); await this.viewService.render(req, res, "/admin-namespaces", query); } - private async updateNameSpaceUsers(users, key) { + private async updateNameSpaceUsers(users, newKey, oldKey = null) { const promises = users.map(async (user) => { const userId = Types.ObjectId(user._id); const existingUser = await this.usersService.getById(userId); - if (!existingUser.role[key]) { - await this.usersService.updateUser(existingUser._id, { - role: { - ...existingUser.role, - [key]: Roles.Regular, - }, - }); + let updatedRoles = { ...existingUser.role }; + + if (oldKey && oldKey !== newKey) { + delete updatedRoles[oldKey]; } + updatedRoles[newKey] = Roles.Regular; + + await this.usersService.updateUser(existingUser._id, { + role: updatedRoles, + }); + return userId; }); diff --git a/server/auth/name-space/schemas/name-space.schema.ts b/server/auth/name-space/schemas/name-space.schema.ts index e604688b6..803564217 100644 --- a/server/auth/name-space/schemas/name-space.schema.ts +++ b/server/auth/name-space/schemas/name-space.schema.ts @@ -7,7 +7,7 @@ export type NameSpaceDocument = NameSpace & mongoose.Document; export enum NameSpaceEnum { Main = "main", } -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class NameSpace { @Prop({ required: true }) name: string; diff --git a/server/auth/ory/ory.controller.ts b/server/auth/ory/ory.controller.ts index f7bc66f4a..dd2546315 100644 --- a/server/auth/ory/ory.controller.ts +++ b/server/auth/ory/ory.controller.ts @@ -3,7 +3,7 @@ import type { Request, Response } from "express"; import type { NextApiRequest, NextApiResponse } from "next"; import { parse } from "url"; import { ViewService } from "../../view/view.service"; -import { IsPublic } from "../decorators/is-public.decorator"; +import { Public } from "../decorators/auth.decorator"; import OryService from "./ory.service"; @Controller("api/.ory") @@ -12,7 +12,7 @@ export default class OryController { private readonly viewService: ViewService, private readonly oryService: OryService ) {} - @IsPublic() + @Public() @Get("sessions/whoami") public async whoAmI(@Req() req: Request, @Res() res: Response) { try { @@ -27,7 +27,7 @@ export default class OryController { } } - @IsPublic() + @Public() @Get("*") public async getOryPaths( @Req() req: NextApiRequest, @@ -36,7 +36,7 @@ export default class OryController { await this.oryPaths(req, res); } - @IsPublic() + @Public() @Post("*") public async postOryPaths( @Req() req: NextApiRequest, diff --git a/server/badge/badge.controller.ts b/server/badge/badge.controller.ts index 11e422356..8490f40d0 100644 --- a/server/badge/badge.controller.ts +++ b/server/badge/badge.controller.ts @@ -6,7 +6,6 @@ import { Put, Req, Res, - UseGuards, } from "@nestjs/common"; import type { Request, Response } from "express"; import { ImageService } from "../claim/types/image/image.service"; @@ -20,11 +19,8 @@ import { UsersService } from "../users/users.service"; import { Types } from "mongoose"; import { ApiTags } from "@nestjs/swagger"; import { UtilService } from "../util"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; -import { - AdminUserAbility, - CheckAbilities, -} from "../auth/ability/ability.decorator"; +import { AdminOnly } from "../auth/decorators/auth.decorator"; +import { ConfigService } from "@nestjs/config"; @Controller(":namespace?") export class BadgeController { @@ -33,9 +29,11 @@ export class BadgeController { private viewService: ViewService, private imageService: ImageService, private usersService: UsersService, - private util: UtilService + private util: UtilService, + private configService: ConfigService, ) {} + @AdminOnly() @ApiTags("badge") @Post("api/badge") public async createBadge(@Body() badge: CreateBadgeDTO, @Req() request) { @@ -64,6 +62,7 @@ export class BadgeController { return createdBadge; } + @AdminOnly() @ApiTags("badge") @Put("api/badge/:id") public async updateBadge(@Body() badge: UpdateBadgeDTO, @Req() request) { @@ -136,10 +135,9 @@ export class BadgeController { return this.badgeService.listAll(); } + @AdminOnly() @ApiTags("admin") @Get("admin/badges") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) public async adminBadges(@Req() req: Request, @Res() res: Response) { const badges = await this.badgeService.listAll(); const users = await this.usersService.findAll({ @@ -156,6 +154,7 @@ export class BadgeController { badges, users, nameSpace: req.params.namespace, + sitekey: this.configService.get("recaptcha_sitekey"), }); await this.viewService.render(req, res, "/admin-badges", query); diff --git a/server/badge/schemas/badge.schema.ts b/server/badge/schemas/badge.schema.ts index b04d25d65..8babf3a19 100644 --- a/server/badge/schemas/badge.schema.ts +++ b/server/badge/schemas/badge.schema.ts @@ -4,7 +4,7 @@ import * as mongoose from "mongoose"; export type BadgeDocument = Badge & mongoose.Document; -@Schema() +@Schema({ timestamps: true }) export class Badge { @Prop({ required: true }) name: string; @@ -14,9 +14,6 @@ export class Badge { @Prop({ type: mongoose.Types.ObjectId, ref: "Image", required: true }) image: Image; - - @Prop({ required: true }) - created_at: string; } const BadgeSchemaRaw = SchemaFactory.createForClass(Badge); diff --git a/server/chat-bot-state/chat-bot-state.schema.ts b/server/chat-bot-state/chat-bot-state.schema.ts index b612d03b1..3b4bccdec 100644 --- a/server/chat-bot-state/chat-bot-state.schema.ts +++ b/server/chat-bot-state/chat-bot-state.schema.ts @@ -14,7 +14,7 @@ export interface ChatBotMachineSnapshot { }; } -@Schema() +@Schema({ timestamps: true }) export class ChatBotState { @Prop({ required: true }) _id: string; diff --git a/server/chat-bot/chat-bot.machine.ts b/server/chat-bot/chat-bot.machine.ts index 087cbc039..d6f3d3900 100644 --- a/server/chat-bot/chat-bot.machine.ts +++ b/server/chat-bot/chat-bot.machine.ts @@ -14,7 +14,7 @@ export const createChatBotMachine = ( verificationRequestStateMachineService: VerificationRequestStateMachineService, value?, context?, - chatbotStateId? + M2MUser?, ) => { const chatBotMachine = createMachine( { @@ -199,7 +199,7 @@ export const createChatBotMachine = ( verificationRequestStateMachineService.request( verificationRequestBody, - chatbotStateId + M2MUser ); }, }, diff --git a/server/chat-bot/chat-bot.service.ts b/server/chat-bot/chat-bot.service.ts index 0da3ea803..d7d8d3235 100644 --- a/server/chat-bot/chat-bot.service.ts +++ b/server/chat-bot/chat-bot.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Scope } from "@nestjs/common"; +import { Injectable, Scope, Logger } from "@nestjs/common"; import { HttpService } from "@nestjs/axios"; import { AxiosResponse } from "axios"; import { catchError, map } from "rxjs/operators"; @@ -7,6 +7,8 @@ import { createChatBotMachine } from "./chat-bot.machine"; import { ConfigService } from "@nestjs/config"; import { ChatBotStateService } from "../chat-bot-state/chat-bot-state.service"; import { VerificationRequestStateMachineService } from "../verification-request/state-machine/verification-request.state-machine.service"; +import { Roles } from "../auth/ability/ability.factory"; +import { M2M } from "../entities/m2m.entity"; const diacriticsRegex = /[\u0300-\u036f]/g; const MESSAGE_MAP = { @@ -22,8 +24,23 @@ interface ChatBotContext { sourceChannel?: string; } +function M2MUser(clientId): M2M { + return { + isM2M: true, + clientId, + subject: "chatbot-service", + scopes: ["read", "write"], + role: { + main: Roles.Integration, + }, + namespace: "main", + }; +} + @Injectable({ scope: Scope.REQUEST }) export class ChatbotService { + private readonly logger = new Logger(ChatbotService.name); + constructor( private configService: ConfigService, private readonly httpService: HttpService, @@ -99,7 +116,7 @@ export class ChatbotService { ...chatbotState.machine.context, sourceChannel: channel, }, - chatbotState._id + M2MUser(chatbotState._id) ); chatBotMachineService.start(chatbotState.machine.value); @@ -206,7 +223,7 @@ export class ChatbotService { ); break; default: - console.warn(`Unhandled state: ${currentState}`); + this.logger.warn(`Unhandled state: ${currentState}`); } } diff --git a/server/claim-review/claim-review.controller.ts b/server/claim-review/claim-review.controller.ts index 3bd5a836d..9dab0a077 100644 --- a/server/claim-review/claim-review.controller.ts +++ b/server/claim-review/claim-review.controller.ts @@ -4,19 +4,13 @@ import { Param, Put, Get, - UseGuards, Header, Delete, Query, } from "@nestjs/common"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public, AdminOnly } from "../auth/decorators/auth.decorator"; import { CaptchaService } from "../captcha/captcha.service"; import { ClaimReviewService } from "./claim-review.service"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; -import { - AdminUserAbility, - CheckAbilities, -} from "../auth/ability/ability.decorator"; import { ApiTags } from "@nestjs/swagger"; import { HistoryService } from "../history/history.service"; import { TargetModel } from "../history/schema/history.schema"; @@ -31,7 +25,7 @@ export class ClaimReviewController { private historyService: HistoryService ) {} - @IsPublic() + @Public() @ApiTags("claim-review") @Get("api/review") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -67,10 +61,9 @@ export class ClaimReviewController { }); } + @AdminOnly() @ApiTags("claim-review") @Put("api/review/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async update(@Param("id") reviewId, @Body() body) { const validateCaptcha = await this.captchaService.validate( body.recaptcha @@ -85,15 +78,14 @@ export class ClaimReviewController { ); } + @AdminOnly() @ApiTags("claim-review") @Delete("api/review/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async delete(@Param("id") reviewId) { return this.claimReviewService.delete(reviewId); } - @IsPublic() + @Public() @ApiTags("claim-review") @Get("api/review/:data_hash") @Header("Cache-Control", "max-age=60, must-revalidate") diff --git a/server/claim-review/claim-review.service.ts b/server/claim-review/claim-review.service.ts index bec890244..9e1b37462 100644 --- a/server/claim-review/claim-review.service.ts +++ b/server/claim-review/claim-review.service.ts @@ -353,7 +353,7 @@ export class ClaimReviewService { const history = this.historyService.getHistoryParams( newReview._id, TargetModel.ClaimReview, - this.req?.user, + this.req.user?._id, hide ? HistoryType.Hide : HistoryType.Unhide, after, before diff --git a/server/claim-review/schemas/claim-review.schema.ts b/server/claim-review/schemas/claim-review.schema.ts index 187f915ae..18fb99475 100644 --- a/server/claim-review/schemas/claim-review.schema.ts +++ b/server/claim-review/schemas/claim-review.schema.ts @@ -11,7 +11,7 @@ import { Source } from "../../source/schemas/source.schema"; export type ClaimReviewDocument = ClaimReview & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class ClaimReview { @Prop({ type: mongoose.Types.ObjectId, diff --git a/server/claim/claim-revision/schema/claim-revision.schema.ts b/server/claim/claim-revision/schema/claim-revision.schema.ts index 96e782a20..48cfd2c8a 100644 --- a/server/claim/claim-revision/schema/claim-revision.schema.ts +++ b/server/claim/claim-revision/schema/claim-revision.schema.ts @@ -6,7 +6,7 @@ import { ContentModelEnum } from "../../../types/enums"; export type ClaimRevisionDocument = ClaimRevision & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class ClaimRevision { @Prop({ required: true }) title: string; diff --git a/server/claim/claim.controller.spec.ts b/server/claim/claim.controller.spec.ts new file mode 100644 index 000000000..e859b2866 --- /dev/null +++ b/server/claim/claim.controller.spec.ts @@ -0,0 +1,136 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { ClaimController } from "./claim.controller"; +import { ImageService } from "./types/image/image.service"; +import { + mockClaimService, + mockImageService, + mockPersonalityService, +} from "../mocks/ClaimMock"; +import { ClaimService } from "./claim.service"; +import { ClaimReviewService } from "../claim-review/claim-review.service"; +import { ReviewTaskService } from "../review-task/review-task.service"; +import { SentenceService } from "./types/sentence/sentence.service"; +import { ConfigService } from "@nestjs/config"; +import { ViewService } from "../view/view.service"; +import { CaptchaService } from "../captcha/captcha.service"; +import { DebateService } from "./types/debate/debate.service"; +import { EditorService } from "../editor/editor.service"; +import { ParserService } from "./parser/parser.service"; +import { HistoryService } from "../history/history.service"; +import { ClaimRevisionService } from "./claim-revision/claim-revision.service"; +import { FeatureFlagService } from "../feature-flag/feature-flag.service"; +import { GroupService } from "../group/group.service"; +import { AbilitiesGuard } from "../auth/ability/abilities.guard"; +import { GetByDataHashDto } from "./dto/get-by-datahash.dto"; + +describe("ClaimController (Unit)", () => { + let controller: ClaimController; + let testingModule: TestingModule; + let imageService: ReturnType; + let claimService: ReturnType; + let personalityService: ReturnType; + + beforeEach(async () => { + imageService = mockImageService(); + claimService = mockClaimService(); + personalityService = mockPersonalityService(); + + testingModule = await Test.createTestingModule({ + controllers: [ClaimController], + providers: [ + { provide: ClaimReviewService, useValue: {} }, + { provide: ReviewTaskService, useValue: {} }, + { provide: "PersonalityService", useValue: personalityService }, + { provide: ClaimService, useValue: claimService }, + { provide: SentenceService, useValue: {} }, + { provide: ConfigService, useValue: {} }, + { provide: ViewService, useValue: {} }, + { provide: CaptchaService, useValue: {} }, + { provide: ImageService, useValue: imageService }, + { provide: DebateService, useValue: {} }, + { provide: EditorService, useValue: {} }, + { provide: ParserService, useValue: {} }, + { provide: HistoryService, useValue: {} }, + { provide: ClaimRevisionService, useValue: {} }, + { provide: FeatureFlagService, useValue: {} }, + { provide: GroupService, useValue: {} }, + + { provide: "REQUEST", useValue: {} }, + ], + }) + .overrideGuard(AbilitiesGuard) + .useValue({}) + .compile(); + + controller = testingModule.get(ClaimController); + }); + + describe("GetByDataHashDto", () => { + it("should accept valid data_hash", () => { + expect(() => + GetByDataHashDto.parse({ + data_hash: "96cffa6efb4c5732c46dfed98be707a5", + }) + ).not.toThrow(); + }); + + it("should throw when data_hash is invalid", () => { + expect(() => + GetByDataHashDto.parse({ + data_hash: 123, + }) + ).toThrow(); + }); + + it("should throw when data_hash is empty", () => { + expect(() => + GetByDataHashDto.parse({ + data_hash: "", + }) + ).toThrow(); + }); + }); + + it("should accept data_hash when it is a string", async () => { + const req = { + params: { data_hash: "96cffa6efb4c5732c46dfed98be707a5" }, + } as any; + const res = {} as any; + + personalityService.getPersonalityBySlug!.mockResolvedValue({ + _id: "507f1f77bcf86cd799439011", + name: "Test Personality", + slug: "test-personality", + description: "Test description", + isHidden: false, + } as any); + + claimService.getByPersonalityIdAndClaimSlug.mockResolvedValue({ + _id: "507f1f77bcf86cd799439012", + slug: "test-claim", + personalities: ["507f1f77bcf86cd799439011"], + isHidden: false, + nameSpace: "main", + } as any); + + imageService.getByDataHash.mockResolvedValue({ + _id: "507f1f77bcf86cd799439013", + type: "Image", + data_hash: "96cffa6efb4c5732c46dfed98be707a5", + content: "https://example.com/image.jpg", + props: { + key: "test-image-key", + extension: "jpg", + }, + } as any); + + jest.spyOn( + controller as any, + "returnClaimReviewPage" + ).mockResolvedValue(undefined); + + await expect( + controller.getImageClaimReviewPage(req, res) + ).resolves.not.toThrow(); + }); +}); diff --git a/server/claim/claim.controller.ts b/server/claim/claim.controller.ts index e58fdbcfc..b342970b1 100644 --- a/server/claim/claim.controller.ts +++ b/server/claim/claim.controller.ts @@ -12,7 +12,6 @@ import { Req, Res, Header, - UseGuards, Inject, } from "@nestjs/common"; import { ClaimReviewService } from "../claim-review/claim-review.service"; @@ -25,7 +24,6 @@ import * as mongoose from "mongoose"; import { CreateClaimDTO } from "./dto/create-claim.dto"; import { GetClaimsDTO } from "./dto/get-claims.dto"; import { UpdateClaimDTO } from "./dto/update-claim.dto"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; import { CaptchaService } from "../captcha/captcha.service"; import { ReviewTaskService } from "../review-task/review-task.service"; import { TargetModel } from "../history/schema/history.schema"; @@ -36,12 +34,8 @@ import { SentenceDocument } from "./types/sentence/schemas/sentence.schema"; import { ImageService } from "./types/image/image.service"; import { ImageDocument } from "./types/image/schemas/image.schema"; import { CreateDebateClaimDTO } from "./dto/create-debate-claim.dto"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; +import { Public, AdminOnly } from "../auth/decorators/auth.decorator"; import type { IPersonalityService } from "../interfaces/personality.service.interface"; -import { - AdminUserAbility, - CheckAbilities, -} from "../auth/ability/ability.decorator"; import { DebateService } from "./types/debate/debate.service"; import { EditorService } from "../editor/editor.service"; import { UpdateDebateDto } from "./dto/update-debate.dto"; @@ -54,6 +48,7 @@ import { ClaimRevisionService } from "./claim-revision/claim-revision.service"; import { FeatureFlagService } from "../feature-flag/feature-flag.service"; import { Types } from "mongoose"; import { GroupService } from "../group/group.service"; +import { GetByDataHashDto } from "../claim/dto/get-by-datahash.dto"; @Controller(":namespace?") export class ClaimController { @@ -95,7 +90,7 @@ export class ClaimController { return inputs; } - @IsPublic() + @Public() @ApiTags("claim") @Get("api/claim") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -200,7 +195,7 @@ export class ClaimController { } } - @IsPublic() // Allow this route to be public temporarily for testing + @Public() // Allow this route to be public temporarily for testing @ApiTags("claim") @Post("api/claim/unattributed") async createUnattributedClaim(@Body() createClaimDTO) { @@ -261,7 +256,7 @@ export class ClaimController { return this.claimService.create(createClaimDTO); } - @IsPublic() + @Public() @Get("api/claim/:id") @Header("Cache-Control", "max-age=60, must-revalidate") @ApiTags("claim") @@ -275,18 +270,16 @@ export class ClaimController { return this.claimService.update(claimId, updateClaimDTO); } + @AdminOnly() @ApiTags("claim") @Delete("api/claim/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async delete(@Param("id") claimId) { return this.claimService.delete(claimId); } + @AdminOnly() @ApiTags("claim") @Put("api/claim/hidden/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async updateHiddenStatus(@Param("id") claimId, @Body() body) { const validateCaptcha = await this.captchaService.validate( body.recaptcha @@ -302,7 +295,7 @@ export class ClaimController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("personality/:personalitySlug/claim/:claimSlug/sentence/:data_hash") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -401,7 +394,7 @@ export class ClaimController { await this.viewService.render(req, res, "/claim-review", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("claim/:claimId/image/:data_hash") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -409,7 +402,13 @@ export class ClaimController { @Req() req: BaseRequest, @Res() res: Response ) { - const { data_hash, claimId, namespace } = req.params; + const validatedParams = GetByDataHashDto.parse({ + data_hash: req.params.data_hash, + }); + + const { data_hash } = validatedParams; + + const { claimId, namespace } = req.params; const claim = await this.claimService.getById( claimId, namespace as NameSpaceEnum @@ -418,11 +417,10 @@ export class ClaimController { await this.returnClaimReviewPage(data_hash, req, res, claim, image); } + @AdminOnly() @Get("claim/:claimId/debate/edit") @ApiTags("pages") @Header("Cache-Control", "max-age=60, must-revalidate") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) public async getDebateEditor( @Req() req: BaseRequest, @Res() res: Response @@ -446,7 +444,7 @@ export class ClaimController { await this.viewService.render(req, res, "/debate-editor", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("claim/:claimId/debate") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -485,7 +483,7 @@ export class ClaimController { await this.viewService.render(req, res, "/debate-page", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("personality/:personalitySlug/claim/:claimSlug/image/:data_hash") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -493,7 +491,13 @@ export class ClaimController { @Req() req: BaseRequest, @Res() res: Response ) { - const { data_hash, personalitySlug, claimSlug } = req.params; + const validatedParams = GetByDataHashDto.parse({ + data_hash: req.params.data_hash, + }); + + const { data_hash } = validatedParams; + + const { personalitySlug, claimSlug } = req.params; const personality = await this.personalityService.getPersonalityBySlug( { slug: personalitySlug, @@ -554,7 +558,7 @@ export class ClaimController { await this.viewService.render(req, res, "/claim-create", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("claim") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -575,7 +579,7 @@ export class ClaimController { ); } - @IsPublic() + @Public() @Redirect() @ApiTags("pages") @Get("claim/:claimSlug") @@ -652,7 +656,7 @@ export class ClaimController { await this.viewService.render(req, res, "/claim-page", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("personality/:personalitySlug/claim/:claimSlug") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -762,7 +766,7 @@ export class ClaimController { await this.viewService.render(req, res, "/claim-page", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("personality/:personalitySlug/claim/:claimSlug/sources") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -795,7 +799,7 @@ export class ClaimController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get( "personality/:personalitySlug/claim/:claimSlug/sentence/:data_hash/sources" @@ -856,7 +860,7 @@ export class ClaimController { await this.viewService.render(req, res, "/history-page", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("claim/:claimSlug/sources") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -931,7 +935,7 @@ export class ClaimController { await this.viewService.render(req, res, "/history-page", queryObject); } - @IsPublic() + @Public() @Redirect() @ApiTags("pages") @Get("claim/:claimSlug/sentence/:data_hash") diff --git a/server/claim/claim.service.ts b/server/claim/claim.service.ts index ad24a1483..abd8dba5d 100644 --- a/server/claim/claim.service.ts +++ b/server/claim/claim.service.ts @@ -1,4 +1,10 @@ -import { Injectable, Inject, Scope, NotFoundException } from "@nestjs/common"; +import { + Injectable, + Inject, + Scope, + NotFoundException, + Logger, +} from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; import { FilterQuery, Model, Types } from "mongoose"; import { Claim, ClaimDocument } from "../claim/schemas/claim.schema"; @@ -30,6 +36,8 @@ type ClaimMatchParameters = ( @Injectable({ scope: Scope.REQUEST }) export class ClaimService { + private readonly logger = new Logger(ClaimService.name); + constructor( @Inject(REQUEST) private req: BaseRequest, @InjectModel(Claim.name) @@ -140,7 +148,7 @@ export class ClaimService { newClaim.latestRevision = newClaimRevision._id; newClaim.slug = newClaimRevision.slug; - const user = this.req.user; + const user = this.req.user?._id; const history = this.historyService.getHistoryParams( newClaim._id, @@ -193,7 +201,7 @@ export class ClaimService { claim.latestRevision = newClaimRevision._id; claim.slug = newClaimRevision.slug; - const user = this.req.user; + const user = this.req.user?._id; const history = this.historyService.getHistoryParams( claimId, @@ -218,7 +226,7 @@ export class ClaimService { * @returns Returns the claim with the param isDeleted equal to true */ async delete(claimId) { - const user = this.req.user; + const user = this.req.user?._id; const previousClaim = await this.getById(claimId); const history = this.historyService.getHistoryParams( claimId, @@ -251,7 +259,7 @@ export class ClaimService { const history = this.historyService.getHistoryParams( newClaim._id, TargetModel.Claim, - this.req?.user, + this.req.user?._id, isHidden ? HistoryType.Hide : HistoryType.Unhide, after, before @@ -263,7 +271,7 @@ export class ClaimService { newClaim ); } catch (e) { - console.error(e); + this.logger.error("Failed to update claim:", e); throw new NotFoundException(); } } diff --git a/server/claim/dto/get-by-datahash.dto.ts b/server/claim/dto/get-by-datahash.dto.ts new file mode 100644 index 000000000..1d4565a9f --- /dev/null +++ b/server/claim/dto/get-by-datahash.dto.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const GetByDataHashDto = z.object({ + data_hash: z.string().regex(/^[a-f0-9]{32}$/i), +}); + +export type GetByDataHashDto = z.infer; diff --git a/server/claim/parser/parser.service.spec.ts b/server/claim/parser/parser.service.spec.ts index 3f5273a57..a66b84242 100644 --- a/server/claim/parser/parser.service.spec.ts +++ b/server/claim/parser/parser.service.spec.ts @@ -2,7 +2,6 @@ import { Test, TestingModule } from "@nestjs/testing"; import { ParserService } from "./parser.service"; import * as fs from "fs"; import { TestConfigOptions } from "../../tests/utils/TestConfigOptions"; -import { MongoMemoryServer } from "mongodb-memory-server"; import { Types } from "mongoose"; import { AppModule } from "../../app.module"; import { SessionGuard } from "../../auth/session.guard"; @@ -11,6 +10,7 @@ import { SessionOrM2MGuard } from "../../auth/m2m-or-session.guard"; import { SessionOrM2MGuardMock } from "../../tests/mocks/SessionOrM2MGuardMock"; import { M2MGuard } from "../../auth/m2m.guard"; import { M2MGuardMock } from "../../tests/mocks/M2MGuardMock"; +import { CleanupDatabase } from "../../tests/utils/CleanupDatabase"; /** * ParserService Unit Test Suite @@ -36,13 +36,12 @@ import { M2MGuardMock } from "../../tests/mocks/M2MGuardMock"; */ describe("ParserService", () => { let parserService: ParserService; - let db: any; let moduleFixture: TestingModule; const claimRevisionIdMock = new Types.ObjectId(); beforeAll(async () => { - db = await MongoMemoryServer.create({ instance: { port: 35025 } }); - const mongoUri = db.getUri(); + // Use shared MongoDB instance from global setup + const mongoUri = process.env.MONGO_URI!; // Update the test config with the actual MongoDB URI const testConfig = { @@ -277,8 +276,6 @@ describe("ParserService", () => { }); afterAll(async () => { - if (db) { - await db.stop(); - } + await CleanupDatabase(process.env.MONGO_URI!); }); }); diff --git a/server/claim/schemas/claim.schema.ts b/server/claim/schemas/claim.schema.ts index 7ef4c4036..b988ddf33 100644 --- a/server/claim/schemas/claim.schema.ts +++ b/server/claim/schemas/claim.schema.ts @@ -8,7 +8,7 @@ import { Group } from "../../group/schemas/group.schema"; export type ClaimDocument = Claim & mongoose.Document & { revisions: any }; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Claim { @Prop({ type: [ diff --git a/server/claim/types/debate/debate.service.ts b/server/claim/types/debate/debate.service.ts index 3bfeff82d..2b7fe5462 100644 --- a/server/claim/types/debate/debate.service.ts +++ b/server/claim/types/debate/debate.service.ts @@ -75,7 +75,7 @@ export class DebateService { const history = this.historyService.getHistoryParams( newDebate._id, TargetModel.Debate, - this.req.user, + this.req.user?._id, HistoryType.Update, newDebate, previousDebate diff --git a/server/claim/types/image/image.service.ts b/server/claim/types/image/image.service.ts index ed2653268..554b69300 100644 --- a/server/claim/types/image/image.service.ts +++ b/server/claim/types/image/image.service.ts @@ -1,4 +1,10 @@ -import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from "@nestjs/common"; +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + Scope, +} from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; import { HistoryService } from "../../../history/history.service"; import { Model } from "mongoose"; @@ -10,6 +16,7 @@ import { import { REQUEST } from "@nestjs/core"; import { ReportService } from "../../../report/report.service"; import type { BaseRequest } from "../../../types"; +import { GetByDataHashDto } from "../../dto/get-by-datahash.dto"; @Injectable({ scope: Scope.REQUEST }) export class ImageService { @@ -37,7 +44,7 @@ export class ImageService { const history = this.historyService.getHistoryParams( newImage._id, TargetModel.Image, - this.req.user, + this.req.user?._id, HistoryType.Create, newImage ); @@ -46,11 +53,18 @@ export class ImageService { } async getByDataHash(data_hash: string) { - if (!data_hash) { - throw new BadRequestException("Invalid data hash format."); - } - const report = await this.reportService.findByDataHash(data_hash); - const image = await this.ImageModel.findOne({ data_hash }); + const validatedParams = GetByDataHashDto.safeParse({ data_hash }); + + if (!validatedParams.success) { + throw new BadRequestException("Invalid data_hash format"); + } + + const { data_hash: validatedHash } = validatedParams.data; + + const report = await this.reportService.findByDataHash(validatedHash); + const image = await this.ImageModel.findOne({ + data_hash: validatedHash, + }); if (image) { image.props = { classification: report?.classification, diff --git a/server/claim/types/image/schemas/image.schema.ts b/server/claim/types/image/schemas/image.schema.ts index 3e58fee62..c295541b6 100644 --- a/server/claim/types/image/schemas/image.schema.ts +++ b/server/claim/types/image/schemas/image.schema.ts @@ -6,7 +6,7 @@ import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-rev export type ImageDocument = Image & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Image { @Prop({ default: ContentModelEnum.Image, diff --git a/server/claim/types/paragraph/schemas/paragraph.schema.ts b/server/claim/types/paragraph/schemas/paragraph.schema.ts index 72a6ff05f..e6cefb881 100644 --- a/server/claim/types/paragraph/schemas/paragraph.schema.ts +++ b/server/claim/types/paragraph/schemas/paragraph.schema.ts @@ -5,7 +5,7 @@ import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-rev export type ParagraphDocument = Paragraph & mongoose.Document; -@Schema() +@Schema({ timestamps: true }) export class Paragraph { @Prop({ type: String, diff --git a/server/claim/types/sentence/schemas/sentence.schema.ts b/server/claim/types/sentence/schemas/sentence.schema.ts index c2ab00505..9d5b5ccbd 100644 --- a/server/claim/types/sentence/schemas/sentence.schema.ts +++ b/server/claim/types/sentence/schemas/sentence.schema.ts @@ -5,7 +5,7 @@ import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-rev export type SentenceDocument = Sentence & mongoose.Document; -@Schema() +@Schema({ timestamps: true }) export class Sentence { @Prop({ type: String, diff --git a/server/claim/types/sentence/sentence.controller.ts b/server/claim/types/sentence/sentence.controller.ts index 9527411c4..97cb33ef1 100644 --- a/server/claim/types/sentence/sentence.controller.ts +++ b/server/claim/types/sentence/sentence.controller.ts @@ -1,14 +1,30 @@ import { Controller, Param, Put, Body, Get } from "@nestjs/common"; import { SentenceService } from "./sentence.service"; import { ApiTags } from "@nestjs/swagger"; -import { IsPublic } from "../../../auth/decorators/is-public.decorator"; +import type { Cop30Sentence } from "../../../../src/types/Cop30Sentence"; +import type { Cop30Stats } from "../../../../src/types/Cop30Stats"; +import { Auth } from "../../../auth/decorators/auth.decorator"; @Controller() export class SentenceController { constructor(private sentenceService: SentenceService) {} + @Auth({ public: true }) @ApiTags("claim") - @IsPublic() + @Get("api/sentence/cop30") + async getAllSentencesWithCop30Topic(): Promise { + return this.sentenceService.getSentencesWithCop30Topics(); + } + + @Auth({ public: true }) + @ApiTags("claim") + @Get("api/sentence/cop30/stats") + async getCop30Stats(): Promise { + return this.sentenceService.getCop30Stats(); + } + + @ApiTags("claim") + @Auth({ public: true }) @Get("api/sentence/:data_hash") getSentenceByHash(@Param("data_hash") data_hash) { return this.sentenceService.getByDataHash(data_hash); diff --git a/server/claim/types/sentence/sentence.service.ts b/server/claim/types/sentence/sentence.service.ts index f9d86de1f..607a40575 100644 --- a/server/claim/types/sentence/sentence.service.ts +++ b/server/claim/types/sentence/sentence.service.ts @@ -1,9 +1,17 @@ -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; import { Model } from "mongoose"; import { SentenceDocument, Sentence } from "./schemas/sentence.schema"; import { InjectModel } from "@nestjs/mongoose"; import { ReportService } from "../../../report/report.service"; import { UtilService } from "../../../util"; +import { allCop30WikiDataIds } from "../../../../src/constants/cop30Filters"; +import type { Cop30Sentence } from "../../../../src/types/Cop30Sentence"; +import type { Cop30Stats } from "../../../../src/types/Cop30Stats"; +import { buildStats } from "../../../../src/components/Home/COP30/utils/classification"; interface FindAllOptionsFilters { searchText: string; @@ -23,6 +31,54 @@ export class SentenceService { private util: UtilService ) {} + async getSentencesWithCop30Topics(): Promise { + const aggregation = [ + { $match: { "topics.value": { $in: allCop30WikiDataIds } } }, + { + $lookup: { + from: "reviewtasks", + let: { sentenceDataHash: "$data_hash" }, + pipeline: [ + { + $match: { + $expr: { + $eq: [ + "$machine.context.reviewData.data_hash", + "$$sentenceDataHash", + ], + }, + }, + }, + { + $project: { + _id: 0, + classification: + "$machine.context.reviewData.classification", + }, + }, + ], + as: "reviewInfo", + }, + }, + { + $addFields: { + classification: { + $arrayElemAt: ["$reviewInfo.classification", 0], + }, + }, + }, + { $project: { reviewInfo: 0 } }, + ]; + + return this.SentenceModel.aggregate(aggregation).exec(); + } + + async getCop30Stats(): Promise { + const sentences = await this.getSentencesWithCop30Topics(); + + return buildStats(sentences); + } + async create(sentenceBody) { const newSentence = await new this.SentenceModel(sentenceBody).save(); return newSentence._id; @@ -30,11 +86,13 @@ export class SentenceService { async getByDataHash(data_hash: string) { if (!data_hash) { - throw new BadRequestException("Invalid data_hash: must be a string."); + throw new BadRequestException( + "Invalid data_hash: must be a string." + ); } const report = await this.reportService.findByDataHash(data_hash); const sentence = await this.SentenceModel.findOne({ - data_hash: { $eq: data_hash }, + data_hash: { $eq: data_hash }, }); if (sentence) { sentence.props = { @@ -51,11 +109,11 @@ export class SentenceService { const sentence = await this.getByDataHash(data_hash); if (!Array.isArray(topics)) { - throw new BadRequestException("Invalid topics array."); + throw new BadRequestException("Invalid topics array."); } return this.SentenceModel.updateOne( - { _id: sentence._id }, - { $set: { topics } } + { _id: sentence._id }, + { $set: { topics } } ); } @@ -186,4 +244,4 @@ export class SentenceService { ), }; } -} \ No newline at end of file +} diff --git a/server/claim/types/speech/schemas/speech.schema.ts b/server/claim/types/speech/schemas/speech.schema.ts index fff66dbfd..ab94df0fc 100644 --- a/server/claim/types/speech/schemas/speech.schema.ts +++ b/server/claim/types/speech/schemas/speech.schema.ts @@ -5,7 +5,7 @@ import { Paragraph } from "../../paragraph/schemas/paragraph.schema"; import { ClaimRevision } from "../../../../claim/claim-revision/schema/claim-revision.schema"; export type SpeechDocument = Speech & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Speech { @Prop({ default: "speech", diff --git a/server/claim/types/unattributed/schemas/unattributed.schema.ts b/server/claim/types/unattributed/schemas/unattributed.schema.ts index 18c7b197a..df55fe089 100644 --- a/server/claim/types/unattributed/schemas/unattributed.schema.ts +++ b/server/claim/types/unattributed/schemas/unattributed.schema.ts @@ -3,7 +3,7 @@ import * as mongoose from "mongoose"; import { Paragraph } from "../../paragraph/schemas/paragraph.schema"; export type UnattributedDocument = Unattributed & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Unattributed { @Prop({ default: "unattributed", diff --git a/server/copilot/copilot-chat.controller.ts b/server/copilot/copilot-chat.controller.ts index 6afef72ac..a311d2402 100644 --- a/server/copilot/copilot-chat.controller.ts +++ b/server/copilot/copilot-chat.controller.ts @@ -17,22 +17,17 @@ * This controller uses decorators to define routes and their configurations, ensuring proper request handling and response formatting. It also integrates file upload handling for PDF documents, enabling document-context chat functionalities. */ -import { Body, Controller, Post, Req, UseGuards } from "@nestjs/common"; +import { Body, Controller, Post, Req } from "@nestjs/common"; import { CopilotChatService } from "./copilot-chat.service"; import { ContextAwareMessagesDto } from "./dtos/context-aware-messages.dto"; -import { - CheckAbilities, - FactCheckerUserAbility, -} from "../auth/ability/ability.decorator"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; +import { FactCheckerOnly } from "../auth/decorators/auth.decorator"; @Controller() export class CopilotChatController { constructor(private readonly copilotChatService: CopilotChatService) {} + @FactCheckerOnly() @Post("api/agent-chat") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) async agentChat( @Body() contextAwareMessagesDto: ContextAwareMessagesDto, @Req() req diff --git a/server/copilot/copilot-chat.service.ts b/server/copilot/copilot-chat.service.ts index e64b05adb..66d78ff73 100644 --- a/server/copilot/copilot-chat.service.ts +++ b/server/copilot/copilot-chat.service.ts @@ -27,12 +27,15 @@ import { import { z } from "zod"; import { DynamicStructuredTool } from "@langchain/core/tools"; -import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents"; +import { + AgentExecutor, + createToolCallingAgent, +} from "@langchain/classic/agents"; import { ChatPromptTemplate, MessagesPlaceholder, } from "@langchain/core/prompts"; -import { HumanMessage, AIMessage } from "langchain/schema"; +import { HumanMessage, AIMessage } from "@langchain/core/messages"; import { AutomatedFactCheckingService } from "../automated-fact-checking/automated-fact-checking.service"; import { EditorParseService } from "../editor-parse/editor-parse.service"; import { ConfigService } from "@nestjs/config"; @@ -127,8 +130,11 @@ export class CopilotChatService { (message) => this.transformMessage(message) ); const tools = [ - new DynamicStructuredTool(this.getFactCheckingReportTool), + new DynamicStructuredTool( + this.getFactCheckingReportTool as any + ), ]; + const currentMessageContent = contextAwareMessagesDto.messages[ contextAwareMessagesDto.messages.length - 1 @@ -175,7 +181,7 @@ export class CopilotChatService { apiKey: this.configService.get("openai.api_key"), }); - const agent = await createOpenAIFunctionsAgent({ + const agent = await createToolCallingAgent({ llm, tools, prompt, @@ -221,7 +227,6 @@ export class CopilotChatService { } private exceptionHandling = (e: unknown) => { - console.log(e); this.logger.error(e); throw new HttpException( customMessage( diff --git a/server/daily-report/daily-report.controller.ts b/server/daily-report/daily-report.controller.ts index 792721396..01dff003c 100644 --- a/server/daily-report/daily-report.controller.ts +++ b/server/daily-report/daily-report.controller.ts @@ -1,9 +1,5 @@ -import { Controller, Param, Post, UseGuards } from "@nestjs/common"; -import { - AdminUserAbility, - CheckAbilities, -} from "../auth/ability/ability.decorator"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; +import { Controller, Param, Post } from "@nestjs/common"; +import { AdminOnly } from "../auth/decorators/auth.decorator"; import { DailyReportService } from "../daily-report/daily-report.service"; import { ClaimReviewService } from "../claim-review/claim-review.service"; import { NotificationService } from "../notifications/notifications.service"; @@ -27,9 +23,8 @@ export class DailyReportController { private sourceService: SourceService ) {} + @AdminOnly() @Post("api/daily-report/topic/:topic/send/:nameSpace") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async sendDailyReport( @Param("topic") topic, @Param("nameSpace") nameSpace diff --git a/server/daily-report/schemas/daily-report.schema.ts b/server/daily-report/schemas/daily-report.schema.ts index 293e900a2..ff12a1dec 100644 --- a/server/daily-report/schemas/daily-report.schema.ts +++ b/server/daily-report/schemas/daily-report.schema.ts @@ -3,7 +3,7 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; export type DailyReportDocument = DailyReport & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class DailyReport { @Prop({ type: mongoose.Types.ObjectId, diff --git a/server/editor-parse/editor-parse.service.spec.ts b/server/editor-parse/editor-parse.service.spec.ts index 5cd5638a1..9530becd5 100644 --- a/server/editor-parse/editor-parse.service.spec.ts +++ b/server/editor-parse/editor-parse.service.spec.ts @@ -6,22 +6,22 @@ import { RemirrorJSON } from "remirror"; /** * EditorParseService Unit Test Suite - * + * * Tests the bidirectional transformation service that converts between different * representation formats for the collaborative fact-checking editor system. - * + * * Business Context: * The editor parse service handles content transformation between three key formats: * 1. Schema format - Internal data structure with source references * 2. Editor format - Remirror JSON for rich text editing * 3. HTML format - Final rendered output for display - * + * * Core Functionality: * - Schema ↔ Editor: Bidirectional conversion for editing workflows * - Schema → HTML: Rendering for final display with source citations * - Source link processing: Converting markup references to interactive citations * - Content validation: Ensuring data integrity across transformations - * + * * Data Flow: * 1. User creates content in editor (Remirror JSON) * 2. Editor → Schema conversion for storage and processing @@ -158,19 +158,19 @@ describe("EditorParseService", () => { describe("parse()", () => { /** * Test: Schema to Editor Conversion - Rich Text Structure Generation - * + * * Purpose: Validates conversion from internal schema format to Remirror editor format * Business Logic: * - Transforms schema content structure to Remirror JSON nodes * - Converts source references to link marks with metadata * - Organizes content by section types (questions, summary, report, verification) * - Maintains source attribution and link relationships - * + * * Test Data: * - Schema with questions, summary, report (with source), verification * - Source reference: {{uniqueId|duplicated}} → link mark with href * - Expected Remirror JSON with proper node structure - * + * * Validates: * - Correct Remirror document structure (doc → sections → paragraphs → text) * - Source markup conversion to link marks @@ -187,19 +187,19 @@ describe("EditorParseService", () => { /** * Test: Editor to Schema Conversion - Content Structure Extraction - * + * * Purpose: Validates conversion from Remirror editor format to internal schema * Business Logic: * - Extracts content from Remirror JSON nodes by section type * - Converts link marks back to source reference markup * - Organizes content into schema structure with source metadata * - Preserves source relationships and attribution data - * + * * Test Data: * - Remirror JSON with structured content sections * - Link marks with id, href, and target attributes * - Expected schema format with {{id|text}} source references - * + * * Validates: * - Correct schema content extraction from editor nodes * - Link mark conversion to source markup format @@ -216,19 +216,19 @@ describe("EditorParseService", () => { /** * Test: Schema to HTML Conversion - Rendering with Citations - * + * * Purpose: Validates conversion from schema format to final HTML display format * Business Logic: * - Renders schema content as HTML with proper markup * - Converts source references to interactive citation links * - Generates superscript numbering for source citations * - Creates accessible link structure with proper attributes - * + * * Test Data: * - Schema content with embedded source references * - Source metadata with href, textRange, targetText, and sup numbering * - Expected HTML with
,

, , and elements - * + * * Validates: * - Correct HTML structure generation (div containers, paragraphs) * - Source reference conversion to citation links @@ -244,19 +244,19 @@ describe("EditorParseService", () => { /** * Test: Source Position Accuracy - Citation Placement Validation - * + * * Purpose: Validates accurate positioning of source citations in HTML output * Business Logic: * - Ensures source citations appear at correct text positions * - Maintains source reference integrity during HTML conversion * - Preserves citation context and readability * - Validates superscript numbering and link formatting - * + * * Test Data: * - Report text: "duplicated word {{uniqueId|duplicated}}" * - Expected HTML: "duplicated word duplicated1" * - Source citation with proper attributes and superscript - * + * * Validates: * - Exact text position of source citations * - Correct HTML link generation with href and rel attributes @@ -272,19 +272,19 @@ describe("EditorParseService", () => { /** * Test: Bidirectional Source Processing - Editor Link Mark Accuracy - * + * * Purpose: Validates accurate source processing in editor-to-schema conversion * Business Logic: * - Converts Remirror link marks back to schema markup format * - Maintains source reference position and metadata accuracy * - Ensures bidirectional conversion consistency * - Preserves source attribution and text relationships - * + * * Test Data: * - Remirror editor content with link marks containing source metadata * - Link marks with id, href, target attributes on specific text ranges * - Expected schema markup: "duplicated word {{uniqueId|duplicated}}" - * + * * Validates: * - Accurate conversion from link marks to schema markup * - Source position preservation in text content @@ -340,12 +340,14 @@ describe("EditorParseService", () => { }; const expectedSchemaWithLineBreaks = { - summary: "First paragraph with important information.\nSecond paragraph after line break.\nThird paragraph with conclusion.", + summary: + "First paragraph with important information.\nSecond paragraph after line break.\nThird paragraph with conclusion.", sources: [], }; const expectedHtmlWithLineBreaks = { - summary: "

First paragraph with important information.

Second paragraph after line break.

Third paragraph with conclusion.

", + summary: + "

First paragraph with important information.

Second paragraph after line break.

Third paragraph with conclusion.

", }; it("Should preserve line breaks when converting from editor to schema", async () => { @@ -353,13 +355,16 @@ describe("EditorParseService", () => { multiParagraphEditorContent ); - expect(schemaResult.summary).toEqual(expectedSchemaWithLineBreaks.summary); - expect(schemaResult.summary.includes('\n')).toBe(true); + expect(schemaResult.summary).toEqual( + expectedSchemaWithLineBreaks.summary + ); + expect(schemaResult.summary.includes("\n")).toBe(true); }); it("Should convert line breaks to separate

elements in HTML output for semantic HTML", async () => { const schemaWithLineBreaks = { - summary: "First paragraph with important information.\nSecond paragraph after line break.\nThird paragraph with conclusion.", + summary: + "First paragraph with important information.\nSecond paragraph after line break.\nThird paragraph with conclusion.", sources: [], }; @@ -367,14 +372,17 @@ describe("EditorParseService", () => { schemaWithLineBreaks ); - expect(htmlResult.summary).toEqual(expectedHtmlWithLineBreaks.summary); - expect(htmlResult.summary.includes('

')).toBe(true); - expect(htmlResult.summary.split('

').length - 1).toBe(3); // Should have 3 paragraphs + expect(htmlResult.summary).toEqual( + expectedHtmlWithLineBreaks.summary + ); + expect(htmlResult.summary.includes("

")).toBe(true); + expect(htmlResult.summary.split("

").length - 1).toBe(3); // Should have 3 paragraphs }); it("Should convert schema with line breaks back to multiple paragraphs in editor", async () => { const schemaWithLineBreaks = { - summary: "First paragraph with important information.\nSecond paragraph after line break.\nThird paragraph with conclusion.", + summary: + "First paragraph with important information.\nSecond paragraph after line break.\nThird paragraph with conclusion.", sources: [], }; @@ -385,16 +393,23 @@ describe("EditorParseService", () => { expect(editorResult.content).toHaveLength(1); expect(editorResult.content[0].type).toBe("summary"); expect(editorResult.content[0].content).toHaveLength(3); // Should have 3 paragraphs - + // Verify each paragraph content - expect(editorResult.content[0].content[0].content[0].text).toBe("First paragraph with important information."); - expect(editorResult.content[0].content[1].content[0].text).toBe("Second paragraph after line break."); - expect(editorResult.content[0].content[2].content[0].text).toBe("Third paragraph with conclusion."); + expect(editorResult.content[0].content[0].content[0].text).toBe( + "First paragraph with important information." + ); + expect(editorResult.content[0].content[1].content[0].text).toBe( + "Second paragraph after line break." + ); + expect(editorResult.content[0].content[2].content[0].text).toBe( + "Third paragraph with conclusion." + ); }); it("Should handle empty paragraphs (multiple consecutive line breaks)", async () => { const schemaWithEmptyLines = { - summary: "First paragraph.\n\nThird paragraph after empty line.", + summary: + "First paragraph.\n\nThird paragraph after empty line.", sources: [], }; @@ -403,9 +418,13 @@ describe("EditorParseService", () => { ); expect(editorResult.content[0].content).toHaveLength(3); - expect(editorResult.content[0].content[0].content[0].text).toBe("First paragraph."); + expect(editorResult.content[0].content[0].content[0].text).toBe( + "First paragraph." + ); expect(editorResult.content[0].content[1].content).toHaveLength(0); // Empty paragraph - expect(editorResult.content[0].content[2].content[0].text).toBe("Third paragraph after empty line."); + expect(editorResult.content[0].content[2].content[0].text).toBe( + "Third paragraph after empty line." + ); }); it("Should maintain backward compatibility with single paragraph content", async () => { @@ -417,9 +436,11 @@ describe("EditorParseService", () => { const editorResult = await editorParseService.schema2editor( singleParagraphSchema ); - + expect(editorResult.content[0].content).toHaveLength(1); - expect(editorResult.content[0].content[0].content[0].text).toBe("Single paragraph without line breaks."); + expect(editorResult.content[0].content[0].content[0].text).toBe( + "Single paragraph without line breaks." + ); }); it("Should preserve line breaks with sources (marked content)", async () => { @@ -471,8 +492,10 @@ describe("EditorParseService", () => { multiParagraphWithSource ); - expect(schemaResult.report).toContain('\n'); - expect(schemaResult.report).toEqual("First paragraph with {{sourceId|source}}\nSecond paragraph continues here."); + expect(schemaResult.report).toContain("\n"); + expect(schemaResult.report).toEqual( + "First paragraph with {{sourceId|source}}\nSecond paragraph continues here." + ); }); }); @@ -550,11 +573,13 @@ describe("EditorParseService", () => { ); // Verify content structure - expect(schemaResult.report).toEqual("First paragraph with {{source1|first source}} and more text.\nSecond paragraph has {{source2|second source}} here."); - + expect(schemaResult.report).toEqual( + "First paragraph with {{source1|first source}} and more text.\nSecond paragraph has {{source2|second source}} here." + ); + // Verify both sources are captured expect(schemaResult.sources).toHaveLength(2); - + // Verify first source expect(schemaResult.sources[0]).toMatchObject({ href: "https://source1.com", @@ -562,9 +587,9 @@ describe("EditorParseService", () => { field: "report", targetText: "first source", id: "source1", - }) + }), }); - + // Verify second source expect(schemaResult.sources[1]).toMatchObject({ href: "https://source2.com", @@ -572,7 +597,7 @@ describe("EditorParseService", () => { field: "report", targetText: "second source", id: "source2", - }) + }), }); }); @@ -586,8 +611,10 @@ describe("EditorParseService", () => { schemaWithLineBreaks ); - expect(htmlResult.report).toContain('

'); - expect(htmlResult.report).toBe('

First paragraph with some text.

Second paragraph with more text.

'); + expect(htmlResult.report).toContain("

"); + expect(htmlResult.report).toBe( + "

First paragraph with some text.

Second paragraph with more text.

" + ); }); it("Should use
tags for content with empty lines (visual spacing)", async () => { @@ -600,13 +627,16 @@ describe("EditorParseService", () => { schemaWithEmptyLines ); - expect(htmlResult.report).toContain('
'); - expect(htmlResult.report).toBe('

First paragraph with content.

Third paragraph after empty line.

'); + expect(htmlResult.report).toContain("
"); + expect(htmlResult.report).toBe( + "

First paragraph with content.

Third paragraph after empty line.

" + ); }); it("Should handle empty paragraphs in editor conversion", async () => { const schemaWithEmptyParagraph = { - summary: "First paragraph text.\n\nThird paragraph after empty line.", + summary: + "First paragraph text.\n\nThird paragraph after empty line.", sources: [], }; @@ -616,17 +646,21 @@ describe("EditorParseService", () => { // Should have 3 paragraphs: content, empty, content expect(editorResult.content[0].content).toHaveLength(3); - + // First paragraph should have content expect(editorResult.content[0].content[0].content).toHaveLength(1); - expect(editorResult.content[0].content[0].content[0].text).toBe("First paragraph text."); - + expect(editorResult.content[0].content[0].content[0].text).toBe( + "First paragraph text." + ); + // Second paragraph should be empty expect(editorResult.content[0].content[1].content).toHaveLength(0); - + // Third paragraph should have content expect(editorResult.content[0].content[2].content).toHaveLength(1); - expect(editorResult.content[0].content[2].content[0].text).toBe("Third paragraph after empty line."); + expect(editorResult.content[0].content[2].content[0].text).toBe( + "Third paragraph after empty line." + ); }); it("Should preserve source text ranges when converting back from schema with line breaks", async () => { @@ -638,7 +672,7 @@ describe("EditorParseService", () => { props: { field: "report", textRange: [19, 43], - targetText: "marked text", + targetText: "marked text", sup: 1, id: "sourceA", }, @@ -647,16 +681,22 @@ describe("EditorParseService", () => { }; // Convert to editor and back to schema - const editorResult = await editorParseService.schema2editor(originalSchemaWithSources); - const roundTripSchema = await editorParseService.editor2schema(editorResult); + const editorResult = await editorParseService.schema2editor( + originalSchemaWithSources + ); + const roundTripSchema = await editorParseService.editor2schema( + editorResult + ); // Content should be preserved with line breaks - expect(roundTripSchema.report).toContain('\n'); - expect(roundTripSchema.report).toContain('{{sourceA|marked text}}'); - + expect(roundTripSchema.report).toContain("\n"); + expect(roundTripSchema.report).toContain("{{sourceA|marked text}}"); + // Source should be preserved expect(roundTripSchema.sources).toHaveLength(1); - expect((roundTripSchema.sources[0] as any).props.targetText).toBe("marked text"); + expect((roundTripSchema.sources[0] as any).props.targetText).toBe( + "marked text" + ); }); it("Should handle source annotations across paragraph boundaries correctly", async () => { @@ -737,14 +777,248 @@ describe("EditorParseService", () => { ); // Verify structure with line breaks - expect(schemaResult.summary).toBe("Start of summary with {{imp-source|important source}}\nSecond paragraph without sources.\nThird paragraph with {{another-source|another source}} at end."); - + expect(schemaResult.summary).toBe( + "Start of summary with {{imp-source|important source}}\nSecond paragraph without sources.\nThird paragraph with {{another-source|another source}} at end." + ); + // Should have exactly 2 sources expect(schemaResult.sources).toHaveLength(2); - + // Both sources should be properly indexed - expect((schemaResult.sources[0] as any).props.targetText).toBe("important source"); - expect((schemaResult.sources[1] as any).props.targetText).toBe("another source"); + expect((schemaResult.sources[0] as any).props.targetText).toBe( + "important source" + ); + expect((schemaResult.sources[1] as any).props.targetText).toBe( + "another source" + ); + }); + }); + + describe("removeTrailingParagraph()", () => { + /** + * Test: Trailing Paragraph Removal - Core Functionality + * + * Purpose: Validates removal of trailing empty paragraphs added by Remirror's + * TrailingNodeExtension before content is persisted to the database. + * + * Business Context: + * Remirror's TrailingNodeExtension appends an empty paragraph at the end of + * documents to allow users to insert new nodes. This paragraph is a UX concern + * and should not be stored in the database, as it causes conflicts when the + * editor reloads content (duplicate trailing nodes, insertion issues). + * + * The removeTrailingParagraph method is used in two paths: + * 1. Write path (frontend): Cleans content before persisting via XState actions + * 2. Read path (backend): Cleans legacy data when serving editor content via API + */ + + it("Should remove a trailing paragraph from editor content", () => { + const editorWithTrailing: RemirrorJSON = { + type: "doc", + content: [ + { + type: "questions", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Question 1" }], + }, + ], + }, + { + type: "summary", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "Summary text" }, + ], + }, + ], + }, + { + type: "paragraph", + }, + ], + }; + + const result = + editorParseService.removeTrailingParagraph(editorWithTrailing); + + expect(result.content).toHaveLength(2); + expect(result.content[0].type).toBe("questions"); + expect(result.content[1].type).toBe("summary"); + }); + + it("Should not modify content when the last node is not a paragraph", () => { + const editorWithoutTrailing: RemirrorJSON = { + type: "doc", + content: [ + { + type: "questions", + content: [ + { + type: "paragraph", + content: [{ type: "text", text: "Question 1" }], + }, + ], + }, + { + type: "verification", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "Verification" }, + ], + }, + ], + }, + ], + }; + + const result = editorParseService.removeTrailingParagraph( + editorWithoutTrailing + ); + + expect(result.content).toHaveLength(2); + expect(result.content[0].type).toBe("questions"); + expect(result.content[1].type).toBe("verification"); + }); + + it("Should handle null or undefined input gracefully", () => { + expect(editorParseService.removeTrailingParagraph(null)).toBeNull(); + expect( + editorParseService.removeTrailingParagraph(undefined) + ).toBeUndefined(); + }); + + it("Should handle content with missing or non-array content field", () => { + const noContent: RemirrorJSON = { type: "doc" }; + const result = + editorParseService.removeTrailingParagraph(noContent); + expect(result).toEqual({ type: "doc" }); + }); + + it("Should not mutate the original input object", () => { + const original: RemirrorJSON = { + type: "doc", + content: [ + { type: "summary", content: [] }, + { type: "paragraph" }, + ], + }; + + const originalLength = original.content.length; + editorParseService.removeTrailingParagraph(original); + + expect(original.content).toHaveLength(originalLength); + }); + + it("Should handle a document with only a trailing paragraph", () => { + const onlyParagraph: RemirrorJSON = { + type: "doc", + content: [{ type: "paragraph" }], + }; + + const result = + editorParseService.removeTrailingParagraph(onlyParagraph); + + expect(result.content).toHaveLength(0); + }); + + it("Should only remove the last paragraph, not inner paragraphs within sections", () => { + const editorWithInnerParagraphs: RemirrorJSON = { + type: "doc", + content: [ + { + type: "report", + content: [ + { + type: "paragraph", + content: [ + { type: "text", text: "Report line 1" }, + ], + }, + { + type: "paragraph", + content: [ + { type: "text", text: "Report line 2" }, + ], + }, + ], + }, + { + type: "paragraph", + }, + ], + }; + + const result = editorParseService.removeTrailingParagraph( + editorWithInnerParagraphs + ); + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe("report"); + expect(result.content[0].content).toHaveLength(2); + }); + }); + + describe("removeTrailingParagraph() integration with read path", () => { + /** + * Test: End-to-End Read Path - Legacy Data Cleanup + * + * Purpose: Validates that editor content with a trailing paragraph + * (as stored in legacy database records) is cleaned when converted + * back through the schema2editor pipeline. + * + * This simulates the backend read path: + * DB (legacy data with trailing

) → schema2editor → removeTrailingParagraph → frontend + */ + + it("Should produce clean editor content from schema round-trip", async () => { + const schema: ReviewTaskMachineContextReviewData = { + sources: [], + questions: ["What is the claim?"], + summary: "Summary of the report", + report: "Report content here", + verification: "Verification details", + }; + + const editorResult = await editorParseService.schema2editor(schema); + const cleaned = + editorParseService.removeTrailingParagraph(editorResult); + + cleaned.content.forEach((node) => { + expect([ + "questions", + "summary", + "report", + "verification", + ]).toContain(node.type); + }); + }); + + it("Should preserve content integrity after trailing paragraph removal", async () => { + const schema: ReviewTaskMachineContextReviewData = { + sources: [], + questions: ["Question A", "Question B"], + summary: "A summary", + report: "Report content here", + verification: "Verified", + }; + + const editorResult = await editorParseService.schema2editor(schema); + const cleaned = + editorParseService.removeTrailingParagraph(editorResult); + const roundTripSchema = await editorParseService.editor2schema( + cleaned + ); + + expect(roundTripSchema.questions).toEqual(schema.questions); + expect(roundTripSchema.summary).toEqual(schema.summary); + expect(roundTripSchema.verification).toEqual(schema.verification); + expect(roundTripSchema.report).toEqual(schema.report); }); }); }); diff --git a/server/editor/schema/editor.schema.ts b/server/editor/schema/editor.schema.ts index fe8e245f6..83ee3309d 100644 --- a/server/editor/schema/editor.schema.ts +++ b/server/editor/schema/editor.schema.ts @@ -18,7 +18,7 @@ export type EditorContentObjectType = { }[]; }[]; }; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Editor { @Prop({ required: true, diff --git a/server/entities/m2m.entity.ts b/server/entities/m2m.entity.ts index cc7a62403..bc2afb427 100644 --- a/server/entities/m2m.entity.ts +++ b/server/entities/m2m.entity.ts @@ -1,7 +1,11 @@ +import { Roles } from "../auth/ability/ability.factory"; export class M2M { - isM2M: boolean; - role: { - main: string; - }; - scopes: string[]; + isM2M: boolean; + clientId: string; + subject: string; + scopes: string[]; + role: { + main: Roles.Integration; + }; + namespace: string; } diff --git a/server/filters/http-exception.filter.ts b/server/filters/http-exception.filter.ts new file mode 100644 index 000000000..fe81202ff --- /dev/null +++ b/server/filters/http-exception.filter.ts @@ -0,0 +1,80 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from "@nestjs/common"; +import { Request, Response } from "express"; +import { randomUUID } from "crypto"; + +@Catch() +export class AllExceptionsFilter implements ExceptionFilter { + private readonly logger = new Logger(AllExceptionsFilter.name); + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : exception instanceof Error + ? exception.message + : "Internal server error"; + + const requestId = + (request as any).requestId || + request.headers["x-request-id"] || + randomUUID(); + + const errorContext = { + requestId, + method: request.method, + url: request.url, + ip: request.ip || request.socket?.remoteAddress, + statusCode: status, + }; + + // Handle "headers already sent" error + if ( + exception instanceof Error && + exception.message.includes("Cannot set headers after they are sent") + ) { + this.logger.error( + `Headers already sent - ${request.method} ${request.url} | RequestId: ${requestId}`, + exception.stack + ); + return; + } + + // Log the error with context + const errorMessage = + typeof message === "string" ? message : JSON.stringify(message); + this.logger.error( + `${request.method} ${request.url} | Status: ${status} | RequestId: ${requestId} | Error: ${errorMessage}`, + exception instanceof Error ? exception.stack : "" + ); + + // Send error response only if headers haven't been sent + if (!response.headersSent) { + response.status(status).json({ + requestId, + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message: + status === HttpStatus.INTERNAL_SERVER_ERROR + ? "Internal server error" + : message, + }); + } + } +} diff --git a/server/group/group.service.ts b/server/group/group.service.ts index 0095dfb55..7dd278065 100644 --- a/server/group/group.service.ts +++ b/server/group/group.service.ts @@ -1,10 +1,12 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { Model, Types } from "mongoose"; import { InjectModel } from "@nestjs/mongoose"; import { Group, GroupDocument } from "./schemas/group.schema"; @Injectable() export class GroupService { + private readonly logger = new Logger(GroupService.name); + constructor( @InjectModel(Group.name) private GroupModel: Model @@ -51,7 +53,7 @@ export class GroupService { return await new this.GroupModel(group).save(); } catch (error) { - console.error("Failed to create or update group:", error); + this.logger.error("Failed to create or update group:", error); throw error; } } @@ -107,7 +109,7 @@ export class GroupService { ); } } catch (error) { - console.error("Failed to remove content:", error); + this.logger.error("Failed to remove content:", error); throw error; } } diff --git a/server/group/schemas/group.schema.ts b/server/group/schemas/group.schema.ts index fc4820cd3..64c8ea2f4 100644 --- a/server/group/schemas/group.schema.ts +++ b/server/group/schemas/group.schema.ts @@ -4,7 +4,7 @@ import { VerificationRequest } from "../../verification-request/schemas/verifica export type GroupDocument = Group & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Group { @Prop({ type: [ diff --git a/server/history/history.controller.spec.ts b/server/history/history.controller.spec.ts new file mode 100644 index 000000000..e9106ff01 --- /dev/null +++ b/server/history/history.controller.spec.ts @@ -0,0 +1,56 @@ +import { Test } from "@nestjs/testing"; +import { HistoryController } from "./history.controller"; +import { HistoryService } from "./history.service"; +import { historyServiceMock, mockHistoryItem } from "../mocks/HistoryMock"; +import { TargetModel } from "./schema/history.schema"; + +describe("HistoryController (Unit)", () => { + let controller: HistoryController; + let historyService: typeof historyServiceMock; + + beforeEach(async () => { + const testingModule = await Test.createTestingModule({ + controllers: [HistoryController], + providers: [{ provide: HistoryService, useValue: historyServiceMock }], + }).compile(); + + controller = testingModule.get(HistoryController); + historyService = testingModule.get(HistoryService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("getHistory", () => { + it("should return history correctly (happy path)", async () => { + historyService.getHistoryForTarget.mockResolvedValue({ + history: [mockHistoryItem], + totalChanges: 1, + totalPages: 1, + page: 1, + pageSize: 10, + }); + + const response = await controller.getHistory( + { targetId: "id", targetModel: TargetModel.Claim }, + {} + ); + expect(response.history.length).toBeGreaterThan(0); + expect(response.totalChanges).toBeGreaterThanOrEqual(1); + }); + + it("should throw error if targetId is empty", async () => { + await expect( + controller.getHistory({ targetId: "", targetModel: TargetModel.Claim }, {}) + ).rejects.toThrow(); + }); + + it("should throw error if service fails", async () => { + historyService.getHistoryForTarget.mockRejectedValue(new Error("fail")); + await expect( + controller.getHistory({ targetId: "id", targetModel: TargetModel.Claim }, {}) + ).rejects.toThrow("fail"); + }); + }); +}); diff --git a/server/history/history.controller.ts b/server/history/history.controller.ts index 0f1e5aab9..e0265b8a1 100644 --- a/server/history/history.controller.ts +++ b/server/history/history.controller.ts @@ -1,34 +1,43 @@ import { Controller, Get, Logger, Param, Query } from "@nestjs/common"; import { HistoryService } from "./history.service"; import { ApiTags } from "@nestjs/swagger"; +import type { + HistoryParams, + HistoryQuery, + HistoryResponse, +} from "./types/history.interfaces"; -@Controller() +@ApiTags("history") +@Controller("api/history") export class HistoryController { - private readonly logger = new Logger("HistoryController"); - constructor(private historyService: HistoryService) {} + private readonly logger = new Logger(HistoryController.name); + constructor(private historyService: HistoryService) { } - @ApiTags("history") - @Get("api/history/:targetModel/:targetId") - public async getHistory(@Param() param, @Query() getHistory: any) { + @Get(":targetModel/:targetId") + public async getHistory( + @Param() param: HistoryParams, + @Query() query: HistoryQuery + ): Promise { const { targetId, targetModel } = param; - const { page, order } = getHistory; - const pageSize = parseInt(getHistory.pageSize, 10); - return this.historyService - .getByTargetIdModelAndType( + + if (!targetId || !targetModel) { + throw new Error("targetId and targetModel are required"); + } + + try { + const response = await this.historyService.getHistoryForTarget( targetId, targetModel, - page, - pageSize, - order - ) - .then((history) => { - const totalChanges = history.length; - const totalPages = Math.ceil(totalChanges / pageSize); - this.logger.log( - `Found ${totalChanges} changes for targetId ${targetId}. Page ${page} of ${totalPages}` - ); - return { history, totalChanges, totalPages, page, pageSize }; - }) - .catch(); + query + ); + this.logger.log( + `Found ${response.totalChanges} changes for targetId ${targetId}. Page ${response.page} of ${response.totalPages}` + ); + + return response; + } catch (err) { + this.logger.error(`Error fetching history: ${err.message}`); + throw err; + } } } diff --git a/server/history/history.service.spec.ts b/server/history/history.service.spec.ts new file mode 100644 index 000000000..84d6bc58e --- /dev/null +++ b/server/history/history.service.spec.ts @@ -0,0 +1,158 @@ +import { getModelToken } from "@nestjs/mongoose"; +import { HistoryService } from "./history.service"; +import { Test, TestingModule } from "@nestjs/testing"; +import { Model } from "mongoose"; +import { + mockHistoryResponse, + mockAggregateMongoResult, + mockHistoryItem, + mockHistoryModel, +} from "../mocks/HistoryMock"; +import { TargetModel, HistoryType } from "./schema/history.schema"; +import { HistoryDocument } from "./schema/history.schema"; + +describe("HistoryService (Unit)", () => { + let service: HistoryService; + let testingModule: TestingModule; + let model: Model; + + beforeAll(async () => { + testingModule = await Test.createTestingModule({ + providers: [ + HistoryService, + { provide: getModelToken("History"), useValue: mockHistoryModel }, + ], + }).compile(); + + model = testingModule.get(getModelToken("History")); + service = testingModule.get(HistoryService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe("HistoryService.createHistory", () => { + it("should create a new history document and call save", async () => { + const result = await service.createHistory(mockHistoryItem); + + const instance = (mockHistoryModel as jest.Mock).mock.instances[0]; + + expect(instance.save).toHaveBeenCalled(); + expect(result._id).toBe("abc"); + }); + + it("should throw if save fails", async () => { + const error = new Error("Database error"); + + (mockHistoryModel as jest.Mock).mockImplementationOnce(function () { + return { + save: jest.fn().mockRejectedValue(error), + }; + }); + + await expect(service.createHistory(mockHistoryItem)).rejects.toThrow( + "Database error" + ); + }); + }); + + describe("HistoryService.getHistoryParams", () => { + it("should throw for invalid dataId", () => { + const invalidId = "not-a-valid-id"; + expect(() => + service.getHistoryParams( + invalidId, + TargetModel.Claim, + null, + HistoryType.Create, + { some: "value" } + ) + ).toThrow(`Invalid dataId received: ${invalidId}`); + }); + + it("should return correct params for valid dataId", () => { + const validId = mockHistoryItem.targetId.toString(); + const params = service.getHistoryParams( + validId, + TargetModel.Claim, + null, + HistoryType.Update, + { some: "latest" }, + { some: "previous" } + ); + + expect(params).toHaveProperty("targetId"); + expect(params.targetModel).toBe(TargetModel.Claim); + expect(params.details.after).toEqual({ some: "latest" }); + expect(params.details.before).toEqual({ some: "previous" }); + }); + }); + + describe("HistoryService.getDescriptionForHide", () => { + it("should return empty string when content missing or not hidden", async () => { + const responseWithoutContent = await service.getDescriptionForHide( + {}, + TargetModel.Claim + ); + expect(responseWithoutContent).toBe(""); + + const responseHidden = await service.getDescriptionForHide( + { content: mockHistoryItem._id, isHidden: false }, + TargetModel.Claim + ); + expect(responseHidden).toBe(""); + }); + + it("should return description from history when hidden", async () => { + jest + .spyOn(service, "getHistoryForTarget") + .mockResolvedValue(mockHistoryResponse); + + const response = await service.getDescriptionForHide( + { _id: mockHistoryItem._id, isHidden: true }, + TargetModel.Claim + ); + expect(response).toBe( + mockHistoryResponse.history[0].details.after.description + ); + }); + }); + + describe("HistoryService.getHistoryForTarget", () => { + it("should return paginated history and counts", async () => { + mockHistoryModel.aggregate.mockResolvedValueOnce( + mockAggregateMongoResult + ); + + const res = await service.getHistoryForTarget( + mockHistoryItem.targetId.toString(), + TargetModel.Claim, + { page: 0, pageSize: 10, order: "asc" } + ); + + expect(res.history).toEqual([mockHistoryItem]); + expect(res.totalChanges).toBe(1); + expect(res.totalPages).toBe(1); + expect(res.page).toBe(0); + expect(res.pageSize).toBe(10); + }); + + it("should handle empty aggregate result", async () => { + (mockHistoryModel.aggregate as jest.Mock).mockResolvedValue( + [{ data: [], totalCount: [] }] + ); + + const res = await service.getHistoryForTarget( + mockHistoryItem.targetId.toString(), + TargetModel.Claim, + { page: 0, pageSize: 10, order: "asc" } + ); + + expect(res.history).toEqual([]); + expect(res.totalChanges).toBe(0); + expect(res.totalPages).toBe(0); + }); + }); +}); diff --git a/server/history/history.service.ts b/server/history/history.service.ts index 96fdacc3f..8bf051cf6 100644 --- a/server/history/history.service.ts +++ b/server/history/history.service.ts @@ -1,7 +1,16 @@ import { Injectable, Logger } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; -import { Model, Types } from "mongoose"; -import { History, HistoryDocument, HistoryType } from "./schema/history.schema"; +import { Model, Types, isValidObjectId } from "mongoose"; +import { History, HistoryDocument, HistoryType, TargetModel } from "./schema/history.schema"; +import { + AfterAndBeforeType, + HEX24, + HistoryItem, + HistoryQuery, + HistoryResponse, + IHideableContent, + PerformedBy, +} from "./types/history.interfaces"; @Injectable() export class HistoryService { @@ -22,25 +31,44 @@ export class HistoryService { * This function return an history object. * @param dataId Target Id. * @param targetModel The model of the target(claim or personality ). - * @param user User who made the change. + * @param performedBy The actor who performed the change. Can be: + * - an array of internal user object IDs + * - a string of the internal user id + * - a string representing chatbot ID + * - a Machine-to-Machine (M2M) object + * - null if unknown or invalid * @param type Type of the change(create, personality or delete). * @param latestChange Model latest change . * @param previousChange Model previous change. * @returns Returns an object with de params necessary to create an history. */ getHistoryParams( - dataId, - targetModel, - user, - type, - latestChange, - previousChange = null + dataId: string, + targetModel: TargetModel, + performedBy: PerformedBy, + type: HistoryType, + latestChange: AfterAndBeforeType, + previousChange?: AfterAndBeforeType ) { + if (!isValidObjectId(dataId)) { + throw new Error(`Invalid dataId received: ${dataId}`); + } + const date = new Date(); + const targetId = Types.ObjectId(dataId); + let currentPerformedBy = null; + + if (typeof performedBy === "string" && HEX24.test(performedBy)) { + currentPerformedBy = Types.ObjectId(performedBy); + } else { + currentPerformedBy = performedBy; + } + + return { - targetId: Types.ObjectId(dataId), + targetId: targetId, targetModel, - user: user?._id || user || null, //I need to make it optional because we still need to do M2M for chatbot + user: currentPerformedBy, type, details: { after: latestChange, @@ -55,62 +83,107 @@ export class HistoryService { * @param data Object with the history data * @returns Returns a new history document to database */ - async createHistory(data) { + async createHistory( + data: Partial + ): Promise { const newHistory = new this.HistoryModel(data); return newHistory.save(); } - /** - * This function queries the database for the history of changes on a target. - * @param targetId The id of the target. - * @param targetModel The model of the target (claim or personality). - * @param page The page of results, used in combination with pageSize to paginate results. - * @param pageSize How many results per page. - * @param order asc or desc. - * @returns The paginated history of a target. - */ - async getByTargetIdModelAndType( - targetId, - targetModel, - page, - pageSize, - order = "asc", - type = "" - ) { - let query; - if (type) { - query = { - targetId: Types.ObjectId(targetId), - targetModel, - type, - }; - } else { - query = { - targetId: Types.ObjectId(targetId), - targetModel, - }; + async getHistoryForTarget( + targetId: string, + targetModel: TargetModel, + query: HistoryQuery + ): Promise { + const page = Math.max(Number(query.page) || 0, 0); + const pageSize = Math.max(Number(query.pageSize) || 10, 1); + const order = query.order === "desc" ? -1 : 1; + + const mongoQuery: HistoryItem = { + targetId: Types.ObjectId(targetId), + targetModel, + }; + if (query.type && query.type.length > 0) { + mongoQuery.type = { $in: query.type }; } - return this.HistoryModel.find(query) - .populate("user", "_id name") - .skip(page * pageSize) - .limit(pageSize) - .sort({ date: order }); + + const result = await this.HistoryModel.aggregate([ + { $match: mongoQuery }, + { + $facet: { + data: [ + { $sort: { date: order } }, + { $skip: page * pageSize }, + { $limit: pageSize }, + ...this.getUserLookupStages(), + ], + totalCount: [{ $count: "total" }], + }, + }, + ]); + + const totalChanges = result[0]?.totalCount[0]?.total || 0; + + return { + history: result[0]?.data || [], + totalChanges, + totalPages: Math.ceil(totalChanges / pageSize), + page, + pageSize, + }; + } + + private getUserLookupStages() { + return [ + { + $lookup: { + from: "users", + let: { userId: "$user" }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: [{ $type: "$$userId" }, "objectId"] }, + { $eq: ["$_id", "$$userId"] } + ] + } + } + }, + { $project: { _id: 1, name: 1 } }, + ], + as: "userLookup", + }, + }, + { + $addFields: { + user: { + $cond: [ + { $gt: [{ $size: "$userLookup" }, 0] }, + { $arrayElemAt: ["$userLookup", 0] }, + "$user", + ], + }, + }, + }, + { $project: { userLookup: 0 } }, + ]; } - async getDescriptionForHide(content, target) { - if (content?.isHidden) { - const history = await this.getByTargetIdModelAndType( - content._id, - target, - 0, - 1, - "desc", - HistoryType.Hide - ); - - return history[0]?.details?.after?.description; + async getDescriptionForHide( + content: IHideableContent, + target: TargetModel + ) { + if (!content?._id || !content?.isHidden) { + return ""; } + const { history } = await this.getHistoryForTarget(content._id, target, { + page: 0, + pageSize: 1, + order: "desc", + type: [HistoryType.Hide], + }); - return ""; + return history[0]?.details?.after?.description || ""; } } diff --git a/server/history/schema/history.schema.ts b/server/history/schema/history.schema.ts index 43ba70643..ecde00392 100644 --- a/server/history/schema/history.schema.ts +++ b/server/history/schema/history.schema.ts @@ -1,6 +1,7 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import * as mongoose from "mongoose"; import { User } from "../../users/schemas/user.schema"; +import { M2M } from "../../entities/m2m.entity"; export type HistoryDocument = History & mongoose.Document; @@ -32,7 +33,7 @@ export type Details = { before: any; }; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class History { @Prop({ type: mongoose.Types.ObjectId, @@ -47,11 +48,10 @@ export class History { targetModel: TargetModel; @Prop({ - type: mongoose.Types.ObjectId, + type: mongoose.Schema.Types.Mixed, required: false, - ref: "User", }) - user: User; + user: User | M2M | string; @Prop({ required: true, @@ -68,7 +68,7 @@ export class History { type: Date, required: true, }) - date: mongoose.Date; + date: Date; } export const HistorySchema = SchemaFactory.createForClass(History); diff --git a/server/history/types/history.interfaces.ts b/server/history/types/history.interfaces.ts new file mode 100644 index 000000000..80d291b8a --- /dev/null +++ b/server/history/types/history.interfaces.ts @@ -0,0 +1,55 @@ +import { M2M } from "../../entities/m2m.entity"; +import { HistoryType, TargetModel } from "../schema/history.schema"; +import { Types } from "mongoose"; + +export const HEX24 = /^[0-9a-fA-F]{24}$/; + +interface HistoryParams { + targetId: string; + targetModel: TargetModel; +} + +interface HistoryQuery { + page?: number; + pageSize?: number; + order?: "asc" | "desc"; + type?: HistoryType[]; +} +interface HistoryResponse { + history: HistoryItem[]; + totalChanges: number; + totalPages: number; + page: number; + pageSize: number; +} +interface HistoryItem { + _id?: string; + targetId: Types.ObjectId; + targetModel: TargetModel; + user?: PerformedBy; + type?: HistoryType | { $in: HistoryType[] }; + details?: HistoryDetails; + date?: Date | string; +} + +type PerformedBy = Types.ObjectId[] | M2M | string | null; +interface HistoryDetails { + after: AfterAndBeforeType; + before?: AfterAndBeforeType | null; +} +type AfterAndBeforeType = Record; +interface IHideableContent { + _id?: string; + isHidden?: boolean; + [key: string]: any; +} + +export type { + HistoryParams, + HistoryQuery, + HistoryResponse, + HistoryItem, + PerformedBy, + AfterAndBeforeType, + IHideableContent +}; diff --git a/server/home/home.controller.ts b/server/home/home.controller.ts index 66fb51d0a..245ba1e8e 100644 --- a/server/home/home.controller.ts +++ b/server/home/home.controller.ts @@ -11,7 +11,7 @@ import { ViewService } from "../view/view.service"; import type { Response } from "express"; import { parse } from "url"; import { StatsService } from "../stats/stats.service"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; import type { BaseRequest } from "../types"; import { DebateService } from "../claim/types/debate/debate.service"; import { ClaimRevisionService } from "../claim/claim-revision/claim-revision.service"; @@ -39,7 +39,7 @@ export class HomeController { return res.redirect("/"); } - @IsPublic() + @Public() @ApiTags("pages") @Get("/:namespace?") @Header("Cache-Control", "max-age=60, must-revalidate") diff --git a/server/main.ts b/server/main.ts index a82a6520b..adfa0d7d3 100644 --- a/server/main.ts +++ b/server/main.ts @@ -7,6 +7,7 @@ import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import loadConfig from "./configLoader"; import * as dotenv from "dotenv"; import { WinstonLogger } from "./winstonLogger"; +import { AllExceptionsFilter } from "./filters/http-exception.filter"; const cookieParser = require("cookie-parser"); const mongoose = require("mongoose"); dotenv.config(); @@ -23,11 +24,40 @@ async function initApp() { origin: options?.cors || "*", credentials: true, methods: "GET,HEAD,PUT,PATCH,POST,DELETE, OPTIONS", - allowedHeaders: ["accept", "x-requested-with", "content-type"], + allowedHeaders: [ + "accept", + "x-requested-with", + "content-type", + "x-request-id", + ], + exposedHeaders: ["x-request-id"], }; const logger = new WinstonLogger(); + // Handle uncaught exceptions + process.on("uncaughtException", (error: Error) => { + logger.error( + `Uncaught Exception: ${error.message}`, + error.stack, + "UncaughtException" + ); + setTimeout(() => process.exit(1), 1000); + }); + + // Handle unhandled promise rejections + process.on("unhandledRejection", (reason: any) => { + const errorMessage = + reason instanceof Error + ? `${reason.message}\n${reason.stack}` + : String(reason); + logger.error( + `Unhandled Rejection: ${errorMessage}`, + "", + "UnhandledRejection" + ); + }); + const app = await NestFactory.create( AppModule.register(options), { @@ -58,6 +88,9 @@ async function initApp() { }) ); + // Global exception filter for consistent error handling and logging + app.useGlobalFilters(new AllExceptionsFilter()); + // FIXME: not working but we need to enable in the future // app.use(helmet()); app.use(cookieParser()); diff --git a/server/middleware/logger.middleware.ts b/server/middleware/logger.middleware.ts index ea30f740b..d20ed09af 100644 --- a/server/middleware/logger.middleware.ts +++ b/server/middleware/logger.middleware.ts @@ -1,24 +1,45 @@ -import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; -import { Request, Response, NextFunction } from 'express'; +import { Injectable, Logger, NestMiddleware } from "@nestjs/common"; +import { Request, Response, NextFunction } from "express"; +import { randomUUID } from "crypto"; + +declare global { + namespace Express { + interface Request { + requestId?: string; + startTime?: number; + } + } +} @Injectable() export class LoggerMiddleware implements NestMiddleware { - private logger = new Logger('HTTP'); + private logger = new Logger("HTTP"); + + use(request: Request, response: Response, next: NextFunction): void { + const startTime = Date.now(); + const { ip, method, originalUrl } = request; + const userAgent = request.get("user-agent") || ""; - use(request: Request, response: Response, next: NextFunction): void { - const { ip, method, originalUrl } = request; - const userAgent = request.get('user-agent') || ''; + // Generate or use existing request ID + const requestId = + (request.headers["x-request-id"] as string) || randomUUID(); - response.on('finish', () => { - const { statusCode } = response; - const contentLength = response.get('content-length'); + // Attach to request for use in exception filters and services + request.requestId = requestId; + request.startTime = startTime; + request.headers["x-request-id"] = requestId; + response.setHeader("x-request-id", requestId); - this.logger.log( - `${method} ${originalUrl} ${statusCode} ${contentLength} - ${userAgent} ${ip}`, - ); - }); + response.on("finish", () => { + const { statusCode } = response; + const contentLength = response.get("content-length") || 0; + const responseTime = Date.now() - startTime; - next(); - } -} + this.logger.log( + `${method} ${originalUrl} ${statusCode} ${contentLength} ${responseTime}ms - ${userAgent} ${ip}` + ); + }); + next(); + } +} diff --git a/server/mocks/ClaimMock.ts b/server/mocks/ClaimMock.ts new file mode 100644 index 000000000..0b5fc647b --- /dev/null +++ b/server/mocks/ClaimMock.ts @@ -0,0 +1,40 @@ +import { jest } from "@jest/globals"; +import type { ClaimDocument } from "../claim/schemas/claim.schema"; +import type { ImageDocument } from "../claim/types/image/schemas/image.schema"; +import type { PersonalityDocument } from "../personality/mongo/schemas/personality.schema"; + +/** + * Mock factory for ClaimService matching actual service interface + */ +export const mockClaimService = () => ({ + getByPersonalityIdAndClaimSlug: + jest.fn<() => Promise>>(), + getById: jest.fn<() => Promise>>(), + create: jest.fn<() => Promise>>(), + update: jest.fn<() => Promise>>(), + delete: jest.fn<() => Promise>(), + listAll: jest.fn<() => Promise[]>>(), + count: jest.fn<() => Promise>(), +}); + +/** + * Mock factory for ImageService matching actual service interface + */ +export const mockImageService = () => ({ + getByDataHash: jest.fn<() => Promise>>(), + create: jest.fn<() => Promise>>(), + updateImageWithTopics: jest.fn<() => Promise>>(), +}); + +/** + * Mock factory for PersonalityService matching actual service interface + */ +export const mockPersonalityService = () => ({ + getPersonalityBySlug: + jest.fn<() => Promise>>(), + listAll: jest.fn<() => Promise[]>>(), + getById: jest.fn<() => Promise>>(), + create: jest.fn<() => Promise>>(), + update: jest.fn<() => Promise>>(), + delete: jest.fn<() => Promise>(), +}); diff --git a/server/mocks/HistoryMock.ts b/server/mocks/HistoryMock.ts new file mode 100644 index 000000000..1d6cac3bf --- /dev/null +++ b/server/mocks/HistoryMock.ts @@ -0,0 +1,55 @@ +import { VerificationRequestStatus } from "../verification-request/dto/types"; +import { HistoryType, TargetModel } from "../history/schema/history.schema"; +import { Types } from "mongoose"; + +export const historyServiceMock = { + getHistoryForTarget: jest.fn(), +}; + +export const mockHistoryItem = { + _id: "23432", + targetId: new Types.ObjectId(), + targetModel: TargetModel.VerificationRequest, + type: HistoryType.Update, + date: new Date(), + details: { + after: { + description: "new", + status: VerificationRequestStatus.PRE_TRIAGE, + }, + before: { + description: "old", + status: VerificationRequestStatus.IN_TRIAGE, + }, + }, +}; + +export const mockHistoryResponse = { + history: [mockHistoryItem], + totalChanges: 1, + totalPages: 1, + page: 0, + pageSize: 10, +}; + +export const mockAggregateMongoResult = [ + { + data: [mockHistoryItem], + totalCount: [{ total: 1 }], + }, +]; + +type MockedModel = jest.Mock & { + aggregate: jest.Mock; +}; + +export const mockHistoryModel = jest.fn() as MockedModel; + +mockHistoryModel.mockImplementation(function (this: any, data) { + this.save = jest.fn().mockResolvedValue({ + ...mockHistoryItem, + _id: "abc", + }); +}); + +mockHistoryModel.aggregate = jest.fn(); diff --git a/server/mocks/TrackingMock.ts b/server/mocks/TrackingMock.ts new file mode 100644 index 000000000..dae8f331f --- /dev/null +++ b/server/mocks/TrackingMock.ts @@ -0,0 +1,22 @@ +import { TrackingResponseDTO } from "../tracking/types/tracking.interfaces"; +import { VerificationRequestStatus } from "../verification-request/dto/types"; + +export const mockTrackingService = { + getTrackingStatus: jest.fn(), +}; + +export const mockResponse: TrackingResponseDTO = { + currentStatus: VerificationRequestStatus.IN_TRIAGE, + historyEvents: [ + { + id: "history-1", + status: VerificationRequestStatus.PRE_TRIAGE, + date: new Date("2024-01-01T10:00:00Z"), + }, + { + id: "history-2", + status: VerificationRequestStatus.IN_TRIAGE, + date: new Date("2024-01-01T11:00:00Z"), + }, + ], + }; \ No newline at end of file diff --git a/server/mocks/VerificationRequestMock.ts b/server/mocks/VerificationRequestMock.ts new file mode 100644 index 000000000..fbcce3dca --- /dev/null +++ b/server/mocks/VerificationRequestMock.ts @@ -0,0 +1,26 @@ +import { VerificationRequestDocument } from "../verification-request/schemas/verification-request.schema"; + +export const createFakeVerificationRequest = ( + overrides?: Partial +) => ({ + _id: "60c72b2f9f1b2c3d4e5f6a7b", + status: "In Triage", + sourceChannel: "Whatsapp", + data_hash: "AABBCCDD1122334455667788", + updatedAt: new Date(), + ...overrides, +}); + +export const mockQuery = { + sort: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + select: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn(), +}; + +export const mockVerificationRequestModel = { + find: jest.fn().mockReturnValue(mockQuery), + aggregate: jest.fn().mockReturnThis(), + getById: jest.fn(), +}; diff --git a/server/notifications/notifications.service.ts b/server/notifications/notifications.service.ts index 171642803..4f7fe2d2d 100644 --- a/server/notifications/notifications.service.ts +++ b/server/notifications/notifications.service.ts @@ -1,10 +1,12 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger } from "@nestjs/common"; import { Novu, TriggerRecipientsTypeEnum } from "@novu/node"; import { InjectNovu } from "./novu.provider"; import { createHmac } from "crypto"; import { ConfigService } from "@nestjs/config"; @Injectable() export class NotificationService { + private readonly logger = new Logger(NotificationService.name); + constructor( @InjectNovu() private readonly novu: Novu, @@ -90,7 +92,7 @@ export class NotificationService { const result = await this.novu.topics.get(key); return result.data; } catch (e) { - console.log(e); + this.logger.error("Failed to get topic:", e); return false; } } diff --git a/server/personality/mongo/personality.service.ts b/server/personality/mongo/personality.service.ts index c0211f539..076096baa 100644 --- a/server/personality/mongo/personality.service.ts +++ b/server/personality/mongo/personality.service.ts @@ -44,8 +44,12 @@ export class MongoPersonalityService { private readonly historyService: HistoryService ) {} - async getWikidataEntities(regex, language) { - return await this.wikidata.queryWikibaseEntities(regex, language); + async getWikidataEntities(regex: string, language: string) { + return await this.wikidata.queryWikibaseEntities( + regex, + language, + false + ); } async getWikidataList(regex, language) { const wbentities = await this.getWikidataEntities(regex, language); @@ -105,7 +109,7 @@ export class MongoPersonalityService { ); } - return await Promise.all( + const processedPersonalities = await Promise.all( personalities.map(async (personality) => { try { return await this.postProcess(personality, language); @@ -113,9 +117,15 @@ export class MongoPersonalityService { this.logger.log( `It was not possible to do postProcess the personality ${personality}` ); + return null; } }) ); + + return processedPersonalities.filter( + (personalities) => + personalities !== null && personalities !== undefined + ); } /** @@ -143,7 +153,7 @@ export class MongoPersonalityService { `Attempting to create new personality with data ${personality}` ); - const user = this.req.user; + const user = this.req.user?._id; const history = this.history.getHistoryParams( newPersonality._id, @@ -259,7 +269,18 @@ export class MongoPersonalityService { select: "_id title content", }); this.logger.log(`Found personality ${personality?._id}`); - return await this.postProcess(personality.toObject(), query.language); + const processed = await this.postProcess( + personality.toObject(), + query.language + ); + + if (!processed) { + throw new NotFoundException( + `Personality with id ${personalityId} not found or has invalid instance type` + ); + } + + return processed; } async getPersonalityBySlug(query, language = "pt") { @@ -272,7 +293,18 @@ export class MongoPersonalityService { const personality = await this.PersonalityModel.findOne( queryOptions ); - return await this.postProcess(personality.toObject(), language); + const processed = await this.postProcess( + personality.toObject(), + language + ); + + if (!processed) { + throw new NotFoundException( + `Personality not found or has invalid instance type` + ); + } + + return processed; } catch { throw new NotFoundException(); } @@ -307,7 +339,18 @@ export class MongoPersonalityService { }) ); this.logger.log(`Found personality ${personality._id}`); - return await this.postProcess(personality.toObject(), language); + const processed = await this.postProcess( + personality.toObject(), + language + ); + + if (!processed) { + throw new NotFoundException( + `Personality not found or has invalid instance type` + ); + } + + return processed; } catch { throw new NotFoundException(); } @@ -378,7 +421,7 @@ export class MongoPersonalityService { ); this.logger.log(`Updated personality with data ${newPersonality}`); - const user = this.req.user; + const user = this.req.user?._id; const history = this.history.getHistoryParams( personalityId, @@ -407,7 +450,7 @@ export class MongoPersonalityService { const history = this.historyService.getHistoryParams( newPersonality._id, TargetModel.Personality, - this.req?.user, + this.req.user?._id, isHidden ? HistoryType.Hide : HistoryType.Unhide, after, before @@ -428,7 +471,7 @@ export class MongoPersonalityService { * @returns Returns the personality with the param isDeleted equal to true */ async delete(personalityId) { - const user = this.req.user; + const user = this.req.user?._id; const previousPersonality = await this.getById(personalityId); const history = this.history.getHistoryParams( personalityId, @@ -559,13 +602,26 @@ export class MongoPersonalityService { }, ]); - return { - totalRows: personalities[0].totalRows, - processedPersonalities: await Promise.all( - personalities[0].rows.map(async (personality) => { + const processedPersonalities = await Promise.all( + personalities[0].rows.map(async (personality) => { + try { return await this.postProcess(personality, language); - }) - ), + } catch (error) { + this.logger.log( + `It was not possible to do postProcess the personality ${personality._id}` + ); + return null; + } + }) + ); + + const filteredPersonalities = processedPersonalities.filter( + (personality) => personality !== null && personality !== undefined + ); + + return { + totalRows: filteredPersonalities.length, + processedPersonalities: filteredPersonalities, }; } } diff --git a/server/personality/mongo/schemas/personality.schema.ts b/server/personality/mongo/schemas/personality.schema.ts index fa9b9d59f..6d1f3ffd3 100644 --- a/server/personality/mongo/schemas/personality.schema.ts +++ b/server/personality/mongo/schemas/personality.schema.ts @@ -4,7 +4,7 @@ import * as mongoose from "mongoose"; export type PersonalityDocument = Personality & mongoose.Document; -@Schema({ toJSON: { virtuals: true }, toObject: { virtuals: true } }) +@Schema({ toJSON: { virtuals: true }, toObject: { virtuals: true }, timestamps: true }) export class Personality { @Prop({ required: true }) name: string; diff --git a/server/personality/personality.controller.ts b/server/personality/personality.controller.ts index 816efc7a3..6898152cb 100644 --- a/server/personality/personality.controller.ts +++ b/server/personality/personality.controller.ts @@ -12,25 +12,19 @@ import { Query, Req, Res, - UseGuards, } from "@nestjs/common"; import { parse } from "url"; import type { Request, Response } from "express"; import { ViewService } from "../view/view.service"; import { GetPersonalities } from "./dto/get-personalities.dto"; import { CreatePersonalityDTO } from "./dto/create-personality.dto"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public, AdminOnly } from "../auth/decorators/auth.decorator"; import { TargetModel } from "../history/schema/history.schema"; import type { BaseRequest } from "../types"; import { ApiTags } from "@nestjs/swagger"; import { ConfigService } from "@nestjs/config"; import { CaptchaService } from "../captcha/captcha.service"; import { HistoryService } from "../history/history.service"; -import { - AdminUserAbility, - CheckAbilities, -} from "../auth/ability/ability.decorator"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; import type { IPersonalityService } from "../interfaces/personality.service.interface"; @Controller(":namespace?") @@ -45,7 +39,7 @@ export class PersonalityController { private historyService: HistoryService ) {} - @IsPublic() + @Public() @ApiTags("personality") @Get("api/personality") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -70,7 +64,7 @@ export class PersonalityController { } } - @IsPublic() + @Public() @ApiTags("personality") @Get("api/personality/:id") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -88,10 +82,9 @@ export class PersonalityController { return this.personalityService.update(personalityId, body); } + @AdminOnly() @ApiTags("personality") @Put("api/personality/hidden/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async updateHiddenStatus(@Param("id") personalityId, @Body() body) { const validateCaptcha = await this.captchaService.validate( body.recaptcha @@ -107,17 +100,16 @@ export class PersonalityController { ); } + @AdminOnly() @ApiTags("personality") @Delete("api/personality/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new AdminUserAbility()) async delete(@Param("id") personalityId) { return this.personalityService.delete(personalityId).catch((err) => { this.logger.error(err); }); } - @IsPublic() + @Public() @ApiTags("personality") @Get("api/personality/:id/reviews") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -150,7 +142,7 @@ export class PersonalityController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("personality/:slug") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -202,7 +194,7 @@ export class PersonalityController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("personality") @Header("Cache-Control", "max-age=60, must-revalidate") diff --git a/server/report/schemas/report.schema.ts b/server/report/schemas/report.schema.ts index 10ba235a3..db1d48082 100644 --- a/server/report/schemas/report.schema.ts +++ b/server/report/schemas/report.schema.ts @@ -6,7 +6,7 @@ import { ReportModelEnum } from "../../types/enums"; export type ReportDocument = Report & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Report { @Prop({ required: true }) data_hash: string; diff --git a/server/review-task/comment/comment.controller.ts b/server/review-task/comment/comment.controller.ts index daedadddc..4284299ba 100644 --- a/server/review-task/comment/comment.controller.ts +++ b/server/review-task/comment/comment.controller.ts @@ -1,49 +1,43 @@ -import { Body, Controller, Param, Patch, Post, Put, UseGuards } from "@nestjs/common"; +import { Body, Controller, Param, Patch, Post, Put } from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; import { CommentService } from "./comment.service"; -import { CheckAbilities, FactCheckerUserAbility } from "../../auth/ability/ability.decorator"; -import { AbilitiesGuard } from "../../auth/ability/abilities.guard"; +import { FactCheckerOnly } from "../../auth/decorators/auth.decorator"; @Controller() export class CommentController { constructor(private commentService: CommentService) {} + @FactCheckerOnly() @ApiTags("comment") @Post("api/comment") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) create(@Body() body) { return this.commentService.create(body); } + @FactCheckerOnly() @ApiTags("comment") @Patch("api/comment/bulk-update") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) updateMany(@Body() body) { return this.commentService.updateManyComments(body); } + @FactCheckerOnly() @ApiTags("comment") @Put("api/comment/:id") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) update(@Param("id") id, @Body() body) { return this.commentService.update(id, body); } + @FactCheckerOnly() @ApiTags("comment") @Put("api/comment/:id/create-reply") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) createReplyComment(@Param("id") id, @Body() body) { return this.commentService.createReplyComment(id, body); } + @FactCheckerOnly() @ApiTags("comment") @Put("api/comment/:id/delete-reply") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) deleteReplyComment(@Param("id") id, @Body() body) { return this.commentService.deleteReplyComment(id, body.replyCommentId); } diff --git a/server/review-task/comment/schema/comment.schema.ts b/server/review-task/comment/schema/comment.schema.ts index 85645b188..7a528fe36 100644 --- a/server/review-task/comment/schema/comment.schema.ts +++ b/server/review-task/comment/schema/comment.schema.ts @@ -9,7 +9,7 @@ export enum CommentEnum { review = "review", } -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class Comment { @Prop({ type: Number }) from: number; @@ -23,9 +23,6 @@ export class Comment { @Prop({ required: true }) text: string; - @Prop({ required: true, default: Date.now() }) - createdAt: number; - @Prop({ type: mongoose.Types.ObjectId, required: true, diff --git a/server/review-task/review-task.service.ts b/server/review-task/review-task.service.ts index 3db8073b4..c8a361a19 100644 --- a/server/review-task/review-task.service.ts +++ b/server/review-task/review-task.service.ts @@ -1,4 +1,10 @@ -import { ForbiddenException, Inject, Injectable, Scope } from "@nestjs/common"; +import { + ForbiddenException, + Inject, + Injectable, + Scope, + Logger, +} from "@nestjs/common"; import { Model, Types } from "mongoose"; import { ReviewTask, ReviewTaskDocument } from "./schemas/review-task.schema"; import { InjectModel } from "@nestjs/mongoose"; @@ -42,23 +48,24 @@ interface IListAllQuery { interface IPostProcess { data_hash: string; machine: Machine; - target: any; - reviewTaskType: string; + target: any; // TODO: Type this properly as Claim | VerificationRequest + reviewTaskType: ReviewTaskTypeEnum; } export interface IReviewTask { content: Source | Sentence | Image; usersName: string[]; value: string; - personalityName: string; - claimTitle: string; + personalityName?: string; + claimTitle?: string; targetId: string; - personalityId: string; - contentModel: string; + personalityId?: string; + contentModel?: string; } @Injectable({ scope: Scope.REQUEST }) export class ReviewTaskService { + private readonly logger = new Logger(ReviewTaskService.name); fieldMap: { assigned: string; crossChecked: string; reviewed: string }; constructor( @Inject(REQUEST) private req: BaseRequest, @@ -84,8 +91,8 @@ export class ReviewTaskService { const query = getQueryMatchForMachineValue(value); Object.keys(filterUser).forEach((key) => { - const value = filterUser[key]; - if (value === true || value === "true") { + const filterValue = filterUser[key]; + if (filterValue === true || filterValue === "true") { const queryPath = this.fieldMap[key]; query[queryPath] = Types.ObjectId(this.req.user._id); } @@ -132,9 +139,18 @@ export class ReviewTaskService { }); } - buildLookupPipeline(reviewTaskType) { + buildLookupPipeline(reviewTaskType: ReviewTaskTypeEnum) { let pipeline: any = [ - { $match: { $expr: { $eq: ["$_id", "$$targetId"] } } }, + { + $match: { + $expr: { + $and: [ + { $ne: ["$$targetId", null] }, + { $eq: ["$_id", "$$targetId"] }, + ], + }, + }, + }, ]; if (reviewTaskType === ReviewTaskTypeEnum.Claim) { @@ -181,13 +197,45 @@ export class ReviewTaskService { from: "personalities", let: { personalityId: { - $toObjectId: "$machine.context.review.personality", + $let: { + vars: { + personalityValue: + "$machine.context.review.personality", + }, + in: { + $cond: [ + { + $and: [ + { + $ne: [ + "$$personalityValue", + null, + ], + }, + { + $ne: [ + "$$personalityValue", + "", + ], + }, + ], + }, + { $toObjectId: "$$personalityValue" }, + null, + ], + }, + }, }, }, pipeline: [ { $match: { - $expr: { $eq: ["$_id", "$$personalityId"] }, + $expr: { + $and: [ + { $ne: ["$$personalityId", null] }, + { $eq: ["$_id", "$$personalityId"] }, + ], + }, }, }, { $project: { slug: 1, name: 1, _id: 1 } }, @@ -204,7 +252,30 @@ export class ReviewTaskService { { $lookup: { from: `${reviewTaskType.toLowerCase()}s`, - let: { targetId: { $toObjectId: "$target" } }, + let: { + targetId: { + $let: { + vars: { targetValue: "$target" }, + in: { + $cond: [ + { + $and: [ + { + $ne: [ + "$$targetValue", + null, + ], + }, + { $ne: ["$$targetValue", ""] }, + ], + }, + { $toObjectId: "$$targetValue" }, + null, + ], + }, + }, + }, + }, pipeline: this.buildLookupPipeline(reviewTaskType), as: "target", }, @@ -238,43 +309,73 @@ export class ReviewTaskService { ); } + /** + * Post-processes review task data after aggregation pipeline + * Handles optional fields gracefully for different review types + * @param params - PostProcess parameters + * @returns Formatted review task data + */ async postProcess({ data_hash, machine, target, reviewTaskType, }: IPostProcess): Promise { + const personality = machine.context?.review?.personality; + const reviewData = machine.context?.reviewData; + const latestRevision = target?.latestRevision; + + const usersId: User[] = reviewData?.usersId || []; + + const usersName: string[] = usersId + .map((user) => user?.name) + .filter( + (name): name is string => + typeof name === "string" && name.length > 0 + ); + + const contentModel = latestRevision?.contentModel; + const claimTitle = latestRevision?.title; + const isContentImage = contentModel === ContentModelEnum.Image; + let content: Source | Sentence | Image = target; - const personality: { _id?: string; name?: string } = - machine.context.review.personality; - const contentModel: string = target?.latestRevision?.contentModel; - const claimTitle: string = target?.latestRevision?.title; - const usersId: User[] = machine.context.reviewData.usersId; - const isContentImage: boolean = contentModel === ContentModelEnum.Image; - const usersName: string[] = usersId.map((user) => user.name); - if (reviewTaskType === ReviewTaskTypeEnum.Claim) { + if (reviewTaskType === ReviewTaskTypeEnum.Claim && latestRevision) { if (isContentImage) { content = await this.imageService.getByDataHash(data_hash); + } else { + content = await this.sentenceService.getByDataHash(data_hash); } - - content = await this.sentenceService.getByDataHash(data_hash); } - return { + const result: IReviewTask = { content, usersName, value: machine.value, - personalityName: personality?.name, - claimTitle, targetId: target._id, - personalityId: personality?._id, - contentModel, }; + + if (personality?.name) { + result.personalityName = personality.name; + } + if (personality && "_id" in personality && personality._id) { + result.personalityId = + typeof personality._id === "string" + ? personality._id + : personality._id.toString(); + } + if (claimTitle) { + result.claimTitle = claimTitle; + } + if (contentModel) { + result.contentModel = contentModel; + } + + return result; } - getById(reviewTaskId: string) { - return this.ReviewTaskModel.findById(reviewTaskId); + getById(reviewTaskId: string): Promise { + return this.ReviewTaskModel.findById(reviewTaskId).exec(); } _createReviewTaskHistory(newReviewTask, previousReviewTask = null) { @@ -289,7 +390,7 @@ export class ReviewTaskService { : Object.keys(newReviewTask.machine.value)[0]; } - const user = this.req.user; + const user = this.req.user?._id; const history = this.historyService.getHistoryParams( newReviewTask._id, @@ -658,17 +759,18 @@ export class ReviewTaskService { return 0; } catch (error) { - console.error("Error in countReviewTasksNotDeleted:", error); + this.logger.error("Error in countReviewTasksNotDeleted:", error); throw error; } } - getEditorContentObject(schema, reportModel, reviewTaskType) { - return this.editorParseService.schema2editor( + async getEditorContentObject(schema, reportModel, reviewTaskType) { + const editorContent = await this.editorParseService.schema2editor( schema, reportModel, reviewTaskType ); + return this.editorParseService.removeTrailingParagraph(editorContent); } async addComment(data_hash, comment) { diff --git a/server/review-task/schemas/review-task.schema.ts b/server/review-task/schemas/review-task.schema.ts index e99e5cc42..365826e9e 100644 --- a/server/review-task/schemas/review-task.schema.ts +++ b/server/review-task/schemas/review-task.schema.ts @@ -7,7 +7,7 @@ import { NameSpaceEnum } from "../../auth/name-space/schemas/name-space.schema"; export type ReviewTaskDocument = ReviewTask & mongoose.Document & { content: any }; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class ReviewTask { @Prop({ type: Object, required: true }) machine: Machine; diff --git a/server/root/root.controller.ts b/server/root/root.controller.ts index 9a695973e..b4365f258 100644 --- a/server/root/root.controller.ts +++ b/server/root/root.controller.ts @@ -1,9 +1,9 @@ import { Controller, Get, Header, Req, Res } from "@nestjs/common"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; @Controller("") export class RootController { - @IsPublic() + @Public() @Get("robots.txt") @Header("Cache-Control", "max-age=60, must-revalidate") robots(@Res() res, @Req() req) { @@ -13,7 +13,7 @@ export class RootController { ); } - @IsPublic() + @Public() @Get("api/health") health() { return { status: "ok" }; diff --git a/server/scripts/createNovuSubscribersFromDB.ts b/server/scripts/createNovuSubscribersFromDB.ts index 3219c80b6..96b46dda8 100644 --- a/server/scripts/createNovuSubscribersFromDB.ts +++ b/server/scripts/createNovuSubscribersFromDB.ts @@ -6,7 +6,7 @@ import { NotificationService } from "../notifications/notifications.service"; import { WinstonLogger } from "../winstonLogger"; import loadConfig from "../configLoader"; -async function createNovuSubscriber(userFromDB, novuService) { +async function createNovuSubscriber(userFromDB, novuService, logger) { if (!userFromDB || !userFromDB.id) { throw new Error(`Invalid user data: ${JSON.stringify(userFromDB)}`); } @@ -17,7 +17,7 @@ async function createNovuSubscriber(userFromDB, novuService) { email: userFromDB.email, name: userFromDB.name, }); - console.log(`Subscriber created for user ${userFromDB.email}`); + logger.log(`Subscriber created for user ${userFromDB.email}`); } catch (error) { throw new Error( `Failed to create Novu subscriber for user ${userFromDB.email}: ${error.message}` @@ -42,8 +42,7 @@ async function initApp() { const users = await userService.getAllUsers(); for (const user of users) { - await createNovuSubscriber(user, novuService); - logger.log(`Novu subscriber created for user: ${user.email}`); + await createNovuSubscriber(user, novuService, logger); } logger.log("All users have been processed for Novu subscription."); diff --git a/server/scripts/updateAllUsersAppAffiliation.ts b/server/scripts/updateAllUsersAppAffiliation.ts index ae3aef3ad..c5899e7bb 100644 --- a/server/scripts/updateAllUsersAppAffiliation.ts +++ b/server/scripts/updateAllUsersAppAffiliation.ts @@ -7,13 +7,15 @@ import loadConfig from "../configLoader"; import { WinstonLogger } from "../winstonLogger"; import OryService from "../auth/ory/ory.service"; -async function updateUserAppAffiliation(userFromDB, app) { +async function updateUserAppAffiliation(userFromDB, app, logger) { const oryService = await app.resolve(OryService); const configService = app.get(ConfigService); const app_affiliation = configService.get("app_affiliation"); if (userFromDB && app_affiliation) { - console.log(userFromDB.email, app_affiliation); + logger.log( + `Updating user ${userFromDB.email} with app_affiliation: ${app_affiliation}` + ); await oryService.updateIdentity(userFromDB, null, { app_affiliation, role: userFromDB.role, @@ -41,8 +43,7 @@ async function initApp() { // Loop through each user and apply the update for (const user of users) { - await updateUserAppAffiliation(user, app); - logger.log(`${user.email} updated`); + await updateUserAppAffiliation(user, app, logger); } logger.log("All users have been updated."); diff --git a/server/search/search.controller.ts b/server/search/search.controller.ts index 709869429..6f711bc15 100644 --- a/server/search/search.controller.ts +++ b/server/search/search.controller.ts @@ -10,7 +10,7 @@ import { } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { ClaimRevisionService } from "../claim/claim-revision/claim-revision.service"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; import { SentenceService } from "../claim/types/sentence/sentence.service"; import { ViewService } from "../view/view.service"; import { parse } from "url"; @@ -31,7 +31,7 @@ export class SearchController { private configService: ConfigService ) {} - @IsPublic() + @Public() @ApiTags("pages") @Get("search") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -168,7 +168,7 @@ export class SearchController { } } - @IsPublic() + @Public() @ApiTags("search") @Get("api/search") @Header("Cache-Control", "max-age=60, must-revalidate") diff --git a/server/sitemap/sitemap.controller.ts b/server/sitemap/sitemap.controller.ts index 8fdee124c..3e3257574 100644 --- a/server/sitemap/sitemap.controller.ts +++ b/server/sitemap/sitemap.controller.ts @@ -1,12 +1,12 @@ import { Controller, Get, Header, Request, Res } from "@nestjs/common"; import { SitemapService } from "./sitemap.service"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; @Controller("/") export class SitemapController { constructor(private sitemapService: SitemapService) {} - @IsPublic() + @Public() @Get("sitemap.xml") @Header("Content-Type", "text/xml") @Header("Cache-Control", "max-age=86400") diff --git a/server/source/schemas/source.schema.ts b/server/source/schemas/source.schema.ts index 05f6fe416..7fd7f57e5 100644 --- a/server/source/schemas/source.schema.ts +++ b/server/source/schemas/source.schema.ts @@ -9,7 +9,7 @@ export enum SourceTargetModel { Claim = "Claim", ClaimReview = "ClaimReview", } -@Schema() +@Schema({ timestamps: true }) export class Source { @Prop({ required: true }) href: string; diff --git a/server/source/source.controller.ts b/server/source/source.controller.ts index 52ea216d0..f90f37421 100644 --- a/server/source/source.controller.ts +++ b/server/source/source.controller.ts @@ -18,7 +18,7 @@ import { parse } from "url"; import { ViewService } from "../view/view.service"; import { ConfigService } from "@nestjs/config"; import type { Response } from "express"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; import { CreateSourceDTO } from "./dto/create-source.dto"; import { CaptchaService } from "../captcha/captcha.service"; import { UnleashService } from "nestjs-unleash"; @@ -99,7 +99,7 @@ export class SourceController { await this.viewService.render(req, res, "/sources-create", queryObject); } - @IsPublic() + @Public() @ApiTags("source") @Get("api/source") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -124,7 +124,7 @@ export class SourceController { }); } - @IsPublic() + @Public() @ApiTags("pages") @Get("source") @Header("Cache-Control", "max-age=60, must-revalidate") @@ -137,7 +137,7 @@ export class SourceController { await this.viewService.render(req, res, "/sources-page", queryObject); } - @IsPublic() + @Public() @ApiTags("pages") @Get("source/:dataHash") @Header("Cache-Control", "max-age=60, must-revalidate") diff --git a/server/state-event/schema/state-event.schema.ts b/server/state-event/schema/state-event.schema.ts index 0becde79a..0fbd1b6c2 100644 --- a/server/state-event/schema/state-event.schema.ts +++ b/server/state-event/schema/state-event.schema.ts @@ -12,7 +12,7 @@ export enum TypeModel { Published = "published", } -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true }, timestamps: true }) export class StateEvent { @Prop({ type: mongoose.Types.ObjectId, diff --git a/server/summarization/summarization-crawler-chain.service.ts b/server/summarization/summarization-crawler-chain.service.ts index 379c9196d..582b4a155 100644 --- a/server/summarization/summarization-crawler-chain.service.ts +++ b/server/summarization/summarization-crawler-chain.service.ts @@ -1,9 +1,9 @@ import { PromptTemplate } from "@langchain/core/prompts"; import { Injectable, Logger } from "@nestjs/common"; -import { loadSummarizationChain, StuffDocumentsChain } from "langchain/chains"; +import { loadSummarizationChain, StuffDocumentsChain } from "@langchain/classic/chains"; import { ChatOpenAI } from "@langchain/openai"; import { openAI } from "../copilot/openAI.constants"; -import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; +import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters"; import { ConfigService } from "@nestjs/config"; @Injectable() diff --git a/server/summarization/summarization-crawler.controller.ts b/server/summarization/summarization-crawler.controller.ts index a02b0c26e..6387bc6da 100644 --- a/server/summarization/summarization-crawler.controller.ts +++ b/server/summarization/summarization-crawler.controller.ts @@ -1,12 +1,8 @@ -import { Controller, Get, Query, Req, UseGuards } from "@nestjs/common"; +import { Controller, Get, Query, Req } from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; import { SummarizationCrawlerService } from "./summarization-crawler.service"; import type { BaseRequest } from "../types"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; -import { - CheckAbilities, - FactCheckerUserAbility, -} from "../auth/ability/ability.decorator"; +import { FactCheckerOnly } from "../auth/decorators/auth.decorator"; @Controller() export class SummarizationCrawlerController { @@ -14,10 +10,9 @@ export class SummarizationCrawlerController { private summarizationCrawlerService: SummarizationCrawlerService ) {} + @FactCheckerOnly() @ApiTags("source") @Get("api/summarization") - @UseGuards(AbilitiesGuard) - @CheckAbilities(new FactCheckerUserAbility()) create(@Req() req: BaseRequest, @Query() query) { return this.summarizationCrawlerService.summarizePage( query.source, diff --git a/server/summarization/summarization-crawler.service.ts b/server/summarization/summarization-crawler.service.ts index e71f0f077..877647ef1 100644 --- a/server/summarization/summarization-crawler.service.ts +++ b/server/summarization/summarization-crawler.service.ts @@ -1,9 +1,12 @@ import { Injectable, Logger } from "@nestjs/common"; import { SummarizationCrawlerChainService } from "./summarization-crawler-chain.service"; -import { WebBrowser } from "langchain/tools/webbrowser"; +import { WebBrowser } from "@langchain/classic/tools/webbrowser"; import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai"; import { openAI } from "../copilot/openAI.constants"; -import { AgentExecutor, createOpenAIToolsAgent } from "langchain/agents"; +import { + AgentExecutor, + createToolCallingAgent, +} from "@langchain/classic/agents"; import { ConfigService } from "@nestjs/config"; import colors from "../../src/styles/colors"; import { @@ -151,7 +154,7 @@ export class SummarizationCrawlerService { const embeddings = new OpenAIEmbeddings(); const tools = [new WebBrowser({ model: llm, embeddings })]; - const agent = await createOpenAIToolsAgent({ + const agent = await createToolCallingAgent({ llm, tools, prompt, diff --git a/server/tests/claim-review.e2e.spec.ts b/server/tests/claim-review.e2e.spec.ts index 1cec51840..ab6bde55a 100644 --- a/server/tests/claim-review.e2e.spec.ts +++ b/server/tests/claim-review.e2e.spec.ts @@ -1,4 +1,3 @@ -import { MongoMemoryServer } from "mongodb-memory-server"; import * as request from "supertest"; import { Test, TestingModule } from "@nestjs/testing"; import { AppModule } from "../app.module"; @@ -24,6 +23,7 @@ import { SeedTestSpeech } from "./utils/SeedTestSpeech"; import { SeedTestClaimRevision } from "./utils/SeedTestClaimRevision"; import { SeedTestClaim } from "./utils/SeedTestClaim"; import { ValidationPipe } from "@nestjs/common"; +import { CleanupDatabase } from "./utils/CleanupDatabase"; const { ObjectId } = require("mongodb"); jest.setTimeout(10000); @@ -45,7 +45,6 @@ jest.setTimeout(10000); */ describe("ClaimReviewController (e2e)", () => { let app: any; - let db: any; let userId: string; let personalitiesId: string[]; let reportId: string; @@ -57,8 +56,8 @@ describe("ClaimReviewController (e2e)", () => { let claimReviewId: string; beforeAll(async () => { - db = await MongoMemoryServer.create({ instance: { port: 35025 } }); - const mongoUri = db.getUri(); + // Use shared MongoDB instance from global setup + const mongoUri = process.env.MONGO_URI!; const user = await SeedTestUser(mongoUri); userId = user.insertedId.toString(); @@ -346,7 +345,7 @@ describe("ClaimReviewController (e2e)", () => { it("api/review/:id (PUT) - Should validate update payload", () => { // First create a new review for this test by re-seeding return SeedTestClaimReview( - db.getUri(), + process.env.MONGO_URI!, claimId, personalitiesId, reportId, @@ -385,7 +384,7 @@ describe("ClaimReviewController (e2e)", () => { }); afterAll(async () => { - await db.stop(); - app.close(); + await app.close(); + await CleanupDatabase(process.env.MONGO_URI!); }); }); diff --git a/server/tests/claim.e2e.spec.ts b/server/tests/claim.e2e.spec.ts index 15e4ad519..13e7dbf7c 100644 --- a/server/tests/claim.e2e.spec.ts +++ b/server/tests/claim.e2e.spec.ts @@ -1,4 +1,3 @@ -import { MongoMemoryServer } from "mongodb-memory-server"; import * as request from "supertest"; import { Test, TestingModule } from "@nestjs/testing"; import { ValidationPipe } from "@nestjs/common"; @@ -20,6 +19,7 @@ import { CaptchaService } from "../captcha/captcha.service"; import { MongoPersonalityService } from "../personality/mongo/personality.service"; import { HistoryService } from "../history/history.service"; import { HistoryServiceMock } from "./mocks/HistoryServiceMock"; +import { CleanupDatabase } from "./utils/CleanupDatabase"; jest.setTimeout(10000); @@ -79,7 +79,6 @@ const personalityService = { describe("ClaimController (e2e)", () => { let app: any; - let db: any; let personalitiesId: string[]; let claimId: string; let speechClaimId: string; @@ -92,14 +91,14 @@ describe("ClaimController (e2e)", () => { const date: string = "2023-11-25T14:49:30.992Z"; beforeAll(async () => { - db = await MongoMemoryServer.create({ instance: { port: 35025 } }); - const mongoUri = db.getUri(); - + // Use shared MongoDB instance from global setup + const mongoUri = process.env.MONGO_URI!; + await SeedTestUser(mongoUri); const { insertedIds } = await SeedTestPersonality(mongoUri); personalitiesId = [insertedIds["0"].toString(), insertedIds["1"].toString()]; - // Update test config with actual MongoDB URI + // Update test config with shared MongoDB URI const testConfig = { ...TestConfigOptions.config, db: { @@ -599,7 +598,7 @@ describe("ClaimController (e2e)", () => { }); afterAll(async () => { - await db.stop(); - app.close(); + await app.close(); + await CleanupDatabase(process.env.MONGO_URI!); }); }); diff --git a/server/tests/globalSetup.ts b/server/tests/globalSetup.ts new file mode 100644 index 000000000..2626f4771 --- /dev/null +++ b/server/tests/globalSetup.ts @@ -0,0 +1,22 @@ +import { MongoMemoryServer } from "mongodb-memory-server"; + +/** + * Global setup for Jest test suite + * Creates a shared MongoMemoryServer instance for all test suites + * to improve performance and reduce resource usage + */ +export default async function globalSetup() { + const instance = await MongoMemoryServer.create({ + instance: { + dbName: "jest-test", + }, + }); + + const uri = instance.getUri(); + + // Store instance globally for teardown + (global as any).__MONGOINSTANCE = instance; + + // Store URI in environment for all tests to use + process.env.MONGO_URI = uri; +} diff --git a/server/tests/globalTeardown.ts b/server/tests/globalTeardown.ts new file mode 100644 index 000000000..486a81fed --- /dev/null +++ b/server/tests/globalTeardown.ts @@ -0,0 +1,11 @@ +/** + * Global teardown for Jest test suite + * Stops the shared MongoMemoryServer instance created in globalSetup + */ +export default async function globalTeardown() { + const instance = (global as any).__MONGOINSTANCE; + + if (instance) { + await instance.stop(); + } +} diff --git a/server/tests/jest-e2e.config.json b/server/tests/jest-e2e.config.json index 486278472..fe728c543 100644 --- a/server/tests/jest-e2e.config.json +++ b/server/tests/jest-e2e.config.json @@ -11,5 +11,9 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "modulePathIgnorePatterns": ["./dist/"] + "modulePathIgnorePatterns": ["./dist/"], + "globalSetup": "./globalSetup.ts", + "globalTeardown": "./globalTeardown.ts", + "maxWorkers": "50%", + "testTimeout": 30000 } diff --git a/server/tests/notification.e2e.spec.ts b/server/tests/notification.e2e.spec.ts index dd1dac726..34ad21c01 100644 --- a/server/tests/notification.e2e.spec.ts +++ b/server/tests/notification.e2e.spec.ts @@ -1,4 +1,3 @@ -import { MongoMemoryServer } from "mongodb-memory-server"; import * as request from "supertest"; import { Test, TestingModule } from "@nestjs/testing"; import { ValidationPipe } from "@nestjs/common"; @@ -15,6 +14,7 @@ import { AbilitiesGuard } from "../auth/ability/abilities.guard"; import { AbilitiesGuardMock } from "./mocks/AbilitiesGuardMock"; import { AdminUserMock } from "./utils/AdminUserMock"; import { NotificationService } from "../notifications/notifications.service"; +import { CleanupDatabase } from "./utils/CleanupDatabase"; jest.setTimeout(10000); @@ -45,12 +45,11 @@ const notificationService = { describe("NotificationController (e2e)", () => { let app: any; - let db: any; const payload = "Test Message Notification"; beforeAll(async () => { - db = await MongoMemoryServer.create({ instance: { port: 35025 } }); - const mongoUri = db.getUri(); + // Use shared MongoDB instance from global setup + const mongoUri = process.env.MONGO_URI!; await SeedTestUser(mongoUri); @@ -245,7 +244,7 @@ describe("NotificationController (e2e)", () => { afterAll(async () => { jest.restoreAllMocks(); - await db.stop(); - app.close(); + await app.close(); + await CleanupDatabase(process.env.MONGO_URI!); }); }); diff --git a/server/tests/personality.e2e.spec.ts b/server/tests/personality.e2e.spec.ts index 07097bd4f..b8a17b158 100644 --- a/server/tests/personality.e2e.spec.ts +++ b/server/tests/personality.e2e.spec.ts @@ -1,4 +1,3 @@ -import { MongoMemoryServer } from "mongodb-memory-server"; import * as request from "supertest"; import { Test, TestingModule } from "@nestjs/testing"; import { ValidationPipe } from "@nestjs/common"; @@ -15,6 +14,7 @@ import { AbilitiesGuard } from "../auth/ability/abilities.guard"; import { AbilitiesGuardMock } from "./mocks/AbilitiesGuardMock"; import { HistoryService } from "../history/history.service"; import { HistoryServiceMock } from "./mocks/HistoryServiceMock"; +import { CleanupDatabase } from "./utils/CleanupDatabase"; jest.setTimeout(10000); @@ -40,12 +40,11 @@ jest.setTimeout(10000); */ describe("PersonalityController (e2e)", () => { let app: any; - let db: any; let personalityId: string; beforeAll(async () => { - db = await MongoMemoryServer.create({ instance: { port: 35025 } }); - const mongoUri = db.getUri(); + // Use shared MongoDB instance from global setup + const mongoUri = process.env.MONGO_URI!; await SeedTestUser(mongoUri); @@ -408,7 +407,7 @@ describe("PersonalityController (e2e)", () => { }); afterAll(async () => { - await db.stop(); - app.close(); + await app.close(); + await CleanupDatabase(process.env.MONGO_URI!); }); }); diff --git a/server/tests/source.e2e.spec.ts b/server/tests/source.e2e.spec.ts index af13c595e..a23af597f 100644 --- a/server/tests/source.e2e.spec.ts +++ b/server/tests/source.e2e.spec.ts @@ -1,4 +1,3 @@ -import { MongoMemoryServer } from "mongodb-memory-server"; import * as request from "supertest"; import { Test, TestingModule } from "@nestjs/testing"; import { ValidationPipe } from "@nestjs/common"; @@ -18,6 +17,7 @@ import { HistoryServiceMock } from "./mocks/HistoryServiceMock"; import { NameSpaceEnum } from "../auth/name-space/schemas/name-space.schema"; import { SeedTestPersonality } from "./utils/SeedTestPersonality"; import { SeedTestClaim } from "./utils/SeedTestClaim"; +import { CleanupDatabase } from "./utils/CleanupDatabase"; const { ObjectId } = require("mongodb"); jest.setTimeout(10000); @@ -45,15 +45,14 @@ jest.setTimeout(10000); */ describe("SourceController (e2e)", () => { let app: any; - let db: any; let userId: string; let personalitiesId: string[]; let claimId: string; let targetId: string; beforeAll(async () => { - db = await MongoMemoryServer.create({ instance: { port: 35025 } }); - const mongoUri = db.getUri(); + // Use shared MongoDB instance from global setup + const mongoUri = process.env.MONGO_URI!; const user = await SeedTestUser(mongoUri); userId = user.insertedId.toString(); @@ -368,7 +367,7 @@ describe("SourceController (e2e)", () => { afterAll(async () => { jest.restoreAllMocks(); - await db.stop(); - app.close(); + await app.close(); + await CleanupDatabase(process.env.MONGO_URI!); }); }); diff --git a/server/tests/utils/CleanupDatabase.ts b/server/tests/utils/CleanupDatabase.ts new file mode 100644 index 000000000..bdfbd7a36 --- /dev/null +++ b/server/tests/utils/CleanupDatabase.ts @@ -0,0 +1,31 @@ +import { MongoClient } from "mongodb"; +import { TEST_DB_NAME } from "./TestConstants"; + +/** + * Cleanup utility to clear database collections between test suites + * This prevents duplicate key errors when running tests sequentially + * + * @param mongoUri - MongoDB connection URI + * @throws Error if database cleanup fails + */ +export async function CleanupDatabase(mongoUri: string): Promise { + const client = await MongoClient.connect(mongoUri); + + try { + const db = client.db(TEST_DB_NAME); + const collections = await db.listCollections().toArray(); + + // Delete all documents from all collections to ensure clean state + for (const collection of collections) { + await db.collection(collection.name).deleteMany({}); + } + } catch (error) { + throw new Error( + `Database cleanup failed for ${TEST_DB_NAME}: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } finally { + await client.close(); + } +} diff --git a/server/tests/utils/SeedTestPersonality.ts b/server/tests/utils/SeedTestPersonality.ts index 46f1e4132..367ae8006 100644 --- a/server/tests/utils/SeedTestPersonality.ts +++ b/server/tests/utils/SeedTestPersonality.ts @@ -1,15 +1,47 @@ import { MongoClient } from "mongodb"; import { PersonalitiesMock } from "./PersonalitiesMock"; +import { TEST_DB_NAME } from "./TestConstants"; export const SeedTestPersonality = async (uri) => { const client = await new MongoClient(uri); await client.connect(); try { - return await client - .db("test") + // Use bulkWrite with upsert to avoid duplicate errors in parallel execution + const operations = PersonalitiesMock.map((personality) => ({ + updateOne: { + filter: { slug: personality.slug }, + update: { $set: personality }, + upsert: true, + }, + })); + + const result = await client + .db(TEST_DB_NAME) + .collection("personalities") + .bulkWrite(operations); + + // Get the inserted/updated IDs with explicit ordering for consistency + const personalities = await client + .db(TEST_DB_NAME) .collection("personalities") - .insertMany(PersonalitiesMock); + .find({ slug: { $in: PersonalitiesMock.map((p) => p.slug) } }) + .sort({ slug: 1 }) + .toArray(); + + // Map by slug to ensure order-independent ID mapping + return { + insertedIds: PersonalitiesMock.reduce((acc, mock, index) => { + const personality = personalities.find( + (p) => p.slug === mock.slug + ); + if (personality) { + acc[index.toString()] = personality._id; + } + return acc; + }, {}), + acknowledged: result.ok === 1, + }; } finally { await client.close(); } diff --git a/server/tests/utils/SeedTestUser.ts b/server/tests/utils/SeedTestUser.ts index e11860bd2..366306dcd 100644 --- a/server/tests/utils/SeedTestUser.ts +++ b/server/tests/utils/SeedTestUser.ts @@ -1,15 +1,26 @@ import { MongoClient } from "mongodb"; import { AdminUserMock } from "./AdminUserMock"; +import { TEST_DB_NAME } from "./TestConstants"; export const SeedTestUser = async (uri) => { const client = await new MongoClient(uri); await client.connect(); try { - return await client - .db("test") + // Use updateOne with upsert to avoid duplicate key errors in parallel execution + const result = await client + .db(TEST_DB_NAME) .collection("users") - .insertOne(AdminUserMock); + .updateOne( + { _id: AdminUserMock._id }, + { $set: AdminUserMock }, + { upsert: true } + ); + + return { + insertedId: AdminUserMock._id, + acknowledged: result.acknowledged, + }; } finally { await client.close(); } diff --git a/server/tests/utils/TestConstants.ts b/server/tests/utils/TestConstants.ts new file mode 100644 index 000000000..ce5a3b889 --- /dev/null +++ b/server/tests/utils/TestConstants.ts @@ -0,0 +1,8 @@ +/** + * Shared constants for test utilities + */ + +/** + * Test database name used across all test suites + */ +export const TEST_DB_NAME = "test"; diff --git a/server/topic/schemas/topic.schema.ts b/server/topic/schemas/topic.schema.ts index 2fc60b58d..38e0cb334 100644 --- a/server/topic/schemas/topic.schema.ts +++ b/server/topic/schemas/topic.schema.ts @@ -3,7 +3,11 @@ import * as mongoose from "mongoose"; export type TopicDocument = Topic & mongoose.Document; -@Schema({ toObject: { virtuals: true }, toJSON: { virtuals: true } }) +@Schema({ + toObject: { virtuals: true }, + toJSON: { virtuals: true }, + timestamps: true, +}) export class Topic { // TODO: Implement topic taxonomy // TODO: Better I18N @@ -21,6 +25,9 @@ export class Topic { @Prop({ required: false }) wikidataId?: string; + @Prop({ required: false, type: [String], default: [] }) + aliases?: string[]; + @Prop({ required: true, }) diff --git a/server/topic/topic.controller.ts b/server/topic/topic.controller.ts index 3b6a932d5..4fc639a74 100644 --- a/server/topic/topic.controller.ts +++ b/server/topic/topic.controller.ts @@ -1,35 +1,35 @@ import { Body, Controller, Get, Post, Query, Req } from "@nestjs/common"; import { TopicService } from "./topic.service"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; import { ApiTags } from "@nestjs/swagger"; @Controller() export class TopicController { - constructor(private topicService: TopicService) {} + constructor(private topicService: TopicService) {} - @IsPublic() - @ApiTags("topics") - @Get("api/topics") - public async getAll(@Query() getTopics) { - return this.topicService.findAll(getTopics); - } + @Public() + @ApiTags("topics") + @Get("api/topics") + public async getAll(@Query() getTopics) { + return this.topicService.findAll(getTopics); + } - @ApiTags("topics") - @Get("api/topics/search") - async searchTopics( - @Query("query") query: string, - @Query("limit") limit: number = 10, - @Query("language") language: string = "pt" - ) { - return this.topicService.searchTopics(query, language, limit); - } + @ApiTags("topics") + @Get("api/topics/search") + async searchTopics( + @Query("query") query: string, + @Query("limit") limit: number = 10, + @Query("language") language: string = "pt" + ) { + return this.topicService.searchTopics(query, language, limit); + } - @ApiTags("topics") - @Post("api/topics") - create(@Body() topicBody, @Req() req) { - return this.topicService.create( - topicBody, - req.cookies.default_language - ); - } + @ApiTags("topics") + @Post("api/topics") + create(@Body() topicBody, @Req() req) { + return this.topicService.create( + topicBody, + req.cookies.default_language + ); + } } diff --git a/server/topic/topic.service.ts b/server/topic/topic.service.ts index 35339a22e..c00b62194 100644 --- a/server/topic/topic.service.ts +++ b/server/topic/topic.service.ts @@ -19,36 +19,63 @@ export class TopicService { private wikidataService: WikidataService ) {} - async getWikidataEntities(regex, language) { + /** + * Normalize a string by removing accents/diacritical marks + * @param text The text to normalize + * @returns Normalized text without accents + */ + private normalizeText(text: string): string { + return text.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + } + + async getWikidataEntities(regex: string, language: string) { return await this.wikidataService.queryWikibaseEntities( regex, language ); } - async searchTopics( - query: string, - language = "pt", - limit = 10 - ): Promise { - if (typeof language !== "string") { - throw new TypeError("Invalid language"); - } + async searchTopics( + query: string, + language = "pt", + limit = 10 + ): Promise { + if (typeof language !== "string") { + throw new TypeError("Invalid language"); + } - return this.TopicModel.find({ - name: { $regex: query, $options: "i" }, - language: { $eq: language } - }) - .limit(limit) - .sort({ name: 1 }); - } + const normalizedQuery = this.normalizeText(query); + const searchRegex = new RegExp(normalizedQuery, "i"); + + const topics = await this.TopicModel.find({ + language: { $eq: language }, + $or: [ + { name: { $regex: searchRegex } }, + { aliases: { $regex: searchRegex } }, + ], + }) + .limit(limit) + .sort({ name: 1 }); + + const normalizedQueryLower = normalizedQuery.toLowerCase(); + return topics.map((topic) => { + const topicObj = topic.toObject(); + const matchedAlias = + topicObj.aliases?.find((alias) => + this.normalizeText(alias) + .toLowerCase() + .includes(normalizedQueryLower) + ) || null; + return { ...topicObj, matchedAlias }; + }); + } /** * * @param getTopics options to fetch topics * @returns return all topics from wikidata database that match to topicName from input */ - async findAll(getTopics, language = "pt"): Promise { + async findAll(getTopics, language = "pt") { return this.getWikidataEntities(getTopics.topicName, language); } @@ -67,9 +94,12 @@ export class TopicService { }: { contentModel?: ContentModelEnum; topics: - | { label: string; value: string }[] + | { label: string; value: string; aliases?: string[] }[] | string[] - | (string | { label: string; value: string })[]; + | ( + | string + | { label: string; value: string; aliases?: string[] } + )[]; data_hash?: string; }, language: string = "pt" @@ -95,6 +125,7 @@ export class TopicService { const newTopic = { name: topic?.label || topic, wikidataId: topic?.value, + aliases: topic?.aliases || [], slug, language, }; @@ -144,12 +175,31 @@ export class TopicService { } /** - * + * Escape special regex characters to prevent ReDoS attacks + * @param str The string to escape + * @returns Escaped string safe for regex + */ + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + + /** + * Find topics by name or alias with case-insensitive matching * @param names topic names array - * @returns topics + * @returns Promise resolving to array of matching topics */ - findByNames(names: string[]) { - return this.TopicModel.find({ name: { $in: names } }); + findByNames(names: string[]): Promise { + const nameConditions = names.flatMap((name) => { + const escapedName = this.escapeRegex(name); + return [ + { name: { $regex: new RegExp(`^${escapedName}$`, "i") } }, + { aliases: { $regex: new RegExp(`^${escapedName}$`, "i") } }, + ]; + }); + + return this.TopicModel.find({ + $or: nameConditions, + }).exec(); } /** diff --git a/server/tracking/tracking.controller.spec.ts b/server/tracking/tracking.controller.spec.ts new file mode 100644 index 000000000..df9085662 --- /dev/null +++ b/server/tracking/tracking.controller.spec.ts @@ -0,0 +1,58 @@ +import { Test } from "@nestjs/testing"; +import { TrackingController } from "./tracking.controller"; +import { TrackingService } from "./tracking.service"; +import { BadRequestException } from "@nestjs/common"; +import { mockResponse, mockTrackingService } from "../mocks/TrackingMock"; +import { VerificationRequestStatus } from "../verification-request/dto/types"; +import { AbilitiesGuard } from "../auth/ability/abilities.guard"; + +describe("TrackingController (Unit)", () => { + let controller: TrackingController; + let trackingService: any; + + beforeEach(async () => { + const testingModule = await Test.createTestingModule({ + controllers: [TrackingController], + providers: [{ provide: TrackingService, useValue: mockTrackingService }], + }) + .overrideGuard(AbilitiesGuard) + .useValue({}) + .compile(); + + controller = testingModule.get(TrackingController); + trackingService = testingModule.get(TrackingService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe("getTracking", () => { + const validId = "507f1f77bcf86cd799439011"; + const invalidId = "123-id-invalido"; + + it("should return tracking status correctly (happy path)", async () => { + trackingService.getTrackingStatus.mockResolvedValue(mockResponse); + + const response = await controller.getTracking(validId); + + expect(response).toEqual(mockResponse); + expect(response.currentStatus).toBe(VerificationRequestStatus.IN_TRIAGE); + expect(trackingService.getTrackingStatus).toHaveBeenCalledWith(validId); + }); + + it("should throw BadRequestException if ID format is invalid", async () => { + await expect(controller.getTracking(invalidId)).rejects.toThrow( + BadRequestException + ); + + expect(trackingService.getTrackingStatus).not.toHaveBeenCalled(); + }); + + it("should propagate service errors", async () => { + trackingService.getTrackingStatus.mockRejectedValue(new Error("Database connection error")); + + await expect(controller.getTracking(validId)).rejects.toThrow("Database connection error"); + }); + }); +}); diff --git a/server/tracking/tracking.controller.ts b/server/tracking/tracking.controller.ts new file mode 100644 index 000000000..e41724f53 --- /dev/null +++ b/server/tracking/tracking.controller.ts @@ -0,0 +1,23 @@ +import { BadRequestException, Controller, Get, Param } from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { TrackingService } from "./tracking.service"; +import { HEX24 } from "../history/types/history.interfaces"; +import { RegularUserOnly } from "../auth/decorators/auth.decorator"; + +@ApiTags("tracking") +@Controller("api/tracking") +export class TrackingController { + constructor(private readonly trackingService: TrackingService) { } + + @RegularUserOnly() + @Get(":verificationRequestId") + async getTracking( + @Param("verificationRequestId") verificationRequestId: string + ) { + if (!HEX24.test(verificationRequestId)) { + throw new BadRequestException("Invalid verificationRequestId format"); + } + + return this.trackingService.getTrackingStatus(verificationRequestId); + } +} diff --git a/server/tracking/tracking.module.ts b/server/tracking/tracking.module.ts new file mode 100644 index 000000000..e4eaeb5b8 --- /dev/null +++ b/server/tracking/tracking.module.ts @@ -0,0 +1,18 @@ +import { Module } from "@nestjs/common"; +import { TrackingController } from "./tracking.controller"; +import { TrackingService } from "./tracking.service"; +import { HistoryModule } from "../history/history.module"; +import { AbilityModule } from "../auth/ability/ability.module"; +import { VerificationRequestModule } from "../verification-request/verification-request.module"; + +@Module({ + imports: [ + HistoryModule, + AbilityModule, + VerificationRequestModule + ], + controllers: [TrackingController], + providers: [TrackingService], + exports: [TrackingService], +}) +export class TrackingModule { } diff --git a/server/tracking/tracking.service.spec.ts b/server/tracking/tracking.service.spec.ts new file mode 100644 index 000000000..43c8268d6 --- /dev/null +++ b/server/tracking/tracking.service.spec.ts @@ -0,0 +1,129 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { TrackingService } from "./tracking.service"; +import { HistoryService } from "../history/history.service"; +import { NotFoundException } from "@nestjs/common"; +import { TargetModel } from "../history/schema/history.schema"; +import { + historyServiceMock, + mockHistoryItem, + mockHistoryResponse +} from "../mocks/HistoryMock"; +import { VerificationRequestStatus } from "../verification-request/dto/types"; +import { VerificationRequestService } from "../verification-request/verification-request.service"; +import { mockVerificationRequestModel } from "../mocks/VerificationRequestMock"; + +describe("TrackingService (Unit)", () => { + let service: TrackingService; + let historyService: typeof historyServiceMock; + + beforeAll(async () => { + const testingModule: TestingModule = await Test.createTestingModule({ + providers: [ + TrackingService, + { + provide: HistoryService, + useValue: historyServiceMock, + }, + { + provide: VerificationRequestService, + useValue: mockVerificationRequestModel, + }, + ], + }).compile(); + + service = testingModule.get(TrackingService); + historyService = testingModule.get(HistoryService); + }); + + beforeEach(() => { + jest.clearAllMocks(); + + mockVerificationRequestModel.getById.mockResolvedValue({ + id: String(mockHistoryItem.targetId), + status: VerificationRequestStatus.PRE_TRIAGE, + }); + }); + + describe("getTrackingStatus", () => { + const targetIdStr = String(mockHistoryItem.targetId); + + it("should return tracking events correctly (happy path)", async () => { + historyService.getHistoryForTarget.mockResolvedValue(mockHistoryResponse); + + const result = await service.getTrackingStatus(targetIdStr); + + expect(result.historyEvents).toHaveLength(1); + expect(result.currentStatus).toBe(VerificationRequestStatus.PRE_TRIAGE); + expect(result.historyEvents[0].date).toBeInstanceOf(Date); + expect(historyService.getHistoryForTarget).toHaveBeenCalledWith( + targetIdStr, + TargetModel.VerificationRequest, + expect.objectContaining({ pageSize: 50 }) + ); + expect(mockVerificationRequestModel.getById).toHaveBeenCalledWith(targetIdStr); + }); + + it("should filter out events where status did not change", async () => { + const historyWithNoChange = { + ...mockHistoryResponse, + history: [ + mockHistoryItem, + { + ...mockHistoryItem, + _id: "another-id", + details: { + before: { status: VerificationRequestStatus.POSTED }, + after: { status: VerificationRequestStatus.POSTED }, + }, + }, + ], + }; + historyService.getHistoryForTarget.mockResolvedValue(historyWithNoChange); + + const result = await service.getTrackingStatus(targetIdStr); + + expect(result.historyEvents).toHaveLength(1); + expect(result.historyEvents[0].id).toBe(mockHistoryItem._id); + }); + + it("should throw NotFoundException and log warning when no history exists", async () => { + historyService.getHistoryForTarget.mockResolvedValue({ history: [] }); + + const loggerWarnSpy = jest.spyOn(service['logger'], 'warn'); + + await expect(service.getTrackingStatus(targetIdStr)).rejects.toThrow( + NotFoundException + ); + expect(loggerWarnSpy).toHaveBeenCalledWith( + expect.stringContaining(`Tracking not found for ID: ${targetIdStr}`) + ); + }); + + it("should throw a generic error and log error when an unexpected failure occurs", async () => { + const unexpectedError = new Error("Database connection lost"); + historyService.getHistoryForTarget.mockRejectedValue(unexpectedError); + + const loggerErrorSpy = jest.spyOn(service['logger'], 'error'); + + await expect(service.getTrackingStatus(targetIdStr)).rejects.toThrow( + "Internal server error while fetching tracking status." + ); + expect(loggerErrorSpy).toHaveBeenCalledWith( + expect.stringContaining(`Failed to fetch tracking for ID: ${targetIdStr}`), + unexpectedError.stack + ); + }); + + it("should return currentStatus from the verification request regardless of history", async () => { + historyService.getHistoryForTarget.mockResolvedValue(mockHistoryResponse); + mockVerificationRequestModel.getById.mockResolvedValue({ + id: targetIdStr, + status: VerificationRequestStatus.IN_TRIAGE, + }); + + const result = await service.getTrackingStatus(targetIdStr); + + expect(result.currentStatus).toBe(VerificationRequestStatus.IN_TRIAGE); + }); + }); +}); diff --git a/server/tracking/tracking.service.ts b/server/tracking/tracking.service.ts new file mode 100644 index 000000000..866eedf4d --- /dev/null +++ b/server/tracking/tracking.service.ts @@ -0,0 +1,64 @@ +import { Injectable, InternalServerErrorException, Logger, NotFoundException } from "@nestjs/common"; +import { HistoryService } from "../history/history.service"; +import { HistoryType, TargetModel } from "../history/schema/history.schema"; +import { TrackingResponseDTO } from "./types/tracking.interfaces"; +import { VerificationRequestService } from "../verification-request/verification-request.service"; + +@Injectable() +export class TrackingService { + private readonly logger = new Logger(TrackingService.name); + + constructor( + private readonly historyService: HistoryService, + private verificationRequestService: VerificationRequestService, +) { } + + /** + * Returns the history of status changes (tracking) for a specific verification request. + * Searches for "create" and "update" type histories and extracts only status transitions. + * @param verificationRequestId The id of the verification request. + * @returns Array of objects { id: string, status: string, date: Date }. + */ + async getTrackingStatus(verificationRequestId: string): Promise { + try { + // We only need to call verification requests here because we don't have histories for all verification request steps yet, so it's safer to use the latestStatus from the verification request instead of the history. + const verificationRequest = await this.verificationRequestService.getById(verificationRequestId); + + const { history } = await this.historyService.getHistoryForTarget(verificationRequestId, TargetModel.VerificationRequest, { + page: 0, + pageSize: 50, + order: "asc", + type: [HistoryType.Create, HistoryType.Update], + }); + + if (!history || history.length === 0) { + throw new NotFoundException(`Verification request history for ID "${verificationRequestId}" not found.`); + } + + const tracking = history + .filter((history) => history.details?.after?.status && history.details?.before?.status !== history.details?.after?.status) + .map((history) => ({ + id: history._id, + status: history.details.after.status, + date: new Date(history.date), + })); + + const latestStatus = verificationRequest.status + + return { + currentStatus: latestStatus, + historyEvents: tracking, + }; + } catch (error) { + if (error instanceof NotFoundException) { + this.logger.warn(`Tracking not found for ID: ${verificationRequestId}`); + throw error; + } + this.logger.error( + `Failed to fetch tracking for ID: ${verificationRequestId}`, + error.stack, + ); + throw new InternalServerErrorException("Internal server error while fetching tracking status."); + } + } +} diff --git a/server/tracking/types/tracking.interfaces.ts b/server/tracking/types/tracking.interfaces.ts new file mode 100644 index 000000000..661c8aaf0 --- /dev/null +++ b/server/tracking/types/tracking.interfaces.ts @@ -0,0 +1,11 @@ +import { VerificationRequestStatus } from "../../verification-request/dto/types"; + +export interface TrackingResponseDTO { + currentStatus: string; + historyEvents: HistoryItem[]; +} +interface HistoryItem { + id: string; + status: VerificationRequestStatus; + date: Date; +} diff --git a/server/users/dto/create-user.dto.ts b/server/users/dto/create-user.dto.ts index 280b04cd5..fed2c53c2 100644 --- a/server/users/dto/create-user.dto.ts +++ b/server/users/dto/create-user.dto.ts @@ -16,4 +16,9 @@ export class CreateUserDTO { @IsString() @ApiProperty() password: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + recaptcha: string; } diff --git a/server/users/schemas/user.schema.ts b/server/users/schemas/user.schema.ts index 17d250f29..4791e7194 100644 --- a/server/users/schemas/user.schema.ts +++ b/server/users/schemas/user.schema.ts @@ -7,7 +7,7 @@ export interface UserDocument extends User, Document { authenticate(): any; } -@Schema() +@Schema({ timestamps: true }) export class User { @Prop({ required: true }) name: string; diff --git a/server/users/users.controller.ts b/server/users/users.controller.ts index 86ca04ca7..af0d60bcf 100644 --- a/server/users/users.controller.ts +++ b/server/users/users.controller.ts @@ -25,6 +25,7 @@ import { ApiTags } from "@nestjs/swagger"; import { UtilService } from "../util"; import { GetUsersDTO } from "./dto/get-users.dto"; import { Public, AdminOnly, Auth } from "../auth/decorators/auth.decorator"; +import { CaptchaService } from "../captcha/captcha.service"; // TODO: check permissions for routes @Controller(":namespace?") @@ -33,7 +34,8 @@ export class UsersController { private readonly usersService: UsersService, private viewService: ViewService, private configService: ConfigService, - private util: UtilService + private readonly util: UtilService, + private readonly captchaService: CaptchaService ) {} @ApiTags("pages") @@ -52,11 +54,12 @@ export class UsersController { @Public() public async signUp(@Req() req: Request, @Res() res: Response) { const parsedUrl = parse(req.url, true); + const sitekey = this.configService.get("recaptcha_sitekey"); await this.viewService.render( req, res, "/sign-up", - Object.assign(parsedUrl.query) + Object.assign(parsedUrl.query, { sitekey }) ); } @@ -64,6 +67,13 @@ export class UsersController { @Post("api/user/register") @Public() public async register(@Body() createUserDto: CreateUserDTO) { + const validateCaptcha = await this.captchaService.validate( + createUserDto.recaptcha + ); + if (!validateCaptcha) { + throw new UnprocessableEntityException("Error validating captcha"); + } + try { return await this.usersService.register(createUserDto); } catch (errorResponse) { diff --git a/server/users/users.module.ts b/server/users/users.module.ts index 1cf6bafa4..41b21683c 100644 --- a/server/users/users.module.ts +++ b/server/users/users.module.ts @@ -11,6 +11,7 @@ import { UtilService } from "../util"; import { NotificationModule } from "../notifications/notifications.module"; import { SessionGuard } from "../auth/session.guard"; import { M2MGuard } from "../auth/m2m.guard"; +import { CaptchaModule } from "../captcha/captcha.module"; const UserModel = MongooseModule.forFeature([ { @@ -26,7 +27,8 @@ const UserModel = MongooseModule.forFeature([ OryModule, ConfigModule, AbilityModule, - NotificationModule + NotificationModule, + CaptchaModule, ], exports: [UsersService, UserModel], controllers: [UsersController], diff --git a/server/verification-request/dto/personality-with-wikidata.dto.ts b/server/verification-request/dto/personality-with-wikidata.dto.ts new file mode 100644 index 000000000..a1484e572 --- /dev/null +++ b/server/verification-request/dto/personality-with-wikidata.dto.ts @@ -0,0 +1,31 @@ +/** + * DTO for personality data enriched with Wikidata information + */ +export interface PersonalityWithWikidataDto { + /** Personality database ID */ + personalityId: string; + + /** Personality name */ + name: string; + + /** Personality URL slug */ + slug: string; + + /** Personality description (from database or Wikidata) */ + description: string | null; + + /** Wikidata avatar/image URL, null if not available */ + avatar: string | null; + + /** Wikidata entity ID (e.g., Q22686), null if not linked to Wikidata */ + wikidata: string | null; +} + +/** + * Type guard to check if a personality has a valid wikidata ID + */ +export function hasWikidataId( + personality: PersonalityWithWikidataDto +): personality is PersonalityWithWikidataDto & { wikidata: string } { + return personality.wikidata !== null && personality.wikidata !== undefined; +} diff --git a/server/verification-request/dto/stats-verification-request-dto.ts b/server/verification-request/dto/stats-verification-request-dto.ts new file mode 100644 index 000000000..e8c4a6e5b --- /dev/null +++ b/server/verification-request/dto/stats-verification-request-dto.ts @@ -0,0 +1,38 @@ + +interface StatsCount { + total: number; + totalThisMonth: number; + verified: number; + inAnalysis: number; + pending: number; +} + + +interface StatsSourceChannels { + label: string; + value: number; + percentage: number; +} + + +interface StatsRecentActivity { + id: string; + status: string; + sourceChannel: string; + data_hash: string; + timestamp: Date; +} + + +interface StatsDto { + statsCount: StatsCount; + statsSourceChannels: StatsSourceChannels[]; + statsRecentActivity: StatsRecentActivity[]; +} + +export type { + StatsCount, + StatsSourceChannels, + StatsRecentActivity, + StatsDto, +}; diff --git a/server/verification-request/dto/types.ts b/server/verification-request/dto/types.ts index 89c98d54b..05131faea 100644 --- a/server/verification-request/dto/types.ts +++ b/server/verification-request/dto/types.ts @@ -22,7 +22,6 @@ export enum VerificationRequestSourceChannel { Website = "Web", Instagram = "instagram", Whatsapp = "whatsapp", - Telegram = "telegram", AutomatedMonitoring = "automated_monitoring", } @@ -85,3 +84,7 @@ export const EXPECTED_STATES = [ "impactArea", "severity", ]; +export interface TimeStamps { + createdAt: Date; + updatedAt: Date; +} diff --git a/server/verification-request/schemas/verification-request.schema.ts b/server/verification-request/schemas/verification-request.schema.ts index 2b107e129..7f820ab60 100644 --- a/server/verification-request/schemas/verification-request.schema.ts +++ b/server/verification-request/schemas/verification-request.schema.ts @@ -3,12 +3,12 @@ import * as mongoose from "mongoose"; import { Group } from "../../group/schemas/group.schema"; import { Topic } from "../../topic/schemas/topic.schema"; import { ContentModelEnum } from "../../types/enums"; -import { SeverityEnum, VerificationRequestStatus } from "../dto/types"; +import { SeverityEnum, TimeStamps, VerificationRequestStatus } from "../dto/types"; export type VerificationRequestDocument = VerificationRequest & - mongoose.Document; + mongoose.Document & TimeStamps; -@Schema() +@Schema({ timestamps: true }) export class VerificationRequest { @Prop({ required: true, unique: true }) data_hash: string; diff --git a/server/verification-request/state-machine/verification-request.state-machine.ts b/server/verification-request/state-machine/verification-request.state-machine.ts index df9bcb72e..af6f912e2 100644 --- a/server/verification-request/state-machine/verification-request.state-machine.ts +++ b/server/verification-request/state-machine/verification-request.state-machine.ts @@ -1,60 +1,103 @@ -import { CommonStateMachineStates, StateMachineBase } from './base' -import { VerificationRequestStateMachineContext } from './verification-request.state-machine.interface' -import { VerificationRequestService } from '../verification-request.service' -import { VerificationRequestStateMachineStates, VerificationRequestStatus } from '../dto/types' -import { getVerificationRequestStateMachineConfig } from './verification-request.state-machine.config' +import { CommonStateMachineStates, StateMachineBase } from "./base"; +import { VerificationRequestStateMachineContext } from "./verification-request.state-machine.interface"; +import { VerificationRequestService } from "../verification-request.service"; +import { + VerificationRequestStateMachineStates, + VerificationRequestStatus, +} from "../dto/types"; +import { getVerificationRequestStateMachineConfig } from "./verification-request.state-machine.config"; -type StatusToStateMap = Record +type StatusToStateMap = Record< + VerificationRequestStatus, + VerificationRequestStateMachineStates +>; export class VerificationRequestStateMachine extends StateMachineBase { - protected stateMachineConfig = getVerificationRequestStateMachineConfig - private getVerificationRequestService: () => VerificationRequestService - - constructor({ getVerificationRequestService }: { getVerificationRequestService: () => VerificationRequestService }) { - super({ verificationRequestService: null as any }) // Initialize with a temporary value - this.getVerificationRequestService = getVerificationRequestService - this.stateMachineService = { verificationRequestService: getVerificationRequestService() } - } - - protected async getEntityCurrentState(context: VerificationRequestStateMachineContext): Promise { - if (!context.verificationRequest?.id) { - return CommonStateMachineStates.REHYDRATE + protected stateMachineConfig = getVerificationRequestStateMachineConfig; + private getVerificationRequestService: () => VerificationRequestService; + + constructor({ + getVerificationRequestService, + }: { + getVerificationRequestService: () => VerificationRequestService; + }) { + super({ verificationRequestService: null as any }); // Initialize with a temporary value + this.getVerificationRequestService = getVerificationRequestService; + this.stateMachineService = { + verificationRequestService: getVerificationRequestService(), + }; } - const verificationRequest = await this.getVerificationRequestService().getById(context.verificationRequest?.id) - // For PRE_TRIAGE status, determine next state based on what's been executed - if (verificationRequest?.status === VerificationRequestStatus.PRE_TRIAGE) { - const statesExecuted = verificationRequest.statesExecuted || []; - - // Define the expected order of states - const expectedStates = [ - { field: 'embedding', state: VerificationRequestStateMachineStates.EMBEDDING }, - { field: 'identifiedData', state: VerificationRequestStateMachineStates.IDENTIFYING_DATA }, - { field: 'topics', state: VerificationRequestStateMachineStates.DEFINING_TOPICS }, - { field: 'impactArea', state: VerificationRequestStateMachineStates.DEFINING_IMPACT_AREA }, - { field: 'severity', state: VerificationRequestStateMachineStates.DEFINING_SEVERITY } - ]; - - // Find the first missing state - for (const { field, state } of expectedStates) { - if (!statesExecuted.includes(field)) { - console.log(`Next missing step: ${field} -> ${state}`); - return state; + protected async getEntityCurrentState( + context: VerificationRequestStateMachineContext + ): Promise { + if (!context.verificationRequest?.id) { + return CommonStateMachineStates.REHYDRATE; } - } - // All steps completed, but still in PRE_TRIAGE - should not happen - return CommonStateMachineStates.REHYDRATE; - } + const verificationRequest = + await this.getVerificationRequestService().getById( + context.verificationRequest?.id + ); + // For PRE_TRIAGE status, determine next state based on what's been executed + if ( + verificationRequest?.status === VerificationRequestStatus.PRE_TRIAGE + ) { + const statesExecuted = verificationRequest.statesExecuted || []; - // For other statuses, use simple mapping - const statusMap: StatusToStateMap = { - [VerificationRequestStatus.PRE_TRIAGE]: VerificationRequestStateMachineStates.IDENTIFYING_DATA, - [VerificationRequestStatus.IN_TRIAGE]: VerificationRequestStateMachineStates.EMBEDDING, - [VerificationRequestStatus.POSTED]: VerificationRequestStateMachineStates.EMBEDDING, - [VerificationRequestStatus.DECLINED]: VerificationRequestStateMachineStates.EMBEDDING - } + // Define the expected order of states + const expectedStates = [ + { + field: "embedding", + state: VerificationRequestStateMachineStates.EMBEDDING, + }, + { + field: "identifiedData", + state: VerificationRequestStateMachineStates.IDENTIFYING_DATA, + }, + { + field: "topics", + state: VerificationRequestStateMachineStates.DEFINING_TOPICS, + }, + { + field: "impactArea", + state: VerificationRequestStateMachineStates.DEFINING_IMPACT_AREA, + }, + { + field: "severity", + state: VerificationRequestStateMachineStates.DEFINING_SEVERITY, + }, + ]; + + // Find the first missing state + for (const { field, state } of expectedStates) { + if (!statesExecuted.includes(field)) { + this.logger.debug( + `Next missing step: ${field} -> ${state}` + ); + return state; + } + } - return statusMap[verificationRequest?.status] || CommonStateMachineStates.REHYDRATE - } + // All steps completed, but still in PRE_TRIAGE - should not happen + return CommonStateMachineStates.REHYDRATE; + } + + // For other statuses, use simple mapping + const statusMap: StatusToStateMap = { + [VerificationRequestStatus.PRE_TRIAGE]: + VerificationRequestStateMachineStates.IDENTIFYING_DATA, + [VerificationRequestStatus.IN_TRIAGE]: + VerificationRequestStateMachineStates.EMBEDDING, + [VerificationRequestStatus.POSTED]: + VerificationRequestStateMachineStates.EMBEDDING, + [VerificationRequestStatus.DECLINED]: + VerificationRequestStateMachineStates.EMBEDDING, + }; + + return ( + statusMap[verificationRequest?.status] || + CommonStateMachineStates.REHYDRATE + ); + } } diff --git a/server/verification-request/verification-request-stats.service.spec.ts b/server/verification-request/verification-request-stats.service.spec.ts new file mode 100644 index 000000000..e6dbe9071 --- /dev/null +++ b/server/verification-request/verification-request-stats.service.spec.ts @@ -0,0 +1,247 @@ +import { Model } from "mongoose"; +import { getModelToken } from "@nestjs/mongoose"; +import { Test, TestingModule } from "@nestjs/testing"; +import { VerificationRequestService } from "./verification-request.service"; +import { VerificationRequestStatsService } from "./verification-request-stats.service"; +import { VerificationRequestDocument } from "./schemas/verification-request.schema"; +import { createFakeVerificationRequest, mockQuery, mockVerificationRequestModel } from "../mocks/VerificationRequestMock"; + +describe("VerificationRequestStatsService (Unit)", () => { + let testingModule: TestingModule; + let service: VerificationRequestStatsService; + let model: Model; + + beforeAll(async () => { + testingModule = await Test.createTestingModule({ + providers: [ + VerificationRequestStatsService, + { + provide: getModelToken("VerificationRequest"), + useValue: mockVerificationRequestModel, + }, + { + provide: "REQUEST", + useValue: { user: { id: "test-user" } }, + }, + { provide: VerificationRequestService, useValue: {} }, + ], + }).compile(); + + model = testingModule.get>( + getModelToken("VerificationRequest") + ); + }); + + beforeEach(async () => { + service = await testingModule.resolve( + VerificationRequestStatsService + ); + + jest.clearAllMocks(); + }); + + describe("getStatsRecentActivity", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const setupMockData = (data: any[]) => + mockQuery.exec.mockResolvedValue(data); + + describe("Success scenarios", () => { + it("should query the database with the correct fields and apply a limit of 10", async () => { + setupMockData([]); + await (service as any).getStatsRecentActivity(); + + expect(model.find).toHaveBeenCalledWith({}); + expect(mockQuery.limit).toHaveBeenCalledWith(10); + expect(mockQuery.select).toHaveBeenCalledWith( + expect.stringContaining("data_hash") + ); + }); + + it("should transform data_hash to the first 8 characters and map _id to id", async () => { + const mockDoc = createFakeVerificationRequest({ + data_hash: "1234567890ABC", + }); + setupMockData([mockDoc]); + + const [result] = await (service as any).getStatsRecentActivity(); + + expect(result.data_hash).toBe("12345678"); + expect(result.id).toBe(mockDoc._id); + }); + }); + + it("should return an empty array when no activity is found", async () => { + setupMockData([]); + const result = await (service as any).getStatsRecentActivity(); + expect(result).toEqual([]); + }); + + it("should propagate an error if the database query fails", async () => { + const mockError = new Error("DB Error"); + mockQuery.exec.mockRejectedValue(mockError); + await expect((service as any).getStatsRecentActivity()).rejects.toThrow( + mockError + ); + }); + }); + + describe("getStatsCount", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should aggregate statuses correctly and calculate totals using $facet structure", async () => { + const firstDay = new Date("2025-11-01"); + + const MOCK_AGGREGATE_RESULT = [ + { + statuses: [ + { _id: "Posted", count: 10 }, + { _id: "In Triage", count: 5 }, + { _id: "Pre Triage", count: 3 }, + { _id: "Declined", count: 2 }, + ], + totalThisMonth: [{ count: 12 }], + totalCount: [{ count: 20 }], + }, + ]; + + mockVerificationRequestModel.aggregate.mockResolvedValue( + MOCK_AGGREGATE_RESULT + ); + + const result = await (service as any).getStatsCount(firstDay); + + expect(result.total).toBe(20); + expect(result.totalThisMonth).toBe(12); + expect(result.verified).toBe(10); + expect(result.inAnalysis).toBe(5); + + expect(result.pending).toBe(5); + }); + + it("should return zeros when the database returns no data", async () => { + const EMPTY_FACET_RESULT = [ + { + statuses: [], + totalThisMonth: [], + totalCount: [], + }, + ]; + + mockVerificationRequestModel.aggregate.mockReturnValue({ + exec: jest.fn().mockResolvedValue(EMPTY_FACET_RESULT), + }); + + const result = await (service as any).getStatsCount(new Date()); + + expect(result.total).toBe(0); + expect(result.pending).toBe(0); + expect(result.totalThisMonth).toBe(0); + }); + + it("should handle missing status fields gracefully", async () => { + const MALFORMED_RESULT = [ + { statuses: null, totalThisMonth: [], totalCount: [] }, + ]; + mockVerificationRequestModel.aggregate.mockResolvedValue( + MALFORMED_RESULT + ); + + const result = await service["getStatsCount"](new Date()); + expect(result.verified).toBe(0); + }); + }); + + describe("getStatsSourceChannels", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should calculate percentages correctly based on provided totalCount", async () => { + const MOCK_AGGREGATE = [ + { _id: "Whatsapp", count: 50 }, + { _id: "Web", count: 50 }, + ]; + const totalCount = 100; + + mockVerificationRequestModel.aggregate.mockResolvedValue(MOCK_AGGREGATE); + + const result = await (service as any).getStatsSourceChannels(totalCount); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + label: "Whatsapp", + value: 50, + percentage: 50, + }); + }); + + it("should return 0 percent when totalCount is zero to avoid division by zero", async () => { + const MOCK_AGGREGATE = [{ _id: "Whatsapp", count: 10 }]; + + mockVerificationRequestModel.aggregate.mockResolvedValue(MOCK_AGGREGATE); + + const result = await (service as any).getStatsSourceChannels(0); + + expect(result[0].percentage).toBe(0); + }); + + it("should use 'Unknown' label if _id is missing from aggregation result", async () => { + const MOCK_AGGREGATE = [{ _id: null, count: 5 }]; + + mockVerificationRequestModel.aggregate.mockResolvedValue(MOCK_AGGREGATE); + + const result = await (service as any).getStatsSourceChannels(10); + + expect(result[0].label).toBe("Unknown"); + expect(result[0].percentage).toBe(50); + }); + }); + + describe("getStats", () => { + it("should orchestrate the calls to private stat methods and return a combined object", async () => { + const mockCount = { + total: 100, + verified: 50, + inAnalysis: 30, + pending: 20, + totalThisMonth: 10, + }; + const mockChannels = [{ label: "Web", value: 100, percentage: 100 }]; + const mockActivity = [ + { id: "1", status: "Posted", data_hash: "ABC", timestamp: new Date() }, + ]; + + const countSpy = jest + .spyOn(service as any, "getStatsCount") + .mockResolvedValue(mockCount); + const channelsSpy = jest + .spyOn(service as any, "getStatsSourceChannels") + .mockResolvedValue(mockChannels); + const activitySpy = jest + .spyOn(service as any, "getStatsRecentActivity") + .mockResolvedValue(mockActivity); + + const result = await service.getStats(); + + expect(countSpy).toHaveBeenCalled(); + expect(activitySpy).toHaveBeenCalled(); + + expect(channelsSpy).toHaveBeenCalledWith(mockCount.total); + + expect(result).toEqual({ + statsCount: mockCount, + statsSourceChannels: mockChannels, + statsRecentActivity: mockActivity, + }); + + const calledDate = countSpy.mock.calls[0][0] as Date; + expect(calledDate.getDate()).toBe(1); + expect(calledDate.getHours()).toBe(0); + }); + }); +}); diff --git a/server/verification-request/verification-request-stats.service.ts b/server/verification-request/verification-request-stats.service.ts new file mode 100644 index 000000000..0e5c04e59 --- /dev/null +++ b/server/verification-request/verification-request-stats.service.ts @@ -0,0 +1,148 @@ +import { Model } from "mongoose"; +import { InjectModel } from "@nestjs/mongoose"; +import { Injectable, Logger, Scope } from "@nestjs/common"; +import { + StatsCount, + StatsRecentActivity, + StatsSourceChannels, +} from "./dto/stats-verification-request-dto"; +import { VerificationRequestStatus } from "./dto/types"; +import { VerificationRequest, VerificationRequestDocument } from "./schemas/verification-request.schema"; + +@Injectable({ scope: Scope.REQUEST }) +export class VerificationRequestStatsService { + private readonly logger = new Logger(VerificationRequestStatsService.name); + + constructor( + @InjectModel(VerificationRequest.name) + private readonly VerificationRequestModel: Model + ) {} + + /** + * Get statistics for verification requests dashboard + * @returns Statistics object with counts, percentages, and distributions + */ + async getStats() { + try { + const now = new Date(); + const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + + const statsCount = await this.getStatsCount(firstDayOfMonth); + const statsSourceChannels = await this.getStatsSourceChannels( + statsCount.total + ); + const statsRecentActivity = await this.getStatsRecentActivity(); + + return { + statsCount, + statsSourceChannels, + statsRecentActivity, + }; + } catch (error) { + this.logger.error( + `Failed to get dashboard stats: ${error.message}`, + error.stack + ); + + return { + statsCount: { total: 0, totalThisMonth: 0, verified: 0, inAnalysis: 0, pending: 0 }, + statsSourceChannels: [], + statsRecentActivity: [], + }; + } + } + + /** + * Get total counts by status + */ + private async getStatsCount(firstDayOfMonth: Date): Promise { + const result = await this.VerificationRequestModel.aggregate([ + { + $facet: { + statuses: [ + { + $group: { + _id: "$status", + count: { $sum: 1 }, + }, + }, + ], + totalThisMonth: [ + { $match: { date: { $gte: firstDayOfMonth } } }, + { $count: "count" }, + ], + totalCount: [{ $count: "count" }], + }, + }, + ]); + + const data = result[0] || { + statuses: [], + totalThisMonth: [], + totalCount: [], + }; + + const total = data.totalCount[0]?.count || 0; + const totalThisMonth = data.totalThisMonth[0]?.count || 0; + + const statusMap = Object.fromEntries( + (data.statuses || []).map((item) => [item._id, item.count]) + ); + + return { + total, + verified: statusMap[VerificationRequestStatus.POSTED] || 0, + inAnalysis: statusMap[VerificationRequestStatus.IN_TRIAGE] || 0, + pending: + (statusMap[VerificationRequestStatus.PRE_TRIAGE] || 0) + + (statusMap[VerificationRequestStatus.DECLINED] || 0), + totalThisMonth, + }; + } + + /** + * Get source channel distribution + */ + private async getStatsSourceChannels( + totalCount?: number + ): Promise { + const sourceChannelAggregation = + await this.VerificationRequestModel.aggregate([ + { + $group: { + _id: "$sourceChannel", + count: { $sum: 1 }, + }, + }, + { + $sort: { count: -1 }, + }, + ]); + + return sourceChannelAggregation.map((item) => ({ + label: item._id || "Unknown", + value: item.count, + percentage: totalCount > 0 ? (item.count / totalCount) * 100 : 0, + })); + } + + /** + * Get recent activity (last 10 updates) + */ + private async getStatsRecentActivity(): Promise { + const recentRequests = await this.VerificationRequestModel.find({}) + .sort({ updatedAt: -1 }) + .limit(10) + .select("_id status sourceChannel content data_hash updatedAt") + .lean() + .exec(); + + return recentRequests.map((req) => ({ + id: req._id.toString(), + status: req.status, + sourceChannel: req.sourceChannel, + data_hash: req.data_hash.substring(0, 8), + timestamp: req.updatedAt, + })); + } +} diff --git a/server/verification-request/verification-request.controller.ts b/server/verification-request/verification-request.controller.ts index 7f958c5fc..21e2ccf60 100644 --- a/server/verification-request/verification-request.controller.ts +++ b/server/verification-request/verification-request.controller.ts @@ -9,7 +9,6 @@ import { Query, Param, Put, - UseGuards, Logger, } from "@nestjs/common"; import { ApiTags } from "@nestjs/swagger"; @@ -26,13 +25,12 @@ import { CaptchaService } from "../captcha/captcha.service"; import { TargetModel } from "../history/schema/history.schema"; import { VerificationRequestStateMachineService } from "./state-machine/verification-request.state-machine.service"; -import { Public, AdminOnly } from "../auth/decorators/auth.decorator"; -import { AbilitiesGuard } from "../auth/ability/abilities.guard"; -import { - AdminUserAbility, - CheckAbilities, -} from "../auth/ability/ability.decorator"; +import { Public, AdminOnly, RegularUserOnly } from "../auth/decorators/auth.decorator"; +import { StatsDto } from "./dto/stats-verification-request-dto"; import { Roles } from "../auth/ability/ability.factory"; +import { WikidataService } from "../wikidata/wikidata.service"; +import { PersonalityWithWikidataDto } from "./dto/personality-with-wikidata.dto"; +import { VerificationRequestStatsService } from "./verification-request-stats.service"; @Controller(":namespace?") export class VerificationRequestController { @@ -40,13 +38,23 @@ export class VerificationRequestController { constructor( private verificationRequestService: VerificationRequestService, + private readonly verificationRequestStatsService: VerificationRequestStatsService, private configService: ConfigService, private viewService: ViewService, private reviewTaskService: ReviewTaskService, private captchaService: CaptchaService, - private verificationRequestStateMachineService: VerificationRequestStateMachineService + private readonly verificationRequestStateMachineService: VerificationRequestStateMachineService, + private readonly wikidataService: WikidataService ) {} + @ApiTags("verification-request") + @Get("api/verification-request/stats") + @Header("Cache-Control", "max-age=300, must-revalidate") + @Public() + public async getStats(): Promise { + return this.verificationRequestStatsService.getStats(); + } + @ApiTags("verification-request") @Get("api/verification-request") @Public() @@ -57,6 +65,8 @@ export class VerificationRequestController { contentFilters = [], topics = [], order, + startDate, + endDate, severity, sourceChannel, status, @@ -71,6 +81,8 @@ export class VerificationRequestController { page, pageSize, order, + startDate, + endDate, severity, sourceChannel, status, @@ -79,6 +91,8 @@ export class VerificationRequestController { this.verificationRequestService.count({ contentFilters, topics, + startDate, + endDate, severity, sourceChannel, status, @@ -111,6 +125,113 @@ export class VerificationRequestController { return this.verificationRequestService.getById(verificationRequestId); } + /** + * Get personalities associated with a verification request, enriched with Wikidata information + * @param verificationRequestId - The verification request ID + * @param language - Language code for Wikidata labels (default: 'en') + * @returns Array of personalities with Wikidata avatar and metadata + */ + @ApiTags("verification-request") + @Get("api/verification-request/:id/personalities") + @Header("Cache-Control", "max-age=60, must-revalidate") + @Public() + public async getPersonalitiesWithWikidata( + @Param("id") verificationRequestId: string, + @Query("language") language: string = "en" + ): Promise { + const verificationRequest = + await this.verificationRequestService.getByIdWithPopulatedFields( + verificationRequestId, + ["identifiedData"] + ); + + if ( + !verificationRequest?.identifiedData || + verificationRequest.identifiedData.length === 0 + ) { + this.logger.debug( + `No identifiedData found for VR ${verificationRequestId}` + ); + return []; + } + + this.logger.debug( + `Found ${verificationRequest.identifiedData.length} personalities for VR ${verificationRequestId}` + ); + + const personalities: PersonalityWithWikidataDto[] = await Promise.all( + verificationRequest.identifiedData.map( + async ( + personality: any + ): Promise => { + if (!personality?.name) { + this.logger.warn( + `Personality missing name field for VR ${verificationRequestId}` + ); + return { + personalityId: personality._id?.toString() || "", + name: "Unknown", + slug: "", + description: null, + avatar: null, + wikidata: null, + }; + } + + const baseData = { + personalityId: personality._id.toString(), + name: personality.name, + slug: personality.slug || "", + description: personality.description || null, + }; + + if (!personality.wikidata) { + this.logger.debug( + `Personality ${personality.name} has no wikidata ID` + ); + return { + ...baseData, + avatar: null, + wikidata: null, + }; + } + + try { + const wikidataProps = + await this.wikidataService.fetchProperties({ + wikidataId: personality.wikidata, + language, + }); + + return { + ...baseData, + name: wikidataProps.name || personality.name, + description: + wikidataProps.description || + personality.description || + null, + avatar: wikidataProps.avatar || null, + wikidata: personality.wikidata, + }; + } catch (error) { + this.logger.error( + `Error fetching wikidata for personality ${personality.name} (${personality.wikidata}):`, + error.message + ); + + return { + ...baseData, + avatar: null, + wikidata: personality.wikidata, + }; + } + } + ) + ); + + return personalities; + } + @ApiTags("verification-request") @Post("api/verification-request") async create( @@ -268,22 +389,32 @@ export class VerificationRequestController { } @ApiTags("pages") - @Get("verification-request/:data_hash/history") - public async verificationRequestHistoryPage( + @RegularUserOnly() + @Get("verification-request/:data_hash/:viewType") + public async verificationRequestPageHistoryOrTracking( @Req() req: BaseRequest, @Res() res: Response ) { const parsedUrl = parse(req.url, true); - const { data_hash } = req.params; + const { data_hash, viewType } = req.params; const verificationRequest = await this.verificationRequestService.findByDataHash(data_hash); - const queryObject = Object.assign(parsedUrl.query, { - targetId: verificationRequest._id, - targetModel: TargetModel.VerificationRequest, - }); + const view = viewType === "history" ? "/history-page" : "/tracking-page"; + + const queryObject = Object.assign( + parsedUrl.query, + viewType === "history" + ? { + targetId: verificationRequest._id, + targetModel: TargetModel.VerificationRequest, + } + : { + verificationRequestId: verificationRequest._id, + } + ); - await this.viewService.render(req, res, "/history-page", queryObject); + await this.viewService.render(req, res, view, queryObject); } } diff --git a/server/verification-request/verification-request.module.ts b/server/verification-request/verification-request.module.ts index 07fecca49..50fca5441 100644 --- a/server/verification-request/verification-request.module.ts +++ b/server/verification-request/verification-request.module.ts @@ -1,4 +1,4 @@ -import { Module, OnModuleInit } from "@nestjs/common"; +import { Module, OnModuleInit, Logger } from "@nestjs/common"; import { MongooseModule } from "@nestjs/mongoose"; import { ModuleRef } from "@nestjs/core"; import { @@ -22,6 +22,8 @@ import { AbilityModule } from "../auth/ability/ability.module"; import { TopicModule } from "../topic/topic.module"; import { PersonalityModule } from "../personality/personality.module"; import { VerificationRequestStateMachineService } from "./state-machine/verification-request.state-machine.service"; +import { WikidataModule } from "../wikidata/wikidata.module"; +import { VerificationRequestStatsService } from "./verification-request-stats.service"; const VerificationRequestModel = MongooseModule.forFeature([ { @@ -43,20 +45,25 @@ const VerificationRequestModel = MongooseModule.forFeature([ AiTaskModule, CallbackDispatcherModule, AbilityModule, + WikidataModule, TopicModule, PersonalityModule.register(), ], exports: [ VerificationRequestService, + VerificationRequestStatsService, VerificationRequestStateMachineService, ], providers: [ VerificationRequestService, + VerificationRequestStatsService, VerificationRequestStateMachineService, ], controllers: [VerificationRequestController], }) export class VerificationRequestModule implements OnModuleInit { + private readonly logger = new Logger(VerificationRequestModule.name); + constructor( private readonly dispatcher: CallbackDispatcherService, private readonly moduleRef: ModuleRef @@ -67,9 +74,10 @@ export class VerificationRequestModule implements OnModuleInit { this.dispatcher.register( CallbackRoute.VERIFICATION_UPDATE_EMBEDDING, async (params, result) => { - console.log( - `[VerificationRequestModule] EMBEDDING callback invoked with params:`, - params + this.logger.debug( + `EMBEDDING callback invoked with params: ${JSON.stringify( + params + )}` ); const verificationService = await this.moduleRef.resolve( VerificationRequestService @@ -82,9 +90,10 @@ export class VerificationRequestModule implements OnModuleInit { this.dispatcher.register( CallbackRoute.VERIFICATION_UPDATE_IDENTIFYING_DATA, async (params, result) => { - console.log( - `[VerificationRequestModule] IDENTIFYING_DATA callback invoked with params:`, - params + this.logger.debug( + `IDENTIFYING_DATA callback invoked with params: ${JSON.stringify( + params + )}` ); const verificationService = await this.moduleRef.resolve( VerificationRequestService @@ -97,9 +106,10 @@ export class VerificationRequestModule implements OnModuleInit { this.dispatcher.register( CallbackRoute.VERIFICATION_UPDATE_DEFINING_TOPICS, async (params, result) => { - console.log( - `[VerificationRequestModule] TOPICS callback invoked with params:`, - params + this.logger.debug( + `TOPICS callback invoked with params: ${JSON.stringify( + params + )}` ); const verificationService = await this.moduleRef.resolve( VerificationRequestService @@ -112,9 +122,10 @@ export class VerificationRequestModule implements OnModuleInit { this.dispatcher.register( CallbackRoute.VERIFICATION_UPDATE_DEFINING_IMPACT_AREA, async (params, result) => { - console.log( - `[VerificationRequestModule] IMPACT_AREA callback invoked with params:`, - params + this.logger.debug( + `IMPACT_AREA callback invoked with params: ${JSON.stringify( + params + )}` ); const verificationService = await this.moduleRef.resolve( VerificationRequestService @@ -127,9 +138,10 @@ export class VerificationRequestModule implements OnModuleInit { this.dispatcher.register( CallbackRoute.VERIFICATION_UPDATE_DEFINING_SEVERITY, async (params, result) => { - console.log( - `[VerificationRequestModule] SEVERITY callback invoked with params:`, - params + this.logger.debug( + `SEVERITY callback invoked with params: ${JSON.stringify( + params + )}` ); const verificationService = await this.moduleRef.resolve( VerificationRequestService diff --git a/server/verification-request/verification-request.service.spec.ts b/server/verification-request/verification-request.service.spec.ts new file mode 100644 index 000000000..8f39a1afd --- /dev/null +++ b/server/verification-request/verification-request.service.spec.ts @@ -0,0 +1,57 @@ +import { Model } from "mongoose"; +import { Test, TestingModule } from "@nestjs/testing"; +import { getModelToken } from "@nestjs/mongoose"; +import { VerificationRequestService } from "./verification-request.service"; +import { VerificationRequestDocument } from "./schemas/verification-request.schema"; +import { VerificationRequestStateMachineService } from "./state-machine/verification-request.state-machine.service"; +import { SourceService } from "../source/source.service"; +import { GroupService } from "../group/group.service"; +import { HistoryService } from "../history/history.service"; +import { AiTaskService } from "../ai-task/ai-task.service"; +import { TopicService } from "../topic/topic.service"; +import { mockVerificationRequestModel } from "../mocks/VerificationRequestMock"; + +describe("VerificationRequestService (Unit)", () => { + let testingModule: TestingModule; + let service: VerificationRequestService; + let model: Model; + + beforeAll(async () => { + testingModule = await Test.createTestingModule({ + providers: [ + VerificationRequestService, + { + provide: getModelToken("VerificationRequest"), + useValue: mockVerificationRequestModel, + }, + { + provide: "REQUEST", + useValue: { user: { id: "test-user" } }, + }, + { provide: VerificationRequestStateMachineService, useValue: {} }, + { provide: SourceService, useValue: {} }, + { provide: GroupService, useValue: {} }, + { provide: HistoryService, useValue: {} }, + { provide: AiTaskService, useValue: {} }, + { provide: TopicService, useValue: {} }, + { provide: "PersonalityService", useValue: {} }, + ], + }).compile(); + + model = testingModule.get>( + getModelToken("VerificationRequest") + ); + }); + + beforeEach(async () => { + service = await testingModule.resolve( + VerificationRequestService + ); + + jest.clearAllMocks(); + }); + + it("should be defined", () => { + expect(service).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/server/verification-request/verification-request.service.ts b/server/verification-request/verification-request.service.ts index 633423bfa..65a350786 100644 --- a/server/verification-request/verification-request.service.ts +++ b/server/verification-request/verification-request.service.ts @@ -15,6 +15,7 @@ import { HistoryType, TargetModel } from "../history/schema/history.schema"; import { AiTaskService } from "../ai-task/ai-task.service"; import { CreateAiTaskDto } from "../ai-task/dto/create-ai-task.dto"; import { VerificationRequestStateMachineService } from "./state-machine/verification-request.state-machine.service"; +import { buildDateQuery } from "../../src/utils/date.utils"; import { BadRequestException, Injectable, @@ -43,7 +44,7 @@ export class VerificationRequestService { constructor( @Inject(REQUEST) private readonly req: BaseRequest, @InjectModel(VerificationRequest.name) - private VerificationRequestModel: Model, + private readonly VerificationRequestModel: Model, @Inject(forwardRef(() => VerificationRequestStateMachineService)) private readonly verificationRequestStateService: VerificationRequestStateMachineService, private sourceService: SourceService, @@ -177,7 +178,9 @@ export class VerificationRequestService { await vr.save(); } - const currentUser = user || this.req?.user; + const currentUser = user?._id + ? user._id + : user || this.req.user?._id; const history = this.historyService.getHistoryParams( vr._id, @@ -604,7 +607,8 @@ export class VerificationRequestService { .populate("group") .populate("source") .populate("impactArea") - .populate("topics"); + .populate("topics") + .populate("identifiedData"); } return this.VerificationRequestModel.findOne({ data_hash }); @@ -649,7 +653,7 @@ export class VerificationRequestService { { new: true, upsert: true } ); } catch (error) { - console.error( + this.logger.error( "Failed to remove verification request from group:", error ); @@ -700,15 +704,7 @@ export class VerificationRequestService { return src._id; }) ); - - updatedVerificationRequestData.source = Array.from( - new Set([ - ...(verificationRequest.source || []).map((sourceId) => - sourceId.toString() - ), - ...newSourceIds.map((id) => id.toString()), - ]) - ).map((id) => Types.ObjectId(id)); + updatedVerificationRequestData.source = newSourceIds.map((id) => Types.ObjectId(id)); } if ( @@ -722,7 +718,7 @@ export class VerificationRequestService { ); } - const user = this.req.user; + const user = this.req.user?._id; const history = this.historyService.getHistoryParams( verificationRequest._id, @@ -739,9 +735,10 @@ export class VerificationRequestService { verificationRequest._id, updatedVerificationRequestData, { new: true, upsert: true } - ).populate("source"); + ).populate("source") + .populate("impactArea"); } catch (error) { - console.error("Failed to update verification request:", error); + this.logger.error("Failed to update verification request:", error); throw error; } } @@ -923,9 +920,9 @@ export class VerificationRequestService { async updateVerificationRequestWithTopics(topics, data_hash) { const verificationRequest = await this.findByDataHash(data_hash, false); const foundTopics = await this.topicService.findByWikidataIds( - topics.map(topic => topic.value || topic.wikidataId) + topics.map((topic) => topic.value || topic.wikidataId) ); - const topicIds = foundTopics.map(topic => topic._id); + const topicIds = foundTopics.map((topic) => topic._id); const latestVerificationRequest = verificationRequest.toObject(); @@ -934,7 +931,7 @@ export class VerificationRequestService { topics: topicIds, }; - const user = this.req.user; + const user = this.req.user?._id; const history = this.historyService.getHistoryParams( verificationRequest._id, @@ -960,6 +957,8 @@ export class VerificationRequestService { sourceChannel?: string; status?: string[]; impactArea?: string[]; + startDate?: string; + endDate?: string; }): Promise> { const { contentFilters, @@ -968,6 +967,8 @@ export class VerificationRequestService { sourceChannel, status, impactArea, + startDate, + endDate, } = filters; const query: any = {}; @@ -983,8 +984,7 @@ export class VerificationRequestService { Types.ObjectId(impactArea._id) ); - if (topicIds.length) - orConditions.push({ topics: { $in: topicIds } }); + if (topicIds.length) orConditions.push({ topics: { $in: topicIds } }); if (impactAreaIds.length) orConditions.push({ impactArea: { $in: impactAreaIds } }); @@ -996,6 +996,10 @@ export class VerificationRequestService { orConditions.push(...contentConditions); } + const dateQuery = buildDateQuery(startDate, endDate); + + if (dateQuery) query.date = dateQuery; + if (orConditions.length) { query.$or = orConditions; } diff --git a/server/view/view.controller.ts b/server/view/view.controller.ts index 9864481e1..f1b375a98 100644 --- a/server/view/view.controller.ts +++ b/server/view/view.controller.ts @@ -10,7 +10,7 @@ import { import type { Request, Response } from "express"; import { parse } from "url"; import { ViewService } from "./view.service"; -import { IsPublic } from "../auth/decorators/is-public.decorator"; +import { Public } from "../auth/decorators/auth.decorator"; import { ApiTags } from "@nestjs/swagger"; @Controller("/") @@ -27,7 +27,7 @@ export class ViewController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("about") @Header("Cache-Control", "max-age=86400") @@ -36,7 +36,7 @@ export class ViewController { await this.viewService.render(req, res, "/about-page", parsedUrl.query); } - @IsPublic() + @Public() @ApiTags("pages") @Get("signup-invite") @Header("Cache-Control", "max-age=86400") @@ -50,7 +50,7 @@ export class ViewController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("about/:person") @Header("Cache-Control", "max-age=86400") @@ -65,7 +65,7 @@ export class ViewController { res.redirect(302, "/about"); } - @IsPublic() + @Public() @ApiTags("pages") @Get("supportive-materials") @Header("Cache-Control", "max-age=86400") @@ -82,7 +82,7 @@ export class ViewController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("privacy-policy") @Header("Cache-Control", "max-age=86400") @@ -99,7 +99,7 @@ export class ViewController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("code-of-conduct") @Header("Cache-Control", "max-age=86400") @@ -113,7 +113,7 @@ export class ViewController { ); } - @IsPublic() + @Public() @Get("_next*") @Header("Cache-Control", "max-age=60") public async assets(@Req() req: Request, @Res() res: Response) { @@ -130,7 +130,7 @@ export class ViewController { * Redirects to our custom 404 page. * The render404() method was not used here as it conflicts with our i18n strategy. */ - @IsPublic() + @Public() @ApiTags("pages") @Get("404") @Header("Cache-Control", "max-age=86400") @@ -151,7 +151,7 @@ export class ViewController { ); } - @IsPublic() + @Public() @ApiTags("pages") @Get("unauthorized") public async acessDeniedPage( diff --git a/server/wikidata/schemas/wikidata.schema.ts b/server/wikidata/schemas/wikidata.schema.ts index 09171c48d..e4eaed63b 100644 --- a/server/wikidata/schemas/wikidata.schema.ts +++ b/server/wikidata/schemas/wikidata.schema.ts @@ -3,7 +3,7 @@ import { Document } from "mongoose"; export type WikidataCacheDocument = WikidataCache & Document; -@Schema() +@Schema({ timestamps: true }) export class WikidataCache { @Prop({ required: true }) wikidataId: string; @@ -13,9 +13,6 @@ export class WikidataCache { @Prop({ type: Object, required: true }) props: object; - - @Prop({ default: Date.now }) - createdAt: Date; } export const WikidataCacheSchema = SchemaFactory.createForClass(WikidataCache); diff --git a/server/wikidata/wikidata.service.ts b/server/wikidata/wikidata.service.ts index f8b068b40..60c37604e 100644 --- a/server/wikidata/wikidata.service.ts +++ b/server/wikidata/wikidata.service.ts @@ -1,6 +1,7 @@ import { Model } from "mongoose"; import { Injectable } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; +import { z } from "zod"; import { WikidataCache, WikidataCacheDocument, @@ -17,6 +18,60 @@ const WIKIMEDIA_HEADERS = { "Aletheia/1.0 (https://github.com/AletheiaFact/aletheia; contato@aletheiafact.org)", }; +const SUPPORTED_LANGUAGES = ["pt", "en", "pt-br"] as const; +type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]; + +export interface WikidataParamsInput { + wikidataId: string; + language?: string; +} + +export interface WikidataParams { + wikidataId: string; + language: SupportedLanguage; +} + +export const WikidataParamsSchema = z.object({ + wikidataId: z + .string() + .regex(/^Q\d+$/, "WikidataId must be in format Q followed by numbers"), + language: z + .string() + .optional() + .transform((val) => { + if (!val) return "en" as SupportedLanguage; + return SUPPORTED_LANGUAGES.includes(val as SupportedLanguage) + ? (val as SupportedLanguage) + : ("en" as SupportedLanguage); + }) + .default("en" as SupportedLanguage), +}); + +export interface WikidataProperties { + name?: string; + description?: string; + isAllowedProp?: boolean; + image?: string; + wikipedia?: string; + avatar?: string; + twitterAccounts: string[]; +} + +export interface WikibaseEntity { + name: string; + description?: string; + wikidata: string; + aliases?: string[]; + matchedAlias?: string | null; +} + +interface WikibaseSearchResult { + id: string; + label?: string; + description?: string; + aliases?: string[]; +} + @Injectable() export class WikidataService { constructor( @@ -24,26 +79,52 @@ export class WikidataService { private wikidataCache: Model ) {} - async fetchProperties(params) { + /** + * Fetches Wikidata properties from cache or API + * @param params - Wikidata parameters (accepts flexible input) + * @returns Wikidata properties object + */ + async fetchProperties( + params: WikidataParamsInput + ): Promise { + const validatedParams = WikidataParamsSchema.parse( + params + ) as WikidataParams; + const wikidataCache = await this.wikidataCache .findOne({ - wikidataId: params.wikidataId, - language: params.language, + wikidataId: validatedParams.wikidataId, + language: validatedParams.language, }) .exec(); + if (!wikidataCache) { - const props = await this.requestProperties(params); - const newWikidataCache = new this.wikidataCache({ - ...params, - props, - }); - newWikidataCache.save(); + const props = await this.requestProperties(validatedParams); + if (props.isAllowedProp === false) { + return props; + } + await this.wikidataCache.findOneAndUpdate( + { + wikidataId: validatedParams.wikidataId, + language: validatedParams.language, + }, + { $setOnInsert: { ...validatedParams, props } }, + { upsert: true } + ); return props; } - return wikidataCache.props; + + return wikidataCache.props as WikidataProperties; } - async requestProperties(params) { + /** + * Requests Wikidata properties from the Wikidata API + * @param params - Validated Wikidata parameters + * @returns Wikidata properties object + */ + async requestProperties( + params: WikidataParams + ): Promise { const { data } = await axios.get("https://www.wikidata.org/w/api.php", { params: { action: "wbgetentities", @@ -60,8 +141,17 @@ export class WikidataService { ); } - async extractProperties(wikidata, language = "en"): Promise { - const wikidataProps = { + /** + * Extracts properties from Wikidata entity response + * @param wikidata - Wikidata entity object (can be any structure) + * @param language - Language code for labels and descriptions + * @returns Extracted Wikidata properties + */ + async extractProperties( + wikidata: any, + language: string = "en" + ): Promise { + const wikidataProps: WikidataProperties = { name: undefined, description: undefined, isAllowedProp: undefined, @@ -71,7 +161,7 @@ export class WikidataService { twitterAccounts: [], }; if (!wikidata) { - return {}; + return wikidataProps; } // Get label for the personality name @@ -142,14 +232,30 @@ export class WikidataService { return wikidataProps; } - getSiteLinkName(language) { + /** + * Gets the site link name for Wikipedia based on language + * @param language - Language code + * @returns Wiki site link name (e.g., 'ptwiki', 'enwiki') + */ + getSiteLinkName(language: string): string { if (languageVariantMap[language]) { language = languageVariantMap[language]; } return `${language}wiki`; } - extractValue(wikidata, property, language) { + /** + * Extracts a value from Wikidata entity for a specific property and language + * @param wikidata - Wikidata entity object + * @param property - Property name to extract (e.g., 'labels', 'descriptions') + * @param language - Language code + * @returns Extracted value or undefined + */ + extractValue( + wikidata: any, + property: string, + language: string + ): string | undefined { if (!wikidata[property]) { return; } @@ -163,7 +269,11 @@ export class WikidataService { ); } - queryWikibaseEntities(query, language = "en") { + queryWikibaseEntities( + query: string, + language: string = "en", + includeAliases: boolean = true + ): Promise { const params = { action: "wbsearchentities", search: query, @@ -181,22 +291,44 @@ export class WikidataService { headers: WIKIMEDIA_HEADERS, }) .then((response) => { - const { search } = response && response.data; - return search.flatMap((wbentity) => { - return wbentity.label - ? [ - { - name: wbentity.label, - description: wbentity.description, - wikidata: wbentity.id, - }, - ] - : []; + const { search }: { search: WikibaseSearchResult[] } = + response && response.data; + + return search.flatMap((searchResult: WikibaseSearchResult) => { + if (!searchResult.label) { + return []; + } + + const result: WikibaseEntity = { + name: searchResult.label, + description: searchResult.description, + wikidata: searchResult.id, + }; + + if (includeAliases) { + const aliases = searchResult.aliases || []; + const matchedAlias = aliases.find((alias) => + alias.toLowerCase().includes(query.toLowerCase()) + ); + result.aliases = aliases; + result.matchedAlias = matchedAlias || null; + } + + return [result]; }); }); } - async getCommonsThumbURL(imageTitle, imageSize) { + /** + * Gets the thumbnail URL from Wikimedia Commons + * @param imageTitle - Title of the image on Commons + * @param imageSize - Desired image width in pixels + * @returns Thumbnail URL or undefined + */ + async getCommonsThumbURL( + imageTitle: string, + imageSize: number + ): Promise { const { data } = await axios.get( "https://commons.wikimedia.org/w/api.php", { diff --git a/server/winstonLogger.ts b/server/winstonLogger.ts index 856ac2a81..5c8074c63 100644 --- a/server/winstonLogger.ts +++ b/server/winstonLogger.ts @@ -1,14 +1,40 @@ import { LoggerService } from "@nestjs/common"; import * as winston from "winston"; +const isDevelopment = process.env.NODE_ENV !== "production"; + +const devFormat = winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.colorize({ all: true }), + winston.format.printf(({ timestamp, level, message, context, trace }) => { + const ctx = context ? `[${context}]` : ""; + const traceStr = trace ? `\n → ${trace}` : ""; + return `${timestamp} ${level} ${ctx} ${message}${traceStr}`; + }) +); + +const prodFormat = winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), + winston.format.errors({ stack: true }), + winston.format.json() +); + +const getLogLevel = (): string => { + if (process.env.LOG_LEVEL) { + return process.env.LOG_LEVEL; + } + return isDevelopment ? "debug" : "info"; +}; + export class WinstonLogger implements LoggerService { private logger: winston.Logger; constructor() { this.logger = winston.createLogger({ + level: getLogLevel(), transports: [ new winston.transports.Console({ - format: winston.format.combine(winston.format.json()), + format: isDevelopment ? devFormat : prodFormat, }), ], }); @@ -18,7 +44,9 @@ export class WinstonLogger implements LoggerService { this.logger.info(message, { context }); } - error(message: string, trace: string, context?: string) { + error(message: string, traceOrError?: string | Error, context?: string) { + const trace = + traceOrError instanceof Error ? traceOrError.stack : traceOrError; this.logger.error(message, { trace, context }); } diff --git a/src/api/historyApi.ts b/src/api/historyApi.ts index d957bbbeb..22285692e 100644 --- a/src/api/historyApi.ts +++ b/src/api/historyApi.ts @@ -5,14 +5,15 @@ const request = axios.create({ baseURL: `/api/history`, }); -type optionsType = { +type OptionsType = { targetId: string; targetModel: string; - page: number; - order: "asc" | "desc"; - pageSize: number; + page?: number; + order?: "asc" | "desc"; + pageSize?: number; }; -const getByTargetId = (options: optionsType) => { + +const getByTargetId = (options: OptionsType) => { const params = { page: options.page ? options.page - 1 : 0, order: options.order || "asc", @@ -31,12 +32,13 @@ const getByTargetId = (options: optionsType) => { }; }) .catch((err) => { - // TODO: use Sentry instead - // console.log(err); + console.error(err); + return { data: [], total: 0, totalPages: 0 }; }); }; const HistoryApi = { getByTargetId, }; + export default HistoryApi; diff --git a/src/api/sentenceApi.ts b/src/api/sentenceApi.ts index e326abde7..8ac61075a 100644 --- a/src/api/sentenceApi.ts +++ b/src/api/sentenceApi.ts @@ -17,6 +17,21 @@ const getSentenceTopicsByDatahash = (data_hash) => { }); }; +const getSentencesWithCop30Topics = () => { + return request + .get("/cop30") + .then((response) => { + return response.data; + }) + .catch((err) => { + throw err; + }); +}; + +const getCop30Stats = () => { + return request.get("/cop30/stats").then((response) => response.data); +}; + const deleteSentenceTopic = (topics, data_hash, t) => { return request .put(`/${data_hash}`, topics) @@ -32,6 +47,8 @@ const deleteSentenceTopic = (topics, data_hash, t) => { const SentenceApi = { deleteSentenceTopic, + getSentencesWithCop30Topics, + getCop30Stats, getSentenceTopicsByDatahash, }; diff --git a/src/api/trackingApi.ts b/src/api/trackingApi.ts new file mode 100644 index 000000000..cba0caef0 --- /dev/null +++ b/src/api/trackingApi.ts @@ -0,0 +1,30 @@ +import axios from "axios"; +import { HEX24 } from "../types/History"; +import { MessageManager } from "../components/Messages"; +import { TFunction } from "i18next"; + +const request = axios.create({ + withCredentials: true, + baseURL: `/api/tracking`, +}); + +const getTrackingById = (verificationRequestId: string, t: TFunction) => { + if (!HEX24.test(verificationRequestId)) { + MessageManager.showMessage("error", t("tracking:errorInvalidId")); + return Promise.reject(new Error("Invalid ID")); + } + + return request + .get(`/${verificationRequestId}`) + .then((response) => response.data) + .catch((err) => { + MessageManager.showMessage("error", t("tracking:errorFetchData")); + throw err; + }); +}; + +const TrackingApi = { + getTrackingById, +}; + +export default TrackingApi; diff --git a/src/api/verificationRequestApi.ts b/src/api/verificationRequestApi.ts index a6dfd716c..4e2267f5c 100644 --- a/src/api/verificationRequestApi.ts +++ b/src/api/verificationRequestApi.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { ActionTypes } from "../store/types"; import { MessageManager } from "../components/Messages"; import { NameSpaceEnum } from "../types/Namespace"; +import { PersonalityWithWikidata } from "../types/PersonalityWithWikidata"; interface SearchOptions { searchText?: string; page?: number; @@ -9,6 +10,8 @@ interface SearchOptions { order?: string; topics?: any; filtersUsed?: any; + startDate?: string; + endDate?: string; severity?: string; noCache?: boolean; sourceChannel?: string; @@ -57,6 +60,8 @@ const get = (options: SearchOptions, dispatch = null) => { order: options.order || "desc", pageSize: options.pageSize ? options.pageSize : 10, topics: options.topics || [], + startDate: options.startDate || undefined, + endDate: options.endDate || undefined, severity: options.severity || "all", sourceChannel: options.sourceChannel || "all", status: options.status || [], @@ -161,13 +166,17 @@ const updateVerificationRequestWithTopics = (topics, data_hash, t) => { return request .put(`/${data_hash}/topics`, topics) .then((response) => { - MessageManager.showMessage("success", + MessageManager.showMessage( + "success", t("verificationRequest:addVerificationRequestSuccess") ); return response.data; }) .catch((e) => { - MessageManager.showMessage("error", t("verificationRequest:addVerificationRequestError")); + MessageManager.showMessage( + "error", + t("verificationRequest:addVerificationRequestError") + ); console.error("error while updating verification request", e); }); }; @@ -204,6 +213,39 @@ const deleteVerificationRequestTopic = (topics, data_hash, t) => { }); }; +const getVerificationRequestStats = () => { + return request + .get(`/stats`) + .then((response) => { + return response.data; + }) + .catch((e) => { + console.error("error while getting verification request stats", e); + throw e; + }); +}; + +/** + * Fetch personalities associated with a verification request, enriched with Wikidata information + * @param verificationRequestId - The verification request ID + * @param language - Language code for Wikidata labels (default: 'en') + * @returns Promise resolving to array of personalities with Wikidata avatar and metadata + */ +const getPersonalitiesWithWikidata = ( + verificationRequestId: string, + language: string = "en" +): Promise => { + return request + .get(`/${verificationRequestId}/personalities`, { + params: { language }, + }) + .then((response) => response.data) + .catch((err) => { + console.error("error while getting personalities", err); + return []; + }); +}; + const verificationRequestApi = { createVerificationRequest, get, @@ -213,6 +255,8 @@ const verificationRequestApi = { updateVerificationRequestWithTopics, removeVerificationRequestFromGroup, deleteVerificationRequestTopic, + getVerificationRequestStats, + getPersonalitiesWithWikidata, }; export default verificationRequestApi; diff --git a/src/components/AletheiaMenu.tsx b/src/components/AletheiaMenu.tsx index ac9d94a66..52cd63765 100644 --- a/src/components/AletheiaMenu.tsx +++ b/src/components/AletheiaMenu.tsx @@ -12,6 +12,7 @@ import { Roles } from "../types/enums"; import { NameSpaceEnum } from "../types/Namespace"; import { currentNameSpace } from "../atoms/namespace"; import localConfig from "../../config/localConfig"; +import { isAdmin } from "../utils/GetUserPermission"; const AletheiaMenu = () => { const { t } = useTranslation(); @@ -99,7 +100,7 @@ const AletheiaMenu = () => { {t("menu:kanbanItem")} )} - {(role === Roles.Admin || role === Roles.SuperAdmin) && ( + {isAdmin(role) && ( <> void; +} + +const ClaimContentDisplay: React.FC = ({ isImage, title, claimContent, showHighlights, dispatchPersonalityAndClaim, }) => { - const { selectedContent } = useAppSelector((state) => state); - const dispatch = useDispatch(); const imageUrl = claimContent.content; const paragraphs = Array.isArray(claimContent) ? claimContent : [claimContent]; - const handleClickOnImage = () => { - ImageApi.getImageTopicsByDatahash(selectedContent?.data_hash) - .then((image) => { - dispatch(actions.setSelectContent(image)); - }) - .catch((e) => e); - dispatch(actions.openReviewDrawer()); - }; - return ( <> {isImage ? ( -

- -
+ ) : ( = ({ + imageUrl, + title, + classification, + dataHash, +}) => { + const dispatch = useDispatch(); + const { t } = useTranslation(); + const { selectedContent } = useAppSelector((state) => state); + + const handleClickOnButton = (): void => { + ImageApi.getImageTopicsByDatahash( + dataHash || selectedContent?.data_hash + ) + .then((image) => { + dispatch(actions.setSelectContent(image)); + }) + .catch((error) => { + console.error("Failed to fetch image topics:", error); + }); + dispatch(actions.openReviewDrawer()); + }; + + const getButtonText = (): string => { + if (classification) { + return t("claim:openReportButton"); + } + return t("claim:reviewImageButton"); + }; + + return ( + + + + + + + {getButtonText()} + + + + ); +}; + +export default ClaimImageBody; diff --git a/src/components/Claim/ClaimView.tsx b/src/components/Claim/ClaimView.tsx index 2fe2d353e..e68ee43da 100644 --- a/src/components/Claim/ClaimView.tsx +++ b/src/components/Claim/ClaimView.tsx @@ -1,4 +1,4 @@ -import { Grid, Typography } from "@mui/material" +import { Grid, Typography } from "@mui/material"; import React, { useEffect, useState } from "react"; import { ContentModelEnum, Roles, TargetModel } from "../../types/enums"; @@ -17,6 +17,7 @@ import claimApi from "../../api/claim"; import { currentUserRole } from "../../atoms/currentUser"; import { useAtom } from "jotai"; import AffixButtonV2 from "../Collaborative/Components/AffixButtonV2"; +import { isAdmin } from "../../utils/GetUserPermission"; const ClaimView = ({ personality, claim, href, hideDescriptions }) => { const dispatch = useDispatch(); @@ -44,7 +45,7 @@ const ClaimView = ({ personality, claim, href, hideDescriptions }) => { return ( <> - {(role === Roles.Admin || role === Roles.SuperAdmin) && ( + {isAdmin(role) && ( { )}
- @@ -94,22 +96,37 @@ const ClaimView = ({ personality, claim, href, hideDescriptions }) => { } /> - { - setShowHighlights(e.target.value); - }} - labelTrue={t("claim:showHighlightsButton")} - labelFalse={t("claim:hideHighlightsButton")} - /> - } - /> + {!isImage && ( + { + setShowHighlights( + e.target.value + ); + }} + labelTrue={t( + "claim:showHighlightsButton" + )} + labelFalse={t( + "claim:hideHighlightsButton" + )} + /> + } + /> + )} {sources.length > 0 && ( <> - + {t("claim:sourceSectionTitle")} { diff --git a/src/components/ClaimReview/ClaimReviewHeader.tsx b/src/components/ClaimReview/ClaimReviewHeader.tsx index a7816d64a..ed16d99e7 100644 --- a/src/components/ClaimReview/ClaimReviewHeader.tsx +++ b/src/components/ClaimReview/ClaimReviewHeader.tsx @@ -74,10 +74,10 @@ const ClaimReviewHeader = ({ /> {showTopicInput && ( )} diff --git a/src/components/ClaimReview/ClaimReviewView.tsx b/src/components/ClaimReview/ClaimReviewView.tsx index 4afe2b98c..b1021f62b 100644 --- a/src/components/ClaimReview/ClaimReviewView.tsx +++ b/src/components/ClaimReview/ClaimReviewView.tsx @@ -19,6 +19,7 @@ import { useAppSelector } from "../../store/store"; import { ReviewTaskStates } from "../../machines/reviewTask/enums"; import { generateReviewContentPath } from "../../utils/GetReviewContentHref"; import SentenceReportPreviewView from "../SentenceReport/SentenceReportPreviewView"; +import { isAdmin } from "../../utils/GetUserPermission"; export interface ClaimReviewViewProps { content: Content; @@ -51,8 +52,13 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => { const hasStartedTask = machineService.state.value !== ReviewTaskStates.unassigned; const origin = window.location.origin ? window.location.origin : ""; - const isClaimTypeAndNotSmallScreen = reviewTaskType === "Claim" && !vw.sm || !hasStartedTask || !userIsNotRegular; - const isSourceOrVerificationRequest = reviewTaskType === "Source" || reviewTaskType === "VerificationRequest"; + const isImageContent = target?.contentModel === "Image"; + const isClaimTypeAndNotSmallScreen = + (reviewTaskType === "Claim" && (!vw.sm || isImageContent)) || + !hasStartedTask || + !userIsNotRegular; + const isSourceOrVerificationRequest = + reviewTaskType === "Source" || reviewTaskType === "VerificationRequest"; const componentStyle = { span: 9, @@ -75,7 +81,7 @@ const ClaimReviewView = (props: ClaimReviewViewProps) => { return (
- {(role === Roles.Admin || role === Roles.SuperAdmin) && ( + {isAdmin(role) && ( <> {review?.isPublished ? ( { )} )} - {(isClaimTypeAndNotSmallScreen || isSourceOrVerificationRequest) && ( - - )} + {(isClaimTypeAndNotSmallScreen || + isSourceOrVerificationRequest) && ( + + )} {enableViewReportPreview ? ( { const { t } = useTranslation(); @@ -31,7 +32,7 @@ const ReviewAlert = ({ isHidden, isPublished, hideDescription }) => { reviewNotStartedSelector ); const reviewData = useSelector(machineService, reviewDataSelector); - const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const userIsAdmin = isAdmin(role); const isCrossChecking = useSelector(machineService, crossCheckingSelector); const isAddCommentCrossChecking = useSelector( machineService, diff --git a/src/components/ClaimReview/ReviewContent.tsx b/src/components/ClaimReview/ReviewContent.tsx index 8cc6d6a9d..151029302 100644 --- a/src/components/ClaimReview/ReviewContent.tsx +++ b/src/components/ClaimReview/ReviewContent.tsx @@ -2,8 +2,19 @@ import React from "react"; import ImageClaim from "../ImageClaim"; import ReviewContentStyled from "./ReviewContent.style"; import Typography from "@mui/material/Typography"; +import { Box } from "@mui/material"; -const ReviewContent = ({ +interface ReviewContentProps { + title: string; + content: string; + isImage: boolean; + contentPath?: string | null; + linkText?: string | null; + style?: React.CSSProperties; + ellipsis?: boolean; +} + +const ReviewContent: React.FC = ({ title, content, isImage, @@ -12,6 +23,52 @@ const ReviewContent = ({ style = {}, ellipsis = false, }) => { + if (isImage) { + return ( + + + + {title} + + + + + {contentPath && linkText && ( + + + {linkText} + + + )} + + + ); + } + return ( {title} - {isImage && } {!ellipsis && contentPath && linkText && ( {linkText} diff --git a/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx b/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx index 121fe8226..54e2067f8 100644 --- a/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx +++ b/src/components/ClaimReview/form/DynamicReviewTaskForm.tsx @@ -29,6 +29,7 @@ import WarningModal from "../../Modal/WarningModal"; import { currentNameSpace } from "../../../atoms/namespace"; import { CommentEnum, Roles } from "../../../types/enums"; import useAutoSaveDraft from "./hooks/useAutoSaveDraft"; +import { isAdmin } from "../../../utils/GetUserPermission"; const DynamicReviewTaskForm = ({ data_hash, personality, target }) => { const { @@ -123,9 +124,9 @@ const DynamicReviewTaskForm = ({ data_hash, personality, target }) => { const isValidReviewer = event === ReviewTaskEvents.sendToCrossChecking ? !data.crossCheckerId || - !reviewData.usersId.includes(data.crossCheckerId) + !reviewData.usersId.includes(data.crossCheckerId) : !data.reviewerId || - !reviewData.usersId.includes(data.reviewerId); + !reviewData.usersId.includes(data.reviewerId); setReviewerError(!isValidReviewer); return isValidReviewer; @@ -202,7 +203,7 @@ const DynamicReviewTaskForm = ({ data_hash, personality, target }) => { const userIsReviewer = reviewData.reviewerId === userId; const userIsCrossChecker = reviewData.crossCheckerId === userId; const userIsAssignee = reviewData.usersId.includes(userId); - const userIsAdmin = role === Roles.Admin || role === Roles.SuperAdmin; + const userIsAdmin = isAdmin(role); if ( isReported && diff --git a/src/components/Collaborative/VisualEditorProvider.tsx b/src/components/Collaborative/VisualEditorProvider.tsx index 91e052cd9..c9b3d383e 100644 --- a/src/components/Collaborative/VisualEditorProvider.tsx +++ b/src/components/Collaborative/VisualEditorProvider.tsx @@ -1,4 +1,11 @@ -import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from "react"; import { useAppSelector } from "../../store/store"; import { createWebsocketConnection } from "./utils/createWebsocketConnection"; import ReviewTaskApi from "../../api/reviewTaskApi"; @@ -27,6 +34,8 @@ interface ContextType { export const VisualEditorContext = createContext({}); +const editorConfig = new EditorConfig(); + interface VisualEditorProviderProps { data_hash: string; children: React.ReactNode; @@ -34,7 +43,6 @@ interface VisualEditorProviderProps { } export const VisualEditorProvider = (props: VisualEditorProviderProps) => { - const editorConfig = new EditorConfig(); const { machineService, reportModel, reviewTaskType } = useContext( ReviewTaskMachineContext ); @@ -101,26 +109,29 @@ export const VisualEditorProvider = (props: VisualEditorProviderProps) => { }; }, [enableCollaborativeEdit, props.data_hash, websocketUrl]); - const extensions = useMemo( + const getExtensions = useCallback( () => editorConfig.getExtensions( reviewTaskType, websocketProvider, enableEditorAnnotations ), - [websocketProvider, reviewTaskType] + [reviewTaskType, websocketProvider, enableEditorAnnotations] ); - const editorConfiguration = { - readonly, - extensions, - isCollaborative, - core: { excludeExtensions: ["history"] }, - stringHandler: "html", - content: isCollaborative - ? undefined - : (editorContentObject as RemirrorContentType), - }; + const editorConfiguration = useMemo( + () => ({ + readonly, + extensions: getExtensions, + isCollaborative, + core: { excludeExtensions: ["history"] }, + stringHandler: "html", + content: isCollaborative + ? undefined + : (editorContentObject as RemirrorContentType), + }), + [readonly, getExtensions, isCollaborative, editorContentObject] + ); const value = useMemo( () => ({ diff --git a/src/components/DateRangePicker.tsx b/src/components/DateRangePicker.tsx new file mode 100644 index 000000000..2373a3830 --- /dev/null +++ b/src/components/DateRangePicker.tsx @@ -0,0 +1,66 @@ +import React, { useState } from "react"; +import TextField from "@mui/material/TextField"; +import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns"; +import { Box } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { ptBR } from "date-fns/locale"; + +interface DateRangePickerProps { + startDate: Date | null; + endDate: Date | null; + setStartDate: React.Dispatch>; + setEndDate: React.Dispatch>; +} + +const DateRangePicker: React.FC = ({ + startDate, + endDate, + setStartDate, + setEndDate, +}) => { + const [openStart, setOpenStart] = useState(false); + const [openEnd, setOpenEnd] = useState(false); + const { t } = useTranslation(); + const today = new Date(); + + return ( + + + setOpenStart(false)} + onChange={(newValue) => setStartDate(newValue)} + renderInput={(params) => ( + setOpenStart(true)} + /> + )} + /> + setOpenEnd(false)} + onChange={(newValue) => setEndDate(newValue)} + renderInput={(params) => ( + setOpenEnd(true)} + /> + )} + /> + + + ); +}; + +export default DateRangePicker; diff --git a/src/components/Debate/DebateHeader.tsx b/src/components/Debate/DebateHeader.tsx index 87ddd4d7e..8ce9e3707 100644 --- a/src/components/Debate/DebateHeader.tsx +++ b/src/components/Debate/DebateHeader.tsx @@ -12,7 +12,7 @@ import { NameSpaceEnum } from "../../types/Namespace"; import { currentNameSpace } from "../../atoms/namespace"; import AletheiaButton, { ButtonType } from "../Button"; import { EditOutlined } from "@mui/icons-material"; -import { Roles } from "../../types/enums"; +import { isAdmin } from "../../utils/GetUserPermission"; const DebateHeader = ({ claim, title, personalities, userRole }) => { const [personalitiesArray, setPersonalitiesArray] = useState(personalities); @@ -49,7 +49,7 @@ const DebateHeader = ({ claim, title, personalities, userRole }) => { style={{ paddingTop: "32px", backgroundColor: colors.lightNeutral, - justifyContent:"center" + justifyContent: "center" }} >
{ {title}
- {userRole === Roles.Admin && claim?.claimId ? ( + {isAdmin(userRole) && claim?.claimId ? ( { marginRight: vw?.lg && vw?.md && vw?.sm ? 0 : 160, }} > - {t("debates:openEditDebateMode")} + {t("debates:openEditDebateMode")} ) : null} { const { t } = useTranslation(); - const [value, setValue] = useState(null); + const [value, setValue] = useState(props.defaultValue || null); const [open, setOpen] = useState(false); return ( @@ -61,7 +61,7 @@ const DatePickerInput = (props) => { setOpen(true)} - data-cy={props} + data-cy={props.dataCy} {...props} />} PopperProps={{ placement: 'bottom-start', }} diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index 55de5a8a6..9a84795b3 100644 --- a/src/components/Form/DynamicForm.tsx +++ b/src/components/Form/DynamicForm.tsx @@ -75,7 +75,11 @@ const DynamicForm = ({ )} /> {errors[fieldName] && ( - + {t(errors[fieldName].message)} )} diff --git a/src/components/Form/DynamicInput.tsx b/src/components/Form/DynamicInput.tsx index dd8eac903..9596e57b4 100644 --- a/src/components/Form/DynamicInput.tsx +++ b/src/components/Form/DynamicInput.tsx @@ -10,20 +10,26 @@ import { VisualEditorContext } from "../Collaborative/VisualEditorProvider"; import AletheiaInput from "../AletheiaInput"; import DatePickerInput from "./DatePickerInput"; import { Checkbox, FormControlLabel } from "@mui/material"; -import ReportTypeSelect from "../VerificationRequest/CreateVerificationRequest/ReportTypeSelect"; -import ImpactAreaSelect from "../VerificationRequest/CreateVerificationRequest/ImpactAreaSelect"; import colors from "../../styles/colors"; +import ReportTypeSelect from "../VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect"; +import ImpactAreaSelect from "../VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect"; +import InputExtraSourcesList from "../VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList"; +import { Topic } from "../../types/Topic"; +import { SourceType } from "../../types/Source"; +import ImageUpload, { UploadFile } from "../ImageUpload"; const VisualEditor = lazy(() => import("../Collaborative/VisualEditor")); +export type UnifiedDefaultValue = Topic | SourceType[] | UploadFile[] | string + interface DynamicInputProps { fieldName: string; type: string; placeholder: string; - value: string | []; + value: UnifiedDefaultValue; onChange: any; addInputLabel: string; - defaultValue: string | []; + defaultValue: UnifiedDefaultValue; "data-cy": string; extraProps: any; disabledDate?: any; @@ -83,6 +89,16 @@ const DynamicInput = (props: DynamicInputProps) => { white="true" /> ); + case "sourceList": + return ( + props.onChange(value)} + disabled={props.disabled} + placeholder={props.placeholder} + dataCy={props["data-cy"]} + /> + ); case "select": return ( { defaultValue={props.defaultValue} onChange={(value) => props.onChange(value)} placeholder={t(props.placeholder)} - + isDisabled={props.disabled} + dataCy={props["data-cy"]} /> ); case "selectImpactArea": return ( props.onChange(value)} placeholder={t(props.placeholder)} + isDisabled={props.disabled} + dataCy={props["data-cy"]} + /> + ); + case "imageUpload": + return ( + props.onChange(value)} + defaultFileList={props.defaultValue} /> ); case "textbox": @@ -141,9 +168,10 @@ const DynamicInput = (props: DynamicInputProps) => { case "date": return ( props.onChange(value)} - data-cy={"testSelectDate"} + data-cy="testSelectDate" disabledDate={props.disabledDate} disabled={props.disabled} style={{ backgroundColor: props.disabled ? colors.lightNeutral : colors.white }} diff --git a/src/components/Form/FormField.ts b/src/components/Form/FormField.ts index c7cf4fbb2..ac53c8d44 100644 --- a/src/components/Form/FormField.ts +++ b/src/components/Form/FormField.ts @@ -16,6 +16,7 @@ export type FormField = { addInputLabel?: string; defaultValue: string | []; extraProps?: FormFieldExtraProps; + disabled?: boolean; }; // Use to add properties specific to one type of field @@ -38,6 +39,7 @@ interface CreateFormFieldProps extends Partial { i18nNamespace?: string; required?: boolean; isURLField?: boolean; + disabled?: boolean; } const createFormField = (props: CreateFormFieldProps): FormField => { @@ -50,6 +52,7 @@ const createFormField = (props: CreateFormFieldProps): FormField => { rules, required = true, isURLField = false, + disabled = false, } = props; return { @@ -58,16 +61,17 @@ const createFormField = (props: CreateFormFieldProps): FormField => { label: `${i18nNamespace}:${i18nKey}Label`, placeholder: `${i18nNamespace}:${i18nKey}Placeholder`, defaultValue, + disabled, ...props, rules: { - required: required && "common:requiredFieldError", + required: !disabled && required && "common:requiredFieldError", ...rules, validate: { - ...(required && { + ...(!disabled && required && { notBlank: (v) => validateBlank(v) || "common:requiredFieldError", }), - ...(isURLField && { + ...(!disabled && isURLField && { validURL: (v) => !v || URL_PATTERN.test(v) || @@ -110,6 +114,10 @@ const fieldValidation = (value, validationFunction) => { return dayjs(value).isValid() && dayjs(value).isBefore(dayjs()); } + if (Array.isArray(value) && value.length > 0 && (value[0].uid || value[0].originFileObj)) { + return true; + } + if (value instanceof Node) { const editorParser = new EditorParser(); const schema = editorParser.editor2schema(value.toJSON()); diff --git a/src/components/Home/COP30/Cop30Section.style.tsx b/src/components/Home/COP30/Cop30Section.style.tsx new file mode 100644 index 000000000..361e5b69d --- /dev/null +++ b/src/components/Home/COP30/Cop30Section.style.tsx @@ -0,0 +1,432 @@ +import styled from "styled-components"; + +const Cop30SectionStyled = styled.div` +width: 100%; +padding-top: 32px; +display: flex; +justify-content: center; + /* COP30 Header Section */ + .cop30-banner { + background: linear-gradient(135deg, #11273a 0%, #657e8e 100%); + border-radius: 12px; + padding: 48px 40px; + margin-bottom: 32px; + position: relative; + overflow: hidden; + box-shadow: 0 4px 20px rgba(17, 39, 58, 0.15); + } + + .cop30-banner::before { + content: ''; + position: absolute; + top: -50%; + right: -10%; + width: 500px; + height: 500px; + background: radial-gradient(circle, rgba(103, 190, 242, 0.15) 0%, transparent 70%); + border-radius: 50%; + } + + .cop30-banner-content { + position: relative; + z-index: 1; + } + + .cop30-badge-wrapper { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; + flex-wrap: wrap; + } + + .cop30-badge { + background: #67bef2; + color: #ffffff; + font-size: 24px; + font-weight: 800; + padding: 10px 24px; + border-radius: 8px; + letter-spacing: 1px; + box-shadow: 0 4px 12px rgba(103, 190, 242, 0.3); + } + + .cop30-location { + display: flex; + align-items: center; + gap: 8px; + color: #9bbcd1; + font-size: 16px; + font-weight: 600; + } + + .cop30-banner h1 { + color: #ffffff; + font-size: 32px; + margin-bottom: 12px; + font-weight: 700; + } + + .cop30-banner .bannerDescription { + color: #9bbcd1; + font-size: 16px; + max-width: 900px; + line-height: 1.7; + } + + .cop30-banner p { + color: #515151; + font-size: 12px; + max-width: 900px; + line-height: 1.7; + } + + /* Statistics Bar */ + .stats-container { + background: #ffffff; + border-radius: 12px; + padding: 16px; + margin-bottom: 32px; + box-shadow: 0 2px 8px rgba(17, 39, 58, 0.08); + border: 1px solid #c2c8cc; + } + + .stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + } + + .stat-item { + text-align: center; + padding: 16px; + border-radius: 8px; + background: #dae8ea; + transition: transform 0.2s ease; + } + + .stat-item:hover { + transform: translateY(-2px); + background: #dae8ea; + } + + .stat-number { + font-size: 42px; + font-weight: 800; + color: #4f8db4; + margin-bottom: 4px; + display: block; + } + + .stat-label { + font-size: 14px; + color: #4d4e4f; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + /* Filters Section */ + .filters-container { + background: #ffffff; + border-radius: 12px; + padding: 24px 28px; + margin-bottom: 32px; + box-shadow: 0 2px 8px rgba(17, 39, 58, 0.08); + border: 1px solid #c2c8cc; + } + + .filters-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + } + + .filters-title { + font-size: 15px; + font-weight: 700; + color: #11273a; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .filters-grid { + display: flex; + gap: 10px; + flex-wrap: wrap; + } + + .filter-chip { + padding: 10px 20px; + border-radius: 20px; + border: 2px solid #c2c8cc; + background: #ffffff; + cursor: pointer; + font-size: 14px; + font-weight: 600; + color: #4d4e4f; + transition: all 0.3s ease; + white-space: nowrap; + } + + .filter-chip:hover { + border-color: #4f8db4; + color: #4f8db4; + background: #dae8ea; + } + + .filter-chip.active { + background: #11273a; + color: #ffffff; + border-color: #11273a; + } + + /* Section Header */ + .section-header { + margin-bottom: 24px; + } + + .section-title { + color: #11273a; + font-size: 28px; + font-weight: 700; + display: flex; + align-items: center; + gap: 12px; + } + + .section-title::before { + content: ''; + width: 5px; + height: 28px; + background: #67bef2; + border-radius: 3px; + } + + /* Checagens Grid */ + .checagens-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + } + + /* Checagem Card */ + .checagem-card { + background: #ffffff; + border-radius: 12px; + padding: 28px; + box-shadow: 0 2px 8px rgba(17, 39, 58, 0.08); + border: 1px solid #c2c8cc; + transition: all 0.3s ease; + } + + .checagem-card:hover { + box-shadow: 0 8px 24px rgba(17, 39, 58, 0.12); + transform: translateY(-2px); + border-color: #4f8db4; + } + + /* Card Header */ + .card-header { + display: flex; + gap: 20px; + margin-bottom: 20px; + align-items: flex-start; + } + + .author-avatar { + width: 64px; + height: 64px; + border-radius: 50%; + background: #dae8ea; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 3px solid #c2c8cc; + overflow: hidden; + } + + .author-avatar img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + color: #979a9b; + } + + .card-meta { + flex: 1; + } + + .card-date { + font-size: 13px; + color: #4d4e4f; + margin-bottom: 10px; + } + + .card-date strong { + color: #4f8db4; + font-weight: 700; + } + + /* Status Badge */ + .status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border-radius: 6px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; + } + + .status-reliable { + background: #dcfce7; + color: #15803d; + } + + .status-misleading { + background: #fee2e2; + color: #991b1b; + } + + /* Author Info */ + .author-name { + font-size: 17px; + color: #11273a; + margin-bottom: 4px; + font-weight: 700; + } + + .author-role { + font-size: 13px; + color: #4d4e4f; + line-height: 1.5; + } + + /* Topic Tags */ + .topic-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 16px; + } + + .topic-tag { + background: #dae8ea; + color: #11273a; + padding: 6px 14px; + border-radius: 14px; + font-size: 12px; + font-weight: 600; + border: 1px solid #b1c2cd; + } + + /* Card Footer */ + .card-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 16px; + border-top: 1px solid #c2c8cc; + gap: 16px; + flex-wrap: wrap; + } + + /* Topics Section */ + .topics-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #dae8ea; + } + + .topics-label { + font-size: 12px; + color: #979a9b; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + /* Responsive Design */ + @media (max-width: 768px) { + .container { + padding: 16px; + } + + .cop30-banner { + padding: 32px 24px; + } + + .cop30-banner h1 { + font-size: 24px; + } + + .cop30-badge { + font-size: 20px; + padding: 8px 18px; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .section-title { + font-size: 22px; + } + + .checagem-card { + padding: 20px; + } + + .card-header { + flex-direction: column; + } + + .card-footer { + flex-direction: column; + align-items: stretch; + } + + .btn-open { + width: 100%; + } + } + + /* Loading Animation */ + @keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .checagem-card { + animation: fadeInUp 0.5s ease forwards; + opacity: 0; + } + + .checagem-card:nth-child(1) { animation-delay: 0.1s; } + .checagem-card:nth-child(2) { animation-delay: 0.2s; } + .checagem-card:nth-child(3) { animation-delay: 0.3s; } + .checagem-card:nth-child(4) { animation-delay: 0.4s; } +`; + +export default Cop30SectionStyled; diff --git a/src/components/Home/COP30/Cop30Section.tsx b/src/components/Home/COP30/Cop30Section.tsx new file mode 100644 index 000000000..37b5287c0 --- /dev/null +++ b/src/components/Home/COP30/Cop30Section.tsx @@ -0,0 +1,102 @@ +import React, { useState, useEffect } from "react"; +import { useTranslation } from "next-i18next"; +import Cop30SectionStyled from "./Cop30Section.style"; +import Statistics from "./statistics"; +import { Grid } from "@mui/material"; +import ReviewsGrid from "../../ClaimReview/ReviewsGrid"; +import { Cop30Sentence } from "../../../types/Cop30Sentence"; +import { Cop30Stats } from "../../../types/Cop30Stats"; +import cop30Filters, { allCop30WikiDataIds } from "../../../constants/cop30Filters"; +import SentenceApi from "../../../api/sentenceApi"; + +interface Cop30SectionProps { + reviews: Cop30Sentence[]; +} + +const Cop30Section: React.FC = ({ reviews }) => { + const { t } = useTranslation(); + const [activeFilter, setActiveFilter] = useState("all"); + const [stats, setStats] = useState(null); + + const hasCop30Topic = (topics?: { value: string }[]) => + topics?.some(topic => allCop30WikiDataIds.includes(topic.value)); + + const filterOptions = cop30Filters.map(filter => ({ + id: filter.id, + wikidataId: filter.wikidataId, + label: t(filter.translationKey), + })); + + const cop30Reviews = reviews.filter(review => hasCop30Topic(review.content.topics)); + + useEffect(() => { + async function fetchStats() { + const stats = await SentenceApi.getCop30Stats() + setStats(stats); + } + setActiveFilter("all") + fetchStats(); + }, []); + + const selectedWikiDataId = cop30Filters.find(filter => filter.id === activeFilter)?.wikidataId + + const filteredReviews = + activeFilter === "all" + ? cop30Reviews + : cop30Reviews.filter(review => + review.content.topics?.some( + ({ value }) => value === selectedWikiDataId + ) + ); + + return ( + + +
+
+
+
{t("cop30:cop30Conference")}
+
+ {t("cop30:bannerLocation")} +
+
+

{t("cop30:bannerTitle")}

+

+ {t("cop30:bannerDescription")} +

+
+ + {stats && ( + + )} + +
+
+

{t("cop30:sectionLatestChecks")}

+
+
+ {filterOptions.map(option => ( + + ))} +
+ + +
+
+
+
+ ); +}; + +export default Cop30Section; \ No newline at end of file diff --git a/src/components/Home/COP30/statistics.tsx b/src/components/Home/COP30/statistics.tsx new file mode 100644 index 000000000..12bcc7acd --- /dev/null +++ b/src/components/Home/COP30/statistics.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { useTranslation } from "next-i18next"; + +interface StatisticsProps { + total: number; + reliable: number; + deceptive: number; + underReview: number; +} +const Statistics: React.FC = ({ + total, + reliable, + deceptive, + underReview, +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+ {total} + {t("cop30:statisticsTotalChecks")} +
+
+ {reliable} + {t("cop30:statisticsReliable")} +
+
+ {deceptive} + {t("cop30:statisticsMisleading")} +
+
+ {underReview} + {t("cop30:statisticsUnderReview")} +
+
+
+ ); +}; + +export default Statistics; diff --git a/src/components/Home/COP30/utils/classification.ts b/src/components/Home/COP30/utils/classification.ts new file mode 100644 index 000000000..3b882efc7 --- /dev/null +++ b/src/components/Home/COP30/utils/classification.ts @@ -0,0 +1,35 @@ +export const classificationMap = { + trustworthy: "reliable", + trustworthyBut: "reliable", + + misleading: "deceptive", + false: "deceptive", + unsustainable: "deceptive", + exaggerated: "deceptive", + unverifiable: "deceptive", + + arguable: "underReview", + notFact: "underReview", +}; + +export function buildStats(sentences: Array<{ classification?: string }>) { + const normalizedSentences = Array.isArray(sentences) ? sentences : []; + + const stats = { + total: normalizedSentences.length, + reliable: 0, + deceptive: 0, + underReview: 0, + }; + + for (const sentence of normalizedSentences) { + const classification = sentence.classification ?? ""; + const group = classificationMap[classification]; + + if (group) { + stats[group] += 1; + } + } + + return stats; +} diff --git a/src/components/Home/CTA/CTASectionButtons.tsx b/src/components/Home/CTA/CTASectionButtons.tsx index 6d6c5c561..0bd09d945 100644 --- a/src/components/Home/CTA/CTASectionButtons.tsx +++ b/src/components/Home/CTA/CTASectionButtons.tsx @@ -27,7 +27,7 @@ const CTASectionButtons = () => { > {!smallDevice && ( - {localConfig.header.ctaButton.show && ( + {localConfig?.header?.ctaButton?.show && ( { {!smallDevice && ( - {localConfig.header.donateButton.show ? ( + {localConfig?.header?.donateButton?.show ? ( diff --git a/src/components/Home/Home.tsx b/src/components/Home/Home.tsx index 592754993..559c09c58 100644 --- a/src/components/Home/Home.tsx +++ b/src/components/Home/Home.tsx @@ -3,6 +3,7 @@ import React from "react"; import HomeContent from "./HomeContent"; import HomeHeader from "./HomeHeader/HomeHeader"; +import Cop30Section from "./COP30/Cop30Section"; const Home = ({ personalities, stats, href, claims, reviews }) => { const { t } = useTranslation(); @@ -10,6 +11,9 @@ const Home = ({ personalities, stats, href, claims, reviews }) => { return ( <> + { return ( <> {results.length > 0 && ( - +

{t("home:homeFeedTitle")}

- + diff --git a/src/components/ImageClaim.tsx b/src/components/ImageClaim.tsx index 18d0667bb..956a4ec7d 100644 --- a/src/components/ImageClaim.tsx +++ b/src/components/ImageClaim.tsx @@ -1,17 +1,42 @@ /* eslint-disable @next/next/no-img-element */ import React from "react"; +import { Box } from "@mui/material"; -const ImageClaim = ({ src, title = "" }) => { +interface ImageClaimProps { + src: string; + title?: string; +} + +const ImageClaim: React.FC = ({ src, title = "" }) => { return ( - {`${title} + > + {`${title} + ); }; diff --git a/src/components/ImageUpload.tsx b/src/components/ImageUpload.tsx index b781e47d7..af5658f62 100644 --- a/src/components/ImageUpload.tsx +++ b/src/components/ImageUpload.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { Grid, Box, @@ -11,6 +11,7 @@ import { FileUploadOutlined, DeleteOutline } from "@mui/icons-material"; import { useTranslation } from "next-i18next"; import AletheiaButton from "./Button"; import { MessageManager } from "../components/Messages"; +import { UnifiedDefaultValue } from "./Form/DynamicInput"; export interface UploadFile { uid: string; @@ -24,18 +25,30 @@ export interface UploadFile { interface ImageUploadProps { onChange: (fileList: UploadFile[]) => void; error?: boolean; - defaultFileList?: UploadFile[]; + defaultFileList?: UnifiedDefaultValue; } const ImageUpload = ({ onChange, error = false, - defaultFileList = [], + defaultFileList, }: ImageUploadProps) => { const { t } = useTranslation(); const fileInputRef = useRef(null); - const [fileList, setFileList] = useState(defaultFileList); + const [fileList, setFileList] = useState(() => { + if (Array.isArray(defaultFileList)) { + return (defaultFileList as UploadFile[]).filter(file => file.uid); + } + return []; + }); + + useEffect(() => { + if (Array.isArray(defaultFileList)) { + setFileList(defaultFileList as UploadFile[]); + } + }, [defaultFileList]); + const [previewOpen, setPreviewOpen] = useState(false); const [previewImage, setPreviewImage] = useState(""); const [previewTitle, setPreviewTitle] = useState(""); diff --git a/src/components/Kanban/KanbanCard.tsx b/src/components/Kanban/KanbanCard.tsx index 432e2394e..7e7e7359d 100644 --- a/src/components/Kanban/KanbanCard.tsx +++ b/src/components/Kanban/KanbanCard.tsx @@ -8,15 +8,31 @@ import actions from "../../store/actions"; import { useDispatch } from "react-redux"; import { ContentModelEnum } from "../../types/enums"; import { PhotoOutlined, ArticleOutlined } from "@mui/icons-material"; -import { AvatarGroup, Grid, Typography } from "@mui/material"; +import { AvatarGroup, Grid, Typography, Chip } from "@mui/material"; import { useAtom } from "jotai"; import { currentNameSpace } from "../../atoms/namespace"; import SourceApi from "../../api/sourceApi"; import { ReviewTaskTypeEnum } from "../../machines/reviewTask/enums"; import verificationRequestApi from "../../api/verificationRequestApi"; import colors from "../../styles/colors"; +import reviewColors from "../../constants/reviewColors"; +import { Content } from "../../types/Content"; -const KanbanCard = ({ reviewTask, reviewTaskType }) => { +interface IKanbanCardProps { + reviewTask: { + targetId: string; + personalityId: string; + personalityName: string; + contentModel: ContentModelEnum; + claimTitle: string; + value: string; + usersName: string[]; + content: Content & { href?: string }; + }; + reviewTaskType: ReviewTaskTypeEnum; +} + +const KanbanCard = ({ reviewTask, reviewTaskType }: IKanbanCardProps) => { const { t } = useTranslation(); const dispatch = useDispatch(); const [nameSpace] = useAtom(currentNameSpace); @@ -73,28 +89,64 @@ const KanbanCard = ({ reviewTask, reviewTaskType }) => { }} > - {title || reviewTask.content.href} - {reviewTask.personalityName} + + {reviewTask.personalityName} + + {reviewTask.value === "published" && + reviewTask.content?.props?.classification && ( + + )} - { {reviewTask.usersName && reviewTask.usersName.map((user, index) => { return ; - })} + })} diff --git a/src/components/LocalizedDate.tsx b/src/components/LocalizedDate.tsx index d9bbb97cc..bfed63c3b 100644 --- a/src/components/LocalizedDate.tsx +++ b/src/components/LocalizedDate.tsx @@ -1,22 +1,26 @@ +import { useTranslation } from "next-i18next"; import React from "react"; -const LocalizedDate = ({ - date, - showTime = false, -}: { - date: Date; - showTime?: boolean; -}) => { - date = new Date(date); - const localizedDate = date.toLocaleDateString(); - const localizedTime = date.toLocaleTimeString(); - return ( - // Suppress hydration warning because currentTime varies between server and client rendering - - {localizedDate} - {showTime && ` - ${localizedTime}`} - - ); +interface LocalizedDateProps { + date: Date | string | number; + showTime?: boolean; +} + +const LocalizedDate = ({ date, showTime = false }: LocalizedDateProps) => { + const { i18n } = useTranslation(); + const currentLocale = i18n.language || "en"; + const dateObj = new Date(date); + + if (Number.isNaN(dateObj.getTime())) return null; + + const formattedDate = dateObj.toLocaleDateString(currentLocale); + const formattedTime = showTime ? ` - ${dateObj.toLocaleTimeString(currentLocale)}` : ""; + + return ( + + ); }; export default LocalizedDate; diff --git a/src/components/Login/LoginView.tsx b/src/components/Login/LoginView.tsx index 5fd68cd5a..da624b283 100644 --- a/src/components/Login/LoginView.tsx +++ b/src/components/Login/LoginView.tsx @@ -74,7 +74,10 @@ const LoginView = ({ isSignUp = false, shouldGoBack = false }) => { if (err.response?.status === 400) { // Yup, it is! setFlow(err.response?.data); - return MessageManager.showMessage("error", `${t("profile:totpIncorectCodeMessage")}`); + return MessageManager.showMessage( + "error", + `${t("profile:totpIncorectCodeMessage")}` + ); } return Promise.reject(err); @@ -122,6 +125,7 @@ const LoginView = ({ isSignUp = false, shouldGoBack = false }) => { email, password, name: values.name, + recaptcha: values.recaptcha, }; userApi.register(payload, t).then((res) => { if (!res?.error) { @@ -143,15 +147,19 @@ const LoginView = ({ isSignUp = false, shouldGoBack = false }) => { const onFinishFailed = (errorInfo) => { if (typeof errorInfo === "string") { - MessageManager.showMessage("error", errorInfo) + MessageManager.showMessage("error", errorInfo); } else { - MessageManager.showMessage("error", `${t("login:loginFailedMessage")}`); + MessageManager.showMessage( + "error", + `${t("login:loginFailedMessage")}` + ); } setIsLoading(false); }; return ( - @@ -176,7 +184,8 @@ const LoginView = ({ isSignUp = false, shouldGoBack = false }) => { + textWhenLoggedOut={t("CTAFolder:button")} + /> )}
diff --git a/src/components/Login/SignUpForm.tsx b/src/components/Login/SignUpForm.tsx index 557fd3d76..0d8a3f659 100644 --- a/src/components/Login/SignUpForm.tsx +++ b/src/components/Login/SignUpForm.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "next-i18next"; -import React from "react"; +import React, { useRef, useState } from "react"; import AletheiaAlert from "../AletheiaAlert"; import Input from "../AletheiaInput"; @@ -9,6 +9,7 @@ import { Grid } from "@mui/material"; import { useForm } from "react-hook-form"; import Label from "../Label"; import TextError from "../TextErrorForm"; +import AletheiaCaptcha from "../AletheiaCaptcha"; const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { const { t } = useTranslation(); @@ -19,6 +20,16 @@ const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { formState: { errors }, } = useForm(); const senha = watch("password"); + const captchaRef = useRef(null); + const [captchaString, setCaptchaString] = useState(""); + + const handleFormSubmit = (values) => { + if (!captchaString) { + onFinishFailed(t("common:requiredFieldError")); + return; + } + onFinish({ ...values, recaptcha: captchaString }); + }; return (
@@ -37,10 +48,11 @@ const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { } />

{t("login:signupFormHeader")}

-
+ - @@ -48,16 +60,18 @@ const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { - @@ -66,10 +80,12 @@ const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { data-cy="emailInputCreateAccount" {...register("email", { required: true, - pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ + pattern: + /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, })} /> { /> - @@ -87,16 +104,18 @@ const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { - @@ -105,16 +124,28 @@ const SignUpForm = ({ onFinish, onFinishFailed, isLoading }) => { data-cy="repeatedPasswordInputCreateAccount" {...register("repeatedPassword", { required: true, - validate: (value) => - value === senha + validate: (value) => value === senha, })} /> + + + + + + diff --git a/src/components/ReviewedImage.tsx b/src/components/ReviewedImage.tsx index 4343442c1..3cd870552 100644 --- a/src/components/ReviewedImage.tsx +++ b/src/components/ReviewedImage.tsx @@ -1,9 +1,35 @@ /* eslint-disable @next/next/no-img-element */ import React, { useEffect, useRef, useState } from "react"; import lottie from "lottie-web"; -import { generateLottie } from "../lottiefiles/generateLottie"; +import { generateLottie, LottieAnimation } from "../lottiefiles/generateLottie"; import { ClassificationEnum } from "../types/enums"; +interface ImageDimensions { + width: number; + height: number; +} + +const getImageMeta = (url: string): Promise => + new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = () => reject(new Error(`Failed to load image: ${url}`)); + img.src = url; + }); + +const getDimensions = async (imageData: string): Promise => { + try { + const image = await getImageMeta(imageData); + const width = image.naturalWidth; + const height = image.naturalHeight; + + return { width, height }; + } catch (error) { + console.error("Failed to load image dimensions:", error); + return { width: 500, height: 500 }; + } +}; + const ReviewedImage = ({ imageUrl, title = "", @@ -13,46 +39,12 @@ const ReviewedImage = ({ title: string; classification?: keyof typeof ClassificationEnum; }) => { - const [animation, setAnimation] = useState(null); - const container = useRef(null); - const getImageMeta = (url) => - new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => resolve(img); - img.onerror = (err) => reject(err); - img.src = url; - }); + const [animation, setAnimation] = useState(null); + const container = useRef(null); - // get the natural width and height of the image or fit it to the window - const getDimensions = async (imageData: any) => { - try { - const image = await getImageMeta(imageData); - - // @ts-ignore - let { naturalWidth: width, naturalHeight: height } = image; - const windowHeight = window.innerHeight; - // 2.25 is the ratio of the lottie container to the window - // determined by 66.6% of the claim container - // inside a section with 66.6% of the window - const windowWidth = window.innerWidth / 2.25; - - const aspectRatio = width / height; - - if (height > windowHeight) { - height = windowHeight; - width = height * aspectRatio; - } - if (width > windowWidth) { - width = windowWidth; - height = width / aspectRatio; - } - return { width, height }; - } catch (error) { - console.log(error); - return { width: 500, height: 500 }; - } - }; useEffect(() => { + if (!classification) return; + getDimensions(imageUrl).then(({ width, height }) => { const newAnimation = generateLottie( classification, @@ -62,7 +54,7 @@ const ReviewedImage = ({ ); setAnimation(newAnimation); }); - }, []); + }, [imageUrl, classification]); useEffect(() => { if (classification) { @@ -73,8 +65,7 @@ const ReviewedImage = ({ autoplay: false, animationData: animation, rendererSettings: { - preserveAspectRatio: "xMinYMin meet", - viewBoxSize: "10 10", + preserveAspectRatio: "xMidYMid meet", }, }); lottie.setSpeed(1.5); @@ -102,9 +93,13 @@ const ReviewedImage = ({ onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} ref={container} + aria-hidden="true" style={{ maxWidth: "100%", maxHeight: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", }} /> )} @@ -116,6 +111,8 @@ const ReviewedImage = ({ style={{ maxWidth: "100%", maxHeight: "100vh", + display: "block", + margin: "0 auto", }} /> )} diff --git a/src/components/Search/AdvancedSearch.tsx b/src/components/Search/AdvancedSearch.tsx index 18291ba8f..89f207b83 100644 --- a/src/components/Search/AdvancedSearch.tsx +++ b/src/components/Search/AdvancedSearch.tsx @@ -3,17 +3,62 @@ import { TextField, Autocomplete } from "@mui/material"; import colors from "../../styles/colors"; import { useTranslation } from "next-i18next"; -const AdvancedSearch = ({ onSearch, options, defaultValue, handleFilter }) => { +interface TopicOption { + name: string; + matchedAlias?: string | null; + slug?: string; + wikidataId?: string; + aliases?: string[]; +} + +interface MappedOption { + label: string; + value: string; +} + +interface AdvancedSearchProps { + onSearch: (value: string) => void; + options?: TopicOption[] | null; + defaultValue?: string[] | MappedOption[]; + handleFilter: (names: string[]) => void; +} + +const AdvancedSearch: React.FC = ({ + onSearch, + options, + defaultValue, + handleFilter, +}) => { const { t } = useTranslation(); + + const mappedOptions: MappedOption[] = (options || []).map((option) => ({ + label: option.matchedAlias + ? `${option.name} (${option.matchedAlias})` + : option.name, + value: option.name, + })); + + const normalizedDefaultValue: (string | MappedOption)[] = defaultValue + ? Array.isArray(defaultValue) + ? defaultValue + : [defaultValue] + : []; + return ( - multiple id="tags-outlined" - defaultValue={ - Array.isArray(defaultValue) ? defaultValue : [defaultValue] + defaultValue={normalizedDefaultValue} + options={mappedOptions} + getOptionLabel={(option) => + typeof option === "string" ? option : option.label } - options={options.map((option) => option.name)} - onChange={(event, newValue) => handleFilter(newValue)} + onChange={(event, newValue) => { + const names = newValue.map((item) => + typeof item === "string" ? item : item.value + ); + handleFilter(names); + }} renderInput={(params) => ( - + { +interface ISharedFormFooter { + isLoading: boolean; + setRecaptchaString: Dispatch>; + hasCaptcha: boolean; + isDrawerOpen?: boolean; + onClose?: () => void; + extraButton?: React.ReactNode; +} + +const SharedFormFooter = ({ + isLoading, + setRecaptchaString, + hasCaptcha, + isDrawerOpen, + onClose, + extraButton +}: ISharedFormFooter) => { const recaptchaRef = useRef(null); const { t } = useTranslation(); const router = useRouter(); @@ -23,10 +39,14 @@ const SharedFormFooter = ({ isLoading, setRecaptchaString, hasCaptcha }) => { > router.back()} + onClick={() => isDrawerOpen ? onClose() : router.back()} + data-cy="testCancelButton" > {t("claimForm:cancelButton")} + + {extraButton} + { - return ( -

- {children} -

- ); +const TextError = ({ children, stateError, "data-cy": dataCy }) => { + return ( +

+ {children} +

+ ); }; export default TextError; diff --git a/src/components/Toolbar/ReviewTaskAdminToolBar.tsx b/src/components/Toolbar/ReviewTaskAdminToolBar.tsx index 88d81d0f3..7fdf461bd 100644 --- a/src/components/Toolbar/ReviewTaskAdminToolBar.tsx +++ b/src/components/Toolbar/ReviewTaskAdminToolBar.tsx @@ -7,12 +7,43 @@ import { currentNameSpace } from "../../atoms/namespace"; import colors from "../../styles/colors"; import { ReviewTaskEvents } from "../../machines/reviewTask/enums"; import { ReviewTaskMachineContext } from "../../machines/reviewTask/ReviewTaskMachineProvider"; +import HistoryIcon from '@mui/icons-material/History'; +import { useRouter } from "next/router"; +import { generateReviewContentPath } from "../../utils/GetReviewContentHref"; +import { useAppSelector } from "../../store/store"; +import { TargetModel } from "../../types/enums"; const ReviewTaskAdminToolBar = () => { const [nameSpace] = useAtom(currentNameSpace); const { machineService, setFormAndEvents } = useContext( ReviewTaskMachineContext ); + const router = useRouter(); + + const { + personality, + claim, + content, + data_hash, + } = useAppSelector((state) => ({ + personality: state.selectedPersonality, + claim: state.selectedTarget, + content: state.selectedContent, + data_hash: state.selectedDataHash, + })); + + const historyPath = () => { + const historyRoute = generateReviewContentPath( + nameSpace, + personality, + claim, + content, + data_hash, + content?.reviewTaskType, + TargetModel.History + ); + router.push(historyRoute) + } const handleReassignUser = () => { machineService.send(ReviewTaskEvents.reAssignUser, { @@ -30,6 +61,15 @@ const ReviewTaskAdminToolBar = () => { style={{ boxShadow: "none", background: colors.lightNeutral }} > +
+ + + +
diff --git a/src/components/Tracking/StepLabel.style.tsx b/src/components/Tracking/StepLabel.style.tsx new file mode 100644 index 000000000..0367556a2 --- /dev/null +++ b/src/components/Tracking/StepLabel.style.tsx @@ -0,0 +1,41 @@ +import styled from "styled-components"; +import colors from "../../styles/colors"; +import StepLabel from "@mui/material/StepLabel"; +import { StepLabelStyledProps } from "../../types/Tracking"; + +export const StepLabelStyled = styled(StepLabel) ` +& .MuiStepLabel-label { + padding: 4px 8px; + border-radius: 8px; + } + + .stepItem { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 6px; + } + + .stepLabel { + color: ${colors.blackSecondary}; + background-color: ${({ backgroundStatusColor }) => backgroundStatusColor}; + border-radius: 4px; + padding: 2px 6px; + } + + .dateLabel { + color: ${colors.blackSecondary}; + background-color: ${colors.lightNeutral}; + border-radius: 4px; + padding: 2px 6px; + } + + .description { + color: ${colors.blackSecondary}; + background-color: ${({ backgroundStatusColor }) => backgroundStatusColor}; + border-radius: 4px; + padding: 8px 6px; + border-left: 4px solid; + border-left-color: ${({ iconColor }) => iconColor} + } +`; diff --git a/src/components/Tracking/TrackingCard.tsx b/src/components/Tracking/TrackingCard.tsx new file mode 100644 index 000000000..49f578829 --- /dev/null +++ b/src/components/Tracking/TrackingCard.tsx @@ -0,0 +1,76 @@ +import * as React from "react"; +import { useEffect, useState } from "react"; +import TrackingApi from "../../api/trackingApi"; +import { TrackingResponseDTO, TrackingCardProps } from "../../types/Tracking"; +import CardBase from "../CardBase"; +import Loading from "../Loading"; +import TrackingStep from "./TrackingStepper"; +import Typography from "@mui/material/Typography"; +import { useTranslation } from "next-i18next"; + +const initialTrackingState: TrackingResponseDTO = { + currentStatus: null, + historyEvents: [], +}; + +const TrackingCard = ({ verificationRequestId, isMinimal }: TrackingCardProps) => { + const [trackingData, setTrackingData] = useState(initialTrackingState); + const [isLoading, setIsLoading] = useState(true); + const { t } = useTranslation(); + + const { currentStatus, historyEvents } = trackingData; + + useEffect(() => { + const fetchTracking = async () => { + try { + const data = await TrackingApi.getTrackingById(verificationRequestId, t); + setTrackingData(data); + } catch (error) { + console.error("Error when searching for tracking:", error); + } finally { + setIsLoading(false); + } + }; + + if (verificationRequestId) { + setIsLoading(true); + fetchTracking(); + } + }, [verificationRequestId]); + + if (isLoading) { + return ; + } + + return ( + + + {t("tracking:verificationProgress")} + + + + ); +}; + +export default TrackingCard; diff --git a/src/components/Tracking/TrackingStep.tsx b/src/components/Tracking/TrackingStep.tsx new file mode 100644 index 000000000..d69737b8d --- /dev/null +++ b/src/components/Tracking/TrackingStep.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; +import { Grid, Typography } from "@mui/material"; +import LocalizedDate from "../LocalizedDate"; +import { useTranslation } from "next-i18next"; +import colors from "../../styles/colors"; +import { TrackingStepProps } from "../../types/Tracking"; +import { StepLabelStyled } from "./StepLabel.style"; +import CheckCircleIcon from "@mui/icons-material/CheckCircle"; +import PendingIcon from "@mui/icons-material/Pending"; +import CancelIcon from "@mui/icons-material/Cancel"; + +const STEP_STATES = { + declined: { + bg: colors.errorTranslucent, + color: colors.error, + Icon: CancelIcon, + }, + completed: { + bg: colors.activeTranslucent, + color: colors.active, + Icon: CheckCircleIcon, + }, + pending: { + bg: colors.lightNeutral, + color: colors.neutralSecondary, + Icon: PendingIcon, + }, +}; + +const TrackingStep = ({ + stepKey, + stepDate, + isCompleted, + isDeclined, + isMinimal +}: TrackingStepProps) => { + const { t } = useTranslation(); + + const currentState = isDeclined ? "declined" : isCompleted ? "completed" : "pending"; + const { bg, color, Icon } = STEP_STATES[currentState]; + + const translationKey = stepKey.toUpperCase().replace(/ /g, "_"); + + return ( + } + error={isDeclined} + backgroundStatusColor={bg} + iconColor={color} + > + + + {t(`verificationRequest:${translationKey}`)} + + + {stepDate ? ( + + {stepDate === "noData" ? ( + t("tracking:noDateFound") + ) : ( + + )} + + ) : null} + + + + {t(`tracking:description_${translationKey}`)} + + + ); +}; + +export default TrackingStep; diff --git a/src/components/Tracking/TrackingStepper.tsx b/src/components/Tracking/TrackingStepper.tsx new file mode 100644 index 000000000..614bacb3a --- /dev/null +++ b/src/components/Tracking/TrackingStepper.tsx @@ -0,0 +1,63 @@ +import * as React from "react"; +import { Step, Stepper } from "@mui/material"; +import { VerificationRequestStatus } from "../../types/enums"; +import { TrackingResponseDTO } from "../../types/Tracking"; +import TrackingStep from "./TrackingStep"; + +const getVisibleSteps = (currentStatus: VerificationRequestStatus): VerificationRequestStatus[] => { + const allSteps = Object.values(VerificationRequestStatus); + + if (currentStatus === VerificationRequestStatus.DECLINED) { + return allSteps.filter(step => step !== VerificationRequestStatus.POSTED); + } + + return allSteps.filter(step => step !== VerificationRequestStatus.DECLINED); +}; + +const TrackingStepper = ({ + currentStatus, + historyEvents, + isMinimal, +}: TrackingResponseDTO) => { + const dynamicSteps = getVisibleSteps(currentStatus); + const completedStepIndex = dynamicSteps.indexOf(currentStatus); + + const getDateForStep = (stepStatus: VerificationRequestStatus) => { + const history = historyEvents.find(history => history.status === stepStatus); + + if (history) { + return new Date(history.date); + } + + // This is only necessary because we are not creating histories for this step, so it's good UX to provide feedback. This will no longer be necessary once all steps have a history. Tracked in issue #2244 + if (stepStatus === VerificationRequestStatus.IN_TRIAGE) { + return "noData" + } + + return null; + }; + + return ( + + {dynamicSteps.map((stepLabel, index) => { + const isCompleted = index <= completedStepIndex; + const isDeclined = stepLabel === VerificationRequestStatus.DECLINED; + const stepDate = getDateForStep(stepLabel); + + return ( + + + + ); + })} + + ); +}; + +export default TrackingStepper; diff --git a/src/components/Tracking/TrackingView.tsx b/src/components/Tracking/TrackingView.tsx new file mode 100644 index 000000000..e0f9936e2 --- /dev/null +++ b/src/components/Tracking/TrackingView.tsx @@ -0,0 +1,21 @@ +import { Grid } from "@mui/material"; +import { TrackingCardProps } from "../../types/Tracking"; +import TrackingCard from "./TrackingCard"; +import CTAFolder from "../Home/CTAFolder"; + +const TrackingView = ({ verificationRequestId }: TrackingCardProps) => { + return ( + + + + + + + + + ); +}; + +export default TrackingView; diff --git a/src/components/VerificationRequest/ActiveFilters.tsx b/src/components/VerificationRequest/ActiveFilters.tsx index ba06a9490..49bfce0c5 100644 --- a/src/components/VerificationRequest/ActiveFilters.tsx +++ b/src/components/VerificationRequest/ActiveFilters.tsx @@ -1,74 +1,93 @@ import React from "react"; import { Grid, Typography, Chip } from "@mui/material"; import { ActionTypes } from "../../store/types"; +import { + FilterItem, + FiltersContext, + FilterType, +} from "../../types/VerificationRequest"; -const ActiveFilters = ({ state, actions }) => { - const { topicFilterUsed, impactAreaFilterUsed } = state; - const { dispatch, t, setPaginationModel, setApplyFilters } = actions; +const ActiveFilters: React.FC = ({ state, actions }) => { + const { topicFilterUsed, impactAreaFilterUsed, autoCompleteTopicsResults } = + state; + const { dispatch, t, setPaginationModel, setApplyFilters } = actions; - const handleRemoveFilter = (removedFilter) => { - if (removedFilter.type === "topic") { - const updatedTopics = topicFilterUsed.filter( - (topic) => topic !== removedFilter.value - ); - dispatch({ - type: ActionTypes.SET_TOPIC_FILTER_USED, - topicFilterUsed: updatedTopics, - }); - } else if (removedFilter.type === "impactArea") { - const updatedImpactAreas = impactAreaFilterUsed.filter( - (impactArea) => impactArea !== removedFilter.value - ); - dispatch({ - type: ActionTypes.SET_IMPACT_AREA_FILTER_USED, - impactAreaFilterUsed: updatedImpactAreas, - }); - } - setPaginationModel((prev) => ({ ...prev, page: 0 })); - setApplyFilters(true); - }; + const getTopicDisplayLabel = (topicName: string): string => { + const topicWithAlias = autoCompleteTopicsResults?.find( + (topic) => topic.name === topicName + ); + if (topicWithAlias?.matchedAlias) { + return `${topicName} (${topicWithAlias.matchedAlias})`; + } + return topicName; + }; - return ( - (topicFilterUsed.length > 0 || impactAreaFilterUsed.length > 0) && ( - - - {t("verificationRequest:activeFiltersLabel")} - - - {topicFilterUsed?.map((topic) => ( - - - handleRemoveFilter({ - label: `Topic: ${topic}`, - value: topic, - type: "topic", - }) - } - /> + const handleRemoveFilter = (removedFilter: FilterItem): void => { + if (removedFilter.type === FilterType.TOPIC) { + const updatedTopics = topicFilterUsed.filter( + (topic) => topic !== removedFilter.value + ); + dispatch({ + type: ActionTypes.SET_TOPIC_FILTER_USED, + topicFilterUsed: updatedTopics, + }); + } else if (removedFilter.type === FilterType.IMPACT_AREA) { + const updatedImpactAreas = impactAreaFilterUsed.filter( + (impactArea) => impactArea !== removedFilter.value + ); + dispatch({ + type: ActionTypes.SET_IMPACT_AREA_FILTER_USED, + impactAreaFilterUsed: updatedImpactAreas, + }); + } + setPaginationModel((prev) => ({ ...prev, page: 0 })); + setApplyFilters(true); + }; + + const ChipComponent = (type: FilterType, value: string): JSX.Element => { + const label = + type === FilterType.TOPIC + ? `${t( + "verificationRequest:topicFilterLabel" + )} ${getTopicDisplayLabel(value)}` + : `${t("verificationRequest:impactAreaFilterLabel")} ${value}`; + + const deleteObject: FilterItem = { + label: + type === FilterType.TOPIC + ? `Topic: ${value}` + : `Impact Area: ${value}`, + value: value, + type: type, + }; + + return ( + + handleRemoveFilter(deleteObject)} + /> - ))} - {impactAreaFilterUsed?.map((impactArea) => ( - - - handleRemoveFilter({ - label: `Impact Area: ${impactArea}`, - value: impactArea, - type: "impactArea", - }) - } - /> + ); + }; + + return ( + (topicFilterUsed.length > 0 || impactAreaFilterUsed.length > 0) && ( + + + {t("verificationRequest:activeFiltersLabel")} + + + {topicFilterUsed?.map((topic) => + ChipComponent(FilterType.TOPIC, topic) + )} + {impactAreaFilterUsed?.map((impactArea) => + ChipComponent(FilterType.IMPACT_AREA, impactArea) + )} + - ))} - - - ) - ); + ) + ); }; export default ActiveFilters; diff --git a/src/components/VerificationRequest/CreateVerificationRequest/CreateVerificationRequestView.tsx b/src/components/VerificationRequest/CreateVerificationRequest/CreateVerificationRequestView.tsx deleted file mode 100644 index 83ff9a6e4..000000000 --- a/src/components/VerificationRequest/CreateVerificationRequest/CreateVerificationRequestView.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; -import { Grid } from "@mui/material"; -import colors from "../../../styles/colors"; -import DynamicVerificationRequestForm from "./DynamicVerificationRequestForm"; - -const CreateVerificationRequestView = () => { - return ( - - - - - - ); -}; - -export default CreateVerificationRequestView; diff --git a/src/components/VerificationRequest/CreateVerificationRequest/ImpactAreaSelect.tsx b/src/components/VerificationRequest/CreateVerificationRequest/ImpactAreaSelect.tsx deleted file mode 100644 index f1dca1ed5..000000000 --- a/src/components/VerificationRequest/CreateVerificationRequest/ImpactAreaSelect.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from "react"; -import { useEffect, useState} from "react"; -import MultiSelectAutocomplete from "../../topics/TopicOrImpactSelect"; - - -interface ImpactAreaSelectProps { - onChange: (value: string) => void; - placeholder?: string; -} - -const ImpactAreaSelect: React.FC = ({ - onChange, - placeholder, -}) => { - const [value, setValue] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - onChange(value || undefined); - }, []); - return ( - - ); -}; - -export default ImpactAreaSelect; diff --git a/src/components/VerificationRequest/CreateVerificationRequest/ReportTypeSelect.tsx b/src/components/VerificationRequest/CreateVerificationRequest/ReportTypeSelect.tsx deleted file mode 100644 index 99051fcae..000000000 --- a/src/components/VerificationRequest/CreateVerificationRequest/ReportTypeSelect.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react"; -import { MenuItem, FormControl } from "@mui/material"; -import { ContentModelEnum } from "../../../types/enums"; -import { SelectInput } from "../../Form/ClaimReviewSelect"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -const ReportTypeSelect = ({ - onChange, - defaultValue, - placeholder, - style = {}, -}) => { - const [value, setValue] = useState(defaultValue || ""); - const { t } = useTranslation(); - - const onChangeSelect = (e) => { - setValue(e.target.value); - }; - - useEffect(() => { - onChange(value || undefined); - }, [value, onChange]); - - return ( - - - - {placeholder} - - {Object.values(ContentModelEnum).map((option) => ( - - {t(`claimForm:${option}`)} - - ))} - - - ); -}; - -export default ReportTypeSelect; diff --git a/src/components/VerificationRequest/Dashboard/SourceChannelDistribution.tsx b/src/components/VerificationRequest/Dashboard/SourceChannelDistribution.tsx new file mode 100644 index 000000000..3617b802e --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/SourceChannelDistribution.tsx @@ -0,0 +1,106 @@ +import { Box, Typography } from "@mui/material"; +import { LegendColor, PieChartSVG } from "./VerificationRequestDashboard.style"; +import colors from "../../../styles/colors"; +import { VerificationRequestSourceChannel } from "../../../../server/verification-request/dto/types"; +import { useTranslation } from "next-i18next"; +import { StatsSourceChannelsProps } from "../../../types/VerificationRequest"; + +const SourceChannelDistribution = ({ + statsSourceChannels, +}: StatsSourceChannelsProps) => { + const { t } = useTranslation("verificationRequest"); + + const sourceColors = { + [VerificationRequestSourceChannel.Whatsapp]: colors.lightPrimary, + [VerificationRequestSourceChannel.Instagram]: colors.secondary, + [VerificationRequestSourceChannel.Website]: colors.tertiary, + [VerificationRequestSourceChannel.AutomatedMonitoring]: + colors.lightSecondary, + }; + + const calculatePieSegments = () => { + let currentAngle = 0; + return statsSourceChannels.map((channel) => { + const percentage = channel.percentage; + const angle = (percentage / 100) * 360; + const segment = { + startAngle: currentAngle, + endAngle: currentAngle + angle, + percentage, + color: sourceColors[channel.label], + label: channel.label, + }; + currentAngle += angle; + return segment; + }); + }; + + const createPiePath = (startAngle: number, endAngle: number) => { + const radius = 80; + const centerX = 100; + const centerY = 100; + + const clampedEnd = + endAngle - startAngle >= 360 ? startAngle + 359.99 : endAngle; + const startRad = (startAngle * Math.PI) / 180; + const endRad = (clampedEnd * Math.PI) / 180; + + const x1 = centerX + radius * Math.cos(startRad); + const y1 = centerY + radius * Math.sin(startRad); + const x2 = centerX + radius * Math.cos(endRad); + const y2 = centerY + radius * Math.sin(endRad); + + const largeArc = clampedEnd - startAngle > 180 ? 1 : 0; + + return `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`; + }; + + const pieSegments = calculatePieSegments(); + + return ( + <> + + {t("dashboard.sourcesTitle")} + + + {t("dashboard.sourcesSubtitle")} + + + + {pieSegments.map((segment) => ( + + ))} + + + + + {statsSourceChannels.map((channel) => ( + + + + {t(`verificationRequest:${channel.label}`, { + defaultValue: + channel.label.charAt(0).toUpperCase() + + channel.label.slice(1), + })} + + + {channel.percentage.toFixed(1)}% + + + ))} + + + ); +}; + +export { SourceChannelDistribution }; diff --git a/src/components/VerificationRequest/Dashboard/StatusDistribution.tsx b/src/components/VerificationRequest/Dashboard/StatusDistribution.tsx new file mode 100644 index 000000000..c2d3acfdd --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/StatusDistribution.tsx @@ -0,0 +1,54 @@ +import { Box, Typography } from "@mui/material"; +import { Bar } from "./VerificationRequestDashboard.style"; +import colors from "../../../styles/colors"; +import { useTranslation } from "next-i18next"; +import { StatsCount } from "../../../types/VerificationRequest"; + +const StatusDistribution = ({ verified, inAnalysis, pending }: StatsCount) => { + const { t } = useTranslation("verificationRequest"); + + const data = [ + { + key: "verified", + value: verified, + color: colors.primary, + }, + { + key: "inAnalysis", + value: inAnalysis, + color: colors.secondary, + }, + { + key: "pending", + value: pending, + color: colors.neutralSecondary, + }, + ]; + + const maxStatusValue = Math.max(...data.map((item) => item.value)); + + return ( + <> + {t("dashboard.statusTitle")} + + {t("dashboard.statusSubtitle")} + + + + {data.map(({ key, value, color }) => ( + + {value} + + + + + {t(`dashboard.${key}`)} + + + ))} + + + ); +}; + +export { StatusDistribution }; diff --git a/src/components/VerificationRequest/Dashboard/VerificationRequestActivity.tsx b/src/components/VerificationRequest/Dashboard/VerificationRequestActivity.tsx new file mode 100644 index 000000000..960c84120 --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/VerificationRequestActivity.tsx @@ -0,0 +1,71 @@ +import { Box, Card, CardContent, Typography } from "@mui/material"; +import colors from "../../../styles/colors"; +import { StatsRecentActivityProps } from "../../../types/VerificationRequest"; +import { useTranslation } from "next-i18next"; +import { formatTimeAgo } from "../../../helpers/formatTimeAgo"; + +const VerificationRequestActivity = ({ + statsRecentActivity, +}: StatsRecentActivityProps) => { + const { t } = useTranslation(); + + const STATUS_CONFIG = { + Posted: { + color: colors.low, + labelKey: "POSTED", + }, + "In Triage": { + color: colors.medium, + labelKey: "IN_TRIAGE", + }, + "Pre Triage": { + color: colors.neutralSecondary, + labelKey: "PRE_TRIAGE", + }, + Declined: { + color: colors.error, + labelKey: "DECLINED", + }, + } as const; + + return ( + + + + {t("verificationRequest:dashboard.activityTitle")} + + + {t("verificationRequest:dashboard.activitySubtitle")} + + + + {statsRecentActivity.map((activity) => { + const { color, labelKey } = STATUS_CONFIG[activity.status]; + + const message = t(`verificationRequest:activity.${activity.status}`, { + hash: activity.data_hash, + source: t( + `verificationRequest:${activity.sourceChannel}`, + activity.sourceChannel + ).toLowerCase(), + }); + + return ( + + + {t(`verificationRequest:${labelKey}`)} + + {message} + + {formatTimeAgo(activity.timestamp, t)} + + + ); + })} + + + + ); +}; + +export default VerificationRequestActivity; diff --git a/src/components/VerificationRequest/Dashboard/VerificationRequestCounts.tsx b/src/components/VerificationRequest/Dashboard/VerificationRequestCounts.tsx new file mode 100644 index 000000000..e78f49626 --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/VerificationRequestCounts.tsx @@ -0,0 +1,77 @@ +import { + CheckCircle, + Description, + HourglassEmpty, + Schedule, +} from "@mui/icons-material"; +import { Box, Card, Grid, Typography } from "@mui/material"; +import { StatsCount } from "../../../types/VerificationRequest"; +import { useTranslation } from "next-i18next"; + +const VerificationRequestCounts = ({ + total, + verified, + inAnalysis, + pending, + totalThisMonth, +}: StatsCount) => { + const { t } = useTranslation("verificationRequest"); + + const statsCards = [ + { + icon: , + label: t("dashboard.totalReports"), + value: total, + subLabel: `${ + total > 0 ? ((totalThisMonth / total) * 100).toFixed(1) : "0.0" + }% ${t("dashboard.receivedThisMonth")}`, + }, + { + icon: , + label: t("dashboard.verified"), + value: verified, + subLabel: `${((verified / total) * 100).toFixed(1)}% ${t( + "dashboard.ofTotal" + )}`, + }, + { + icon: , + label: t("dashboard.inAnalysis"), + value: inAnalysis, + subLabel: `${((inAnalysis / total) * 100).toFixed(1)}% ${t( + "dashboard.ofTotal" + )}`, + }, + { + icon: , + label: t("dashboard.pending"), + value: pending, + subLabel: `${((pending / total) * 100).toFixed(1)}% ${t( + "dashboard.ofTotal" + )}`, + }, + ]; + + return ( + + {statsCards.map((card) => ( + + + + {card.icon} + + + {card.value.toLocaleString()} + + {card.label} + + {card.subLabel} + + + + ))} + + ); +}; + +export default VerificationRequestCounts; diff --git a/src/components/VerificationRequest/Dashboard/VerificationRequestDashboard.style.tsx b/src/components/VerificationRequest/Dashboard/VerificationRequestDashboard.style.tsx new file mode 100644 index 000000000..7a4ed7608 --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/VerificationRequestDashboard.style.tsx @@ -0,0 +1,164 @@ +import { Box } from "@mui/material"; +import styled from "styled-components"; +import colors from "../../../styles/colors"; + +const PieChartSVG = styled.svg` + transform: rotate(-90deg); +`; + +const LegendColor = styled(Box)(({ color }) => ({ + width: 12, + height: 12, + borderRadius: 2, + backgroundColor: color, +})); + +const Bar = styled(Box)<{ height: number; color: string }>` + width: 80px; + height: ${(props) => props.height}%; + background-color: ${(props) => props.color}; + border-radius: 8px 8px 0 0; + transition: height 0.3s ease; + position: relative; + + &:hover { + opacity: 0.8; + } +`; + +const Dashboard = styled(Box)` + width: 90%; + margin-top: 16px; + padding: 24px; + background-color: ${colors.lightNeutral}; + + .title { + font-size: 18px; + color: ${colors.primary}; + } + + .subtitle { + font-size: 14px; + color: ${colors.neutralSecondary}; + margin-top: 4px; + } + + .label { + font-size: 12px; + color: ${colors.neutralSecondary}; + text-align: center; + margin-top: 12px; + } + + .card { + padding: 16px; + border-radius: 4px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + } + .card:hover { + transform: translateY(-4px); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + } + + .stats-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + } + + .stats-icon-wrapper { + display: flex; + align-items: center; + gap: 8px; + color: ${colors.neutralSecondary}; + } + + .value { + font-size: 18px; + color: ${colors.primary}; + margin-bottom: 8px; + } + + .stats-label { + font-size: 14px; + color: ${colors.neutralSecondary}; + } + + .PieChart-container { + display: flex; + justify-content: center; + align-items: center; + padding: 40px 0; + position: relative; + } + + .legend { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 24px; + } + + .legend-item { + display: flex; + align-items: center; + gap: 8px; + } + + .legend-label { + font-size: 14px; + color: ${colors.neutral}; + flex: 1; + } + + .text { + font-size: 14px; + color: ${colors.neutral}; + flex: 1; + } + + .legend-percentage { + font-size: 14px; + color: ${colors.neutralSecondary}; + white-space: nowrap; + } + + .BarChart-container { + display: flex; + justify-content: space-around; + align-items: flex-end; + padding: 40px 20px; + height: 300px; + } + .wrapper { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + max-width: 120px; + } + + .item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid ${colors.lightNeutralSecondary}; + + &:last-child { + border-bottom: none; + } + } + + .badge { + padding: 2px 6px; + border-radius: 4px; + color: ${colors.white}; + text-transform: uppercase; + background-color: ${(props) => props.color}; + } +`; + +export { Dashboard, LegendColor, PieChartSVG, Bar }; diff --git a/src/components/VerificationRequest/Dashboard/VerificationRequestDashboard.tsx b/src/components/VerificationRequest/Dashboard/VerificationRequestDashboard.tsx new file mode 100644 index 000000000..12c66e365 --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/VerificationRequestDashboard.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from "react"; +import { Grid, Typography } from "@mui/material"; +import verificationRequestApi from "../../../api/verificationRequestApi"; +import { + StatsCount, + StatsRecentActivity, + StatsSourceChannels, +} from "../../../types/VerificationRequest"; +import { Dashboard } from "./VerificationRequestDashboard.style"; +import VerificationRequestCounts from "./VerificationRequestCounts"; +import VerificationRequestOverview from "./VerificationRequestOverview"; +import VerificationRequestActivity from "./VerificationRequestActivity"; +import { useTranslation } from "next-i18next"; +import Loading from "../../Loading"; + +const VerificationRequestDashboard: React.FC = () => { + const { t } = useTranslation("verificationRequest"); + const [stats, setStats] = useState<{ + statsCount: StatsCount; + statsSourceChannels: StatsSourceChannels[]; + statsRecentActivity: StatsRecentActivity[]; + } | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + fetchStats(); + }, []); + + const fetchStats = async () => { + try { + setLoading(true); + setError(false); + const data = + await verificationRequestApi.getVerificationRequestStats(); + if (!data) { + throw new Error("Empty data"); + } + setStats(data); + } catch (err) { + console.error("Error fetching stats:", err); + setError(true); + } finally { + setLoading(false); + } + }; + + if (loading) { + return ; + } + + if (error || !stats) { + return ( + + {t("dashboard.errorLoading")} + + ); + } + + return ( + + + + + {t("dashboard.title")} + + + {t("dashboard.subtitle")} + + + + + + + + + + + + + + ); +}; + +export default VerificationRequestDashboard; diff --git a/src/components/VerificationRequest/Dashboard/VerificationRequestOverview.tsx b/src/components/VerificationRequest/Dashboard/VerificationRequestOverview.tsx new file mode 100644 index 000000000..f3e56c9ae --- /dev/null +++ b/src/components/VerificationRequest/Dashboard/VerificationRequestOverview.tsx @@ -0,0 +1,27 @@ +import { Card, Grid } from "@mui/material"; +import { StatsSourceChannelsProps } from "../../../types/VerificationRequest"; +import { SourceChannelDistribution } from "./SourceChannelDistribution"; +import { StatusDistribution } from "./StatusDistribution"; + +const VerificationRequestOverview = ({ + statsCounts, + statsSourceChannels, +}: StatsSourceChannelsProps) => ( + + + + + + + + + + + + + +); + +export default VerificationRequestOverview; diff --git a/src/components/VerificationRequest/EditVerificationRequestDrawer.tsx b/src/components/VerificationRequest/EditVerificationRequestDrawer.tsx deleted file mode 100644 index 146ae9441..000000000 --- a/src/components/VerificationRequest/EditVerificationRequestDrawer.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { VerificationRequest } from "../../types/VerificationRequest"; -import { useTranslation } from "react-i18next"; -import { useForm } from "react-hook-form"; -import createVerificationRequestForm from "./CreateVerificationRequest/fieldLists/CreateVerificationRequestForm"; -import DynamicForm from "../Form/DynamicForm"; -import AletheiaButton, { ButtonType } from "../Button"; -import { Box, Divider, Grid } from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import DeleteIcon from "@mui/icons-material/Delete"; -import verificationRequestApi from "../../api/verificationRequestApi"; -import AletheiaInput from "../AletheiaInput"; -import LargeDrawer from "../LargeDrawer"; - -interface EditVerificationRequestDrawerProps { - open: boolean; - onClose: () => void; - verificationRequest: VerificationRequest; - onSave: (updatedRequest: VerificationRequest) => void; -} - -interface ExtraSourceItem { - id: string; - value: string; -} - -const useExtraSources = (initialSources: string[] = []) => { - const [extraSources, setExtraSources] = useState( - initialSources.map((value) => ({ id: crypto.randomUUID(), value })) - ); - - const addSource = () => - setExtraSources([...extraSources, { id: crypto.randomUUID(), value: "" }]); - - const removeSource = (idToRemove: string) => - setExtraSources(extraSources.filter((source) => source.id !== idToRemove)); - - const updateSource = (id: string, value: string) => { - setExtraSources((prev) => - prev.map((source) => - source.id === id ? { ...source, value } : source - ) - ); - }; - - return { extraSources, addSource, removeSource, updateSource }; -}; - -const EditVerificationRequestDrawer: React.FC = ({ - open, - onClose, - verificationRequest, - onSave, -}) => { - const { t } = useTranslation(); - const [isSubmit, setIsSubmit] = useState(false); - const { extraSources, addSource, removeSource, updateSource } = useExtraSources([]); - - const { handleSubmit, control, formState: { errors }, reset } = useForm({ - defaultValues: { - content: "", - publicationDate: "", - date: "", - heardFrom: "", - source: [], - } - }); - - useEffect(() => { - if (open && verificationRequest) { - reset({ - content: verificationRequest.content, - publicationDate: verificationRequest.publicationDate, - heardFrom: verificationRequest.heardFrom, - source: verificationRequest.source || [], - }); - } - }, [open, verificationRequest, reset]); - - const editabledFields = new Set(["publicationDate", "source"]); - const excludedField = new Set(["email"]); - - const isFieldDisabled = (fieldName: string, verificationRequest) => { - if (fieldName === "source") { - return verificationRequest?.source?.length > 0; - } - return !editabledFields.has(fieldName); - } - - const configureFieldRules = (field, verificationRequest) => { - const disabled = isFieldDisabled(field.fieldName, verificationRequest); - - if (field.fieldName === "source") { - return { ...field, disabled, rules: {} }; - } - return { ...field, disabled }; - } - - const shouldIncludeField = (field) => { - return !excludedField.has(field.fieldName) - } - - const getEditableFields = (verificationRequest) => { - return createVerificationRequestForm - .map(field => configureFieldRules(field, verificationRequest)) - .filter(shouldIncludeField); - } - - const formFields = getEditableFields(verificationRequest); - - const formatExistingSources = (sources) => - Array.isArray(sources) - ? sources - .map(src => { - if (typeof src === "object" && src._id) { - const originalSource = verificationRequest.source?.find(existingSource => existingSource.targetId === src.targetId); - return originalSource?.href ? { href: originalSource.href } : null; - } - return null; - }) - .filter(src => src?.href) - : []; - - const formatNewSources = (sources: ExtraSourceItem[]) => - sources.filter(src => src.value.trim() !== "").map(src => ({ href: src.value })); - - const onSubmit = async (data) => { - setIsSubmit(true); - try { - const existing = formatExistingSources(data.source); - const newSources = formatNewSources(extraSources); - const allSources = [...existing, ...newSources]; - const validSources = allSources.filter(src => src.href?.trim()); - - const updateData = { - publicationDate: data.publicationDate, - source: validSources, - }; - - const response = await verificationRequestApi.updateVerificationRequest( - verificationRequest._id, - updateData, - t, - 'update' - ); - - if (response) { - onSave(response); - onClose(); - } - } catch (error) { - console.error("Edit Verification Request error", error) - } finally { - setIsSubmit(false); - } - }; - - const machineValues = { - ...verificationRequest, - source: Array.isArray(verificationRequest.source) - ? verificationRequest.source.map(src => src.href) - : [], - }; - - return ( - - - -

- {t("verificationRequest:titleEditVerificationRequest")} -

- -
- - - - {extraSources.map((source) => ( - - updateSource(source.id, e.target.value)} - placeholder={t("verificationRequest:sourcePlaceholder")} - /> - removeSource(source.id)} - style={{ minWidth: "auto", padding: "8px" }} - > - - - - ))} - - - - - - - - {t("admin:saveButtonLabel")} - - - {t("orderModal:cancelButton")} - - - -
-
- ) -}; - -export default EditVerificationRequestDrawer; \ No newline at end of file diff --git a/src/components/VerificationRequest/FilterBar.tsx b/src/components/VerificationRequest/FilterBar.tsx new file mode 100644 index 000000000..c20973250 --- /dev/null +++ b/src/components/VerificationRequest/FilterBar.tsx @@ -0,0 +1,63 @@ +import { FormControl } from "@mui/material"; +import FilterPopover from "./FilterPopover"; +import SelectFilter from "./SelectFilter"; +import { useAppSelector } from "../../store/store"; +import DateRangePicker from "../DateRangePicker"; + +const FilterBar = ({ state, actions }) => { + const { vw } = useAppSelector((state) => state); + const { priorityFilter, sourceChannelFilter, startDate, endDate } = state; + + const { + setStartDate, + setEndDate, + setApplyFilters, + setPaginationModel, + setPriorityFilter, + setSourceChannelFilter, + createFilterChangeHandler, + } = actions; + + const handleStartDateChange = (date) => { + setStartDate(date); + setPaginationModel((prev) => ({ ...prev, page: 0 })); + setApplyFilters(true); + }; + + const handleEndDateChange = (date) => { + setEndDate(date); + setPaginationModel((prev) => ({ ...prev, page: 0 })); + setApplyFilters(true); + }; + + return ( + + + + + + + ); +}; + +export default FilterBar; diff --git a/src/components/VerificationRequest/FilterManagers.tsx b/src/components/VerificationRequest/FilterManagers.tsx index eb5d24940..ae184294d 100644 --- a/src/components/VerificationRequest/FilterManagers.tsx +++ b/src/components/VerificationRequest/FilterManagers.tsx @@ -1,78 +1,37 @@ import React from "react"; -import { useAppSelector } from "../../store/store"; import { ActionTypes } from "../../store/types"; -import SelectFilter from "./SelectFilter"; -import FilterPopover from "./FilterPopover"; -import { - Grid, - IconButton, - Button, - FormControl, -} from "@mui/material"; -import { FilterList } from "@mui/icons-material"; +import { Grid, Button } from "@mui/material"; +import FilterToggleButtons from "./FilterToggleButtons"; +import FilterBar from "./FilterBar"; const FilterManager = ({ state, actions }) => { const { priorityFilter, sourceChannelFilter, - filterValue, - filterType, - anchorEl, - autoCompleteTopicsResults, topicFilterUsed, impactAreaFilterUsed, + viewMode, + startDate, + endDate, } = state; const { + setViewMode, setPriorityFilter, setSourceChannelFilter, setFilterValue, - setFilterType, - setAnchorEl, - setPaginationModel, setApplyFilters, - fetchTopicList, - createFilterChangeHandler, dispatch, t, + setStartDate, + setEndDate, } = actions; - const { vw } = useAppSelector((state) => state); - - const handleFilterClick = (event) => setAnchorEl(event.currentTarget); - const handleFilterClose = () => setAnchorEl(null); - - const handleFilterApply = () => { - setAnchorEl(null); - if (filterType === "topics" && filterValue) { - const topicsToAdd = Array.isArray(filterValue) - ? filterValue - : [filterValue]; - const updatedTopics = [...new Set([...topicFilterUsed, ...topicsToAdd])]; - dispatch({ - type: ActionTypes.SET_TOPIC_FILTER_USED, - topicFilterUsed: updatedTopics, - }); - } - if (filterType === "impactArea" && filterValue) { - const impactAreasToAdd = Array.isArray(filterValue) - ? filterValue - : [filterValue]; - const updatedImpactAreas = [ - ...new Set([...impactAreaFilterUsed, ...impactAreasToAdd]), - ]; - dispatch({ - type: ActionTypes.SET_IMPACT_AREA_FILTER_USED, - impactAreaFilterUsed: updatedImpactAreas, - }); - } - setPaginationModel((prev) => ({ ...prev, page: 0 })); - setApplyFilters(true); - }; const handleResetFilters = () => { setFilterValue([]); setPriorityFilter("all"); setSourceChannelFilter("all"); - setPaginationModel({ pageSize: 10, page: 0 }); + setStartDate(null); + setEndDate(null); dispatch({ type: ActionTypes.SET_TOPIC_FILTER_USED, topicFilterUsed: [], @@ -94,55 +53,21 @@ const FilterManager = ({ state, actions }) => { style={{ marginTop: 30 }} > - - - - - - - - + + {viewMode === "board" && } - {(topicFilterUsed.length > 0 || - priorityFilter || - sourceChannelFilter !== "all" || - impactAreaFilterUsed.length > 0) && ( - + {viewMode === "board" && + (topicFilterUsed.length > 0 || + priorityFilter !== "all" || + sourceChannelFilter !== "all" || + impactAreaFilterUsed.length > 0 || + startDate || + endDate) && ( - - )} + )}
); }; - export default FilterManager; diff --git a/src/components/VerificationRequest/FilterPopover.tsx b/src/components/VerificationRequest/FilterPopover.tsx index e73d09ac7..f7ae84a68 100644 --- a/src/components/VerificationRequest/FilterPopover.tsx +++ b/src/components/VerificationRequest/FilterPopover.tsx @@ -1,68 +1,113 @@ import React from "react"; -import { - Grid, - Popover, - Button, - FormControl, -} from "@mui/material"; +import { Grid, Popover, Button, FormControl, IconButton } from "@mui/material"; import AdvancedSearch from "../Search/AdvancedSearch"; import SelectFilter from "./SelectFilter"; +import { FilterList } from "@mui/icons-material"; +import { ActionTypes } from "../../store/types"; -const FilterPopover = ({ +const FilterPopover = ({ state, actions }) => { + const { anchorEl, - onClose, filterType, - setFilterType, + filterValue, + topicFilterUsed, + impactAreaFilterUsed, + autoCompleteTopicsResults, + } = state; + + const { + setPaginationModel, setFilterValue, fetchTopicList, - autoCompleteTopicsResults, - onFilterApply, + setApplyFilters, + setFilterType, + setAnchorEl, + dispatch, t, -}) => ( - setAnchorEl(event.currentTarget); + const handleFilterClose = () => setAnchorEl(null); + + const handleFilterApply = () => { + setAnchorEl(null); + if (filterType === "topics" && filterValue) { + const topicsToAdd = Array.isArray(filterValue) + ? filterValue + : [filterValue]; + const updatedTopics = [...new Set([...topicFilterUsed, ...topicsToAdd])]; + dispatch({ + type: ActionTypes.SET_TOPIC_FILTER_USED, + topicFilterUsed: updatedTopics, + }); + } + if (filterType === "impactArea" && filterValue) { + const impactAreasToAdd = Array.isArray(filterValue) + ? filterValue + : [filterValue]; + const updatedImpactAreas = [ + ...new Set([...impactAreaFilterUsed, ...impactAreasToAdd]), + ]; + dispatch({ + type: ActionTypes.SET_IMPACT_AREA_FILTER_USED, + impactAreaFilterUsed: updatedImpactAreas, + }); + } + setPaginationModel((prev) => ({ ...prev, page: 0 })); + setApplyFilters(true); + }; + + return ( + + + + + + > + + + setFilterType(e)} + /> + + + {filterType === "topics" && ( - - setFilterType(e)} - /> - + - {filterType === "topics" && ( - - - - )} - {filterType === "impactArea" && ( - - - - )} - - + )} + {filterType === "impactArea" && ( + + + )} + + + - -); + +
+ ); +}; export default FilterPopover; diff --git a/src/components/VerificationRequest/FilterToggleButtons.tsx b/src/components/VerificationRequest/FilterToggleButtons.tsx new file mode 100644 index 000000000..2d02d4f28 --- /dev/null +++ b/src/components/VerificationRequest/FilterToggleButtons.tsx @@ -0,0 +1,41 @@ +import { ViewList, ViewModule } from "@mui/icons-material"; +import { ToggleButton, ToggleButtonGroup } from "@mui/material"; +import colors from "../../styles/colors"; + +const FilterToggleButtons = ({ viewMode, setViewMode }) => ( + { + if (newView !== null) { + setViewMode(newView); + } + }} + aria-label="view mode" + size="small" + sx={{ + "& .MuiToggleButton-root:not(.Mui-selected)": { + backgroundColor: colors.lightNeutral, + color: colors.shadow, + "&:hover": { + backgroundColor: colors.lightNeutralSecondary, + }, + }, + "& .Mui-selected": { + backgroundColor: colors.primary, + "&:hover": { + backgroundColor: colors.primary, + }, + }, + }} + > + + + + + + + +); + +export default FilterToggleButtons; diff --git a/src/components/VerificationRequest/MetaChip.tsx b/src/components/VerificationRequest/MetaChip.tsx index 8d38b5aaa..d79060d19 100644 --- a/src/components/VerificationRequest/MetaChip.tsx +++ b/src/components/VerificationRequest/MetaChip.tsx @@ -6,9 +6,10 @@ interface MetaChipProps { label: string; label_value: string; style: React.CSSProperties; + dataCy?: string; } -export const MetaChip: React.FC = ({ icon, label, label_value, style }) => { +export const MetaChip: React.FC = ({ icon, label, label_value, style, dataCy }) => { const { t } = useTranslation(); return ( = ({ icon, label, label_value, st ); -}; \ No newline at end of file +}; diff --git a/src/components/VerificationRequest/PersonalitiesSection.tsx b/src/components/VerificationRequest/PersonalitiesSection.tsx new file mode 100644 index 000000000..67089fec1 --- /dev/null +++ b/src/components/VerificationRequest/PersonalitiesSection.tsx @@ -0,0 +1,302 @@ +import React from "react"; +import Link from "next/link"; +import { + Box, + Typography, + Chip, + Stack, + Card, + Skeleton, + Avatar, + Button, + Alert, +} from "@mui/material"; +import { + Person, + InfoOutlined, + ErrorOutline, + Refresh, +} from "@mui/icons-material"; +import { useTranslation } from "next-i18next"; +import type { PersonalityWithWikidata } from "../../types/PersonalityWithWikidata"; +import colors from "../../styles/colors"; + +interface PersonalitiesSectionProps { + personalities: PersonalityWithWikidata[]; + isLoading: boolean; + error: Error | null; + expectedCount: number; + onRetry: () => void; +} + +interface PersonalityCardProps { + personality: PersonalityWithWikidata; +} + +const PersonalityCard: React.FC = ({ personality }) => { + const { t } = useTranslation(); + const personalityUrl = `/personality/${personality.slug}`; + + const cardContent = ( + + + {!personality.avatar && } + + + + {personality.name} + + {personality.description && ( + + + + {personality.description} + + + )} + + + ); + + return ( + + {cardContent} + + ); +}; + +const PersonalitiesSection: React.FC = ({ + personalities, + isLoading, + error, + expectedCount, + onRetry, +}) => { + const { t } = useTranslation(); + + const renderContent = () => { + if (error) { + return ( + } + action={ + + } + > + {t("verificationRequest:errorLoadingPersonalities")} + + ); + } + + if (isLoading) { + return ( + + + {Array.from({ length: expectedCount }).map( + (_, index) => ( + + + + + + + + ) + )} + + + ); + } + + if (personalities.length === 0) { + return ( + + {t("verificationRequest:noPersonalitiesFound")} + + ); + } + + return ( + + {personalities.map((personality) => ( + + + + ))} + + ); + }; + + return ( + + + +
{renderContent()}
+
+ ); +}; + +export default PersonalitiesSection; diff --git a/src/components/VerificationRequest/RequestDates.tsx b/src/components/VerificationRequest/RequestDates.tsx index c8b808838..d35a92bc7 100644 --- a/src/components/VerificationRequest/RequestDates.tsx +++ b/src/components/VerificationRequest/RequestDates.tsx @@ -6,18 +6,21 @@ interface RequestDatesProps { icon?: React.ReactNode; label: string; value: string; + dataCy?: string; } -export const RequestDates: React.FC = ({ icon, label, value }) => { +export const RequestDates: React.FC = ({ icon, label, value, dataCy }) => { const publicationDate = new Date(value); const isValidDate = !Number.isNaN(publicationDate.getTime()); return ( - + {icon} {label} @@ -25,4 +28,4 @@ export const RequestDates: React.FC = ({ icon, label, value } ); -}; \ No newline at end of file +}; diff --git a/src/components/VerificationRequest/SelectFilter.tsx b/src/components/VerificationRequest/SelectFilter.tsx index 047f428e7..89d6be020 100644 --- a/src/components/VerificationRequest/SelectFilter.tsx +++ b/src/components/VerificationRequest/SelectFilter.tsx @@ -25,10 +25,10 @@ interface SelectFilterProps { const filterActions: Record SelectOption[]> = { filterByPriority: () => [ { value: "all", labelKey: "allPriorities" }, - { value: "critical", labelKey: "priorityCritical" }, - { value: "high", labelKey: "priorityHigh" }, - { value: "medium", labelKey: "priorityMedium" }, - { value: "low", labelKey: "priorityLow" }, + { value: "critical", labelKey: "priority.critical" }, + { value: "high", labelKey: "priority.filter_high" }, + { value: "medium", labelKey: "priority.filter_medium" }, + { value: "low", labelKey: "priority.filter_low" }, ], filterBySourceChannel: () => { diff --git a/src/components/VerificationRequest/SourceListVerificationRequest.tsx b/src/components/VerificationRequest/SourceListVerificationRequest.tsx index c372c6e02..1f6a93914 100644 --- a/src/components/VerificationRequest/SourceListVerificationRequest.tsx +++ b/src/components/VerificationRequest/SourceListVerificationRequest.tsx @@ -1,15 +1,15 @@ import React from "react"; import Link from "next/link"; import { VerificationRequestContent } from "./VerificationRequestContent"; +import { truncateUrl } from "../../helpers/verificationRequestCardHelper"; interface SourceListProps { sources: Array<{ href?: string }>; t: (key: string) => string; - truncateUrl: (url: string) => string; id: string; } -const SourceList: React.FC = ({ sources, t, truncateUrl, id }) => { +const SourceList: React.FC = ({ sources, t, id }) => { if (!sources?.length) return null; const flatSources = sources.flat().filter((source) => !!source.href); @@ -21,6 +21,7 @@ const SourceList: React.FC = ({ sources, t, truncateUrl, id }) {flatSources.map((source, index) => ( { description: ( {t("seo:claimCreateTitle")} @@ -44,40 +44,19 @@ const VerificationRequestAlert = ({ targetId, verificationRequestId }) => { } if (targetId) { return { - type: "warning", + type: "success", showIcon: false, - message: ( -
- - {t( - "verificationRequest:openVerificationRequestClaimLabel" - )} - + message: t("verificationRequest:openVerificationRequestClaimLabel"), + description: ( {t( "verificationRequest:openVerificationRequestClaimButton" )} -
), - description: null, }; } return null; diff --git a/src/components/VerificationRequest/VerificationRequestBoardView.tsx b/src/components/VerificationRequest/VerificationRequestBoardView.tsx index 7ccf5d154..f36ff2247 100644 --- a/src/components/VerificationRequest/VerificationRequestBoardView.tsx +++ b/src/components/VerificationRequest/VerificationRequestBoardView.tsx @@ -11,16 +11,12 @@ import { } from "@mui/material"; import colors from "../../styles/colors"; import AletheiaButton, { ButtonType } from "../Button"; -import { SeverityEnum, VerificationRequestStatus } from "../../../server/verification-request/dto/types"; -import { SeverityLevel } from "../../types/VerificationRequest"; +import { VerificationRequestStatus } from "../../types/enums"; import VerificationRequestDetailDrawer from "./VerificationRequestDetailDrawer"; - -const SEVERITY_COLOR_MAP: Record = { - low: colors.low, - medium: colors.medium, - high: colors.high, - critical: colors.critical, -}; +import { + getSeverityColor, + getSeverityLabel, +} from "../../helpers/verificationRequestCardHelper"; const VerificationRequestBoardView = ({ state, actions }) => { const { loading, filteredRequests, totalVerificationRequests, paginationModel } = state; @@ -30,9 +26,9 @@ const VerificationRequestBoardView = ({ state, actions }) => { const [drawerOpen, setDrawerOpen] = useState(false); const statuses = [ - { key: VerificationRequestStatus.PRE_TRIAGE, label: t("verificationRequest:statusPreTriage") }, - { key: VerificationRequestStatus.IN_TRIAGE, label: t("verificationRequest:statusInTriage") }, - { key: VerificationRequestStatus.POSTED, label: t("verificationRequest:statusPosted") }, + { key: VerificationRequestStatus.PRE_TRIAGE, label: t("verificationRequest:PRE_TRIAGE") }, + { key: VerificationRequestStatus.IN_TRIAGE, label: t("verificationRequest:IN_TRIAGE") }, + { key: VerificationRequestStatus.POSTED, label: t("verificationRequest:POSTED") }, ]; const handleCardClick = (request: any) => { @@ -46,9 +42,9 @@ const VerificationRequestBoardView = ({ state, actions }) => { }; const handleRequestUpdate = (newStatus) => { - setSelectedRequest(null); - fetchData(newStatus); -}; + setSelectedRequest(null); + fetchData(newStatus); + }; useEffect(() => { for (const status in paginationModel) { @@ -64,33 +60,6 @@ const VerificationRequestBoardView = ({ state, actions }) => { statuses.map(({ key }) => [key, totalVerificationRequests[key]]) ); - const getSeverityColor = (severity: SeverityEnum | undefined): string => { - if (!severity) return colors.neutralSecondary; - - const severityStr = String(severity); - - let severityLevel: SeverityLevel; - - if (severityStr.startsWith("low")) severityLevel = "low"; - else if (severityStr.startsWith("medium")) severityLevel = "medium"; - else if (severityStr.startsWith("high")) severityLevel = "high"; - else if (severityStr === "critical") severityLevel = "critical"; - else severityLevel = "low"; - - return SEVERITY_COLOR_MAP[severityLevel]; - }; - - const formatSeverityLabel = (severity: string) => { - if (!severity) return "N/A"; - const parts = severity.split("_"); - if (parts.length === 2) { - return `${parts[0].charAt(0).toUpperCase() + parts[0].slice(1)} (${ - parts[1] - })`; - } - return severity.charAt(0).toUpperCase() + severity.slice(1); - }; - const truncateText = (text: string, maxLength: number) => { if (!text) return ""; return text.length > maxLength @@ -154,10 +123,10 @@ const VerificationRequestBoardView = ({ state, actions }) => { {t("common:loading")} ) : ( - - {groupedRequests[status.key].map((request) => ( + {groupedRequests[status.key].map((request, index) => ( { }} > { @@ -236,7 +206,13 @@ const VerificationRequestBoardView = ({ state, actions }) => { {t("verificationRequest:tagSourceChannel")}: {" "} - {truncateText(request.sourceChannel, 30)} + {truncateText( + t( + `verificationRequest:${request.sourceChannel}`, + { defaultValue: request.sourceChannel }, + ), + 30, + )} )} diff --git a/src/components/VerificationRequest/VerificationRequestCard.tsx b/src/components/VerificationRequest/VerificationRequestCard.tsx index 1b594cee9..f5b7d3bbd 100644 --- a/src/components/VerificationRequest/VerificationRequestCard.tsx +++ b/src/components/VerificationRequest/VerificationRequestCard.tsx @@ -21,6 +21,10 @@ import { MetaChip } from "./MetaChip"; import { VerificationRequestContent } from "./VerificationRequestContent"; import { RequestDates } from "./RequestDates"; import SourceList from "./SourceListVerificationRequest"; +import { + getSeverityColor, + getSeverityLabel, +} from "../../helpers/verificationRequestCardHelper"; const ContentWrapper = styled.div` display: flex; @@ -28,7 +32,7 @@ const ContentWrapper = styled.div` word-wrap: break-word; overflow: hidden; width: 50vw; - @media (max-width: 768px) { + @media (max-width: 1024px) { flex-direction: column; width: 70vw; flex-wrap: "wrap"; @@ -40,24 +44,11 @@ const MetaChipContainer = styled(Grid)` justify-content: space-around; margin-top: 16px; flex-wrap: wrap; + gap: 2px; `; const smallGreyIcon = { fontSize: 18, color: "grey" }; -const truncateUrl = (url) => { - try { - const { hostname, pathname } = new URL(url); - const maxLength = 30; - const shortPath = - pathname.length > maxLength - ? `${pathname.substring(0, maxLength)}...` - : pathname; - return `${hostname}${shortPath}`; - } catch (e) { - return url; - } -}; - const VerificationRequestCard = ({ verificationRequest, actions = [], @@ -77,13 +68,19 @@ const VerificationRequestCard = ({ label_value: t( `claimForm:${verificationRequest.reportType || "undefined"}` ), + dataCy: "testVerificationRequestReportType", style: { backgroundColor: colors.secondary, color: colors.white }, }, { icon: , key: `${verificationRequest._id}|receptionChannel`, label: t("verificationRequest:tagSourceChannel"), - label_value: verificationRequest.sourceChannel, + label_value: + t( + `verificationRequest:${verificationRequest.sourceChannel}`, + { defaultValue: verificationRequest.sourceChannel }, + ), + dataCy: "testVerificationRequestSourceChannel", style: { backgroundColor: colors.primary, color: colors.white }, }, { @@ -91,6 +88,7 @@ const VerificationRequestCard = ({ key: `${verificationRequest._id}|impactArea`, label: t("verificationRequest:tagImpactArea"), label_value: verificationRequest.impactArea?.name, + dataCy: "testVerificationRequestImpactArea", style: { backgroundColor: colors.neutralSecondary, color: colors.white, @@ -100,8 +98,12 @@ const VerificationRequestCard = ({ icon: , key: `${verificationRequest._id}|severity`, label: t("verificationRequest:tagSeverity"), - label_value: verificationRequest.severity, - style: { backgroundColor: colors.error, color: colors.white }, + label_value: getSeverityLabel(verificationRequest.severity, t), + dataCy: "testVerificationRequestSeverity", + style: { + backgroundColor: getSeverityColor(verificationRequest.severity), + color: colors.white, + }, }, ]; @@ -134,6 +136,7 @@ const VerificationRequestCard = ({ {verificationRequest.publicationDate && ( } label={t("verificationRequest:tagPublicationDate")} value={verificationRequest.publicationDate} @@ -164,6 +168,7 @@ const VerificationRequestCard = ({ {verificationRequest.date && ( } label={t("verificationRequest:tagDate")} value={verificationRequest.date} @@ -175,6 +180,7 @@ const VerificationRequestCard = ({ )}
@@ -183,7 +189,6 @@ const VerificationRequestCard = ({ )} @@ -191,7 +196,7 @@ const VerificationRequestCard = ({ {metaChipData.map( - ({ icon, key, label, label_value, style }) => ( + ({ icon, key, label, label_value, style, dataCy }) => ( ) diff --git a/src/components/VerificationRequest/VerificationRequestContent.tsx b/src/components/VerificationRequest/VerificationRequestContent.tsx index 09239789f..ab37969a9 100644 --- a/src/components/VerificationRequest/VerificationRequestContent.tsx +++ b/src/components/VerificationRequest/VerificationRequestContent.tsx @@ -4,15 +4,16 @@ import { Box, Typography } from "@mui/material"; interface VerificationRequestContentProps { label: string; value: React.ReactNode; + dataCy?: string; } -export const VerificationRequestContent: React.FC = ({ label, value }) => { +export const VerificationRequestContent: React.FC = ({ label, value, dataCy }) => { return ( - + {label} @@ -20,4 +21,4 @@ export const VerificationRequestContent: React.FC ); -}; \ No newline at end of file +}; diff --git a/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx b/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx index 752540d67..d5d837c71 100644 --- a/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx +++ b/src/components/VerificationRequest/VerificationRequestDetailDrawer.tsx @@ -24,8 +24,7 @@ import { ReviewTaskEvents, ReviewTaskTypeEnum, } from "../../machines/reviewTask/enums"; -import { Roles } from "../../types/enums"; -import { VerificationRequestStatus } from "../../../server/verification-request/dto/types"; +import { VerificationRequestStatus } from "../../types/enums"; import { currentUserRole } from "../../atoms/currentUser"; import { useAppSelector } from "../../store/store"; import colors from "../../styles/colors"; @@ -42,6 +41,13 @@ import { currentUserId } from "../../atoms/currentUser"; import reviewTaskApi from "../../api/reviewTaskApi"; import sendReviewNotifications from "../../notifications/sendReviewNotifications"; import AletheiaCaptcha from "../AletheiaCaptcha"; +import PersonalitiesSection from "./PersonalitiesSection"; +import { usePersonalities } from "../../hooks/usePersonalities"; +import { + getSeverityColor, + getSeverityLabel, +} from "../../helpers/verificationRequestCardHelper"; +import { isStaff } from "../../utils/GetUserPermission"; interface VerificationRequestDetailDrawerProps { verificationRequest: any; @@ -50,44 +56,9 @@ interface VerificationRequestDetailDrawerProps { onUpdate?: (oldStatus: string, newStatus: string) => void; } -const getSeverityColor = (severity: string) => { - if (!severity) return colors.neutralSecondary; - const lowerSeverity = severity.toLowerCase(); - if (lowerSeverity === "critical") return "#d32f2f"; - if (lowerSeverity.startsWith("high")) return "#f57c00"; - if (lowerSeverity.startsWith("medium")) return "#fbc02d"; - if (lowerSeverity.startsWith("low")) return "#388e3c"; - return colors.neutralSecondary; -}; - -const formatSeverityLabel = (severity: string) => { - if (!severity) return "N/A"; - const parts = severity.split("_"); - if (parts.length === 2) { - return `${parts[0].charAt(0).toUpperCase() + parts[0].slice(1)} (${ - parts[1] - })`; - } - return severity.charAt(0).toUpperCase() + severity.slice(1); -}; - -const truncateUrl = (url) => { - try { - const { hostname, pathname } = new URL(url); - const maxLength = 30; - const shortPath = - pathname.length > maxLength - ? `${pathname.substring(0, maxLength)}...` - : pathname; - return `${hostname}${shortPath}`; - } catch (e) { - return url; - } -}; - const VerificationRequestDetailDrawer: React.FC = ({ verificationRequest, open, onClose, onUpdate }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { vw } = useAppSelector((state) => state); const [role] = useAtom(currentUserRole); @@ -99,11 +70,21 @@ const VerificationRequestDetailDrawer: React.FC 0, + language: i18n.language || "en", + }); + + const canApprove = isStaff(role); useEffect(() => { setCurrentRequest(verificationRequest); @@ -209,54 +190,53 @@ const VerificationRequestDetailDrawer: React.FC, - key: `${currentRequest._id}|reportType`, - label: t("verificationRequest:tagReportType"), - label_value: t( - `claimForm:${ - currentRequest.reportType || "undefined" - }` - ), - style: { - backgroundColor: colors.secondary, - color: colors.white, - }, - }, - { - icon: , - key: `${currentRequest._id}|receptionChannel`, - label: t("verificationRequest:tagSourceChannel"), - label_value: currentRequest.sourceChannel, - style: { - backgroundColor: colors.primary, - color: colors.white, - }, - }, - { - icon: , - key: `${currentRequest._id}|impactArea`, - label: t("verificationRequest:tagImpactArea"), - label_value: currentRequest.impactArea?.name, - style: { - backgroundColor: colors.neutralSecondary, - color: colors.white, - }, - }, - { - icon: , - key: `${currentRequest._id}|severity`, - label: t("verificationRequest:tagSeverity"), - label_value: - formatSeverityLabel(currentRequest.severity) || "N/A", - style: { - backgroundColor: getSeverityColor( - currentRequest.severity - ), - color: colors.white, - }, - }, - ] + { + icon: , + key: `${currentRequest._id}|reportType`, + label: t("verificationRequest:tagReportType"), + label_value: t( + `claimForm:${currentRequest.reportType || "undefined" + }` + ), + style: { + backgroundColor: colors.secondary, + color: colors.white, + }, + }, + { + icon: , + key: `${currentRequest._id}|receptionChannel`, + label: t("verificationRequest:tagSourceChannel"), + label_value: t( + `verificationRequest:${currentRequest.sourceChannel}`, + { defaultValue: currentRequest.sourceChannel }, + ), + style: { + backgroundColor: colors.primary, + color: colors.white, + }, + }, + { + icon: , + key: `${currentRequest._id}|impactArea`, + label: t("verificationRequest:tagImpactArea"), + label_value: currentRequest.impactArea?.name, + style: { + backgroundColor: colors.neutralSecondary, + color: colors.white, + }, + }, + { + icon: , + key: `${currentRequest._id}|severity`, + label: t("verificationRequest:tagSeverity"), + label_value: getSeverityLabel(currentRequest.severity, t), + style: { + backgroundColor: getSeverityColor(currentRequest.severity), + color: colors.white, + }, + }, + ] : []; return ( @@ -299,6 +279,7 @@ const VerificationRequestDetailDrawer: React.FC @@ -476,14 +456,29 @@ const VerificationRequestDetailDrawer: React.FC + + {currentRequest?.identifiedData && + currentRequest.identifiedData.length > + 0 && ( + + )} diff --git a/src/components/VerificationRequest/VerificationRequestDisplay.style.tsx b/src/components/VerificationRequest/VerificationRequestDisplay.style.tsx index 7d4d59885..b67b57087 100644 --- a/src/components/VerificationRequest/VerificationRequestDisplay.style.tsx +++ b/src/components/VerificationRequest/VerificationRequestDisplay.style.tsx @@ -1,24 +1,68 @@ import styled from "styled-components"; -import { queries } from "../../styles/mediaQueries"; import { Grid } from "@mui/material"; +import colors from "../../styles/colors"; const VerificationRequestDisplayStyle = styled(Grid)` display: flex; gap: 16px; - .cta-create-claim { - display: flex; - gap: 32px; - align-items: center; - margin-bottom: 32px; - } - - @media ${queries.sm} { - .cta-create-claim { - gap: 16px; - flex-direction: column; - } - } + .container { + width: 100%; + height: auto; + } + + .title { + font-family: initial; + font-weight: 600; + font-size: 26px; + line-height: 1.35; + margin: 8px 0px; + } + + .flex-container { + display: flex; + gap: 16px; + flex-wrap: wrap; + justify-content: space-around; + } + + .box-title { + display: flex; + justify-content: space-between; + align-items: center; + } + + .container-alert { + margin-top: 32px; + width: fit-content; + } + + .edit-icon { + font-size: 18px; + } + + .verification-request-list ::-webkit-scrollbar { + height: 4px; + width: 4px; + background: ${colors.neutralTertiary}; + } + + .verification-request-list ::-webkit-scrollbar-thumb { + background: ${colors.blackTertiary}; + border-radius: 4px; + } + + .verification-request-list ::-moz-scrollbar { + height: 4px; + width: 4px; + background: ${colors.neutralTertiary}; + } + + .verification-request-list ::-mz-scrollbar { + height: 4px; + width: 4px; + background: ${colors.neutralTertiary}; + } `; export default VerificationRequestDisplayStyle; diff --git a/src/components/VerificationRequest/VerificationRequestFilters.tsx b/src/components/VerificationRequest/VerificationRequestFilters.tsx index 3d711ae83..0621b3299 100644 --- a/src/components/VerificationRequest/VerificationRequestFilters.tsx +++ b/src/components/VerificationRequest/VerificationRequestFilters.tsx @@ -5,12 +5,13 @@ import { useTranslation } from "next-i18next"; import TopicsApi from "../../api/topicsApi"; import verificationRequestApi from "../../api/verificationRequestApi"; import debounce from "lodash.debounce"; -import { FiltersContext } from "../../types/VerificationRequest"; +import { FiltersContext, ViewMode } from "../../types/VerificationRequest"; export const useVerificationRequestFilters = (): FiltersContext => { const { t } = useTranslation(); const dispatch = useDispatch(); + const [viewMode, setViewMode] = useState("board"); const [priorityFilter, setPriorityFilter] = useState("all"); const [sourceChannelFilter, setSourceChannelFilter] = useState("all"); const [filterValue, setFilterValue] = useState([]); @@ -41,6 +42,8 @@ export const useVerificationRequestFilters = (): FiltersContext => { Posted: false, }); + const [startDate, setStartDate] = useState(null); + const [endDate, setEndDate] = useState(null); const { autoCompleteTopicsResults, topicFilterUsed, impactAreaFilterUsed } = useAppSelector((state) => ({ autoCompleteTopicsResults: state?.search?.autocompleteTopicsResults || [], @@ -60,6 +63,8 @@ export const useVerificationRequestFilters = (): FiltersContext => { sourceChannel: sourceChannelFilter, impactArea: impactAreaFilterUsed, status: status, + startDate: startDate?.toISOString(), + endDate: endDate?.toISOString(), }); if (response) { @@ -77,13 +82,17 @@ export const useVerificationRequestFilters = (): FiltersContext => { } finally { setLoading((prev) => ({ ...prev, [status]: false })); } - }, [ + }, + [ paginationModel, topicFilterUsed, priorityFilter, sourceChannelFilter, impactAreaFilterUsed, - ]); + startDate, + endDate, + ] + ); useEffect(() => { if (isInitialLoad || applyFilters) { @@ -134,6 +143,9 @@ export const useVerificationRequestFilters = (): FiltersContext => { impactAreaFilterUsed, applyFilters, isInitialLoad, + viewMode, + startDate, + endDate, }, actions: { setPriorityFilter, @@ -149,6 +161,9 @@ export const useVerificationRequestFilters = (): FiltersContext => { fetchData, dispatch, t, + setViewMode, + setStartDate, + setEndDate, }, }; }; diff --git a/src/components/VerificationRequest/VerificationRequestMainContent.tsx b/src/components/VerificationRequest/VerificationRequestMainContent.tsx index a6a4df447..d5ca4a0b8 100644 --- a/src/components/VerificationRequest/VerificationRequestMainContent.tsx +++ b/src/components/VerificationRequest/VerificationRequestMainContent.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, IconButton } from "@mui/material"; +import { Box, Typography, IconButton, Grid } from "@mui/material"; import React, { useState } from "react"; import { useTranslation } from "next-i18next"; import VerificationRequestCard from "./VerificationRequestCard"; @@ -6,9 +6,10 @@ import { useAppSelector } from "../../store/store"; import ManageVerificationRequestGroup from "./ManageVerificationRequestGroup"; import { useAtom } from "jotai"; import { currentUserRole } from "../../atoms/currentUser"; -import { Roles } from "../../types/enums"; import EditIcon from '@mui/icons-material/Edit'; -import EditVerificationRequestDrawer from "./EditVerificationRequestDrawer"; +import EditVerificationRequestDrawer from "./verificationRequestForms/EditVerificationRequestDrawer"; +import TrackingCard from "../Tracking/TrackingCard"; +import { isAdmin, isStaff } from "../../utils/GetUserPermission"; const VerificationRequestMainContent = ({ verificationRequestGroup, @@ -28,34 +29,43 @@ const VerificationRequestMainContent = ({ }; return ( -
- - +
+ + {t("verificationRequest:verificationRequestTitle")} - {role == Roles.Admin && ( + {isAdmin(role) && ( setOpenEditDrawer(true)} /> )} + {openEditDrawer && ( + setOpenEditDrawer(false)} + verificationRequest={currentRequest} + onSave={handleSave} + /> + )} - - {openEditDrawer && ( - setOpenEditDrawer(false)} - verificationRequest={currentRequest} - onSave={handleSave} - /> - )} - - + + + + + + + + {!vw.xs && - role !== Roles.Regular && + isStaff(role) && verificationRequestGroup?.length > 0 && ( { return ( <> {recommendations?.length > 0 && ( -
- +
+ {t("verificationRequest:recommendationTitle")} diff --git a/src/components/VerificationRequest/VerificationRequestResultList.tsx b/src/components/VerificationRequest/VerificationRequestResultList.tsx index 541c25dba..75b4ef0ec 100644 --- a/src/components/VerificationRequest/VerificationRequestResultList.tsx +++ b/src/components/VerificationRequest/VerificationRequestResultList.tsx @@ -4,7 +4,6 @@ import VerificationRequestCard from "./VerificationRequestCard"; import AletheiaButton from "../Button"; import { VerificationRequestContext } from "./VerificationRequestProvider"; import { useTranslation } from "next-i18next"; -import VerificationRequestResultListStyled from "./VerificationRequestRecommedations.style"; const VerificationRequestResultList = ({ results }) => { const { t } = useTranslation(); @@ -24,46 +23,47 @@ const VerificationRequestResultList = ({ results }) => { }; return ( - + {results?.length > 0 && results.map((verificationRequest) => ( - + { - setIsLoading(true); - await addRecommendation( - verificationRequest - ); - setIsLoading(false); - }} > - {checkIfIsInGroup(verificationRequest._id) - ? t( - "verificationRequest:alreadyInGroupMessage" - ) - : t( - "verificationRequest:addInGroupButton" - )} - , + { + setIsLoading(true); + await addRecommendation( + verificationRequest + ); + setIsLoading(false); + }} + > + {checkIfIsInGroup(verificationRequest._id) + ? t( + "verificationRequest:alreadyInGroupMessage" + ) + : t( + "verificationRequest:addInGroupButton" + )} + + ]} /> ))} - + ); }; diff --git a/src/components/VerificationRequest/VerificationRequestSearch.tsx b/src/components/VerificationRequest/VerificationRequestSearch.tsx index 6868afa08..db36e0e5d 100644 --- a/src/components/VerificationRequest/VerificationRequestSearch.tsx +++ b/src/components/VerificationRequest/VerificationRequestSearch.tsx @@ -38,16 +38,15 @@ const VerificationRequestSearch = () => { }; return ( -
-
- +
+ @@ -63,11 +62,10 @@ const VerificationRequestSearch = () => { } }} /> - -
+ {verificationRequests && ( - - + + {t("verificationRequest:searchResultsTitle")} {isLoading && } @@ -80,7 +78,7 @@ const VerificationRequestSearch = () => { )} )} -
+
); }; diff --git a/src/components/VerificationRequest/VerificationRequestList.tsx b/src/components/VerificationRequest/VerificationRequestView.tsx similarity index 54% rename from src/components/VerificationRequest/VerificationRequestList.tsx rename to src/components/VerificationRequest/VerificationRequestView.tsx index 683c0c75d..3c9a20624 100644 --- a/src/components/VerificationRequest/VerificationRequestList.tsx +++ b/src/components/VerificationRequest/VerificationRequestView.tsx @@ -4,16 +4,22 @@ import FilterManager from "./FilterManagers"; import ActiveFilters from "./ActiveFilters"; import VerificationRequestBoardView from "./VerificationRequestBoardView"; import { useVerificationRequestFilters } from "./VerificationRequestFilters"; +import VerificationRequestDashboard from "./Dashboard/VerificationRequestDashboard"; -const VerificationRequestList = () => { +const VerificationRequestView = () => { const { state, actions } = useVerificationRequestFilters(); + const { viewMode } = state; + return ( - + - + {viewMode === "board" && ( + + )} + {viewMode === "dashboard" && } ); }; -export default VerificationRequestList; +export default VerificationRequestView; diff --git a/src/components/VerificationRequest/CreateVerificationRequest/DynamicVerificationRequestForm.tsx b/src/components/VerificationRequest/verificationRequestForms/CreateVerificationRequestView.tsx similarity index 55% rename from src/components/VerificationRequest/CreateVerificationRequest/DynamicVerificationRequestForm.tsx rename to src/components/VerificationRequest/verificationRequestForms/CreateVerificationRequestView.tsx index 148a4ca84..19eb45c8c 100644 --- a/src/components/VerificationRequest/CreateVerificationRequest/DynamicVerificationRequestForm.tsx +++ b/src/components/VerificationRequest/verificationRequestForms/CreateVerificationRequestView.tsx @@ -1,23 +1,14 @@ import React, { useState } from "react"; -import { useForm } from "react-hook-form"; +import { Grid } from "@mui/material"; +import colors from "../../../styles/colors"; +import DynamicVerificationRequestForm from "./DynamicVerificationRequestForm"; import { useTranslation } from "next-i18next"; import { useRouter } from "next/router"; import { useAtom } from "jotai"; import { currentNameSpace } from "../../../atoms/namespace"; -import createVerificationRequestForm from "./fieldLists/CreateVerificationRequestForm"; import verificationRequestApi from "../../../api/verificationRequestApi"; -import moment from "moment"; -import DynamicForm from "../../Form/DynamicForm"; -import SharedFormFooter from "../../SharedFormFooter"; -const DynamicVerificationRequestForm = () => { - const { - handleSubmit, - control, - formState: { errors }, - } = useForm(); - const disabledDate = (current) => - current && current > moment().endOf("day"); +const CreateVerificationRequestView = () => { const router = useRouter(); const { t } = useTranslation(); const [nameSpace] = useAtom(currentNameSpace); @@ -49,24 +40,18 @@ const DynamicVerificationRequestForm = () => { }; return ( -
- - - - + + + + + ); }; -export default DynamicVerificationRequestForm; +export default CreateVerificationRequestView; diff --git a/src/components/VerificationRequest/verificationRequestForms/DynamicVerificationRequestForm.tsx b/src/components/VerificationRequest/verificationRequestForms/DynamicVerificationRequestForm.tsx new file mode 100644 index 000000000..0d53c759e --- /dev/null +++ b/src/components/VerificationRequest/verificationRequestForms/DynamicVerificationRequestForm.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import createVerificationRequestForm from "./fieldLists/CreateVerificationRequestForm"; +import moment from "moment"; +import DynamicForm from "../../Form/DynamicForm"; +import SharedFormFooter from "../../SharedFormFooter"; +import editVerificationRequestForm from "./fieldLists/EditVerificationRequestForm"; +import { IDynamicVerificationRequestForm } from "../../../types/VerificationRequest"; + +const DynamicVerificationRequestForm = ({ + data, + onSubmit, + isLoading, + setRecaptchaString, + hasCaptcha, + isEdit, + isDrawerOpen, + onClose +}: IDynamicVerificationRequestForm) => { + const { + handleSubmit, + control, + formState: { errors }, + } = useForm(); + const disabledDate = (current) => + current && current > moment().endOf("day"); + + return ( +
+ + + + + ); +}; + +export default DynamicVerificationRequestForm; diff --git a/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx b/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx new file mode 100644 index 000000000..a66b3dcfb --- /dev/null +++ b/src/components/VerificationRequest/verificationRequestForms/EditVerificationRequestDrawer.tsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; +import { IEditVerificationRequestDrawer } from "../../../types/VerificationRequest"; +import { useTranslation } from "react-i18next"; +import { Divider, Grid } from "@mui/material"; +import verificationRequestApi from "../../../api/verificationRequestApi"; +import LargeDrawer from "../../LargeDrawer"; +import DynamicVerificationRequestForm from "./DynamicVerificationRequestForm"; + +const EditVerificationRequestDrawer = ({ + open, + onClose, + verificationRequest, + onSave, +}: IEditVerificationRequestDrawer) => { + const { t } = useTranslation(); + const [recaptchaString, setRecaptchaString] = useState(""); + const hasCaptcha = !!recaptchaString; + const [isLoading, setIsLoading] = useState(false); + const sourceMapped = verificationRequest.source?.map(source => source.href); + + const updatedVerificationRequest = { + ...verificationRequest, + source: sourceMapped?.length > 0 ? sourceMapped : undefined + } + + const onSubmit = async (data) => { + try { + const updateData = { + publicationDate: data.publicationDate, + source: data.source?.map(url => ({ href: url })), + }; + + const response = await verificationRequestApi.updateVerificationRequest( + verificationRequest._id, + updateData, + t, + 'update' + ); + + if (response) { + onSave(response); + onClose(); + } + } catch (error) { + console.error("Edit Verification Request error", error) + } finally { + setIsLoading(false); + } + }; + + return ( + + +

+ {t("verificationRequest:titleEditVerificationRequest")} +

+ + +
+
+ ) +}; + +export default EditVerificationRequestDrawer; diff --git a/src/components/VerificationRequest/CreateVerificationRequest/fieldLists/CreateVerificationRequestForm.ts b/src/components/VerificationRequest/verificationRequestForms/fieldLists/CreateVerificationRequestForm.ts similarity index 100% rename from src/components/VerificationRequest/CreateVerificationRequest/fieldLists/CreateVerificationRequestForm.ts rename to src/components/VerificationRequest/verificationRequestForms/fieldLists/CreateVerificationRequestForm.ts diff --git a/src/components/VerificationRequest/verificationRequestForms/fieldLists/EditVerificationRequestForm.ts b/src/components/VerificationRequest/verificationRequestForms/fieldLists/EditVerificationRequestForm.ts new file mode 100644 index 000000000..339eea04b --- /dev/null +++ b/src/components/VerificationRequest/verificationRequestForms/fieldLists/EditVerificationRequestForm.ts @@ -0,0 +1,48 @@ +import { createFormField, FormField } from "../../../Form/FormField"; + +const editVerificationRequestForm: FormField[] = [ + createFormField({ + fieldName: "content", + type: "text", + defaultValue: "", + i18nNamespace: "verificationRequest", + disabled: true, + }), + createFormField({ + fieldName: "reportType", + type: "selectReportType", + defaultValue: "", + i18nNamespace: "verificationRequest", + disabled: true, + }), + createFormField({ + fieldName: "impactArea", + type: "selectImpactArea", + defaultValue: "", + i18nNamespace: "verificationRequest", + disabled: true, + }), + createFormField({ + fieldName: "heardFrom", + type: "text", + defaultValue: "", + i18nNamespace: "verificationRequest", + disabled: true, + }), + createFormField({ + fieldName: "publicationDate", + type: "date", + defaultValue: "", + i18nNamespace: "verificationRequest", + }), + createFormField({ + fieldName: "source", + type: "sourceList", + defaultValue: "", + i18nNamespace: "verificationRequest", + required: false, + isURLField: true, + }), +]; + +export default editVerificationRequestForm; diff --git a/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx b/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx new file mode 100644 index 000000000..cfd5a2a07 --- /dev/null +++ b/src/components/VerificationRequest/verificationRequestForms/formInputs/ImpactAreaSelect.tsx @@ -0,0 +1,34 @@ +import React, { useState, useEffect } from "react"; +import { IImpactAreaSelect, ManualTopic } from "../../../../types/Topic"; +import MultiSelectAutocomplete from "../../../topics/TopicOrImpactSelect"; + +const ImpactAreaSelect = ({ + defaultValue, + onChange, + placeholder, + isDisabled, + dataCy, +}: IImpactAreaSelect) => { + const [value, setValue] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + onChange(value); + }, []); + + return ( + + ); +}; + +export default ImpactAreaSelect; diff --git a/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx b/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx new file mode 100644 index 000000000..5e6e844cd --- /dev/null +++ b/src/components/VerificationRequest/verificationRequestForms/formInputs/InputExtraSourcesList.tsx @@ -0,0 +1,118 @@ +import React, { useCallback, useMemo, useState } from "react"; +import { Grid } from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useTranslation } from "next-i18next"; +import AletheiaButton, { ButtonType } from "../../../Button"; +import AletheiaInput from "../../../AletheiaInput"; +import { IInputExtraSourcesList } from "../../../../types/VerificationRequest"; +import { SourceType } from "../../../../types/Source"; +import { debounce } from "lodash"; + +const formatSources = (sources: SourceType[]) => { + const sourceArray = Array.isArray(sources) ? sources : []; + if (sourceArray.length === 0) return [createEmptySource()]; + + return sourceArray.map((source) => ({ + id: Math.random().toString(), + href: typeof source === "object" ? source.href : source || "", + isNewSource: !!(typeof source === "object" ? source.href : source), + })); +}; + +const createEmptySource = () => ({ + id: Math.random().toString(), + href: "", + isNewSource: false +}); + +const InputExtraSourcesList = ({ defaultSources, onChange, disabled, placeholder, dataCy }: IInputExtraSourcesList) => { + const { t } = useTranslation(); + const [sourcesList, setSourcesList] = useState(() => formatSources(defaultSources as SourceType[])); + + const handleListChange = useCallback((newSourcesList: typeof sourcesList) => { + const cleanedSources = [...new Set(newSourcesList.map(source => source.href.trim()).filter(Boolean))]; + onChange(cleanedSources); + }, [onChange]); + + const debouncedOnChange = useMemo( + () => + debounce((SourcesList: typeof sourcesList) => { + handleListChange(SourcesList) + }, 800), + [handleListChange] + ); + + const updateSources = (id: string, newHref: string) => { + const newSourcesList = sourcesList.map(source => source.id === id ? { ...source, href: newHref } : source); + setSourcesList(newSourcesList); + debouncedOnChange(newSourcesList) + }; + + const addField = () => { + if (disabled) return; + const newSourcesList = [...sourcesList, createEmptySource()]; + setSourcesList(newSourcesList); + }; + + const removeField = (id: string, index: number) => { + if (disabled || index === 0) return; + const newSourcesList = sourcesList.filter((source) => source.id !== id); + + setSourcesList(newSourcesList); + debouncedOnChange.cancel(); + + handleListChange(newSourcesList) + }; + + return ( + + {sourcesList.map((source, index) => ( + + updateSources(source.id, newHref.target.value)} + placeholder={t(placeholder)} + data-cy={`${dataCy}Edit-${index}`} + white="true" + /> + + {!disabled && index !== 0 && ( + removeField(source.id, index)} + data-cy={`${dataCy}Remove-${index}`} + style={{ minWidth: "40px" }} + > + + + )} + + ))} + + {!disabled && ( + + + + {t("sourceForm:addNewSourceButton")} + + + )} + + ); +}; + +export default InputExtraSourcesList; diff --git a/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx b/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx new file mode 100644 index 000000000..30110105a --- /dev/null +++ b/src/components/VerificationRequest/verificationRequestForms/formInputs/ReportTypeSelect.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { MenuItem, FormControl } from "@mui/material"; +import { ContentModelEnum } from "../../../../types/enums"; +import { SelectInput } from "../../../Form/ClaimReviewSelect"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { IReportTypeSelect } from "../../../../types/VerificationRequest"; + +const ReportTypeSelect = ({ + onChange, + defaultValue, + placeholder, + style = {}, + isDisabled, + dataCy, +}: IReportTypeSelect) => { + const [value, setValue] = useState(defaultValue || ""); + const { t } = useTranslation(); + + const onChangeSelect = (e) => { + setValue(e.target.value); + }; + + useEffect(() => { + onChange(value || undefined); + }, [value, onChange]); + + return ( + + + + {placeholder} + + {Object.values(ContentModelEnum).map((option) => ( + + {t(`claimForm:${option}`)} + + ))} + + + ); +}; + +export default ReportTypeSelect; diff --git a/src/components/adminArea/Drawer/HeaderUserStatus.tsx b/src/components/adminArea/Drawer/HeaderUserStatus.tsx index ee2646d11..46f161e0d 100644 --- a/src/components/adminArea/Drawer/HeaderUserStatus.tsx +++ b/src/components/adminArea/Drawer/HeaderUserStatus.tsx @@ -18,7 +18,7 @@ const HeaderStatusStyle = styled(Grid)` } `; -const HeaderUserStatus = ({ status, style }) => { +const HeaderUserStatus = ({ status, style = {} }) => { const { t } = useTranslation(); const statusColor = status === Status.Active ? colors.active : colors.inactive; diff --git a/src/components/adminArea/Drawer/UserEditForm.tsx b/src/components/adminArea/Drawer/UserEditForm.tsx index 93c3fca2f..b055899cf 100644 --- a/src/components/adminArea/Drawer/UserEditForm.tsx +++ b/src/components/adminArea/Drawer/UserEditForm.tsx @@ -15,6 +15,7 @@ import { canEdit } from "../../../utils/GetUserPermission"; import UserEditRoles from "./UserEditRoles"; import { NameSpace, NameSpaceEnum } from "../../../types/Namespace"; import NameSpacesApi from "../../../api/namespace"; +import { currentNameSpace } from "../../../atoms/namespace"; const UserEditForm = ({ currentUser, setIsLoading }) => { const { t } = useTranslation(); @@ -25,6 +26,7 @@ const UserEditForm = ({ currentUser, setIsLoading }) => { const [userId] = useAtom(currentUserId); const [options, setOptions] = useState([]); const [selectedNamespaces, setSelectedNamespaces] = useState([]); + const [nameSpace] = useAtom(currentNameSpace); const handleChangeBadges = (_event, newValue: Badge[]) => { setBadges(newValue); @@ -86,32 +88,29 @@ const UserEditForm = ({ currentUser, setIsLoading }) => { updatedRole[slug] ??= Roles.Regular; }); - for (const ns of selectedNamespaces) { - const { id, __v, ...rest } = ns as any; - const userAlreadyExist = ns.users.some(user => user._id === currentUser._id); + for (const namespace of selectedNamespaces) { + const userAlreadyExist = namespace.users.some(user => user._id === currentUser._id); const updatedNamespaces = { - ...rest, + _id: namespace._id, users: userAlreadyExist - ? ns.users - : [...ns.users, currentUser], + ? namespace.users + : [...namespace.users, currentUser], }; - if (!currentIds.includes(ns._id)) { + if (!currentIds.includes(namespace._id)) { await NameSpacesApi.updateNameSpace(updatedNamespaces, t); } } - for (const ns of currentNamespacesUser) { - if (!selectedIds.includes(ns._id)) { - const updatedUser = ns.users + for (const namespace of currentNamespacesUser) { + if (!selectedIds.includes(namespace._id)) { + const updatedUser = namespace.users .filter(user => String(user._id || user) !== String(currentUser._id)) .map(user => (typeof user === 'string' ? { _id: user } : user)); - const { id, __v, ...rest } = ns; - const updatedNamespaces = { - ...rest, + _id: namespace._id, users: updatedUser, }; await NameSpacesApi.updateNameSpace(updatedNamespaces, t); @@ -146,27 +145,32 @@ const UserEditForm = ({ currentUser, setIsLoading }) => { shouldEdit={shouldEdit} />
- - - option.name} - value={selectedNamespaces} - onChange={handleChangeNameSpaces} - disableCloseOnSelect - renderInput={(params) => ( - - )} - renderOption={(props, option) => ( - - {option.name} - - )} - /> - + {nameSpace === NameSpaceEnum.Main ? + + + option.name} + value={selectedNamespaces} + onChange={handleChangeNameSpaces} + disableCloseOnSelect + renderInput={(params) => ( + + )} + renderOption={(props, option) => ( + + {option.name} + + )} + /> + + : null + } { const { t } = useTranslation(); - const [open, setOpen] = useAtom(isEditDrawerOpen); const [badgeEdited] = useAtom(badgeBeeingEdited); + const [userList] = useAtom(atomUserList); + + const updatedBadges = { + ...badgeEdited, + usersId: badgeEdited?.users?.map(badges => badges._id) || [], + imageField: badgeEdited?.image?.content ? [{ + uid: badgeEdited.image._id, + name: badgeEdited.name || "Existing Badge Image", + status: "done", + url: badgeEdited.image.content, + }] : [] + } + const [open, setOpen] = useAtom(isEditDrawerOpen); const addBadge = useSetAtom(addBadgeToList); const finishEditing = useSetAtom(finishEditingItem); const cancelEditing = useSetAtom(cancelEditingItem); - const [userList] = useAtom(atomUserList); - const [userId] = useAtom(currentUserId); - const userListFiltered = userList.filter((u) => canEdit(u, userId)); const isEdit = !!badgeEdited; - - let initialFileList: UploadFile[] = []; - if (badgeEdited?.image) { - initialFileList = [ - { - uid: badgeEdited.image._id, - name: badgeEdited.name || "Existing Badge Image", - status: "done", - url: badgeEdited.image.content, - }, - ]; - } - - const { - register, - handleSubmit, - formState: { errors }, - control, - reset, - } = useForm<{ - name: string; - description: string; - image: UploadFile[]; - users: string[]; - }>({ - defaultValues: { - name: badgeEdited?.name || "", - description: badgeEdited?.description || "", - image: initialFileList, - users: badgeEdited?.users?.map((u) => u._id) || [], - }, - }); - - const [users, setUsers] = useState([]); - const [imageError, setImageError] = useState(false); const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - if (badgeEdited) { - const userIds = badgeEdited.users?.map((u) => u._id) || []; - const found = userListFiltered.filter((u) => - userIds.includes(u._id) - ); - setUsers(found); - - reset({ - name: badgeEdited.name, - description: badgeEdited.description, - image: initialFileList, - users: userIds, - }); - } - }, [badgeEdited]); + const onSubmit = async (data: IBadgeData) => { + const { name, description, imageField, usersId } = data; - const onCloseDrawer = () => { - resetForm(); - cancelEditing(); - }; - - const resetForm = () => { - reset({ - name: "", - description: "", - image: [], - users: [], - }); - setUsers([]); - setImageError(false); - setIsLoading(false); - }; + const usersAsObjects = userList.filter(user => + usersId?.includes(user._id) + ); - const onSubmit = async (data: { - name: string; - description: string; - image: UploadFile[]; - users: string[]; - }) => { - const { name, description, image } = data; - if (image.length === 0 && !badgeEdited?.image) { - setImageError(true); - return; - } setIsLoading(true); - const newFiles = image + const newFiles = imageField .filter((f) => f.originFileObj) .map((f) => f.originFileObj); @@ -140,24 +63,19 @@ const BadgesFormDrawer = () => { }); try { const imagesUploaded = await ImageApi.uploadImage(formData, t); - setImageError(false); const newImage = imagesUploaded[0]; - handleBadgeSave({ name, description, image: newImage }); + handleBadgeSave({ name, description, image: newImage, users: usersAsObjects }); } catch (err) { setIsLoading(false); console.error("Error uploading images:", err); } } else { - handleBadgeSave({ name, description, image: badgeEdited?.image }); + handleBadgeSave({ name, description, image: badgeEdited?.image, users: usersAsObjects }); } }; - const handleBadgeSave = (props: { - name: string; - description: string; - image: any; - }) => { - const { name, description, image } = props; + const handleBadgeSave = (props: IBadgeProps) => { + const { name, description, image, users } = props; if (isEdit && badgeEdited) { const newItem = { _id: badgeEdited._id, name, description, image }; BadgesApi.updateBadge(newItem, users, t).then(() => { @@ -166,7 +84,6 @@ const BadgesFormDrawer = () => { listAtom: atomBadgesList, closeDrawer: true, }); - resetForm(); }); } else { const newBadge = { @@ -178,7 +95,6 @@ const BadgesFormDrawer = () => { BadgesApi.createBadge(newBadge, users, t) .then((createdBadge) => { addBadge(createdBadge); - resetForm(); setOpen(false); }) .catch(() => { @@ -187,107 +103,32 @@ const BadgesFormDrawer = () => { } }; + const onCloseDrawer = () => { + setOpen(false); + cancelEditing(); + }; + return ( - +

{t(isEdit ? "badges:editBadge" : "badges:addBadge")}

- -
- - - - {errors.name && ( - - {t("common:requiredFieldError")} - - )} - - - - - {errors.description && ( - - {t("common:requiredFieldError")} - - )} - - - - ( - { - field.onChange(uploadFiles); - if (uploadFiles.length > 0) { - setImageError(false); - } - }} - error={!!errors.image || imageError} - defaultFileList={field.value} - /> - )} - /> - {imageError && ( - - {t("common:requiredFieldError")} - - )} - - - option.name} - disableCloseOnSelect - limitTags={3} - value={users} - onChange={(_e, newValue) => setUsers(newValue)} - renderInput={(params) => ( - - )} - /> - - -
-
+
); diff --git a/src/components/badges/BadgesView.tsx b/src/components/badges/BadgesView.tsx index 3a8545a21..7c5b32e06 100644 --- a/src/components/badges/BadgesView.tsx +++ b/src/components/badges/BadgesView.tsx @@ -35,7 +35,7 @@ const BadgesView = () => { () => [ { field: "image", - headerName: t("badges:imageColumn"), + headerName: t("badges:imageFieldLabel"), flex: 1, renderCell: (params) => ( { }, { field: "name", - headerName: t("badges:nameColumn"), + headerName: t("badges:nameLabel"), flex: 2, }, { field: "description", - headerName: t("badges:descriptionColumn"), + headerName: t("badges:descriptionLabel"), flex: 4, }, { diff --git a/src/components/badges/DynamicBadgesForm.tsx b/src/components/badges/DynamicBadgesForm.tsx new file mode 100644 index 000000000..1a22d08c5 --- /dev/null +++ b/src/components/badges/DynamicBadgesForm.tsx @@ -0,0 +1,50 @@ +import moment from "moment"; +import { useForm } from "react-hook-form"; +import DynamicForm from "../Form/DynamicForm"; +import SharedFormFooter from "../SharedFormFooter"; +import { useState } from "react"; +import { Grid } from "@mui/material"; +import { IDynamicBadgesForm } from "../../types/Badge"; +import lifecycleBadgesForm from "./BadgesForm"; + +const DynamicBadgesForm = ({ + badges, + onSubmit, + isLoading, + isDrawerOpen, + onClose +}: IDynamicBadgesForm) => { + const { + handleSubmit, + control, + formState: { errors }, + } = useForm(); + const disabledDate = (current) => + current && current > moment().endOf("day"); + const [recaptchaString, setRecaptchaString] = useState(""); + const hasCaptcha = !!recaptchaString; + + return ( + +
+ + + + +
+ ) +} + +export default DynamicBadgesForm diff --git a/src/components/history/HistoryListItem.tsx b/src/components/history/HistoryListItem.tsx index 31025f233..8bb839ae9 100644 --- a/src/components/history/HistoryListItem.tsx +++ b/src/components/history/HistoryListItem.tsx @@ -1,42 +1,54 @@ -import Typography from "@mui/material/Typography" -import { useTranslation } from "next-i18next" -import React from "react" -import LocalizedDate from "../LocalizedDate" -interface IHistoryListItemProps { - history: { - _id: string, - targetId: string, - targetModel: string, - user: { name: string }, - type: string, - date: string - details: { after: any, before: any } - } -} - -const HistoryListItem = ({ history }: IHistoryListItemProps) => { - const { t } = useTranslation() - const username = history.user ? history.user.name : t("history:anonymousUserName") - const type = t(`history:${history.type}`) - const targetModel = t(`history:${history.targetModel}`) - const titleTag = history.targetModel === "personality" ? "name" : "title" - - let title = history.details.after?.[titleTag] ? history.details.after?.[titleTag] : '' - const oldTitle = history.details.before?.[titleTag] ? history.details.before?.[titleTag] : '' - if (history.type === "delete") { - title = oldTitle +import React, { useMemo } from "react"; +import { useTranslation } from "next-i18next"; +import LocalizedDate from "../LocalizedDate"; +import { TFunction } from "i18next"; +import { HistoryListItemProps, PerformedBy } from "../../types/History"; +import { isM2M, isUser } from "../../utils/TypeGuards"; +import { M2MSubject } from "../../types/enums"; + +const getDisplayName = (user: PerformedBy, t: TFunction): string => { + if (isM2M(user) && user.subject === M2MSubject.Chatbot) { + return t("virtualAssistant"); + } + if (isM2M(user)) { + return t("automatedMonitoring"); + } + if (isUser(user)) { + return user.name; + } + return t("anonymousUserName"); +}; + +const HistoryListItem: React.FC = ({ history }) => { + const { t } = useTranslation("history"); + + const currentHistory = useMemo(() => { + try { + const { user, type, targetModel, details, date } = history; + + const username = getDisplayName(user, t); + + const titleField = targetModel === "Personality" ? "name" : "title"; + const data = type === "delete" ? details?.before : details?.after; + const displayTitle = data?.[titleField]; + + return { username, type, targetModel, displayTitle, date }; + } catch (err) { + console.error("Mapping error:", err); + return null; } - return ( -
- {` - `} - {t('history:historyItem', { username, type, targetModel, title })} - {oldTitle && oldTitle !== title && ( - - ({oldTitle}) - - )} -
- ) -} - -export default HistoryListItem + }, [history, t]); + + if (!currentHistory) return null; + + return ( +
+ + {` - `} + {currentHistory.username} {t(currentHistory.type)} {t(currentHistory.targetModel)}{" "} + {currentHistory.displayTitle && `"${currentHistory.displayTitle}"`} +
+ ); +}; + +export default HistoryListItem; diff --git a/src/components/namespace/DynamicNameSpaceForm.tsx b/src/components/namespace/DynamicNameSpaceForm.tsx new file mode 100644 index 000000000..c576cf3da --- /dev/null +++ b/src/components/namespace/DynamicNameSpaceForm.tsx @@ -0,0 +1,78 @@ +import moment from "moment"; +import { useForm } from "react-hook-form"; +import { IDynamicNameSpaceForm } from "../../types/Namespace"; +import lifecycleNameSpaceForm from "./NameSpaceForm"; +import DynamicForm from "../Form/DynamicForm"; +import SharedFormFooter from "../SharedFormFooter"; +import { useState } from "react"; +import { Grid } from "@mui/material"; +import Button from "../Button"; +import DailyReportApi from "../../api/dailyReport"; + +const DynamicNameSpaceForm = ({ + nameSpace, + onSubmit, + isLoading, + setIsLoading, + isDrawerOpen, + onClose, + t, +}: IDynamicNameSpaceForm) => { + const { + handleSubmit, + control, + formState: { errors }, + } = useForm(); + const disabledDate = (current) => + current && current > moment().endOf("day"); + const [recaptchaString, setRecaptchaString] = useState(""); + const hasCaptcha = !!recaptchaString; + + const handleDailyReviews = async () => { + setIsLoading(true); + try { + await DailyReportApi.sendDailyReportEmail( + nameSpace._id, + nameSpace.slug, + t + ); + } catch (err) { + console.error(err); + } finally { + setIsLoading(false); + } + }; + + return ( + +
+ + + + {t("notification:dailyReportButton") as string} + + )} + /> + +
+ ) +} + +export default DynamicNameSpaceForm diff --git a/src/components/namespace/NameSpaceForm.ts b/src/components/namespace/NameSpaceForm.ts new file mode 100644 index 000000000..58fff2f03 --- /dev/null +++ b/src/components/namespace/NameSpaceForm.ts @@ -0,0 +1,21 @@ +import { fetchUserList } from "../ClaimReview/form/fieldLists/unassignedForm"; +import { createFormField, FormField } from "../Form/FormField"; + +const lifecycleNameSpaceForm: FormField[] = [ + createFormField({ + fieldName: "name", + type: "text", + defaultValue: "", + i18nNamespace: "namespaces", + + }), + createFormField({ + fieldName: "usersId", + type: "inputSearch", + defaultValue: "", + i18nKey: "assignUser", + extraProps: { dataLoader: fetchUserList }, + }), +]; + +export default lifecycleNameSpaceForm; diff --git a/src/components/namespace/NameSpaceFormDrawer.tsx b/src/components/namespace/NameSpaceFormDrawer.tsx index b6958f902..735ab6978 100644 --- a/src/components/namespace/NameSpaceFormDrawer.tsx +++ b/src/components/namespace/NameSpaceFormDrawer.tsx @@ -1,117 +1,58 @@ -import { - Autocomplete, - Divider, - Grid, - TextField, - Typography, -} from "@mui/material"; +import { Divider, Grid } from "@mui/material"; import { useAtom, useSetAtom } from "jotai"; import { useTranslation } from "next-i18next"; -import React, { useEffect, useState } from "react"; - +import React, { useState } from "react"; import { finishEditingItem, isEditDrawerOpen, cancelEditingItem, } from "../../atoms/editDrawer"; import colors from "../../styles/colors"; -import AletheiaInput from "../AletheiaInput"; -import Button from "../Button"; -import Label from "../Label"; import LargeDrawer from "../LargeDrawer"; -import { useForm } from "react-hook-form"; -import { atomUserList } from "../../atoms/userEdit"; -import { User } from "../../types/User"; -import { currentUserId } from "../../atoms/currentUser"; -import { canEdit } from "../../utils/GetUserPermission"; import { nameSpaceBeeingEdited, addNameSpaceToList, atomNameSpacesList, } from "../../atoms/namespace"; import NameSpacesApi from "../../api/namespace"; -import DailyReportApi from "../../api/dailyReport"; +import DynamicNameSpaceForm from "./DynamicNameSpaceForm"; +import { atomUserList } from "../../atoms/userEdit"; const NameSpacesFormDrawer = () => { + const { t } = useTranslation(); const [nameSpace] = useAtom(nameSpaceBeeingEdited); const [userList] = useAtom(atomUserList); - const { - register, - handleSubmit, - formState: { errors }, - reset, - } = useForm({ - defaultValues: { - name: nameSpace?.name, - users: nameSpace?.users?.map((user) => user?._id), - }, - }); + const updatedNamespace = { + ...nameSpace, + usersId: nameSpace?.users?.map(namespace => namespace._id) || [] + } - const { t } = useTranslation(); const [open, setOpen] = useAtom(isEditDrawerOpen); const addNameSpace = useSetAtom(addNameSpaceToList); const finishEditing = useSetAtom(finishEditingItem); const cancelEditing = useSetAtom(cancelEditingItem); - const [userId] = useAtom(currentUserId); const isEdit = !!nameSpace; const [isLoading, setIsLoading] = useState(false); - const [users, setUsers] = useState([]); - - const userListFiltered = userList.filter((user) => { - return canEdit(user, userId); - }); - useEffect(() => { - const userIds = nameSpace?.users?.map((user) => user._id); - if (nameSpace) { - setUsers( - nameSpace?.users?.length - ? userListFiltered.filter((user) => - userIds.includes(user._id) - ) - : [] - ); - - reset({ - name: nameSpace?.name, - }); - } - }, [nameSpace]); - - const resetForm = () => { - reset({ - name: "", - }); - setUsers([]); - setIsLoading(false); - }; const onSubmit = async (data) => { setIsLoading(true); + const usersAsObjects = userList.filter(user => + data.usersId?.includes(user._id) + ); + + const updatedData = { + ...data, + users: usersAsObjects + }; try { if (isEdit) { - await handleEditNameSpace(data); + await handleEditNameSpace(updatedData); } else { - await handleCreateNameSpace(data); + await handleCreateNameSpace(updatedData); } setOpen(false); - resetForm(); - } catch (err) { - console.error(err); - } finally { - setIsLoading(false); - } - }; - - const handleDailyReviews = async () => { - setIsLoading(true); - try { - await DailyReportApi.sendDailyReportEmail( - nameSpace._id, - nameSpace.slug, - t - ); } catch (err) { console.error(err); } finally { @@ -123,7 +64,7 @@ const NameSpacesFormDrawer = () => { const newItem = { _id: nameSpace._id, name: data.name, - users, + users: data.users, }; await NameSpacesApi.updateNameSpace(newItem, t); @@ -138,18 +79,18 @@ const NameSpacesFormDrawer = () => { async function handleCreateNameSpace(data) { const values = { name: data.name, - users, + users: data.users, }; const createdNameSpace = await NameSpacesApi.createNameSpace(values, t); addNameSpace({ ...createdNameSpace, - users, + users: data.users, }); } const onCloseDrawer = () => { - resetForm(); + setOpen(false); cancelEditing(); }; @@ -159,13 +100,7 @@ const NameSpacesFormDrawer = () => { onClose={onCloseDrawer} backgroundColor={colors.lightNeutralSecondary} > - +

{t( @@ -176,61 +111,15 @@ const NameSpacesFormDrawer = () => {

- -
- - - - {errors.name && ( - - {t("common:requiredFieldError")} - - )} - - - option.name} - getOptionKey={(option) => option._id} - disableCloseOnSelect - limitTags={3} - renderInput={(params) => ( - - )} - value={users} - onChange={(_event, newValue) => { - setUsers(newValue); - }} - /> - - - - - - {nameSpace?._id && ( - - - - )} - -
-
+
); diff --git a/src/components/namespace/NameSpaceView.tsx b/src/components/namespace/NameSpaceView.tsx index 7ed583b71..a42db9880 100644 --- a/src/components/namespace/NameSpaceView.tsx +++ b/src/components/namespace/NameSpaceView.tsx @@ -38,7 +38,7 @@ const NameSpaceView = () => { () => [ { field: "name", - headerName: t("namespaces:nameColumn"), + headerName: t("namespaces:nameLabel"), flex: 2, }, { diff --git a/src/components/topics/TagDisplay.tsx b/src/components/topics/TagDisplay.tsx index 05b57f646..0ba6f76eb 100644 --- a/src/components/topics/TagDisplay.tsx +++ b/src/components/topics/TagDisplay.tsx @@ -6,12 +6,7 @@ import TagsList from "./TagsList"; import { useAtom } from "jotai"; import { isUserLoggedIn } from "../../atoms/currentUser"; import TagDisplayStyled from "./TagDisplay.style"; - -interface ITagDisplay { - handleClose: (removedTopicValue: any) => Promise; - tags: any[]; - setShowTopicsForm: (topicsForm: any) => void; -} +import { ITagDisplay } from "../../types/Topic"; const TagDisplay = ({ handleClose, tags, setShowTopicsForm }: ITagDisplay) => { const [isLoggedIn] = useAtom(isUserLoggedIn); @@ -26,12 +21,13 @@ const TagDisplay = ({ handleClose, tags, setShowTopicsForm }: ITagDisplay) => { {isLoggedIn && ( setShowTopicsForm((prev: boolean) => !prev)} style={{ color: colors.primary, }} > - {tags.length ? : } + {tags.length ? : } )} diff --git a/src/components/topics/TagsList.tsx b/src/components/topics/TagsList.tsx index b832fe85b..089497382 100644 --- a/src/components/topics/TagsList.tsx +++ b/src/components/topics/TagsList.tsx @@ -27,14 +27,23 @@ const TagsList = ({ tags, editable = false, handleClose }: TagsListProps) => { {tags.length <= 0 && {t("topics:noTopics")}} {tags && - tags.map((tag) => { - const displayLabel = tag?.name || tag?.label || tag; + tags.map((tag, index) => { + const baseName = tag?.name || tag?.label || tag; + const aliasToShow = + tag?.matchedAlias || + (tag?.aliases && tag.aliases.length > 0 + ? tag.aliases[0] + : null); + const displayLabel = aliasToShow + ? `${baseName} (${aliasToShow})` + : baseName; const tagKey = - tag?._id || tag?.value || tag?.slug || displayLabel; + tag?._id || tag?.value || tag?.slug || baseName; const tagValue = tag?.name || tag?.label || tag; return ( { onDelete={ editable ? () => - handleClose( - tag?.wikidataId || tag?.value || tag - ) + handleClose( + tag?.wikidataId || + tag?.value || + tag + ) : undefined } deleteIcon={ { marginTop: 4, marginBottom: 4, cursor: "pointer", + maxWidth: "300px", }} /> ); diff --git a/src/components/topics/TopicDisplay.tsx b/src/components/topics/TopicDisplay.tsx index d6d39078e..44fc12961 100644 --- a/src/components/topics/TopicDisplay.tsx +++ b/src/components/topics/TopicDisplay.tsx @@ -7,49 +7,55 @@ import TagDisplay from "./TagDisplay"; import SentenceApi from "../../api/sentenceApi"; import verificationRequestApi from "../../api/verificationRequestApi"; import { ReviewTaskTypeEnum } from "../../machines/reviewTask/enums"; +import { ITopicDisplay } from "../../types/Topic"; const TopicDisplay = ({ data_hash, topics, reviewTaskType, contentModel = null, -}) => { +}: ITopicDisplay) => { const [showTopicsForm, setShowTopicsForm] = useState(false); const [topicsArray, setTopicsArray] = useState(topics); - const [inputValue, setInputValue] = useState([]); + const [selectedTags, setSelectedTags] = useState([]); const [tags, setTags] = useState([]); const { t } = useTranslation(); useEffect(() => { - const inputValueFormatted = inputValue.map((inputValue) => - inputValue?.value + const formattedSelectedTags = selectedTags.map((selectedTag) => + selectedTag?.value ? { - label: inputValue?.label.toLowerCase().replace(" ", "-"), - value: inputValue?.value, + label: selectedTag?.label, + value: selectedTag?.value, + aliases: selectedTag?.aliases || [], + matchedAlias: selectedTag?.matchedAlias || null, } - : inputValue + : selectedTag ); - const filterValues = inputValueFormatted.filter( - (inputValue) => + const filterSelectedTags = formattedSelectedTags.filter( + (selectedTag) => !topicsArray.some( (topic) => (topic?.value || topic) === - (inputValue?.value || inputValue) + (selectedTag?.value || selectedTag) ) ); - setTags(topicsArray?.concat(filterValues) || []); - }, [inputValue, topicsArray]); + setTags(topicsArray?.concat(filterSelectedTags) || []); + }, [selectedTags, topicsArray]); const handleClose = async (removedTopicValue: any) => { + // NOTE: Filtering logic differs due to inconsistent data structures across collections: + // 1. topicsArray: May contain persisted topics from VerificationRequest, requiring 'wikidataId' as an identifier. + // 2. selectedTags: Follows the 'ManualTopic' UI schema, which consistently uses 'value'. const newTopicsArray = topicsArray.filter( - (topic) => (topic?.value || topic) !== removedTopicValue + (topic) => (topic?.value || topic?.wikidataId || topic) !== removedTopicValue ); - const newInputValue = inputValue.filter( + const newSelectedTag = selectedTags.filter( (topic) => (topic?.value || topic) !== removedTopicValue ); setTopicsArray(newTopicsArray); - setInputValue(newInputValue); + setSelectedTags(newSelectedTag); if (reviewTaskType === ReviewTaskTypeEnum.VerificationRequest) { return await verificationRequestApi.deleteVerificationRequestTopic( @@ -62,10 +68,10 @@ const TopicDisplay = ({ return contentModel === ContentModelEnum.Image ? await ImageApi.deleteImageTopic(newTopicsArray, data_hash) : await SentenceApi.deleteSentenceTopic( - newTopicsArray, - data_hash, - t - ); + newTopicsArray, + data_hash, + t + ); }; return ( @@ -82,7 +88,7 @@ const TopicDisplay = ({ data_hash={data_hash} topicsArray={topicsArray} setTopicsArray={setTopicsArray} - setInputValue={setInputValue} + setSelectedTags={setSelectedTags} tags={tags} reviewTaskType={reviewTaskType} /> diff --git a/src/components/topics/TopicForm.tsx b/src/components/topics/TopicForm.tsx index 8c0f538a2..a363216b1 100644 --- a/src/components/topics/TopicForm.tsx +++ b/src/components/topics/TopicForm.tsx @@ -5,26 +5,16 @@ import AletheiaButton from "../Button"; import TopicInputErrorMessages from "./TopicInputErrorMessages"; import { useTranslation } from "next-i18next"; import TopicsApi from "../../api/topicsApi"; -import { ContentModelEnum } from "../../types/enums"; import MultiSelectAutocomplete from "./TopicOrImpactSelect"; import verificationRequestApi from "../../api/verificationRequestApi"; - -interface ITopicForm { - contentModel: ContentModelEnum; - data_hash: string; - topicsArray: any[]; - setTopicsArray: (topicsArray) => void; - setInputValue: (inputValue) => void; - tags: any[]; - reviewTaskType: string; -} +import { ITopicForm } from "../../types/Topic"; const TopicForm = ({ contentModel, data_hash, topicsArray, setTopicsArray, - setInputValue, + setSelectedTags, tags, reviewTaskType, }: ITopicForm) => { @@ -92,12 +82,14 @@ const TopicForm = ({ onChange={onChange} setIsLoading={setIsLoading} isLoading={isLoading} - setInputValue={setInputValue} + setSelectedTags={setSelectedTags} + dataCy="testVerificationRequestTopicsInput" /> )} /> { - const { t } = useTranslation(); - const [options, setOptions] = useState([]); - const dispatch = useDispatch(); - let timeout: NodeJS.Timeout; +const MultiSelectAutocomplete = ({ + defaultValue, + isMultiple = true, + onChange, + isLoading, + placeholder, + setSelectedTags, + setIsLoading, + isDisabled, + dataCy, +}: IMultiSelectAutocomplete) => { + const { t } = useTranslation(); + const [options, setOptions] = useState([]); + const dispatch = useDispatch(); + let timeout: NodeJS.Timeout; - const fetchFromApi = async (topic: string, t: any, dispatch: any) => { - const topicSearchResults = await TopicsApi.getTopics({ topicName: topic, t, dispatch }); - return mapTopics(topicSearchResults); - }; + const fetchFromApi = async (topic: string, t: any, dispatch: any) => { + const topicSearchResults = await TopicsApi.getTopics({ + topicName: topic, + t, + dispatch, + }); + return mapTopics(topicSearchResults); + }; - const mapTopics = (topicSearchResults: any[]) => - topicSearchResults?.map(({ name, wikidata }) => ({ - label: name, - value: wikidata, - })) || []; + const mapTopics = (topicSearchResults: any[]) => + topicSearchResults?.map( + ({ name, wikidata, aliases, matchedAlias }) => ({ + label: name, + value: wikidata, + aliases: aliases || [], + matchedAlias: matchedAlias || null, + displayLabel: matchedAlias ? `${name} (${matchedAlias})` : name, + }) + ) || []; + const fetchTopicList = ( + topic: string + ): Promise => { + return new Promise((resolve) => { + if (timeout) clearTimeout(timeout); + if (topic.length >= 3) { + timeout = setTimeout(async () => { + const topics = await fetchFromApi(topic, t, dispatch); + resolve(topics); + }, 1000); + } else { + resolve([]); + } + }); + }; -const fetchTopicList = ( - topic: string -): Promise<{ label: string; value: string }[]> => { - return new Promise((resolve) => { - if (timeout) clearTimeout(timeout); - if (topic.length >= 3) { - timeout = setTimeout(async () => { - const topics = await fetchFromApi(topic, t, dispatch); - resolve(topics); - }, 1000); - } else { - resolve([]); - } - }); -}; - - const fetchOptions = async (inputValue: string) => { - if (inputValue.length >= 3) { - setIsLoading(true); - const fetchedOptions = await fetchTopicList(inputValue); - setOptions(fetchedOptions); - setIsLoading(false); - } else { - setOptions([]); - } - }; + const fetchOptions = async (inputValue: string) => { + if (inputValue.length >= 3) { + setIsLoading(true); + const fetchedOptions = await fetchTopicList(inputValue); + setOptions(fetchedOptions); + setIsLoading(false); + } else { + setOptions([]); + } + }; - return ( - - fetchOptions(value)} - onChange={(_, selectedValues) => { - onChange(selectedValues); - setInputValue(selectedValues); - }} - getOptionLabel={(option) => option.label || ""} - isOptionEqualToValue={(option, value) => option.value === value.value} - loading={isLoading} - renderInput={(params) => ( - - {isLoading && } - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> - - ); + return ( + + fetchOptions(value)} + onChange={(_, inputTag) => { + onChange(inputTag); + setSelectedTags(inputTag); + }} + getOptionLabel={(option) => + option.displayLabel || option.label || option.name || option || "" + } + isOptionEqualToValue={(option, value) => + option.value === value.value + } + loading={isLoading} + renderInput={(params) => ( + + {isLoading && ( + + )} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + disabled={isDisabled} + /> + + ); }; export default MultiSelectAutocomplete; diff --git a/src/constants/cop30Filters.ts b/src/constants/cop30Filters.ts new file mode 100644 index 000000000..92c34fb18 --- /dev/null +++ b/src/constants/cop30Filters.ts @@ -0,0 +1,31 @@ +export interface Cop30Filter { + id: string; + translationKey: string; + wikidataId: string | null; +} + +const cop30FilterMap = { + all: null, + cop30Conference: "Q115323194", + deforestation: "Q5251680", + climateFinancing: "Q113141562", + emissions: "Q106358009", + blueAmazon: "Q131583389", + energy: "Q731859", + environment: "Q43619", + infrastructure: "Q121359", +}; + +const cop30Filters: Cop30Filter[] = Object.entries(cop30FilterMap).map( + ([id, wikidataId]) => ({ + id, + wikidataId, + translationKey: `cop30:${id}`, + }) +); + +export const allCop30WikiDataIds = cop30Filters + .filter((filter) => filter.wikidataId !== null) + .map((filter) => filter.wikidataId); + +export default cop30Filters; diff --git a/src/helpers/formatTimeAgo.ts b/src/helpers/formatTimeAgo.ts new file mode 100644 index 000000000..208621c51 --- /dev/null +++ b/src/helpers/formatTimeAgo.ts @@ -0,0 +1,17 @@ +import { TFunction } from "i18next"; + +export function formatTimeAgo(date: Date | string, t: TFunction) { + const now = new Date(); + const diff = now.getTime() - new Date(date).getTime(); + + const minutes = Math.floor(diff / 60000); + if (minutes < 60) return t("timeAgo:minutesAgo", { count: minutes }); + + const hours = Math.floor(diff / 3600000); + if (hours < 24) return t("timeAgo:hoursAgo", { count: hours }); + + const days = Math.floor(diff / 86400000); + if (days < 7) return t("timeAgo:daysAgo", { count: days }); + + return new Date(date).toLocaleDateString("pt-BR"); +} diff --git a/src/helpers/verificationRequestCardHelper.ts b/src/helpers/verificationRequestCardHelper.ts new file mode 100644 index 000000000..3ec8e45c7 --- /dev/null +++ b/src/helpers/verificationRequestCardHelper.ts @@ -0,0 +1,46 @@ +import colors from "../styles/colors"; +import { SeverityLevel } from "../types/VerificationRequest"; +import { TFunction } from "i18next"; + +export const SEVERITY_COLOR_MAP: Record = { + low: colors.low, + medium: colors.medium, + high: colors.high, + critical: colors.critical, +}; + +export const getSeverityLabel = (severity: string, t: TFunction): string => { + if (!severity || severity === "N/A") return t("claimForm:noAnswer"); + + const [label, level] = severity.split("_"); + + return t(`verificationRequest:priority.${label}`, { + level: level, + }); +}; + +export const getSeverityColor = (severity: string): string => { + if (!severity || severity === "N/A") return colors.neutralSecondary; + + const severityStr = String(severity).toLowerCase(); + const levels: SeverityLevel[] = ["critical", "high", "medium", "low"]; + const matchedLevel = levels.find((level) => severityStr.startsWith(level)); + + return SEVERITY_COLOR_MAP[matchedLevel] || colors.neutralSecondary; +}; + +export const truncateUrl = (url: string) => { + try { + if (!url || typeof url !== 'string') return url; + + const { hostname, pathname } = new URL(url); + const maxLength = 30; + const shortPath = pathname.length > maxLength + ? `${pathname.substring(0, maxLength)}...` + : pathname; + return `${hostname}${shortPath}`; + } catch (error) { + console.warn("Invalid URL for truncation:", url, error); + return url; + } +}; diff --git a/src/hooks/usePersonalities.ts b/src/hooks/usePersonalities.ts new file mode 100644 index 000000000..6ff5836ad --- /dev/null +++ b/src/hooks/usePersonalities.ts @@ -0,0 +1,100 @@ +import { useState, useEffect, useCallback, useRef } from "react"; +import verificationRequestApi from "../api/verificationRequestApi"; +import type { PersonalityWithWikidata } from "../types/PersonalityWithWikidata"; + +interface UsePersonalitiesParams { + requestId: string | undefined; + isOpen: boolean; + hasIdentifiedData: boolean; + language: string; +} + +interface UsePersonalitiesReturn { + personalities: PersonalityWithWikidata[]; + isLoading: boolean; + error: Error | null; + retry: () => void; +} + +export const usePersonalities = ({ + requestId, + isOpen, + hasIdentifiedData, + language, +}: UsePersonalitiesParams): UsePersonalitiesReturn => { + const [personalities, setPersonalities] = useState< + PersonalityWithWikidata[] + >([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const abortControllerRef = useRef(null); + const currentRequestIdRef = useRef(); + + const fetchPersonalities = useCallback(async () => { + if (!requestId || !isOpen || !hasIdentifiedData) { + setPersonalities([]); + setIsLoading(false); + setError(null); + return; + } + + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + abortControllerRef.current = new AbortController(); + currentRequestIdRef.current = requestId; + + setPersonalities([]); + setIsLoading(true); + setError(null); + + try { + const personalitiesData = + await verificationRequestApi.getPersonalitiesWithWikidata( + requestId, + language + ); + + if (currentRequestIdRef.current === requestId) { + setPersonalities(personalitiesData); + setError(null); + } + } catch (err) { + if ( + currentRequestIdRef.current === requestId && + err instanceof Error && + err.name !== "AbortError" + ) { + console.error("Error fetching personalities:", err); + setError(err); + setPersonalities([]); + } + } finally { + if (currentRequestIdRef.current === requestId) { + setIsLoading(false); + } + } + }, [requestId, isOpen, hasIdentifiedData, language]); + + const retry = useCallback(() => { + fetchPersonalities(); + }, [fetchPersonalities]); + + useEffect(() => { + fetchPersonalities(); + + return () => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, [fetchPersonalities]); + + return { + personalities, + isLoading, + error, + retry, + }; +}; diff --git a/src/lottiefiles/generateLottie.ts b/src/lottiefiles/generateLottie.ts index e8eb88032..6c243b6ec 100644 --- a/src/lottiefiles/generateLottie.ts +++ b/src/lottiefiles/generateLottie.ts @@ -7,8 +7,45 @@ import unverifiable from "./unverifiable.json"; import exaggerated from "./exaggerated.json"; import arguable from "./arguable.json"; import trustworthyBut from "./trustworthy-but.json"; +import { ClassificationEnum } from "../types/enums"; -const animations = { +export interface LottieLayer { + ty: number; + ks?: { + a?: { k: number[] }; + p?: { k: number[] }; + s?: { k: number[] }; + }; +} + +export interface LottieImageAsset { + nm: string; + mn: string; + h: number; + w: number; + id: string; + e: number; + u: string; + p: string; +} + +export interface LottiePrecompAsset { + nm: string; + mn: string; + id: string; + layers: unknown[]; +} + +export type LottieAsset = LottieImageAsset | LottiePrecompAsset; + +export interface LottieAnimation { + w: number; + h: number; + assets: LottieAsset[]; + layers: LottieLayer[]; +} + +const animations: Record = { trustworthy, "not-fact": notFact, false: fake, @@ -20,22 +57,63 @@ const animations = { "trustworthy-but": trustworthyBut, }; -export const generateLottie = (classification, imageData, width, height) => { - if (!classification) { +type ClassificationKey = keyof typeof ClassificationEnum; + +export const generateLottie = ( + classification: ClassificationKey | undefined, + imageData: string, + width: number, + height: number +): LottieAnimation | null => { + if (!classification || !animations[classification]) { return null; } + // get the right lottie json file for the classification - const animation = animations[classification]; + const animation = structuredClone(animations[classification]); + + if (!animation.assets || animation.assets.length === 0) { + console.error("Invalid animation: no assets found"); + return null; + } + + const isImageAsset = (asset: LottieAsset): asset is LottieImageAsset => { + return "p" in asset && "w" in asset && "h" in asset; + }; + + const imageAsset = animation.assets[0]; + if (!isImageAsset(imageAsset)) { + console.error("Invalid animation: first asset is not an image"); + return null; + } // set the image url in the lottie file - animation.assets[0].p = imageData; + imageAsset.p = imageData; - // update the size of the svg element + // update the size of the svg canvas to match the actual image dimensions animation.w = width; animation.h = height; - // update the size of the image inside the svg - animation.assets[0].w = width; - animation.assets[0].h = height; + + // Keep the image at its original dimensions + imageAsset.w = width; + imageAsset.h = height; + + // Find the image layer (type 2) and update its anchor, position, and scale + const imageLayers = animation.layers.filter((layer) => layer.ty === 2); + for (const layer of imageLayers) { + // Update anchor point to center of image dimensions + if (layer.ks?.a) { + layer.ks.a.k = [width / 2, height / 2]; + } + // Update position to center of canvas (which now matches image size) + if (layer.ks?.p) { + layer.ks.p.k = [width / 2, height / 2]; + } + // Set scale to 100% (display image at full asset size) + if (layer.ks?.s) { + layer.ks.s.k = [100, 100]; + } + } return animation; }; diff --git a/src/machines/reviewTask/actions.ts b/src/machines/reviewTask/actions.ts index 4056f0a5a..0a51e6c59 100644 --- a/src/machines/reviewTask/actions.ts +++ b/src/machines/reviewTask/actions.ts @@ -18,9 +18,13 @@ const saveContext = assign( supportedEvents.includes(event.type as ReviewTaskEvents) && "visualEditor" in event.reviewData ) { - const schema = editorParser.editor2schema( - event.reviewData.visualEditor.toJSON() - ); + const visualEditorJSON = event.reviewData.visualEditor.toJSON(); + // Remove the trailing paragraph added by Remirror's TrailingNodeExtension. + // This empty paragraph is necessary to allow insertion of new nodes in the editor, + // but should not be persisted in the database to avoid issues when loading content. + const cleanedVisualEditor = + editorParser.removeTrailingParagraph(visualEditorJSON); + const schema = editorParser.editor2schema(cleanedVisualEditor); const reviewDataHtml = editorParser.schema2html(schema); event.reviewData = { ...event.reviewData, diff --git a/src/pages/admin-badges.tsx b/src/pages/admin-badges.tsx index cd7c1afdf..fdb911cd8 100644 --- a/src/pages/admin-badges.tsx +++ b/src/pages/admin-badges.tsx @@ -11,11 +11,14 @@ import { useTranslation } from "next-i18next"; import { atomUserList } from "../atoms/userEdit"; import { NameSpaceEnum } from "../types/Namespace"; import { currentNameSpace } from "../atoms/namespace"; +import actions from "../store/actions"; +import { useDispatch } from "react-redux"; const AdminBadgesPage: NextPage<{ data: string }> = ({ badges, users, nameSpace, + sitekey }: InferGetServerSidePropsType) => { const setBadgesList = useSetAtom(atomBadgesList); const setUserlist = useSetAtom(atomUserList); @@ -26,6 +29,8 @@ const AdminBadgesPage: NextPage<{ data: string }> = ({ setUserlist(users); }, [badges, setBadgesList, setUserlist, users]); const { t } = useTranslation(); + const dispatch = useDispatch(); + dispatch(actions.setSitekey(sitekey)); return ( <> @@ -45,6 +50,7 @@ export async function getServerSideProps({ query, locale, locales, req }) { badges: JSON.parse(JSON.stringify(query.badges)), users: JSON.parse(JSON.stringify(query.users)), nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main, + sitekey: query.sitekey, }, }; } diff --git a/src/pages/admin-namespaces.tsx b/src/pages/admin-namespaces.tsx index 2a7198dca..2ee3362b9 100644 --- a/src/pages/admin-namespaces.tsx +++ b/src/pages/admin-namespaces.tsx @@ -7,8 +7,11 @@ import { useSetAtom } from "jotai"; import { atomUserList } from "../atoms/userEdit"; import { atomNameSpacesList } from "../atoms/namespace"; import NameSpacesFormDrawer from "../components/namespace/NameSpaceFormDrawer"; +import { useDispatch } from "react-redux"; +import actions from "../store/actions"; const AdminNameSpacesPage: NextPage<{ data: string }> = ({ + sitekey, nameSpaces, users, }: InferGetServerSidePropsType) => { @@ -16,6 +19,8 @@ const AdminNameSpacesPage: NextPage<{ data: string }> = ({ const setUserlist = useSetAtom(atomUserList); setNameSpacesList(nameSpaces); setUserlist(users); + const dispatch = useDispatch(); + dispatch(actions.setSitekey(sitekey)); return ( <> @@ -32,6 +37,7 @@ export async function getServerSideProps({ query, locale, locales, req }) { props: { ...(await serverSideTranslations(locale)), nameSpaces: JSON.parse(JSON.stringify(query.nameSpaces)), + sitekey: query.sitekey, users: JSON.parse(JSON.stringify(query.users)), }, }; diff --git a/src/pages/claim-review.tsx b/src/pages/claim-review.tsx index 116dca94e..28b3b9006 100644 --- a/src/pages/claim-review.tsx +++ b/src/pages/claim-review.tsx @@ -67,6 +67,9 @@ const ClaimReviewPage: NextPage = (props) => { enableViewReportPreview ) ); + dispatch(actions.setSelectPersonality(personality)); + dispatch(actions.setSelectTarget(claim)); + dispatch(actions.setSelectContent(content)); const isImage = claim?.contentModel === ContentModelEnum.Image; const review = content?.props?.classification; diff --git a/src/pages/history-page.tsx b/src/pages/history-page.tsx index d2d5dadd7..9cb8fb2e5 100644 --- a/src/pages/history-page.tsx +++ b/src/pages/history-page.tsx @@ -7,8 +7,8 @@ import { useSetAtom } from "jotai"; import { currentNameSpace } from "../atoms/namespace"; const HistoryPage: NextPage<{ - targetId: any; - targetModel: any; + targetId: string; + targetModel: string; nameSpace: NameSpaceEnum; }> = ({ targetId, targetModel, nameSpace }) => { const setCurrentNameSpace = useSetAtom(currentNameSpace); diff --git a/src/pages/personality-page.tsx b/src/pages/personality-page.tsx index 029c14a7a..4d2d0d6d3 100644 --- a/src/pages/personality-page.tsx +++ b/src/pages/personality-page.tsx @@ -14,6 +14,7 @@ import actions from "../store/actions"; import { useEffect } from "react"; import { currentNameSpace } from "../atoms/namespace"; import { NameSpaceEnum } from "../types/Namespace"; +import { isAdmin } from "../utils/GetUserPermission"; const PersonalityPage: NextPage<{ personality: any; @@ -49,7 +50,7 @@ const PersonalityPage: NextPage<{ return ( <> - {(role === Roles.SuperAdmin || role === Roles.Admin) && ( + {isAdmin(role) && ( { +const SignUpPage: NextPage<{ sitekey: string }> = ({ sitekey }) => { const { t } = useTranslation(); + const dispatch = useDispatch(); + dispatch(actions.setSitekey(sitekey)); + return ( <> { ); }; -export async function getServerSideProps({ locale, locales, req }) { +export async function getServerSideProps({ locale, locales, req, query }) { locale = GetLocale(req, locale, locales); + query = JSON.parse(query.props); + return { props: { ...(await serverSideTranslations(locale)), + sitekey: query.sitekey, }, }; } diff --git a/src/pages/tracking-page.tsx b/src/pages/tracking-page.tsx new file mode 100644 index 000000000..3a9f52182 --- /dev/null +++ b/src/pages/tracking-page.tsx @@ -0,0 +1,31 @@ +import { NextPage } from "next"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; +import { GetLocale } from "../utils/GetLocale"; +import { NameSpaceEnum } from "../types/Namespace"; +import { useSetAtom } from "jotai"; +import { currentNameSpace } from "../atoms/namespace"; +import TrackingView from "../components/Tracking/TrackingView"; + +const TrackingPage: NextPage<{ + verificationRequestId: string; + nameSpace: NameSpaceEnum; +}> = ({ verificationRequestId, nameSpace }) => { + const setCurrentNameSpace = useSetAtom(currentNameSpace); + setCurrentNameSpace(nameSpace); + + return ; +}; + +export async function getServerSideProps({ query, locale, locales, req }) { + locale = GetLocale(req, locale, locales); + query = JSON.parse(query.props); + return { + props: { + ...(await serverSideTranslations(locale)), + verificationRequestId: query.verificationRequestId, + href: req.protocol + "://" + req.get("host") + req.originalUrl, + nameSpace: query.nameSpace ? query.nameSpace : NameSpaceEnum.Main, + }, + }; +} +export default TrackingPage; diff --git a/src/pages/verification-request-create.tsx b/src/pages/verification-request-create.tsx index 1cb5f6046..70ec40a16 100644 --- a/src/pages/verification-request-create.tsx +++ b/src/pages/verification-request-create.tsx @@ -8,7 +8,7 @@ import actions from "../store/actions"; import { GetLocale } from "../utils/GetLocale"; import { NameSpaceEnum } from "../types/Namespace"; import { currentNameSpace } from "../atoms/namespace"; -import CreateVerificationRequestView from "../components/VerificationRequest/CreateVerificationRequest/CreateVerificationRequestView"; +import CreateVerificationRequestView from "../components/VerificationRequest/verificationRequestForms/CreateVerificationRequestView"; const CreateVerificationRequestPage: NextPage = ({ sitekey, diff --git a/src/pages/verification-request-page.tsx b/src/pages/verification-request-page.tsx index fe7b0890d..eb1cbc6e4 100644 --- a/src/pages/verification-request-page.tsx +++ b/src/pages/verification-request-page.tsx @@ -8,7 +8,7 @@ import { NameSpaceEnum } from "../types/Namespace"; import { currentNameSpace } from "../atoms/namespace"; import { useSetAtom } from "jotai"; import AffixButton from "../components/AffixButton/AffixButton"; -import VerificationRequestList from "../components/VerificationRequest/VerificationRequestList"; +import VerificationRequestView from "../components/VerificationRequest/VerificationRequestView"; import { useDispatch } from "react-redux"; import actions from "../store/actions"; @@ -26,7 +26,7 @@ const VerificationRequestPage: NextPage<{ nameSpace, sitekey }> = ({ nameSpace, title={t("seo:verificationRequestTitle")} description={t("seo:verificationRequestDescription")} /> - + ); diff --git a/src/styles/colors.ts b/src/styles/colors.ts index bab2a76da..74d074200 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -19,6 +19,8 @@ const colors = { white: "rgb(255, 255, 255)", //#ffffff warning: "rgba(219, 159, 13, 0.3)", //#db9f0d shadow: "rgba(0, 0, 0, 0.25)", + errorTranslucent: "rgba(255, 77, 79, 0.1)", + activeTranslucent: "rgba(73, 222, 128, 0.1)", logo: "#E8E8E8", error: "#ff4d4f", active: "#49DE80", diff --git a/src/types/Badge.ts b/src/types/Badge.ts index 0680a0640..c9a828d0b 100644 --- a/src/types/Badge.ts +++ b/src/types/Badge.ts @@ -1,17 +1,44 @@ -export type Badge = { +import { UploadFile } from "../components/ImageUpload"; +import { User } from "./User"; + +export interface Image { + type: string; + _id: string; + data_hash: string; + props: { + key: string; + extension: string; + }; + content: string; +}; + +export interface Badge { name: string; description: string; - image: { - type: string; - _id: string; - data_hash: string; - props: { - key: string; - extension: string; - }; - content: string; - }; + image: Image created_at: string; _id: string; users: any[]; }; + +export interface IDynamicBadgesForm { + badges: Badge; + onSubmit: (value: IBadgeData) => void; + isLoading: boolean; + isDrawerOpen:boolean; + onClose: () => void; +} + +export interface IBadgeData { + name: string; + description: string; + imageField: UploadFile[]; + usersId: string[]; +} + +export interface IBadgeProps { + name: string; + description: string; + image: Image; + users: User[]; +} diff --git a/src/types/Cop30Sentence.ts b/src/types/Cop30Sentence.ts new file mode 100644 index 000000000..765f45b49 --- /dev/null +++ b/src/types/Cop30Sentence.ts @@ -0,0 +1,7 @@ +import { Sentence } from "./Sentence"; +import { Review } from "./Review"; + +export interface Cop30Sentence extends Sentence { + classification?: string; + review?: Review; +} diff --git a/src/types/Cop30Stats.ts b/src/types/Cop30Stats.ts new file mode 100644 index 000000000..08a23c5fb --- /dev/null +++ b/src/types/Cop30Stats.ts @@ -0,0 +1,6 @@ +export interface Cop30Stats { + total: number; + reliable: number; + deceptive: number; + underReview: number; +} diff --git a/src/types/History.ts b/src/types/History.ts new file mode 100644 index 000000000..a683acad9 --- /dev/null +++ b/src/types/History.ts @@ -0,0 +1,19 @@ +import { TargetModel } from "./enums"; +import { M2M, User } from "./User"; + +export const HEX24 = /^[0-9a-fA-F]{24}$/; + +export type PerformedBy = M2M | User; + +export interface HistoryListItemProps { + history: { + user: PerformedBy; + type: string; + targetModel: TargetModel; + date?: Date; + details: { + before?: Record; + after: Record; + }; + }; +} \ No newline at end of file diff --git a/src/types/Namespace.ts b/src/types/Namespace.ts index 80a78ceb9..3bdece4cb 100644 --- a/src/types/Namespace.ts +++ b/src/types/Namespace.ts @@ -1,6 +1,9 @@ +import { TFunction } from "next-i18next"; +import { User } from "./User"; + export type NameSpace = { name: string; - users: any[]; + users: User[]; slug: string; _id: string; }; @@ -8,3 +11,13 @@ export type NameSpace = { export enum NameSpaceEnum { Main = "main", } + +export interface IDynamicNameSpaceForm { + nameSpace: NameSpace; + onSubmit: (value: NameSpace) => void; + isLoading: boolean; + setIsLoading: React.Dispatch>; + isDrawerOpen: boolean; + onClose: () => void; + t: TFunction; +} diff --git a/src/types/PersonalityWithWikidata.ts b/src/types/PersonalityWithWikidata.ts new file mode 100644 index 000000000..40c917e78 --- /dev/null +++ b/src/types/PersonalityWithWikidata.ts @@ -0,0 +1,40 @@ +/** + * Personality data enriched with Wikidata information + */ +export interface PersonalityWithWikidata { + /** Personality database ID */ + personalityId: string; + + /** Personality name */ + name: string; + + /** Personality URL slug */ + slug: string; + + /** Personality description (from database or Wikidata) */ + description: string | null; + + /** Wikidata avatar/image URL, null if not available */ + avatar: string | null; + + /** Wikidata entity ID (e.g., Q22686), null if not linked to Wikidata */ + wikidata: string | null; +} + +/** + * Type guard to check if a personality has a Wikidata ID + */ +export function hasWikidataId( + personality: PersonalityWithWikidata +): personality is PersonalityWithWikidata & { wikidata: string } { + return personality.wikidata !== null && personality.wikidata !== ""; +} + +/** + * Type guard to check if a personality has an avatar + */ +export function hasAvatar( + personality: PersonalityWithWikidata +): personality is PersonalityWithWikidata & { avatar: string } { + return personality.avatar !== null && personality.avatar !== ""; +} diff --git a/src/types/Review.ts b/src/types/Review.ts new file mode 100644 index 000000000..f32a67115 --- /dev/null +++ b/src/types/Review.ts @@ -0,0 +1,5 @@ +export interface Review { + personality: string; + usersId: string; + isPartialReview: boolean; +} diff --git a/src/types/Sentence.ts b/src/types/Sentence.ts new file mode 100644 index 000000000..df3281fe1 --- /dev/null +++ b/src/types/Sentence.ts @@ -0,0 +1,16 @@ +import { SentenceTopic } from "./SentenceTopic"; + +export interface SentenceContent { + _id: string; + type: string; + data_hash: string; + props: any; + topics: SentenceTopic[]; +} + +export interface Sentence { + _id: string; + content: SentenceContent; + data_hash: string; + topics: SentenceTopic[] | string[]; +} diff --git a/src/types/SentenceTopic.ts b/src/types/SentenceTopic.ts new file mode 100644 index 000000000..859e88e72 --- /dev/null +++ b/src/types/SentenceTopic.ts @@ -0,0 +1,5 @@ +export interface SentenceTopic { + id: string; + label: string; + value: string; +} diff --git a/src/types/Topic.ts b/src/types/Topic.ts new file mode 100644 index 000000000..777730dd6 --- /dev/null +++ b/src/types/Topic.ts @@ -0,0 +1,65 @@ +import React from "react"; +import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums"; +import { ContentModelEnum } from "./enums"; +import { UnifiedDefaultValue } from "../components/Form/DynamicInput"; + +export interface Topic { + _id: string; + name: string; + wikidataId: string; + slug: string; + language: string; +} + +export interface ManualTopic { + id?: string; + aliases?: string[]; + displayLabel?: string; + label: string; + matchedAlias?: string | null; + value: string; +} + +export type UnifiedTopic = Topic | ManualTopic; +export interface ITagDisplay { + handleClose: (removedTopicValue: string) => Promise; + tags: ManualTopic[]; + setShowTopicsForm: React.Dispatch>; +} + +export interface ITopicForm { + contentModel: ContentModelEnum; + data_hash: string; + topicsArray: UnifiedTopic[]; + setTopicsArray: React.Dispatch>; + setSelectedTags: React.Dispatch>; + tags: ManualTopic[]; + reviewTaskType: ReviewTaskTypeEnum; +} + +export interface ITopicDisplay { + data_hash: string; + topics: UnifiedTopic[]; + reviewTaskType: ReviewTaskTypeEnum; + contentModel?: ContentModelEnum | null; +} + +export interface IImpactAreaSelect { + defaultValue: UnifiedDefaultValue; + onChange: (value: ManualTopic) => void; + placeholder?: string; + isDisabled: boolean; + dataCy?: string; +} + +export interface IMultiSelectAutocomplete { + defaultValue?: UnifiedDefaultValue; + isMultiple?: boolean; + onChange: (value: ManualTopic[] | ManualTopic) => void; + isLoading: boolean; + placeholder: string; + setSelectedTags: React.Dispatch>; + setIsLoading: React.Dispatch>; + isDisabled?: boolean; + dataCy?: string; +} diff --git a/src/types/Tracking.ts b/src/types/Tracking.ts new file mode 100644 index 000000000..33424d0a1 --- /dev/null +++ b/src/types/Tracking.ts @@ -0,0 +1,32 @@ +import { VerificationRequestStatus } from "./enums"; +interface TrackingCardProps { + verificationRequestId: string; + isMinimal?: boolean +} + +interface TrackingResponseDTO { + currentStatus: VerificationRequestStatus; + historyEvents: HistoryItem[]; + isMinimal?: boolean +} + +interface HistoryItem { + id: string; + status: VerificationRequestStatus; + date: Date; +} + +interface TrackingStepProps { + stepKey: string; + stepDate: Date | "noData" | null; + isCompleted: boolean; + isDeclined: boolean; + isMinimal?: boolean +} + +interface StepLabelStyledProps { + backgroundStatusColor: string; + iconColor: string; +} + +export type { TrackingResponseDTO, TrackingCardProps, TrackingStepProps, StepLabelStyledProps }; diff --git a/src/types/User.ts b/src/types/User.ts index bcb166fda..e8e33e842 100644 --- a/src/types/User.ts +++ b/src/types/User.ts @@ -10,3 +10,14 @@ export type User = { state: Status; totp: boolean; }; + +export class M2M { + isM2M: boolean; + clientId: string; + subject: string; + scopes: string[]; + role: { + main: Roles.Integration; + }; + namespace: string; +} diff --git a/src/types/VerificationRequest.ts b/src/types/VerificationRequest.ts index 8da533eca..78baf4cf5 100644 --- a/src/types/VerificationRequest.ts +++ b/src/types/VerificationRequest.ts @@ -1,11 +1,29 @@ import { Group } from "./Group"; import { Source } from "../../server/source/schemas/source.schema"; -import { Dispatch } from "react"; +import { ActionTypes } from "../store/types"; +import { Topic } from "./Topic"; +import { UnifiedDefaultValue } from "../components/Form/DynamicInput"; -type PaginationSettings = { +export enum FilterType { + TOPIC = "topic", + IMPACT_AREA = "impactArea", +} + +interface PaginationSettings { pageSize: number; page: number; }; +interface TopicOption { + name: string; + matchedAlias?: string | null; +} +interface FilterItem { + label: string; + value: string; + type: FilterType; +} + +type ViewMode = "board" | "dashboard"; type verificationRequestStatus = "Pre Triage" | "In Triage" | "Posted"; @@ -13,17 +31,19 @@ type PaginationModel = Record; type SeverityLevel = "low" | "medium" | "high" | "critical"; -type VerificationRequest = { +interface VerificationRequest { data_hash: string; content: string; isSensitive: boolean; rejected: boolean; group: Group; date: Date; - source?: Source[]; + source?: Source[] | string[]; _id?: string; publicationDate: string; heardFrom: string; + reportType?: string; + impactArea?: Topic; }; interface FiltersState { loading: Record; @@ -36,10 +56,13 @@ interface FiltersState { filterType: string; anchorEl: HTMLElement | null; paginationModel: PaginationModel; - autoCompleteTopicsResults: string[]; + autoCompleteTopicsResults?: TopicOption[]; topicFilterUsed: string[]; impactAreaFilterUsed: string[]; applyFilters: boolean; + viewMode: ViewMode; + startDate: Date | null; + endDate: Date | null; } interface FiltersActions { setIsInitialLoad: (initial: boolean) => void; @@ -49,19 +72,81 @@ interface FiltersActions { setFilterValue: (value: string[]) => void; setFilterType: (type: string) => void; setAnchorEl: (el: HTMLElement | null) => void; - setPaginationModel: (model: FiltersState["paginationModel"]) => void; + setPaginationModel: React.Dispatch>; setApplyFilters: (apply: boolean) => void; fetchTopicList: (term: string) => Promise; createFilterChangeHandler: ( setter: (v: any) => void ) => (newValue: any) => void; - dispatch: Dispatch; + dispatch: (action: { type: ActionTypes; [key: string]: any }) => void; t: (key: string) => string; + setViewMode: (mode: ViewMode) => void; + setStartDate: (date: Date | null) => void; + setEndDate: (date: Date | null) => void; } interface FiltersContext { state: FiltersState; actions: FiltersActions; } +interface StatsCount { + total?: number; + totalThisMonth?: number; + verified: number; + inAnalysis: number; + pending: number; +} +interface StatsSourceChannels { + label: string; + value: number; + percentage: number; +} +interface StatsRecentActivity { + id: string; + status: string; + sourceChannel: string; + data_hash: string; + timestamp: Date; +} + +interface StatsSourceChannelsProps { + statsCounts?: StatsCount; + statsSourceChannels: StatsSourceChannels[]; +} +interface StatsRecentActivityProps { + statsRecentActivity: StatsRecentActivity[]; +} +interface IEditVerificationRequestDrawer { + open: boolean; + onClose: () => void; + verificationRequest: VerificationRequest; + onSave: (updatedRequest: VerificationRequest) => void; +} +interface IInputExtraSourcesList { + defaultSources: UnifiedDefaultValue; + onChange: (value: string[]) => void; + disabled: boolean; + placeholder: string; + dataCy?: string; +} +interface IReportTypeSelect { + onChange: (value: UnifiedDefaultValue) => void; + defaultValue: UnifiedDefaultValue; + placeholder: string; + style?: React.CSSProperties; + isDisabled: boolean; + dataCy?: string; +} + +interface IDynamicVerificationRequestForm { + data?: VerificationRequest; + onSubmit: (value: VerificationRequest) => void; + isLoading: boolean; + setRecaptchaString: React.Dispatch>; + hasCaptcha: boolean; + isEdit: boolean; + isDrawerOpen: boolean; + onClose: () => void; +} export type { VerificationRequest, @@ -70,4 +155,16 @@ export type { FiltersContext, PaginationModel, SeverityLevel, + ViewMode, + FilterItem, + TopicOption, + StatsCount, + StatsSourceChannels, + StatsRecentActivity, + StatsSourceChannelsProps, + StatsRecentActivityProps, + IEditVerificationRequestDrawer, + IInputExtraSourcesList, + IReportTypeSelect, + IDynamicVerificationRequestForm }; diff --git a/src/types/enums.ts b/src/types/enums.ts index e1fb5471f..4372c9900 100644 --- a/src/types/enums.ts +++ b/src/types/enums.ts @@ -16,6 +16,7 @@ enum Roles { Reviewer = "reviewer", Admin = "admin", SuperAdmin = "super-admin", + Integration = "integration", } enum Status { @@ -37,6 +38,7 @@ export enum TargetModel { ClaimReview = "ClaimReview", ReviewTask = "ReviewTask", Image = "Image", + History = "History", } enum CommentEnum { @@ -49,6 +51,17 @@ enum SenderEnum { User = "You", } +enum M2MSubject { + Chatbot = "chatbot-service", +} + +enum VerificationRequestStatus { + PRE_TRIAGE = "Pre Triage", + IN_TRIAGE = "In Triage", + POSTED = "Posted", + DECLINED = "Declined", +} + export { ClassificationEnum, Roles, @@ -56,4 +69,6 @@ export { ContentModelEnum, CommentEnum, SenderEnum, + M2MSubject, + VerificationRequestStatus, }; diff --git a/src/utils/GetReviewContentHref.ts b/src/utils/GetReviewContentHref.ts index c443e545d..4026ec387 100644 --- a/src/utils/GetReviewContentHref.ts +++ b/src/utils/GetReviewContentHref.ts @@ -1,15 +1,25 @@ import { ReviewTaskTypeEnum } from "../machines/reviewTask/enums"; import { NameSpaceEnum } from "../types/Namespace"; -import { ContentModelEnum } from "../types/enums"; +import { ContentModelEnum, TargetModel } from "../types/enums"; + +interface Personality { + slug: string; +} + +interface Claim { + _id: string; + slug: string; +} export const generateReviewContentPath = ( - nameSpace, - personality, - claim, - contentModel, - data_hash, - reviewTaskType -) => { + nameSpace: NameSpaceEnum, + personality: Personality | null | undefined, + claim: Claim | null | undefined, + contentModel: ContentModelEnum, + data_hash: string, + reviewTaskType, //TODO: Track reviewTaskType params to properly type this param + targetModel?: TargetModel, +): string => { const basePath = nameSpace !== NameSpaceEnum.Main ? `/${nameSpace}` : ""; if (reviewTaskType === ReviewTaskTypeEnum.Source) { @@ -20,13 +30,19 @@ export const generateReviewContentPath = ( return `${basePath}/verification-request/${data_hash}`; } + if (targetModel === TargetModel.History) { + return `${basePath}/personality/${personality?.slug}/claim/${claim?.slug}/sentence/${data_hash}/history`; + } + switch (contentModel) { case ContentModelEnum.Speech: return `${basePath}/personality/${personality?.slug}/claim/${claim?.slug}/sentence/${data_hash}`; case ContentModelEnum.Image: return `${basePath}${ - personality ? `/personality/${personality?.slug}` : "" - }/claim/${claim?.slug}/image/${data_hash}`; + personality + ? `/personality/${personality?.slug}/claim/${claim?.slug}` + : `/claim/${claim?._id}` + }/image/${data_hash}`; case ContentModelEnum.Debate: return `${basePath}/personality/${personality?.slug}/claim/${claim?.slug}/sentence/${data_hash}`; case ContentModelEnum.Unattributed: diff --git a/src/utils/GetUserPermission.ts b/src/utils/GetUserPermission.ts index e201d1e6b..9de8b7b9a 100644 --- a/src/utils/GetUserPermission.ts +++ b/src/utils/GetUserPermission.ts @@ -5,3 +5,11 @@ export const canEdit = (currentUser, userId) => { const editingSelf = currentUser?._id === userId; return (isSelectedSuperAdmin && editingSelf) || !isSelectedSuperAdmin; }; + +const Permission = { + isAdmin: (role: Roles) => [Roles.Admin, Roles.SuperAdmin].includes(role), + isChecker: (role: Roles) => [Roles.Reviewer, Roles.FactChecker].includes(role), + isStaff: (role: Roles) => Permission.isAdmin(role) || Permission.isChecker(role) +}; + +export const { isAdmin, isChecker, isStaff } = Permission; diff --git a/src/utils/TypeGuards.ts b/src/utils/TypeGuards.ts new file mode 100644 index 000000000..e15d9f77a --- /dev/null +++ b/src/utils/TypeGuards.ts @@ -0,0 +1,10 @@ +import { PerformedBy } from "../types/History"; +import { M2M, User } from "../types/User"; + +export function isM2M(user: PerformedBy): user is M2M { + return !!user && typeof user === "object" && "isM2M" in user; +} + +export function isUser(user: PerformedBy): user is User { + return !!user && typeof user === "object" && "name" in user; +} \ No newline at end of file diff --git a/src/utils/ValidateFloatingLink.ts b/src/utils/ValidateFloatingLink.ts index 4ee6cdfc0..fbe586b0b 100644 --- a/src/utils/ValidateFloatingLink.ts +++ b/src/utils/ValidateFloatingLink.ts @@ -1,5 +1,5 @@ export const URL_PATTERN = -/^(?!.*\.$)(https?|ftp):\/\/[^\s/$.?#]+(\.[A-Za-z]{2,})+([/?#][^\s]*)?$/i; + /^(?!.*\.$)(ftp|http|https):\/\/[^ "]+\.[a-zA-Z]{2,}(\/|\?|#|$)/i; export const validateFloatingLink = (href?: string, t?: (key: string) => string) => { if (href?.endsWith('.')) { diff --git a/src/utils/date.utils.ts b/src/utils/date.utils.ts new file mode 100644 index 000000000..319adeff3 --- /dev/null +++ b/src/utils/date.utils.ts @@ -0,0 +1,19 @@ +export function buildDateQuery(startDate?: string, endDate?: string) { + if (!startDate && !endDate) return undefined; + + const query: any = {}; + + if (startDate) { + const start = new Date(startDate); + start.setHours(0, 0, 0, 0); + query.$gte = start; + } + + if (endDate) { + const end = new Date(endDate); + end.setHours(23, 59, 59, 999); + query.$lte = end; + } + + return query; +} diff --git a/yarn.lock b/yarn.lock index 81d718af0..c95818c68 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,114 +12,37 @@ __metadata: languageName: node linkType: hard -"@ai-sdk/provider-utils@npm:1.0.22": - version: 1.0.22 - resolution: "@ai-sdk/provider-utils@npm:1.0.22" +"@ai-sdk/gateway@npm:1.0.29": + version: 1.0.29 + resolution: "@ai-sdk/gateway@npm:1.0.29" dependencies: - "@ai-sdk/provider": 0.0.26 - eventsource-parser: ^1.1.2 - nanoid: ^3.3.7 - secure-json-parse: ^2.7.0 + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/provider-utils": 3.0.9 peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - checksum: 0657ea6b78e4b14caf3127ec4a3ab6b72dc6d54f1c7058d74c57b997ad2c62b5ca15f8b7f4963f269fe31ba6423b80ade9f7090efce74e271ef9e21103f3f1df - languageName: node - linkType: hard - -"@ai-sdk/provider@npm:0.0.26": - version: 0.0.26 - resolution: "@ai-sdk/provider@npm:0.0.26" - dependencies: - json-schema: ^0.4.0 - checksum: 9f6281b90ded0d45dc76b5f099011e7e1746cdee916af0900e8249e71e4ec549d574f01fa0d15a6f8d4839a0c12ef282d726118818ddaced0a19bb80b8a64ae1 - languageName: node - linkType: hard - -"@ai-sdk/react@npm:0.0.70": - version: 0.0.70 - resolution: "@ai-sdk/react@npm:0.0.70" - dependencies: - "@ai-sdk/provider-utils": 1.0.22 - "@ai-sdk/ui-utils": 0.0.50 - swr: ^2.2.5 - throttleit: 2.1.0 - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - zod: ^3.0.0 - peerDependenciesMeta: - react: - optional: true - zod: - optional: true - checksum: 8a28523ba3cba731e4ba69fe9b371c49e68410a28f23e91a1d2125a4962e133928d75f265cfc6b3043d686eb94104fcf0a80557830f91ca76bf429fd9232dd8f - languageName: node - linkType: hard - -"@ai-sdk/solid@npm:0.0.54": - version: 0.0.54 - resolution: "@ai-sdk/solid@npm:0.0.54" - dependencies: - "@ai-sdk/provider-utils": 1.0.22 - "@ai-sdk/ui-utils": 0.0.50 - peerDependencies: - solid-js: ^1.7.7 - peerDependenciesMeta: - solid-js: - optional: true - checksum: c9d9e6284dcaf7f3f731d802f9c2bdad74ecc40ed3e61968dac6c342714fffb51162fcdc8bec3298e940172c89fae3a386367fa4850344c86562ef14a9ed6707 + zod: ^3.25.76 || ^4 + checksum: 3bd41160ab5edb7fc54e5006fb1f641ab4bb4090bcdbbe8f9eb7457ea963859e02e65b91861d081f372f6bbd3bdeaa1971972f73bfe0b0b5d8d45a656ae6d294 languageName: node linkType: hard -"@ai-sdk/svelte@npm:0.0.57": - version: 0.0.57 - resolution: "@ai-sdk/svelte@npm:0.0.57" +"@ai-sdk/provider-utils@npm:3.0.9": + version: 3.0.9 + resolution: "@ai-sdk/provider-utils@npm:3.0.9" dependencies: - "@ai-sdk/provider-utils": 1.0.22 - "@ai-sdk/ui-utils": 0.0.50 - sswr: ^2.1.0 + "@ai-sdk/provider": 2.0.0 + "@standard-schema/spec": ^1.0.0 + eventsource-parser: ^3.0.5 peerDependencies: - svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - checksum: 7b8db6add3f96845bd020baf7f58d0906f835d29ae991f9e8a62286a3df835c8b499dea7c054777eeec6423248ba74cbf382531cbf86075098ab5ad47e48031f + zod: ^3.25.76 || ^4 + checksum: b1c7a3ef435ed4a8b77256c59454aa6e1f6e33b13b0dd88822af213b376b8d55a0aad820201718e269b45b7b77b6e63d6cabbd47bcef05c8e5000a6801b9f104 languageName: node linkType: hard -"@ai-sdk/ui-utils@npm:0.0.50": - version: 0.0.50 - resolution: "@ai-sdk/ui-utils@npm:0.0.50" +"@ai-sdk/provider@npm:2.0.0": + version: 2.0.0 + resolution: "@ai-sdk/provider@npm:2.0.0" dependencies: - "@ai-sdk/provider": 0.0.26 - "@ai-sdk/provider-utils": 1.0.22 json-schema: ^0.4.0 - secure-json-parse: ^2.7.0 - zod-to-json-schema: ^3.23.3 - peerDependencies: - zod: ^3.0.0 - peerDependenciesMeta: - zod: - optional: true - checksum: 6610b8e82e5f6347811adf2e07b092c3ac8f32e298ed81bb6ae1acd8f31edc923175008e880b80abc338d3e6233fd5c7b59cbe6c84f80931bffa9384e98e4e06 - languageName: node - linkType: hard - -"@ai-sdk/vue@npm:0.0.59": - version: 0.0.59 - resolution: "@ai-sdk/vue@npm:0.0.59" - dependencies: - "@ai-sdk/provider-utils": 1.0.22 - "@ai-sdk/ui-utils": 0.0.50 - swrv: ^1.0.4 - peerDependencies: - vue: ^3.3.4 - peerDependenciesMeta: - vue: - optional: true - checksum: f489da3d1d152f4ddb8d541254cddbae4d059719dc71f4df6c2b5a99b4e4f46576721193d7f241143be302b0249575bcf0480f006dd811f54854e6af067ad5d0 + checksum: 6068b327d48d37a5cb994652c29af4206f228c1fd1e5ab38f2db860b5b6ec3e1e7c75a2fab98e633fec59dc7f0ac6f115ce68aad66175264b263509928c67b91 languageName: node linkType: hard @@ -305,23 +228,6 @@ __metadata: languageName: node linkType: hard -"@anthropic-ai/sdk@npm:^0.9.1": - version: 0.9.1 - resolution: "@anthropic-ai/sdk@npm:0.9.1" - dependencies: - "@types/node": ^18.11.18 - "@types/node-fetch": ^2.6.4 - abort-controller: ^3.0.0 - agentkeepalive: ^4.2.1 - digest-fetch: ^1.3.0 - form-data-encoder: 1.7.2 - formdata-node: ^4.3.2 - node-fetch: ^2.6.7 - web-streams-polyfill: ^3.2.1 - checksum: 0ec50abc0ffc694d903d516f4ac110aafa3a588438791851dd2c3d220da49ceaf2723e218b7f1bc13e737fee1561b18ad8c3b4cc4347e8212131f69637216413 - languageName: node - linkType: hard - "@aw-web-design/x-default-browser@npm:1.4.126": version: 1.4.126 resolution: "@aw-web-design/x-default-browser@npm:1.4.126" @@ -1834,12 +1740,26 @@ __metadata: languageName: node linkType: hard -"@casl/ability@npm:^6.0.0": - version: 6.7.3 - resolution: "@casl/ability@npm:6.7.3" +"@borewit/text-codec@npm:^0.2.1": + version: 0.2.1 + resolution: "@borewit/text-codec@npm:0.2.1" + checksum: 72cbd41913220be173e635b965ff85a1753610f893438790b9a975662dfacff4df3af400bab8d67e452364aa8cd0578a6dc917d3dc82e63e1e09186b2563b548 + languageName: node + linkType: hard + +"@casl/ability@npm:6.8.0": + version: 6.8.0 + resolution: "@casl/ability@npm:6.8.0" dependencies: "@ucast/mongo2js": ^1.3.0 - checksum: 9905944ed6b95bc610e145be7e1170a79ae5b3e63f051bd8277a765bad60283951c99965e96c7cb39820373c11d9fe66a103686a52dcaa2cc2f7e5f851044ba0 + checksum: a0ccee805ed8234cd411cddaf1a9deb5ca4cc0ecdd66b2dd4d4477bccff5b2cd8631561a46450a48927eb4cc7e7a639d31a585ba02c2e259ec2a063e86283a9d + languageName: node + linkType: hard + +"@cfworker/json-schema@npm:^4.0.2": + version: 4.1.1 + resolution: "@cfworker/json-schema@npm:4.1.1" + checksum: 35b5b246eff7bc75a17befb6e6d56475ab9261279c5d727610dc6827cce557d11db353cca3c06b8272f3974eb2ac508a7bbae3accd3d6c8402dfe0aafbfea0aa languageName: node linkType: hard @@ -2461,6 +2381,117 @@ __metadata: languageName: node linkType: hard +"@eslint-community/eslint-utils@npm:^4.8.0": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" + dependencies: + eslint-visitor-keys: ^3.4.3 + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + checksum: 0a27c2d676c4be6b329ebb5dd8f6c5ef5fae9a019ff575655306d72874bb26f3ab20e0b241a5f086464bb1f2511ca26a29ff6f80c1e2b0b02eca4686b4dfe1b5 + languageName: node + linkType: hard + +"@eslint-community/regexpp@npm:^4.12.1": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 1770bc81f676a72f65c7200b5675ff7a349786521f30e66125faaf767fde1ba1c19c3790e16ba8508a62a3933afcfc806a893858b3b5906faf693d862b9e4120 + languageName: node + linkType: hard + +"@eslint/compat@npm:^2.0.2": + version: 2.0.2 + resolution: "@eslint/compat@npm:2.0.2" + dependencies: + "@eslint/core": ^1.1.0 + peerDependencies: + eslint: ^8.40 || 9 || 10 + peerDependenciesMeta: + eslint: + optional: true + checksum: c2445b59eaa83e9f943a4fbf0b769f1971832263dad693c0417ec6191694731f9cbbcd6aee2c774b15382ea119776399d32a7ff1a9510b81e95408ea7f3633be + languageName: node + linkType: hard + +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" + dependencies: + "@eslint/object-schema": ^2.1.7 + debug: ^4.3.1 + minimatch: ^3.1.2 + checksum: fc5b57803b059f7c1f62950ef83baf045a01887fc00551f9e87ac119246fcc6d71c854a7f678accc79cbf829ed010e8135c755a154b0f54b129c538950cd7e6a + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": ^0.17.0 + checksum: 63ff6a0730c9fff2edb80c89b39b15b28d6a635a1c3f32cf0d7eb3e2625f2efbc373c5531ae84e420ae36d6e37016dd40c365b6e5dee6938478e9907aaadae0b + languageName: node + linkType: hard + +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: ff9b5b4987f0bae4f2a4cfcdc7ae584ad3b0cb58526ca562fb281d6837700a04c7f3c86862e95126462318f33f60bf38e1cb07ed0e2449532d4b91cd5f4ab1f2 + languageName: node + linkType: hard + +"@eslint/core@npm:^1.1.0": + version: 1.1.0 + resolution: "@eslint/core@npm:1.1.0" + dependencies: + "@types/json-schema": ^7.0.15 + checksum: 8f0f11540c1b42a91e694bc10861acbc1e29994ba6e2c8782186c0b9ec1a95ca0272ea9eb02408cdcc5f55843b584e0572a9e3cf7d924d912214a0c86fe3cde0 + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.3.1, @eslint/eslintrc@npm:^3.3.3": + version: 3.3.3 + resolution: "@eslint/eslintrc@npm:3.3.3" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^10.0.1 + globals: ^14.0.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.1 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: d1e16e47f1bb29af32defa597eaf84ac0ff8c06760c0a5f4933c604cd9d931d48c89bed96252222f22abac231898a53bc41385a5e6129257f0060b5ec431bdb2 + languageName: node + linkType: hard + +"@eslint/js@npm:9.39.2, @eslint/js@npm:^9.39.2": + version: 9.39.2 + resolution: "@eslint/js@npm:9.39.2" + checksum: 362aa447266fa5717e762b2b252f177345cb0d7b2954113db9773b3a28898f7cbbc807e07f8078995e6da3f62791f7c5fa2c03517b7170a8e76613cf7fd83c92 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: fc5708f192476956544def13455d60fd1bafbf8f062d1e05ec5c06dd470b02078eaf721e696a8b31c1c45d2056723a514b941ae5eea1398cc7e38eba6711a775 + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" + dependencies: + "@eslint/core": ^0.17.0 + levn: ^0.4.1 + checksum: 3f4492e02a3620e05d46126c5cfeff5f651ecf33466c8f88efb4812ae69db5f005e8c13373afabc070ecca7becd319b656d6670ad5093f05ca63c2a8841d99ba + languageName: node + linkType: hard + "@fal-works/esbuild-plugin-global-externals@npm:^2.1.2": version: 2.1.2 resolution: "@fal-works/esbuild-plugin-global-externals@npm:2.1.2" @@ -2616,19 +2647,80 @@ __metadata: languageName: node linkType: hard -"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0": - version: 9.3.0 - resolution: "@hapi/hoek@npm:9.3.0" - checksum: 4771c7a776242c3c022b168046af4e324d116a9d2e1d60631ee64f474c6e38d1bb07092d898bf95c7bc5d334c5582798a1456321b2e53ca817d4e7c88bc25b43 +"@hapi/address@npm:^5.1.1": + version: 5.1.1 + resolution: "@hapi/address@npm:5.1.1" + dependencies: + "@hapi/hoek": ^11.0.2 + checksum: 678790d86d120e36ff5ea7db061763447cd2589e99fd82bcb5d86c8e6ec290f1c0370d3e2050bf483413757e798f5a9495af0af13ba7e0d8a747c5bcd5682b33 languageName: node linkType: hard -"@hapi/topo@npm:^5.1.0": - version: 5.1.0 - resolution: "@hapi/topo@npm:5.1.0" +"@hapi/formula@npm:^3.0.2": + version: 3.0.2 + resolution: "@hapi/formula@npm:3.0.2" + checksum: a774d8133e4cfc7059a1196f1233ba4ed204ac9a6835db476eb94b301555038ba2511664b5e941df3b6e973759e8c0ddf76124e1c04716a48b7ffeedb070d288 + languageName: node + linkType: hard + +"@hapi/hoek@npm:^11.0.2, @hapi/hoek@npm:^11.0.7": + version: 11.0.7 + resolution: "@hapi/hoek@npm:11.0.7" + checksum: 3da5546649c0fa2bb264ce4b2aea8dc13d6fbdd9179bba553c90ab8fb7bd56413d2237d60c0b97c1e747216597eeb13694a947325bfff62ae3725209ec8bb8c6 + languageName: node + linkType: hard + +"@hapi/pinpoint@npm:^2.0.1": + version: 2.0.1 + resolution: "@hapi/pinpoint@npm:2.0.1" + checksum: 8a1bb399f7cf48948669fdc80dff83892ed56eb62eee2cca128fc8b0683733b16e274b395eddfae6154a698aa57c66a3905ceaa3119c59de29751d4601831057 + languageName: node + linkType: hard + +"@hapi/tlds@npm:^1.1.1": + version: 1.1.4 + resolution: "@hapi/tlds@npm:1.1.4" + checksum: 2bd39d5198155f13cc006638cd52193914d90e1a3afced3b588cbfbd3f8f4cca698d7bb32142debbbc3123950cb80adbb2aea05565025070bd95be61f083e29a + languageName: node + linkType: hard + +"@hapi/topo@npm:^6.0.2": + version: 6.0.2 + resolution: "@hapi/topo@npm:6.0.2" + dependencies: + "@hapi/hoek": ^11.0.2 + checksum: c11da8a995ac66d94dcc8ffe37cc094ace15ab8aec30ba2b556e8318394454cc93967a5bcc6519b846586fea4d347f95d924cb6ce5d51236c199c5e677b9575c + languageName: node + linkType: hard + +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 611e0545146f55ddfdd5c20239cfb7911f9d0e28258787c4fc1a1f6214250830c9367aaaeace0096ed90b6739bee1e9c52ad5ba8adaf74ab8b449119303babfe + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.7 + resolution: "@humanfs/node@npm:0.16.7" dependencies: - "@hapi/hoek": ^9.0.0 - checksum: 604dfd5dde76d5c334bd03f9001fce69c7ce529883acf92da96f4fe7e51221bf5e5110e964caca287a6a616ba027c071748ab636ff178ad750547fba611d6014 + "@humanfs/core": ^0.19.1 + "@humanwhocodes/retry": ^0.4.0 + checksum: 7d2a396a94d80158ce320c0fd7df9aebb82edb8b667e5aaf8f87f4ca50518d0941ca494e0cd68e06b061e777ce5f7d26c45f93ac3fa9f7b11fd1ff26e3cd1440 + languageName: node + linkType: hard + +"@humanwhocodes/module-importer@npm:^1.0.1": + version: 1.0.1 + resolution: "@humanwhocodes/module-importer@npm:1.0.1" + checksum: 0fd22007db8034a2cdf2c764b140d37d9020bbfce8a49d3ec5c05290e77d4b0263b1b972b752df8c89e5eaa94073408f2b7d977aed131faf6cf396ebb5d7fb61 + languageName: node + linkType: hard + +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: d423455b9d53cf01f778603404512a4246fb19b83e74fe3e28c70d9a80e9d4ae147d2411628907ca983e91a855a52535859a8bb218050bc3f6dbd7a553b7b442 languageName: node linkType: hard @@ -3183,430 +3275,193 @@ __metadata: languageName: node linkType: hard -"@langchain/community@npm:^0.0.54": - version: 0.0.54 - resolution: "@langchain/community@npm:0.0.54" +"@langchain/classic@npm:1.0.18": + version: 1.0.18 + resolution: "@langchain/classic@npm:1.0.18" dependencies: - "@langchain/core": ~0.1.60 - "@langchain/openai": ~0.0.28 - expr-eval: ^2.0.2 - flat: ^5.0.2 - langsmith: ~0.1.1 - uuid: ^9.0.0 - zod: ^3.22.3 - zod-to-json-schema: ^3.22.5 - peerDependencies: - "@aws-crypto/sha256-js": ^5.0.0 - "@aws-sdk/client-bedrock-agent-runtime": ^3.485.0 - "@aws-sdk/client-bedrock-runtime": ^3.422.0 - "@aws-sdk/client-dynamodb": ^3.310.0 - "@aws-sdk/client-kendra": ^3.352.0 - "@aws-sdk/client-lambda": ^3.310.0 - "@aws-sdk/client-sagemaker-runtime": ^3.310.0 - "@aws-sdk/client-sfn": ^3.310.0 - "@aws-sdk/credential-provider-node": ^3.388.0 - "@azure/search-documents": ^12.0.0 - "@clickhouse/client": ^0.2.5 - "@cloudflare/ai": "*" - "@datastax/astra-db-ts": ^1.0.0 - "@elastic/elasticsearch": ^8.4.0 - "@getmetal/metal-sdk": "*" - "@getzep/zep-js": ^0.9.0 - "@gomomento/sdk": ^1.51.1 - "@gomomento/sdk-core": ^1.51.1 - "@google-ai/generativelanguage": ^0.2.1 - "@gradientai/nodejs-sdk": ^1.2.0 - "@huggingface/inference": ^2.6.4 - "@mozilla/readability": "*" - "@neondatabase/serverless": "*" - "@opensearch-project/opensearch": "*" - "@pinecone-database/pinecone": "*" - "@planetscale/database": ^1.8.0 - "@premai/prem-sdk": ^0.3.25 - "@qdrant/js-client-rest": ^1.8.2 - "@raycast/api": ^1.55.2 - "@rockset/client": ^0.9.1 - "@smithy/eventstream-codec": ^2.0.5 - "@smithy/protocol-http": ^3.0.6 - "@smithy/signature-v4": ^2.0.10 - "@smithy/util-utf8": ^2.0.0 - "@supabase/postgrest-js": ^1.1.1 - "@supabase/supabase-js": ^2.10.0 - "@tensorflow-models/universal-sentence-encoder": "*" - "@tensorflow/tfjs-converter": "*" - "@tensorflow/tfjs-core": "*" - "@upstash/redis": ^1.20.6 - "@upstash/vector": ^1.0.7 - "@vercel/kv": ^0.2.3 - "@vercel/postgres": ^0.5.0 - "@writerai/writer-sdk": ^0.40.2 - "@xata.io/client": ^0.28.0 - "@xenova/transformers": ^2.5.4 - "@zilliz/milvus2-sdk-node": ">=2.2.7" - better-sqlite3: ^9.4.0 - cassandra-driver: ^4.7.2 - cborg: ^4.1.1 - chromadb: "*" - closevector-common: 0.1.3 - closevector-node: 0.1.6 - closevector-web: 0.1.6 - cohere-ai: "*" - convex: ^1.3.1 - couchbase: ^4.3.0 - discord.js: ^14.14.1 - dria: ^0.0.3 - duck-duck-scrape: ^2.2.5 - faiss-node: ^0.5.1 - firebase-admin: ^11.9.0 || ^12.0.0 - google-auth-library: ^8.9.0 - googleapis: ^126.0.1 - hnswlib-node: ^3.0.0 - html-to-text: ^9.0.5 - interface-datastore: ^8.2.11 - ioredis: ^5.3.2 - it-all: ^3.0.4 - jsdom: "*" - jsonwebtoken: ^9.0.2 - llmonitor: ^0.5.9 - lodash: ^4.17.21 - lunary: ^0.6.11 - mongodb: ">=5.2.0" - mysql2: ^3.3.3 - neo4j-driver: "*" - node-llama-cpp: "*" - pg: ^8.11.0 - pg-copy-streams: ^6.0.5 - pickleparser: ^0.2.1 - portkey-ai: ^0.1.11 - redis: "*" - replicate: ^0.18.0 - typeorm: ^0.3.12 - typesense: ^1.5.3 - usearch: ^1.1.1 - vectordb: ^0.1.4 - voy-search: 0.6.2 - weaviate-ts-client: "*" - web-auth-library: ^1.0.3 - ws: ^8.14.2 - peerDependenciesMeta: - "@aws-crypto/sha256-js": - optional: true - "@aws-sdk/client-bedrock-agent-runtime": - optional: true - "@aws-sdk/client-bedrock-runtime": - optional: true - "@aws-sdk/client-dynamodb": - optional: true - "@aws-sdk/client-kendra": - optional: true - "@aws-sdk/client-lambda": - optional: true - "@aws-sdk/client-sagemaker-runtime": - optional: true - "@aws-sdk/client-sfn": - optional: true - "@aws-sdk/credential-provider-node": - optional: true - "@azure/search-documents": - optional: true - "@clickhouse/client": - optional: true - "@cloudflare/ai": - optional: true - "@datastax/astra-db-ts": - optional: true - "@elastic/elasticsearch": - optional: true - "@getmetal/metal-sdk": - optional: true - "@getzep/zep-js": - optional: true - "@gomomento/sdk": - optional: true - "@gomomento/sdk-core": - optional: true - "@google-ai/generativelanguage": - optional: true - "@gradientai/nodejs-sdk": - optional: true - "@huggingface/inference": - optional: true - "@mozilla/readability": - optional: true - "@neondatabase/serverless": - optional: true - "@opensearch-project/opensearch": - optional: true - "@pinecone-database/pinecone": - optional: true - "@planetscale/database": - optional: true - "@premai/prem-sdk": - optional: true - "@qdrant/js-client-rest": - optional: true - "@raycast/api": - optional: true - "@rockset/client": - optional: true - "@smithy/eventstream-codec": - optional: true - "@smithy/protocol-http": - optional: true - "@smithy/signature-v4": - optional: true - "@smithy/util-utf8": - optional: true - "@supabase/postgrest-js": - optional: true - "@supabase/supabase-js": - optional: true - "@tensorflow-models/universal-sentence-encoder": - optional: true - "@tensorflow/tfjs-converter": - optional: true - "@tensorflow/tfjs-core": - optional: true - "@upstash/redis": - optional: true - "@upstash/vector": - optional: true - "@vercel/kv": - optional: true - "@vercel/postgres": - optional: true - "@writerai/writer-sdk": - optional: true - "@xata.io/client": - optional: true - "@xenova/transformers": - optional: true - "@zilliz/milvus2-sdk-node": - optional: true - better-sqlite3: - optional: true - cassandra-driver: - optional: true - cborg: - optional: true - chromadb: - optional: true - closevector-common: - optional: true - closevector-node: - optional: true - closevector-web: - optional: true - cohere-ai: - optional: true - convex: - optional: true - couchbase: - optional: true - discord.js: - optional: true - dria: - optional: true - duck-duck-scrape: - optional: true - faiss-node: - optional: true - firebase-admin: - optional: true - google-auth-library: - optional: true - googleapis: - optional: true - hnswlib-node: - optional: true - html-to-text: - optional: true - interface-datastore: - optional: true - ioredis: - optional: true - it-all: - optional: true - jsdom: - optional: true - jsonwebtoken: - optional: true - llmonitor: - optional: true - lodash: - optional: true - lunary: - optional: true - mongodb: - optional: true - mysql2: - optional: true - neo4j-driver: - optional: true - node-llama-cpp: - optional: true - pg: - optional: true - pg-copy-streams: - optional: true - pickleparser: - optional: true - portkey-ai: + "@langchain/openai": 1.2.8 + "@langchain/textsplitters": 1.0.1 + handlebars: ^4.7.8 + js-yaml: ^4.1.1 + jsonpointer: ^5.0.1 + langsmith: ">=0.4.0 <1.0.0" + openapi-types: ^12.1.3 + uuid: ^10.0.0 + yaml: ^2.2.1 + zod: ^3.25.76 || ^4 + peerDependencies: + "@langchain/core": ^1.0.0 + cheerio: "*" + peggy: ^3.0.2 + typeorm: "*" + dependenciesMeta: + langsmith: optional: true - redis: + peerDependenciesMeta: + cheerio: optional: true - replicate: + peggy: optional: true typeorm: optional: true - typesense: - optional: true - usearch: - optional: true - vectordb: - optional: true - voy-search: - optional: true - weaviate-ts-client: - optional: true - web-auth-library: - optional: true - ws: - optional: true - checksum: bfc01f4dc0e7caf1a475672ee34a749b1d82de347317883a8436850e9dfc55dd6a82d72c392715527276401e622c5d3879b172af8f91ff7ff81e736901dc525c + checksum: 0141934d90f8c89fc9fce0ba83c2453979bf741961800cb85d21412e39337426751f85a2be4ee10bb4e492400d06e459dbadbdab67b9aa9606925208a2e2ad94 languageName: node linkType: hard -"@langchain/community@npm:~0.0.47": - version: 0.0.57 - resolution: "@langchain/community@npm:0.0.57" +"@langchain/community@npm:1.1.16": + version: 1.1.16 + resolution: "@langchain/community@npm:1.1.16" dependencies: - "@langchain/core": ~0.1.60 - "@langchain/openai": ~0.0.28 - expr-eval: ^2.0.2 + "@langchain/classic": 1.0.18 + "@langchain/openai": 1.2.8 + binary-extensions: ^2.2.0 flat: ^5.0.2 - langsmith: ~0.1.1 - uuid: ^9.0.0 - zod: ^3.22.3 - zod-to-json-schema: ^3.22.5 + js-yaml: ^4.1.1 + math-expression-evaluator: ^2.0.0 + uuid: ^10.0.0 + zod: ^3.25.76 || ^4 peerDependencies: + "@arcjet/redact": ^v1.1.0 "@aws-crypto/sha256-js": ^5.0.0 - "@aws-sdk/client-bedrock-agent-runtime": ^3.485.0 - "@aws-sdk/client-bedrock-runtime": ^3.422.0 - "@aws-sdk/client-dynamodb": ^3.310.0 - "@aws-sdk/client-kendra": ^3.352.0 - "@aws-sdk/client-lambda": ^3.310.0 - "@aws-sdk/client-sagemaker-runtime": ^3.310.0 - "@aws-sdk/client-sfn": ^3.310.0 + "@aws-sdk/client-dynamodb": ^3.991.0 + "@aws-sdk/client-lambda": ^3.991.0 + "@aws-sdk/client-s3": ^3.991.0 + "@aws-sdk/client-sagemaker-runtime": ^3.991.0 + "@aws-sdk/client-sfn": ^3.991.0 "@aws-sdk/credential-provider-node": ^3.388.0 - "@azure/search-documents": ^12.0.0 + "@azure/search-documents": ^12.2.0 + "@azure/storage-blob": ^12.31.0 + "@browserbasehq/sdk": "*" + "@browserbasehq/stagehand": ^1.0.0 "@clickhouse/client": ^0.2.5 - "@cloudflare/ai": "*" "@datastax/astra-db-ts": ^1.0.0 "@elastic/elasticsearch": ^8.4.0 "@getmetal/metal-sdk": "*" - "@getzep/zep-js": ^0.9.0 - "@gomomento/sdk": ^1.51.1 - "@gomomento/sdk-core": ^1.51.1 - "@google-ai/generativelanguage": ^0.2.1 + "@getzep/zep-cloud": ^1.0.6 + "@getzep/zep-js": ^2.0.2 + "@gomomento/sdk-core": ^1.117.2 + "@google-cloud/storage": ^6.10.1 || ^7.7.0 "@gradientai/nodejs-sdk": ^1.2.0 - "@huggingface/inference": ^2.6.4 - "@mlc-ai/web-llm": ^0.2.35 + "@huggingface/inference": ^4.13.12 + "@huggingface/transformers": ^3.8.1 + "@ibm-cloud/watsonx-ai": "*" + "@lancedb/lancedb": ^0.19.1 + "@langchain/core": ^1.1.25 + "@layerup/layerup-security": ^1.5.12 + "@libsql/client": ^0.17.0 + "@mendable/firecrawl-js": ^4.13.0 + "@mlc-ai/web-llm": "*" "@mozilla/readability": "*" "@neondatabase/serverless": "*" + "@notionhq/client": ^5.9.0 "@opensearch-project/opensearch": "*" - "@pinecone-database/pinecone": "*" "@planetscale/database": ^1.8.0 "@premai/prem-sdk": ^0.3.25 - "@qdrant/js-client-rest": ^1.8.2 "@raycast/api": ^1.55.2 "@rockset/client": ^0.9.1 - "@smithy/eventstream-codec": ^2.0.5 - "@smithy/protocol-http": ^3.0.6 - "@smithy/signature-v4": ^2.0.10 - "@smithy/util-utf8": ^2.0.0 - "@supabase/postgrest-js": ^1.1.1 - "@supabase/supabase-js": ^2.10.0 + "@smithy/eventstream-codec": ^4.2.8 + "@smithy/protocol-http": ^5.3.8 + "@smithy/signature-v4": ^5.3.8 + "@smithy/util-utf8": ^4.2.0 + "@spider-cloud/spider-client": ^0.1.85 + "@supabase/supabase-js": ^2.45.0 "@tensorflow-models/universal-sentence-encoder": "*" - "@tensorflow/tfjs-converter": "*" "@tensorflow/tfjs-core": "*" + "@upstash/ratelimit": ^1.1.3 || ^2.0.3 "@upstash/redis": ^1.20.6 - "@upstash/vector": ^1.0.7 - "@vercel/kv": ^0.2.3 - "@vercel/postgres": ^0.5.0 - "@writerai/writer-sdk": ^0.40.2 + "@upstash/vector": ^1.1.1 + "@vercel/kv": "*" + "@vercel/postgres": "*" + "@writerai/writer-sdk": ^3.6.0 "@xata.io/client": ^0.28.0 - "@xenova/transformers": ^2.5.4 - "@zilliz/milvus2-sdk-node": ">=2.2.7" - better-sqlite3: ^9.4.0 + "@zilliz/milvus2-sdk-node": ">=2.3.5" + apify-client: ^2.22.1 + assemblyai: ^4.23.0 + azion: ^3.1.1 + better-sqlite3: ">=9.4.0 <12.0.0" cassandra-driver: ^4.7.2 - cborg: ^4.1.1 + cborg: ^4.5.8 + cheerio: ^1.2.0 chromadb: "*" closevector-common: 0.1.3 closevector-node: 0.1.6 closevector-web: 0.1.6 - cohere-ai: "*" convex: ^1.3.1 - couchbase: ^4.3.0 - discord.js: ^14.14.1 - dria: ^0.0.3 + couchbase: ^4.6.0 + crypto-js: ^4.2.0 + d3-dsv: ^3.0.1 + discord.js: ^14.25.1 duck-duck-scrape: ^2.2.5 - faiss-node: ^0.5.1 - firebase-admin: ^11.9.0 || ^12.0.0 - google-auth-library: ^8.9.0 - googleapis: ^126.0.1 + epub2: ^3.0.1 + faiss-node: "*" + fast-xml-parser: "*" + firebase-admin: ^13.6.1 + google-auth-library: "*" + googleapis: "*" hnswlib-node: ^3.0.0 html-to-text: ^9.0.5 - interface-datastore: ^8.2.11 + ibm-cloud-sdk-core: "*" + ignore: ^7.0.5 + interface-datastore: ^9.0.2 ioredis: ^5.3.2 it-all: ^3.0.4 jsdom: "*" - jsonwebtoken: ^9.0.2 - llmonitor: ^0.5.9 - lodash: ^4.17.21 - lunary: ^0.6.11 - mongodb: ">=5.2.0" - mysql2: ^3.3.3 + jsonwebtoken: ^9.0.3 + lodash: ^4.17.23 + lunary: ^0.7.10 + mammoth: ^1.11.0 + mariadb: ^3.4.0 + mem0ai: ^2.1.8 + mysql2: ^3.17.2 neo4j-driver: "*" - node-llama-cpp: "*" + node-llama-cpp: ">=3.0.0" + notion-to-md: ^3.1.0 + officeparser: ^6.0.4 + openai: "*" + pdf-parse: 2.4.5 pg: ^8.11.0 - pg-copy-streams: ^6.0.5 + pg-copy-streams: ^7.0.0 pickleparser: ^0.2.1 - portkey-ai: ^0.1.11 - redis: "*" - replicate: ^0.18.0 - typeorm: ^0.3.12 - typesense: ^1.5.3 + playwright: ^1.58.2 + portkey-ai: ^3.0.1 + puppeteer: "*" + pyodide: ">=0.24.1 <0.27.0" + replicate: "*" + sonix-speech-recognition: ^2.1.1 + srt-parser-2: ^1.2.3 + typeorm: ^0.3.28 + typesense: ^3.0.1 usearch: ^1.1.1 - vectordb: ^0.1.4 voy-search: 0.6.2 - weaviate-ts-client: "*" - web-auth-library: ^1.0.3 + word-extractor: "*" ws: ^8.14.2 + youtubei.js: "*" peerDependenciesMeta: - "@aws-crypto/sha256-js": - optional: true - "@aws-sdk/client-bedrock-agent-runtime": + "@arcjet/redact": optional: true - "@aws-sdk/client-bedrock-runtime": + "@aws-crypto/sha256-js": optional: true "@aws-sdk/client-dynamodb": optional: true - "@aws-sdk/client-kendra": - optional: true "@aws-sdk/client-lambda": optional: true + "@aws-sdk/client-s3": + optional: true "@aws-sdk/client-sagemaker-runtime": optional: true "@aws-sdk/client-sfn": optional: true "@aws-sdk/credential-provider-node": optional: true + "@aws-sdk/dsql-signer": + optional: true "@azure/search-documents": optional: true - "@clickhouse/client": + "@azure/storage-blob": + optional: true + "@browserbasehq/sdk": optional: true - "@cloudflare/ai": + "@clickhouse/client": optional: true "@datastax/astra-db-ts": optional: true @@ -3614,24 +3469,36 @@ __metadata: optional: true "@getmetal/metal-sdk": optional: true - "@getzep/zep-js": + "@getzep/zep-cloud": optional: true - "@gomomento/sdk": + "@getzep/zep-js": optional: true "@gomomento/sdk-core": optional: true - "@google-ai/generativelanguage": + "@google-cloud/storage": optional: true "@gradientai/nodejs-sdk": optional: true "@huggingface/inference": optional: true + "@huggingface/transformers": + optional: true + "@lancedb/lancedb": + optional: true + "@layerup/layerup-security": + optional: true + "@libsql/client": + optional: true + "@mendable/firecrawl-js": + optional: true "@mlc-ai/web-llm": optional: true "@mozilla/readability": optional: true "@neondatabase/serverless": optional: true + "@notionhq/client": + optional: true "@opensearch-project/opensearch": optional: true "@pinecone-database/pinecone": @@ -3654,16 +3521,16 @@ __metadata: optional: true "@smithy/util-utf8": optional: true - "@supabase/postgrest-js": + "@spider-cloud/spider-client": optional: true "@supabase/supabase-js": optional: true "@tensorflow-models/universal-sentence-encoder": optional: true - "@tensorflow/tfjs-converter": - optional: true "@tensorflow/tfjs-core": optional: true + "@upstash/ratelimit": + optional: true "@upstash/redis": optional: true "@upstash/vector": @@ -3680,12 +3547,20 @@ __metadata: optional: true "@zilliz/milvus2-sdk-node": optional: true + apify-client: + optional: true + assemblyai: + optional: true + azion: + optional: true better-sqlite3: optional: true cassandra-driver: optional: true cborg: optional: true + cheerio: + optional: true chromadb: optional: true closevector-common: @@ -3700,14 +3575,20 @@ __metadata: optional: true couchbase: optional: true - discord.js: + crypto-js: + optional: true + d3-dsv: optional: true - dria: + discord.js: optional: true duck-duck-scrape: optional: true + epub2: + optional: true faiss-node: optional: true + fast-xml-parser: + optional: true firebase-admin: optional: true google-auth-library: @@ -3718,6 +3599,8 @@ __metadata: optional: true html-to-text: optional: true + ignore: + optional: true interface-datastore: optional: true ioredis: @@ -3728,12 +3611,16 @@ __metadata: optional: true jsonwebtoken: optional: true - llmonitor: - optional: true lodash: optional: true lunary: optional: true + mammoth: + optional: true + mariadb: + optional: true + mem0ai: + optional: true mongodb: optional: true mysql2: @@ -3742,110 +3629,146 @@ __metadata: optional: true node-llama-cpp: optional: true + notion-to-md: + optional: true + officeparser: + optional: true + pdf-parse: + optional: true pg: optional: true pg-copy-streams: optional: true pickleparser: optional: true + playwright: + optional: true portkey-ai: optional: true + puppeteer: + optional: true + pyodide: + optional: true redis: optional: true replicate: optional: true + sonix-speech-recognition: + optional: true + srt-parser-2: + optional: true typeorm: optional: true typesense: optional: true usearch: optional: true - vectordb: - optional: true voy-search: optional: true - weaviate-ts-client: + weaviate-client: optional: true - web-auth-library: + word-extractor: optional: true ws: optional: true - checksum: 9a9f8396af0571bad920c0871329953f74a2907c5cdbab544f94269db2eff3de475c07e25c69dcf1e3b014f21ab9fc9f3cf79717ed44a7878bf780e58f11da87 + youtubei.js: + optional: true + checksum: de64f5f81cd8ee7533cb81ef25601e1dac1b36b4d2a7403dee7022733f18187c555f68a0f6ebce8cc947afd2d41e47b16318c71f0ddc776fbcb4c4cd5a33abdf languageName: node linkType: hard -"@langchain/core@npm:>0.1.56 <0.3.0, @langchain/core@npm:>0.2.0 <0.3.0": - version: 0.2.36 - resolution: "@langchain/core@npm:0.2.36" +"@langchain/core@npm:1.1.26": + version: 1.1.26 + resolution: "@langchain/core@npm:1.1.26" dependencies: - ansi-styles: ^5.0.0 + "@cfworker/json-schema": ^4.0.2 + ansi-styles: ^6.2.3 camelcase: 6 decamelize: 1.2.0 js-tiktoken: ^1.0.12 - langsmith: ^0.1.56-rc.1 + langsmith: ">=0.5.0 <1.0.0" mustache: ^4.2.0 p-queue: ^6.6.2 - p-retry: 4 uuid: ^10.0.0 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: 963aca44a8422dbbe8259d063e65867bc1ad0735e805c7b46048d48e12822ae669f64f46f077a31dd5a9aa0c43b2f1ed7a5e57369392902e8d419dd9fa3611f3 + zod: ^3.25.76 || ^4 + checksum: ff4a294e2645a60afe09948046690b6a19992a20b09791dd27025b02387d18df3eeeab393b59b5dd82821734152854563c36b6334d475ac7d2a54fad6f4448da languageName: node linkType: hard -"@langchain/core@npm:^0.1.63, @langchain/core@npm:~0.1.56, @langchain/core@npm:~0.1.60": - version: 0.1.63 - resolution: "@langchain/core@npm:0.1.63" +"@langchain/langgraph-checkpoint@npm:^1.0.0": + version: 1.0.0 + resolution: "@langchain/langgraph-checkpoint@npm:1.0.0" dependencies: - ansi-styles: ^5.0.0 - camelcase: 6 - decamelize: 1.2.0 - js-tiktoken: ^1.0.12 - langsmith: ~0.1.7 - ml-distance: ^4.0.0 - mustache: ^4.2.0 - p-queue: ^6.6.2 - p-retry: 4 - uuid: ^9.0.0 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: 0aa18f55e5e5cab2c609a6c0d37bcfe9a36b574658acaeb7857f6a0e23dd9ad5617d40988a48f6f00c461a33a5abee983d2ed8b311b92c60b641002b42f0ab26 + uuid: ^10.0.0 + peerDependencies: + "@langchain/core": ^1.0.1 + checksum: 0e85633bd7975230b61ba709e30da7b74e542efe4b37f105b17ef0e1dde658d24851fa37d0850442e8aa934c6bf0cb3e85cfa98d9064e406ff9f3888ebfc3505 + languageName: node + linkType: hard + +"@langchain/langgraph-sdk@npm:~2.0.0": + version: 2.0.0 + resolution: "@langchain/langgraph-sdk@npm:2.0.0" + dependencies: + "@types/json-schema": ^7.0.15 + p-queue: ^9.0.1 + p-retry: ^7.1.1 + uuid: ^13.0.0 + peerDependencies: + "@langchain/core": ^1.1.16 + react: ^18 || ^19 + react-dom: ^18 || ^19 + peerDependenciesMeta: + "@langchain/core": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 17b73bdb256967d8fbd0854f83e209bc85774488fcd8e1d3df1978b517495ff2414f24b1b4b3e629bd0e93fa5447da95ea5b67bb6386d4f2464c880ef07bbada languageName: node linkType: hard -"@langchain/openai@npm:^0.0.28": - version: 0.0.28 - resolution: "@langchain/openai@npm:0.0.28" +"@langchain/langgraph@npm:^1.1.2, @langchain/langgraph@npm:^1.1.5": + version: 1.1.5 + resolution: "@langchain/langgraph@npm:1.1.5" dependencies: - "@langchain/core": ~0.1.56 - js-tiktoken: ^1.0.7 - openai: ^4.32.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: ba7c8e4e57fd76c9653b9447e294635774e9aa5cda6107ccf28b06499f7464d4df167f878ccdf98006e2b3af531cd2c663df4d01b107ef0871de183f18f29b5d + "@langchain/langgraph-checkpoint": ^1.0.0 + "@langchain/langgraph-sdk": ~2.0.0 + "@standard-schema/spec": 1.1.0 + uuid: ^10.0.0 + peerDependencies: + "@langchain/core": ^1.1.16 + zod: ^3.25.32 || ^4.2.0 + zod-to-json-schema: ^3.x + peerDependenciesMeta: + zod-to-json-schema: + optional: true + checksum: b1b9c848d78b7c1314ea5894f15a462f566cc8d9530477cd23ecc256fef4540a8008948635f994b3dfac96d34efdf63db44d48d8e73c4f29a87e9ce92822a37f languageName: node linkType: hard -"@langchain/openai@npm:~0.0.28": - version: 0.0.34 - resolution: "@langchain/openai@npm:0.0.34" +"@langchain/openai@npm:1.2.8": + version: 1.2.8 + resolution: "@langchain/openai@npm:1.2.8" dependencies: - "@langchain/core": ">0.1.56 <0.3.0" js-tiktoken: ^1.0.12 - openai: ^4.41.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - checksum: d2a5568b7fd0507af2510d68cf568945d5c7f6e45f57ac1178f65d0092e0437e7c908e2af3e20b829ce4507f63e71ddc38df7dc35b7e3d21394daa380243e297 + openai: ^6.18.0 + zod: ^3.25.76 || ^4 + peerDependencies: + "@langchain/core": ^1.0.0 + checksum: 0f5de0f6a77fd2afa9d6d601123a6d21b7cd09eacab50da2085d1adfe416dfa5a832f5f023a59132423f4c180bb3f88aed61f47309f752c430bf7073999e06e7 languageName: node linkType: hard -"@langchain/textsplitters@npm:~0.0.0": - version: 0.0.3 - resolution: "@langchain/textsplitters@npm:0.0.3" +"@langchain/textsplitters@npm:1.0.1": + version: 1.0.1 + resolution: "@langchain/textsplitters@npm:1.0.1" dependencies: - "@langchain/core": ">0.2.0 <0.3.0" js-tiktoken: ^1.0.12 - checksum: f0b32d65c863a280ce7104bff4d367734b8f76f2ec42b741fb690fbc20737bb4a3a412b82d8ba308a524441b6084ecd59cf61c3ce13cbb9639fbd02241c341d1 + peerDependencies: + "@langchain/core": ^1.0.0 + checksum: 1a553ee321d6638213328f69927e9883b5c29d26b91d011a233097c579f8347dd0e57dde939def43430835fe516b49c9a26ee5d98f9f285ee5e5861495bec974 languageName: node linkType: hard @@ -4563,27 +4486,25 @@ __metadata: languageName: node linkType: hard -"@nestjs/common@npm:^9.2.0": - version: 9.4.3 - resolution: "@nestjs/common@npm:9.4.3" +"@nestjs/common@npm:10.4.22": + version: 10.4.22 + resolution: "@nestjs/common@npm:10.4.22" dependencies: + file-type: 20.4.1 iterare: 1.2.1 - tslib: 2.5.3 + tslib: 2.8.1 uid: 2.0.2 peerDependencies: - cache-manager: <=5 class-transformer: "*" class-validator: "*" - reflect-metadata: ^0.1.12 + reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: - cache-manager: - optional: true class-transformer: optional: true class-validator: optional: true - checksum: 45ccb5acac2521a05576f37b37ab94c63a83aefb1839ed670635090bb934c1bd532852cb7be9863ee3b9d1ad80aa84e0602be023c626623f9233257dcb754914 + checksum: f78006a582b45d3e9de61211e1c67f072e23356268bc963b8f8438151cb8027afe5e76d1109a7f36e95b6f0bcd4b618453ae2924e395f2bfcfeb2cc91afc10d2 languageName: node linkType: hard @@ -7631,29 +7552,6 @@ __metadata: languageName: node linkType: hard -"@sideway/address@npm:^4.1.5": - version: 4.1.5 - resolution: "@sideway/address@npm:4.1.5" - dependencies: - "@hapi/hoek": ^9.0.0 - checksum: 3e3ea0f00b4765d86509282290368a4a5fd39a7995fdc6de42116ca19a96120858e56c2c995081def06e1c53e1f8bccc7d013f6326602bec9d56b72ee2772b9d - languageName: node - linkType: hard - -"@sideway/formula@npm:^3.0.1": - version: 3.0.1 - resolution: "@sideway/formula@npm:3.0.1" - checksum: e4beeebc9dbe2ff4ef0def15cec0165e00d1612e3d7cea0bc9ce5175c3263fc2c818b679bd558957f49400ee7be9d4e5ac90487e1625b4932e15c4aa7919c57a - languageName: node - linkType: hard - -"@sideway/pinpoint@npm:^2.0.0": - version: 2.0.0 - resolution: "@sideway/pinpoint@npm:2.0.0" - checksum: 0f4491e5897fcf5bf02c46f5c359c56a314e90ba243f42f0c100437935daa2488f20482f0f77186bd6bf43345095a95d8143ecf8b1f4d876a7bc0806aba9c3d2 - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -7686,6 +7584,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/spec@npm:1.1.0, @standard-schema/spec@npm:^1.0.0": + version: 1.1.0 + resolution: "@standard-schema/spec@npm:1.1.0" + checksum: 6245ebef5e698bb04752a22e996a7cc40406a404d9f68a9d4e1a7a10f2422da287247508e7b495a2f32bb38f3d57b4daf2c9ab4bf22d9bca13e20a3dc5ec575e + languageName: node + linkType: hard + "@storybook/addon-actions@npm:7.6.20, @storybook/addon-actions@npm:^7.4.5": version: 7.6.20 resolution: "@storybook/addon-actions@npm:7.6.20" @@ -7885,14 +7790,14 @@ __metadata: languageName: node linkType: hard -"@storybook/builder-manager@npm:7.6.20": - version: 7.6.20 - resolution: "@storybook/builder-manager@npm:7.6.20" +"@storybook/builder-manager@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/builder-manager@npm:7.6.21" dependencies: "@fal-works/esbuild-plugin-global-externals": ^2.1.2 - "@storybook/core-common": 7.6.20 - "@storybook/manager": 7.6.20 - "@storybook/node-logger": 7.6.20 + "@storybook/core-common": 7.6.21 + "@storybook/manager": 7.6.21 + "@storybook/node-logger": 7.6.21 "@types/ejs": ^3.1.1 "@types/find-cache-dir": ^3.2.1 "@yarnpkg/esbuild-plugin-pnp": ^3.0.0-rc.10 @@ -7905,7 +7810,7 @@ __metadata: fs-extra: ^11.1.0 process: ^0.11.10 util: ^0.12.4 - checksum: 3a0129e8e29d85c2e6cb4ec3c832e59a0d66075b8a48cab30bc3aa997ac019fc4fecaca5c12ad0758a66fd6dfc52c3e993cb15911ac949fae74ec21dcbc3f84e + checksum: 07bc0afc6a15ab5f9ff4ed16b5d535d49283751527b16fcc025d49938ad3746fa604ee923e9470e30f5358e47ed5d7559a4131eddd2aabafb0d28996976347b2 languageName: node linkType: hard @@ -7972,22 +7877,36 @@ __metadata: languageName: node linkType: hard -"@storybook/cli@npm:7.6.20": - version: 7.6.20 - resolution: "@storybook/cli@npm:7.6.20" +"@storybook/channels@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/channels@npm:7.6.21" + dependencies: + "@storybook/client-logger": 7.6.21 + "@storybook/core-events": 7.6.21 + "@storybook/global": ^5.0.0 + qs: ^6.10.0 + telejson: ^7.2.0 + tiny-invariant: ^1.3.1 + checksum: 0d08c340b3621b275028bdf1d901b61f5729fb78da18a2cde1440939133bdd78718f3b615361bf759a4a3929c4a9e8c711780cd162ed936f74f8f960c13de2d1 + languageName: node + linkType: hard + +"@storybook/cli@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/cli@npm:7.6.21" dependencies: "@babel/core": ^7.23.2 "@babel/preset-env": ^7.23.2 "@babel/types": ^7.23.0 "@ndelangen/get-tarball": ^3.0.7 - "@storybook/codemod": 7.6.20 - "@storybook/core-common": 7.6.20 - "@storybook/core-events": 7.6.20 - "@storybook/core-server": 7.6.20 - "@storybook/csf-tools": 7.6.20 - "@storybook/node-logger": 7.6.20 - "@storybook/telemetry": 7.6.20 - "@storybook/types": 7.6.20 + "@storybook/codemod": 7.6.21 + "@storybook/core-common": 7.6.21 + "@storybook/core-events": 7.6.21 + "@storybook/core-server": 7.6.21 + "@storybook/csf-tools": 7.6.21 + "@storybook/node-logger": 7.6.21 + "@storybook/telemetry": 7.6.21 + "@storybook/types": 7.6.21 "@types/semver": ^7.3.4 "@yarnpkg/fslib": 2.10.3 "@yarnpkg/libzip": 2.3.0 @@ -8019,7 +7938,7 @@ __metadata: bin: getstorybook: ./bin/index.js sb: ./bin/index.js - checksum: a3e0334c9521ae78783de3f161e9a05def1fa0b4fbf38818b1cd5ae5083e0bf050171ba16b786168ea86af5bd0e57d156213527654cdfc649b3fb4fcffeda052 + checksum: 2e06c6edb17137440bc19a5590b0e1fdd45928540239daec663b0d140ff1476b2cacb38f54a88c1fb21ad01c8a08dab51394483edcf4284b23674e8cee662beb languageName: node linkType: hard @@ -8032,17 +7951,26 @@ __metadata: languageName: node linkType: hard -"@storybook/codemod@npm:7.6.20": - version: 7.6.20 - resolution: "@storybook/codemod@npm:7.6.20" +"@storybook/client-logger@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/client-logger@npm:7.6.21" + dependencies: + "@storybook/global": ^5.0.0 + checksum: 0b2cf1fd27c4bb66a67568f25c2cb448d675ae61993af3ad7be95b88bb8f4be316f899ab0c57f7219c4ab008dbb237d95f545cc11b696c6fd94d8a54ff75dfb6 + languageName: node + linkType: hard + +"@storybook/codemod@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/codemod@npm:7.6.21" dependencies: "@babel/core": ^7.23.2 "@babel/preset-env": ^7.23.2 "@babel/types": ^7.23.0 "@storybook/csf": ^0.1.2 - "@storybook/csf-tools": 7.6.20 - "@storybook/node-logger": 7.6.20 - "@storybook/types": 7.6.20 + "@storybook/csf-tools": 7.6.21 + "@storybook/node-logger": 7.6.21 + "@storybook/types": 7.6.21 "@types/cross-spawn": ^6.0.2 cross-spawn: ^7.0.3 globby: ^11.0.2 @@ -8050,7 +7978,7 @@ __metadata: lodash: ^4.17.21 prettier: ^2.8.0 recast: ^0.23.1 - checksum: e8a507ea29764d0b73ca39046b8f47518ebfed1fcd8e3c572d1cc16d285a60f50d950bc177bbd9215df2a36abcaafb536f228b419218a47720154c4ab27db20b + checksum: 768d44787276d9727186ccca8a07a2658fa2516f44699883a6a23304a1d2600081a6b62fb1841d2276e04a277e1b7fd34655da9e9806f62858375366b60f3b02 languageName: node linkType: hard @@ -8116,6 +8044,37 @@ __metadata: languageName: node linkType: hard +"@storybook/core-common@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/core-common@npm:7.6.21" + dependencies: + "@storybook/core-events": 7.6.21 + "@storybook/node-logger": 7.6.21 + "@storybook/types": 7.6.21 + "@types/find-cache-dir": ^3.2.1 + "@types/node": ^18.0.0 + "@types/node-fetch": ^2.6.4 + "@types/pretty-hrtime": ^1.0.0 + chalk: ^4.1.0 + esbuild: ^0.18.0 + esbuild-register: ^3.5.0 + file-system-cache: 2.3.0 + find-cache-dir: ^3.0.0 + find-up: ^5.0.0 + fs-extra: ^11.1.0 + glob: ^10.0.0 + handlebars: ^4.7.7 + lazy-universal-dotenv: ^4.0.0 + node-fetch: ^2.0.0 + picomatch: ^2.3.0 + pkg-dir: ^5.0.0 + pretty-hrtime: ^1.0.3 + resolve-from: ^5.0.0 + ts-dedent: ^2.0.0 + checksum: 4de8f501cd405c56a0515ec01a3cfcc607da51e357a1fbf6994f3f5317abe977d9c1bcf8d134f47281ef296f4c5baebf84826b2f57304b1e59d2d1360a2fdf85 + languageName: node + linkType: hard + "@storybook/core-events@npm:7.6.20": version: 7.6.20 resolution: "@storybook/core-events@npm:7.6.20" @@ -8125,25 +8084,34 @@ __metadata: languageName: node linkType: hard -"@storybook/core-server@npm:7.6.20": - version: 7.6.20 - resolution: "@storybook/core-server@npm:7.6.20" +"@storybook/core-events@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/core-events@npm:7.6.21" + dependencies: + ts-dedent: ^2.0.0 + checksum: 65db88aff2d792f117c09f18938c934f648169df58a947c03e672ff1e5ae251d0f38564f755c486a6cfbf22e4620bcbbadf487ab0932774a7a633db493f03ee5 + languageName: node + linkType: hard + +"@storybook/core-server@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/core-server@npm:7.6.21" dependencies: "@aw-web-design/x-default-browser": 1.4.126 "@discoveryjs/json-ext": ^0.5.3 - "@storybook/builder-manager": 7.6.20 - "@storybook/channels": 7.6.20 - "@storybook/core-common": 7.6.20 - "@storybook/core-events": 7.6.20 + "@storybook/builder-manager": 7.6.21 + "@storybook/channels": 7.6.21 + "@storybook/core-common": 7.6.21 + "@storybook/core-events": 7.6.21 "@storybook/csf": ^0.1.2 - "@storybook/csf-tools": 7.6.20 + "@storybook/csf-tools": 7.6.21 "@storybook/docs-mdx": ^0.1.0 "@storybook/global": ^5.0.0 - "@storybook/manager": 7.6.20 - "@storybook/node-logger": 7.6.20 - "@storybook/preview-api": 7.6.20 - "@storybook/telemetry": 7.6.20 - "@storybook/types": 7.6.20 + "@storybook/manager": 7.6.21 + "@storybook/node-logger": 7.6.21 + "@storybook/preview-api": 7.6.21 + "@storybook/telemetry": 7.6.21 + "@storybook/types": 7.6.21 "@types/detect-port": ^1.3.0 "@types/node": ^18.0.0 "@types/pretty-hrtime": ^1.0.0 @@ -8169,7 +8137,7 @@ __metadata: util-deprecate: ^1.0.2 watchpack: ^2.2.0 ws: ^8.2.3 - checksum: 85ca3c14f03adc94d64f4769fbc09b1339fa29e89d24667e5be098a0bdd1fc4291c31ecc6078224aa717cd01a3e11e9389cf69ae340d2e609fbb42130ee19aed + checksum: e19862e6dafc17d5e67e12267550a40f93e05a8a51ccec81956054ca3763ae8330b7c2c5c5ec0d17fd6c371f36389e1999aa40b45882b2ff98e1085585a1b5fb languageName: node linkType: hard @@ -8213,6 +8181,23 @@ __metadata: languageName: node linkType: hard +"@storybook/csf-tools@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/csf-tools@npm:7.6.21" + dependencies: + "@babel/generator": ^7.23.0 + "@babel/parser": ^7.23.0 + "@babel/traverse": ^7.23.2 + "@babel/types": ^7.23.0 + "@storybook/csf": ^0.1.2 + "@storybook/types": 7.6.21 + fs-extra: ^11.1.0 + recast: ^0.23.1 + ts-dedent: ^2.0.0 + checksum: 3c7d619a4c98334b125b38f73132f6d5c95adc55ded32b125d3034689caf2696f6d3f0611c92357995c1a0ce3f495b847422122ed6826c1ae5dfc95222249bfe + languageName: node + linkType: hard + "@storybook/csf@npm:^0.0.1": version: 0.0.1 resolution: "@storybook/csf@npm:0.0.1" @@ -8282,10 +8267,10 @@ __metadata: languageName: node linkType: hard -"@storybook/manager@npm:7.6.20": - version: 7.6.20 - resolution: "@storybook/manager@npm:7.6.20" - checksum: 4596149367e9dfb7154cc48ec1552514664628136373c832cb41284c24a143324701249961b716d971b688d00f84393935c1c49a123187a784da5f4ae10ecc41 +"@storybook/manager@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/manager@npm:7.6.21" + checksum: b2f171c3cac534b53e6d3b4ca9f94234392448b8005edc8e6822c3236182262c1a6dd8ba88012cc487c9baccd1664ee64708e8e059fb3b3f7030803f2dd0ca61 languageName: node linkType: hard @@ -8365,6 +8350,13 @@ __metadata: languageName: node linkType: hard +"@storybook/node-logger@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/node-logger@npm:7.6.21" + checksum: 52685ccc58ee641d7592b6d65b0266147af0e0a0f77e8fc275945312e16ed962ea29b2abeecaa895343fa3006b5a7d0346830784de4b0cf82b14768e9ad9f7fb + languageName: node + linkType: hard + "@storybook/postinstall@npm:7.6.20": version: 7.6.20 resolution: "@storybook/postinstall@npm:7.6.20" @@ -8443,6 +8435,28 @@ __metadata: languageName: node linkType: hard +"@storybook/preview-api@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/preview-api@npm:7.6.21" + dependencies: + "@storybook/channels": 7.6.21 + "@storybook/client-logger": 7.6.21 + "@storybook/core-events": 7.6.21 + "@storybook/csf": ^0.1.2 + "@storybook/global": ^5.0.0 + "@storybook/types": 7.6.21 + "@types/qs": ^6.9.5 + dequal: ^2.0.2 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + qs: ^6.10.0 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + checksum: 5e81f22f9d609f62006d85d1dbe0310c150f6510cb801fa7dad8d647bc23e28253ba6f84eb8e5976315abaa3df5046a28f9c6a8394e5d2e165366aed06047dd6 + languageName: node + linkType: hard + "@storybook/preview@npm:7.6.20": version: 7.6.20 resolution: "@storybook/preview@npm:7.6.20" @@ -8525,19 +8539,19 @@ __metadata: languageName: node linkType: hard -"@storybook/telemetry@npm:7.6.20": - version: 7.6.20 - resolution: "@storybook/telemetry@npm:7.6.20" +"@storybook/telemetry@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/telemetry@npm:7.6.21" dependencies: - "@storybook/client-logger": 7.6.20 - "@storybook/core-common": 7.6.20 - "@storybook/csf-tools": 7.6.20 + "@storybook/client-logger": 7.6.21 + "@storybook/core-common": 7.6.21 + "@storybook/csf-tools": 7.6.21 chalk: ^4.1.0 detect-package-manager: ^2.0.1 fetch-retry: ^5.0.2 fs-extra: ^11.1.0 read-pkg-up: ^7.0.1 - checksum: 546f2da99858f995d61943e9c62de87bb95df506c75192bbd357b0174b2c0e23d3a61bc4b6ea1c1c9de99b982b7a7cacc2884758ce91314edf389a466daf1291 + checksum: b51aebeca12836766d7078e2700a93007eab802a35f8d05aa4974517be21b1b515d5f469e3581f470615234cef2d6a4ec2e5c7379475a98a15bcf34ccfaefb5a languageName: node linkType: hard @@ -8579,6 +8593,18 @@ __metadata: languageName: node linkType: hard +"@storybook/types@npm:7.6.21": + version: 7.6.21 + resolution: "@storybook/types@npm:7.6.21" + dependencies: + "@storybook/channels": 7.6.21 + "@types/babel__core": ^7.0.0 + "@types/express": ^4.7.0 + file-system-cache: 2.3.0 + checksum: 95b7d9cc9bf13dbdd0c738e13c3f19558004daa7358e97d7439a28fa6115ec5b002e7ff28f71ff7210ae3d69815b438c0b8315ff7d7f5c7f0734dab337282315 + languageName: node + linkType: hard + "@svgmoji/blob@npm:^3.2.0": version: 3.2.0 resolution: "@svgmoji/blob@npm:3.2.0" @@ -8834,6 +8860,24 @@ __metadata: languageName: node linkType: hard +"@tokenizer/inflate@npm:^0.2.6": + version: 0.2.7 + resolution: "@tokenizer/inflate@npm:0.2.7" + dependencies: + debug: ^4.4.0 + fflate: ^0.8.2 + token-types: ^6.0.0 + checksum: 0c77759d453501aa0509f6c60a3f2ec2ff6db9e33f196d077af719be6c2cc5a6b2c686e425610ed25d6416ddcb7f495a5e9ef689c8df225a80703b03e83aa956 + languageName: node + linkType: hard + +"@tokenizer/token@npm:^0.3.0": + version: 0.3.0 + resolution: "@tokenizer/token@npm:0.3.0" + checksum: 1d575d02d2a9f0c5a4ca5180635ebd2ad59e0f18b42a65f3d04844148b49b3db35cf00b6012a1af2d59c2ab3caca59451c5689f747ba8667ee586ad717ee58e1 + languageName: node + linkType: hard + "@ts-morph/common@npm:~0.19.0": version: 0.19.0 resolution: "@ts-morph/common@npm:0.19.0" @@ -8993,13 +9037,6 @@ __metadata: languageName: node linkType: hard -"@types/diff-match-patch@npm:^1.0.36": - version: 1.0.36 - resolution: "@types/diff-match-patch@npm:1.0.36" - checksum: 7d7ce03422fcc3e79d0cda26e4748aeb176b75ca4b4e5f38459b112bf24660d628424bdb08d330faefa69039d19a5316e7a102a8ab68b8e294c8346790e55113 - languageName: node - linkType: hard - "@types/direction@npm:^1.0.0": version: 1.0.0 resolution: "@types/direction@npm:1.0.0" @@ -9072,7 +9109,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:^1.0.8": +"@types/estree@npm:*, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 @@ -9374,7 +9411,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.0.0, @types/node@npm:^18.11.18": +"@types/node@npm:^18.0.0": version: 18.19.124 resolution: "@types/node@npm:18.19.124" dependencies: @@ -9543,13 +9580,6 @@ __metadata: languageName: node linkType: hard -"@types/retry@npm:0.12.0": - version: 0.12.0 - resolution: "@types/retry@npm:0.12.0" - checksum: 61a072c7639f6e8126588bf1eb1ce8835f2cb9c2aba795c4491cf6310e013267b0c8488039857c261c387e9728c1b43205099223f160bb6a76b4374f741b5603 - languageName: node - linkType: hard - "@types/sax@npm:^1.2.0": version: 1.2.7 resolution: "@types/sax@npm:1.2.7" @@ -9773,23 +9803,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^4.29.0": - version: 4.33.0 - resolution: "@typescript-eslint/parser@npm:4.33.0" - dependencies: - "@typescript-eslint/scope-manager": 4.33.0 - "@typescript-eslint/types": 4.33.0 - "@typescript-eslint/typescript-estree": 4.33.0 - debug: ^4.3.1 - peerDependencies: - eslint: ^5.0.0 || ^6.0.0 || ^7.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 102457eae1acd516211098fea081c8a2ed728522bbda7f5a557b6ef23d88970514f9a0f6285d53fca134d3d4d7d17822b5d5e12438d5918df4d1f89cc9e67d57 - languageName: node - linkType: hard - "@typescript-eslint/parser@npm:^5.21.0": version: 5.62.0 resolution: "@typescript-eslint/parser@npm:5.62.0" @@ -9807,6 +9820,35 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/parser@npm:8.54.0" + dependencies: + "@typescript-eslint/scope-manager": 8.54.0 + "@typescript-eslint/types": 8.54.0 + "@typescript-eslint/typescript-estree": 8.54.0 + "@typescript-eslint/visitor-keys": 8.54.0 + debug: ^4.4.3 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 1a4c8c6edd67b3f301d00f0ad1739d0536b7843ef1a7091d2444c3fe752932786851c49d4d26e87cc914dfae49dddf77f0354d71dbfc382ff8959cd1b7bcbbbe + languageName: node + linkType: hard + +"@typescript-eslint/project-service@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/project-service@npm:8.54.0" + dependencies: + "@typescript-eslint/tsconfig-utils": ^8.54.0 + "@typescript-eslint/types": ^8.54.0 + debug: ^4.4.3 + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 3c2a5c758aa92d3673050383f4a9889c8175738372caf40082929082dfff87d5dbf54b9d22d97915f0f47393950df9fc338526dcc10be0512315aff82e65ad99 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/scope-manager@npm:4.33.0" @@ -9827,6 +9869,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/scope-manager@npm:8.54.0" + dependencies: + "@typescript-eslint/types": 8.54.0 + "@typescript-eslint/visitor-keys": 8.54.0 + checksum: 9a6bbdf019c3bed31aa81f11cd2d4f98e1b71a83d3f68ccdbd2a6539bfe1575ec59f37cd96b74311df1183c78348325d6b8ddcb653f7096f0d3e36299ae3c3e9 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: f8907f6e803563b460e035a688f30dbbb690d40c3fd9bb8e30c4628905bd49cf9de4947042268c0b50ce4e7aac3249712a33e91afde9a08df064ad782cd38dee + languageName: node + linkType: hard + "@typescript-eslint/types@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/types@npm:4.33.0" @@ -9841,6 +9902,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/types@npm:8.54.0" + checksum: 53ee5c5ef804e8cd1dd9a4c7f7a82e45a17d97ee78b1e108c56c919d08f86c2c9e4fec8c732e0d23995cf63532923456e7757b41833f40b93f1fca28b2db571a + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:4.33.0": version: 4.33.0 resolution: "@typescript-eslint/typescript-estree@npm:4.33.0" @@ -9877,6 +9945,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" + dependencies: + "@typescript-eslint/project-service": 8.54.0 + "@typescript-eslint/tsconfig-utils": 8.54.0 + "@typescript-eslint/types": 8.54.0 + "@typescript-eslint/visitor-keys": 8.54.0 + debug: ^4.4.3 + minimatch: ^9.0.5 + semver: ^7.7.3 + tinyglobby: ^0.2.15 + ts-api-utils: ^2.4.0 + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 0a4cf84abba5fba389515224e60fa0830c3d5403a2954e43d7390311cab25bb37728de124eb17e9d5bd05ee067e3b7ef815808e3c3abd58d8eeb3eae1988b6f1 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:^5.45.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -9915,6 +10002,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" + dependencies: + "@typescript-eslint/types": 8.54.0 + eslint-visitor-keys: ^4.2.1 + checksum: 36aafcffee5223041e3c898a8622589ae04e89cfad3d785bf506ab2126606af5ddac48bd6dbbf1c1098a0e21206b4f9edc90971f9f11a220423a924345adb184 + languageName: node + linkType: hard + "@tyriar/fibonacci-heap@npm:^2.0.7": version: 2.0.9 resolution: "@tyriar/fibonacci-heap@npm:2.0.9" @@ -10453,7 +10550,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^7.4.0, acorn@npm:^7.4.1": +"acorn@npm:^7.4.1": version: 7.4.1 resolution: "acorn@npm:7.4.1" bin: @@ -10511,15 +10608,6 @@ __metadata: languageName: node linkType: hard -"agentkeepalive@npm:^4.2.1": - version: 4.6.0 - resolution: "agentkeepalive@npm:4.6.0" - dependencies: - humanize-ms: ^1.2.1 - checksum: b3cdd10efca04876defda3c7671163523fcbce20e8ef7a8f9f30919a242e32b846791c0f1a8a0269718a585805a2cdcd031779ff7b9927a1a8dd8586f8c2e8c5 - languageName: node - linkType: hard - "aggregate-error@npm:^3.0.0": version: 3.1.0 resolution: "aggregate-error@npm:3.1.0" @@ -10530,41 +10618,17 @@ __metadata: languageName: node linkType: hard -"ai@npm:^3.1.1": - version: 3.4.33 - resolution: "ai@npm:3.4.33" +"ai@npm:5.0.52": + version: 5.0.52 + resolution: "ai@npm:5.0.52" dependencies: - "@ai-sdk/provider": 0.0.26 - "@ai-sdk/provider-utils": 1.0.22 - "@ai-sdk/react": 0.0.70 - "@ai-sdk/solid": 0.0.54 - "@ai-sdk/svelte": 0.0.57 - "@ai-sdk/ui-utils": 0.0.50 - "@ai-sdk/vue": 0.0.59 + "@ai-sdk/gateway": 1.0.29 + "@ai-sdk/provider": 2.0.0 + "@ai-sdk/provider-utils": 3.0.9 "@opentelemetry/api": 1.9.0 - eventsource-parser: 1.1.2 - json-schema: ^0.4.0 - jsondiffpatch: 0.6.0 - secure-json-parse: ^2.7.0 - zod-to-json-schema: ^3.23.3 - peerDependencies: - openai: ^4.42.0 - react: ^18 || ^19 || ^19.0.0-rc - sswr: ^2.1.0 - svelte: ^3.0.0 || ^4.0.0 || ^5.0.0 - zod: ^3.0.0 - peerDependenciesMeta: - openai: - optional: true - react: - optional: true - sswr: - optional: true - svelte: - optional: true - zod: - optional: true - checksum: e46ea18f2036a8df75802ddee3e6ecd2c2a2f9179d18563dbc294e4661edf2cac43a224ef2619f29fad239802acc78d59719b3d1f475adae368963eec1502fcf + peerDependencies: + zod: ^3.25.76 || ^4 + checksum: 496f07b394961a2b40d56f19a400855a3dc29b1c375b7475c63d1fddf4c59e9d24277b893f006dc544d129a27922a320035250adda0076710a87444be01086c0 languageName: node linkType: hard @@ -10626,7 +10690,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.10.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -10660,15 +10724,19 @@ __metadata: "@babel/polyfill": ^7.12.1 "@babel/preset-env": ^7.22.20 "@babel/preset-react": ^7.14.5 - "@casl/ability": ^6.0.0 + "@casl/ability": 6.8.0 "@compodoc/compodoc": 1.1.21 "@cypress/react": ^9.0.0 "@dhaiwat10/react-link-preview": ^1.9.1 "@emotion/react": ^11.10.4 "@emotion/styled": ^11.10.4 - "@langchain/community": ^0.0.54 - "@langchain/core": ^0.1.63 - "@langchain/openai": ^0.0.28 + "@eslint/compat": ^2.0.2 + "@eslint/eslintrc": ^3.3.3 + "@eslint/js": ^9.39.2 + "@langchain/community": 1.1.16 + "@langchain/core": 1.1.26 + "@langchain/langgraph": ^1.1.5 + "@langchain/openai": 1.2.8 "@mui/icons-material": ^5.10.9 "@mui/joy": ^5.0.0-beta.48 "@mui/lab": ^6.0.0-beta.30 @@ -10677,7 +10745,7 @@ __metadata: "@mui/x-date-pickers": 5.0.20 "@nestjs/axios": ^3.0.0 "@nestjs/cli": 9.1.5 - "@nestjs/common": ^9.2.0 + "@nestjs/common": 10.4.22 "@nestjs/config": ^2.2.0 "@nestjs/core": ^9.2.0 "@nestjs/jwt": ^10.2.0 @@ -10720,17 +10788,17 @@ __metadata: "@types/react": ^18.3.1 "@types/react-dom": ^18.3.0 "@typescript-eslint/eslint-plugin": ^4.29.0 - "@typescript-eslint/parser": ^4.29.0 + "@typescript-eslint/parser": ^8.54.0 "@xstate/react": ^3.0.0 accept-language-parser: ^1.5.0 - ai: ^3.1.1 + ai: 5.0.52 aws-sdk: ^2.1154.0 axios: ^1.12.0 babel-eslint: ^10.1.0 babel-jest: ^29.7.0 babel-loader: ^8.2.5 babel-plugin-styled-components: ^1.13.2 - body-parser: ^1.20.2 + body-parser: 1.20.3 class-transformer: ^0.5.1 class-validator: ^0.14.0 compromise: ^13.11.4 @@ -10745,19 +10813,18 @@ __metadata: dompurify: ^3.0.5 dotenv: ^16.4.5 env-cmd: ^10.1.0 - eslint: 7.0.0 + eslint: ^9.39.2 eslint-config-next: ^12.0.10 eslint-config-prettier: 8.3.0 - eslint-config-react-app: ^6.0.0 - eslint-plugin-flowtype: ^5.9.0 eslint-plugin-import: ^2.18.0 eslint-plugin-jsx-a11y: ^6.3.1 eslint-plugin-prettier: 3.4.0 eslint-plugin-react: ^7.20.3 eslint-plugin-react-hooks: ^4.0.8 eslint-plugin-storybook: ^0.6.14 - express: ^4.19.2 + express: 4.19.2 express-session: ^1.17.2 + globals: ^17.3.0 handlebars: ^4.7.7 helmet: ^6.0.0 husky: ^8.0.1 @@ -10767,7 +10834,7 @@ __metadata: jotai-xstate: ^0.3.0 js-cookie: ^3.0.1 jsonwebtoken: ^9.0.2 - langchain: ^0.1.36 + langchain: 1.2.25 lint-staged: ^13.0.3 lottie-web: ^5.10.1 md5: ^2.3.0 @@ -10782,7 +10849,7 @@ __metadata: next: ^12.1.0 next-i18next: ^10.2.0 next-seo: ^5.4.0 - nodemailer: ^6.9.9 + nodemailer: 7.0.11 nodemon: ^2.0.12 prettier: 2.3.2 react: ^18.3.1 @@ -10806,13 +10873,13 @@ __metadata: sitemap: 5 slugify: ^1.6.1 socket.io: ^4.7.2 - storybook: ^7.4.5 + storybook: 7.6.21 styled-components: ^5.3.0 supertest: ^6.2.4 ts-jest: ^29.1.1 ts-node: ^10.2.0 - typescript: ^4.3.5 - wait-on: ^6.0.1 + typescript: ^5.3.0 + wait-on: 9.0.3 winston: ^3.13.0 ws: ^8.13.0 xstate: ^4.32.1 @@ -10867,13 +10934,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^4.1.0": - version: 4.1.1 - resolution: "ansi-regex@npm:4.1.1" - checksum: b1a6ee44cb6ecdabaa770b2ed500542714d4395d71c7e5c25baa631f680fb2ad322eb9ba697548d498a6fd366949fc8b5bfcf48d49a32803611f648005b01888 - languageName: node - linkType: hard - "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" @@ -10888,12 +10948,10 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^3.2.0": - version: 3.2.1 - resolution: "ansi-styles@npm:3.2.1" - dependencies: - color-convert: ^1.9.0 - checksum: d85ade01c10e5dd77b6c89f34ed7531da5830d2cb5882c645f330079975b716438cd7ebb81d0d6e6b4f9c577f19ae41ab55f07f19786b02f9dfd9e0377395665 +"ansi-styles@npm:5.2.0, ansi-styles@npm:^5.0.0": + version: 5.2.0 + resolution: "ansi-styles@npm:5.2.0" + checksum: d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 languageName: node linkType: hard @@ -10906,13 +10964,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": - version: 5.2.0 - resolution: "ansi-styles@npm:5.2.0" - checksum: d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 - languageName: node - linkType: hard - "ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": version: 6.2.3 resolution: "ansi-styles@npm:6.2.3" @@ -11199,13 +11250,6 @@ __metadata: languageName: node linkType: hard -"astral-regex@npm:^1.0.0": - version: 1.0.0 - resolution: "astral-regex@npm:1.0.0" - checksum: 93417fc0879531cd95ace2560a54df865c9461a3ac0714c60cbbaa5f1f85d2bee85489e78d82f70b911b71ac25c5f05fc5a36017f44c9bb33c701bee229ff848 - languageName: node - linkType: hard - "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -11307,15 +11351,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^0.25.0": - version: 0.25.0 - resolution: "axios@npm:0.25.0" - dependencies: - follow-redirects: ^1.14.7 - checksum: 2a8a3787c05f2a0c9c3878f49782357e2a9f38945b93018fb0c4fd788171c43dceefbb577988628e09fea53952744d1ecebde234b561f1e703aa43e0a598a3ad - languageName: node - linkType: hard - "axios@npm:^1.12.0": version: 1.12.2 resolution: "axios@npm:1.12.2" @@ -11327,6 +11362,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.13.2": + version: 1.13.4 + resolution: "axios@npm:1.13.4" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.4 + proxy-from-env: ^1.1.0 + checksum: 1d1f360cf54c8a4b602d4d5af1b13f04612be3876a7958e9cd49c5869448399d75978ce4a099839d55ccb9f9aff9d49a312db879dba9d76fac994479fd1ad11c + languageName: node + linkType: hard + "axios@npm:^1.4.0, axios@npm:^1.6.1, axios@npm:^1.7.4": version: 1.11.0 resolution: "axios@npm:1.11.0" @@ -11678,13 +11724,6 @@ __metadata: languageName: node linkType: hard -"base-64@npm:^0.1.0": - version: 0.1.0 - resolution: "base-64@npm:0.1.0" - checksum: 5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb - languageName: node - linkType: hard - "base64-js@npm:1.3.1": version: 1.3.1 resolution: "base64-js@npm:1.3.1" @@ -11775,13 +11814,6 @@ __metadata: languageName: node linkType: hard -"binary-search@npm:^1.3.5": - version: 1.3.6 - resolution: "binary-search@npm:1.3.6" - checksum: 2e6b3459a9c1ba1bd674a6a855a5ef7505f70707422244430e3510e989c0df6074a49fe60784a98b93b51545c9bcace1db1defee06ff861b124c036a2f2836bf - languageName: node - linkType: hard - "binaryextensions@npm:^4.18.0": version: 4.19.0 resolution: "binaryextensions@npm:4.19.0" @@ -11885,7 +11917,7 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.3, body-parser@npm:^1.20.2": +"body-parser@npm:1.20.3": version: 1.20.3 resolution: "body-parser@npm:1.20.3" dependencies: @@ -12363,13 +12395,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.3.0": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 4ee2d47a626d79ca27cb5299ecdcce840ef5755e287412536522344db0fc51ca0f6d6433202332c29e2288c6a90a2b31f3bd626bc8c14743b6b6ee28abd3b796 - languageName: node - linkType: hard - "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -12780,7 +12805,7 @@ __metadata: languageName: node linkType: hard -"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": +"color-convert@npm:^1.9.3": version: 1.9.3 resolution: "color-convert@npm:1.9.3" dependencies: @@ -13091,13 +13116,6 @@ __metadata: languageName: node linkType: hard -"confusing-browser-globals@npm:^1.0.10": - version: 1.0.11 - resolution: "confusing-browser-globals@npm:1.0.11" - checksum: 3afc635abd37e566477f610e7978b15753f0e84025c25d49236f1f14d480117185516bdd40d2a2167e6bed8048641a9854964b9c067e3dcdfa6b5d0ad3c3a5ef - languageName: node - linkType: hard - "connect@npm:^3.7.0": version: 3.7.0 resolution: "connect@npm:3.7.0" @@ -13131,6 +13149,15 @@ __metadata: languageName: node linkType: hard +"console-table-printer@npm:^2.12.1": + version: 2.15.0 + resolution: "console-table-printer@npm:2.15.0" + dependencies: + simple-wcswidth: ^1.1.2 + checksum: a878e446303eabaa86a2fd7f0d956d24252e0837be23249e7ad24daf3f304ce7bf155ca79b25ccd4f380f31fa35e427adc762e49ff29236f82a96c4b1d88551e + languageName: node + linkType: hard + "constants-browserify@npm:^1.0.0": version: 1.0.0 resolution: "constants-browserify@npm:1.0.0" @@ -13199,6 +13226,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.6.0": + version: 0.6.0 + resolution: "cookie@npm:0.6.0" + checksum: f56a7d32a07db5458e79c726b77e3c2eff655c36792f2b6c58d351fb5f61531e5b1ab7f46987150136e366c65213cbe31729e02a3eaed630c3bf7334635fb410 + languageName: node + linkType: hard + "cookie@npm:0.7.1": version: 0.7.1 resolution: "cookie@npm:0.7.1" @@ -13428,7 +13462,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" dependencies: @@ -13764,7 +13798,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.2.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.1": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.2.0, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.1": version: 4.4.1 resolution: "debug@npm:4.4.1" dependencies: @@ -13809,6 +13843,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.4.0, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 4805abd570e601acdca85b6aa3757186084a45cff9b2fa6eee1f3b173caa776b45f478b2a71a572d616d2010cea9211d0ac4a02a610e4c18ac4324bde3760834 + languageName: node + linkType: hard + "debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": version: 4.3.7 resolution: "debug@npm:4.3.7" @@ -14051,7 +14097,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0, dequal@npm:^2.0.2, dequal@npm:^2.0.3": +"dequal@npm:^2.0.0, dequal@npm:^2.0.2": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 8679b850e1a3d0ebbc46ee780d5df7b478c23f335887464023a631d1b9af051ad4a6595a44220f9ff8ff95a8ddccf019b5ad778a976fd7bbf77383d36f412f90 @@ -14142,13 +14188,6 @@ __metadata: languageName: node linkType: hard -"diff-match-patch@npm:^1.0.5": - version: 1.0.5 - resolution: "diff-match-patch@npm:1.0.5" - checksum: 841522d01b09cccbc4e4402cf61514a81b906349a7d97b67222390f2d35cf5df277cb23959eeed212d5e46afb5629cebab41b87918672c5a05c11c73688630e3 - languageName: node - linkType: hard - "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -14188,16 +14227,6 @@ __metadata: languageName: node linkType: hard -"digest-fetch@npm:^1.3.0": - version: 1.3.0 - resolution: "digest-fetch@npm:1.3.0" - dependencies: - base-64: ^0.1.0 - md5: ^2.3.0 - checksum: 8ebdb4b9ef02b1ac0da532d25c7d08388f2552813dfadabfe7c4630e944bb4a48093b997fc926440a10e1ccf4912f2ce9adcf2d6687b0518dab8480e08f22f9d - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -14510,13 +14539,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^7.0.1": - version: 7.0.3 - resolution: "emoji-regex@npm:7.0.3" - checksum: 9159b2228b1511f2870ac5920f394c7e041715429a68459ebe531601555f11ea782a8e1718f969df2711d38c66268174407cbca57ce36485544f695c2dfdc96e - languageName: node - linkType: hard - "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -15140,32 +15162,6 @@ __metadata: languageName: node linkType: hard -"eslint-config-react-app@npm:^6.0.0": - version: 6.0.0 - resolution: "eslint-config-react-app@npm:6.0.0" - dependencies: - confusing-browser-globals: ^1.0.10 - peerDependencies: - "@typescript-eslint/eslint-plugin": ^4.0.0 - "@typescript-eslint/parser": ^4.0.0 - babel-eslint: ^10.0.0 - eslint: ^7.5.0 - eslint-plugin-flowtype: ^5.2.0 - eslint-plugin-import: ^2.22.0 - eslint-plugin-jest: ^24.0.0 - eslint-plugin-jsx-a11y: ^6.3.1 - eslint-plugin-react: ^7.20.3 - eslint-plugin-react-hooks: ^4.0.8 - eslint-plugin-testing-library: ^3.9.0 - peerDependenciesMeta: - eslint-plugin-jest: - optional: true - eslint-plugin-testing-library: - optional: true - checksum: b265852455b1c10e9c5f0cebe199306fffc7f8e1b6548fcb0bccdc4415c288dfee8ab10717122a32275b91130dfb482dcbbc87d2fb79d8728d4c2bfa889f0915 - languageName: node - linkType: hard - "eslint-import-resolver-node@npm:^0.3.6, eslint-import-resolver-node@npm:^0.3.9": version: 0.3.9 resolution: "eslint-import-resolver-node@npm:0.3.9" @@ -15205,18 +15201,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-flowtype@npm:^5.9.0": - version: 5.10.0 - resolution: "eslint-plugin-flowtype@npm:5.10.0" - dependencies: - lodash: ^4.17.15 - string-natural-compare: ^3.0.1 - peerDependencies: - eslint: ^7.1.0 - checksum: 791cd53c886bf819d52d6353cdfb4d49276dcd8a14f564a85d275d5017d81c7b1cc1921013ac9749f69c3f1bc4d23f36182137aab42bc059c2ae3f9773dd7740 - languageName: node - linkType: hard - "eslint-plugin-import@npm:^2.18.0, eslint-plugin-import@npm:^2.26.0": version: 2.32.0 resolution: "eslint-plugin-import@npm:2.32.0" @@ -15337,7 +15321,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.0.0, eslint-scope@npm:^5.1.1": +"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -15347,12 +15331,13 @@ __metadata: languageName: node linkType: hard -"eslint-utils@npm:^2.0.0": - version: 2.1.0 - resolution: "eslint-utils@npm:2.1.0" +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" dependencies: - eslint-visitor-keys: ^1.1.0 - checksum: 27500938f348da42100d9e6ad03ae29b3de19ba757ae1a7f4a087bdcf83ac60949bbb54286492ca61fac1f5f3ac8692dd21537ce6214240bf95ad0122f24d71d + esrecurse: ^4.3.0 + estraverse: ^5.2.0 + checksum: cf88f42cd5e81490d549dc6d350fe01e6fe420f9d9ea34f134bb359b030e3c4ef888d36667632e448937fe52449f7181501df48c08200e3d3b0fee250d05364e languageName: node linkType: hard @@ -15367,7 +15352,7 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^1.0.0, eslint-visitor-keys@npm:^1.1.0, eslint-visitor-keys@npm:^1.3.0": +"eslint-visitor-keys@npm:^1.0.0": version: 1.3.0 resolution: "eslint-visitor-keys@npm:1.3.0" checksum: 37a19b712f42f4c9027e8ba98c2b06031c17e0c0a4c696cd429bd9ee04eb43889c446f2cd545e1ff51bef9593fcec94ecd2c2ef89129fcbbf3adadbef520376a @@ -15388,60 +15373,70 @@ __metadata: languageName: node linkType: hard -"eslint@npm:7.0.0": - version: 7.0.0 - resolution: "eslint@npm:7.0.0" - dependencies: - "@babel/code-frame": ^7.0.0 - ajv: ^6.10.0 +"eslint-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 3a77e3f99a49109f6fb2c5b7784bc78f9743b834d238cdba4d66c602c6b52f19ed7bcd0a5c5dbbeae3a8689fd785e76c001799f53d2228b278282cf9f699fff5 + languageName: node + linkType: hard + +"eslint@npm:^9.39.2": + version: 9.39.2 + resolution: "eslint@npm:9.39.2" + dependencies: + "@eslint-community/eslint-utils": ^4.8.0 + "@eslint-community/regexpp": ^4.12.1 + "@eslint/config-array": ^0.21.1 + "@eslint/config-helpers": ^0.4.2 + "@eslint/core": ^0.17.0 + "@eslint/eslintrc": ^3.3.1 + "@eslint/js": 9.39.2 + "@eslint/plugin-kit": ^0.4.1 + "@humanfs/node": ^0.16.6 + "@humanwhocodes/module-importer": ^1.0.1 + "@humanwhocodes/retry": ^0.4.2 + "@types/estree": ^1.0.6 + ajv: ^6.12.4 chalk: ^4.0.0 - cross-spawn: ^7.0.2 - debug: ^4.0.1 - doctrine: ^3.0.0 - eslint-scope: ^5.0.0 - eslint-utils: ^2.0.0 - eslint-visitor-keys: ^1.1.0 - espree: ^7.0.0 - esquery: ^1.2.0 + cross-spawn: ^7.0.6 + debug: ^4.3.2 + escape-string-regexp: ^4.0.0 + eslint-scope: ^8.4.0 + eslint-visitor-keys: ^4.2.1 + espree: ^10.4.0 + esquery: ^1.5.0 esutils: ^2.0.2 - file-entry-cache: ^5.0.1 - functional-red-black-tree: ^1.0.1 - glob-parent: ^5.0.0 - globals: ^12.1.0 - ignore: ^4.0.6 - import-fresh: ^3.0.0 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^8.0.0 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + ignore: ^5.2.0 imurmurhash: ^0.1.4 - inquirer: ^7.0.0 is-glob: ^4.0.0 - js-yaml: ^3.13.1 json-stable-stringify-without-jsonify: ^1.0.1 - levn: ^0.4.1 - lodash: ^4.17.14 - minimatch: ^3.0.4 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 natural-compare: ^1.4.0 - optionator: ^0.9.1 - progress: ^2.0.0 - regexpp: ^3.1.0 - semver: ^7.2.1 - strip-ansi: ^6.0.0 - strip-json-comments: ^3.1.0 - table: ^5.2.3 - text-table: ^0.2.0 - v8-compile-cache: ^2.0.3 + optionator: ^0.9.3 + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true bin: eslint: bin/eslint.js - checksum: dd5066d9f841a58569b3c6957035cedff543bdc0aa67ca645734da0ffa64672b9dfea9af577a8b7192db7efa2a30ad3d5f5bff8b68a45d3de868f8a0f40950b5 + checksum: bfa288fe6b19b6e7f8868e1434d8e469603203d6259e4451b8be4e2172de3172f3b07ed8943ba3904f3545c7c546062c0d656774baa0a10a54483f3907c525e3 languageName: node linkType: hard -"espree@npm:^7.0.0": - version: 7.3.1 - resolution: "espree@npm:7.3.1" +"espree@npm:^10.0.1, espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" dependencies: - acorn: ^7.4.0 - acorn-jsx: ^5.3.1 - eslint-visitor-keys: ^1.3.0 - checksum: aa9b50dcce883449af2e23bc2b8d9abb77118f96f4cb313935d6b220f77137eaef7724a83c3f6243b96bc0e4ab14766198e60818caad99f9519ae5a336a39b45 + acorn: ^8.15.0 + acorn-jsx: ^5.3.2 + eslint-visitor-keys: ^4.2.1 + checksum: 5f9d0d7c81c1bca4bfd29a55270067ff9d575adb8c729a5d7f779c2c7b910bfc68ccf8ec19b29844b707440fc159a83868f22c8e87bbf7cbcb225ed067df6c85 languageName: node linkType: hard @@ -15455,12 +15450,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.2.0": - version: 1.6.0 - resolution: "esquery@npm:1.6.0" +"esquery@npm:^1.5.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" dependencies: estraverse: ^5.1.0 - checksum: 08ec4fe446d9ab27186da274d979558557fbdbbd10968fa9758552482720c54152a5640e08b9009e5a30706b66aba510692054d4129d32d0e12e05bbc0b96fb2 + checksum: 3239792b68cf39fe18966d0ca01549bb15556734f0144308fd213739b0f153671ae916013fce0bca032044a4dbcda98b43c1c667f20c20a54dec3597ac0d7c27 languageName: node linkType: hard @@ -15558,10 +15553,10 @@ __metadata: languageName: node linkType: hard -"eventsource-parser@npm:1.1.2, eventsource-parser@npm:^1.1.2": - version: 1.1.2 - resolution: "eventsource-parser@npm:1.1.2" - checksum: 01896eea7203e097e50f7554da6ca3c6d33593ff11e8e3c970461932c3643bed2aea8402bf7fd16a1b25117de4e6fed15fef80672964a7dbd42b1e6781bf6e7a +"eventsource-parser@npm:^3.0.5": + version: 3.0.6 + resolution: "eventsource-parser@npm:3.0.6" + checksum: b90ec27f8d992afa7df171db202faaedb1782214f64e50690cbf78bc2629f7751575aa27a72d8ae447e5a7094938406b1a3ea1d89e5f0f2d6916cc8a694b6587 languageName: node linkType: hard @@ -15670,13 +15665,6 @@ __metadata: languageName: node linkType: hard -"expr-eval@npm:^2.0.2": - version: 2.0.2 - resolution: "expr-eval@npm:2.0.2" - checksum: 01862f09b50b17b45a6268b1153280afede99e1b51752a323661f7f4010eaed34cd6c682bf439b7f8a92df6aa82f326f0ce0aa20964d175feee97377fe53921d - languageName: node - linkType: hard - "express-session@npm:^1.17.2": version: 1.18.2 resolution: "express-session@npm:1.18.2" @@ -15693,16 +15681,55 @@ __metadata: languageName: node linkType: hard -"express@npm:4.18.2": - version: 4.18.2 - resolution: "express@npm:4.18.2" +"express@npm:4.18.2": + version: 4.18.2 + resolution: "express@npm:4.18.2" + dependencies: + accepts: ~1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: ~1.0.4 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: ~1.0.2 + escape-html: ~1.0.3 + etag: ~1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: ~1.1.2 + on-finished: 2.4.1 + parseurl: ~1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: ~2.0.7 + qs: 6.11.0 + range-parser: ~1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: ~1.6.18 + utils-merge: 1.0.1 + vary: ~1.1.2 + checksum: 3c4b9b076879442f6b968fe53d85d9f1eeacbb4f4c41e5f16cc36d77ce39a2b0d81b3f250514982110d815b2f7173f5561367f9110fcc541f9371948e8c8b037 + languageName: node + linkType: hard + +"express@npm:4.19.2": + version: 4.19.2 + resolution: "express@npm:4.19.2" dependencies: accepts: ~1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.1 + body-parser: 1.20.2 content-disposition: 0.5.4 content-type: ~1.0.4 - cookie: 0.5.0 + cookie: 0.6.0 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 @@ -15728,11 +15755,11 @@ __metadata: type-is: ~1.6.18 utils-merge: 1.0.1 vary: ~1.1.2 - checksum: 3c4b9b076879442f6b968fe53d85d9f1eeacbb4f4c41e5f16cc36d77ce39a2b0d81b3f250514982110d815b2f7173f5561367f9110fcc541f9371948e8c8b037 + checksum: 212dbd6c2c222a96a61bc927639c95970a53b06257080bb9e2838adb3bffdb966856551fdad1ab5dd654a217c35db94f987d0aa88d48fb04d306340f5f34dca5 languageName: node linkType: hard -"express@npm:^4.17.3, express@npm:^4.19.2": +"express@npm:^4.17.3": version: 4.21.2 resolution: "express@npm:4.21.2" dependencies: @@ -15990,6 +16017,13 @@ __metadata: languageName: node linkType: hard +"fflate@npm:^0.8.2": + version: 0.8.2 + resolution: "fflate@npm:0.8.2" + checksum: 29470337b85d3831826758e78f370e15cda3169c5cd4477c9b5eea2402261a74b2975bae816afabe1c15d21d98591e0d30a574f7103aa117bff60756fa3035d4 + languageName: node + linkType: hard + "figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -15999,12 +16033,12 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^5.0.1": - version: 5.0.1 - resolution: "file-entry-cache@npm:5.0.1" +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" dependencies: - flat-cache: ^2.0.1 - checksum: 9014b17766815d59b8b789633aed005242ef857348c09be558bd85b4a24e16b0ad1e0e5229ccea7a2109f74ef1b3db1a559b58afe12b884f09019308711376fd + flat-cache: ^4.0.0 + checksum: f67802d3334809048c69b3d458f672e1b6d26daefda701761c81f203b80149c35dea04d78ea4238969dd617678e530876722a0634c43031a0957f10cc3ed190f languageName: node linkType: hard @@ -16018,6 +16052,18 @@ __metadata: languageName: node linkType: hard +"file-type@npm:20.4.1": + version: 20.4.1 + resolution: "file-type@npm:20.4.1" + dependencies: + "@tokenizer/inflate": ^0.2.6 + strtok3: ^10.2.0 + token-types: ^6.0.0 + uint8array-extras: ^1.4.0 + checksum: 672504dff2a15aaebaf32457be3a89fa8b09a15b0da96d82411bab9474bc18aae740a90863f3777e1493a34093994e69cfd19b5be801fdc0f7a7646d19138b40 + languageName: node + linkType: hard + "filelist@npm:^1.0.4": version: 1.0.4 resolution: "filelist@npm:1.0.4" @@ -16173,17 +16219,6 @@ __metadata: languageName: node linkType: hard -"flat-cache@npm:^2.0.1": - version: 2.0.1 - resolution: "flat-cache@npm:2.0.1" - dependencies: - flatted: ^2.0.0 - rimraf: 2.6.3 - write: 1.0.3 - checksum: 0f5e66467658039e6fcaaccb363b28f43906ba72fab7ff2a4f6fcd5b4899679e13ca46d9fc6cc48b68ac925ae93137106d4aaeb79874c13f21f87a361705f1b1 - languageName: node - linkType: hard - "flat-cache@npm:^3.0.4": version: 3.2.0 resolution: "flat-cache@npm:3.2.0" @@ -16195,6 +16230,16 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: ^3.2.9 + keyv: ^4.5.4 + checksum: 899fc86bf6df093547d76e7bfaeb900824b869d7d457d02e9b8aae24836f0a99fbad79328cfd6415ee8908f180699bf259dc7614f793447cb14f707caf5996f6 + languageName: node + linkType: hard + "flat@npm:^5.0.2": version: 5.0.2 resolution: "flat@npm:5.0.2" @@ -16204,13 +16249,6 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^2.0.0": - version: 2.0.2 - resolution: "flatted@npm:2.0.2" - checksum: 473c754db7a529e125a22057098f1a4c905ba17b8cc269c3acf77352f0ffa6304c851eb75f6a1845f74461f560e635129ca6b0b8a78fb253c65cea4de3d776f2 - languageName: node - linkType: hard - "flatted@npm:^3.2.7, flatted@npm:^3.2.9": version: 3.3.3 resolution: "flatted@npm:3.3.3" @@ -16315,13 +16353,6 @@ __metadata: languageName: node linkType: hard -"form-data-encoder@npm:1.7.2": - version: 1.7.2 - resolution: "form-data-encoder@npm:1.7.2" - checksum: aeebd87a1cb009e13cbb5e4e4008e6202ed5f6551eb6d9582ba8a062005178907b90f4887899d3c993de879159b6c0c940af8196725b428b4248cec5af3acf5f - languageName: node - linkType: hard - "form-data@npm:^2.2.0": version: 2.5.5 resolution: "form-data@npm:2.5.5" @@ -16360,16 +16391,6 @@ __metadata: languageName: node linkType: hard -"formdata-node@npm:^4.3.2": - version: 4.4.1 - resolution: "formdata-node@npm:4.4.1" - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 4.0.0-beta.3 - checksum: d91d4f667cfed74827fc281594102c0dabddd03c9f8b426fc97123eedbf73f5060ee43205d89284d6854e2fc5827e030cd352ef68b93beda8decc2d72128c576 - languageName: node - linkType: hard - "formidable@npm:^2.1.2": version: 2.1.5 resolution: "formidable@npm:2.1.5" @@ -16731,7 +16752,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.0.0, glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -16740,6 +16761,15 @@ __metadata: languageName: node linkType: hard +"glob-parent@npm:^6.0.2": + version: 6.0.2 + resolution: "glob-parent@npm:6.0.2" + dependencies: + is-glob: ^4.0.3 + checksum: c13ee97978bef4f55106b71e66428eb1512e71a7466ba49025fc2aec59a5bfb0954d5abd58fc5ee6c9b076eef4e1f6d3375c2e964b88466ca390da4419a786a8 + languageName: node + linkType: hard + "glob-to-regexp@npm:^0.4.1": version: 0.4.1 resolution: "glob-to-regexp@npm:0.4.1" @@ -16768,12 +16798,17 @@ __metadata: languageName: node linkType: hard -"globals@npm:^12.1.0": - version: 12.4.0 - resolution: "globals@npm:12.4.0" - dependencies: - type-fest: ^0.8.1 - checksum: 7ae5ee16a96f1e8d71065405f57da0e33267f6b070cd36a5444c7780dd28639b48b92413698ac64f04bf31594f9108878bd8cb158ecdf759c39e05634fefcca6 +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 534b8216736a5425737f59f6e6a5c7f386254560c9f41d24a9227d60ee3ad4a9e82c5b85def0e212e9d92162f83a92544be4c7fd4c902cb913736c10e08237ac + languageName: node + linkType: hard + +"globals@npm:^17.3.0": + version: 17.3.0 + resolution: "globals@npm:17.3.0" + checksum: 4316ace3d0890ac3d72d50bfd723df93fd375cb4b328c503e1fae81dd73e4ab9c76051ae28ce02c2a6b049e9a8149e86e8861d185433d3af65c774976802a718 languageName: node linkType: hard @@ -17378,15 +17413,6 @@ __metadata: languageName: node linkType: hard -"humanize-ms@npm:^1.2.1": - version: 1.2.1 - resolution: "humanize-ms@npm:1.2.1" - dependencies: - ms: ^2.0.0 - checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 - languageName: node - linkType: hard - "husky@npm:^8.0.1": version: 8.0.3 resolution: "husky@npm:8.0.3" @@ -17485,13 +17511,6 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^4.0.6": - version: 4.0.6 - resolution: "ignore@npm:4.0.6" - checksum: 248f82e50a430906f9ee7f35e1158e3ec4c3971451dd9f99c9bc1548261b4db2b99709f60ac6c6cac9333494384176cc4cc9b07acbe42d52ac6a09cad734d800 - languageName: node - linkType: hard - "ignore@npm:^5.1.8, ignore@npm:^5.2.0": version: 5.3.2 resolution: "ignore@npm:5.3.2" @@ -17526,7 +17545,7 @@ __metadata: languageName: node linkType: hard -"import-fresh@npm:^3.0.0, import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": +"import-fresh@npm:^3.2.1, import-fresh@npm:^3.3.0": version: 3.3.1 resolution: "import-fresh@npm:3.3.1" dependencies: @@ -17609,7 +17628,7 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:7.3.3, inquirer@npm:^7.0.0": +"inquirer@npm:7.3.3": version: 7.3.3 resolution: "inquirer@npm:7.3.3" dependencies: @@ -17723,13 +17742,6 @@ __metadata: languageName: node linkType: hard -"is-any-array@npm:^2.0.0": - version: 2.0.1 - resolution: "is-any-array@npm:2.0.1" - checksum: 472ed80e17d32951435087951af30c29498b163c31bf723dd5af76545b100bcfac6fad2df3f1a648b45e3b027de8f5dc2389935267ba5258eae85762804b4982 - languageName: node - linkType: hard - "is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": version: 1.2.0 resolution: "is-arguments@npm:1.2.0" @@ -17928,13 +17940,6 @@ __metadata: languageName: node linkType: hard -"is-fullwidth-code-point@npm:^2.0.0": - version: 2.0.0 - resolution: "is-fullwidth-code-point@npm:2.0.0" - checksum: eef9c6e15f68085fec19ff6a978a6f1b8f48018fd1265035552078ee945573594933b09bbd6f562553e2a241561439f1ef5339276eba68d272001343084cfab8 - languageName: node - linkType: hard - "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" @@ -18055,6 +18060,13 @@ __metadata: languageName: node linkType: hard +"is-network-error@npm:^1.1.0": + version: 1.3.0 + resolution: "is-network-error@npm:1.3.0" + checksum: 56dc0b8ed9c0bb72202058f172ad0c3121cf68772e8cbba343d3775f6e2ec7877d423cbcea45f4cedcd345de8693de1b52dfe0c6fc15d652c4aa98c2abf0185a + languageName: node + linkType: hard + "is-number-object@npm:^1.1.1": version: 1.1.1 resolution: "is-number-object@npm:1.1.1" @@ -18909,16 +18921,18 @@ __metadata: languageName: node linkType: hard -"joi@npm:^17.6.0": - version: 17.13.3 - resolution: "joi@npm:17.13.3" +"joi@npm:^18.0.1": + version: 18.0.2 + resolution: "joi@npm:18.0.2" dependencies: - "@hapi/hoek": ^9.3.0 - "@hapi/topo": ^5.1.0 - "@sideway/address": ^4.1.5 - "@sideway/formula": ^3.0.1 - "@sideway/pinpoint": ^2.0.0 - checksum: 66ed454fee3d8e8da1ce21657fd2c7d565d98f3e539d2c5c028767e5f38cbd6297ce54df8312d1d094e62eb38f9452ebb43da4ce87321df66cf5e3f128cbc400 + "@hapi/address": ^5.1.1 + "@hapi/formula": ^3.0.2 + "@hapi/hoek": ^11.0.7 + "@hapi/pinpoint": ^2.0.1 + "@hapi/tlds": ^1.1.1 + "@hapi/topo": ^6.0.2 + "@standard-schema/spec": ^1.0.0 + checksum: 39d9e3584505817d7c80ab11c0b6eccf353715d0e83c33f039caa5d917fa9ef700966b0b87dfebaaec8693f9850cb0745812afb620ba9633ba9e895ada3210f7 languageName: node linkType: hard @@ -19003,7 +19017,7 @@ __metadata: languageName: node linkType: hard -"js-tiktoken@npm:^1.0.12, js-tiktoken@npm:^1.0.7": +"js-tiktoken@npm:^1.0.12": version: 1.0.21 resolution: "js-tiktoken@npm:1.0.21" dependencies: @@ -19042,6 +19056,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: ^2.0.1 + bin: + js-yaml: bin/js-yaml.js + checksum: ea2339c6930fe048ec31b007b3c90be2714ab3e7defcc2c27ebf30c74fd940358f29070b4345af0019ef151875bf3bc3f8644bea1bab0372652b5044813ac02d + languageName: node + linkType: hard + "jsbn@npm:~0.1.0": version: 0.1.1 resolution: "jsbn@npm:0.1.1" @@ -19194,19 +19219,6 @@ __metadata: languageName: node linkType: hard -"jsondiffpatch@npm:0.6.0": - version: 0.6.0 - resolution: "jsondiffpatch@npm:0.6.0" - dependencies: - "@types/diff-match-patch": ^1.0.36 - chalk: ^5.3.0 - diff-match-patch: ^1.0.5 - bin: - jsondiffpatch: bin/jsondiffpatch.js - checksum: 27d7aa42c3b9f9359fd179bb49c621ed848f6095615014cd0acbd29c37e364c11cb5a19c3ef2c873631e7b5f7ba6d1465978a489efa28cb1dc9fba98b0498712 - languageName: node - linkType: hard - "jsonfile@npm:^4.0.0": version: 4.0.0 resolution: "jsonfile@npm:4.0.0" @@ -19337,259 +19349,90 @@ __metadata: languageName: node linkType: hard -"keyv@npm:^4.5.3": +"keyv@npm:^4.5.3, keyv@npm:^4.5.4": version: 4.5.4 - resolution: "keyv@npm:4.5.4" - dependencies: - json-buffer: 3.0.1 - checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 - languageName: node - linkType: hard - -"kind-of@npm:^6.0.2": - version: 6.0.3 - resolution: "kind-of@npm:6.0.3" - checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b - languageName: node - linkType: hard - -"kleur@npm:^3.0.3": - version: 3.0.3 - resolution: "kleur@npm:3.0.3" - checksum: df82cd1e172f957bae9c536286265a5cdbd5eeca487cb0a3b2a7b41ef959fc61f8e7c0e9aeea9c114ccf2c166b6a8dd45a46fd619c1c569d210ecd2765ad5169 - languageName: node - linkType: hard - -"kleur@npm:^4.0.3": - version: 4.1.5 - resolution: "kleur@npm:4.1.5" - checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 - languageName: node - linkType: hard - -"klona@npm:^2.0.4": - version: 2.0.6 - resolution: "klona@npm:2.0.6" - checksum: ac9ee3732e42b96feb67faae4d27cf49494e8a3bf3fa7115ce242fe04786788e0aff4741a07a45a2462e2079aa983d73d38519c85d65b70ef11447bbc3c58ce7 - languageName: node - linkType: hard - -"kuler@npm:^2.0.0": - version: 2.0.0 - resolution: "kuler@npm:2.0.0" - checksum: 9e10b5a1659f9ed8761d38df3c35effabffbd19fc6107324095238e4ef0ff044392cae9ac64a1c2dda26e532426485342226b93806bd97504b174b0dcf04ed81 - languageName: node - linkType: hard - -"langchain@npm:^0.1.36": - version: 0.1.37 - resolution: "langchain@npm:0.1.37" - dependencies: - "@anthropic-ai/sdk": ^0.9.1 - "@langchain/community": ~0.0.47 - "@langchain/core": ~0.1.60 - "@langchain/openai": ~0.0.28 - "@langchain/textsplitters": ~0.0.0 - binary-extensions: ^2.2.0 - js-tiktoken: ^1.0.7 - js-yaml: ^4.1.0 - jsonpointer: ^5.0.1 - langchainhub: ~0.0.8 - langsmith: ~0.1.7 - ml-distance: ^4.0.0 - openapi-types: ^12.1.3 - p-retry: 4 - uuid: ^9.0.0 - yaml: ^2.2.1 - zod: ^3.22.4 - zod-to-json-schema: ^3.22.3 - peerDependencies: - "@aws-sdk/client-s3": ^3.310.0 - "@aws-sdk/client-sagemaker-runtime": ^3.310.0 - "@aws-sdk/client-sfn": ^3.310.0 - "@aws-sdk/credential-provider-node": ^3.388.0 - "@azure/storage-blob": ^12.15.0 - "@browserbasehq/sdk": "*" - "@gomomento/sdk": ^1.51.1 - "@gomomento/sdk-core": ^1.51.1 - "@gomomento/sdk-web": ^1.51.1 - "@google-ai/generativelanguage": ^0.2.1 - "@google-cloud/storage": ^6.10.1 || ^7.7.0 - "@mendable/firecrawl-js": ^0.0.13 - "@notionhq/client": ^2.2.10 - "@pinecone-database/pinecone": "*" - "@supabase/supabase-js": ^2.10.0 - "@vercel/kv": ^0.2.3 - "@xata.io/client": ^0.28.0 - apify-client: ^2.7.1 - assemblyai: ^4.0.0 - axios: "*" - cheerio: ^1.0.0-rc.12 - chromadb: "*" - convex: ^1.3.1 - couchbase: ^4.3.0 - d3-dsv: ^2.0.0 - epub2: ^3.0.1 - fast-xml-parser: "*" - google-auth-library: ^8.9.0 - handlebars: ^4.7.8 - html-to-text: ^9.0.5 - ignore: ^5.2.0 - ioredis: ^5.3.2 - jsdom: "*" - mammoth: ^1.6.0 - mongodb: ">=5.2.0" - node-llama-cpp: "*" - notion-to-md: ^3.1.0 - officeparser: ^4.0.4 - pdf-parse: 1.1.1 - peggy: ^3.0.2 - playwright: ^1.32.1 - puppeteer: ^19.7.2 - pyodide: ^0.24.1 - redis: ^4.6.4 - sonix-speech-recognition: ^2.1.1 - srt-parser-2: ^1.2.3 - typeorm: ^0.3.12 - weaviate-ts-client: "*" - web-auth-library: ^1.0.3 - ws: ^8.14.2 - youtube-transcript: ^1.0.6 - youtubei.js: ^9.1.0 - peerDependenciesMeta: - "@aws-sdk/client-s3": - optional: true - "@aws-sdk/client-sagemaker-runtime": - optional: true - "@aws-sdk/client-sfn": - optional: true - "@aws-sdk/credential-provider-node": - optional: true - "@azure/storage-blob": - optional: true - "@browserbasehq/sdk": - optional: true - "@gomomento/sdk": - optional: true - "@gomomento/sdk-core": - optional: true - "@gomomento/sdk-web": - optional: true - "@google-ai/generativelanguage": - optional: true - "@google-cloud/storage": - optional: true - "@mendable/firecrawl-js": - optional: true - "@notionhq/client": - optional: true - "@pinecone-database/pinecone": - optional: true - "@supabase/supabase-js": - optional: true - "@vercel/kv": - optional: true - "@xata.io/client": - optional: true - apify-client: - optional: true - assemblyai: - optional: true - axios: - optional: true - cheerio: - optional: true - chromadb: - optional: true - convex: - optional: true - couchbase: - optional: true - d3-dsv: - optional: true - epub2: - optional: true - faiss-node: - optional: true - fast-xml-parser: - optional: true - google-auth-library: - optional: true - handlebars: - optional: true - html-to-text: - optional: true - ignore: - optional: true - ioredis: - optional: true - jsdom: - optional: true - mammoth: - optional: true - mongodb: - optional: true - node-llama-cpp: - optional: true - notion-to-md: - optional: true - officeparser: - optional: true - pdf-parse: - optional: true - peggy: - optional: true - playwright: - optional: true - puppeteer: - optional: true - pyodide: - optional: true - redis: - optional: true - sonix-speech-recognition: - optional: true - srt-parser-2: - optional: true - typeorm: - optional: true - weaviate-ts-client: - optional: true - web-auth-library: - optional: true - ws: - optional: true - youtube-transcript: - optional: true - youtubei.js: - optional: true - checksum: 6c1106d02c7198db973f0714cd71fb4c11a28a6630818f39ec07c2e0af573af64118bef49d7eb4cea3e2b2b701a2644fb95021377f75b8a9862e6faf9fa8a93a + resolution: "keyv@npm:4.5.4" + dependencies: + json-buffer: 3.0.1 + checksum: 74a24395b1c34bd44ad5cb2b49140d087553e170625240b86755a6604cd65aa16efdbdeae5cdb17ba1284a0fbb25ad06263755dbc71b8d8b06f74232ce3cdd72 languageName: node linkType: hard -"langchainhub@npm:~0.0.8": - version: 0.0.11 - resolution: "langchainhub@npm:0.0.11" - checksum: 511371a6d9f277ddb0425b830afe41b029cf101becfa8ac55c3e7bf3dba2191d13772130b3c3d99f39d25f3bb22345808e0e8ce956296f49c728f8713072ce0b +"kind-of@npm:^6.0.2": + version: 6.0.3 + resolution: "kind-of@npm:6.0.3" + checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b + languageName: node + linkType: hard + +"kleur@npm:^3.0.3": + version: 3.0.3 + resolution: "kleur@npm:3.0.3" + checksum: df82cd1e172f957bae9c536286265a5cdbd5eeca487cb0a3b2a7b41ef959fc61f8e7c0e9aeea9c114ccf2c166b6a8dd45a46fd619c1c569d210ecd2765ad5169 + languageName: node + linkType: hard + +"kleur@npm:^4.0.3": + version: 4.1.5 + resolution: "kleur@npm:4.1.5" + checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 + languageName: node + linkType: hard + +"klona@npm:^2.0.4": + version: 2.0.6 + resolution: "klona@npm:2.0.6" + checksum: ac9ee3732e42b96feb67faae4d27cf49494e8a3bf3fa7115ce242fe04786788e0aff4741a07a45a2462e2079aa983d73d38519c85d65b70ef11447bbc3c58ce7 + languageName: node + linkType: hard + +"kuler@npm:^2.0.0": + version: 2.0.0 + resolution: "kuler@npm:2.0.0" + checksum: 9e10b5a1659f9ed8761d38df3c35effabffbd19fc6107324095238e4ef0ff044392cae9ac64a1c2dda26e532426485342226b93806bd97504b174b0dcf04ed81 + languageName: node + linkType: hard + +"langchain@npm:1.2.25": + version: 1.2.25 + resolution: "langchain@npm:1.2.25" + dependencies: + "@langchain/langgraph": ^1.1.2 + "@langchain/langgraph-checkpoint": ^1.0.0 + langsmith: ">=0.5.0 <1.0.0" + uuid: ^10.0.0 + zod: ^3.25.76 || ^4 + peerDependencies: + "@langchain/core": ^1.1.26 + checksum: 2336ca4ec42cb9fcec854c08c2515ef7f938fe924246f87f8ad612709c9f345ce95d61354e062e05e6f87bb6206d84b62b79441006df0209a53d253aee02e4e3 languageName: node linkType: hard -"langsmith@npm:^0.1.56-rc.1, langsmith@npm:~0.1.1, langsmith@npm:~0.1.7": - version: 0.1.68 - resolution: "langsmith@npm:0.1.68" +"langsmith@npm:>=0.4.0 <1.0.0, langsmith@npm:>=0.5.0 <1.0.0": + version: 0.5.4 + resolution: "langsmith@npm:0.5.4" dependencies: "@types/uuid": ^10.0.0 - commander: ^10.0.1 + chalk: ^4.1.2 + console-table-printer: ^2.12.1 p-queue: ^6.6.2 - p-retry: 4 semver: ^7.6.3 uuid: ^10.0.0 peerDependencies: + "@opentelemetry/api": "*" + "@opentelemetry/exporter-trace-otlp-proto": "*" + "@opentelemetry/sdk-trace-base": "*" openai: "*" peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@opentelemetry/exporter-trace-otlp-proto": + optional: true + "@opentelemetry/sdk-trace-base": + optional: true openai: optional: true - checksum: b5422f9d52d22992b928089704233d7c67581744058f141f78dc9e7e0b789bf0b7cd7e0a964a23d7777d9d5bc55e3a0f2f86f760d6b23844817b2ad80367d704 + checksum: 11b68ced854b9dbefa2917f091069b9e62f0de327073e995021d6d57ecbbdc91d5cf87a877a485e9c455a4e0f194c4cb7673ec89633d74258d677f44ac91da0a languageName: node linkType: hard @@ -20050,7 +19893,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.0.1, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21": +"lodash@npm:4.17.21, lodash@npm:^4.0.1, lodash@npm:^4.17.11, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -20395,6 +20238,13 @@ __metadata: languageName: node linkType: hard +"math-expression-evaluator@npm:^2.0.0": + version: 2.0.7 + resolution: "math-expression-evaluator@npm:2.0.7" + checksum: af644ba331b7463176b21a7e2d379e63bc5a975af072c22c6dd22cf94d5a2c444d1ca9fd7e9ba808925bce77e0a201e1444eb44b23e25c479573ff85cc7c84f9 + languageName: node + linkType: hard + "math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" @@ -20995,7 +20845,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.3": +"minimatch@npm:^9.0.3, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" dependencies: @@ -21133,7 +20983,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.4": +"mkdirp@npm:^0.5.4": version: 0.5.6 resolution: "mkdirp@npm:0.5.6" dependencies: @@ -21162,52 +21012,6 @@ __metadata: languageName: node linkType: hard -"ml-array-mean@npm:^1.1.6": - version: 1.1.6 - resolution: "ml-array-mean@npm:1.1.6" - dependencies: - ml-array-sum: ^1.1.6 - checksum: 81999dac8bad3bf2dafb23a9bc71883879b9d55889e48d00b91dd4a2568957a6f5373632ae57324760d1e1d7d29ad45ab4ea7ae32de67ce144d57a21e36dd9c2 - languageName: node - linkType: hard - -"ml-array-sum@npm:^1.1.6": - version: 1.1.6 - resolution: "ml-array-sum@npm:1.1.6" - dependencies: - is-any-array: ^2.0.0 - checksum: 369dbb3681e3f8b0d0facba9fcfc981656dac49a80924859c3ed8f0a5880fb6db2d6e534f8b7b9c3cda59248152e61b27d6419d19c69539de7c3aa6aea3094eb - languageName: node - linkType: hard - -"ml-distance-euclidean@npm:^2.0.0": - version: 2.0.0 - resolution: "ml-distance-euclidean@npm:2.0.0" - checksum: e31f98a947ce6971c35d74e6d2521800f0d219efb34c78b20b5f52debd206008d52e677685c09839e6bab5d2ed233aa009314236e4e548d5fafb60f2f71e2b3e - languageName: node - linkType: hard - -"ml-distance@npm:^4.0.0": - version: 4.0.1 - resolution: "ml-distance@npm:4.0.1" - dependencies: - ml-array-mean: ^1.1.6 - ml-distance-euclidean: ^2.0.0 - ml-tree-similarity: ^1.0.0 - checksum: 21ea014064eb7795c6c8c16e76bb834cba73f9f1ee2f761a3c3c34536f70bd6299b044dd05c495c533f5bdfea7401011dd4bdd159545ef69f5a021f5be4c77a2 - languageName: node - linkType: hard - -"ml-tree-similarity@npm:^1.0.0": - version: 1.0.0 - resolution: "ml-tree-similarity@npm:1.0.0" - dependencies: - binary-search: ^1.3.5 - num-sort: ^2.0.0 - checksum: f99e217dc94acf75c089469dc3c278f388146e43c82212160b6b75daa14309902f84eb0a00c67d502fc79dc171cf15a33d392326e024b2e89881adc585d15513 - languageName: node - linkType: hard - "mlly@npm:^1.7.4": version: 1.8.0 resolution: "mlly@npm:1.8.0" @@ -21473,7 +21277,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -21556,7 +21360,7 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.11, nanoid@npm:^3.3.4, nanoid@npm:^3.3.6, nanoid@npm:^3.3.7": +"nanoid@npm:^3.3.11, nanoid@npm:^3.3.4, nanoid@npm:^3.3.6": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -21898,13 +21702,6 @@ __metadata: languageName: node linkType: hard -"node-domexception@npm:1.0.0": - version: 1.0.0 - resolution: "node-domexception@npm:1.0.0" - checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f - languageName: node - linkType: hard - "node-emoji@npm:1.11.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" @@ -21921,7 +21718,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.0.0, node-fetch@npm:^2.6.1": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -22046,10 +21843,10 @@ __metadata: languageName: node linkType: hard -"nodemailer@npm:^6.9.9": - version: 6.10.1 - resolution: "nodemailer@npm:6.10.1" - checksum: 39e9208e13c40b58c59242205bce855e74def25d953070ab6a5b1c23b0bf37df0725b3c6dea52d4220e2e60dd1fffcd486a697dece79afdf98842aae6395d393 +"nodemailer@npm:7.0.11": + version: 7.0.11 + resolution: "nodemailer@npm:7.0.11" + checksum: 02940a5a620e219522c049f54f69af7abdc99c2ec168f592d3c67e26980a9857d7c45c65989cbd835af2c0ffabc66c20198aff484197566385594b2653069cb8 languageName: node linkType: hard @@ -22160,13 +21957,6 @@ __metadata: languageName: node linkType: hard -"num-sort@npm:^2.0.0": - version: 2.1.0 - resolution: "num-sort@npm:2.1.0" - checksum: 5a80cd0456c8847f71fb80ad3c3596714cebede76de585aa4fed2b9a4fb0907631edca1f7bb31c24dbb9928b66db3d03059994cc365d2ae011b80ddddac28f6e - languageName: node - linkType: hard - "number-is-nan@npm:^1.0.0": version: 1.0.1 resolution: "number-is-nan@npm:1.0.1" @@ -22404,20 +22194,12 @@ __metadata: languageName: node linkType: hard -"openai@npm:^4.32.1, openai@npm:^4.41.1": - version: 4.104.0 - resolution: "openai@npm:4.104.0" - dependencies: - "@types/node": ^18.11.18 - "@types/node-fetch": ^2.6.4 - abort-controller: ^3.0.0 - agentkeepalive: ^4.2.1 - form-data-encoder: 1.7.2 - formdata-node: ^4.3.2 - node-fetch: ^2.6.7 +"openai@npm:^6.18.0": + version: 6.22.0 + resolution: "openai@npm:6.22.0" peerDependencies: ws: ^8.18.0 - zod: ^3.23.8 + zod: ^3.25 || ^4.0 peerDependenciesMeta: ws: optional: true @@ -22425,7 +22207,7 @@ __metadata: optional: true bin: openai: bin/cli - checksum: 2bd3ba14a37a3421703d9108bdb61be38d2555fbefbfb491c890d3c47cef72f5c9fefada78f33923f86e5d2ada86bcb3a3c04662f16e85f735a2c1ecd0fa031e + checksum: f4781328eb719c118b8b4190f589356b6cb13b7f180b3069a8a0c6171daa78f9992b3d336acffeeeed8d433ba53cfd2348eeace316abc2931352ade83c3a075b languageName: node linkType: hard @@ -22461,7 +22243,7 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.9.1": +"optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" dependencies: @@ -22651,13 +22433,22 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:4": - version: 4.6.2 - resolution: "p-retry@npm:4.6.2" +"p-queue@npm:^9.0.1": + version: 9.1.0 + resolution: "p-queue@npm:9.1.0" + dependencies: + eventemitter3: ^5.0.1 + p-timeout: ^7.0.0 + checksum: 30b9b4a36a9b2d1aa372ad58f3eb1a89da1e98cfed077972016d6b1dd0309b3f55252f3b775533ada98b81ac547e5ac0946ccf72f6ba338a5dfc40c05e596fc9 + languageName: node + linkType: hard + +"p-retry@npm:^7.1.1": + version: 7.1.1 + resolution: "p-retry@npm:7.1.1" dependencies: - "@types/retry": 0.12.0 - retry: ^0.13.1 - checksum: 45c270bfddaffb4a895cea16cb760dcc72bdecb6cb45fef1971fa6ea2e91ddeafddefe01e444ac73e33b1b3d5d29fb0dd18a7effb294262437221ddc03ce0f2e + is-network-error: ^1.1.0 + checksum: ae5ac18118e16fcb968095e6aee4ca9f82398f14b061177e00eaa949f6a0752b73afb41680193b56870475213989ef8bb7eacbacde827383b7f460d2de485ff1 languageName: node linkType: hard @@ -22670,6 +22461,13 @@ __metadata: languageName: node linkType: hard +"p-timeout@npm:^7.0.0": + version: 7.0.1 + resolution: "p-timeout@npm:7.0.1" + checksum: 696d5e3b46cd4f81e1c6ec37e8f8555e56441c3e5e98cff080f795edd423b1b0a42c4f113f7a0369842994e9c2ea017bf10b9101574f7e5a477c6638664be84d + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -23402,7 +23200,7 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.0, progress@npm:^2.0.1": +"progress@npm:^2.0.1": version: 2.0.3 resolution: "progress@npm:2.0.3" checksum: f67403fe7b34912148d9252cb7481266a354bd99ce82c835f79070643bb3c6583d10dbcfda4d41e04bbc1d8437e9af0fb1e1f2135727878f5308682a579429b7 @@ -24998,13 +24796,6 @@ __metadata: languageName: node linkType: hard -"retry@npm:^0.13.1": - version: 0.13.1 - resolution: "retry@npm:0.13.1" - checksum: 47c4d5be674f7c13eee4cfe927345023972197dbbdfba5d3af7e461d13b44de1bfd663bfc80d2f601f8ef3fc8164c16dd99655a221921954a65d044a2fc1233b - languageName: node - linkType: hard - "reusify@npm:^1.0.4": version: 1.1.0 resolution: "reusify@npm:1.1.0" @@ -25028,17 +24819,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:2.6.3, rimraf@npm:~2.6.2": - version: 2.6.3 - resolution: "rimraf@npm:2.6.3" - dependencies: - glob: ^7.1.3 - bin: - rimraf: ./bin.js - checksum: 3ea587b981a19016297edb96d1ffe48af7e6af69660e3b371dbfc73722a73a0b0e9be5c88089fbeeb866c389c1098e07f64929c7414290504b855f54f901ab10 - languageName: node - linkType: hard - "rimraf@npm:3.0.2, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" @@ -25061,6 +24841,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:~2.6.2": + version: 2.6.3 + resolution: "rimraf@npm:2.6.3" + dependencies: + glob: ^7.1.3 + bin: + rimraf: ./bin.js + checksum: 3ea587b981a19016297edb96d1ffe48af7e6af69660e3b371dbfc73722a73a0b0e9be5c88089fbeeb866c389c1098e07f64929c7414290504b855f54f901ab10 + languageName: node + linkType: hard + "ringbufferjs@npm:^2.0.0": version: 2.0.0 resolution: "ringbufferjs@npm:2.0.0" @@ -25149,7 +24940,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.3.0, rxjs@npm:^7.5.1, rxjs@npm:^7.5.4, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1": +"rxjs@npm:^7.3.0, rxjs@npm:^7.5.1, rxjs@npm:^7.5.5, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -25327,13 +25118,6 @@ __metadata: languageName: node linkType: hard -"secure-json-parse@npm:^2.7.0": - version: 2.7.0 - resolution: "secure-json-parse@npm:2.7.0" - checksum: d9d7d5a01fc6db6115744ba23cf9e67ecfe8c524d771537c062ee05ad5c11b64c730bc58c7f33f60bd6877f96b86f0ceb9ea29644e4040cb757f6912d4dd6737 - languageName: node - linkType: hard - "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.6.0, semver@npm:^5.7.1": version: 5.7.2 resolution: "semver@npm:5.7.2" @@ -25352,7 +25136,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.2.1, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.1, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.3, semver@npm:^7.7.2": +"semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.1, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.3, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -25361,6 +25145,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + languageName: node + linkType: hard + "semver@npm:~7.0.0": version: 7.0.0 resolution: "semver@npm:7.0.0" @@ -25824,6 +25617,13 @@ __metadata: languageName: node linkType: hard +"simple-wcswidth@npm:^1.1.2": + version: 1.1.2 + resolution: "simple-wcswidth@npm:1.1.2" + checksum: 210eea36d28fb8dbadb1dcf5a19e747c1435548ec56bfd86f1c79fb9ddf676b252d84632d3ab9a787251281c499520e63bca1d9f5997cbd66c25996c10e056b1 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -25853,17 +25653,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^2.1.0": - version: 2.1.0 - resolution: "slice-ansi@npm:2.1.0" - dependencies: - ansi-styles: ^3.2.0 - astral-regex: ^1.0.0 - is-fullwidth-code-point: ^2.0.0 - checksum: 4e82995aa59cef7eb03ef232d73c2239a15efa0ace87a01f3012ebb942e963fbb05d448ce7391efcd52ab9c32724164aba2086f5143e0445c969221dde3b6b1e - languageName: node - linkType: hard - "slice-ansi@npm:^3.0.0": version: 3.0.0 resolution: "slice-ansi@npm:3.0.0" @@ -26166,17 +25955,6 @@ __metadata: languageName: node linkType: hard -"sswr@npm:^2.1.0": - version: 2.2.0 - resolution: "sswr@npm:2.2.0" - dependencies: - swrev: ^4.0.0 - peerDependencies: - svelte: ^4.0.0 || ^5.0.0 - checksum: 680b7c435368684788154313fc4b8318add4661453ffe45788e4a43c5c0eb4449574bcbb51457b9e0db372b80fc830dda1485c9d7f71462820208b8b90b340be - languageName: node - linkType: hard - "stack-trace@npm:0.0.x": version: 0.0.10 resolution: "stack-trace@npm:0.0.10" @@ -26238,15 +26016,15 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^7.4.5": - version: 7.6.20 - resolution: "storybook@npm:7.6.20" +"storybook@npm:7.6.21": + version: 7.6.21 + resolution: "storybook@npm:7.6.21" dependencies: - "@storybook/cli": 7.6.20 + "@storybook/cli": 7.6.21 bin: sb: ./index.js storybook: ./index.js - checksum: 7442d0bf404fafdfa6921d388b7af78e835bd4278ec7cc0c6565d3723cb1cd7d24621a97186abd553f1345ddf78628e517656af64bcb4a3d9e9c6253b01461e8 + checksum: a58bfff2a0c18a976d1095bb076f9c1228fe85f468398006402c43b70a47b73f906faab46700a4a4539f51a64d475253fc801510b5cf1edccdcfc998fbb46f1f languageName: node linkType: hard @@ -26338,24 +26116,6 @@ __metadata: languageName: node linkType: hard -"string-natural-compare@npm:^3.0.1": - version: 3.0.1 - resolution: "string-natural-compare@npm:3.0.1" - checksum: 65910d9995074086e769a68728395effbba9b7186be5b4c16a7fad4f4ef50cae95ca16e3e9086e019cbb636ae8daac9c7b8fe91b5f21865c5c0f26e3c0725406 - languageName: node - linkType: hard - -"string-width@npm:^3.0.0": - version: 3.1.0 - resolution: "string-width@npm:3.1.0" - dependencies: - emoji-regex: ^7.0.1 - is-fullwidth-code-point: ^2.0.0 - strip-ansi: ^5.1.0 - checksum: 57f7ca73d201682816d573dc68bd4bb8e1dff8dc9fcf10470fdfc3474135c97175fec12ea6a159e67339b41e86963112355b64529489af6e7e70f94a7caf08b2 - languageName: node - linkType: hard - "string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -26490,15 +26250,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^5.1.0": - version: 5.2.0 - resolution: "strip-ansi@npm:5.2.0" - dependencies: - ansi-regex: ^4.1.0 - checksum: bdb5f76ade97062bd88e7723aa019adbfacdcba42223b19ccb528ffb9fb0b89a5be442c663c4a3fb25268eaa3f6ea19c7c3fbae830bd1562d55adccae1fcec46 - languageName: node - linkType: hard - "strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -26554,7 +26305,7 @@ __metadata: languageName: node linkType: hard -"strip-json-comments@npm:^3.0.1, strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": +"strip-json-comments@npm:^3.0.1, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" checksum: 492f73e27268f9b1c122733f28ecb0e7e8d8a531a6662efbd08e22cccb3f9475e90a1b82cab06a392f6afae6d2de636f977e231296400d0ec5304ba70f166443 @@ -26568,6 +26319,15 @@ __metadata: languageName: node linkType: hard +"strtok3@npm:^10.2.0": + version: 10.3.4 + resolution: "strtok3@npm:10.3.4" + dependencies: + "@tokenizer/token": ^0.3.0 + checksum: 7faf008cdf7f96c3d066d349116affbd5e5a49da0f03520299a9a04ad55d57b338d368231d469a64d358a86a03c2a0a3b67d61aa124000c941f65093ada9ae98 + languageName: node + linkType: hard + "style-loader@npm:^2.0.0": version: 2.0.0 resolution: "style-loader@npm:2.0.0" @@ -26768,34 +26528,6 @@ __metadata: languageName: node linkType: hard -"swr@npm:^2.2.5": - version: 2.3.6 - resolution: "swr@npm:2.3.6" - dependencies: - dequal: ^2.0.3 - use-sync-external-store: ^1.4.0 - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: c7cc7dfb73d2437a16951b7217f7a1e1b103cc19c85456a1da2cd07cefcb300fbd5a9a2df5abbbb608c92af8f77777c0c93e9c80e907f145cacf0bd8176360cb - languageName: node - linkType: hard - -"swrev@npm:^4.0.0": - version: 4.0.0 - resolution: "swrev@npm:4.0.0" - checksum: 454aed0e0367ef8faabfbe46e83e088199874beaa24c6a0e3aa58e10ec4464e33514d559f0e7be5adcbadffd3323c1b096c0625aaa13f029cbcc03ad40590a2f - languageName: node - linkType: hard - -"swrv@npm:^1.0.4": - version: 1.1.0 - resolution: "swrv@npm:1.1.0" - peerDependencies: - vue: ">=3.2.26 < 4" - checksum: 997fcd97692eeaae01766f8d63af8d4a1d1117428fbc4df4f2ab9b796daff47ecebcae70d276640f112cf7fc62ad6f1977208429538e7bf76cb48045e231f48f - languageName: node - linkType: hard - "symbol-observable@npm:4.0.0": version: 4.0.0 resolution: "symbol-observable@npm:4.0.0" @@ -26837,18 +26569,6 @@ __metadata: languageName: node linkType: hard -"table@npm:^5.2.3": - version: 5.4.6 - resolution: "table@npm:5.4.6" - dependencies: - ajv: ^6.10.2 - lodash: ^4.17.14 - slice-ansi: ^2.1.0 - string-width: ^3.0.0 - checksum: 9e35d3efa788edc17237eef8852f8e4b9178efd65a7d115141777b2ee77df4b7796c05f4ed3712d858f98894ac5935a481ceeb6dcb9895e2f67a61cce0e63b6c - languageName: node - linkType: hard - "tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.3 resolution: "tapable@npm:2.2.3" @@ -27024,13 +26744,6 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: b6937a38c80c7f84d9c11dd75e49d5c44f71d95e810a3250bd1f1797fc7117c57698204adf676b71497acc205d769d65c16ae8fa10afad832ae1322630aef10a - languageName: node - linkType: hard - "textextensions@npm:^5.14.0": version: 5.16.0 resolution: "textextensions@npm:5.16.0" @@ -27071,13 +26784,6 @@ __metadata: languageName: node linkType: hard -"throttleit@npm:2.1.0": - version: 2.1.0 - resolution: "throttleit@npm:2.1.0" - checksum: a2003947aafc721c4a17e6f07db72dc88a64fa9bba0f9c659f7997d30f9590b3af22dadd6a41851e0e8497d539c33b2935c2c7919cf4255922509af6913c619b - languageName: node - linkType: hard - "throttleit@npm:^1.0.0": version: 1.0.1 resolution: "throttleit@npm:1.0.1" @@ -27146,7 +26852,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -27240,6 +26946,17 @@ __metadata: languageName: node linkType: hard +"token-types@npm:^6.0.0": + version: 6.1.2 + resolution: "token-types@npm:6.1.2" + dependencies: + "@borewit/text-codec": ^0.2.1 + "@tokenizer/token": ^0.3.0 + ieee754: ^1.2.1 + checksum: ddade9c99fdf8636ff765f7280798cd8204de8cc99a6bffa42a70c0166ca562817617426f4c7fbeb7c66c7ffd22900c02297a19bd7db0622b7458deded184d16 + languageName: node + linkType: hard + "touch@npm:^3.1.0": version: 3.1.1 resolution: "touch@npm:3.1.1" @@ -27325,6 +27042,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^2.4.0": + version: 2.4.0 + resolution: "ts-api-utils@npm:2.4.0" + peerDependencies: + typescript: ">=4.8.4" + checksum: beae72a4fa22a7cc91a8a0f3dfb487d72e30f06ac50ff72f327d061dea2d4940c6451d36578d949caad3893d4d2c7d42d53b7663597ccda54ad32cdb842c3e34 + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0, ts-dedent@npm:^2.2.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -27726,23 +27452,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.3.5": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" +"typescript@npm:^5.0.4": + version: 5.9.2 + resolution: "typescript@npm:5.9.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db + checksum: f619cf6773cfe31409279711afd68cdf0859780006c50bc2a7a0c3227f85dea89a3b97248846326f3a17dad72ea90ec27cf61a8387772c680b2252fd02d8497b languageName: node linkType: hard -"typescript@npm:^5.0.4": - version: 5.9.2 - resolution: "typescript@npm:5.9.2" +"typescript@npm:^5.3.0": + version: 5.9.3 + resolution: "typescript@npm:5.9.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: f619cf6773cfe31409279711afd68cdf0859780006c50bc2a7a0c3227f85dea89a3b97248846326f3a17dad72ea90ec27cf61a8387772c680b2252fd02d8497b + checksum: 0d0ffb84f2cd072c3e164c79a2e5a1a1f4f168e84cb2882ff8967b92afe1def6c2a91f6838fb58b168428f9458c57a2ba06a6737711fdd87a256bbe83e9a217f languageName: node linkType: hard @@ -27756,23 +27482,23 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^4.3.5#~builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" +"typescript@patch:typescript@^5.0.4#~builtin": + version: 5.9.2 + resolution: "typescript@patch:typescript@npm%3A5.9.2#~builtin::version=5.9.2&hash=f3b441" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1f8f3b6aaea19f0f67cba79057674ba580438a7db55057eb89cc06950483c5d632115c14077f6663ea76fd09fce3c190e6414bb98582ec80aa5a4eaf345d5b68 + checksum: e42a701947325500008334622321a6ad073f842f5e7d5e7b588a6346b31fdf51d56082b9ce5cef24312ecd3e48d6c0d4d44da7555f65e2feec18cf62ec540385 languageName: node linkType: hard -"typescript@patch:typescript@^5.0.4#~builtin": - version: 5.9.2 - resolution: "typescript@patch:typescript@npm%3A5.9.2#~builtin::version=5.9.2&hash=f3b441" +"typescript@patch:typescript@^5.3.0#~builtin": + version: 5.9.3 + resolution: "typescript@patch:typescript@npm%3A5.9.3#~builtin::version=5.9.3&hash=f3b441" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: e42a701947325500008334622321a6ad073f842f5e7d5e7b588a6346b31fdf51d56082b9ce5cef24312ecd3e48d6c0d4d44da7555f65e2feec18cf62ec540385 + checksum: 8bb8d86819ac86a498eada254cad7fb69c5f74778506c700c2a712daeaff21d3a6f51fd0d534fe16903cb010d1b74f89437a3d02d4d0ff5ca2ba9a4660de8497 languageName: node linkType: hard @@ -27810,6 +27536,13 @@ __metadata: languageName: node linkType: hard +"uint8array-extras@npm:^1.4.0": + version: 1.5.0 + resolution: "uint8array-extras@npm:1.5.0" + checksum: e7fb42b57e73a5891fe3d5358ca6ac2e2db8e8666b1621220319e2dd0523965cd35562a8171c3e9b2ff26693cc658dc5bc1256e6f4935e4e4c13070d779bf76e + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -28251,7 +27984,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: @@ -28330,6 +28063,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^13.0.0": + version: 13.0.0 + resolution: "uuid@npm:13.0.0" + bin: + uuid: dist-node/bin/uuid + checksum: 7510ee1ab371be5339ef26ff8cabc2f4a2c60640ff880652968f758072f53bd4f4af1c8b0e671a8c9bb29ef926a24dec3ef0e3861d78183b39291a85743a9f96 + languageName: node + linkType: hard + "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -28369,13 +28111,6 @@ __metadata: languageName: node linkType: hard -"v8-compile-cache@npm:^2.0.3": - version: 2.4.0 - resolution: "v8-compile-cache@npm:2.4.0" - checksum: 8eb6ddb59d86f24566503f1e6ca98f3e6f43599f05359bd3ab737eaaf1585b338091478a4d3d5c2646632cf8030288d7888684ea62238cdce15a65ae2416718f - languageName: node - linkType: hard - "v8-to-istanbul@npm:^9.0.1": version: 9.3.0 resolution: "v8-to-istanbul@npm:9.3.0" @@ -28475,18 +28210,18 @@ __metadata: languageName: node linkType: hard -"wait-on@npm:^6.0.1": - version: 6.0.1 - resolution: "wait-on@npm:6.0.1" +"wait-on@npm:9.0.3": + version: 9.0.3 + resolution: "wait-on@npm:9.0.3" dependencies: - axios: ^0.25.0 - joi: ^17.6.0 + axios: ^1.13.2 + joi: ^18.0.1 lodash: ^4.17.21 - minimist: ^1.2.5 - rxjs: ^7.5.4 + minimist: ^1.2.8 + rxjs: ^7.8.2 bin: wait-on: bin/wait-on - checksum: e4d62aa4145d99fe34747ccf7506d4b4d6e60dd677c0eb18a51e316d38116ace2d194e4b22a9eb7b767b0282f39878ddcc4ae9440dcb0c005c9150668747cf5b + checksum: 088472eb3c997ef6607f7042f44103c391e49310db8fdc15edd16823bf0c11a93653d02a6125fb71f8be36065e8b02cb1a133e9cdc110cfa0862913bff094729 languageName: node linkType: hard @@ -28525,20 +28260,6 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:4.0.0-beta.3": - version: 4.0.0-beta.3 - resolution: "web-streams-polyfill@npm:4.0.0-beta.3" - checksum: dfec1fbf52b9140e4183a941e380487b6c3d5d3838dd1259be81506c1c9f2abfcf5aeb670aeeecfd9dff4271a6d8fef931b193c7bedfb42542a3b05ff36c0d16 - languageName: node - linkType: hard - -"web-streams-polyfill@npm:^3.2.1": - version: 3.3.3 - resolution: "web-streams-polyfill@npm:3.3.3" - checksum: 21ab5ea08a730a2ef8023736afe16713b4f2023ec1c7085c16c8e293ee17ed085dff63a0ad8722da30c99c4ccbd4ccd1b2e79c861829f7ef2963d7de7004c2cb - languageName: node - linkType: hard - "webfontloader@npm:^1.6.28": version: 1.6.28 resolution: "webfontloader@npm:1.6.28" @@ -28938,15 +28659,6 @@ __metadata: languageName: node linkType: hard -"write@npm:1.0.3": - version: 1.0.3 - resolution: "write@npm:1.0.3" - dependencies: - mkdirp: ^0.5.1 - checksum: 6496197ceb2d6faeeb8b5fe2659ca804e801e4989dff9fb8a66fe76179ce4ccc378c982ef906733caea1220c8dbe05a666d82127959ac4456e70111af8b8df73 - languageName: node - linkType: hard - "ws@npm:8.17.1": version: 8.17.1 resolution: "ws@npm:8.17.1" @@ -29234,22 +28946,20 @@ __metadata: languageName: node linkType: hard -"zod-to-json-schema@npm:^3.22.3, zod-to-json-schema@npm:^3.22.5, zod-to-json-schema@npm:^3.23.3": - version: 3.24.6 - resolution: "zod-to-json-schema@npm:3.24.6" - peerDependencies: - zod: ^3.24.1 - checksum: 5f4d29597cfd88d8fb8a539f0169affb8705d67ee9cbe478aa01bb1d2554e0540ca713fa4ddeb2fd834e87e7cdff61fa396f6d1925a9006de70afe6cd68bf7d2 - languageName: node - linkType: hard - -"zod@npm:^3.21.4, zod@npm:^3.22.3, zod@npm:^3.22.4": +"zod@npm:^3.21.4": version: 3.25.76 resolution: "zod@npm:3.25.76" checksum: c9a403a62b329188a5f6bd24d5d935d2bba345f7ab8151d1baa1505b5da9f227fb139354b043711490c798e91f3df75991395e40142e6510a4b16409f302b849 languageName: node linkType: hard +"zod@npm:^3.25.76 || ^4": + version: 4.3.6 + resolution: "zod@npm:4.3.6" + checksum: 19cec761b46bae4b6e7e861ea740f3f248e50a6671825afc8a5758e27b35d6f20ccde9942422fd5cf6f8b697f18bd05ef8bb33f5f2db112ab25cc628de2fae47 + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4"