mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
Merge branch 'master' into migrate/brand-image-setting-to-function-component
This commit is contained in:
commit
ac0ea510b5
938 changed files with 40603 additions and 41043 deletions
3
.github/actions/webapp-setup/action.yml
vendored
3
.github/actions/webapp-setup/action.yml
vendored
|
|
@ -15,6 +15,9 @@ runs:
|
|||
path: |
|
||||
webapp/node_modules
|
||||
webapp/channels/node_modules
|
||||
webapp/platform/client/node_modules
|
||||
webapp/platform/components/node_modules
|
||||
webapp/platform/types/node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ hashFiles('webapp/package-lock.json') }}
|
||||
- name: ci/cache-platform-builds
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
|
|
|
|||
2
.github/workflows/api.yml
vendored
2
.github/workflows/api.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: "npm"
|
||||
|
|
|
|||
24
.github/workflows/build-server-image.yml
vendored
24
.github/workflows/build-server-image.yml
vendored
|
|
@ -33,8 +33,8 @@ jobs:
|
|||
- name: buildenv/docker-login
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: buildenv/build
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
|
|
@ -44,16 +44,16 @@ jobs:
|
|||
load: true
|
||||
push: false
|
||||
pull: false
|
||||
tags: mattermostdevelopment/mattermost-build-server:test
|
||||
tags: mattermost/mattermost-build-server:test
|
||||
|
||||
- name: buildenv/test
|
||||
run: |
|
||||
docker run --rm mattermostdevelopment/mattermost-build-server:test /bin/sh -c "go version && node --version"
|
||||
docker run --rm mattermost/mattermost-build-server:test /bin/sh -c "go version && node --version"
|
||||
|
||||
- name: buildenv/calculate-golang-version
|
||||
id: go
|
||||
run: |
|
||||
GO_VERSION=$(docker run --rm mattermostdevelopment/mattermost-build-server:test go version | awk '{print $3}' | sed 's/go//')
|
||||
GO_VERSION=$(docker run --rm mattermost/mattermost-build-server:test go version | awk '{print $3}' | sed 's/go//')
|
||||
echo "GO_VERSION=${GO_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: buildenv/push
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
load: false
|
||||
push: true
|
||||
pull: true
|
||||
tags: mattermostdevelopment/mattermost-build-server:${{ steps.go.outputs.GO_VERSION }}
|
||||
tags: mattermost/mattermost-build-server:${{ steps.go.outputs.GO_VERSION }}
|
||||
|
||||
build-image-fips:
|
||||
runs-on: ubuntu-22.04
|
||||
|
|
@ -79,8 +79,8 @@ jobs:
|
|||
- name: buildenv/docker-login
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_DEV_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_DEV_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: buildenv/build
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
|
|
@ -90,16 +90,16 @@ jobs:
|
|||
load: true
|
||||
push: false
|
||||
pull: false
|
||||
tags: mattermostdevelopment/mattermost-build-server-fips:test
|
||||
tags: mattermost/mattermost-build-server-fips:test
|
||||
|
||||
- name: buildenv/test
|
||||
run: |
|
||||
docker run --rm --entrypoint bash mattermostdevelopment/mattermost-build-server-fips:test -c "go version && node --version"
|
||||
docker run --rm --entrypoint bash mattermost/mattermost-build-server-fips:test -c "go version && node --version"
|
||||
|
||||
- name: buildenv/calculate-golang-version
|
||||
id: go
|
||||
run: |
|
||||
GO_VERSION=$(docker run --rm --entrypoint bash mattermostdevelopment/mattermost-build-server-fips:test -c "go version" | awk '{print $3}' | sed 's/go//')
|
||||
GO_VERSION=$(docker run --rm --entrypoint bash mattermost/mattermost-build-server-fips:test -c "go version" | awk '{print $3}' | sed 's/go//')
|
||||
echo "GO_VERSION=${GO_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: buildenv/push
|
||||
|
|
@ -111,4 +111,4 @@ jobs:
|
|||
load: false
|
||||
push: true
|
||||
pull: true
|
||||
tags: mattermostdevelopment/mattermost-build-server-fips:${{ steps.go.outputs.GO_VERSION }}
|
||||
tags: mattermost/mattermost-build-server-fips:${{ steps.go.outputs.GO_VERSION }}
|
||||
|
|
|
|||
10
.github/workflows/e2e-tests-ci-template.yml
vendored
10
.github/workflows/e2e-tests-ci-template.yml
vendored
|
|
@ -133,7 +133,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
- name: ci/setup-node
|
||||
if: "${{ inputs.run_preflight_checks }}"
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
|
@ -164,7 +164,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
- name: ci/setup-node
|
||||
if: "${{ inputs.run_preflight_checks }}"
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
|
@ -246,7 +246,7 @@ jobs:
|
|||
ref: ${{ inputs.commit_sha }}
|
||||
fetch-depth: 0
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
|
@ -333,7 +333,7 @@ jobs:
|
|||
ln -sfn /usr/local/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose
|
||||
sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
|
@ -412,7 +412,7 @@ jobs:
|
|||
e2e-tests/${{ inputs.TEST }}/results/
|
||||
- name: ci/setup-node
|
||||
if: "${{ inputs.enable_reporting }}"
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
id: setup_node
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
|
|
|||
2
.github/workflows/mmctl-test-template.yml
vendored
2
.github/workflows/mmctl-test-template.yml
vendored
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
echo "BUILD_IMAGE=mattermost/mattermost-build-server-fips:${{ inputs.go-version }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "LOG_ARTIFACT_NAME=${{ inputs.logsartifact }}-fips" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "BUILD_IMAGE=mattermostdevelopment/mattermost-build-server:${{ inputs.go-version }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "BUILD_IMAGE=mattermost/mattermost-build-server:${{ inputs.go-version }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "LOG_ARTIFACT_NAME=${{ inputs.logsartifact }}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
|
|
|
|||
26
.github/workflows/server-ci.yml
vendored
26
.github/workflows/server-ci.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
name: Check mocks
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
name: Check go mod tidy
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -74,7 +74,7 @@ jobs:
|
|||
name: check-style
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -91,7 +91,7 @@ jobs:
|
|||
name: Check serialization methods for hot structs
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -108,7 +108,7 @@ jobs:
|
|||
name: Vet API
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -123,7 +123,7 @@ jobs:
|
|||
name: Check migration files
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -138,7 +138,7 @@ jobs:
|
|||
name: Generate email templates
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -155,7 +155,7 @@ jobs:
|
|||
name: Check store layers
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -172,7 +172,7 @@ jobs:
|
|||
name: Check mmctl docs
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -271,7 +271,7 @@ jobs:
|
|||
name: Build mattermost server app
|
||||
needs: go
|
||||
runs-on: ubuntu-22.04
|
||||
container: mattermostdevelopment/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
container: mattermost/mattermost-build-server:${{ needs.go.outputs.version }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: server
|
||||
|
|
@ -282,6 +282,12 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout mattermost project
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/setup-node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: "npm"
|
||||
cache-dependency-path: "webapp/package-lock.json"
|
||||
- name: Run setup-go-work
|
||||
run: make setup-go-work
|
||||
- name: Build
|
||||
|
|
|
|||
2
.github/workflows/server-test-template.yml
vendored
2
.github/workflows/server-test-template.yml
vendored
|
|
@ -59,7 +59,7 @@ jobs:
|
|||
echo "BUILD_IMAGE=mattermost/mattermost-build-server-fips:${{ inputs.go-version }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "LOG_ARTIFACT_NAME=${{ inputs.logsartifact }}-fips" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
echo "BUILD_IMAGE=mattermostdevelopment/mattermost-build-server:${{ inputs.go-version }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "BUILD_IMAGE=mattermost/mattermost-build-server:${{ inputs.go-version }}" >> "${GITHUB_OUTPUT}"
|
||||
echo "LOG_ARTIFACT_NAME=${{ inputs.logsartifact }}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
|
||||
|
|
|
|||
12
.github/workflows/webapp-ci.yml
vendored
12
.github/workflows/webapp-ci.yml
vendored
|
|
@ -41,18 +41,10 @@ jobs:
|
|||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: ci/setup
|
||||
uses: ./.github/actions/webapp-setup
|
||||
- name: ci/lint
|
||||
- name: ci/i18n-extract
|
||||
working-directory: webapp/channels
|
||||
run: |
|
||||
cp src/i18n/en.json /tmp/en.json
|
||||
mkdir -p /tmp/fake-mobile-dir/assets/base/i18n/
|
||||
echo '{}' > /tmp/fake-mobile-dir/assets/base/i18n/en.json
|
||||
npm run mmjstool -- i18n extract-webapp --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
||||
diff /tmp/en.json src/i18n/en.json
|
||||
# Address weblate behavior which does not remove whole translation item when translation string is set to empty
|
||||
npm run mmjstool -- i18n clean-empty --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir --check
|
||||
npm run mmjstool -- i18n check-empty-src --webapp-dir ./src --mobile-dir /tmp/fake-mobile-dir
|
||||
rm -rf tmp
|
||||
npm run i18n-extract:check
|
||||
|
||||
check-types:
|
||||
needs: check-lint
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -161,5 +161,5 @@ docker-compose.override.yaml
|
|||
.env
|
||||
|
||||
**/CLAUDE.local.md
|
||||
CLAUDE.md
|
||||
**/CLAUDE.md
|
||||
.cursorrules
|
||||
|
|
|
|||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
|||
20.11
|
||||
24.11
|
||||
|
|
|
|||
52
NOTICE.txt
52
NOTICE.txt
|
|
@ -9695,41 +9695,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## oov/psd
|
||||
|
||||
This product contains 'psd' by oov.
|
||||
|
||||
A PSD/PSB file reader for go
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/oov/psd
|
||||
|
||||
* LICENSE: MIT
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 oov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## opensearch-project/opensearch-go
|
||||
|
|
@ -10896,6 +10861,21 @@ Internationalize React apps. This library provides React components and an API t
|
|||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-intl
|
||||
|
||||
This product contains 'react-intl' by Eric Ferraiuolo.
|
||||
|
||||
Internationalize React apps. This library provides React components and an API to format dates, numbers, and strings, including pluralization and handling translations.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://formatjs.github.io/docs/react-intl
|
||||
|
||||
* LICENSE: BSD-3-Clause
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## react-is
|
||||
|
|
@ -13051,5 +13031,3 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ build-v4: node_modules playbooks
|
|||
@cat $(V4_SRC)/posts.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/preferences.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/files.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/recaps.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/ai.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/uploads.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/jobs.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/system.yaml >> $(V4_YAML)
|
||||
|
|
|
|||
54
api/v4/source/ai.yaml
Normal file
54
api/v4/source/ai.yaml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/api/v4/ai/agents:
|
||||
get:
|
||||
tags:
|
||||
- ai
|
||||
summary: Get available AI agents
|
||||
description: >
|
||||
Retrieve all available AI agents from the AI plugin's bridge API.
|
||||
If a user ID is provided, only agents accessible to that user are returned.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: GetAIAgents
|
||||
responses:
|
||||
"200":
|
||||
description: AI agents retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AgentsResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalServerError"
|
||||
/api/v4/ai/services:
|
||||
get:
|
||||
tags:
|
||||
- ai
|
||||
summary: Get available AI services
|
||||
description: >
|
||||
Retrieve all available AI services from the AI plugin's bridge API.
|
||||
If a user ID is provided, only services accessible to that user
|
||||
(via their permitted bots) are returned.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: GetAIServices
|
||||
responses:
|
||||
"200":
|
||||
description: AI services retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServicesResponse"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalServerError"
|
||||
|
||||
|
|
@ -3704,7 +3704,7 @@ components:
|
|||
type: array
|
||||
description: list of users participating in this thread. only includes IDs unless 'extended' was set to 'true'
|
||||
items:
|
||||
$ref: "#/components/schemas/Post"
|
||||
$ref: "#/components/schemas/User"
|
||||
post:
|
||||
$ref: "#/components/schemas/Post"
|
||||
RelationalIntegrityCheckData:
|
||||
|
|
@ -4633,6 +4633,83 @@ components:
|
|||
active:
|
||||
type: boolean
|
||||
description: The active status of the policy.
|
||||
Recap:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for the recap
|
||||
user_id:
|
||||
type: string
|
||||
description: ID of the user who created the recap
|
||||
title:
|
||||
type: string
|
||||
description: AI-generated title for the recap (max 5 words)
|
||||
create_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The time in milliseconds the recap was created
|
||||
update_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The time in milliseconds the recap was last updated
|
||||
delete_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The time in milliseconds the recap was deleted
|
||||
read_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The time in milliseconds the recap was marked as read
|
||||
total_message_count:
|
||||
type: integer
|
||||
description: Total number of messages summarized across all channels
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, processing, completed, failed]
|
||||
description: Current status of the recap job
|
||||
bot_id:
|
||||
type: string
|
||||
description: ID of the AI agent/bot used to generate this recap
|
||||
channels:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/RecapChannel"
|
||||
description: List of channel summaries included in this recap
|
||||
RecapChannel:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for the recap channel
|
||||
recap_id:
|
||||
type: string
|
||||
description: ID of the parent recap
|
||||
channel_id:
|
||||
type: string
|
||||
description: ID of the channel that was summarized
|
||||
channel_name:
|
||||
type: string
|
||||
description: Display name of the channel
|
||||
highlights:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Key discussion points and important information from the channel
|
||||
action_items:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Tasks, todos, and action items mentioned in the channel
|
||||
source_post_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IDs of the posts used to generate this summary
|
||||
create_at:
|
||||
type: integer
|
||||
format: int64
|
||||
description: The time in milliseconds the recap channel was created
|
||||
externalDocs:
|
||||
description: Find out more about Mattermost
|
||||
url: 'https://about.mattermost.com'
|
||||
|
|
|
|||
|
|
@ -462,8 +462,10 @@ tags:
|
|||
description: Endpoints related to metrics, including the Client Performance Monitoring feature.
|
||||
- name: audit_logs
|
||||
description: Endpoints for managing audit log certificates and configuration.
|
||||
- name: ai
|
||||
description: Endpoints for interacting with AI agents and services.
|
||||
- name: recaps
|
||||
description: Endpoints for creating and managing AI-powered channel recaps that summarize unread messages.
|
||||
- name: agents
|
||||
description: Endpoints for interacting with AI agents and LLM services.
|
||||
servers:
|
||||
- url: "{your-mattermost-url}"
|
||||
variables:
|
||||
|
|
|
|||
240
api/v4/source/recaps.yaml
Normal file
240
api/v4/source/recaps.yaml
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
"/api/v4/recaps":
|
||||
post:
|
||||
tags:
|
||||
- recaps
|
||||
- ai
|
||||
summary: Create a channel recap
|
||||
description: >
|
||||
Create a new AI-powered recap for the specified channels. The recap will
|
||||
summarize unread messages in the selected channels, extracting highlights
|
||||
and action items. This creates a background job that processes the recap
|
||||
asynchronously. The recap is created for the authenticated user.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated. User must be a member of all specified channels.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: CreateRecap
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- channel_ids
|
||||
- title
|
||||
- agent_id
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Title for the recap
|
||||
channel_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of channel IDs to include in the recap
|
||||
minItems: 1
|
||||
agent_id:
|
||||
type: string
|
||||
description: ID of the AI agent to use for generating the recap
|
||||
description: Recap creation request
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
description: Recap creation successful. The recap will be processed asynchronously.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Recap"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
get:
|
||||
tags:
|
||||
- recaps
|
||||
- ai
|
||||
summary: Get current user's recaps
|
||||
description: >
|
||||
Get a paginated list of recaps created by the authenticated user.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: GetRecapsForUser
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: The page to select.
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
- name: per_page
|
||||
in: query
|
||||
description: The number of recaps per page.
|
||||
schema:
|
||||
type: integer
|
||||
default: 60
|
||||
responses:
|
||||
"200":
|
||||
description: Recaps retrieval successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Recap"
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"/api/v4/recaps/{recap_id}":
|
||||
get:
|
||||
tags:
|
||||
- recaps
|
||||
- ai
|
||||
summary: Get a specific recap
|
||||
description: >
|
||||
Get a recap by its ID, including all channel summaries. Only the authenticated
|
||||
user who created the recap can retrieve it.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated. Can only retrieve recaps created by the current user.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: GetRecap
|
||||
parameters:
|
||||
- name: recap_id
|
||||
in: path
|
||||
description: Recap GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Recap retrieval successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Recap"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
delete:
|
||||
tags:
|
||||
- recaps
|
||||
- ai
|
||||
summary: Delete a recap
|
||||
description: >
|
||||
Delete a recap by its ID. Only the authenticated user who created the recap
|
||||
can delete it.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated. Can only delete recaps created by the current user.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: DeleteRecap
|
||||
parameters:
|
||||
- name: recap_id
|
||||
in: path
|
||||
description: Recap GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Recap deletion successful
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/StatusOK"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"/api/v4/recaps/{recap_id}/read":
|
||||
post:
|
||||
tags:
|
||||
- recaps
|
||||
- ai
|
||||
summary: Mark a recap as read
|
||||
description: >
|
||||
Mark a recap as read by the authenticated user. This updates the recap's
|
||||
read status and timestamp.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated. Can only mark recaps created by the current user as read.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: MarkRecapAsRead
|
||||
parameters:
|
||||
- name: recap_id
|
||||
in: path
|
||||
description: Recap GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Recap marked as read successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Recap"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
"/api/v4/recaps/{recap_id}/regenerate":
|
||||
post:
|
||||
tags:
|
||||
- recaps
|
||||
- ai
|
||||
summary: Regenerate a recap
|
||||
description: >
|
||||
Regenerate a recap by its ID. This creates a new background job to
|
||||
regenerate the AI-powered recap with the latest messages from the
|
||||
specified channels.
|
||||
|
||||
##### Permissions
|
||||
|
||||
Must be authenticated. Can only regenerate recaps created by the current user.
|
||||
|
||||
__Minimum server version__: 11.2
|
||||
operationId: RegenerateRecap
|
||||
parameters:
|
||||
- name: recap_id
|
||||
in: path
|
||||
description: Recap GUID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Recap regeneration initiated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Recap"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
|
||||
|
|
@ -260,7 +260,7 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
|
|||
# shellcheck disable=SC2016
|
||||
echo '
|
||||
webhook-interactions:
|
||||
image: mattermostdevelopment/mirrored-node:${NODE_VERSION_REQUIRED}
|
||||
image: node:${NODE_VERSION_REQUIRED}
|
||||
command: sh -c "npm install --global --legacy-peer-deps && exec node webhook_serve.js"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-s", "-o/dev/null", "127.0.0.1:3000"]
|
||||
|
|
@ -275,11 +275,21 @@ $(if mme2e_is_token_in_list "webhook-interactions" "$ENABLED_DOCKER_SERVICES"; t
|
|||
fi)
|
||||
|
||||
$(if mme2e_is_token_in_list "playwright" "$ENABLED_DOCKER_SERVICES"; then
|
||||
# shellcheck disable=SC2016
|
||||
echo '
|
||||
playwright:
|
||||
image: mcr.microsoft.com/playwright:v1.56.0-noble
|
||||
image: mcr.microsoft.com/playwright:v1.57.0-noble
|
||||
entrypoint: ["/bin/bash", "-c"]
|
||||
command: ["until [ -f /var/run/mm_terminate ]; do sleep 5; done"]
|
||||
command:
|
||||
- |
|
||||
# Install Node.js based on .nvmrc
|
||||
NODE_VERSION=$$(cat /mattermost/.nvmrc)
|
||||
echo "Installing Node.js $${NODE_VERSION}..."
|
||||
curl -fsSL https://deb.nodesource.com/setup_$${NODE_VERSION%%.*}.x | bash -
|
||||
apt-get install -y nodejs
|
||||
echo "Node.js version: $$(node --version)"
|
||||
# Wait for termination signal
|
||||
until [ -f /var/run/mm_terminate ]; do sleep 5; done
|
||||
env_file:
|
||||
- "./.env.playwright"
|
||||
environment:
|
||||
|
|
|
|||
223
e2e-tests/cypress/package-lock.json
generated
223
e2e-tests/cypress/package-lock.json
generated
|
|
@ -74,7 +74,6 @@
|
|||
"mochawesome-merge": "4.4.1",
|
||||
"mochawesome-report-generator": "6.2.0",
|
||||
"moment-timezone": "0.6.0",
|
||||
"mysql": "2.18.1",
|
||||
"path": "0.12.7",
|
||||
"pdf-parse": "1.1.1",
|
||||
"pg": "8.16.3",
|
||||
|
|
@ -93,7 +92,6 @@
|
|||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
|
|
@ -1071,7 +1069,6 @@
|
|||
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -1114,6 +1111,7 @@
|
|||
"integrity": "sha512-N4ntErOlKvcbTt01rr5wj3y55xnIdx1ymrfIr8C2WnM1Y9glFgWaGDEULJIazOX3XM9NRzhfJ6zZnQ1sBNWU+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1",
|
||||
"eslint-visitor-keys": "^2.1.0",
|
||||
|
|
@ -1150,7 +1148,6 @@
|
|||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/types": "^7.28.0",
|
||||
|
|
@ -1168,7 +1165,6 @@
|
|||
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.27.2",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
|
|
@ -1186,7 +1182,6 @@
|
|||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -1197,7 +1192,6 @@
|
|||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.27.1",
|
||||
"@babel/types": "^7.27.1"
|
||||
|
|
@ -1212,7 +1206,6 @@
|
|||
"integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
|
|
@ -1231,7 +1224,6 @@
|
|||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -1252,7 +1244,6 @@
|
|||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -1263,7 +1254,6 @@
|
|||
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.2"
|
||||
|
|
@ -1278,7 +1268,6 @@
|
|||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.0"
|
||||
},
|
||||
|
|
@ -1305,7 +1294,6 @@
|
|||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
|
|
@ -1321,7 +1309,6 @@
|
|||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.0",
|
||||
|
|
@ -1341,7 +1328,6 @@
|
|||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
|
|
@ -1813,7 +1799,6 @@
|
|||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
|
|
@ -1825,7 +1810,6 @@
|
|||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
|
|
@ -1836,7 +1820,6 @@
|
|||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
|
|
@ -1847,8 +1830,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.30",
|
||||
|
|
@ -1856,7 +1838,6 @@
|
|||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
|
|
@ -1884,6 +1865,7 @@
|
|||
"integrity": "sha512-2795KUkp2EkuJ9NVohPkJmrgKunt6OZiLyo8zUoIWPJjxQ0upjiWJz/KenABx38v8+QfpSEN8tZSBN3lsZCueg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"typescript": "^4.3.0 || ^5.0.0"
|
||||
},
|
||||
|
|
@ -2866,7 +2848,6 @@
|
|||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
|
|
@ -2878,7 +2859,6 @@
|
|||
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
"@types/estree": "*"
|
||||
|
|
@ -3179,6 +3159,7 @@
|
|||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
|
|
@ -3397,7 +3378,6 @@
|
|||
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
|
||||
|
|
@ -3408,24 +3388,21 @@
|
|||
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
|
||||
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
|
||||
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
|
||||
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.13.2",
|
||||
|
|
@ -3433,7 +3410,6 @@
|
|||
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
|
|
@ -3445,8 +3421,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
|
||||
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.14.1",
|
||||
|
|
@ -3454,7 +3429,6 @@
|
|||
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
|
|
@ -3468,7 +3442,6 @@
|
|||
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
}
|
||||
|
|
@ -3479,7 +3452,6 @@
|
|||
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
|
|
@ -3489,8 +3461,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
|
||||
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.14.1",
|
||||
|
|
@ -3498,7 +3469,6 @@
|
|||
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
|
|
@ -3516,7 +3486,6 @@
|
|||
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
|
|
@ -3531,7 +3500,6 @@
|
|||
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
|
|
@ -3545,7 +3513,6 @@
|
|||
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
|
|
@ -3561,7 +3528,6 @@
|
|||
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@xtuc/long": "4.2.2"
|
||||
|
|
@ -3572,16 +3538,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
|
|
@ -3603,6 +3567,7 @@
|
|||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -3616,7 +3581,6 @@
|
|||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
|
|
@ -3671,7 +3635,6 @@
|
|||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
|
|
@ -3690,7 +3653,6 @@
|
|||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
|
|
@ -3707,8 +3669,7 @@
|
|||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ally.js": {
|
||||
"version": "1.4.1",
|
||||
|
|
@ -4198,16 +4159,6 @@
|
|||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
|
||||
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/blob-util": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz",
|
||||
|
|
@ -4340,8 +4291,7 @@
|
|||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/byte-length": {
|
||||
"version": "1.0.2",
|
||||
|
|
@ -4462,8 +4412,7 @@
|
|||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0",
|
||||
"peer": true
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
|
|
@ -4558,7 +4507,6 @@
|
|||
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
|
|
@ -4848,8 +4796,7 @@
|
|||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
|
|
@ -4871,13 +4818,6 @@
|
|||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-env": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.0.0.tgz",
|
||||
|
|
@ -4935,6 +4875,7 @@
|
|||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cypress/request": "^3.0.9",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
|
|
@ -5423,8 +5364,7 @@
|
|||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.200.tgz",
|
||||
"integrity": "sha512-rFCxROw7aOe4uPTfIAx+rXv9cEcGx+buAF4npnhtTqCJk5KDFRnh3+KYj7rdVh6lsFt5/aPs+Irj9rZ33WMA7w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
|
|
@ -5473,6 +5413,7 @@
|
|||
"integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-colors": "^4.1.1",
|
||||
"strip-ansi": "^6.0.1"
|
||||
|
|
@ -5603,8 +5544,7 @@
|
|||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
|
|
@ -5702,6 +5642,7 @@
|
|||
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -5903,6 +5844,7 @@
|
|||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
|
|
@ -6535,8 +6477,7 @@
|
|||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.2.5",
|
||||
|
|
@ -6916,7 +6857,6 @@
|
|||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
|
@ -7083,8 +7023,7 @@
|
|||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "10.0.3",
|
||||
|
|
@ -7985,13 +7924,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
|
@ -8046,7 +7978,6 @@
|
|||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"merge-stream": "^2.0.0",
|
||||
|
|
@ -8062,7 +7993,6 @@
|
|||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
|
|
@ -8079,6 +8009,7 @@
|
|||
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
|
|
@ -8116,7 +8047,6 @@
|
|||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
|
|
@ -8136,8 +8066,7 @@
|
|||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
|
|
@ -8173,7 +8102,6 @@
|
|||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
|
|
@ -8449,7 +8377,6 @@
|
|||
"integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
}
|
||||
|
|
@ -8652,7 +8579,6 @@
|
|||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
|
|
@ -8874,6 +8800,7 @@
|
|||
"integrity": "sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"browser-stdout": "^1.3.1",
|
||||
"chokidar": "^4.0.1",
|
||||
|
|
@ -9372,29 +9299,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mysql": {
|
||||
"version": "2.18.1",
|
||||
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
|
||||
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "9.0.0",
|
||||
"readable-stream": "2.3.7",
|
||||
"safe-buffer": "5.1.2",
|
||||
"sqlstring": "2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mysql/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
|
|
@ -9417,8 +9321,7 @@
|
|||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-ensure": {
|
||||
"version": "0.0.0",
|
||||
|
|
@ -9432,8 +9335,7 @@
|
|||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/notp": {
|
||||
"version": "2.0.3",
|
||||
|
|
@ -10264,13 +10166,6 @@
|
|||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
|
@ -10441,29 +10336,6 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
|
||||
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||
|
|
@ -10619,7 +10491,6 @@
|
|||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -10852,7 +10723,6 @@
|
|||
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
|
|
@ -10891,7 +10761,6 @@
|
|||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
|
|
@ -10904,8 +10773,7 @@
|
|||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
|
|
@ -10972,6 +10840,7 @@
|
|||
"integrity": "sha512-b0IrY3b1gVMsWvJppCf19g1p3JSnS0hQi6xu4Hi40CIhf0Lx8pQHcvBL+xunShpmOiQzg1NOia812NAWdSaShw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@servie/events": "^1.0.0",
|
||||
"byte-length": "^1.0.2",
|
||||
|
|
@ -11229,7 +11098,6 @@
|
|||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -11240,7 +11108,6 @@
|
|||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
|
|
@ -11256,16 +11123,6 @@
|
|||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
|
||||
"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
|
||||
|
|
@ -11659,7 +11516,6 @@
|
|||
"integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
|
|
@ -11679,7 +11535,6 @@
|
|||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
|
|
@ -11714,8 +11569,7 @@
|
|||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/thirty-two": {
|
||||
"version": "0.0.2",
|
||||
|
|
@ -12038,6 +11892,7 @@
|
|||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -12106,6 +11961,7 @@
|
|||
"integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.41.0",
|
||||
"@typescript-eslint/types": "8.41.0",
|
||||
|
|
@ -12394,7 +12250,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
|
|
@ -12513,7 +12368,6 @@
|
|||
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
|
|
@ -12528,7 +12382,6 @@
|
|||
"integrity": "sha512-rHY3vHXRbkSfhG6fH8zYQdth/BtDgXXuR2pHF++1f/EBkI8zkgM5XWfsC3BvOoW9pr1CvZ1qQCxhCEsbNgT50g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
|
|
@ -12578,7 +12431,6 @@
|
|||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
|
|
@ -12589,7 +12441,6 @@
|
|||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
|
|
@ -12604,7 +12455,6 @@
|
|||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
|
|
@ -12615,7 +12465,6 @@
|
|||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
|
|
@ -12629,7 +12478,6 @@
|
|||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
|
@ -12906,8 +12754,7 @@
|
|||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@
|
|||
"mochawesome-merge": "4.4.1",
|
||||
"mochawesome-report-generator": "6.2.0",
|
||||
"moment-timezone": "0.6.0",
|
||||
"mysql": "2.18.1",
|
||||
"path": "0.12.7",
|
||||
"pdf-parse": "1.1.1",
|
||||
"pg": "8.16.3",
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ describe('Verify Accessibility Support in Popovers', () => {
|
|||
{ariaLabel: 'People & Body', header: 'People & Body'},
|
||||
{ariaLabel: 'Animals & Nature', header: 'Animals & Nature'},
|
||||
{ariaLabel: 'Food & Drink', header: 'Food & Drink'},
|
||||
{ariaLabel: 'Travel Places', header: 'Travel Places'},
|
||||
{ariaLabel: 'Travel & Places', header: 'Travel & Places'},
|
||||
{ariaLabel: 'Activities', header: 'Activities'},
|
||||
{ariaLabel: 'Objects', header: 'Objects'},
|
||||
{ariaLabel: 'Symbols', header: 'Symbols'},
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ describe('Profile > Profile Settings > Email', () => {
|
|||
cy.get('#primaryEmail').should('be.visible').click().blur();
|
||||
|
||||
// * Check that the correct error message is shown.
|
||||
cy.get('#error_primaryEmail').should('be.visible').should('have.text', 'Please enter a valid email address');
|
||||
cy.get('#error_primaryEmail').should('be.visible').should('have.text', 'Please enter a valid email address.');
|
||||
});
|
||||
|
||||
it('MM-T2067 email address already taken error', () => {
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ describe('Archived channels', () => {
|
|||
function verifyViewingArchivedChannel(channel) {
|
||||
// * Verify that we've switched to the correct channel and that the header contains the archived icon
|
||||
cy.get('#channelHeaderTitle').should('contain', channel.display_name);
|
||||
cy.get('#channelHeaderInfo .icon__archive').should('be.visible');
|
||||
cy.findByTestId('channel-header-archive-icon').should('be.visible');
|
||||
|
||||
// * Verify that the channel is visible in the sidebar with the archived icon
|
||||
cy.get(`#sidebarItem_${channel.name}`).should('be.visible').
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_name').clear().type(`test${getRandomId()}`);
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Make sure account was created successfully and we are at the select team page
|
||||
cy.findByText('Teams you can join:', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
|
|
@ -113,7 +113,7 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_name').clear().type(`test${getRandomId()}`);
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Make sure account was not created successfully
|
||||
cy.get('.AlertBanner__title').scrollIntoView().should('be.visible');
|
||||
|
|
@ -146,7 +146,7 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_name').clear().type(username);
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Make sure account was created successfully and we are on the team joining page
|
||||
cy.findByText('Teams you can join:', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
|
|
|
|||
|
|
@ -61,14 +61,14 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_password-input').clear().type('less');
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert the error is what is expected;
|
||||
cy.findByText('Your password must be 7-72 characters long.').should('be.visible');
|
||||
|
||||
cy.get('#input_password-input').clear().type('greaterthan7');
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert that we are not shown an MFA screen and instead a Teams You Can join page
|
||||
cy.findByText('Teams you can join:', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
|
|
@ -114,7 +114,7 @@ describe('Authentication', () => {
|
|||
|
||||
['NOLOWERCASE123!', 'noupppercase123!', 'NoNumber!', 'NoSymbol123'].forEach((option) => {
|
||||
cy.get('#input_password-input').clear().type(option);
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert the error is what is expected;
|
||||
cy.findByText('Your password must be 5-72 characters long and include both lowercase and uppercase letters, numbers, and special characters.').should('be.visible');
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ describe('Authentication', () => {
|
|||
|
||||
['1user', 'te', 'user#1', 'user!1'].forEach((option) => {
|
||||
cy.get('#input_name').clear().type(option);
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert the error is what is expected;
|
||||
cy.get('.Input___error').scrollIntoView().should('be.visible');
|
||||
|
|
@ -183,7 +183,7 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_name').clear().type(`Test${getRandomId()}`);
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Make sure account was created successfully and we are on the team joining page
|
||||
cy.findByText('Teams you can join:', {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
|
|
@ -245,7 +245,7 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_name').clear().type(`Test${getRandomId()}`);
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Make sure account was not created successfully
|
||||
cy.get('.AlertBanner__title').scrollIntoView().should('be.visible');
|
||||
|
|
@ -271,7 +271,7 @@ describe('Authentication', () => {
|
|||
cy.findByText('Copy invite link').click();
|
||||
|
||||
// # Input email, select member
|
||||
cy.findByLabelText('Add or Invite People').type(`test-${getRandomId()}@mattermost.com{downarrow}{downarrow}{enter}`, {force: true});
|
||||
cy.findByLabelText('Invite People').type(`test-${getRandomId()}@mattermost.com{downarrow}{downarrow}{enter}`, {force: true});
|
||||
|
||||
// # Click invite members button
|
||||
cy.findByRole('button', {name: 'Invite'}).click({force: true});
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ describe('Verify Accessibility Support in different input fields', () => {
|
|||
|
||||
// * Verify Accessibility Support in Add or Invite People input field
|
||||
cy.get('.users-emails-input__control').should('be.visible').within(() => {
|
||||
cy.get('input').should('have.attr', 'aria-label', 'Add or Invite People').and('have.attr', 'aria-autocomplete', 'list');
|
||||
cy.get('input').should('have.attr', 'aria-label', 'Invite People').and('have.attr', 'aria-autocomplete', 'list');
|
||||
cy.get('.users-emails-input__placeholder').should('have.text', 'Enter a name or email address');
|
||||
});
|
||||
|
||||
|
|
@ -58,7 +58,7 @@ describe('Verify Accessibility Support in different input fields', () => {
|
|||
|
||||
// * Verify Accessibility Support in Invite People input field
|
||||
cy.get('.users-emails-input__control').should('be.visible').within(() => {
|
||||
cy.get('input').should('have.attr', 'aria-label', 'Add or Invite People').and('have.attr', 'aria-autocomplete', 'list');
|
||||
cy.get('input').should('have.attr', 'aria-label', 'Invite People').and('have.attr', 'aria-autocomplete', 'list');
|
||||
cy.get('.users-emails-input__placeholder').should('have.text', 'Enter a name or email address');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('Authentication', () => {
|
|||
|
||||
cy.get('#input_name').clear().type(`Test${getRandomId()}`);
|
||||
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Make sure account was not created successfully
|
||||
cy.get('.AlertBanner__title').scrollIntoView().should('be.visible');
|
||||
|
|
|
|||
|
|
@ -78,13 +78,16 @@ describe('Guest Accounts', () => {
|
|||
// # Click "Save".
|
||||
cy.findByText('Save').click().wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
// # Get MFA secret
|
||||
// # Visit a page to trigger MFA setup redirect, then complete MFA setup for admin
|
||||
cy.visit('/');
|
||||
cy.url().should('include', 'mfa/setup');
|
||||
cy.uiGetMFASecret(sysadmin.id).then((secret) => {
|
||||
adminMFASecret = secret;
|
||||
});
|
||||
|
||||
// # Navigate to Guest Access page.
|
||||
cy.visit('/admin_console/authentication/guest_access');
|
||||
cy.url().should('include', '/admin_console/authentication/guest_access');
|
||||
|
||||
// # Enable guest accounts.
|
||||
cy.findByTestId('GuestAccountsSettings.Enabletrue').check();
|
||||
|
|
@ -144,20 +147,20 @@ describe('Guest Accounts', () => {
|
|||
// # Create an account with Email and Password.
|
||||
cy.get('#input_name').type(username);
|
||||
cy.get('#input_password-input').type(username);
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * When MFA is enforced for Guest Access, guest user should be forced to configure MFA while creating an account.
|
||||
cy.url().should('include', 'mfa/setup');
|
||||
cy.get('#mfa').wait(TIMEOUTS.HALF_SEC).find('.col-sm-12').then((p) => {
|
||||
cy.get('#mfa').wait(TIMEOUTS.HALF_SEC).find('p.col-sm-12 span').then((p) => {
|
||||
const secretp = p.text();
|
||||
const secret = secretp.split(' ')[1];
|
||||
|
||||
const token = authenticator.generateToken(secret);
|
||||
cy.get('#mfa').find('.form-control').type(token);
|
||||
cy.get('#mfa').find('.btn.btn-primary').click();
|
||||
cy.findByPlaceholderText('MFA Code').type(token);
|
||||
cy.findByText('Save').click();
|
||||
|
||||
cy.wait(TIMEOUTS.ONE_SEC);
|
||||
cy.get('#mfa').find('.btn.btn-primary').click();
|
||||
cy.findByText('Okay').click();
|
||||
});
|
||||
cy.apiLogout();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ describe('Guest Account - Member Invitation Flow', () => {
|
|||
cy.get('#input_email').type(email);
|
||||
cy.get('#input_name').type(username);
|
||||
cy.get('#input_password-input').type('Testing123');
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Verify if user is added to the invited team
|
||||
cy.uiGetLHSHeader().findByText(testTeam.display_name);
|
||||
|
|
|
|||
|
|
@ -65,8 +65,19 @@ describe('Incoming webhook', () => {
|
|||
|
||||
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
|
||||
|
||||
// # Post webhook and wait for attachment to render
|
||||
cy.postIncomingWebhook({url: incomingWebhook.url, data: payload});
|
||||
|
||||
// # Verify the post appears in the channel with attachment
|
||||
cy.getLastPost().within(() => {
|
||||
cy.get('.attachment__body').should('be.visible').should('contain', 'Findme.');
|
||||
});
|
||||
|
||||
// # Explicitly wait to give Elasticsearch time to index before searching
|
||||
// Using a longer wait time since Elasticsearch indexing can be slow in test environments
|
||||
cy.wait(TIMEOUTS.THREE_SEC);
|
||||
|
||||
// # Search for text in the attachment
|
||||
cy.uiGetSearchContainer().click();
|
||||
cy.uiGetSearchBox().
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
|
|
|
|||
|
|
@ -14,17 +14,24 @@ import * as TIMEOUTS from '../../../../fixtures/timeouts';
|
|||
|
||||
describe('Archived channels', () => {
|
||||
let testChannel;
|
||||
let testPrivateChannel;
|
||||
|
||||
before(() => {
|
||||
cy.apiRequireLicense();
|
||||
|
||||
cy.apiInitSetup({
|
||||
channelPrefix: {name: '000-archive', displayName: '000 Archive Test'},
|
||||
}).then(({channel}) => {
|
||||
}).then(({channel, team}) => {
|
||||
testChannel = channel;
|
||||
|
||||
// # Archive the channel
|
||||
// # Archive the public channel
|
||||
cy.apiDeleteChannel(testChannel.id);
|
||||
|
||||
// # Create and archive a private channel with a prefix to ensure proper sorting
|
||||
cy.apiCreateChannel(team.id, '000-private-archive', '000 Private Archive Test', 'P').then(({channel: privateChannel}) => {
|
||||
testPrivateChannel = privateChannel;
|
||||
cy.apiDeleteChannel(privateChannel.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -83,4 +90,26 @@ describe('Archived channels', () => {
|
|||
expect(channel.delete_at).to.eq(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('display archive icon for public archived channels in channel list', () => {
|
||||
// # Go to the channels list view
|
||||
cy.visit('/admin_console/user_management/channels');
|
||||
|
||||
// * Verify the archived public channel is visible
|
||||
cy.findByText(testChannel.display_name, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
|
||||
// * Verify the archive icon is displayed
|
||||
cy.findByTestId(`${testChannel.name}-archive-icon`).should('be.visible');
|
||||
});
|
||||
|
||||
it('display archive-lock icon for private archived channels in channel list', () => {
|
||||
// # Go to the channels list view
|
||||
cy.visit('/admin_console/user_management/channels');
|
||||
|
||||
// * Verify the archived private channel is visible
|
||||
cy.findByText(testPrivateChannel.display_name, {timeout: TIMEOUTS.ONE_MIN}).should('be.visible');
|
||||
|
||||
// * Verify the archive icon is displayed for private channel
|
||||
cy.findByTestId(`${testPrivateChannel.name}-archive-icon`).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,18 +92,6 @@ describe('Upload Files - Image', () => {
|
|||
testImage(properties);
|
||||
});
|
||||
|
||||
it('MM-T2264_6 - PSD', () => {
|
||||
const properties = {
|
||||
filePath: 'mm_file_testing/Images/PSD.psd',
|
||||
fileName: 'PSD.psd',
|
||||
originalWidth: 400,
|
||||
originalHeight: 479,
|
||||
mimeType: 'application/psd',
|
||||
};
|
||||
|
||||
testImage(properties);
|
||||
});
|
||||
|
||||
it('MM-T2264_7 - WEBP', () => {
|
||||
const properties = {
|
||||
filePath: 'mm_file_testing/Images/WEBP.webp',
|
||||
|
|
|
|||
|
|
@ -81,6 +81,13 @@ describe('Messaging', () => {
|
|||
});
|
||||
|
||||
function writeLinesToPostTextBox(lines) {
|
||||
let previousHeight;
|
||||
|
||||
// Get the initial previous height from the alias
|
||||
cy.get('@previousHeight').then((height) => {
|
||||
previousHeight = height;
|
||||
});
|
||||
|
||||
Cypress._.forEach(lines, (line, i) => {
|
||||
// # Add the text
|
||||
cy.uiGetPostTextBox().type(line, {delay: TIMEOUTS.ONE_HUNDRED_MILLIS}).wait(TIMEOUTS.HALF_SEC);
|
||||
|
|
@ -91,11 +98,14 @@ function writeLinesToPostTextBox(lines) {
|
|||
|
||||
// * Verify new height
|
||||
cy.uiGetPostTextBox().invoke('height').then((height) => {
|
||||
const currentHeight = parseInt(height, 10);
|
||||
|
||||
// * Verify previous height should be lower than the current height
|
||||
cy.get('@previousHeight').should('be.lessThan', parseInt(height, 10));
|
||||
expect(previousHeight).to.be.lessThan(currentHeight);
|
||||
|
||||
// # Store the current height as the previous height for the next loop
|
||||
cy.wrap(parseInt(height, 10)).as('previousHeight');
|
||||
previousHeight = currentHeight;
|
||||
cy.wrap(currentHeight).as('previousHeight');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('Multi-user group header', () => {
|
|||
cy.get('#editChannelHeaderModalLabel').should('be.visible').wait(TIMEOUTS.ONE_SEC);
|
||||
|
||||
// # Add the header in the modal
|
||||
cy.findByPlaceholderText('Edit the Channel Header...').should('be.visible').type(`${header}{enter}`);
|
||||
cy.findByPlaceholderText('Enter the Channel Header').should('be.visible').type(`${header}{enter}`);
|
||||
|
||||
// # Wait for modal to disappear
|
||||
cy.waitUntil(() => cy.get('#editChannelHeaderModalLabel').should('not.be.visible'));
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ function signupWithEmail(name, pw) {
|
|||
cy.get('#input_password-input').type(pw);
|
||||
|
||||
// # Click on Create Account button
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
}
|
||||
|
||||
describe('Cloud Onboarding', () => {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ describe('Onboarding', () => {
|
|||
cy.get('#name').should('be.visible').type(usernameTwo);
|
||||
cy.get('#password').should('be.visible').type(password);
|
||||
|
||||
// # Attempt to create an account by clicking on the 'Create Account' button
|
||||
// # Attempt to create an account by clicking on the 'Create account' button
|
||||
cy.get('#createAccountButton').click();
|
||||
|
||||
// * Ensure that since the invite was invalidated, the correct error message should be shown
|
||||
|
|
@ -99,7 +99,7 @@ describe('Onboarding', () => {
|
|||
});
|
||||
|
||||
function inviteNewUser(email) {
|
||||
cy.findByRole('textbox', {name: 'Add or Invite People'}).
|
||||
cy.findByRole('textbox', {name: 'Invite People'}).
|
||||
typeWithForce(email).wait(TIMEOUTS.HALF_SEC).
|
||||
typeWithForce('{enter}');
|
||||
cy.findByTestId('inviteButton').click();
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ describe('Onboarding', () => {
|
|||
cy.get('#input_email').should('be.focused').and('be.visible').type(email);
|
||||
cy.get('#input_name').should('be.visible').type(username);
|
||||
cy.get('#input_password-input').should('be.visible').type(password);
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
cy.findByText('You’re almost done!').should('be.visible');
|
||||
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ describe('Onboarding', () => {
|
|||
cy.get('#input_name').should('be.visible').type(username);
|
||||
cy.get('#input_password-input').should('be.visible').type(password);
|
||||
|
||||
// # Attempt to create an account by clicking on the 'Create Account' button
|
||||
cy.findByText('Create Account').click();
|
||||
// # Attempt to create an account by clicking on the 'Create account' button
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ describe('Signin/Authentication', () => {
|
|||
cy.url().should('contain', '/login?extra=password_change');
|
||||
|
||||
// * Should show that the password is updated successfully
|
||||
cy.get('.AlertBanner.success').should('be.visible').and('have.text', ' Password updated successfully');
|
||||
cy.get('.AlertBanner.success').should('be.visible').and('have.text', 'Password updated successfully');
|
||||
|
||||
// # Type email and new password, then click login button
|
||||
cy.get('#input_loginId').should('be.visible').type(testUser.username);
|
||||
|
|
|
|||
|
|
@ -54,11 +54,6 @@ describe('Signup Email page', () => {
|
|||
});
|
||||
|
||||
it('should match elements, body', () => {
|
||||
const {
|
||||
PRIVACY_POLICY_LINK,
|
||||
TERMS_OF_SERVICE_LINK,
|
||||
} = FixedCloudConfig.SupportSettings;
|
||||
|
||||
// * Check elements in the body
|
||||
cy.get('.signup-body').should('be.visible');
|
||||
cy.get('.header-logo-link').should('be.visible');
|
||||
|
|
@ -78,11 +73,15 @@ describe('Signup Email page', () => {
|
|||
cy.findByText('Your password must be 5-72 characters long.').should('be.visible');
|
||||
|
||||
cy.get('#saveSetting').scrollIntoView().should('be.visible');
|
||||
cy.get('#saveSetting').should('contain', 'Create Account');
|
||||
cy.get('#saveSetting').should('contain', 'Create account');
|
||||
|
||||
cy.get('.signup-body-card-agreement').should('contain', `By proceeding to create your account and use ${config.TeamSettings.SiteName}, you agree to our Terms of Use and Privacy Policy. If you do not agree, you cannot use ${config.TeamSettings.SiteName}.`);
|
||||
cy.get(`.signup-body-card-agreement > span > [href="${config.SupportSettings.TermsOfServiceLink || TERMS_OF_SERVICE_LINK}"]`).should('be.visible');
|
||||
cy.get(`.signup-body-card-agreement > span > [href="${config.SupportSettings.PrivacyPolicyLink || PRIVACY_POLICY_LINK}"]`).should('be.visible');
|
||||
// * Check newsletter subscription checkbox text and links
|
||||
cy.findByText('I would like to receive Mattermost security updates via newsletter.').should('be.visible');
|
||||
cy.findByText(/By subscribing, I consent to receive emails from Mattermost with product updates, promotions, and company news\./).should('be.visible');
|
||||
cy.findByText(/I have read the/).parent().within(() => {
|
||||
cy.findByRole('link', {name: 'Privacy Policy'}).should('be.visible').and('have.attr', 'href').and('include', 'mattermost.com/pl/privacy-policy/');
|
||||
cy.findByRole('link', {name: 'unsubscribe'}).should('be.visible').and('have.attr', 'href').and('include', 'forms.mattermost.com/UnsubscribePage.html');
|
||||
});
|
||||
});
|
||||
|
||||
it('should match elements, footer', () => {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ describe('Team Settings', () => {
|
|||
cy.get('.InviteAs').findByTestId('inviteMembersLink').click();
|
||||
}
|
||||
|
||||
cy.findByRole('combobox', {name: 'Add or Invite People'}).type(email, {force: true}).wait(TIMEOUTS.HALF_SEC).type('{enter}', {force: true});
|
||||
cy.findByRole('combobox', {name: 'Invite People'}).type(email, {force: true}).wait(TIMEOUTS.HALF_SEC).type('{enter}', {force: true});
|
||||
cy.findByTestId('inviteButton').click();
|
||||
|
||||
// # Wait for a while to ensure that email notification is sent and logout from sysadmin account
|
||||
|
|
@ -115,7 +115,7 @@ describe('Team Settings', () => {
|
|||
cy.get('#input_password-input').type(password);
|
||||
|
||||
// # Attempt to create an account by clicking on the 'Create Account' button
|
||||
cy.findByText('Create Account').click();
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// # Close the onboarding tutorial
|
||||
cy.uiCloseOnboardingTaskList();
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const inviteUserByEmail = (email) => {
|
|||
// # Wait half a second to ensure that the modal has been fully loaded
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
cy.findByRole('textbox', {name: 'Add or Invite People'}).
|
||||
cy.findByRole('textbox', {name: 'Invite People'}).
|
||||
typeWithForce(email).
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
typeWithForce('{enter}');
|
||||
|
|
@ -71,7 +71,7 @@ export const signupAndVerifyTutorial = (username, password, teamDisplayName) =>
|
|||
cy.get('#name', {timeout: TIMEOUTS.HALF_MIN}).should('be.visible').type(username);
|
||||
cy.get('#password').should('be.visible').type(password);
|
||||
|
||||
// # Attempt to create an account by clicking on the 'Create Account' button
|
||||
// # Attempt to create an account by clicking on the 'Create account' button
|
||||
cy.get('#createAccountButton').click();
|
||||
|
||||
// # Close the onboarding tutorial
|
||||
|
|
|
|||
|
|
@ -49,11 +49,14 @@ describe('Team Settings', () => {
|
|||
// # Set 'sample.mattermost.com' as the only allowed email domain and save
|
||||
cy.get('#allowedDomains').click().type(emailDomain).type(' ');
|
||||
cy.findByText('Save').should('be.visible').click();
|
||||
|
||||
// # Close the modal
|
||||
cy.get('#teamSettingsModalLabel').find('button').should('be.visible').click();
|
||||
});
|
||||
|
||||
// # Close the modal
|
||||
cy.findByLabelText('Close').click();
|
||||
|
||||
// * Wait for modal to be closed
|
||||
cy.get('#teamSettingsModal').should('not.exist');
|
||||
|
||||
// # Open team menu and click 'Invite People'
|
||||
cy.uiOpenTeamMenu('Invite people');
|
||||
|
||||
|
|
@ -63,7 +66,7 @@ describe('Team Settings', () => {
|
|||
// * Assert that the user has successfully been invited to the team
|
||||
cy.get('.invitation-modal-confirm--sent').should('be.visible').within(() => {
|
||||
cy.get('.username-or-icon').find('span').eq(0).should('have.text', userDetailsString);
|
||||
cy.get('.InviteResultRow').find('div').eq(1).should('have.text', inviteSuccessMessage);
|
||||
cy.get('.InviteResultRow').find('.reason').should('have.text', inviteSuccessMessage);
|
||||
});
|
||||
|
||||
// # Click on the 'Invite More People button'
|
||||
|
|
@ -75,14 +78,14 @@ describe('Team Settings', () => {
|
|||
// * Assert that the invite failed and the correct error message is shown
|
||||
cy.get('.invitation-modal-confirm--not-sent').should('be.visible').within(() => {
|
||||
cy.get('.username-or-icon').find('span').eq(1).should('have.text', invalidEmail);
|
||||
cy.get('.InviteResultRow').find('div').eq(1).should('have.text', inviteFailedMessage);
|
||||
cy.get('.InviteResultRow').find('.reason').should('have.text', inviteFailedMessage);
|
||||
});
|
||||
});
|
||||
|
||||
function inviteNewMemberToTeam(email) {
|
||||
cy.wait(TIMEOUTS.HALF_SEC);
|
||||
|
||||
cy.findByRole('combobox', {name: 'Add or Invite People'}).
|
||||
cy.findByRole('combobox', {name: 'Invite People'}).
|
||||
typeWithForce(email).
|
||||
wait(TIMEOUTS.HALF_SEC).
|
||||
typeWithForce('{enter}');
|
||||
|
|
|
|||
|
|
@ -87,8 +87,8 @@ describe('Team Settings', () => {
|
|||
cy.get('#input_name').type(username);
|
||||
cy.get('#input_password-input').type(password);
|
||||
|
||||
// # Attempt to create an account by clicking on the 'Create Account' button
|
||||
cy.findByText('Create Account').click();
|
||||
// # Attempt to create an account by clicking on the 'Create account' button
|
||||
cy.findByText('Create account').click();
|
||||
|
||||
// * Assert that the expected error message from creating an account with an email not from the allowed email domain exists and is visible
|
||||
cy.findByText(errorMessage).should('be.visible');
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/**
|
||||
* Functions here are expected to work with MySQL and PostgreSQL (known as dialect).
|
||||
* When updating this file, make sure to test in both dialect.
|
||||
* You'll find table and columns names are being converted to lowercase. Reason being is that
|
||||
* in MySQL, first letter is capitalized.
|
||||
*/
|
||||
|
||||
const mapKeys = require('lodash.mapkeys');
|
||||
|
||||
function convertKeysToLowercase(obj) {
|
||||
|
|
@ -33,12 +26,12 @@ const dbGetActiveUserSessions = async ({dbConfig, params: {username, userId, lim
|
|||
try {
|
||||
let user;
|
||||
if (username) {
|
||||
user = await knexClient(toLowerCase(dbConfig, 'Users')).where('username', username).first();
|
||||
user = await knexClient('users').where('username', username).first();
|
||||
user = convertKeysToLowercase(user);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const sessions = await knexClient(toLowerCase(dbConfig, 'Sessions')).
|
||||
const sessions = await knexClient('sessions').
|
||||
where('userid', user ? user.id : userId).
|
||||
where('expiresat', '>', now).
|
||||
orderBy('lastactivityat', 'desc').
|
||||
|
|
@ -60,7 +53,7 @@ const dbGetUser = async ({dbConfig, params: {username}}) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const user = await knexClient(toLowerCase(dbConfig, 'Users')).where('username', username).first();
|
||||
const user = await knexClient('users').where('username', username).first();
|
||||
|
||||
return {user: convertKeysToLowercase(user)};
|
||||
} catch (error) {
|
||||
|
|
@ -75,7 +68,7 @@ const dbGetUserSession = async ({dbConfig, params: {sessionId}}) => {
|
|||
}
|
||||
|
||||
try {
|
||||
const session = await knexClient(toLowerCase(dbConfig, 'Sessions')).
|
||||
const session = await knexClient('sessions').
|
||||
where('id', '=', sessionId).
|
||||
first();
|
||||
|
||||
|
|
@ -92,7 +85,7 @@ const dbUpdateUserSession = async ({dbConfig, params: {sessionId, userId, fields
|
|||
}
|
||||
|
||||
try {
|
||||
let user = await knexClient(toLowerCase(dbConfig, 'Users')).where('id', userId).first();
|
||||
let user = await knexClient('users').where('id', userId).first();
|
||||
if (!user) {
|
||||
return {errorMessage: `No user found with id: ${userId}.`};
|
||||
}
|
||||
|
|
@ -102,12 +95,12 @@ const dbUpdateUserSession = async ({dbConfig, params: {sessionId, userId, fields
|
|||
|
||||
user = convertKeysToLowercase(user);
|
||||
|
||||
await knexClient(toLowerCase(dbConfig, 'Sessions')).
|
||||
await knexClient('sessions').
|
||||
where('id', '=', sessionId).
|
||||
where('userid', '=', user.id).
|
||||
update(fieldsToUpdate);
|
||||
|
||||
const session = await knexClient(toLowerCase(dbConfig, 'Sessions')).
|
||||
const session = await knexClient('sessions').
|
||||
where('id', '=', sessionId).
|
||||
where('userid', '=', user.id).
|
||||
first();
|
||||
|
|
@ -119,27 +112,11 @@ const dbUpdateUserSession = async ({dbConfig, params: {sessionId, userId, fields
|
|||
}
|
||||
};
|
||||
|
||||
function toLowerCase(config, name) {
|
||||
if (config.client === 'mysql') {
|
||||
return name;
|
||||
}
|
||||
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
const dbRefreshPostStats = async ({dbConfig}) => {
|
||||
if (!knexClient) {
|
||||
knexClient = getKnexClient(dbConfig);
|
||||
}
|
||||
|
||||
// Only run for PostgreSQL
|
||||
if (dbConfig.client !== 'postgres') {
|
||||
return {
|
||||
skipped: true,
|
||||
message: 'Refresh post stats is only supported for PostgreSQL',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await knexClient.raw('REFRESH MATERIALIZED VIEW posts_by_team_day;');
|
||||
await knexClient.raw('REFRESH MATERIALIZED VIEW bot_posts_by_team_day;');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as TIMEOUTS from '../../fixtures/timeouts';
|
|||
Cypress.Commands.add('uiEnableComplianceExport', (exportFormat = 'csv') => {
|
||||
// * Verify that the page is loaded
|
||||
cy.findByText('Enable Compliance Export:').should('be.visible');
|
||||
cy.findByText('Compliance Export time:').should('be.visible');
|
||||
cy.findByText('Compliance Export Time:').should('be.visible');
|
||||
cy.findByText('Export Format:').should('be.visible');
|
||||
|
||||
// # Enable compliance export
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ export const adminConsoleNavigation = [
|
|||
},
|
||||
{
|
||||
type: ['team', 'e20', 'cloud_enterprise'],
|
||||
header: 'Email Authentication',
|
||||
header: 'Email',
|
||||
sidebar: 'Email',
|
||||
url: 'admin_console/authentication/email',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ test(
|
|||
Change to the `./` project directory, then run the docker container. (See https://playwright.dev/docs/docker for reference.)
|
||||
|
||||
```bash
|
||||
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.56.0-noble /bin/bash
|
||||
docker run -it --rm -v "$(pwd):/mattermost/" --ipc=host mcr.microsoft.com/playwright:v1.57.0-noble /bin/bash
|
||||
```
|
||||
|
||||
#### 2. Inside the docker container
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@mattermost/playwright-lib",
|
||||
"version": "11.0.0",
|
||||
"version": "11.3.0",
|
||||
"description": "A comprehensive end-to-end testing library for Mattermost web, desktop and plugin applications using Playwright",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -42,27 +42,26 @@
|
|||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axe-core/playwright": "4.10.2",
|
||||
"@axe-core/playwright": "4.11.0",
|
||||
"@mattermost/client": "file:../../../webapp/platform/client",
|
||||
"@mattermost/types": "file:../../../webapp/platform/types",
|
||||
"@percy/cli": "1.31.3",
|
||||
"@percy/playwright": "1.0.9",
|
||||
"async-wait-until": "2.0.30",
|
||||
"@percy/cli": "1.31.5",
|
||||
"@percy/playwright": "1.0.10",
|
||||
"async-wait-until": "2.0.31",
|
||||
"axe-core": "4.11.0",
|
||||
"deepmerge": "4.3.1",
|
||||
"dotenv": "17.2.3",
|
||||
"mime-types": "3.0.1",
|
||||
"mime-types": "3.0.2",
|
||||
"uuid": "13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-typescript": "12.1.4",
|
||||
"@rollup/plugin-typescript": "12.3.0",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/node": "24.7.2",
|
||||
"@types/react": "19.2.2",
|
||||
"rollup": "4.52.4",
|
||||
"@types/node": "25.0.3",
|
||||
"rollup": "4.53.5",
|
||||
"rollup-plugin-copy": "3.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@playwright/test": "1.56.0"
|
||||
"@playwright/test": "1.57.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {PluginManifest} from '@mattermost/types/plugins';
|
|||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import {UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {createRandomTeam, getAdminClient, getDefaultAdminUser, makeClient} from './server';
|
||||
import {createNewTeam, getAdminClient, getDefaultAdminUser, makeClient} from './server';
|
||||
import {testConfig} from './test_config';
|
||||
import {defaultTeam} from './util';
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ async function sysadminSetup(client: Client4, user: UserProfile | null) {
|
|||
const myTeams = await client.getMyTeams();
|
||||
const myDefaultTeam = myTeams && myTeams.length > 0 && myTeams.find((team) => team.name === defaultTeam.name);
|
||||
if (!myDefaultTeam) {
|
||||
await client.createTeam(await createRandomTeam(defaultTeam.name, defaultTeam.displayName, 'O', false));
|
||||
await createNewTeam(client, {name: defaultTeam.name, displayName: defaultTeam.displayName});
|
||||
} else if (myDefaultTeam && testConfig.resetBeforeTest) {
|
||||
await Promise.all(
|
||||
myTeams.filter((team) => team.name !== defaultTeam.name).map((team) => client.deleteTeam(team.id)),
|
||||
|
|
@ -163,6 +163,8 @@ async function savePreferences(client: Client4, userId: UserProfile['id']) {
|
|||
const preferences: PreferenceType[] = [
|
||||
{user_id: userId, category: 'tutorial_step', name: userId, value: '999'},
|
||||
{user_id: userId, category: 'crt_thread_pane_step', name: userId, value: '999'},
|
||||
{user_id: userId, category: 'onboarding_task_list', name: 'onboarding_task_list_show', value: 'false'},
|
||||
{user_id: userId, category: 'onboarding_task_list', name: 'onboarding_task_list_open', value: 'false'},
|
||||
];
|
||||
|
||||
await client.savePreferences(userId, preferences);
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const onPremServerConfig = (): Partial<TestAdminConfig> => {
|
|||
};
|
||||
|
||||
// Should be based only from the generated default config from ./server via "make config-reset"
|
||||
// Based on v11.0 server
|
||||
// Based on v11.3 server
|
||||
const defaultServerConfig: AdminConfig = {
|
||||
ServiceSettings: {
|
||||
SiteURL: '',
|
||||
|
|
@ -110,6 +110,7 @@ const defaultServerConfig: AdminConfig = {
|
|||
MaximumLoginAttempts: 10,
|
||||
GoroutineHealthThreshold: -1,
|
||||
EnableOAuthServiceProvider: true,
|
||||
EnableDynamicClientRegistration: false,
|
||||
EnableIncomingWebhooks: true,
|
||||
EnableOutgoingWebhooks: true,
|
||||
EnableOutgoingOAuthConnections: false,
|
||||
|
|
@ -187,6 +188,10 @@ const defaultServerConfig: AdminConfig = {
|
|||
PersistentNotificationIntervalMinutes: 5,
|
||||
PersistentNotificationMaxCount: 6,
|
||||
PersistentNotificationMaxRecipients: 5,
|
||||
EnableBurnOnRead: false,
|
||||
BurnOnReadDurationSeconds: 600,
|
||||
BurnOnReadMaximumTimeToLiveSeconds: 604800,
|
||||
BurnOnReadSchedulerFrequencySeconds: 600,
|
||||
EnableAPIChannelDeletion: false,
|
||||
EnableLocalMode: false,
|
||||
LocalModeSocketLocation: '/var/tmp/mattermost_local.socket',
|
||||
|
|
@ -599,6 +604,7 @@ const defaultServerConfig: AdminConfig = {
|
|||
ClientSideUserIds: [],
|
||||
},
|
||||
ExperimentalSettings: {
|
||||
ClientSideCertEnable: false,
|
||||
LinkMetadataTimeoutMilliseconds: 5000,
|
||||
RestrictSystemAdmin: false,
|
||||
EnableSharedChannels: false,
|
||||
|
|
@ -765,12 +771,13 @@ const defaultServerConfig: AdminConfig = {
|
|||
ExperimentalAuditSettingsSystemConsoleUI: true,
|
||||
CustomProfileAttributes: true,
|
||||
AttributeBasedAccessControl: true,
|
||||
ContentFlagging: false,
|
||||
ContentFlagging: true,
|
||||
InteractiveDialogAppsForm: true,
|
||||
EnableMattermostEntry: true,
|
||||
ChannelAdminManageABACRules: false,
|
||||
MobileSSOCodeExchange: true,
|
||||
AutoTranslation: false,
|
||||
BurnOnRead: false,
|
||||
EnableAIPluginBridge: false,
|
||||
},
|
||||
ImportSettings: {
|
||||
Directory: './import',
|
||||
|
|
@ -836,14 +843,14 @@ const defaultServerConfig: AdminConfig = {
|
|||
AutoTranslationSettings: {
|
||||
Enable: false,
|
||||
Provider: '',
|
||||
LibreTranslate: {
|
||||
URL: '',
|
||||
APIKey: '',
|
||||
},
|
||||
TimeoutMs: {
|
||||
TimeoutsMs: {
|
||||
NewPost: 800,
|
||||
Fetch: 2000,
|
||||
Notification: 300,
|
||||
},
|
||||
LibreTranslate: {
|
||||
URL: '',
|
||||
APIKey: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ export {createRandomChannel} from './channel';
|
|||
export {getOnPremServerConfig, mergeWithOnPremServerConfig} from './default_config';
|
||||
export {initSetup, getAdminClient} from './init';
|
||||
export {createRandomPost} from './post';
|
||||
export {createRandomTeam} from './team';
|
||||
export {createNewTeam, createRandomTeam} from './team';
|
||||
export {createNewUserProfile, createRandomUser, getDefaultAdminUser, isOutsideRemoteUserHour} from './user';
|
||||
|
|
|
|||
|
|
@ -2,21 +2,27 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
import {TeamType} from '@mattermost/types/teams';
|
||||
|
||||
import {makeClient} from './client';
|
||||
import {getOnPremServerConfig} from './default_config';
|
||||
import {createRandomTeam} from './team';
|
||||
import {createRandomUser} from './user';
|
||||
import {createNewTeam} from './team';
|
||||
import {createNewUserProfile} from './user';
|
||||
|
||||
import {getFileFromCommonAsset} from '@/file';
|
||||
import {testConfig} from '@/test_config';
|
||||
|
||||
type InitSetupOptions = {
|
||||
userOptions?: Partial<Parameters<typeof createNewUserProfile>[1]>;
|
||||
teamsOptions?: Partial<Parameters<typeof createNewTeam>[1]>;
|
||||
withDefaultProfileImage?: boolean;
|
||||
};
|
||||
|
||||
export async function initSetup({
|
||||
userPrefix = 'user',
|
||||
teamPrefix = {name: 'team', displayName: 'Team'},
|
||||
userOptions = {prefix: 'user', disableTutorial: true, disableOnboarding: true},
|
||||
teamsOptions = {name: 'team', displayName: 'Team', type: 'O' as TeamType, unique: true},
|
||||
withDefaultProfileImage = true,
|
||||
} = {}) {
|
||||
}: Partial<InitSetupOptions> = {}) {
|
||||
try {
|
||||
// Login the admin user via API
|
||||
const {adminClient, adminUser} = await getAdminClient();
|
||||
|
|
@ -33,12 +39,10 @@ export async function initSetup({
|
|||
const adminConfig = await adminClient.updateConfig(getOnPremServerConfig() as any);
|
||||
|
||||
// Create new team
|
||||
const team = await adminClient.createTeam(await createRandomTeam(teamPrefix.name, teamPrefix.displayName));
|
||||
const team = await createNewTeam(adminClient, teamsOptions);
|
||||
|
||||
// Create new user and add to newly created team
|
||||
const randomUser = await createRandomUser(userPrefix);
|
||||
const user = await adminClient.createUser(randomUser, '', '');
|
||||
user.password = randomUser.password;
|
||||
const user = await createNewUserProfile(adminClient, userOptions);
|
||||
await adminClient.addToTeam(team.id, user.id);
|
||||
|
||||
// Log in new user via API
|
||||
|
|
@ -49,13 +53,6 @@ export async function initSetup({
|
|||
await userClient.uploadProfileImage(user.id, file);
|
||||
}
|
||||
|
||||
// Update user preference
|
||||
const preferences: PreferenceType[] = [
|
||||
{user_id: user.id, category: 'tutorial_step', name: user.id, value: '999'},
|
||||
{user_id: user.id, category: 'crt_thread_pane_step', name: user.id, value: '999'},
|
||||
];
|
||||
await userClient.savePreferences(user.id, preferences);
|
||||
|
||||
return {
|
||||
adminClient,
|
||||
adminUser,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,26 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Client4} from '@mattermost/client';
|
||||
import {Team, TeamType} from '@mattermost/types/teams';
|
||||
|
||||
import {getRandomId} from '@/util';
|
||||
|
||||
export async function createNewTeam(
|
||||
client: Client4,
|
||||
options: {name?: string; displayName?: string; type?: TeamType; unique?: boolean} = {
|
||||
name: 'team',
|
||||
displayName: 'Team',
|
||||
type: 'O' as TeamType,
|
||||
unique: true,
|
||||
},
|
||||
) {
|
||||
const randomTeam = await createRandomTeam(options.name, options.displayName, options.type, options.unique);
|
||||
const newTeam = await client.createTeam(randomTeam);
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
export async function createRandomTeam(
|
||||
name = 'team',
|
||||
displayName = 'Team',
|
||||
|
|
|
|||
|
|
@ -9,12 +9,34 @@ import {getRandomId} from '@/util';
|
|||
import {testConfig} from '@/test_config';
|
||||
import {REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY, REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY} from '@/constant';
|
||||
|
||||
export async function createNewUserProfile(client: Client4, prefix = 'user') {
|
||||
const randomUser = await createRandomUser(prefix);
|
||||
export async function createNewUserProfile(
|
||||
client: Client4,
|
||||
options: {prefix?: string; disableTutorial?: boolean; disableOnboarding?: boolean} = {
|
||||
prefix: 'user',
|
||||
disableTutorial: true,
|
||||
disableOnboarding: true,
|
||||
},
|
||||
) {
|
||||
const randomUser = await createRandomUser(options.prefix);
|
||||
|
||||
const newUser = await client.createUser(randomUser, '', '');
|
||||
// Set password to the created user profile so it can be used to login later
|
||||
newUser.password = randomUser.password;
|
||||
|
||||
if (options.disableTutorial) {
|
||||
await client.savePreferences(newUser.id, [
|
||||
{user_id: newUser.id, category: 'tutorial_step', name: newUser.id, value: '999'},
|
||||
{user_id: newUser.id, category: 'crt_thread_pane_step', name: newUser.id, value: '999'},
|
||||
]);
|
||||
}
|
||||
|
||||
if (options.disableOnboarding) {
|
||||
await client.savePreferences(newUser.id, [
|
||||
{user_id: newUser.id, category: 'onboarding_task_list', name: 'onboarding_task_list_show', value: 'false'},
|
||||
{user_id: newUser.id, category: 'onboarding_task_list', name: 'onboarding_task_list_open', value: 'false'},
|
||||
]);
|
||||
}
|
||||
|
||||
return newUser;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
import {getBlobFromAsset, getFileFromAsset} from './file';
|
||||
import {
|
||||
createNewUserProfile,
|
||||
createNewTeam,
|
||||
createRandomChannel,
|
||||
createRandomPost,
|
||||
createRandomTeam,
|
||||
|
|
@ -102,6 +103,7 @@ export class PlaywrightExtended {
|
|||
|
||||
// ./server
|
||||
readonly createNewUserProfile;
|
||||
readonly createNewTeam;
|
||||
readonly isOutsideRemoteUserHour;
|
||||
readonly makeClient;
|
||||
|
||||
|
|
@ -167,6 +169,7 @@ export class PlaywrightExtended {
|
|||
|
||||
// ./server
|
||||
this.createNewUserProfile = createNewUserProfile;
|
||||
this.createNewTeam = createNewTeam;
|
||||
this.makeClient = makeClient;
|
||||
|
||||
// ./visual
|
||||
|
|
|
|||
|
|
@ -85,4 +85,12 @@ export default class ChannelsPost {
|
|||
async toContainText(text: string) {
|
||||
await expect(this.container).toContainText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* `toNotContainText` verifies if the post does not contain the specified text.
|
||||
* @param text Text to be verified not in the post
|
||||
*/
|
||||
async toNotContainText(text: string) {
|
||||
await expect(this.container).not.toContainText(text);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,14 @@ export default class ChannelsPage {
|
|||
await this.centerView.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* `toNotContainText` verifies if the page does not contain the specified text.
|
||||
* @param text Text to be verified not in the page
|
||||
*/
|
||||
async toNotContainText(text: string) {
|
||||
await expect(this.page.locator('body')).not.toContainText(text);
|
||||
}
|
||||
|
||||
async getLastPost() {
|
||||
return this.centerView.getLastPost();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default class SignupPage {
|
|||
this.usernameInput = page.locator('#input_name');
|
||||
this.passwordInput = page.locator('#input_password-input');
|
||||
this.passwordToggleButton = page.locator('#password_toggle');
|
||||
this.createAccountButton = page.locator('button:has-text("Create Account")');
|
||||
this.createAccountButton = page.locator('button:has-text("Create account")');
|
||||
this.emailError = page.locator('text=Please enter a valid email address');
|
||||
this.usernameError = page.locator(
|
||||
'text=Usernames have to begin with a lowercase letter and be 3-22 characters long. You can use lowercase letters, numbers, periods, dashes, and underscores.',
|
||||
|
|
|
|||
1552
e2e-tests/playwright/package-lock.json
generated
1552
e2e-tests/playwright/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -32,19 +32,19 @@
|
|||
"@mattermost/types": "file:../../webapp/platform/types"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.56.0",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.46.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.0",
|
||||
"cross-env": "10.1.0",
|
||||
"dayjs": "1.11.18",
|
||||
"eslint": "9.37.0",
|
||||
"dayjs": "1.11.19",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-header": "3.1.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"glob": "11.0.3",
|
||||
"glob": "13.0.0",
|
||||
"luxon": "3.7.2",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.7.4",
|
||||
"typescript": "5.9.3",
|
||||
"zod": "4.1.12"
|
||||
"zod": "4.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ test.beforeEach(async ({pw}) => {
|
|||
await pw.skipIfNoLicense();
|
||||
|
||||
// Initialize with admin client
|
||||
({team, user, adminClient, userClient} = await pw.initSetup({userPrefix: 'cpa-test-'}));
|
||||
({team, user, adminClient, userClient} = await pw.initSetup({userOptions: {prefix: 'cpa-test-'}}));
|
||||
const channel = pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: `test-channel`,
|
||||
|
|
@ -85,7 +85,7 @@ test.beforeEach(async ({pw}) => {
|
|||
testChannel = await adminClient.createChannel(channel);
|
||||
|
||||
// Create another user to test profile popover
|
||||
otherUser = await pw.createNewUserProfile(adminClient, 'cpa-other-');
|
||||
otherUser = await pw.createNewUserProfile(adminClient, {prefix: 'cpa-other-'});
|
||||
await adminClient.addToTeam(team.id, otherUser.id);
|
||||
await adminClient.addToChannel(otherUser.id, testChannel.id);
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ test.beforeEach(async ({pw}) => {
|
|||
await pw.skipIfNoLicense();
|
||||
|
||||
// Initialize with admin client
|
||||
({team, user, adminClient, userClient} = await pw.initSetup({userPrefix: 'cpa-test-'}));
|
||||
({team, user, adminClient, userClient} = await pw.initSetup({userOptions: {prefix: 'cpa-test-'}}));
|
||||
const channel = pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: `test-channel`,
|
||||
|
|
@ -94,7 +94,7 @@ test.beforeEach(async ({pw}) => {
|
|||
testChannel = await adminClient.createChannel(channel);
|
||||
|
||||
// Create another user to test profile popover
|
||||
otherUser = await pw.createNewUserProfile(adminClient, 'cpa-other-');
|
||||
otherUser = await pw.createNewUserProfile(adminClient, {prefix: 'cpa-other-'});
|
||||
await adminClient.addToTeam(team.id, otherUser.id);
|
||||
await adminClient.addToChannel(otherUser.id, testChannel.id);
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import {Page} from '@playwright/test';
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
test('MM-T5654_1 should be able to add attachments while editing a post', async ({pw}) => {
|
||||
const originalMessage = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit';
|
||||
|
|
@ -107,8 +107,8 @@ test('MM-T5654_2 should be able to add attachments while editing a threaded post
|
|||
updatedReplyPost = await channelsPage.sidebarRight.getLastPost();
|
||||
await updatedReplyPost.toBeVisible();
|
||||
await updatedReplyPost.toContainText('Edited reply message with files');
|
||||
expect(updatedReplyPost).not.toContain('sample_text_file.txt');
|
||||
expect(updatedReplyPost).not.toContain('mattermost.png');
|
||||
await updatedReplyPost.toNotContainText('sample_text_file.txt');
|
||||
await updatedReplyPost.toNotContainText('mattermost.png');
|
||||
});
|
||||
|
||||
test('MM-T5654_3 should be able to edit post message originally containing files', async ({pw}) => {
|
||||
|
|
@ -217,7 +217,7 @@ test('MM-5654_5 should be able to remove attachments while editing a post', asyn
|
|||
await updatedPost.toContainText(originalMessage);
|
||||
await updatedPost.toContainText('mattermost.png');
|
||||
await updatedPost.toContainText('archive.zip');
|
||||
expect(updatedPost).not.toContain('archive.zip');
|
||||
await updatedPost.toNotContainText('sample_text_file.txt');
|
||||
});
|
||||
|
||||
test('MM-T5655_1 removing message content and files should delete the post', async ({pw}) => {
|
||||
|
|
@ -250,8 +250,8 @@ test('MM-T5655_1 removing message content and files should delete the post', asy
|
|||
await channelsPage.centerView.postEdit.deleteConfirmationDialog.confirmDeletion();
|
||||
await channelsPage.centerView.postEdit.deleteConfirmationDialog.notToBeVisible();
|
||||
|
||||
expect(channelsPage).not.toContain(originalMessage);
|
||||
expect(channelsPage).not.toContain('sample_text_file.txt');
|
||||
await channelsPage.toNotContainText(originalMessage);
|
||||
await channelsPage.toNotContainText('sample_text_file.txt');
|
||||
});
|
||||
|
||||
test('MM-T5655_2 should be able to remove all files when editing a post', async ({pw}) => {
|
||||
|
|
@ -287,9 +287,9 @@ test('MM-T5655_2 should be able to remove all files when editing a post', async
|
|||
const updatedPost = await channelsPage.getLastPost();
|
||||
await updatedPost.toBeVisible();
|
||||
await updatedPost.toContainText(originalMessage);
|
||||
expect(updatedPost).not.toContain('archive.zip');
|
||||
expect(updatedPost).not.toContain('mattermost.png');
|
||||
expect(updatedPost).not.toContain('sample_text_file.txt');
|
||||
await updatedPost.toNotContainText('archive.zip');
|
||||
await updatedPost.toNotContainText('mattermost.png');
|
||||
await updatedPost.toNotContainText('sample_text_file.txt');
|
||||
});
|
||||
|
||||
test('MM-T5656_1 should be able to restore previously edited post version that contains attachments', async ({pw}) => {
|
||||
|
|
@ -322,7 +322,7 @@ test('MM-T5656_1 should be able to restore previously edited post version that c
|
|||
const updatedPost = await channelsPage.getLastPost();
|
||||
await updatedPost.toBeVisible();
|
||||
await updatedPost.toContainText(newMessage);
|
||||
expect(updatedPost).not.toContain('sample_text_file.txt');
|
||||
await updatedPost.toNotContainText('sample_text_file.txt');
|
||||
|
||||
const postID = await channelsPage.centerView.getLastPostID();
|
||||
await channelsPage.centerView.clickOnLastEditedPost(postID);
|
||||
|
|
@ -338,7 +338,7 @@ test('MM-T5656_1 should be able to restore previously edited post version that c
|
|||
|
||||
const restoredPost = await channelsPage.getLastPost();
|
||||
await restoredPost.toBeVisible();
|
||||
expect(restoredPost.toContainText('sample_text_file.txt'));
|
||||
await restoredPost.toContainText('sample_text_file.txt');
|
||||
});
|
||||
|
||||
async function moveMouseToCenter(page: Page) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify that pressing Shift+Up in the textbox in center channel opens the thread for the last post in RHS
|
||||
* and correctly focuses the reply textbox, even when there are large messages with attachments from other users.
|
||||
*/
|
||||
test(
|
||||
'Keyboard shortcuts Shift+Up on center textbox opens the last post in the RHS and correctly focuses the reply textbox',
|
||||
{tag: '@keyboard_shortcuts'},
|
||||
async ({pw}, testInfo) => {
|
||||
const ROOT_MESSAGE = 'The root message for testing Shift+Up keyboard shortcut';
|
||||
const NUMBER_OF_REPLIES = 10;
|
||||
const ATTACHMENT_FILES = ['mattermost.png', 'sample_text_file.txt', 'archive.zip'];
|
||||
|
||||
test.skip(testInfo.project.name === 'ipad', 'Skipping test on iPad');
|
||||
|
||||
// # Initialize setup with admin and user
|
||||
const {adminUser, user, team} = await pw.initSetup();
|
||||
|
||||
// # Log in as admin in one browser session
|
||||
const {channelsPage: adminChannelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await adminChannelsPage.goto(team.name, 'town-square');
|
||||
await adminChannelsPage.toBeVisible();
|
||||
|
||||
// # Have admin post the root message for the thread
|
||||
await adminChannelsPage.centerView.postCreate.postMessage(ROOT_MESSAGE);
|
||||
|
||||
// # Have admin open the thread and post multiple replies with attachments
|
||||
const rootPost = await adminChannelsPage.getLastPost();
|
||||
await rootPost.hover();
|
||||
await rootPost.postMenu.toBeVisible();
|
||||
await rootPost.postMenu.reply();
|
||||
|
||||
// * Verify RHS is visible for admin
|
||||
await adminChannelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// # Firstly let admin create a series of random replies to the root message
|
||||
for (let i = 1; i <= NUMBER_OF_REPLIES; i++) {
|
||||
await adminChannelsPage.sidebarRight.postCreate.postMessage(`Random replies number ${i}`.repeat(40));
|
||||
}
|
||||
|
||||
// # Secondly let admin create a series of random replies to the root message with attachments
|
||||
for (const file of ATTACHMENT_FILES) {
|
||||
await adminChannelsPage.sidebarRight.postCreate.postMessage(
|
||||
`Random replies number with attachment: ${file}`,
|
||||
[file],
|
||||
);
|
||||
}
|
||||
|
||||
// # Admin closes the RHS
|
||||
await adminChannelsPage.sidebarRight.close();
|
||||
|
||||
// # Log in as regular user in a separate browser session
|
||||
const {channelsPage: userChannelsPage, page: userPage} = await pw.testBrowser.login(user);
|
||||
await userChannelsPage.goto(team.name, 'town-square');
|
||||
await userChannelsPage.toBeVisible();
|
||||
|
||||
// # Bring focus to the post textbox in center channel
|
||||
await userChannelsPage.centerView.postCreate.input.focus();
|
||||
|
||||
// * Verify the post textbox in center channel is focused
|
||||
await expect(userChannelsPage.centerView.postCreate.input).toBeFocused();
|
||||
|
||||
// # Press Shift+Up to open the latest thread in the channel in the RHS
|
||||
await userPage.keyboard.press('Shift+ArrowUp');
|
||||
|
||||
// * Verify RHS is visible
|
||||
await userChannelsPage.sidebarRight.toBeVisible();
|
||||
|
||||
// * Verify the correct thread (admin's root message) is shown in RHS
|
||||
await userChannelsPage.sidebarRight.toContainText(ROOT_MESSAGE);
|
||||
|
||||
// * Verify RHS reply textbox is focused only
|
||||
await expect(userChannelsPage.sidebarRight.postCreate.input).toBeFocused();
|
||||
|
||||
// # Type a message to verify the textbox can receive input immediately
|
||||
await userPage.keyboard.type('Reply typed after Shift+Up');
|
||||
|
||||
// * Verify the message was typed into the RHS textbox
|
||||
const inputValue = await userChannelsPage.sidebarRight.postCreate.getInputValue();
|
||||
expect(inputValue).toBe('Reply typed after Shift+Up');
|
||||
|
||||
// # Clear the input and close RHS
|
||||
await userChannelsPage.sidebarRight.postCreate.input.clear();
|
||||
await userChannelsPage.sidebarRight.close();
|
||||
},
|
||||
);
|
||||
|
|
@ -14,13 +14,7 @@ test('displays multiple mentions correctly in Recent Mentions panel', {tag: '@me
|
|||
const MENTION_COUNT = 20;
|
||||
|
||||
// # Initialize the first user who will create the mentions
|
||||
const {
|
||||
team,
|
||||
user: mentioningUser,
|
||||
userClient,
|
||||
} = await pw.initSetup({
|
||||
userPrefix: 'mentioner',
|
||||
});
|
||||
const {team, user: mentioningUser, userClient} = await pw.initSetup({userOptions: {prefix: 'mentioner'}});
|
||||
|
||||
// # Create a second user to be mentioned
|
||||
const {adminClient} = await pw.getAdminClient();
|
||||
|
|
|
|||
|
|
@ -173,5 +173,12 @@ test('MM-63378 System Manager without team access permissions cannot view team d
|
|||
await expect(teamStatsHeading).toBeVisible();
|
||||
|
||||
// Verify the user has no API access to the otherTeam.
|
||||
await expect(systemManagerClient.getTeam(otherTeam.id)).rejects.toThrow();
|
||||
let apiError: Error | null = null;
|
||||
try {
|
||||
await systemManagerClient.getTeam(otherTeam.id);
|
||||
} catch (error) {
|
||||
apiError = error as Error;
|
||||
}
|
||||
expect(apiError).not.toBeNull();
|
||||
expect(apiError?.message).toContain('You do not have the appropriate permissions');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ test.describe('System Console - Admin User Profile Editing', () => {
|
|||
({team, adminUser, adminClient} = await pw.initSetup());
|
||||
|
||||
// Create test user to edit
|
||||
testUser = await pw.createNewUserProfile(adminClient, 'admin-edit-target-');
|
||||
testUser = await pw.createNewUserProfile(adminClient, {prefix: 'admin-edit-target-'});
|
||||
await adminClient.addToTeam(team.id, testUser.id);
|
||||
|
||||
// Set up custom user attribute fields
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {expect, test} from '@mattermost/playwright-lib';
|
||||
|
||||
/**
|
||||
* @objective Verify archived channel icons display correctly for public and private channels in various UI contexts
|
||||
*/
|
||||
test(
|
||||
'displays archive icons for public and private channels in sidebar',
|
||||
{tag: ['@visual', '@archived_channels', '@snapshots']},
|
||||
async ({pw, browserName, viewport}, testInfo) => {
|
||||
// # Initialize setup and create test channels
|
||||
const {team, user, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create public and private channels
|
||||
const publicChannel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'public-to-archive',
|
||||
displayName: 'Public Archive Test',
|
||||
type: 'O',
|
||||
}),
|
||||
);
|
||||
|
||||
const privateChannel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'private-to-archive',
|
||||
displayName: 'Private Archive Test',
|
||||
type: 'P',
|
||||
}),
|
||||
);
|
||||
|
||||
// # Archive both channels
|
||||
await adminClient.deleteChannel(publicChannel.id);
|
||||
await adminClient.deleteChannel(privateChannel.id);
|
||||
|
||||
// # Log in user
|
||||
const {page, channelsPage} = await pw.testBrowser.login(user);
|
||||
|
||||
// # Visit town square to ensure we're in a stable state
|
||||
await channelsPage.goto(team.name, 'town-square');
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
// # Open browse channels modal to show archived channels
|
||||
await page.keyboard.press('Control+K');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// # Type to search for archived channels
|
||||
await page.keyboard.type('archive');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// # Hide dynamic content
|
||||
await pw.hideDynamicChannelsContent(page);
|
||||
|
||||
// * Verify channel switcher shows both archived channels with correct icons
|
||||
const testArgs = {page, browserName, viewport};
|
||||
await pw.matchSnapshot(testInfo, testArgs);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify archived channel icons display correctly in admin console channel list
|
||||
*/
|
||||
test(
|
||||
'displays archive icons in admin console channel list',
|
||||
{tag: ['@visual', '@archived_channels', '@admin_console', '@snapshots']},
|
||||
async ({pw, browserName, viewport}, testInfo) => {
|
||||
// # Initialize setup with admin user
|
||||
const {team, adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create public and private channels
|
||||
const publicChannel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'admin-public-archive',
|
||||
displayName: 'Admin Public Archive',
|
||||
type: 'O',
|
||||
}),
|
||||
);
|
||||
|
||||
const privateChannel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'admin-private-archive',
|
||||
displayName: 'Admin Private Archive',
|
||||
type: 'P',
|
||||
}),
|
||||
);
|
||||
|
||||
// # Archive both channels
|
||||
await adminClient.deleteChannel(publicChannel.id);
|
||||
await adminClient.deleteChannel(privateChannel.id);
|
||||
|
||||
// # Log in as admin
|
||||
const {page} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Navigate to admin console channels list
|
||||
await page.goto('/admin_console/user_management/channels');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// # Wait for channel list to load
|
||||
await expect(page.locator('.DataGrid')).toBeVisible({timeout: 10000});
|
||||
|
||||
// # Search for our test channels to bring them into view
|
||||
await page.fill('[data-testid="searchInput"]', 'Admin');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// * Verify both archived channels are visible with correct icons
|
||||
const testArgs = {page, browserName, viewport};
|
||||
await pw.matchSnapshot(testInfo, testArgs);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @objective Verify archived private channel icon displays in channel header when viewing archived channel
|
||||
*/
|
||||
test(
|
||||
'displays archive icon in channel header for archived private channel',
|
||||
{tag: ['@visual', '@archived_channels', '@channel_header', '@snapshots']},
|
||||
async ({pw, browserName, viewport}, testInfo) => {
|
||||
// # Initialize setup
|
||||
const {team, adminUser, adminClient} = await pw.initSetup();
|
||||
|
||||
// # Create a private channel
|
||||
const privateChannel = await adminClient.createChannel(
|
||||
pw.random.channel({
|
||||
teamId: team.id,
|
||||
name: 'private-header-test',
|
||||
displayName: 'Private Header Test',
|
||||
type: 'P',
|
||||
}),
|
||||
);
|
||||
|
||||
// # Archive the channel
|
||||
await adminClient.deleteChannel(privateChannel.id);
|
||||
|
||||
const {page, channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
|
||||
// # Visit the archived channel directly
|
||||
await channelsPage.goto(team.name, privateChannel.name);
|
||||
|
||||
// # Wait for channel header to load (archived channels don't have post-create)
|
||||
await expect(page.locator('.channel-header')).toBeVisible();
|
||||
|
||||
// # Verify archived channel message is visible
|
||||
await expect(page.locator('#channelArchivedMessage')).toBeVisible();
|
||||
|
||||
// # Hide dynamic content
|
||||
await pw.hideDynamicChannelsContent(page);
|
||||
|
||||
// # Focus on channel header area for snapshot
|
||||
const headerElement = page.locator('.channel-header');
|
||||
await expect(headerElement).toBeVisible();
|
||||
|
||||
// * Verify channel header shows archive-lock icon for private archived channel
|
||||
const testArgs = {page, browserName, viewport};
|
||||
await pw.matchSnapshot(testInfo, testArgs);
|
||||
},
|
||||
);
|
||||
16
enable-claude-docs.sh
Executable file
16
enable-claude-docs.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This script enables the optional Claude documentation by copying
|
||||
# CLAUDE.OPTIONAL.md files to CLAUDE.md.
|
||||
# CLAUDE.md files are gitignored, so they act as local-only documentation.
|
||||
|
||||
echo "Enabling Claude documentation..."
|
||||
|
||||
find . -name "CLAUDE.OPTIONAL.md" -not -path "*/node_modules/*" | while read -r file; do
|
||||
target_file="${file%.OPTIONAL.md}.md"
|
||||
echo "Copying $file to $target_file"
|
||||
cp "$file" "$target_file"
|
||||
done
|
||||
|
||||
echo "Done! CLAUDE.md files are now active (and ignored by git). *NOTE: Re-running this script will overwrite any changes you'd made to the CLAUDE.md files. If you have an improvement, please change the relevant CLAUDE.OPTIONAL.md file instead, and submit a PR."
|
||||
|
||||
|
|
@ -1 +1 @@
|
|||
1.24.6
|
||||
1.24.11
|
||||
|
|
|
|||
|
|
@ -41,9 +41,6 @@ linters:
|
|||
- linters:
|
||||
- misspell
|
||||
path: platform/shared/markdown/html_entities.go
|
||||
- linters:
|
||||
- unqueryvet
|
||||
path: channels/store/sqlstore/post_store.go
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: SA1019
|
||||
|
|
|
|||
|
|
@ -155,12 +155,12 @@ PLUGIN_PACKAGES ?= $(PLUGIN_PACKAGES:)
|
|||
PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-github-v2.5.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.11.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jira-v4.5.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-jira-v4.5.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.6.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.11.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-agents-v1.7.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.2
|
||||
PLUGIN_PACKAGES += mattermost-plugin-user-survey-v1.1.1
|
||||
PLUGIN_PACKAGES += mattermost-plugin-mscalendar-v1.5.0
|
||||
PLUGIN_PACKAGES += mattermost-plugin-msteams-meetings-v2.3.0
|
||||
|
|
@ -789,7 +789,18 @@ endif
|
|||
|
||||
vet: ## Run mattermost go vet specific checks
|
||||
$(GO) install github.com/mattermost/mattermost-govet/v2@7d8db289e508999dfcac47b97c9490a0fec12d66
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -requestCtxNaming -license -inconsistentReceiverName.ignore=session_serial_gen.go,team_member_serial_gen.go,user_serial_gen.go,utils_serial_gen.go ./...
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet \
|
||||
-structuredLogging \
|
||||
-inconsistentReceiverName \
|
||||
-emptyStrCmp \
|
||||
-tFatal \
|
||||
-configtelemetry \
|
||||
-errorAssertions \
|
||||
-requestCtxNaming \
|
||||
-license \
|
||||
-inconsistentReceiverName.ignore=session_serial_gen.go,team_member_serial_gen.go,user_serial_gen.go,utils_serial_gen.go \
|
||||
-noSelectStar \
|
||||
./...
|
||||
ifeq ($(BUILD_ENTERPRISE_READY),true)
|
||||
ifneq ($(MM_NO_ENTERPRISE_LINT),true)
|
||||
$(GO) vet -vettool=$(GOBIN)/mattermost-govet -structuredLogging -inconsistentReceiverName -emptyStrCmp -tFatal -configtelemetry -errorAssertions -requestCtxNaming -enterpriseLicense $(BUILD_ENTERPRISE_DIR)/...
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1.24.6-bullseye@sha256:cf78ce8205287fdb2ca403aac77d68965c75734749e560c577c00e20ecb11954
|
||||
FROM mattermost/golang-bullseye:1.24.11@sha256:648e6d4bd76751787cf8eb2674942f931a01043872ce15ac9501382dabcefbe8
|
||||
ARG NODE_VERSION=20.11.1
|
||||
|
||||
RUN apt-get update && apt-get install -y make git apt-transport-https ca-certificates curl software-properties-common build-essential zip xmlsec1 jq pgloader gnupg
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM cgr.dev/mattermost.com/go-msft-fips:1.24.6-dev@sha256:53d076b1cfa53f8189c4723d813d711d92107c2e8b140805c71e39f4a06dc9cc
|
||||
FROM cgr.dev/mattermost.com/go-msft-fips:1.24.11-dev@sha256:181a7db41bbff8cf0e522bd5f951a44f2a39a5f58ca930930dfbecdc6b690272
|
||||
ARG NODE_VERSION=20.11.1
|
||||
|
||||
RUN apk add curl ca-certificates mailcap unrtf wv poppler-utils tzdata gpg xmlsec
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# First stage - FIPS dev image with dependencies for building
|
||||
FROM cgr.dev/mattermost.com/glibc-openssl-fips:15-dev@sha256:9223f9245fb026a3c255ce9b7028a069fe11432aa7710713a331eaa36f44851c AS builder
|
||||
FROM cgr.dev/mattermost.com/glibc-openssl-fips:15-dev@sha256:ab5285209fff77fbe56e58aeed6d7f557cf74c6f90d1d8ee26053003f039b419 AS builder
|
||||
# Setting bash as our shell, and enabling pipefail option
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func createAccessControlPolicy(c *Context, w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, policy.ID, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, policy.ID, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
@ -197,7 +197,7 @@ func checkExpression(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
@ -245,7 +245,7 @@ func testExpression(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
@ -321,7 +321,7 @@ func validateExpressionAgainstRequester(c *Context, w http.ResponseWriter, r *ht
|
|||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
@ -450,7 +450,7 @@ func setActiveStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
hasManageSystemPermission := c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem)
|
||||
if !hasManageSystemPermission {
|
||||
for _, entry := range list.Entries {
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, entry.ID, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, entry.ID, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
@ -661,7 +661,7 @@ func getFieldsAutocomplete(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
@ -735,7 +735,7 @@ func convertToVisualAST(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// SECURE: Check specific channel permission
|
||||
hasChannelPermission := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
hasChannelPermission, _ := c.App.HasPermissionToChannel(c.AppContext, c.AppContext.Session().UserId, channelId, model.PermissionManageChannelAccessRules)
|
||||
if !hasChannelPermission {
|
||||
c.SetPermissionError(model.PermissionManageChannelAccessRules)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ type Routes struct {
|
|||
|
||||
Jobs *mux.Router // 'api/v4/jobs'
|
||||
|
||||
Recaps *mux.Router // 'api/v4/recaps'
|
||||
|
||||
Preferences *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/preferences'
|
||||
|
||||
License *mux.Router // 'api/v4/license'
|
||||
|
|
@ -256,6 +258,7 @@ func Init(srv *app.Server) (*API, error) {
|
|||
api.BaseRoutes.Public = api.BaseRoutes.APIRoot.PathPrefix("/public").Subrouter()
|
||||
api.BaseRoutes.Reactions = api.BaseRoutes.APIRoot.PathPrefix("/reactions").Subrouter()
|
||||
api.BaseRoutes.Jobs = api.BaseRoutes.APIRoot.PathPrefix("/jobs").Subrouter()
|
||||
api.BaseRoutes.Recaps = api.BaseRoutes.APIRoot.PathPrefix("/recaps").Subrouter()
|
||||
api.BaseRoutes.Elasticsearch = api.BaseRoutes.APIRoot.PathPrefix("/elasticsearch").Subrouter()
|
||||
api.BaseRoutes.DataRetention = api.BaseRoutes.APIRoot.PathPrefix("/data_retention").Subrouter()
|
||||
|
||||
|
|
@ -337,6 +340,7 @@ func Init(srv *app.Server) (*API, error) {
|
|||
api.InitDataRetention()
|
||||
api.InitBrand()
|
||||
api.InitJob()
|
||||
api.InitRecap()
|
||||
api.InitCommand()
|
||||
api.InitStatus()
|
||||
api.InitWebSocket()
|
||||
|
|
|
|||
|
|
@ -166,13 +166,13 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
switch oldChannel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
|
||||
return
|
||||
}
|
||||
|
|
@ -277,14 +277,18 @@ func updateChannelPrivacy(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
auditRec.AddEventPriorState(channel)
|
||||
|
||||
if model.ChannelType(privacy) == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPrivateChannelToPublic) {
|
||||
c.SetPermissionError(model.PermissionConvertPrivateChannelToPublic)
|
||||
return
|
||||
if model.ChannelType(privacy) == model.ChannelTypeOpen {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPrivateChannelToPublic); !ok {
|
||||
c.SetPermissionError(model.PermissionConvertPrivateChannelToPublic)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if model.ChannelType(privacy) == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPublicChannelToPrivate) {
|
||||
c.SetPermissionError(model.PermissionConvertPublicChannelToPrivate)
|
||||
return
|
||||
if model.ChannelType(privacy) == model.ChannelTypePrivate {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionConvertPublicChannelToPrivate); !ok {
|
||||
c.SetPermissionError(model.PermissionConvertPublicChannelToPrivate)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if channel.Name == model.DefaultChannelName && model.ChannelType(privacy) == model.ChannelTypePrivate {
|
||||
|
|
@ -342,13 +346,13 @@ func patchChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
switch oldChannel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelProperties); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePublicChannelProperties)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelProperties); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePrivateChannelProperties)
|
||||
return
|
||||
}
|
||||
|
|
@ -664,15 +668,15 @@ func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
if !isContentReviewer {
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadPublicChannel)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -698,7 +702,7 @@ func getChannelUnread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -723,7 +727,7 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -813,7 +817,8 @@ func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
var hasPermission, isMember bool
|
||||
if hasPermission, isMember = c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel); !hasPermission {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -829,7 +834,7 @@ func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
clientPostList := c.App.PreparePostListForClient(c.AppContext, posts)
|
||||
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
clientPostList, isMemberForAllPreviews, err := c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -839,6 +844,14 @@ func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := clientPostList.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventUpdateChannelMemberRoles, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
|
||||
if !isMember || !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
}
|
||||
|
||||
func getAllChannels(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -1035,7 +1048,7 @@ func getPublicChannelsByIdsForTeam(c *Context, w http.ResponseWriter, r *http.Re
|
|||
|
||||
if session := c.AppContext.Session(); session.IsGuest() {
|
||||
for _, channel := range channels {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *session, channel.Id, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *session, channel.Id, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -1387,14 +1400,18 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePublicChannel) {
|
||||
c.SetPermissionError(model.PermissionDeletePublicChannel)
|
||||
return
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePublicChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionDeletePublicChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePrivateChannel) {
|
||||
c.SetPermissionError(model.PermissionDeletePrivateChannel)
|
||||
return
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionDeletePrivateChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionDeletePrivateChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if c.Params.Permanent {
|
||||
|
|
@ -1437,16 +1454,19 @@ func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
|
||||
c.SetPermissionError(model.PermissionReadPublicChannel)
|
||||
return
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadPublicChannel)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// allows team admins to access private channel
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageTeam) &&
|
||||
!c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel) {
|
||||
c.Err = model.NewAppError("getChannelByName", "app.channel.get_by_name.missing.app_error", nil, "teamId="+channel.TeamId+", "+"name="+channel.Name+"", http.StatusNotFound)
|
||||
return
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionManageTeam) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel); !ok {
|
||||
c.Err = model.NewAppError("getChannelByName", "app.channel.get_by_name.missing.app_error", nil, "teamId="+channel.TeamId+", "+"name="+channel.Name+"", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1474,7 +1494,7 @@ func getChannelByNameForTeamName(c *Context, w http.ResponseWriter, r *http.Requ
|
|||
return
|
||||
}
|
||||
|
||||
channelOk := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel)
|
||||
channelOk, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionReadChannel)
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
teamOk := c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionReadPublicChannel)
|
||||
if !teamOk && !channelOk {
|
||||
|
|
@ -1506,7 +1526,7 @@ func getChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -1534,7 +1554,7 @@ func getChannelMembersTimezones(c *Context, w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -1565,7 +1585,7 @@ func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -1593,7 +1613,7 @@ func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -1746,7 +1766,7 @@ func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request
|
|||
model.AddEventParameterToAuditRec(auditRec, "props", props)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles); !ok {
|
||||
c.SetPermissionError(model.PermissionManageChannelRoles)
|
||||
return
|
||||
}
|
||||
|
|
@ -1778,7 +1798,7 @@ func updateChannelMemberSchemeRoles(c *Context, w http.ResponseWriter, r *http.R
|
|||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "roles", &schemeRoles)
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManageChannelRoles); !ok {
|
||||
c.SetPermissionError(model.PermissionManageChannelRoles)
|
||||
return
|
||||
}
|
||||
|
|
@ -1889,7 +1909,7 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
// Security check: if the user is a guest, they must have access to the channel
|
||||
// to view its members
|
||||
if c.AppContext.Session().IsGuest() {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if hasPermission, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !hasPermission {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -1917,13 +1937,13 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), channel.TeamId, model.PermissionJoinPublicChannels) {
|
||||
canAddSelf = true
|
||||
}
|
||||
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
|
||||
if hasPermission, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers); hasPermission {
|
||||
canAddOthers = true
|
||||
}
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
|
||||
if hasPermission, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers); !hasPermission {
|
||||
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
|
||||
return
|
||||
}
|
||||
|
|
@ -2086,14 +2106,18 @@ func removeChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if c.Params.UserId != c.AppContext.Session().UserId {
|
||||
if channel.Type == model.ChannelTypeOpen && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers) {
|
||||
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
|
||||
return
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePublicChannelMembers); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePublicChannelMembers)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypePrivate && !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers) {
|
||||
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
|
||||
return
|
||||
if channel.Type == model.ChannelTypePrivate {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channel.Id, model.PermissionManagePrivateChannelMembers); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePrivateChannelMembers)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2235,7 +2259,7 @@ func channelMemberCountsByGroup(c *Context, w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -2457,7 +2481,7 @@ func getDirectOrGroupMessageMembersCommonTeams(c *Context, w http.ResponseWriter
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -2531,12 +2555,12 @@ func canEditChannelBanner(c *Context, originalChannel *model.Channel) {
|
|||
|
||||
switch originalChannel.Type {
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelBanner) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePrivateChannelBanner); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePrivateChannelBanner)
|
||||
return
|
||||
}
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelBanner) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionManagePublicChannelBanner); !ok {
|
||||
c.SetPermissionError(model.PermissionManagePublicChannelBanner)
|
||||
return
|
||||
}
|
||||
|
|
@ -2551,7 +2575,7 @@ func getChannelAccessControlAttributes(c *Context, w http.ResponseWriter, r *htt
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionReadChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannel)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ func createChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPublicChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPublicChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionAddBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPrivateChannel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionAddBookmarkPrivateChannel); !ok {
|
||||
c.SetPermissionError(model.PermissionAddBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
|
@ -158,18 +158,23 @@ func updateChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
isMember := false
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPublicChannel) {
|
||||
ok, member := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPublicChannel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionEditBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
isMember = member
|
||||
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPrivateChannel) {
|
||||
ok, member := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionEditBookmarkPrivateChannel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionEditBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
isMember = member
|
||||
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
|
|
@ -178,6 +183,7 @@ func updateChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
isMember = true
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
|
|
@ -201,6 +207,10 @@ func updateChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updateChannelBookmarkResponse)
|
||||
auditRec.AddEventObjectType("updateChannelBookmarkResponse")
|
||||
|
|
@ -250,19 +260,22 @@ func updateChannelBookmarkSortOrder(c *Context, w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
isMember := false
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPublicChannel) {
|
||||
ok, member := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPublicChannel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionOrderBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
isMember = member
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPrivateChannel) {
|
||||
ok, member := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionOrderBookmarkPrivateChannel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionOrderBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
||||
isMember = member
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
|
||||
|
|
@ -270,6 +283,7 @@ func updateChannelBookmarkSortOrder(c *Context, w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
isMember = true
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
|
|
@ -292,6 +306,10 @@ func updateChannelBookmarkSortOrder(c *Context, w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
for _, b := range bookmarks {
|
||||
if b.Id == c.Params.ChannelBookmarkId {
|
||||
auditRec.AddEventResultState(b)
|
||||
|
|
@ -335,19 +353,22 @@ func deleteChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
isMember := false
|
||||
switch channel.Type {
|
||||
case model.ChannelTypeOpen:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPublicChannel) {
|
||||
ok, member := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPublicChannel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionDeleteBookmarkPublicChannel)
|
||||
return
|
||||
}
|
||||
|
||||
isMember = member
|
||||
case model.ChannelTypePrivate:
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPrivateChannel) {
|
||||
ok, member := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionDeleteBookmarkPrivateChannel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionDeleteBookmarkPrivateChannel)
|
||||
return
|
||||
}
|
||||
|
||||
isMember = member
|
||||
case model.ChannelTypeGroup, model.ChannelTypeDirect:
|
||||
// Any member of DM/GMs but guests can manage channel bookmarks
|
||||
if _, errGet := c.App.GetChannelMember(c.AppContext, channel.Id, c.AppContext.Session().UserId); errGet != nil {
|
||||
|
|
@ -355,6 +376,7 @@ func deleteChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
isMember = true
|
||||
user, gAppErr := c.App.GetUser(c.AppContext.Session().UserId)
|
||||
if gAppErr != nil {
|
||||
c.Err = gAppErr
|
||||
|
|
@ -390,6 +412,10 @@ func deleteChannelBookmark(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(bookmark)
|
||||
c.LogAudit("bookmark=" + bookmark.DisplayName)
|
||||
|
|
@ -416,7 +442,8 @@ func listChannelBookmarksForChannel(c *Context, w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
hasPermission, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -427,6 +454,13 @@ func listChannelBookmarksForChannel(c *Context, w http.ResponseWriter, r *http.R
|
|||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventListChannelBookmarksForChannel, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(bookmarks); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -256,7 +255,7 @@ func TestCreateChannel(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Verify that the guest user can access the private channel they were added to
|
||||
_, _, err = guestClient.GetChannel(context.Background(), private.Id, "")
|
||||
_, _, err = guestClient.GetChannel(context.Background(), private.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that the guest user cannot add members to the private channel
|
||||
|
|
@ -269,7 +268,7 @@ func TestCreateChannel(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Verify that the guest user can access the public channel they were added to
|
||||
_, _, err = guestClient.GetChannel(context.Background(), public.Id, "")
|
||||
_, _, err = guestClient.GetChannel(context.Background(), public.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify that the guest user cannot add members to the public channel
|
||||
|
|
@ -1625,50 +1624,50 @@ func TestGetChannel(t *testing.T) {
|
|||
th := Setup(t).InitBasic(t)
|
||||
client := th.Client
|
||||
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, th.BasicChannel.Id, channel.Id, "ids did not match")
|
||||
|
||||
_, err = client.RemoveUserFromChannel(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
_, _, err = client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
_, _, err = client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channel, _, err = client.GetChannel(context.Background(), th.BasicPrivateChannel.Id, "")
|
||||
channel, _, err = client.GetChannel(context.Background(), th.BasicPrivateChannel.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, th.BasicPrivateChannel.Id, channel.Id, "ids did not match")
|
||||
|
||||
_, err = client.RemoveUserFromChannel(context.Background(), th.BasicPrivateChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
_, resp, err := client.GetChannel(context.Background(), th.BasicPrivateChannel.Id, "")
|
||||
_, resp, err := client.GetChannel(context.Background(), th.BasicPrivateChannel.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
_, resp, err = client.GetChannel(context.Background(), model.NewId(), "")
|
||||
_, resp, err = client.GetChannel(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
|
||||
_, err = client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
_, resp, err = client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
_, resp, err = client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
|
||||
user := th.CreateUser(t)
|
||||
_, _, err = client.Login(context.Background(), user.Email, user.Password)
|
||||
require.NoError(t, err)
|
||||
_, resp, err = client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
_, resp, err = client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
|
||||
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
|
||||
_, _, err = client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
_, _, err = client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = client.GetChannel(context.Background(), th.BasicPrivateChannel.Id, "")
|
||||
_, _, err = client.GetChannel(context.Background(), th.BasicPrivateChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, resp, err = client.GetChannel(context.Background(), th.BasicUser.Id, "")
|
||||
_, resp, err = client.GetChannel(context.Background(), th.BasicUser.Id)
|
||||
require.Error(t, err)
|
||||
CheckNotFoundStatus(t, resp)
|
||||
})
|
||||
|
|
@ -1930,21 +1929,22 @@ func TestGetPublicChannelsByIdsForTeam(t *testing.T) {
|
|||
|
||||
t.Run("should return 2 channels", func(t *testing.T) {
|
||||
input := []string{th.BasicChannel.Id}
|
||||
output := []string{th.BasicChannel.DisplayName}
|
||||
expectedDisplayNames := []string{th.BasicChannel.DisplayName}
|
||||
|
||||
input = append(input, GenerateTestID())
|
||||
input = append(input, th.BasicChannel2.Id)
|
||||
input = append(input, th.BasicPrivateChannel.Id)
|
||||
output = append(output, th.BasicChannel2.DisplayName)
|
||||
sort.Strings(output)
|
||||
expectedDisplayNames = append(expectedDisplayNames, th.BasicChannel2.DisplayName)
|
||||
|
||||
channels, _, err := client.GetPublicChannelsByIdsForTeam(context.Background(), teamId, input)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, channels, 2, "should return 2 channels")
|
||||
|
||||
actualDisplayNames := make([]string, len(channels))
|
||||
for i, c := range channels {
|
||||
require.Equal(t, output[i], c.DisplayName, "missing channel")
|
||||
actualDisplayNames[i] = c.DisplayName
|
||||
}
|
||||
require.ElementsMatch(t, expectedDisplayNames, actualDisplayNames, "missing channel")
|
||||
})
|
||||
|
||||
t.Run("forbidden for invalid team", func(t *testing.T) {
|
||||
|
|
@ -3644,7 +3644,7 @@ func TestViewChannel(t *testing.T) {
|
|||
|
||||
member, _, err := client.GetChannelMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
channel, _, err = client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
channel, _, err = client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, channel.TotalMsgCount, member.MsgCount, "should match message counts")
|
||||
require.Equal(t, int64(0), member.MentionCount, "should have no mentions")
|
||||
|
|
@ -3679,9 +3679,9 @@ func TestReadMultipleChannels(t *testing.T) {
|
|||
user := th.BasicUser
|
||||
|
||||
t.Run("Should successfully mark public channels as read for self", func(t *testing.T) {
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
channel2, _, err := client.GetChannel(context.Background(), th.BasicChannel2.Id, "")
|
||||
channel2, _, err := client.GetChannel(context.Background(), th.BasicChannel2.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channelResponse, _, err := client.ReadMultipleChannels(context.Background(), user.Id, []string{channel.Id, channel2.Id})
|
||||
|
|
@ -3692,7 +3692,7 @@ func TestReadMultipleChannels(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Should successfully mark private channels as read for self", func(t *testing.T) {
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicPrivateChannel.Id, "")
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicPrivateChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
// private channel without membership should be ignored
|
||||
|
|
@ -3704,7 +3704,7 @@ func TestReadMultipleChannels(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("Should fail marking public/private channels for other user", func(t *testing.T) {
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
channel, _, err := client.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = client.ReadMultipleChannels(context.Background(), th.BasicUser2.Id, []string{channel.Id})
|
||||
|
|
@ -3713,9 +3713,9 @@ func TestReadMultipleChannels(t *testing.T) {
|
|||
|
||||
t.Run("Admin should succeed in marking public/private channels for other user", func(t *testing.T) {
|
||||
adminClient := th.SystemAdminClient
|
||||
channel, _, err := adminClient.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
channel, _, err := adminClient.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
privateChannel, _, err := adminClient.GetChannel(context.Background(), th.BasicPrivateChannel.Id, "")
|
||||
privateChannel, _, err := adminClient.GetChannel(context.Background(), th.BasicPrivateChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
channelResponse, _, err := adminClient.ReadMultipleChannels(context.Background(), th.BasicUser2.Id, []string{channel.Id, privateChannel.Id})
|
||||
|
|
@ -3729,9 +3729,9 @@ func TestReadMultipleChannels(t *testing.T) {
|
|||
th.LoginSystemManager(t)
|
||||
sysMgrClient := th.SystemManagerClient
|
||||
|
||||
channel, _, err := sysMgrClient.GetChannel(context.Background(), th.BasicChannel.Id, "")
|
||||
channel, _, err := sysMgrClient.GetChannel(context.Background(), th.BasicChannel.Id)
|
||||
require.NoError(t, err)
|
||||
privateChannel, _, err := sysMgrClient.GetChannel(context.Background(), th.BasicPrivateChannel.Id, "")
|
||||
privateChannel, _, err := sysMgrClient.GetChannel(context.Background(), th.BasicPrivateChannel.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = sysMgrClient.ReadMultipleChannels(context.Background(), th.BasicUser2.Id, []string{channel.Id, privateChannel.Id})
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
model.AddEventParameterAuditableToAuditRec(auditRec, "command_args", &commandArgs)
|
||||
|
||||
// Checks that user is a member of the specified channel, and that they have permission to create a post in it.
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost); !ok {
|
||||
c.SetPermissionError(model.PermissionCreatePost)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,9 @@ func updateConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
// modifications to the slice.
|
||||
cfg.PluginSettings.SignaturePublicKeyFiles = appCfg.PluginSettings.SignaturePublicKeyFiles
|
||||
|
||||
// Do not allow import directory to be changed through the API
|
||||
*cfg.ImportSettings.Directory = *appCfg.ImportSettings.Directory
|
||||
|
||||
// Do not allow marketplace URL to be toggled through the API if EnableUploads are disabled.
|
||||
if cfg.PluginSettings.EnableUploads != nil && !*appCfg.PluginSettings.EnableUploads {
|
||||
*cfg.PluginSettings.MarketplaceURL = *appCfg.PluginSettings.MarketplaceURL
|
||||
|
|
@ -305,6 +308,12 @@ func patchConfig(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Do not allow import directory to be changed through the API
|
||||
if cfg.ImportSettings.Directory != nil && *cfg.ImportSettings.Directory != *appCfg.ImportSettings.Directory {
|
||||
c.Err = model.NewAppError("patchConfig", "api.config.update_config.not_allowed_security.app_error", map[string]any{"Name": "ImportSettings.Directory"}, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Do not allow marketplace URL to be toggled if plugin uploads are disabled.
|
||||
if cfg.PluginSettings.MarketplaceURL != nil && cfg.PluginSettings.EnableUploads != nil {
|
||||
// Breaking it down to 2 conditions to make it simple.
|
||||
|
|
|
|||
|
|
@ -305,6 +305,43 @@ func TestUpdateConfig(t *testing.T) {
|
|||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify ImportSettings.Directory", func(t *testing.T) {
|
||||
t.Run("sysadmin", func(t *testing.T) {
|
||||
oldDirectory := *th.App.Config().ImportSettings.Directory
|
||||
cfg2 := th.App.Config().Clone()
|
||||
*cfg2.ImportSettings.Directory = "./new-import-dir"
|
||||
|
||||
cfg2, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldDirectory, *cfg2.ImportSettings.Directory)
|
||||
assert.Equal(t, oldDirectory, *th.App.Config().ImportSettings.Directory)
|
||||
|
||||
cfg2.ImportSettings.Directory = nil
|
||||
cfg2, _, err = th.SystemAdminClient.UpdateConfig(context.Background(), cfg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldDirectory, *cfg2.ImportSettings.Directory)
|
||||
assert.Equal(t, oldDirectory, *th.App.Config().ImportSettings.Directory)
|
||||
})
|
||||
|
||||
t.Run("local mode", func(t *testing.T) {
|
||||
oldDirectory := *th.App.Config().ImportSettings.Directory
|
||||
cfg2 := th.App.Config().Clone()
|
||||
newDirectory := "./new-import-dir"
|
||||
*cfg2.ImportSettings.Directory = newDirectory
|
||||
|
||||
cfg2, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, newDirectory, *cfg2.ImportSettings.Directory)
|
||||
assert.Equal(t, newDirectory, *th.App.Config().ImportSettings.Directory)
|
||||
|
||||
cfg2.ImportSettings.Directory = nil
|
||||
cfg2, _, err = th.LocalClient.UpdateConfig(context.Background(), cfg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldDirectory, *cfg2.ImportSettings.Directory)
|
||||
assert.Equal(t, oldDirectory, *th.App.Config().ImportSettings.Directory)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
|
||||
siteURL := cfg.ServiceSettings.SiteURL
|
||||
defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
|
||||
|
|
@ -819,6 +856,30 @@ func TestPatchConfig(t *testing.T) {
|
|||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not allowing to change import directory via api, unless local mode", func(t *testing.T) {
|
||||
oldDirectory := *th.App.Config().ImportSettings.Directory
|
||||
config := model.Config{ImportSettings: model.ImportSettings{
|
||||
Directory: model.NewPointer("./new-import-dir"),
|
||||
}}
|
||||
|
||||
updatedConfig, resp, err := client.PatchConfig(context.Background(), &config)
|
||||
if client == th.LocalClient {
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
assert.Equal(t, "./new-import-dir", *updatedConfig.ImportSettings.Directory)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
|
||||
// Reset for local mode
|
||||
if client == th.LocalClient {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ImportSettings.Directory = oldDirectory
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Should not be able to modify PluginSettings.MarketplaceURL if EnableUploads is disabled", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ func flagPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
model.AddEventParameterToAuditRec(auditRec, "postId", postId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "userId", userId)
|
||||
|
||||
post, appErr := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
post, appErr, _ := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
|
|
@ -341,7 +341,7 @@ func getFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, &model.PreparePostForClientOpts{IncludePriority: true, RetainContent: true, IncludeDeleted: true})
|
||||
post, err := c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
|
||||
post, isMemberForPreviews, err := c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -352,6 +352,14 @@ func getFlaggedPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !isMemberForPreviews {
|
||||
previewPost := post.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ func upsertDraft(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
hasPermission := false
|
||||
|
||||
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), draft.ChannelId, model.PermissionCreatePost) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), draft.ChannelId, model.PermissionCreatePost); ok {
|
||||
hasPermission = true
|
||||
} else if channel, err := c.App.GetChannel(c.AppContext, draft.ChannelId); err == nil {
|
||||
// Temporary permission check method until advanced permissions, please do not copy
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ func uploadFileSimple(c *Context, r *http.Request, timestamp time.Time) *model.F
|
|||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", c.Params.ChannelId)
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile); !ok {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@ NextPart:
|
|||
if c.Err != nil {
|
||||
return nil
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, model.PermissionUploadFile); !ok {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -429,7 +429,7 @@ func uploadFileMultipartLegacy(c *Context, mr *multipart.Reader,
|
|||
if c.Err != nil {
|
||||
return nil
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionUploadFile); !ok {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -570,8 +570,8 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "file", fileInfo)
|
||||
|
||||
perm, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if !isContentReviewer {
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if fileInfo.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
|
|
@ -594,6 +594,10 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
auditRec.Success()
|
||||
|
||||
web.WriteFileResponse(fileInfo.Name, fileInfo.MimeType, fileInfo.Size, time.Unix(0, fileInfo.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
}
|
||||
|
||||
func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -615,7 +619,7 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
perm, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
|
|
@ -640,6 +644,13 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
defer fileReader.Close()
|
||||
|
||||
web.WriteFileResponse(info.Name, ThumbnailImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetFileThumbnail, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "file_id", c.Params.FileId)
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
}
|
||||
|
||||
func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -669,7 +680,7 @@ func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
perm, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
|
|
@ -685,6 +696,10 @@ func getFileLink(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
resp := make(map[string]string)
|
||||
link := c.App.GeneratePublicLink(c.GetSiteURLHeader(), info)
|
||||
resp["link"] = link
|
||||
|
|
@ -715,7 +730,7 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
perm, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
|
|
@ -740,6 +755,13 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
defer fileReader.Close()
|
||||
|
||||
web.WriteFileResponse(info.Name, PreviewImageType, 0, time.Unix(0, info.UpdateAt*int64(1000*1000)), *c.App.Config().ServiceSettings.WebserverMode, fileReader, forceDownload, w, r)
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetFilePreview, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "file_id", c.Params.FileId)
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
}
|
||||
|
||||
func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -760,7 +782,7 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
perm := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
perm, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if info.CreatorId == model.BookmarkFileOwner {
|
||||
if !perm {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
|
|
@ -775,6 +797,14 @@ func getFileInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := json.NewEncoder(w).Encode(info); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetFileInfo, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "file_id", c.Params.FileId)
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
}
|
||||
|
||||
func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -879,7 +909,7 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID stri
|
|||
|
||||
startTime := time.Now()
|
||||
|
||||
results, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
results, allFilesHaveMembership, err := c.App.SearchFilesInTeamForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamID, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
|
||||
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
||||
metrics := c.App.Metrics()
|
||||
|
|
@ -897,6 +927,16 @@ func searchFiles(c *Context, w http.ResponseWriter, r *http.Request, teamID stri
|
|||
if err := json.NewEncoder(w).Encode(results); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventSearchFiles, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "search_params", params)
|
||||
|
||||
if !allFilesHaveMembership {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func setInaccessibleFileHeader(w http.ResponseWriter, appErr *model.AppError) {
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@ func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType
|
|||
permission = model.PermissionManagePublicChannelMembers
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, permission) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), syncableID, permission); !ok {
|
||||
return model.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
|
||||
}
|
||||
}
|
||||
|
|
@ -972,7 +972,7 @@ func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppEr
|
|||
} else {
|
||||
permission = model.PermissionReadPublicChannelGroups
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission); !ok {
|
||||
return nil, model.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
|
||||
}
|
||||
|
||||
|
|
@ -1138,7 +1138,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
} else {
|
||||
permission = model.PermissionManagePublicChannelMembers
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), NotAssociatedToChannelID, permission) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), NotAssociatedToChannelID, permission); !ok {
|
||||
c.SetPermissionError(permission)
|
||||
return
|
||||
}
|
||||
|
|
@ -1157,7 +1157,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
} else {
|
||||
permission = model.PermissionManagePublicChannelMembers
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ChannelIDForMemberCount, permission) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ChannelIDForMemberCount, permission); !ok {
|
||||
c.SetPermissionError(permission)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2052,16 +2052,8 @@ func TestGetGroups(t *testing.T) {
|
|||
|
||||
t.Run("not associated to channel", func(t *testing.T) {
|
||||
opts := baseOpts
|
||||
resp, err := th.SystemAdminClient.UpdateChannelRoles(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
opts.NotAssociatedToChannel = th.BasicChannel.Id
|
||||
|
||||
resp, err = th.SystemAdminClient.UpdateChannelRoles(context.Background(), th.BasicChannel.Id, th.BasicUser.Id, "channel_user channel_admin")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
groups, resp, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
|
@ -2070,16 +2062,8 @@ func TestGetGroups(t *testing.T) {
|
|||
|
||||
t.Run("not associated to team", func(t *testing.T) {
|
||||
opts := baseOpts
|
||||
resp, err := th.SystemAdminClient.UpdateTeamMemberRoles(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
opts.NotAssociatedToTeam = th.BasicTeam.Id
|
||||
|
||||
resp, err = th.SystemAdminClient.UpdateTeamMemberRoles(context.Background(), th.BasicTeam.Id, th.BasicUser.Id, "team_user team_admin")
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
groups, resp, err := th.SystemAdminClient.GetGroups(context.Background(), opts)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
|
|
|||
|
|
@ -67,12 +67,12 @@ func doPostAction(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -136,7 +136,7 @@ func submitDialog(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -189,7 +189,7 @@ func lookupDialog(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,13 @@ func createPostChecks(where string, c *Context, post *model.Post) {
|
|||
return
|
||||
}
|
||||
|
||||
if len(post.FileIds) > 0 {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionUploadFile); !ok {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
postHardenedModeCheckWithContext(where, c, post.GetProps())
|
||||
if c.Err != nil {
|
||||
return
|
||||
|
|
@ -110,7 +117,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
rp, err := c.App.CreatePostAsUser(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), c.AppContext.Session().Id, setOnlineBool)
|
||||
rp, isMemberForPreviews, err := c.App.CreatePostAsUser(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), c.AppContext.Session().Id, setOnlineBool)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -119,6 +126,14 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
auditRec.AddEventResultState(rp)
|
||||
auditRec.AddEventObjectType("post")
|
||||
|
||||
if !isMemberForPreviews {
|
||||
previewPost := rp.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
if setOnlineBool {
|
||||
c.App.SetStatusOnline(c.AppContext.Session().UserId, false)
|
||||
}
|
||||
|
|
@ -182,12 +197,13 @@ func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
rp := c.App.SendEphemeralPost(c.AppContext, ephRequest.UserID, c.App.PostWithProxyRemovedFromImageURLs(ephRequest.Post))
|
||||
// We prepare again the post here, so we can ignore the isMemberForPreviews return value from SendEphemeralPost
|
||||
rp, _ := c.App.SendEphemeralPost(c.AppContext, ephRequest.UserID, c.App.PostWithProxyRemovedFromImageURLs(ephRequest.Post))
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
rp = model.AddPostActionCookies(rp, c.App.PostActionCookieSecret())
|
||||
rp = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, rp, &model.PreparePostForClientOpts{IsNewPost: true, IncludePriority: true})
|
||||
rp, err := c.App.SanitizePostMetadataForUser(c.AppContext, rp, c.AppContext.Session().UserId)
|
||||
rp, isMemberForPreviews, err := c.App.SanitizePostMetadataForUser(c.AppContext, rp, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -195,6 +211,19 @@ func createEphemeralPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := rp.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateEphemeralPost, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_id", rp.Id)
|
||||
|
||||
if !isMemberForPreviews {
|
||||
previewPost := rp.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -243,7 +272,8 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
hasPermission, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -294,7 +324,7 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
// to ensure they only reference posts that are actually in the response
|
||||
c.App.AddCursorIdsForPostList(clientPostList, afterPost, beforePost, since, page, perPage, collapsedThreads)
|
||||
|
||||
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
clientPostList, isMemberForAllPreviews, err := c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -303,6 +333,16 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := clientPostList.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetPostsForChannel, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", channelId)
|
||||
if !isMember || !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access_on_previews", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -323,7 +363,8 @@ func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *ht
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
hasPermission, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if !hasPermission {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -364,7 +405,7 @@ func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *ht
|
|||
// to ensure they only reference posts that are actually in the response
|
||||
clientPostList.NextPostId = c.App.GetNextPostIdFromPostList(clientPostList, collapsedThreads)
|
||||
clientPostList.PrevPostId = c.App.GetPrevPostIdFromPostList(clientPostList, collapsedThreads)
|
||||
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
clientPostList, isMemberForAllPreviews, err := c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -376,6 +417,17 @@ func getPostsForChannelAroundLastUnread(c *Context, w http.ResponseWriter, r *ht
|
|||
if err := clientPostList.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetPostsForChannelAroundLastUnread, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", channelId)
|
||||
|
||||
if !isMember || !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access_on_previews", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -423,6 +475,7 @@ func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request)
|
|||
|
||||
pl := model.NewPostList()
|
||||
channelReadPermission := make(map[string]bool)
|
||||
isMemberForAllPosts := true
|
||||
|
||||
for _, post := range posts.Posts {
|
||||
allowed, ok := channelReadPermission[post.ChannelId]
|
||||
|
|
@ -434,8 +487,11 @@ func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request)
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
if c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
|
||||
hasPermission, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if hasPermission {
|
||||
allowed = true
|
||||
isMemberForAllPosts = isMemberForAllPosts && isMember
|
||||
}
|
||||
|
||||
channelReadPermission[post.ChannelId] = allowed
|
||||
|
|
@ -451,11 +507,23 @@ func getFlaggedPostsForUser(c *Context, w http.ResponseWriter, r *http.Request)
|
|||
|
||||
pl.SortByCreateAt()
|
||||
clientPostList := c.App.PreparePostListForClient(c.AppContext, pl)
|
||||
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
clientPostList, isMemberForAllPreviews, err := c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetFlaggedPosts, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_id", channelId)
|
||||
|
||||
if !isMemberForAllPosts || !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access_on_previews", true)
|
||||
}
|
||||
}
|
||||
|
||||
if err := clientPostList.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
|
@ -474,7 +542,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
post, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), includeDeleted)
|
||||
post, err, isMember := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), includeDeleted)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
|
||||
|
|
@ -487,7 +555,7 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
post = c.App.PreparePostForClientWithEmbedsAndImages(c.AppContext, post, &model.PreparePostForClientOpts{IncludePriority: true})
|
||||
post, err = c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
|
||||
post, previewIsMember, err := c.App.SanitizePostMetadataForUser(c.AppContext, post, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -501,6 +569,20 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := post.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetPost, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
|
||||
|
||||
if !isMember || !previewIsMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !previewIsMember {
|
||||
previewPost := post.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getPostsByIds also sets a header to indicate, if posts were truncated as per the cloud plan's limit.
|
||||
|
|
@ -540,16 +622,20 @@ func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
var posts = []*model.Post{}
|
||||
isMemberForAllPosts := true
|
||||
for _, post := range postsList {
|
||||
channel, ok := channelMap[post.ChannelId]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
hasPermission, isMemberForCurrentPost := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if !hasPermission {
|
||||
continue
|
||||
}
|
||||
|
||||
isMemberForAllPosts = isMemberForAllPosts && isMemberForCurrentPost
|
||||
|
||||
post = c.App.PreparePostForClient(c.AppContext, post, &model.PreparePostForClientOpts{IncludePriority: true})
|
||||
post.StripActionIntegrations()
|
||||
posts = append(posts, post)
|
||||
|
|
@ -560,6 +646,14 @@ func getPostsByIds(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := json.NewEncoder(w).Encode(posts); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetPostsByIds, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_ids", postIDs)
|
||||
|
||||
if !isMemberForAllPosts {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
}
|
||||
|
||||
func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -574,7 +668,8 @@ func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditPost) {
|
||||
ok, isMember := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditPost)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionEditPost)
|
||||
return
|
||||
}
|
||||
|
|
@ -590,6 +685,14 @@ func getEditHistoryForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetEditHistoryForPost, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(postsList); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
|
@ -629,12 +732,12 @@ func deletePost(c *Context, w http.ResponseWriter, _ *http.Request) {
|
|||
auditRec.AddEventObjectType("post")
|
||||
|
||||
if c.AppContext.Session().UserId == post.UserId {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeletePost) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeletePost); !ok {
|
||||
c.SetPermissionError(model.PermissionDeletePost)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeleteOthersPosts) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), post.ChannelId, model.PermissionDeleteOthersPosts); !ok {
|
||||
c.SetPermissionError(model.PermissionDeleteOthersPosts)
|
||||
return
|
||||
}
|
||||
|
|
@ -760,7 +863,8 @@ func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if _, err = c.App.GetPostIfAuthorized(c.AppContext, post.Id, c.AppContext.Session(), false); err != nil {
|
||||
var isMember bool
|
||||
if _, err, isMember = c.App.GetPostIfAuthorized(c.AppContext, post.Id, c.AppContext.Session(), false); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
|
@ -770,7 +874,7 @@ func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
clientPostList := c.App.PreparePostListForClient(c.AppContext, list)
|
||||
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
clientPostList, isMemberForAllPreviews, err := c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
|
|
@ -781,6 +885,17 @@ func getPostThread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
if err := clientPostList.EncodeJSON(w); err != nil {
|
||||
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetPostThread, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
|
||||
|
||||
if !isMember || !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access_on_previews", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func searchPostsInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -845,7 +960,7 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId stri
|
|||
|
||||
startTime := time.Now()
|
||||
|
||||
results, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
results, allPostHaveMembership, err := c.App.SearchPostsForUser(c.AppContext, terms, c.AppContext.Session().UserId, teamId, isOrSearch, includeDeletedChannels, timeZoneOffset, page, perPage)
|
||||
|
||||
elapsedTime := float64(time.Since(startTime)) / float64(time.Second)
|
||||
metrics := c.App.Metrics()
|
||||
|
|
@ -860,12 +975,19 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request, teamId stri
|
|||
}
|
||||
|
||||
clientPostList := c.App.PreparePostListForClient(c.AppContext, results.PostList)
|
||||
clientPostList, err = c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
clientPostList, isMemberForAllPreviews, err := c.App.SanitizePostListMetadataForUser(c.AppContext, clientPostList, c.AppContext.Session().UserId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !allPostHaveMembership || !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForAllPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access_on_previews", true)
|
||||
}
|
||||
}
|
||||
|
||||
results = model.MakePostSearchResults(clientPostList, results.Matches)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "search_results", results)
|
||||
auditRec.Success()
|
||||
|
|
@ -909,7 +1031,8 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditPost) {
|
||||
ok, isMember := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditPost)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionEditPost)
|
||||
return
|
||||
}
|
||||
|
|
@ -923,8 +1046,15 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
post.FileIds = originalPost.FileIds
|
||||
}
|
||||
|
||||
// Check upload_file permission only if update is adding NEW files (not just keeping existing ones)
|
||||
checkUploadFilePermissionForNewFiles(c, post.FileIds, originalPost)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if c.AppContext.Session().UserId != originalPost.UserId {
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditOthersPosts) {
|
||||
// We don't need to check the member here, since we already checked it above
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionEditOthersPosts); !ok {
|
||||
c.SetPermissionError(model.PermissionEditOthersPosts)
|
||||
return
|
||||
}
|
||||
|
|
@ -937,12 +1067,22 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
rpost, err := c.App.UpdatePost(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), &model.UpdatePostOptions{SafeUpdate: false})
|
||||
rpost, isMemberForPreviews, err := c.App.UpdatePost(c.AppContext, c.App.PostWithProxyRemovedFromImageURLs(&post), &model.UpdatePostOptions{SafeUpdate: false})
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !isMember || !isMemberForPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForPreviews {
|
||||
previewPost := rpost.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(rpost)
|
||||
|
||||
|
|
@ -975,17 +1115,34 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
postPatchChecks(c, auditRec, post.Message)
|
||||
isMember := postPatchChecks(c, auditRec, post.Message)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(&post), nil)
|
||||
originalPost, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
||||
if err != nil {
|
||||
c.SetPermissionError(model.PermissionEditPost)
|
||||
return
|
||||
}
|
||||
|
||||
if post.FileIds != nil {
|
||||
checkUploadFilePermissionForNewFiles(c, *post.FileIds, originalPost)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
patchedPost, isMemberForPReviews, err := c.App.PatchPost(c.AppContext, c.Params.PostId, c.App.PostPatchWithProxyRemovedFromImageURLs(&post), nil)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !isMember || !isMemberForPReviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(patchedPost)
|
||||
|
||||
|
|
@ -994,11 +1151,11 @@ func patchPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func postPatchChecks(c *Context, auditRec *model.AuditRecord, message *string) {
|
||||
func postPatchChecks(c *Context, auditRec *model.AuditRecord, message *string) bool {
|
||||
originalPost, err := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
||||
if err != nil {
|
||||
c.SetPermissionError(model.PermissionEditPost)
|
||||
return
|
||||
return false
|
||||
}
|
||||
auditRec.AddEventPriorState(originalPost)
|
||||
auditRec.AddEventObjectType("post")
|
||||
|
|
@ -1011,15 +1168,18 @@ func postPatchChecks(c *Context, auditRec *model.AuditRecord, message *string) {
|
|||
permission = model.PermissionEditOthersPosts
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, permission) {
|
||||
ok, isMember := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, permission)
|
||||
if !ok {
|
||||
c.SetPermissionError(permission)
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
if *c.App.Config().ServiceSettings.PostEditTimeLimit != -1 && model.GetMillis() > originalPost.CreateAt+int64(*c.App.Config().ServiceSettings.PostEditTimeLimit*1000) && message != nil {
|
||||
c.Err = model.NewAppError("patchPost", "api.post.update_post.permissions_time_limit.app_error", map[string]any{"timeLimit": *c.App.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
|
||||
return
|
||||
return isMember
|
||||
}
|
||||
|
||||
return isMember
|
||||
}
|
||||
|
||||
func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
|
@ -1035,7 +1195,7 @@ func setPostUnread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -1060,7 +1220,7 @@ func setPostReminder(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
c.SetPermissionError(model.PermissionEditOtherUsers)
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -1103,7 +1263,8 @@ func saveIsPinnedPost(c *Context, w http.ResponseWriter, isPinned bool) {
|
|||
c.Err = err
|
||||
return
|
||||
}
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
ok, isMember := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -1111,11 +1272,22 @@ func saveIsPinnedPost(c *Context, w http.ResponseWriter, isPinned bool) {
|
|||
patch := &model.PostPatch{}
|
||||
patch.IsPinned = model.NewPointer(isPinned)
|
||||
|
||||
patchedPost, err := c.App.PatchPost(c.AppContext, c.Params.PostId, patch, nil)
|
||||
patchedPost, isMemberForPreviews, err := c.App.PatchPost(c.AppContext, c.Params.PostId, patch, nil)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if !isMember || !isMemberForPreviews {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForPreviews {
|
||||
previewPost := patchedPost.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.AddEventResultState(patchedPost)
|
||||
|
||||
auditRec.Success()
|
||||
|
|
@ -1147,7 +1319,7 @@ func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -1186,7 +1358,7 @@ func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -1265,7 +1437,7 @@ func moveThread(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
sourcePost, err := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), false)
|
||||
sourcePost, err, _ := c.App.GetPostIfAuthorized(c.AppContext, c.Params.PostId, c.AppContext.Session(), false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
if err.Id == "app.post.cloud.get.app_error" {
|
||||
|
|
@ -1292,7 +1464,8 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
ok, isMember := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId)
|
||||
if !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -1319,6 +1492,14 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetFileInfosForPost, model.AuditStatusSuccess)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
model.AddEventParameterToAuditRec(auditRec, "post_id", c.Params.PostId)
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "max-age=2592000, private")
|
||||
w.Header().Set(model.HeaderEtagServer, model.GetEtagForFileInfos(infos))
|
||||
if _, err := w.Write(js); err != nil {
|
||||
|
|
@ -1332,7 +1513,86 @@ func getPostInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
info, appErr := c.App.GetPostInfo(c.AppContext, c.Params.PostId)
|
||||
userID := c.AppContext.Session().UserId
|
||||
post, appErr := c.App.GetSinglePost(c.AppContext, c.Params.PostId, false)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
channel, appErr := c.App.GetChannel(c.AppContext, post.ChannelId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
notFoundError := model.NewAppError("GetPostInfo", "app.post.get.app_error", nil, "", http.StatusNotFound)
|
||||
|
||||
var team *model.Team
|
||||
hasPermissionToAccessTeam := false
|
||||
if channel.TeamId != "" {
|
||||
team, appErr = c.App.GetTeam(channel.TeamId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
var teamMember *model.TeamMember
|
||||
teamMember, appErr = c.App.GetTeamMember(c.AppContext, channel.TeamId, userID)
|
||||
if appErr != nil && appErr.StatusCode != http.StatusNotFound {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if appErr == nil {
|
||||
if teamMember.DeleteAt == 0 {
|
||||
hasPermissionToAccessTeam = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPermissionToAccessTeam {
|
||||
if team.AllowOpenInvite {
|
||||
hasPermissionToAccessTeam = c.App.HasPermissionToTeam(c.AppContext, userID, team.Id, model.PermissionJoinPublicTeams)
|
||||
} else {
|
||||
hasPermissionToAccessTeam = c.App.HasPermissionToTeam(c.AppContext, userID, team.Id, model.PermissionJoinPrivateTeams)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This happens in case of DMs and GMs.
|
||||
hasPermissionToAccessTeam = true
|
||||
}
|
||||
|
||||
if !hasPermissionToAccessTeam {
|
||||
c.Err = notFoundError
|
||||
return
|
||||
}
|
||||
|
||||
hasPermissionToAccessChannel := false
|
||||
hasJoinedChannel := false
|
||||
|
||||
_, channelMemberErr := c.App.GetChannelMember(c.AppContext, channel.Id, userID)
|
||||
|
||||
if channelMemberErr == nil {
|
||||
hasPermissionToAccessChannel = true
|
||||
hasJoinedChannel = true
|
||||
}
|
||||
|
||||
if !hasPermissionToAccessChannel {
|
||||
if channel.Type == model.ChannelTypeOpen {
|
||||
hasPermissionToAccessChannel = true
|
||||
} else if channel.Type == model.ChannelTypePrivate {
|
||||
hasPermissionToAccessChannel, _ = c.App.HasPermissionToChannel(c.AppContext, userID, channel.Id, model.PermissionManagePrivateChannelMembers)
|
||||
} else if channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup {
|
||||
hasPermissionToAccessChannel, _ = c.App.HasPermissionToReadChannel(c.AppContext, userID, channel)
|
||||
}
|
||||
}
|
||||
|
||||
if !hasPermissionToAccessChannel {
|
||||
c.Err = notFoundError
|
||||
return
|
||||
}
|
||||
|
||||
info, appErr := c.App.GetPostInfo(c.AppContext, c.Params.PostId, channel, team, userID, hasJoinedChannel)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
|
|
@ -1379,17 +1639,27 @@ func restorePostVersion(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
postPatchChecks(c, auditRec, &toRestorePost.Message)
|
||||
isMember := postPatchChecks(c, auditRec, &toRestorePost.Message)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
updatedPost, appErr := c.App.RestorePostVersion(c.AppContext, c.AppContext.Session().UserId, c.Params.PostId, restoreVersionId)
|
||||
updatedPost, isMemberForPreview, appErr := c.App.RestorePostVersion(c.AppContext, c.AppContext.Session().UserId, c.Params.PostId, restoreVersionId)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
if !isMember || !isMemberForPreview {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
if !isMemberForPreview {
|
||||
previewPost := updatedPost.GetPreviewPost()
|
||||
if previewPost != nil {
|
||||
model.AddEventParameterToAuditRec(auditRec, "preview_post_id", previewPost.Post.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updatedPost)
|
||||
|
||||
|
|
@ -1469,7 +1739,7 @@ func revealPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
model.AddEventParameterToAuditRec(auditRec, "post_id", postId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "user_id", userId)
|
||||
|
||||
post, err := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
post, err, isMember := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
if err.Id == "app.post.cloud.get.app_error" {
|
||||
|
|
@ -1502,6 +1772,10 @@ func revealPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
model.AddEventParameterToAuditRec(auditRec, "non_channel_member_access", true)
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(revealedPost)
|
||||
|
||||
|
|
@ -1526,7 +1800,7 @@ func burnPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
model.AddEventParameterToAuditRec(auditRec, "post_id", postId)
|
||||
model.AddEventParameterToAuditRec(auditRec, "user_id", userId)
|
||||
|
||||
post, err := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
post, err, _ := c.App.GetPostIfAuthorized(c.AppContext, postId, c.AppContext.Session(), false)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
if err.Id == "app.post.cloud.get.app_error" {
|
||||
|
|
|
|||
|
|
@ -274,6 +274,8 @@ func TestCreatePost(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("not logged in", func(t *testing.T) {
|
||||
defer th.LoginBasic(t)
|
||||
|
||||
resp, err := client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
|
@ -285,6 +287,46 @@ func TestCreatePost(t *testing.T) {
|
|||
assert.Nil(t, rpost)
|
||||
})
|
||||
|
||||
t.Run("should prevent creating post with files when user lacks upload_file permission in target channel", func(t *testing.T) {
|
||||
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), th.BasicChannel.Id, "test-file.txt")
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
fileId := fileResp.FileInfos[0].Id
|
||||
|
||||
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
|
||||
defer func() {
|
||||
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
|
||||
}()
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "Test post with file",
|
||||
FileIds: model.StringArray{fileId},
|
||||
}
|
||||
rpost, resp, err := client.CreatePost(context.Background(), post)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
assert.Nil(t, rpost)
|
||||
})
|
||||
|
||||
t.Run("should allow creating post with files when user has upload_file permission", func(t *testing.T) {
|
||||
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), th.BasicChannel.Id, "test-file.txt")
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
fileId := fileResp.FileInfos[0].Id
|
||||
|
||||
post := &model.Post{
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "Test post with file",
|
||||
FileIds: model.StringArray{fileId},
|
||||
}
|
||||
rpost, resp, err := client.CreatePost(context.Background(), post)
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
require.NotNil(t, rpost)
|
||||
assert.Contains(t, rpost.FileIds, fileId)
|
||||
})
|
||||
|
||||
t.Run("CreateAt should match the one provided in the request", func(t *testing.T) {
|
||||
post := basicPost()
|
||||
post.CreateAt = 123
|
||||
|
|
@ -311,6 +353,17 @@ func TestCreatePost(t *testing.T) {
|
|||
require.Nil(t, appErr)
|
||||
require.Zero(t, *createdPost.RemoteId)
|
||||
})
|
||||
t.Run("not logged in", func(t *testing.T) {
|
||||
resp, err := client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
|
||||
post := basicPost()
|
||||
rpost, resp, err := client.CreatePost(context.Background(), post)
|
||||
require.Error(t, err)
|
||||
CheckUnauthorizedStatus(t, resp)
|
||||
assert.Nil(t, rpost)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreatePostForPriority(t *testing.T) {
|
||||
|
|
@ -1424,7 +1477,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
fileIds[i] = fileResp.FileInfos[0].Id
|
||||
}
|
||||
|
||||
rpost, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
rpost, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
|
|
@ -1457,7 +1510,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
|
||||
t.Run("join/leave post", func(t *testing.T) {
|
||||
var rpost2 *model.Post
|
||||
rpost2, appErr = th.App.CreatePost(th.Context, &model.Post{
|
||||
rpost2, _, appErr = th.App.CreatePost(th.Context, &model.Post{
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
Type: model.PostTypeJoinLeave,
|
||||
|
|
@ -1475,7 +1528,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
rpost3, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
rpost3, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
UserId: th.BasicUser.Id,
|
||||
|
|
@ -1507,7 +1560,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
*cfg.ServiceSettings.PostEditTimeLimit = -1
|
||||
})
|
||||
|
||||
rpost4, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
rpost4, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
UserId: th.BasicUser.Id,
|
||||
|
|
@ -1545,6 +1598,62 @@ func TestUpdatePost(t *testing.T) {
|
|||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("should prevent updating post with files when user lacks upload_file permission in target channel", func(t *testing.T) {
|
||||
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Post without files",
|
||||
}, channel, model.CreatePostFlags{SetOnline: true})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), channel.Id, "test-file.txt")
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
fileId := fileResp.FileInfos[0].Id
|
||||
|
||||
th.RemovePermissionFromRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
|
||||
defer func() {
|
||||
th.AddPermissionToRole(t, model.PermissionUploadFile.Id, model.ChannelUserRoleId)
|
||||
}()
|
||||
|
||||
updatePost := &model.Post{
|
||||
Id: postWithoutFiles.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Updated post with file",
|
||||
FileIds: model.StringArray{fileId},
|
||||
}
|
||||
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
assert.Nil(t, updatedPost)
|
||||
})
|
||||
|
||||
t.Run("should allow updating post with files when user has upload_file permission", func(t *testing.T) {
|
||||
postWithoutFiles, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Post without files",
|
||||
}, channel, model.CreatePostFlags{SetOnline: true})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
fileResp, resp, err := client.UploadFile(context.Background(), []byte("test file data"), channel.Id, "test-file.txt")
|
||||
require.NoError(t, err)
|
||||
CheckCreatedStatus(t, resp)
|
||||
fileId := fileResp.FileInfos[0].Id
|
||||
|
||||
updatePost := &model.Post{
|
||||
Id: postWithoutFiles.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "Updated post with file",
|
||||
FileIds: model.StringArray{fileId},
|
||||
}
|
||||
updatedPost, resp, err := client.UpdatePost(context.Background(), postWithoutFiles.Id, updatePost)
|
||||
require.NoError(t, err)
|
||||
CheckOKStatus(t, resp)
|
||||
require.NotNil(t, updatedPost)
|
||||
assert.Contains(t, updatedPost.FileIds, fileId)
|
||||
})
|
||||
|
||||
t.Run("logged out", func(t *testing.T) {
|
||||
_, err := client.Logout(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1587,7 +1696,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
fileInfo := fileResponse.FileInfos[0]
|
||||
|
||||
// create new post
|
||||
post, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
|
|
@ -1623,7 +1732,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
fileInfo := fileResponse.FileInfos[0]
|
||||
|
||||
// create new post
|
||||
post, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
|
|
@ -1661,7 +1770,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
fileInfo := fileResponse.FileInfos[0]
|
||||
|
||||
// create new post
|
||||
post, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
|
|
@ -1707,7 +1816,7 @@ func TestUpdatePost(t *testing.T) {
|
|||
fileInfo2 := fileResponse2.FileInfos[0]
|
||||
|
||||
// create new post
|
||||
post, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
post, _, appErr := th.App.CreatePost(th.Context, &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: channel.Id,
|
||||
Message: "zz" + model.NewId() + "a",
|
||||
|
|
@ -4320,13 +4429,13 @@ func TestSetChannelUnread(t *testing.T) {
|
|||
|
||||
t.Run("Unread on a direct channel in a thread", func(t *testing.T) {
|
||||
dc := th.CreateDmChannel(t, th.CreateUser(t))
|
||||
rootPost, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: u1.Id, CreateAt: now, ChannelId: dc.Id, Message: "root"}, dc, model.CreatePostFlags{})
|
||||
rootPost, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: u1.Id, CreateAt: now, ChannelId: dc.Id, Message: "root"}, dc, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 10, ChannelId: dc.Id, Message: "reply 1"}, dc, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 10, ChannelId: dc.Id, Message: "reply 1"}, dc, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
reply2, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 20, ChannelId: dc.Id, Message: "reply 2"}, dc, model.CreatePostFlags{})
|
||||
reply2, _, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 20, ChannelId: dc.Id, Message: "reply 2"}, dc, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 30, ChannelId: dc.Id, Message: "reply 3"}, dc, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost.Id, UserId: u1.Id, CreateAt: now + 30, ChannelId: dc.Id, Message: "reply 3"}, dc, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// Ensure that post have been read
|
||||
|
|
@ -4425,19 +4534,19 @@ func TestSetPostUnreadWithoutCollapsedThreads(t *testing.T) {
|
|||
// user1: a root post
|
||||
// user2: Another root mention @u1
|
||||
user1Mention := " @" + th.BasicUser.Username
|
||||
rootPost1, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "first root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
rootPost1, _, appErr := th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "first root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hello"}, th.BasicChannel, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "hello"}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
replyPost1, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
replyPost1, _, appErr := th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another reply"}, th.BasicChannel, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another reply"}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{RootId: rootPost1.Id, UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "a root post"}, th.BasicChannel, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "a root post"}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
_, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
_, _, appErr = th.App.CreatePost(th.Context, &model.Post{UserId: th.BasicUser2.Id, CreateAt: model.GetMillis(), ChannelId: th.BasicChannel.Id, Message: "another root mention" + user1Mention}, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
t.Run("Mark reply post as unread", func(t *testing.T) {
|
||||
|
|
@ -4541,7 +4650,7 @@ func TestGetEditHistoryForPost(t *testing.T) {
|
|||
UserId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
rpost, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
||||
rpost, _, err := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
||||
require.Nil(t, err)
|
||||
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
|
|
@ -4612,7 +4721,7 @@ func TestGetEditHistoryForPost(t *testing.T) {
|
|||
FileIds: []string{fileInfo1.Id, fileInfo2.Id},
|
||||
}
|
||||
|
||||
createdPost, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
||||
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{SetOnline: true})
|
||||
require.Nil(t, appErr)
|
||||
require.Contains(t, createdPost.FileIds, fileInfo1.Id)
|
||||
require.Contains(t, createdPost.FileIds, fileInfo2.Id)
|
||||
|
|
@ -4766,7 +4875,7 @@ func TestCreatePostNotificationsWithCRT(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// post a reply on the thread
|
||||
_, appErr := th.App.CreatePostAsUser(th.Context, tc.post, th.Context.Session().Id, false)
|
||||
_, _, appErr := th.App.CreatePostAsUser(th.Context, tc.post, th.Context.Session().Id, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
var caught bool
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import (
|
|||
|
||||
func userCreatePostPermissionCheckWithContext(c *Context, channelId string) {
|
||||
hasPermission := false
|
||||
if c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionCreatePost) {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelId, model.PermissionCreatePost); ok {
|
||||
hasPermission = true
|
||||
} else if channel, err := c.App.GetChannel(c.AppContext, channelId); err == nil {
|
||||
// Temporary permission check method until advanced permissions, please do not copy
|
||||
|
|
@ -41,3 +41,31 @@ func postPriorityCheckWithContext(where string, c *Context, priority *model.Post
|
|||
c.Err = appErr
|
||||
}
|
||||
}
|
||||
|
||||
// checkUploadFilePermissionForNewFiles checks upload_file permission only when
|
||||
// adding new files to a post, preventing permission bypass via cross-channel file attachments.
|
||||
func checkUploadFilePermissionForNewFiles(c *Context, newFileIds []string, originalPost *model.Post) {
|
||||
if len(newFileIds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
originalFileIDsMap := make(map[string]bool, len(originalPost.FileIds))
|
||||
for _, fileID := range originalPost.FileIds {
|
||||
originalFileIDsMap[fileID] = true
|
||||
}
|
||||
|
||||
hasNewFiles := false
|
||||
for _, fileID := range newFileIds {
|
||||
if !originalFileIDsMap[fileID] {
|
||||
hasNewFiles = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasNewFiles {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), originalPost.ChannelId, model.PermissionUploadFile); !ok {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ func updatePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func getReactions(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), c.Params.PostId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ func getBulkReactions(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
for _, postId := range postIds {
|
||||
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), postId, model.PermissionReadChannelContent) {
|
||||
if ok, _ := c.App.SessionHasPermissionToReadPost(c.AppContext, *c.AppContext.Session(), postId); !ok {
|
||||
c.SetPermissionError(model.PermissionReadChannelContent)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
286
server/channels/api4/recap.go
Normal file
286
server/channels/api4/recap.go
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
)
|
||||
|
||||
func (api *API) InitRecap() {
|
||||
api.BaseRoutes.Recaps.Handle("", api.APISessionRequired(createRecap)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Recaps.Handle("", api.APISessionRequired(getRecaps)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Recaps.Handle("/{recap_id:[A-Za-z0-9]+}", api.APISessionRequired(getRecap)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.Recaps.Handle("/{recap_id:[A-Za-z0-9]+}/read", api.APISessionRequired(markRecapAsRead)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Recaps.Handle("/{recap_id:[A-Za-z0-9]+}/regenerate", api.APISessionRequired(regenerateRecap)).Methods(http.MethodPost)
|
||||
api.BaseRoutes.Recaps.Handle("/{recap_id:[A-Za-z0-9]+}", api.APISessionRequired(deleteRecap)).Methods(http.MethodDelete)
|
||||
}
|
||||
|
||||
func requireRecapsEnabled(c *Context) {
|
||||
if !c.App.Config().FeatureFlags.EnableAIRecaps {
|
||||
c.Err = model.NewAppError("requireRecapsEnabled", "api.recap.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// addRecapChannelIDsToAuditRec extracts channel IDs from a recap and adds them to the audit record.
|
||||
// This logs which channels' content was accessed through the recap operation.
|
||||
func addRecapChannelIDsToAuditRec(auditRec *model.AuditRecord, recap *model.Recap) {
|
||||
if len(recap.Channels) == 0 {
|
||||
return
|
||||
}
|
||||
channelIDs := make([]string, 0, len(recap.Channels))
|
||||
for _, channel := range recap.Channels {
|
||||
channelIDs = append(channelIDs, channel.ChannelId)
|
||||
}
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_ids", channelIDs)
|
||||
}
|
||||
|
||||
func createRecap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireRecapsEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req model.CreateRecapRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
c.SetInvalidParamWithErr("body", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.ChannelIds) == 0 {
|
||||
c.SetInvalidParam("channel_ids")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title == "" {
|
||||
c.SetInvalidParam("title")
|
||||
return
|
||||
}
|
||||
|
||||
if req.AgentID == "" {
|
||||
c.SetInvalidParam("agent_id")
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventCreateRecap, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
auditRec.AddEventObjectType("recap")
|
||||
model.AddEventParameterToAuditRec(auditRec, "channel_ids", req.ChannelIds)
|
||||
model.AddEventParameterToAuditRec(auditRec, "title", req.Title)
|
||||
model.AddEventParameterToAuditRec(auditRec, "agent_id", req.AgentID)
|
||||
|
||||
recap, err := c.App.CreateRecap(c.AppContext, req.Title, req.ChannelIds, req.AgentID)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(recap)
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
if err := json.NewEncoder(w).Encode(recap); err != nil {
|
||||
c.Logger.Warn("Error encoding response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getRecap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireRecapsEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireRecapId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetRecap, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
auditRec.AddEventObjectType("recap")
|
||||
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)
|
||||
|
||||
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if recap.UserId != c.AppContext.Session().UserId {
|
||||
c.Err = model.NewAppError("getRecap", "api.recap.permission_denied", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Log channel IDs accessed through viewing this recap summary
|
||||
addRecapChannelIDsToAuditRec(auditRec, recap)
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(recap)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(recap); err != nil {
|
||||
c.Logger.Warn("Error encoding response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func getRecaps(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireRecapsEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventGetRecaps, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelAPI)
|
||||
model.AddEventParameterToAuditRec(auditRec, "page", c.Params.Page)
|
||||
model.AddEventParameterToAuditRec(auditRec, "per_page", c.Params.PerPage)
|
||||
|
||||
recaps, err := c.App.GetRecapsForUser(c.AppContext, c.Params.Page, c.Params.PerPage)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
if len(recaps) > 0 {
|
||||
auditRec.AddMeta("recap_count", len(recaps))
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(recaps); err != nil {
|
||||
c.Logger.Warn("Error encoding response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func markRecapAsRead(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireRecapsEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireRecapId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventMarkRecapAsRead, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
auditRec.AddEventObjectType("recap")
|
||||
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)
|
||||
|
||||
// Check permissions
|
||||
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if recap.UserId != c.AppContext.Session().UserId {
|
||||
c.Err = model.NewAppError("markRecapAsRead", "api.recap.permission_denied", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(recap)
|
||||
|
||||
updatedRecap, err := c.App.MarkRecapAsRead(c.AppContext, recap)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updatedRecap)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedRecap); err != nil {
|
||||
c.Logger.Warn("Error encoding response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func regenerateRecap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireRecapsEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireRecapId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventRegenerateRecap, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
auditRec.AddEventObjectType("recap")
|
||||
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)
|
||||
|
||||
// Check permissions
|
||||
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if recap.UserId != c.AppContext.Session().UserId {
|
||||
c.Err = model.NewAppError("regenerateRecap", "api.recap.permission_denied", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Log channel IDs that will be re-summarized
|
||||
addRecapChannelIDsToAuditRec(auditRec, recap)
|
||||
|
||||
auditRec.AddEventPriorState(recap)
|
||||
|
||||
updatedRecap, err := c.App.RegenerateRecap(c.AppContext, c.AppContext.Session().UserId, recap)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
auditRec.AddEventResultState(updatedRecap)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(updatedRecap); err != nil {
|
||||
c.Logger.Warn("Error encoding response", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteRecap(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireRecapsEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireRecapId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := c.MakeAuditRecord(model.AuditEventDeleteRecap, model.AuditStatusFail)
|
||||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
auditRec.AddEventObjectType("recap")
|
||||
model.AddEventParameterToAuditRec(auditRec, "recap_id", c.Params.RecapId)
|
||||
|
||||
// Check permissions
|
||||
recap, err := c.App.GetRecap(c.AppContext, c.Params.RecapId)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if recap.UserId != c.AppContext.Session().UserId {
|
||||
c.Err = model.NewAppError("deleteRecap", "api.recap.permission_denied", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.AddEventPriorState(recap)
|
||||
|
||||
if err := c.App.DeleteRecap(c.AppContext, c.Params.RecapId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ func TestGetPostsForReporting(t *testing.T) {
|
|||
CreateAt: baseTime + (int64(i) * 1000), // 1 second apart
|
||||
UpdateAt: baseTime + (int64(i) * 1000),
|
||||
}
|
||||
createdPost, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
||||
createdPost, _, appErr := th.App.CreatePost(th.Context, post, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
testPosts = append(testPosts, createdPost)
|
||||
}
|
||||
|
|
@ -596,7 +596,7 @@ func TestGetPostsForReporting(t *testing.T) {
|
|||
CreateAt: baseTime + (int64(20+i) * 1000), // After all test posts
|
||||
UpdateAt: baseTime + (int64(20+i) * 1000),
|
||||
}
|
||||
_, appErr := th.App.CreatePost(th.Context, systemPost, th.BasicChannel, model.CreatePostFlags{})
|
||||
_, _, appErr := th.App.CreatePost(th.Context, systemPost, th.BasicChannel, model.CreatePostFlags{})
|
||||
require.Nil(t, appErr)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ package api4
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
|
@ -299,9 +298,8 @@ func TestPatchRole(t *testing.T) {
|
|||
assert.Equal(t, received.Name, role.Name)
|
||||
assert.Equal(t, received.DisplayName, role.DisplayName)
|
||||
assert.Equal(t, received.Description, role.Description)
|
||||
perms := []string{"create_direct_channel", "create_public_channel", "manage_incoming_webhooks", "manage_outgoing_webhooks"}
|
||||
sort.Strings(perms)
|
||||
assert.EqualValues(t, received.Permissions, perms)
|
||||
expectedPermissions := []string{"create_direct_channel", "create_public_channel", "manage_incoming_webhooks", "manage_outgoing_webhooks"}
|
||||
assert.ElementsMatch(t, expectedPermissions, received.Permissions)
|
||||
assert.Equal(t, received.SchemeManaged, role.SchemeManaged)
|
||||
|
||||
// Check a no-op patch succeeds.
|
||||
|
|
@ -333,9 +331,8 @@ func TestPatchRole(t *testing.T) {
|
|||
assert.Equal(t, received.Name, role.Name)
|
||||
assert.Equal(t, received.DisplayName, role.DisplayName)
|
||||
assert.Equal(t, received.Description, role.Description)
|
||||
perms := []string{"create_direct_channel", "manage_incoming_webhooks", "manage_outgoing_webhooks"}
|
||||
sort.Strings(perms)
|
||||
assert.EqualValues(t, received.Permissions, perms)
|
||||
expectedPermissions := []string{"create_direct_channel", "manage_incoming_webhooks", "manage_outgoing_webhooks"}
|
||||
assert.ElementsMatch(t, expectedPermissions, received.Permissions)
|
||||
assert.Equal(t, received.SchemeManaged, role.SchemeManaged)
|
||||
|
||||
t.Run("Check guest permissions editing without E20 license", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,13 @@ func createSchedulePost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "scheduledPost", &scheduledPost)
|
||||
|
||||
if len(scheduledPost.FileIds) > 0 {
|
||||
if ok, _ := c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), scheduledPost.ChannelId, model.PermissionUploadFile); !ok {
|
||||
c.SetPermissionError(model.PermissionUploadFile)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
scheduledPostChecks("Api4.createSchedulePost", c, &scheduledPost)
|
||||
if c.Err != nil {
|
||||
return
|
||||
|
|
@ -169,12 +176,38 @@ func updateScheduledPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
defer c.LogAuditRecWithLevel(auditRec, app.LevelContent)
|
||||
model.AddEventParameterAuditableToAuditRec(auditRec, "scheduledPost", &scheduledPost)
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
existingScheduledPost, err := c.App.Srv().Store().ScheduledPost().Get(scheduledPost.Id)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateScheduledPost", "app.update_scheduled_post.get_scheduled_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
if existingScheduledPost == nil {
|
||||
c.Err = model.NewAppError("updateScheduledPost", "app.update_scheduled_post.existing_scheduled_post.not_exist", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if existingScheduledPost.UserId != userId {
|
||||
c.Err = model.NewAppError("updateScheduledPost", "app.update_scheduled_post.update_permission.error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if len(scheduledPost.FileIds) > 0 {
|
||||
originalPost, err := existingScheduledPost.ToPost()
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("updateScheduledPost", "app.update_scheduled_post.convert_to_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
checkUploadFilePermissionForNewFiles(c, scheduledPost.FileIds, originalPost)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
scheduledPostChecks("Api4.updateScheduledPost", c, &scheduledPost)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
updatedScheduledPost, appErr := c.App.UpdateScheduledPost(c.AppContext, userId, &scheduledPost, connectionID)
|
||||
if appErr != nil {
|
||||
c.Err = appErr
|
||||
|
|
@ -209,6 +242,21 @@ func deleteScheduledPost(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||
model.AddEventParameterToAuditRec(auditRec, "scheduledPostId", scheduledPostId)
|
||||
|
||||
userId := c.AppContext.Session().UserId
|
||||
|
||||
existingScheduledPost, err := c.App.Srv().Store().ScheduledPost().Get(scheduledPostId)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("deleteScheduledPost", "app.delete_scheduled_post.get_scheduled_post.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
return
|
||||
}
|
||||
if existingScheduledPost == nil {
|
||||
c.Err = model.NewAppError("deleteScheduledPost", "app.delete_scheduled_post.existing_scheduled_post.not_exist", nil, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if existingScheduledPost.UserId != userId {
|
||||
c.Err = model.NewAppError("deleteScheduledPost", "app.delete_scheduled_post.delete_permission.error", nil, "", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
connectionID := r.Header.Get(model.ConnectionId)
|
||||
deletedScheduledPost, appErr := c.App.DeleteScheduledPost(c.AppContext, userId, scheduledPostId, connectionID)
|
||||
if appErr != nil {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue