You've already forked focalboard
							
							
				mirror of
				https://github.com/mattermost/focalboard.git
				synced 2025-10-31 00:17:42 +02:00 
			
		
		
		
	Permissions feature branch (#2578)
* wip
* Added data migration for populating categories
* wip
* Added data migration for populating categories
* Store WIP
* migration WIP
* category CRUD APIs complete
* category block API WIP
* block category update API done
* Fetcehed data into store
* Started displayting sidebar data
* sidebar WIP
* Dashboard - basic changes
* Sidebar dashboard btn and board switcher UI only
* Sidebar dashboard btn and board switcher UI only
* create category dialog WIP
* Create category webapp side done
* Integrated move card to other category
* board to block
* Disabled dashboard route for now as we'll implement it in phase 2
* WIP
* Added logic to open last board/view on per team level
* Add workspace to teams and boards migrations (#1986)
* Add workspace to teams and boards migrations
* Update json annotations on board models
* boards search dialog WIP
* Seach dialog WIP
* Implemented opening boiard from search results
* Boards switcher styliung
* Handled update category WS event
* Template support
* personal server support and styling fixes
* test fix WIP
* Fixed a bug causing boards to not be moved correctly beteen categories
* Fixed webapp tests
* fix
* Store changes (#2011)
* Permissions phase 1 - Websocket updates (#2014)
* Store changes
* Websockets changes
* Permissions phase 1 - Permissions service (#2015)
* Store changes
* Websockets changes
* Permissions service
* Api and app updates (#2016)
* Store changes
* Websockets changes
* Permissions service
* New API and App changes
* Delete and Patch boards and blocks endpoints
* Used correct variable
* Webapp changes WIP
* Open correct team URL
* Fixed get block API
* Used React context for workspace users
* WIP
* On load navigation sorted out
* WIP
* Nav fix
* categories WS broadcast
* Used real search API
* Fixed unfurl ppreview
* set active team in sidebar
* IMplemented navigation on changing team in sidebar
* Misc fixes
* close rows inside transaction (#2045)
* update syntax for mysql (#2044)
* Upadted mutator for new patchBlock API
* Updated patchBlock API to use new URL
* Listeining to correct event in plugin mode
* Implemented WS messages for category operations:
* Fix duplicated build tags on Makefile
* Sidebar enhancements
* Add missing prefix to SQLite migration and fix flaky tests
* Sidebar boards menu enhancement
* Fix board page interactions (#2144)
* Fix patch board card properties error
* Fix board interactions
* Fix insert blocks interactions
* Fix app tests (#2104)
* Add json1 tag to vscode launch (#2157)
* Fix add, delete and update boards and add board patch generation (#2146)
* Fix update boards and add board patch generation
* Make add board and add template work, as well as deleting a board
* Update the state on board deletion
* Delete unused variable
* Fix bad parenthesis
* Fix board creation inside plugin, options were coming null due websocket message serialization
* update property type mutators to use boards API (#2168)
* Add permissions modal (#2196)
* Initial integration
* Permissions modal, websocket updates and API tests implemented
* Avoid updating/removing user if there is only one admin left
* Fix duplicates on board search
* Adds integration test
* Addressing PR review comments
Co-authored-by: Jesús Espino <jespinog@gmail.com>
* Merge
* I'm able to compile now
* Some fixes around tests execution
* Fixing migrations
* Fixing migrations order
* WIP
* Fixing some other compilation problems on tests
* Some typescript tests fixed
* Fixing javascript tests
* Fixing compilation
* Fixing some problems to create boards
* Load the templates on initial load
* Improvements over initial team templates import
* Adding new fields in the database
* Working on adding duplicate board api
* Removing RootID concept entirely
* Improving a bit the subscriptions
* Fixing store tests for notificationHints
* Fixing more tests
* fixing tests
* Fixing tests
* Fixing tests
* Fixing some small bugs related to templates
* Fixing registration link generation/regeneration
* Fixing cypress tests
* Adding store tests for duplicateBoard and duplicateBlock
* Addressing some TODO comments
* Making the export api simpler
* Add redirect component for old workspace urls
* Removing Dashboard code
* Delete only the built-in templates on update
* fixing tests
* Adding users autocompletion
* Updating snapshots
* Fixing bad merge
* fix panic when creating new card in notifysubscriptions (#2352)
* fix lint errors (#2353)
* fix lint errors
* fix panic when creating new card in notifysubscriptions (#2352)
* fix lint errors
* fix unit test
* Revert "fix unit test"
This reverts commit 0ad78aed65.
Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
* fix sql syntax error for SearchUsersByTeam (#2357)
* Fix mentions delivery (#2358)
* fix sql syntax error for SearchUsersByTeam
* fix mentions delivery
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
* update api for octoClient calls, pass correct variables to mutator (#2359)
* Fixing tests after merge
* Fix sidebar context menu UI issue (#2399)
* Fix notification diff for text blocks (#2386)
* fix notification diff for text blocks; fix various linter errors.
* fix URLs to cards
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
* Permissions branch: Fix card links (#2391)
* fix notification diff for text blocks; fix various linter errors.
* fix URLs to cards
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
* Fixing sqlite tests
* Fixing server tests
* Update migrations to create global templates. (#2397)
* fix duplicate templates
* revert migrate.go
* update UI for empty templates
* implement updating built-in templates as global (teamId = 0)
* handle error if board not found
* update unit test
* fix more tests
* Update blocks_test.go
Fix merge issue
* fix migration sql error (#2414)
* Fixing frontend tests
* Set target team ID when using a global template (#2419)
* Fix some server tests
* Fixing onboarding creation
* Permissions branch: Fix unit tests and CI errors (part 1) (#2425)
* Fixing some small memory leaks (#2400)
* Fixing some small memory leaks
* fixing tests
* passing the tags to all test targets
* Increasing the timeout of the tests
* Fix some type checkings
* Permissions branch: Fixes all the linter errors (#2429)
* fix linter errors
* Reestructuring the router and splitting in more subcomponents (#2403)
* Reestructuring the router and splitting in more subcomponents
* Removing console.log calls
* Removing unneeded selector
* Addressing PR comment
* Fix redirection to one team when you load directly the boards home path
* Using properly the lastTeamID to redirect the user if needed
* don't allow last admin change/deleted (#2416)
* don't allow last admin change/deleted
* update for i18-extract
* fixed en.json
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
* Splitting BoardPage component into simpler/smaller components (#2435)
* Splitting BoardPage component into simpler/smaller components
* Removing unneeded import
* Replace go migrate with morph permissions (#2424)
* merge origin/replace-go-migrate-with-morph
* run go mod tidy on mattermost-plugin and increase test timeout
* fix merge issue temprorarily
* remove some debug changes
* fixing the linter
* Allow always team 0 (global) templates fetch (#2472)
* Fix problem with viewId 0 in the URL (#2473)
* Migrate from binddata to goembed (#2471)
* Adding join logic to the board switcher (#2434)
* Adding join logic to the board switcher
* Using already existing client function and removing the joinBoard one
* Adding support for autojoin based on url
* Fixing frontend tests
* fix webapp compile error, missing enableSharedBoards (#2501)
* Fixing duplication on postgres
* Adding back views to the sidebar (#2494)
* Fix #2507. Update Swagger comments (#2508)
* Fix the flash of the template selector on board/team switch (#2490)
* Fix the flash of the template selector on board/team switch
* More fixes specially around error handling
* Fixing the bot badge (#2487)
* simplifying a bit the team store sync between channels and focalboard (#2481)
* Fix menu tests (#2528)
* fix failing menu tests
* fix lint error
* Added keyboard shortcut for boards switcher (#2407)
* Added keyboard shortcut for boards switcher
* Fixed a type error
* Added some inline comments
* Fixed lint
* Fixed bug with scroll jumping when the card is opened: (#2477)
- avoid remounting of `ScrollingComponent` for each render of `Kanban` component
  - property `autoFocus` set to false for `CalculationOptions` because it triggers `blur` even for the button in Jest tests and closes the menu
  - snapshots for tests with `CalculationOptions` updated
* Adding the frontend support for permissions and applying it to a big part of the interface. (#2536)
* Initial work on permissions gates
* Applying permissions gates in more places
* Adding more checks to the interface
* Adding more permissions gates and keeping the store up to date
* fixing some tests
* Fixing some more tests
* Fixing another test
* Fixing all tests and adding some more
* Adding no-permission snapshot tests
* Addressing PR review comments
* Fixing invert behavior
* Permissions branch:  No sqlstore calls after app shutdown (#2530)
* fix webapp compile error, missing enableSharedBoards
* refactor app init wip
* - ensure all block change notifications are finished before shutting down app
- fix unit tests for mysql (insert_at only has 1 second resolution!)
* adjust logging
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
* Fixed migrations to allow upgrading from previous version (#2535)
* Added mechanism to check if schema migration is needed
* WIP
* WIP
* WIP
* WIP
* Fixed migration
* Fixed for SQLite
* minor cleaniup
* Deleted old schema migration table after running migrations
* Removed a debug log
* Fixed a bug where the code always tried to delete a table which may or may not exist
* Show properly the user avatar in the ShareBoard component (#2542)
* Fixing the last CI problems from the permissions-branch (#2541)
* Fix history ordering
* Giving some times to avoid possible race conditions
* Empty
* Reverting accidental change in the config.json
* Optimizing table view (#2540)
* Optimizing table view
* Reducing the amount of rendering for tables
* Some other performance improvements
* Improve the activeView updates
* Some extra simplifications
* Another small improvement
* Fixing tests
* Fixing linter errors
* Reducing a bit the amount of dependency with big objects in the store
* Small simplification
* Removing Commenter role from the user role selector (#2561)
* Shareboard cleanup (#2550)
* Initial work on permissions gates
* Applying permissions gates in more places
* Adding more checks to the interface
* Adding more permissions gates and keeping the store up to date
* fixing some tests
* Fixing some more tests
* Fixing another test
* Fixing all tests and adding some more
* Adding no-permission snapshot tests
* Addressing PR review comments
* cleanup some shareboard settings
* remove unused property, fix for user items being displayed for non admin
* revert change, allow users to show
Co-authored-by: Jesús Espino <jespinog@gmail.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
* Fixing comments and cards with the new optimizations in the store (#2560)
* Fixing property creation (#2563)
* Fix user selection in table view (#2565)
* Fixing focus new row in table view (#2567)
* Permissions branch: Fix sqlite table lock (CI) (#2568)
* fix sqlite table lock
* remove test db on teardown
* revert .gitignore
* fix goimport on migration code
* fix typo
* more linter fixes
* clean up tmp db for sqlstore tests
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
* Fixing snapshots
* Migrating center panel to functional component (#2562)
* Migrating center panel to functional component
* Fixing some tests
* Fixing another test
* Fixing linter errors
* Fixing types errors
* Fixing linter error
* Fixing cypress tests
* Fixing the last cypress test
* Simpliying a bit the code
* Making property insertion more robust
* Updating checkbox test
Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com>
Co-authored-by: Miguel de la Cruz <miguel@mcrx.me>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Chen-I Lim <46905241+chenilim@users.noreply.github.com>
Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com>
Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com>
Co-authored-by: kamre <eremchenko@gmail.com>
			
			
This commit is contained in:
		
							
								
								
									
										1
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -9,6 +9,7 @@ | ||||
|             "type": "go", | ||||
|             "request": "launch", | ||||
|             "mode": "debug", | ||||
|             "buildFlags": "-tags 'json1'", | ||||
|             "program": "${workspaceFolder}/server/main", | ||||
|             "cwd": "${workspaceFolder}" | ||||
|         }, | ||||
|   | ||||
							
								
								
									
										29
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								Makefile
									
									
									
									
									
								
							| @@ -12,6 +12,8 @@ ifeq ($(BUILD_NUMBER),) | ||||
| 	BUILD_DATE := n/a | ||||
| endif | ||||
|  | ||||
| BUILD_TAGS += json1 | ||||
|  | ||||
| LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildNumber=$(BUILD_NUMBER)" | ||||
| LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildDate=$(BUILD_DATE)" | ||||
| LDFLAGS += -X "github.com/mattermost/focalboard/server/model.BuildHash=$(BUILD_HASH)" | ||||
| @@ -35,25 +37,25 @@ ci: server-test | ||||
|  | ||||
| server: ## Build server for local environment. | ||||
| 	$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=dev") | ||||
| 	cd server; go build -ldflags '$(LDFLAGS)' -o ../bin/focalboard-server ./main | ||||
| 	cd server; go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/focalboard-server ./main | ||||
|  | ||||
| server-mac: ## Build server for Mac. | ||||
| 	mkdir -p bin/mac | ||||
| 	$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=mac") | ||||
| 	cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -o ../bin/mac/focalboard-server ./main | ||||
| 	cd server; env GOOS=darwin GOARCH=$(MAC_GO_ARCH) go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/mac/focalboard-server ./main | ||||
|  | ||||
| server-linux: ## Build server for Linux. | ||||
| 	mkdir -p bin/linux | ||||
| 	$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=linux") | ||||
| 	cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/linux/focalboard-server ./main | ||||
| 	cd server; env GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/linux/focalboard-server ./main | ||||
|  | ||||
| server-win: ## Build server for Windows. | ||||
| 	$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win") | ||||
| 	cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o ../bin/win/focalboard-server.exe ./main | ||||
| 	cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -o ../bin/win/focalboard-server.exe ./main | ||||
|  | ||||
| server-dll: ## Build server as Windows DLL. | ||||
| 	$(eval LDFLAGS += -X "github.com/mattermost/focalboard/server/model.Edition=win") | ||||
| 	cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main | ||||
| 	cd server; env GOOS=windows GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -tags '$(BUILD_TAGS)' -buildmode=c-shared -o ../bin/win-dll/focalboard-server.dll ./main | ||||
|  | ||||
| server-linux-package: server-linux webapp | ||||
| 	rm -rf package | ||||
| @@ -83,7 +85,6 @@ server-linux-package-docker: | ||||
|  | ||||
| generate: ## Install and run code generators. | ||||
| 	cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen | ||||
| 	cd server; go get -modfile=go.tools.mod github.com/jteeuwen/go-bindata | ||||
| 	cd server; go generate ./... | ||||
|  | ||||
| server-lint: ## Run linters on server code. | ||||
| @@ -101,18 +102,18 @@ modd-precheck: | ||||
| 	fi; \ | ||||
|  | ||||
| watch: modd-precheck ## Run both server and webapp watching for changes | ||||
| 	modd | ||||
| 	env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd | ||||
|  | ||||
| watch-single-user: modd-precheck ## Run both server and webapp in single user mode watching for changes | ||||
| 	env FOCALBOARDSERVER_ARGS=--single-user modd | ||||
| 	env FOCALBOARDSERVER_ARGS=--single-user FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd | ||||
|  | ||||
| watch-server-test: modd-precheck ## Run server tests watching for changes | ||||
| 	modd -f modd-servertest.conf | ||||
| 	env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-servertest.conf | ||||
|  | ||||
| server-test: server-test-sqlite server-test-mysql server-test-postgres ## Run server tests | ||||
|  | ||||
| server-test-sqlite: ## Run server tests using sqlite | ||||
| 	cd server; go test -race -v -count=1 ./... | ||||
| 	cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./... | ||||
|  | ||||
| server-test-mysql: export FB_UNIT_TESTING=1 | ||||
| server-test-mysql: export FB_STORE_TEST_DB_TYPE=mysql | ||||
| @@ -120,8 +121,9 @@ server-test-mysql: export FB_STORE_TEST_DOCKER_PORT=44445 | ||||
|  | ||||
| server-test-mysql: ## Run server tests using mysql | ||||
| 	@echo Starting docker container for mysql | ||||
| 	docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans | ||||
| 	docker-compose -f ./docker-testing/docker-compose-mysql.yml run start_dependencies | ||||
| 	cd server; go test -race -v -count=1 ./... | ||||
| 	cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./... | ||||
| 	docker-compose -f ./docker-testing/docker-compose-mysql.yml down -v --remove-orphans | ||||
|  | ||||
| server-test-postgres: export FB_UNIT_TESTING=1 | ||||
| @@ -130,8 +132,9 @@ server-test-postgres: export FB_STORE_TEST_DOCKER_PORT=44446 | ||||
|  | ||||
| server-test-postgres: ## Run server tests using postgres | ||||
| 	@echo Starting docker container for postgres | ||||
| 	docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans | ||||
| 	docker-compose -f ./docker-testing/docker-compose-postgres.yml run start_dependencies | ||||
| 	cd server; go test -race -v -count=1 ./... | ||||
| 	cd server; go test -tags '$(BUILD_TAGS)' -race -v -count=1 -timeout=30m ./... | ||||
| 	docker-compose -f ./docker-testing/docker-compose-postgres.yml down -v --remove-orphans | ||||
|  | ||||
| webapp: ## Build webapp. | ||||
| @@ -141,7 +144,7 @@ webapp-test: ## jest tests for webapp | ||||
| 	cd webapp; npm run test | ||||
|  | ||||
| watch-plugin: modd-precheck ## Run and upload the plugin to a development server | ||||
| 	modd -f modd-watchplugin.conf | ||||
| 	env FOCALBOARD_BUILD_TAGS='$(BUILD_TAGS)' modd -f modd-watchplugin.conf | ||||
|  | ||||
| live-watch-plugin: modd-precheck ## Run and update locally the plugin in the development server | ||||
| 	cd mattermost-plugin; make live-watch | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| { | ||||
| 	"serverRoot": "http://localhost:8000", | ||||
| 	"port": 8000, | ||||
| 	"dbtype": "sqlite3", | ||||
| 	"dbconfig": "./focalboard.db", | ||||
|     "port": 8000, | ||||
|     "dbtype": "sqlite3", | ||||
|     "dbconfig": "./focalboard.db", | ||||
| 	"dbtableprefix": "", | ||||
| 	"postgres_dbconfig": "dbname=focalboard sslmode=disable", | ||||
| 	"test_dbconfig": "file::memory:?cache=shared", | ||||
| @@ -21,5 +21,5 @@ | ||||
| 	"authMode": "native", | ||||
| 	"logging_cfg_file": "", | ||||
| 	"audit_cfg_file": "", | ||||
| 	"enablepublicsharedboards": false | ||||
| 	"enablePublicSharedBoards": false | ||||
| } | ||||
|   | ||||
| @@ -38,7 +38,7 @@ export async function logIn(host: string, username: string, password: string) { | ||||
|  | ||||
| export async function getBoards(host: string, token: string) { | ||||
|   const json = await request('GET', host, 'workspaces/0/blocks?type=board', null, token) as Board[] | ||||
|   return json.filter(board => !board.fields.isTemplate) | ||||
|   return json.filter(board => !board.isTemplate) | ||||
| } | ||||
|  | ||||
| export async function findUrlPropertyId(host: string, token: string, boardId: string) { | ||||
|   | ||||
| @@ -129,7 +129,7 @@ function convert(input: Asana): Block[] { | ||||
|         type: 'select', | ||||
|         options | ||||
|     } | ||||
|     board.fields.cardProperties = [cardProperty] | ||||
|     board.cardProperties = [cardProperty] | ||||
|     blocks.push(board) | ||||
|  | ||||
|     // Board view | ||||
|   | ||||
| @@ -90,25 +90,25 @@ function convert(items: any[]) { | ||||
|     board.title = 'Jira import' | ||||
|  | ||||
|     // Compile standard properties | ||||
|     board.fields.cardProperties = [] | ||||
|     board.cardProperties = [] | ||||
|  | ||||
|     const priorityProperty = buildCardPropertyFromValues('Priority', items.map(o => o.priority?._)) | ||||
|     board.fields.cardProperties.push(priorityProperty) | ||||
|     board.cardProperties.push(priorityProperty) | ||||
|  | ||||
|     const statusProperty = buildCardPropertyFromValues('Status', items.map(o => o.status?._)) | ||||
|     board.fields.cardProperties.push(statusProperty) | ||||
|     board.cardProperties.push(statusProperty) | ||||
|  | ||||
|     const resolutionProperty = buildCardPropertyFromValues('Resolution', items.map(o => o.resolution?._)) | ||||
|     board.fields.cardProperties.push(resolutionProperty) | ||||
|     board.cardProperties.push(resolutionProperty) | ||||
|  | ||||
|     const typeProperty = buildCardPropertyFromValues('Type', items.map(o => o.type?._)) | ||||
|     board.fields.cardProperties.push(typeProperty) | ||||
|     board.cardProperties.push(typeProperty) | ||||
|  | ||||
|     const assigneeProperty = buildCardPropertyFromValues('Assignee', items.map(o => o.assignee?._)) | ||||
|     board.fields.cardProperties.push(assigneeProperty) | ||||
|     board.cardProperties.push(assigneeProperty) | ||||
|  | ||||
|     const reporterProperty = buildCardPropertyFromValues('Reporter', items.map(o => o.reporter?._)) | ||||
|     board.fields.cardProperties.push(reporterProperty) | ||||
|     board.cardProperties.push(reporterProperty) | ||||
|  | ||||
|     const originalUrlProperty: IPropertyTemplate = { | ||||
|         id: Utils.createGuid(), | ||||
| @@ -116,7 +116,7 @@ function convert(items: any[]) { | ||||
|         type: 'url', | ||||
|         options: [] | ||||
|     } | ||||
|     board.fields.cardProperties.push(originalUrlProperty) | ||||
|     board.cardProperties.push(originalUrlProperty) | ||||
|  | ||||
|     const createdDateProperty: IPropertyTemplate = { | ||||
|         id: Utils.createGuid(), | ||||
| @@ -124,7 +124,7 @@ function convert(items: any[]) { | ||||
|         type: 'date', | ||||
|         options: [] | ||||
|     } | ||||
|     board.fields.cardProperties.push(createdDateProperty) | ||||
|     board.cardProperties.push(createdDateProperty) | ||||
|  | ||||
|     blocks.push(board) | ||||
|  | ||||
| @@ -240,4 +240,4 @@ function showHelp() { | ||||
|     exit(1) | ||||
| } | ||||
|  | ||||
| export { run } | ||||
| export { run } | ||||
|   | ||||
| @@ -135,7 +135,7 @@ function convert(input: any[], title: string): Block[] { | ||||
|             type: 'select', | ||||
|             options: [] | ||||
|         } | ||||
|         board.fields.cardProperties.push(cardProperty) | ||||
|         board.cardProperties.push(cardProperty) | ||||
|     }) | ||||
|  | ||||
|     // Set all column types to select | ||||
| @@ -177,7 +177,7 @@ function convert(input: any[], title: string): Block[] { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             const cardProperty = board.fields.cardProperties.find((o) => o.name === key)! | ||||
|             const cardProperty = board.cardProperties.find((o) => o.name === key)! | ||||
|             let option = cardProperty.options.find((o) => o.value === value) | ||||
|             if (!option) { | ||||
|                 const color = optionColors[optionColorIndex % optionColors.length] | ||||
|   | ||||
| @@ -82,7 +82,7 @@ function convert(input: Todoist, project: Project): Block[] { | ||||
|     console.log(`Board: ${project.name}`) | ||||
|     board.rootId = board.id | ||||
|     board.title = project.name | ||||
|     board.fields.description = project.name | ||||
|     board.description = project.name | ||||
|  | ||||
|     // Convert lists (columns) to a Select property | ||||
|     const optionIdMap = new Map<string, string>() | ||||
| @@ -114,7 +114,7 @@ function convert(input: Todoist, project: Project): Block[] { | ||||
|         type: 'select', | ||||
|         options | ||||
|     } | ||||
|     board.fields.cardProperties = [cardProperty] | ||||
|     board.cardProperties = [cardProperty] | ||||
|     blocks.push(board) | ||||
|  | ||||
|     // Board view | ||||
|   | ||||
| @@ -68,7 +68,7 @@ function convert(input: Trello): Block[] { | ||||
|     console.log(`Board: ${input.name}`) | ||||
|     board.rootId = board.id | ||||
|     board.title = input.name | ||||
|     board.fields.description = input.desc | ||||
|     board.description = input.desc | ||||
|  | ||||
|     // Convert lists (columns) to a Select property | ||||
|     const optionIdMap = new Map<string, string>() | ||||
| @@ -92,7 +92,7 @@ function convert(input: Trello): Block[] { | ||||
|         type: 'select', | ||||
|         options | ||||
|     } | ||||
|     board.fields.cardProperties = [cardProperty] | ||||
|     board.cardProperties = [cardProperty] | ||||
|     blocks.push(board) | ||||
|  | ||||
|     // Board view | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import ( | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/mattermost/focalboard/server/server" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions/localpermissions" | ||||
| 	"github.com/webview/webview" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| @@ -66,6 +67,8 @@ func runServer(port int) (*server.Server, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	permissionsService := localpermissions.New(db, logger) | ||||
|  | ||||
| 	params := server.Params{ | ||||
| 		Cfg:             config, | ||||
| 		SingleUserToken: sessionToken, | ||||
| @@ -74,6 +77,7 @@ func runServer(port int) (*server.Server, error) { | ||||
| 		ServerID:        "", | ||||
| 		WSAdapter:       nil, | ||||
| 		NotifyBackends:  nil, | ||||
| 		PermissionsService: permissionsService, | ||||
| 	} | ||||
|  | ||||
| 	server, err := server.New(params) | ||||
|   | ||||
| @@ -61,7 +61,6 @@ github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOv | ||||
| github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= | ||||
| github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= | ||||
| github.com/Azure/azure-storage-blob-go v0.13.0/go.mod h1:pA9kNqtjUeQF2zOSu4s//nUdBD+e64lEuc4sVnuOfNs= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= | ||||
| github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= | ||||
| github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= | ||||
| @@ -91,7 +90,6 @@ github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/ | ||||
| github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= | ||||
| github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= | ||||
| github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= | ||||
| github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= | ||||
| github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= | ||||
| github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||
| github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk= | ||||
| @@ -235,7 +233,6 @@ github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkb | ||||
| github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= | ||||
| github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||
| github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||
| github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY= | ||||
| github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||
| github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||
| github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | ||||
| @@ -260,6 +257,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1 | ||||
| github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= | ||||
| github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= | ||||
| github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= | ||||
| github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| @@ -272,20 +270,15 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | ||||
| github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ= | ||||
| github.com/dhui/dktest v0.3.4 h1:VbUEcaSP+U2/yUr9d2JhSThXYEnDlGabRSHe2rIE46E= | ||||
| github.com/dhui/dktest v0.3.4/go.mod h1:4m4n6lmXlmVfESth7mzdcv8nBI5mOb5UROPqjM02csU= | ||||
| github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU= | ||||
| github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= | ||||
| github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= | ||||
| github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= | ||||
| github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= | ||||
| github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= | ||||
| github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= | ||||
| github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible h1:nhVo1udYfMj0Jsw0lnqrTjjf33aLpdgW9Wve9fHVzhQ= | ||||
| github.com/docker/docker v17.12.0-ce-rc1.0.20210128214336-420b1d36250f+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= | ||||
| github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= | ||||
| github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= | ||||
| github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= | ||||
| github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= | ||||
| github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= | ||||
| github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= | ||||
| @@ -315,6 +308,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL | ||||
| github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= | ||||
| github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= | ||||
| github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | ||||
| github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= | ||||
| github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= | ||||
| github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= | ||||
| github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= | ||||
| @@ -410,10 +404,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a | ||||
| github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | ||||
| github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= | ||||
| github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= | ||||
| github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= | ||||
| github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= | ||||
| github.com/golang-migrate/migrate/v4 v4.15.0 h1:LKvQ+CgezLw0zuR/ib1y9sQStG0vepWaEVUsQof0bo0= | ||||
| github.com/golang-migrate/migrate/v4 v4.15.0/go.mod h1:g9qbiDvB47WyrRnNu2t2gMZFNHKnatsYRxsGZbCi4EM= | ||||
| github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= | ||||
| github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= | ||||
| @@ -542,7 +534,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p | ||||
| github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= | ||||
| github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= | ||||
| @@ -557,7 +548,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP | ||||
| github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= | ||||
| github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= | ||||
| github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= | ||||
| github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= | ||||
| @@ -684,6 +674,7 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb | ||||
| github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= | ||||
| github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= | ||||
| github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= | ||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= | ||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | ||||
| github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= | ||||
| github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= | ||||
| @@ -778,6 +769,8 @@ github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go | ||||
| github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08= | ||||
| github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3 h1:+E2WOqMrCGZTGjhmWVsszj2Qqx7Amh/OBUedkLLtnP4= | ||||
| github.com/mattermost/mattermost-server/v6 v6.0.0-20211022142730-a6cca93ba3c3/go.mod h1:VH26NcOr3xgkSBAvh4q+9+RoBD/M9gYO2F1PISq9KMw= | ||||
| github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14= | ||||
| github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw= | ||||
| github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= | ||||
| github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= | ||||
| github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= | ||||
| @@ -807,6 +800,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh | ||||
| github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= | ||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= | ||||
| @@ -852,7 +846,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb | ||||
| github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= | ||||
| github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||
| github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= | ||||
| github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= | ||||
| github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= | ||||
| github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= | ||||
| github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= | ||||
| @@ -905,9 +898,7 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y | ||||
| github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= | ||||
| github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= | ||||
| github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= | ||||
| github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= | ||||
| github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= | ||||
| github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= | ||||
| github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= | ||||
| github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= | ||||
| github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= | ||||
| @@ -990,6 +981,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn | ||||
| github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= | ||||
| github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/richardlehane/mscfb v1.0.3/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= | ||||
| github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= | ||||
| @@ -1233,7 +1225,6 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= | ||||
| go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= | ||||
| go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/atomic v1.8.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= | ||||
| go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||||
| go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= | ||||
| go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= | ||||
| @@ -1321,6 +1312,7 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hM | ||||
| golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= | ||||
| golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= | ||||
| golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| @@ -1512,9 +1504,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc | ||||
| golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed h1:E159xujlywdAeN3FqsTBPzRKGUq/pDHolXbuttkC36E= | ||||
| golang.org/x/sys v0.0.0-20211006225509-1a26e0398eed/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= | ||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| @@ -1612,6 +1606,7 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= | ||||
| golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= | ||||
| golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| @@ -1830,31 +1825,147 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh | ||||
| honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= | ||||
| honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= | ||||
| honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= | ||||
| lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= | ||||
| lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= | ||||
| modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= | ||||
| modernc.org/cc/v3 v3.32.4/go.mod h1:0R6jl1aZlIl2avnYfbfHBS1QB6/f+16mihBObaBC878= | ||||
| modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= | ||||
| modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/ccgo/v3 v3.9.2/go.mod h1:gnJpy6NIVqkETT+L5zPsQFj7L2kkhfPMzOghRNv/CFo= | ||||
| modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= | ||||
| modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= | ||||
| modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= | ||||
| modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= | ||||
| modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= | ||||
| modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= | ||||
| modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= | ||||
| modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= | ||||
| modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= | ||||
| modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= | ||||
| modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= | ||||
| modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= | ||||
| modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= | ||||
| modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= | ||||
| modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= | ||||
| modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= | ||||
| modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= | ||||
| modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= | ||||
| modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= | ||||
| modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= | ||||
| modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= | ||||
| modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= | ||||
| modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= | ||||
| modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= | ||||
| modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= | ||||
| modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= | ||||
| modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= | ||||
| modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= | ||||
| modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= | ||||
| modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= | ||||
| modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= | ||||
| modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= | ||||
| modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= | ||||
| modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= | ||||
| modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= | ||||
| modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM= | ||||
| modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= | ||||
| modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= | ||||
| modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc= | ||||
| modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8= | ||||
| modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= | ||||
| modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= | ||||
| modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= | ||||
| modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= | ||||
| modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= | ||||
| modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= | ||||
| modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= | ||||
| modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= | ||||
| modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= | ||||
| modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= | ||||
| modernc.org/libc v1.9.5/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= | ||||
| modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= | ||||
| modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= | ||||
| modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= | ||||
| modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= | ||||
| modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= | ||||
| modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= | ||||
| modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= | ||||
| modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= | ||||
| modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= | ||||
| modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= | ||||
| modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= | ||||
| modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= | ||||
| modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= | ||||
| modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= | ||||
| modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= | ||||
| modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= | ||||
| modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= | ||||
| modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= | ||||
| modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= | ||||
| modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= | ||||
| modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= | ||||
| modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= | ||||
| modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= | ||||
| modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= | ||||
| modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= | ||||
| modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= | ||||
| modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= | ||||
| modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= | ||||
| modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= | ||||
| modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= | ||||
| modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= | ||||
| modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= | ||||
| modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= | ||||
| modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= | ||||
| modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= | ||||
| modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= | ||||
| modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= | ||||
| modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= | ||||
| modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= | ||||
| modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q= | ||||
| modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= | ||||
| modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= | ||||
| modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= | ||||
| modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= | ||||
| modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= | ||||
| modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= | ||||
| modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= | ||||
| modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= | ||||
| modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= | ||||
| modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= | ||||
| modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= | ||||
| modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs= | ||||
| modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ= | ||||
| modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y= | ||||
| modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= | ||||
| modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= | ||||
| modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= | ||||
| modernc.org/tcl v1.5.2/go.mod h1:pmJYOLgpiys3oI4AeAafkcUfE+TKKilminxNyU/+Zlo= | ||||
| modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw= | ||||
| modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg= | ||||
| modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= | ||||
| modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= | ||||
| modernc.org/z v1.0.1-0.20210308123920-1f282aa71362/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= | ||||
| modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= | ||||
| modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk= | ||||
| modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo= | ||||
| modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= | ||||
| rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= | ||||
| rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import ( | ||||
| 	"github.com/mattermost/focalboard/server/server" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions/mmpermissions" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/services/store/mattermostauthlayer" | ||||
| 	"github.com/mattermost/focalboard/server/services/store/sqlstore" | ||||
| @@ -29,16 +30,17 @@ import ( | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	boardsFeatureFlagName     = "BoardsFeatureFlags" | ||||
| 	pluginName                = "focalboard" | ||||
| 	sharedBoardsName          = "enablepublicsharedboards" | ||||
| 	boardsFeatureFlagName = "BoardsFeatureFlags" | ||||
| 	pluginName            = "focalboard" | ||||
| 	sharedBoardsName      = "enablepublicsharedboards" | ||||
|  | ||||
| 	notifyFreqCardSecondsKey  = "notify_freq_card_seconds" | ||||
| 	notifyFreqBoardSecondsKey = "notify_freq_board_seconds" | ||||
| ) | ||||
|  | ||||
| type BoardsEmbed struct { | ||||
| 	OriginalPath string `json:"originalPath"` | ||||
| 	WorkspaceID  string `json:"workspaceID"` | ||||
| 	TeamID       string `json:"teamID"` | ||||
| 	ViewID       string `json:"viewID"` | ||||
| 	BoardID      string `json:"boardID"` | ||||
| 	CardID       string `json:"cardID"` | ||||
| @@ -112,7 +114,9 @@ func (p *Plugin) OnActivate() error { | ||||
| 		db = layeredStore | ||||
| 	} | ||||
|  | ||||
| 	p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db), logger) | ||||
| 	permissionsService := mmpermissions.New(db, p.API) | ||||
|  | ||||
| 	p.wsPluginAdapter = ws.NewPluginAdapter(p.API, auth.New(cfg, db, permissionsService), db, logger) | ||||
|  | ||||
| 	backendParams := notifyBackendParams{ | ||||
| 		cfg:        cfg, | ||||
| @@ -137,13 +141,14 @@ func (p *Plugin) OnActivate() error { | ||||
| 	mentionsBackend.AddListener(subscriptionsBackend) | ||||
|  | ||||
| 	params := server.Params{ | ||||
| 		Cfg:             cfg, | ||||
| 		SingleUserToken: "", | ||||
| 		DBStore:         db, | ||||
| 		Logger:          logger, | ||||
| 		ServerID:        serverID, | ||||
| 		WSAdapter:       p.wsPluginAdapter, | ||||
| 		NotifyBackends:  notifyBackends, | ||||
| 		Cfg:                cfg, | ||||
| 		SingleUserToken:    "", | ||||
| 		DBStore:            db, | ||||
| 		Logger:             logger, | ||||
| 		ServerID:           serverID, | ||||
| 		WSAdapter:          p.wsPluginAdapter, | ||||
| 		NotifyBackends:     notifyBackends, | ||||
| 		PermissionsService: permissionsService, | ||||
| 	} | ||||
|  | ||||
| 	server, err := server.New(params) | ||||
| @@ -373,11 +378,11 @@ func postWithBoardsEmbed(post *mmModel.Post) *mmModel.Post { | ||||
| 		return post | ||||
| 	} | ||||
|  | ||||
| 	workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit) | ||||
| 	teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit) | ||||
|  | ||||
| 	if workspaceID != "" && boardID != "" && viewID != "" && cardID != "" { | ||||
| 	if teamID != "" && boardID != "" && viewID != "" && cardID != "" { | ||||
| 		b, _ := json.Marshal(BoardsEmbed{ | ||||
| 			WorkspaceID:  workspaceID, | ||||
| 			TeamID:       teamID, | ||||
| 			BoardID:      boardID, | ||||
| 			ViewID:       viewID, | ||||
| 			CardID:       cardID, | ||||
| @@ -430,7 +435,7 @@ func getFirstLinkAndShortenAllBoardsLink(postMessage string) (firstLink, newPost | ||||
| 	return firstLink, newPostMessage | ||||
| } | ||||
|  | ||||
| func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardID string) { | ||||
| func returnBoardsParams(pathArray []string) (teamID, boardID, viewID, cardID string) { | ||||
| 	// The reason we are doing this search for the first instance of boards or plugins is to take into account URL subpaths | ||||
| 	index := -1 | ||||
| 	for i := 0; i < len(pathArray); i++ { | ||||
| @@ -441,7 +446,7 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI | ||||
| 	} | ||||
|  | ||||
| 	if index == -1 { | ||||
| 		return workspaceID, boardID, viewID, cardID | ||||
| 		return teamID, boardID, viewID, cardID | ||||
| 	} | ||||
|  | ||||
| 	// If at index, the parameter in the path is boards, | ||||
| @@ -450,27 +455,27 @@ func returnBoardsParams(pathArray []string) (workspaceID, boardID, viewID, cardI | ||||
| 	// If at index, the parameter in the path is plugins, | ||||
| 	// then we've copied this from a shared board | ||||
|  | ||||
| 	// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/workspace/workspaceID/boardID/viewID/cardID | ||||
| 	// For card links copied on a non-shared board, the path looks like {...Mattermost Url}.../boards/team/teamID/boardID/viewID/cardID | ||||
|  | ||||
| 	// For card links copied on a shared board, the path looks like | ||||
| 	// {...Mattermost Url}.../plugins/focalboard/workspace/workspaceID/shared/boardID/viewID/cardID?r=read_token | ||||
| 	// {...Mattermost Url}.../plugins/focalboard/team/teamID/shared/boardID/viewID/cardID?r=read_token | ||||
|  | ||||
| 	// This is a non-shared board card link | ||||
| 	if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "workspace" { | ||||
| 		workspaceID = pathArray[index+2] | ||||
| 	if len(pathArray)-index == 6 && pathArray[index] == "boards" && pathArray[index+1] == "team" { | ||||
| 		teamID = pathArray[index+2] | ||||
| 		boardID = pathArray[index+3] | ||||
| 		viewID = pathArray[index+4] | ||||
| 		cardID = pathArray[index+5] | ||||
| 	} else if len(pathArray)-index == 8 && pathArray[index] == "plugins" && | ||||
| 		pathArray[index+1] == "focalboard" && | ||||
| 		pathArray[index+2] == "workspace" && | ||||
| 		pathArray[index+2] == "team" && | ||||
| 		pathArray[index+4] == "shared" { // This is a shared board card link | ||||
| 		workspaceID = pathArray[index+3] | ||||
| 		teamID = pathArray[index+3] | ||||
| 		boardID = pathArray[index+5] | ||||
| 		viewID = pathArray[index+6] | ||||
| 		cardID = pathArray[index+7] | ||||
| 	} | ||||
| 	return workspaceID, boardID, viewID, cardID | ||||
| 	return teamID, boardID, viewID, cardID | ||||
| } | ||||
|  | ||||
| func isBoardsLink(link string) bool { | ||||
| @@ -489,6 +494,6 @@ func isBoardsLink(link string) bool { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	workspaceID, boardID, viewID, cardID := returnBoardsParams(pathSplit) | ||||
| 	return workspaceID != "" && boardID != "" && viewID != "" && cardID != "" | ||||
| 	teamID, boardID, viewID, cardID := returnBoardsParams(pathSplit) | ||||
| 	return teamID != "" && boardID != "" && viewID != "" && cardID != "" | ||||
| } | ||||
|   | ||||
| @@ -36,17 +36,34 @@ function mapStateToProps(state: GlobalState) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| class FocalboardEmbeddedData { | ||||
|     teamID: string | ||||
|     cardID: string | ||||
|     boardID: string | ||||
|     readToken: string | ||||
|     originalPath: string | ||||
|  | ||||
|     constructor(rawData: string) { | ||||
|         const parsed = JSON.parse(rawData) | ||||
|         this.teamID = parsed.teamID || parsed.workspaceID | ||||
|         this.cardID = parsed.cardID | ||||
|         this.boardID = parsed.boardID | ||||
|         this.readToken = parsed.readToken | ||||
|         this.originalPath = parsed.originalPath | ||||
|     } | ||||
| } | ||||
|  | ||||
| const BoardsUnfurl = (props: Props): JSX.Element => { | ||||
|     if (!props.embed || !props.embed.data) { | ||||
|         return <></> | ||||
|     } | ||||
|  | ||||
|     const {embed, locale} = props | ||||
|     const focalboardInformation = JSON.parse(embed.data) | ||||
|     const {workspaceID, cardID, boardID, readToken, originalPath} = focalboardInformation | ||||
|     const focalboardInformation: FocalboardEmbeddedData = new FocalboardEmbeddedData(embed.data) | ||||
|     const {teamID, cardID, boardID, readToken, originalPath} = focalboardInformation | ||||
|     const baseURL = window.location.origin | ||||
|  | ||||
|     if (!workspaceID || !cardID || !boardID) { | ||||
|     if (!teamID || !cardID || !boardID) { | ||||
|         return <></> | ||||
|     } | ||||
|  | ||||
| @@ -57,20 +74,19 @@ const BoardsUnfurl = (props: Props): JSX.Element => { | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const fetchData = async () => { | ||||
|             const [cards, boards] = await Promise.all( | ||||
|             const [cards, fetchedBoard] = await Promise.all( | ||||
|                 [ | ||||
|                     octoClient.getBlocksWithBlockID(cardID, workspaceID, readToken), | ||||
|                     octoClient.getBlocksWithBlockID(boardID, workspaceID, readToken), | ||||
|                     octoClient.getBlocksWithBlockID(cardID, boardID, readToken), | ||||
|                     octoClient.getBoard(boardID), | ||||
|                 ], | ||||
|             ) | ||||
|             const [firstCard] = cards as Card[] | ||||
|             const [firstBoard] = boards as Board[] | ||||
|             if (!firstCard || !firstBoard) { | ||||
|             if (!firstCard || !fetchedBoard) { | ||||
|                 setLoading(false) | ||||
|                 return null | ||||
|             } | ||||
|             setCard(firstCard) | ||||
|             setBoard(firstBoard) | ||||
|             setBoard(fetchedBoard) | ||||
|  | ||||
|             if (firstCard.fields.contentOrder.length) { | ||||
|                 let [firstContentBlockID] = firstCard.fields?.contentOrder | ||||
| @@ -79,7 +95,7 @@ const BoardsUnfurl = (props: Props): JSX.Element => { | ||||
|                     [firstContentBlockID] = firstContentBlockID | ||||
|                 } | ||||
|  | ||||
|                 const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, workspaceID, readToken) as ContentBlock[] | ||||
|                 const contentBlock = await octoClient.getBlocksWithBlockID(firstContentBlockID, boardID, readToken) as ContentBlock[] | ||||
|                 const [firstContentBlock] = contentBlock | ||||
|                 if (!firstContentBlock) { | ||||
|                     setLoading(false) | ||||
| @@ -103,8 +119,8 @@ const BoardsUnfurl = (props: Props): JSX.Element => { | ||||
|         let totalNumberOfCheckBoxes = 0 | ||||
|  | ||||
|         // We will just display the first 3 or less select/multi-select properties and do a +n for remainder if any remainder | ||||
|         for (let i = 0; i < board.fields.cardProperties.length; i++) { | ||||
|             const optionInBoard = board.fields.cardProperties[i] | ||||
|         for (let i = 0; i < board.cardProperties.length; i++) { | ||||
|             const optionInBoard = board.cardProperties[i] | ||||
|             let valueToLookUp = card.fields.properties[optionInBoard.id] | ||||
|  | ||||
|             // Since these are always set and not included in the card properties | ||||
| @@ -132,7 +148,11 @@ const BoardsUnfurl = (props: Props): JSX.Element => { | ||||
|                 continue | ||||
|             } | ||||
|  | ||||
|             propertiesToDisplay.push({optionName: optionInBoard.name, optionValue: optionSelected.value, optionValueColour: optionSelected.color}) | ||||
|             propertiesToDisplay.push({ | ||||
|                 optionName: optionInBoard.name, | ||||
|                 optionValue: optionSelected.value, | ||||
|                 optionValueColour: optionSelected.color, | ||||
|             }) | ||||
|         } | ||||
|         remainder += (Object.keys(card.fields.properties).length - propertiesToDisplay.length - totalNumberOfCheckBoxes) | ||||
|         html = Utils.htmlFromMarkdown(content?.title || '') | ||||
|   | ||||
| @@ -2,7 +2,6 @@ | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| import React from 'react' | ||||
| import PropTypes from 'prop-types' | ||||
|  | ||||
| import {Utils} from '../../../webapp/src/utils' | ||||
|  | ||||
| @@ -10,9 +9,12 @@ type State = { | ||||
|     hasError: boolean | ||||
| } | ||||
|  | ||||
| export default class ErrorBoundary extends React.Component { | ||||
| type Props = { | ||||
|     children: React.ReactNode | ||||
| } | ||||
|  | ||||
| export default class ErrorBoundary extends React.Component<Props, State> { | ||||
|     state = {hasError: false} | ||||
|     propTypes = {children: PropTypes.node.isRequired} | ||||
|     msg = 'Redirecting to error page...' | ||||
|  | ||||
|     handleError = (): void => { | ||||
|   | ||||
| @@ -9,18 +9,22 @@ import {rudderAnalytics, RudderTelemetryHandler} from 'mattermost-redux/client/r | ||||
|  | ||||
| import {GlobalState} from 'mattermost-redux/types/store' | ||||
|  | ||||
| const windowAny = (window as any) | ||||
| import {selectTeam} from 'mattermost-redux/actions/teams' | ||||
|  | ||||
| import {SuiteWindow} from '../../../webapp/src/types/index' | ||||
|  | ||||
| const windowAny = (window as SuiteWindow) | ||||
| windowAny.baseURL = '/plugins/focalboard' | ||||
| windowAny.frontendBaseURL = '/boards' | ||||
| windowAny.isFocalboardPlugin = true | ||||
|  | ||||
| import App from '../../../webapp/src/app' | ||||
| import store from '../../../webapp/src/store' | ||||
| import {setTeam} from '../../../webapp/src/store/teams' | ||||
| import {Utils} from '../../../webapp/src/utils' | ||||
| import GlobalHeader from '../../../webapp/src/components/globalHeader/globalHeader' | ||||
| import FocalboardIcon from '../../../webapp/src/widgets/icons/logo' | ||||
| import {setMattermostTheme} from '../../../webapp/src/theme' | ||||
| import {UserSettings} from '../../../webapp/src/userSettings' | ||||
|  | ||||
| import TelemetryClient, {TelemetryCategory, TelemetryActions} from '../../../webapp/src/telemetry/telemetryClient' | ||||
|  | ||||
| @@ -30,7 +34,13 @@ import '../../../webapp/src/styles/labels.scss' | ||||
| import octoClient from '../../../webapp/src/octoClient' | ||||
|  | ||||
| import BoardsUnfurl from './components/boardsUnfurl/boardsUnfurl' | ||||
| import wsClient, {MMWebSocketClient, ACTION_UPDATE_BLOCK, ACTION_UPDATE_CLIENT_CONFIG, ACTION_UPDATE_SUBSCRIPTION} from './../../../webapp/src/wsclient' | ||||
| import wsClient, { | ||||
|     MMWebSocketClient, | ||||
|     ACTION_UPDATE_BLOCK, | ||||
|     ACTION_UPDATE_CLIENT_CONFIG, | ||||
|     ACTION_UPDATE_SUBSCRIPTION, | ||||
|     ACTION_UPDATE_CATEGORY, ACTION_UPDATE_BLOCK_CATEGORY, ACTION_UPDATE_BOARD, | ||||
| } from './../../../webapp/src/wsclient' | ||||
|  | ||||
| import manifest from './manifest' | ||||
| import ErrorBoundary from './error_boundary' | ||||
| @@ -164,6 +174,7 @@ export default class Plugin { | ||||
|         let theme = mmStore.getState().entities.preferences.myPreferences.theme | ||||
|         setMattermostTheme(theme) | ||||
|         let lastViewedChannel = mmStore.getState().entities.channels.currentChannelId | ||||
|         let prevTeamID: string | ||||
|         mmStore.subscribe(() => { | ||||
|             const currentUserId = mmStore.getState().entities.users.currentUserId | ||||
|             const currentChannel = mmStore.getState().entities.channels.currentChannelId | ||||
| @@ -171,22 +182,41 @@ export default class Plugin { | ||||
|                 localStorage.setItem('focalboardLastViewedChannel:' + currentUserId, currentChannel) | ||||
|                 lastViewedChannel = currentChannel | ||||
|             } | ||||
|  | ||||
|             // Watch for change in active team. | ||||
|             // This handles the user selecting a team from the team sidebar. | ||||
|             const currentTeamID = mmStore.getState().entities.teams.currentTeamId | ||||
|             if (currentTeamID && currentTeamID !== prevTeamID) { | ||||
|                 prevTeamID = currentTeamID | ||||
|                 store.dispatch(setTeam(currentTeamID)) | ||||
|                 browserHistory.push(`/team/${currentTeamID}`) | ||||
|                 wsClient.subscribeToTeam(currentTeamID) | ||||
|             } | ||||
|         }) | ||||
|  | ||||
|         if (this.registry.registerProduct) { | ||||
|             windowAny.frontendBaseURL = subpath + '/boards' | ||||
|             const goToFocalboardWorkspace = () => { | ||||
|                 const currentChannel = mmStore.getState().entities.channels.currentChannelId | ||||
|                 TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {workspaceID: currentChannel}) | ||||
|                 window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener') | ||||
|             const goToFocalboard = () => { | ||||
|                 const currentTeam = mmStore.getState().entities.teams.currentTeamId | ||||
|                 TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelHeader, {teamID: currentTeam}) | ||||
|                 window.open(`${windowAny.frontendBaseURL}/team/${currentTeam}`, '_blank', 'noopener') | ||||
|             } | ||||
|             this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboardWorkspace, 'Boards', 'Boards') | ||||
|  | ||||
|             this.channelHeaderButtonId = registry.registerChannelHeaderButtonAction(<FocalboardIcon/>, goToFocalboard, 'Boards', 'Boards') | ||||
|             this.registry.registerProduct( | ||||
|                 '/boards', | ||||
|                 'product-boards', | ||||
|                 'Boards', | ||||
|                 '/boards', | ||||
|                 MainApp, | ||||
|                 HeaderComponent, | ||||
|                 () => null, | ||||
|                 true, | ||||
|             ) | ||||
|  | ||||
|             const goToFocalboardTemplate = () => { | ||||
|                 const currentChannel = mmStore.getState().entities.channels.currentChannelId | ||||
|                 TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {workspaceID: currentChannel}) | ||||
|                 UserSettings.lastBoardId = null | ||||
|                 UserSettings.lastViewId = null | ||||
|                 TelemetryClient.trackEvent(TelemetryCategory, TelemetryActions.ClickChannelIntro, {channelID: currentChannel}) | ||||
|                 window.open(`${windowAny.frontendBaseURL}/workspace/${currentChannel}`, '_blank', 'noopener') | ||||
|             } | ||||
|  | ||||
| @@ -194,11 +224,9 @@ export default class Plugin { | ||||
|                 this.channelHeaderButtonId = registry.registerChannelIntroButtonAction(<FocalboardIcon/>, goToFocalboardTemplate, 'Boards') | ||||
|             } | ||||
|  | ||||
|             this.registry.registerProduct('/boards', 'product-boards', 'Boards', '/boards/welcome', MainApp, HeaderComponent) | ||||
|  | ||||
|             if (this.registry.registerAppBarComponent) { | ||||
|                 const appBarIconURL = windowAny.baseURL + '/public/app-bar-icon.png' | ||||
|                 this.registry.registerAppBarComponent(appBarIconURL, goToFocalboardWorkspace, 'Open Boards Workspace') | ||||
|                 this.registry.registerAppBarComponent(appBarIconURL, goToFocalboard, 'Open Boards') | ||||
|             } | ||||
|  | ||||
|             this.registry.registerPostWillRenderEmbedComponent((embed) => embed.type === 'boards', BoardsUnfurl, false) | ||||
| @@ -247,7 +275,9 @@ export default class Plugin { | ||||
|         } | ||||
|  | ||||
|         // register websocket handlers | ||||
|         this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK}`, (e: any) => wsClient.updateBlockHandler(e.data)) | ||||
|         this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BOARD}`, (e: any) => wsClient.updateHandler(e.data)) | ||||
|         this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data)) | ||||
|         this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_BLOCK_CATEGORY}`, (e: any) => wsClient.updateHandler(e.data)) | ||||
|         this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_CLIENT_CONFIG}`, (e: any) => wsClient.updateClientConfigHandler(e.data)) | ||||
|         this.registry?.registerWebSocketEventHandler(`custom_${manifest.id}_${ACTION_UPDATE_SUBSCRIPTION}`, (e: any) => wsClient.updateSubscriptionHandler(e.data)) | ||||
|         this.registry?.registerWebSocketEventHandler('plugin_statuses_changed', (e: any) => wsClient.pluginStatusesChangedHandler(e.data)) | ||||
| @@ -267,6 +297,16 @@ export default class Plugin { | ||||
|                 } | ||||
|             } | ||||
|         }) | ||||
|         windowAny.setTeamInSidebar = (teamID: string) => { | ||||
|             // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||
|             // @ts-ignore | ||||
|             mmStore.dispatch(selectTeam(teamID)) | ||||
|         } | ||||
|         windowAny.getCurrentTeamId = (): string => { | ||||
|             // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||||
|             // @ts-ignore | ||||
|             return mmStore.getState().entities.teams.currentTeamId | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     uninitialize(): void { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export interface PluginRegistry { | ||||
|     registerCustomRoute(route: string, component: React.ElementType) | ||||
|     registerProductRoute(route: string, component: React.ElementType) | ||||
|     unregisterComponent(componentId: string) | ||||
|     registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCompoent: React.ElementType) | ||||
|     registerProduct(baseURL: string, switcherIcon: string, switcherText: string, switcherLinkURL: string, mainComponent: React.ElementType, headerCentreComponent: React.ElementType, headerRightComponent?: React.ElementType, showTeamSidebar: boolean) | ||||
|     registerPostWillRenderEmbedComponent(match: (embed: {type: string, data: any}) => void, component: any, toggleable: boolean) | ||||
|     registerWebSocketEventHandler(event: string, handler: (e: any) => void) | ||||
|     unregisterWebSocketEventHandler(event: string) | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| **/*.go { | ||||
|     prep: cd server && go test -race -v ./... | ||||
|     prep: cd server && go test -tags $FOCALBOARD_BUILD_TAGS -race -v ./... | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| **/*.go !**/*_test.go { | ||||
|     prep: cd server && go build -o ../bin/focalboard-server ./main | ||||
|     prep: cd server && go build -tags $FOCALBOARD_BUILD_TAGS -o ../bin/focalboard-server ./main | ||||
|     daemon +sigterm: ./bin/focalboard-server $FOCALBOARDSERVER_ARGS | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,7 @@ linters-settings: | ||||
|     disable: | ||||
|       - fieldalignment | ||||
|   lll: | ||||
|     line-length: 150 | ||||
|     line-length: 180 | ||||
|   dupl: | ||||
|     threshold: 200 | ||||
|   revive: | ||||
|   | ||||
							
								
								
									
										2627
									
								
								server/api/api.go
									
									
									
									
									
								
							
							
						
						
									
										2627
									
								
								server/api/api.go
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -5,6 +5,7 @@ import ( | ||||
| 	"net/http" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/gorilla/mux" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/audit" | ||||
| ) | ||||
| @@ -13,26 +14,20 @@ const ( | ||||
| 	archiveExtension = ".boardarchive" | ||||
| ) | ||||
|  | ||||
| func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /api/v1/workspaces/{workspaceID}/archive/export archiveExport | ||||
| func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /api/v1/boards/{boardID}/archive/export archiveExportBoard | ||||
| 	// | ||||
| 	// Exports an archive of all blocks for one or more boards. If board_id is provided then | ||||
| 	// only that board will be exported, otherwise all boards in the workspace are exported. | ||||
| 	// Exports an archive of all blocks for one boards. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: workspaceID | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Workspace ID | ||||
| 	//   description: Id of board to export | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// - name: board_id | ||||
| 	//   in: path | ||||
| 	//   description: Id of board to to export | ||||
| 	//   required: false | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| @@ -47,25 +42,92 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) { | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	query := r.URL.Query() | ||||
| 	boardID := query.Get("board_id") | ||||
| 	container, err := a.getContainer(r) | ||||
| 	if err != nil { | ||||
| 		a.noContainerErrorResponse(w, r.URL.Path, err) | ||||
| 		return | ||||
| 	} | ||||
| 	vars := mux.Vars(r) | ||||
| 	boardID := vars["boardID"] | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "archiveExport", audit.Fail) | ||||
| 	auditRec := a.makeAuditRecord(r, "archiveExportBoard", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("BoardID", boardID) | ||||
|  | ||||
| 	var boardIDs []string | ||||
| 	if boardID != "" { | ||||
| 		boardIDs = []string{boardID} | ||||
| 	board, err := a.app.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	opts := model.ExportArchiveOptions{ | ||||
| 		WorkspaceID: container.WorkspaceID, | ||||
| 		BoardIDs:    boardIDs, | ||||
| 		TeamID:   board.TeamID, | ||||
| 		BoardIDs: []string{board.ID}, | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) | ||||
| 	w.Header().Set("Content-Type", "application/octet-stream") | ||||
| 	w.Header().Set("Content-Disposition", "attachment; filename="+filename) | ||||
| 	w.Header().Set("Content-Transfer-Encoding", "binary") | ||||
|  | ||||
| 	if err := a.app.ExportArchive(w, opts); err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 	} | ||||
|  | ||||
| 	auditRec.Success() | ||||
| } | ||||
|  | ||||
| func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation GET /api/v1/teams/{teamID}/archive/export archiveExportTeam | ||||
| 	// | ||||
| 	// Exports an archive of all blocks for all the boards in a team. | ||||
| 	// | ||||
| 	// --- | ||||
| 	// produces: | ||||
| 	// - application/json | ||||
| 	// parameters: | ||||
| 	// - name: teamID | ||||
| 	//   in: path | ||||
| 	//   description: Id of team | ||||
| 	//   required: true | ||||
| 	//   type: string | ||||
| 	// security: | ||||
| 	// - BearerAuth: [] | ||||
| 	// responses: | ||||
| 	//   '200': | ||||
| 	//     description: success | ||||
| 	//     content: | ||||
| 	//       application-octet-stream: | ||||
| 	//         type: string | ||||
| 	//         format: binary | ||||
| 	//   default: | ||||
| 	//     description: internal error | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session, _ := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	auditRec := a.makeAuditRecord(r, "archiveExportTeam", audit.Fail) | ||||
| 	defer a.audit.LogRecord(audit.LevelRead, auditRec) | ||||
| 	auditRec.AddMeta("TeamID", teamID) | ||||
|  | ||||
| 	boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err) | ||||
| 		return | ||||
| 	} | ||||
| 	ids := []string{} | ||||
| 	for _, board := range boards { | ||||
| 		ids = append(ids, board.ID) | ||||
| 	} | ||||
|  | ||||
| 	opts := model.ExportArchiveOptions{ | ||||
| 		TeamID:   teamID, | ||||
| 		BoardIDs: ids, | ||||
| 	} | ||||
|  | ||||
| 	filename := fmt.Sprintf("archive-%s%s", time.Now().Format("2006-01-02"), archiveExtension) | ||||
| @@ -81,7 +143,7 @@ func (a *API) handleArchiveExport(w http.ResponseWriter, r *http.Request) { | ||||
| } | ||||
|  | ||||
| func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { | ||||
| 	// swagger:operation POST /api/v1/workspaces/{workspaceID}/archive/import archiveImport | ||||
| 	// swagger:operation POST /api/v1/boards/{boardID}/archive/import archiveImport | ||||
| 	// | ||||
| 	// Import an archive of boards. | ||||
| 	// | ||||
| @@ -91,7 +153,7 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { | ||||
| 	// consumes: | ||||
| 	// - multipart/form-data | ||||
| 	// parameters: | ||||
| 	// - name: workspaceID | ||||
| 	// - name: boardID | ||||
| 	//   in: path | ||||
| 	//   description: Workspace ID | ||||
| 	//   required: true | ||||
| @@ -111,16 +173,13 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { | ||||
| 	//     schema: | ||||
| 	//       "$ref": "#/definitions/ErrorResponse" | ||||
|  | ||||
| 	container, err := a.getContainer(r) | ||||
| 	if err != nil { | ||||
| 		a.noContainerErrorResponse(w, r.URL.Path, err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx := r.Context() | ||||
| 	session, _ := ctx.Value(sessionContextKey).(*model.Session) | ||||
| 	userID := session.UserID | ||||
|  | ||||
| 	vars := mux.Vars(r) | ||||
| 	teamID := vars["teamID"] | ||||
|  | ||||
| 	file, handle, err := r.FormFile(UploadFormFileKey) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprintf(w, "%v", err) | ||||
| @@ -134,8 +193,8 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) { | ||||
| 	auditRec.AddMeta("size", handle.Size) | ||||
|  | ||||
| 	opt := model.ImportArchiveOptions{ | ||||
| 		WorkspaceID: container.WorkspaceID, | ||||
| 		ModifiedBy:  userID, | ||||
| 		TeamID:     teamID, | ||||
| 		ModifiedBy: userID, | ||||
| 	} | ||||
|  | ||||
| 	if err := a.app.ImportArchive(file, opt); err != nil { | ||||
|   | ||||
| @@ -17,12 +17,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin | ||||
| 		userID = session.UserID | ||||
| 	} | ||||
|  | ||||
| 	workspaceID := "unknown" | ||||
| 	container, err := a.getContainer(r) | ||||
| 	if err == nil { | ||||
| 		workspaceID = container.WorkspaceID | ||||
| 	} | ||||
|  | ||||
| 	teamID := "unknown" | ||||
| 	rec := &audit.Record{ | ||||
| 		APIPath:   r.URL.Path, | ||||
| 		Event:     event, | ||||
| @@ -31,7 +26,7 @@ func (a *API) makeAuditRecord(r *http.Request, event string, initialStatus strin | ||||
| 		SessionID: sessionID, | ||||
| 		Client:    r.UserAgent(), | ||||
| 		IPAddress: r.RemoteAddr, | ||||
| 		Meta:      []audit.Meta{{K: audit.KeyWorkspaceID, V: workspaceID}}, | ||||
| 		Meta:      []audit.Meta{{K: audit.KeyTeamID, V: teamID}}, | ||||
| 	} | ||||
|  | ||||
| 	return rec | ||||
|   | ||||
| @@ -302,13 +302,13 @@ func (a *API) handleRegister(w http.ResponseWriter, r *http.Request) { | ||||
|  | ||||
| 	// Validate token | ||||
| 	if len(registerData.Token) > 0 { | ||||
| 		workspace, err2 := a.app.GetRootWorkspace() | ||||
| 		team, err2 := a.app.GetRootTeam() | ||||
| 		if err2 != nil { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err2) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		if registerData.Token != workspace.SignupToken { | ||||
| 		if registerData.Token != team.SignupToken { | ||||
| 			a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "invalid token", nil) | ||||
| 			return | ||||
| 		} | ||||
|   | ||||
| @@ -1,12 +1,16 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/auth" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/metrics" | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/services/webhook" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/mattermost/focalboard/server/ws" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| @@ -14,6 +18,12 @@ import ( | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	blockChangeNotifierQueueSize       = 100 | ||||
| 	blockChangeNotifierPoolSize        = 10 | ||||
| 	blockChangeNotifierShutdownTimeout = time.Second * 10 | ||||
| ) | ||||
|  | ||||
| type Services struct { | ||||
| 	Auth             *auth.Auth | ||||
| 	Store            store.Store | ||||
| @@ -22,19 +32,21 @@ type Services struct { | ||||
| 	Metrics          *metrics.Metrics | ||||
| 	Notifications    *notify.Service | ||||
| 	Logger           *mlog.Logger | ||||
| 	Permissions      permissions.PermissionsService | ||||
| 	SkipTemplateInit bool | ||||
| } | ||||
|  | ||||
| type App struct { | ||||
| 	config        *config.Configuration | ||||
| 	store         store.Store | ||||
| 	auth          *auth.Auth | ||||
| 	wsAdapter     ws.Adapter | ||||
| 	filesBackend  filestore.FileBackend | ||||
| 	webhook       *webhook.Client | ||||
| 	metrics       *metrics.Metrics | ||||
| 	notifications *notify.Service | ||||
| 	logger        *mlog.Logger | ||||
| 	config              *config.Configuration | ||||
| 	store               store.Store | ||||
| 	auth                *auth.Auth | ||||
| 	wsAdapter           ws.Adapter | ||||
| 	filesBackend        filestore.FileBackend | ||||
| 	webhook             *webhook.Client | ||||
| 	metrics             *metrics.Metrics | ||||
| 	notifications       *notify.Service | ||||
| 	logger              *mlog.Logger | ||||
| 	blockChangeNotifier *utils.CallbackQueue | ||||
| } | ||||
|  | ||||
| func (a *App) SetConfig(config *config.Configuration) { | ||||
| @@ -43,15 +55,16 @@ func (a *App) SetConfig(config *config.Configuration) { | ||||
|  | ||||
| func New(config *config.Configuration, wsAdapter ws.Adapter, services Services) *App { | ||||
| 	app := &App{ | ||||
| 		config:        config, | ||||
| 		store:         services.Store, | ||||
| 		auth:          services.Auth, | ||||
| 		wsAdapter:     wsAdapter, | ||||
| 		filesBackend:  services.FilesBackend, | ||||
| 		webhook:       services.Webhook, | ||||
| 		metrics:       services.Metrics, | ||||
| 		notifications: services.Notifications, | ||||
| 		logger:        services.Logger, | ||||
| 		config:              config, | ||||
| 		store:               services.Store, | ||||
| 		auth:                services.Auth, | ||||
| 		wsAdapter:           wsAdapter, | ||||
| 		filesBackend:        services.FilesBackend, | ||||
| 		webhook:             services.Webhook, | ||||
| 		metrics:             services.Metrics, | ||||
| 		notifications:       services.Notifications, | ||||
| 		logger:              services.Logger, | ||||
| 		blockChangeNotifier: utils.NewCallbackQueue("blockChangeNotifier", blockChangeNotifierQueueSize, blockChangeNotifierPoolSize, services.Logger), | ||||
| 	} | ||||
| 	app.initialize(services.SkipTemplateInit) | ||||
| 	return app | ||||
|   | ||||
| @@ -3,7 +3,6 @@ package app | ||||
| import ( | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/auth" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| @@ -25,8 +24,8 @@ func (a *App) GetSession(token string) (*model.Session, error) { | ||||
| } | ||||
|  | ||||
| // IsValidReadToken validates the read token for a block. | ||||
| func (a *App) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) { | ||||
| 	return a.auth.IsValidReadToken(c, blockID, readToken) | ||||
| func (a *App) IsValidReadToken(boardID string, readToken string) (bool, error) { | ||||
| 	return a.auth.IsValidReadToken(boardID, readToken) | ||||
| } | ||||
|  | ||||
| // GetRegisteredUserCount returns the number of registered users. | ||||
|   | ||||
| @@ -1,137 +1,192 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *App) GetBlocks(c store.Container, parentID string, blockType string) ([]model.Block, error) { | ||||
| var ErrBlocksFromMultipleBoards = errors.New("the block set contain blocks from multiple boards") | ||||
|  | ||||
| func (a *App) GetBlocks(boardID, parentID string, blockType string) ([]model.Block, error) { | ||||
| 	if boardID == "" { | ||||
| 		return []model.Block{}, nil | ||||
| 	} | ||||
|  | ||||
| 	if blockType != "" && parentID != "" { | ||||
| 		return a.store.GetBlocksWithParentAndType(c, parentID, blockType) | ||||
| 		return a.store.GetBlocksWithParentAndType(boardID, parentID, blockType) | ||||
| 	} | ||||
|  | ||||
| 	if blockType != "" { | ||||
| 		return a.store.GetBlocksWithType(c, blockType) | ||||
| 		return a.store.GetBlocksWithType(boardID, blockType) | ||||
| 	} | ||||
|  | ||||
| 	return a.store.GetBlocksWithParent(c, parentID) | ||||
| 	return a.store.GetBlocksWithParent(boardID, parentID) | ||||
| } | ||||
|  | ||||
| func (a *App) GetBlocksWithRootID(c store.Container, rootID string) ([]model.Block, error) { | ||||
| 	return a.store.GetBlocksWithRootID(c, rootID) | ||||
| func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTemplate bool) ([]model.Block, error) { | ||||
| 	board, err := a.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if board == nil { | ||||
| 		return nil, fmt.Errorf("cannot fetch board %s for DuplicateBlock: %w", boardID, err) | ||||
| 	} | ||||
|  | ||||
| 	blocks, err := a.store.DuplicateBlock(boardID, blockID, userID, asTemplate) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		for _, block := range blocks { | ||||
| 			a.wsAdapter.BroadcastBlockChange(board.TeamID, block) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	return blocks, err | ||||
| } | ||||
|  | ||||
| func (a *App) GetRootID(c store.Container, blockID string) (string, error) { | ||||
| 	return a.store.GetRootID(c, blockID) | ||||
| func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) { | ||||
| 	return a.store.GetBlocksWithBoardID(boardID) | ||||
| } | ||||
|  | ||||
| func (a *App) GetParentID(c store.Container, blockID string) (string, error) { | ||||
| 	return a.store.GetParentID(c, blockID) | ||||
| } | ||||
|  | ||||
| func (a *App) PatchBlock(c store.Container, blockID string, blockPatch *model.BlockPatch, modifiedByID string) error { | ||||
| 	oldBlock, err := a.store.GetBlock(c, blockID) | ||||
| func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error { | ||||
| 	oldBlock, err := a.store.GetBlock(blockID) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = a.store.PatchBlock(c, blockID, blockPatch, modifiedByID) | ||||
| 	board, err := a.store.GetBoard(oldBlock.BoardID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = a.store.PatchBlock(blockID, blockPatch, modifiedByID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	a.metrics.IncrementBlocksPatched(1) | ||||
| 	block, err := a.store.GetBlock(c, blockID) | ||||
| 	block, err := a.store.GetBlock(blockID) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block) | ||||
| 	go func() { | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		// broadcast on websocket | ||||
| 		a.wsAdapter.BroadcastBlockChange(board.TeamID, *block) | ||||
|  | ||||
| 		// broadcast on webhooks | ||||
| 		a.webhook.NotifyUpdate(*block) | ||||
| 		a.notifyBlockChanged(notify.Update, c, block, oldBlock, modifiedByID) | ||||
| 	}() | ||||
|  | ||||
| 		// send notifications | ||||
| 		a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID) | ||||
| 		return nil | ||||
| 	}) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) PatchBlocks(c store.Container, blockPatches *model.BlockPatchBatch, modifiedByID string) error { | ||||
| func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error { | ||||
| 	oldBlocks := make([]model.Block, 0, len(blockPatches.BlockIDs)) | ||||
| 	for _, blockID := range blockPatches.BlockIDs { | ||||
| 		oldBlock, err := a.store.GetBlock(c, blockID) | ||||
| 		oldBlock, err := a.store.GetBlock(blockID) | ||||
| 		if err != nil { | ||||
| 			return nil | ||||
| 		} | ||||
| 		oldBlocks = append(oldBlocks, *oldBlock) | ||||
| 	} | ||||
|  | ||||
| 	err := a.store.PatchBlocks(c, blockPatches, modifiedByID) | ||||
| 	err := a.store.PatchBlocks(blockPatches, modifiedByID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	a.metrics.IncrementBlocksPatched(len(oldBlocks)) | ||||
| 	for i, blockID := range blockPatches.BlockIDs { | ||||
| 		newBlock, err := a.store.GetBlock(c, blockID) | ||||
| 		if err != nil { | ||||
| 			return nil | ||||
| 		} | ||||
| 		a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *newBlock) | ||||
| 		go func(currentIndex int) { | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		a.metrics.IncrementBlocksPatched(len(oldBlocks)) | ||||
| 		for i, blockID := range blockPatches.BlockIDs { | ||||
| 			newBlock, err := a.store.GetBlock(blockID) | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 			} | ||||
| 			a.wsAdapter.BroadcastBlockChange(teamID, *newBlock) | ||||
| 			a.webhook.NotifyUpdate(*newBlock) | ||||
| 			a.notifyBlockChanged(notify.Update, c, newBlock, &oldBlocks[currentIndex], modifiedByID) | ||||
| 		}(i) | ||||
| 	} | ||||
|  | ||||
| 			a.notifyBlockChanged(notify.Update, newBlock, &oldBlocks[i], modifiedByID) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) InsertBlock(c store.Container, block model.Block, modifiedByID string) error { | ||||
| 	err := a.store.InsertBlock(c, &block, modifiedByID) | ||||
| func (a *App) InsertBlock(block model.Block, modifiedByID string) error { | ||||
| 	board, bErr := a.store.GetBoard(block.BoardID) | ||||
| 	if bErr != nil { | ||||
| 		return bErr | ||||
| 	} | ||||
|  | ||||
| 	err := a.store.InsertBlock(&block, modifiedByID) | ||||
| 	if err == nil { | ||||
| 		a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, block) | ||||
| 		a.metrics.IncrementBlocksInserted(1) | ||||
| 		go func() { | ||||
| 		a.blockChangeNotifier.Enqueue(func() error { | ||||
| 			a.wsAdapter.BroadcastBlockChange(board.TeamID, block) | ||||
| 			a.metrics.IncrementBlocksInserted(1) | ||||
| 			a.webhook.NotifyUpdate(block) | ||||
| 			a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID) | ||||
| 		}() | ||||
| 			a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID) | ||||
| 			return nil | ||||
| 		}) | ||||
| 	} | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func (a *App) InsertBlocks(c store.Container, blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) { | ||||
| func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) { | ||||
| 	if len(blocks) == 0 { | ||||
| 		return []model.Block{}, nil | ||||
| 	} | ||||
|  | ||||
| 	// all blocks must belong to the same board | ||||
| 	boardID := blocks[0].BoardID | ||||
| 	for _, block := range blocks { | ||||
| 		if block.BoardID != boardID { | ||||
| 			return nil, ErrBlocksFromMultipleBoards | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	board, err := a.store.GetBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	needsNotify := make([]model.Block, 0, len(blocks)) | ||||
| 	for i := range blocks { | ||||
| 		err := a.store.InsertBlock(c, &blocks[i], modifiedByID) | ||||
| 		err := a.store.InsertBlock(&blocks[i], modifiedByID) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		blocks[i].WorkspaceID = c.WorkspaceID | ||||
| 		needsNotify = append(needsNotify, blocks[i]) | ||||
|  | ||||
| 		a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, blocks[i]) | ||||
| 		a.wsAdapter.BroadcastBlockChange(board.TeamID, blocks[i]) | ||||
| 		a.metrics.IncrementBlocksInserted(1) | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		for _, b := range needsNotify { | ||||
| 			block := b | ||||
| 			a.webhook.NotifyUpdate(block) | ||||
| 			if allowNotifications { | ||||
| 				a.notifyBlockChanged(notify.Add, c, &block, nil, modifiedByID) | ||||
| 				a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID) | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	return blocks, nil | ||||
| } | ||||
|  | ||||
| func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks []model.Block) error { | ||||
| func (a *App) CopyCardFiles(sourceBoardID string, blocks []model.Block) error { | ||||
| 	// Images attached in cards have a path comprising the card's board ID. | ||||
| 	// When we create a template from this board, we need to copy the files | ||||
| 	// with the new board ID in path. | ||||
| @@ -139,7 +194,7 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks | ||||
| 	// template) to fail to load. | ||||
|  | ||||
| 	// look up ID of source board, which may be different than the blocks. | ||||
| 	board, err := a.GetBlockByID(store.Container{}, sourceBoardID) | ||||
| 	board, err := a.GetBlockByID(sourceBoardID) | ||||
| 	if err != nil || board == nil { | ||||
| 		return fmt.Errorf("cannot fetch board %s for CopyCardFiles: %w", sourceBoardID, err) | ||||
| 	} | ||||
| @@ -153,8 +208,8 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks | ||||
| 			ext := filepath.Ext(fileName.(string)) | ||||
| 			destFilename := utils.NewID(utils.IDTypeNone) + ext | ||||
|  | ||||
| 			sourceFilePath := filepath.Join(board.WorkspaceID, sourceBoardID, fileName.(string)) | ||||
| 			destinationFilePath := filepath.Join(destWorkspaceID, block.RootID, destFilename) | ||||
| 			sourceFilePath := filepath.Join(sourceBoardID, fileName.(string)) | ||||
| 			destinationFilePath := filepath.Join(block.BoardID, destFilename) | ||||
|  | ||||
| 			a.logger.Debug( | ||||
| 				"Copying card file", | ||||
| @@ -179,24 +234,26 @@ func (a *App) CopyCardFiles(sourceBoardID string, destWorkspaceID string, blocks | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetSubTree(c store.Container, blockID string, levels int) ([]model.Block, error) { | ||||
| func (a *App) GetSubTree(boardID, blockID string, levels int, opts model.QuerySubtreeOptions) ([]model.Block, error) { | ||||
| 	// Only 2 or 3 levels are supported for now | ||||
| 	if levels >= 3 { | ||||
| 		return a.store.GetSubTree3(c, blockID, model.QuerySubtreeOptions{}) | ||||
| 		return a.store.GetSubTree3(boardID, blockID, opts) | ||||
| 	} | ||||
| 	return a.store.GetSubTree2(c, blockID, model.QuerySubtreeOptions{}) | ||||
|  | ||||
| 	return a.store.GetSubTree2(boardID, blockID, opts) | ||||
| } | ||||
|  | ||||
| func (a *App) GetAllBlocks(c store.Container) ([]model.Block, error) { | ||||
| 	return a.store.GetAllBlocks(c) | ||||
| func (a *App) GetBlockByID(blockID string) (*model.Block, error) { | ||||
| 	return a.store.GetBlock(blockID) | ||||
| } | ||||
|  | ||||
| func (a *App) GetBlockByID(c store.Container, blockID string) (*model.Block, error) { | ||||
| 	return a.store.GetBlock(c, blockID) | ||||
| } | ||||
| func (a *App) DeleteBlock(blockID string, modifiedBy string) error { | ||||
| 	block, err := a.store.GetBlock(blockID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) error { | ||||
| 	block, err := a.store.GetBlock(c, blockID) | ||||
| 	board, err := a.store.GetBoard(block.BoardID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -206,7 +263,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = a.store.DeleteBlock(c, blockID, modifiedBy) | ||||
| 	err = a.store.DeleteBlock(blockID, modifiedBy) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -214,7 +271,7 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) | ||||
| 	if block.Type == model.TypeImage { | ||||
| 		fileName, fileIDExists := block.Fields["fileId"] | ||||
| 		if fileName, fileIDIsString := fileName.(string); fileIDExists && fileIDIsString { | ||||
| 			filePath := filepath.Join(block.WorkspaceID, block.RootID, fileName) | ||||
| 			filePath := filepath.Join(block.BoardID, fileName) | ||||
| 			err = a.filesBackend.RemoveFile(filePath) | ||||
|  | ||||
| 			if err != nil { | ||||
| @@ -225,31 +282,32 @@ func (a *App) DeleteBlock(c store.Container, blockID string, modifiedBy string) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	a.wsAdapter.BroadcastBlockDelete(c.WorkspaceID, blockID, block.ParentID) | ||||
| 	a.metrics.IncrementBlocksDeleted(1) | ||||
| 	go func() { | ||||
| 		a.notifyBlockChanged(notify.Delete, c, block, block, modifiedBy) | ||||
| 	}() | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID) | ||||
| 		a.metrics.IncrementBlocksDeleted(1) | ||||
| 		a.notifyBlockChanged(notify.Delete, block, block, modifiedBy) | ||||
| 		return nil | ||||
| 	}) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string) error { | ||||
| 	blocks, err := a.store.GetBlockHistory(c, blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) | ||||
| func (a *App) UndeleteBlock(blockID string, modifiedBy string) error { | ||||
| 	blocks, err := a.store.GetBlockHistory(blockID, model.QueryBlockHistoryOptions{Limit: 1, Descending: true}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if len(blocks) == 0 { | ||||
| 		// deleting non-existing block not considered an error | ||||
| 		// undeleting non-existing block not considered an error | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	err = a.store.UndeleteBlock(c, blockID, modifiedBy) | ||||
| 	err = a.store.UndeleteBlock(blockID, modifiedBy) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	block, err := a.store.GetBlock(c, blockID) | ||||
| 	block, err := a.store.GetBlock(blockID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -259,12 +317,19 @@ func (a *App) UndeleteBlock(c store.Container, blockID string, modifiedBy string | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	a.wsAdapter.BroadcastBlockChange(c.WorkspaceID, *block) | ||||
| 	a.metrics.IncrementBlocksInserted(1) | ||||
| 	go func() { | ||||
| 	board, err := a.store.GetBoard(block.BoardID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		a.wsAdapter.BroadcastBlockChange(board.TeamID, *block) | ||||
| 		a.metrics.IncrementBlocksInserted(1) | ||||
| 		a.webhook.NotifyUpdate(*block) | ||||
| 		a.notifyBlockChanged(notify.Add, c, block, nil, modifiedBy) | ||||
| 	}() | ||||
| 		a.notifyBlockChanged(notify.Add, block, nil, modifiedBy) | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @@ -272,13 +337,17 @@ func (a *App) GetBlockCountsByType() (map[string]int64, error) { | ||||
| 	return a.store.GetBlockCountsByType() | ||||
| } | ||||
|  | ||||
| func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block *model.Block, oldBlock *model.Block, modifiedByID string) { | ||||
| func (a *App) GetBlocksForBoard(boardID string) ([]model.Block, error) { | ||||
| 	return a.store.GetBlocksForBoard(boardID) | ||||
| } | ||||
|  | ||||
| func (a *App) notifyBlockChanged(action notify.Action, block *model.Block, oldBlock *model.Block, modifiedByID string) { | ||||
| 	if a.notifications == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// find card and board for the changed block. | ||||
| 	board, card, err := a.store.GetBoardAndCard(c, block) | ||||
| 	board, card, err := a.getBoardAndCard(block) | ||||
| 	if err != nil { | ||||
| 		a.logger.Error("Error notifying for block change; cannot determine board or card", mlog.Err(err)) | ||||
| 		return | ||||
| @@ -286,7 +355,7 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block | ||||
|  | ||||
| 	evt := notify.BlockChangeEvent{ | ||||
| 		Action:       action, | ||||
| 		Workspace:    c.WorkspaceID, | ||||
| 		TeamID:       board.TeamID, | ||||
| 		Board:        board, | ||||
| 		Card:         card, | ||||
| 		BlockChanged: block, | ||||
| @@ -295,3 +364,35 @@ func (a *App) notifyBlockChanged(action notify.Action, c store.Container, block | ||||
| 	} | ||||
| 	a.notifications.BlockChanged(evt) | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	maxSearchDepth = 50 | ||||
| ) | ||||
|  | ||||
| // getBoardAndCard returns the first parent of type `card` its board for the specified block. | ||||
| // `board` and/or `card` may return nil without error if the block does not belong to a board or card. | ||||
| func (a *App) getBoardAndCard(block *model.Block) (board *model.Board, card *model.Block, err error) { | ||||
| 	board, err = a.store.GetBoard(block.BoardID) | ||||
| 	if err != nil { | ||||
| 		return board, card, err | ||||
| 	} | ||||
|  | ||||
| 	var count int // don't let invalid blocks hierarchy cause infinite loop. | ||||
| 	iter := block | ||||
| 	for { | ||||
| 		count++ | ||||
| 		if card == nil && iter.Type == model.TypeCard { | ||||
| 			card = iter | ||||
| 		} | ||||
|  | ||||
| 		if iter.ParentID == "" || (board != nil && card != nil) || count > maxSearchDepth { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		iter, err = a.store.GetBlock(iter.ParentID) | ||||
| 		if err != nil || iter == nil { | ||||
| 			return board, card, err | ||||
| 		} | ||||
| 	} | ||||
| 	return board, card, nil | ||||
| } | ||||
|   | ||||
| @@ -3,10 +3,9 @@ package app | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	st "github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| @@ -18,47 +17,28 @@ func (be blockError) Error() string { | ||||
| 	return be.msg | ||||
| } | ||||
|  | ||||
| func TestGetParentID(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: "0", | ||||
| 	} | ||||
| 	t.Run("success query", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("test-parent-id", nil) | ||||
| 		result, err := th.App.GetParentID(container, "test-id") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, "test-parent-id", result) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("fail query", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().GetParentID(gomock.Eq(container), gomock.Eq("test-id")).Return("", blockError{"block-not-found"}) | ||||
| 		_, err := th.App.GetParentID(container, "test-id") | ||||
| 		require.Error(t, err) | ||||
| 		require.ErrorIs(t, err, blockError{"block-not-found"}) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestInsertBlock(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: "0", | ||||
| 	} | ||||
|  | ||||
| 	t.Run("success scenerio", func(t *testing.T) { | ||||
| 		block := model.Block{} | ||||
| 		th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		err := th.App.InsertBlock(container, block, "user-id-1") | ||||
| 		boardID := testBoardID | ||||
| 		block := model.Block{BoardID: boardID} | ||||
| 		board := &model.Board{ID: boardID} | ||||
| 		th.Store.EXPECT().GetBoard(boardID).Return(board, nil) | ||||
| 		th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(nil) | ||||
| 		th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) | ||||
| 		err := th.App.InsertBlock(block, "user-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error scenerio", func(t *testing.T) { | ||||
| 		block := model.Block{} | ||||
| 		th.Store.EXPECT().InsertBlock(gomock.Eq(container), gomock.Eq(&block), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		err := th.App.InsertBlock(container, block, "user-id-1") | ||||
| 		boardID := testBoardID | ||||
| 		block := model.Block{BoardID: boardID} | ||||
| 		board := &model.Board{ID: boardID} | ||||
| 		th.Store.EXPECT().GetBoard(boardID).Return(board, nil) | ||||
| 		th.Store.EXPECT().InsertBlock(&block, "user-id-1").Return(blockError{"error"}) | ||||
| 		err := th.App.InsertBlock(block, "user-id-1") | ||||
| 		require.Error(t, err, "error") | ||||
| 	}) | ||||
| } | ||||
| @@ -67,20 +47,17 @@ func TestPatchBlocks(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: "0", | ||||
| 	} | ||||
| 	t.Run("patchBlocks success scenerio", func(t *testing.T) { | ||||
| 		blockPatches := model.BlockPatchBatch{} | ||||
| 		th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		err := th.App.PatchBlocks(container, &blockPatches, "user-id-1") | ||||
| 		th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("patchBlocks error scenerio", func(t *testing.T) { | ||||
| 		blockPatches := model.BlockPatchBatch{} | ||||
| 		th.Store.EXPECT().PatchBlocks(gomock.Eq(container), gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		err := th.App.PatchBlocks(container, &blockPatches, "user-id-1") | ||||
| 		th.Store.EXPECT().PatchBlocks(gomock.Eq(&blockPatches), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		err := th.App.PatchBlocks("team-id", &blockPatches, "user-id-1") | ||||
| 		require.Error(t, err, "error") | ||||
| 	}) | ||||
| } | ||||
| @@ -89,27 +66,32 @@ func TestDeleteBlock(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: "0", | ||||
| 	} | ||||
|  | ||||
| 	t.Run("success scenerio", func(t *testing.T) { | ||||
| 		boardID := testBoardID | ||||
| 		board := &model.Board{ID: boardID} | ||||
| 		block := model.Block{ | ||||
| 			ID: "block-id", | ||||
| 			ID:      "block-id", | ||||
| 			BoardID: board.ID, | ||||
| 		} | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		err := th.App.DeleteBlock(container, "block-id", "user-id-1") | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil) | ||||
| 		th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) | ||||
| 		err := th.App.DeleteBlock("block-id", "user-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("error scenerio", func(t *testing.T) { | ||||
| 		boardID := testBoardID | ||||
| 		board := &model.Board{ID: boardID} | ||||
| 		block := model.Block{ | ||||
| 			ID: "block-id", | ||||
| 			ID:      "block-id", | ||||
| 			BoardID: board.ID, | ||||
| 		} | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		th.Store.EXPECT().DeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		err := th.App.DeleteBlock(container, "block-id", "user-id-1") | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		th.Store.EXPECT().DeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		th.Store.EXPECT().GetBoard(gomock.Eq(testBoardID)).Return(board, nil) | ||||
| 		err := th.App.DeleteBlock("block-id", "user-id-1") | ||||
| 		require.Error(t, err, "error") | ||||
| 	}) | ||||
| } | ||||
| @@ -118,22 +100,22 @@ func TestUndeleteBlock(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: "0", | ||||
| 	} | ||||
|  | ||||
| 	t.Run("success scenerio", func(t *testing.T) { | ||||
| 		boardID := testBoardID | ||||
| 		board := &model.Board{ID: boardID} | ||||
| 		block := model.Block{ | ||||
| 			ID: "block-id", | ||||
| 			ID:      "block-id", | ||||
| 			BoardID: board.ID, | ||||
| 		} | ||||
| 		th.Store.EXPECT().GetBlockHistory( | ||||
| 			gomock.Eq(container), | ||||
| 			gomock.Eq("block-id"), | ||||
| 			gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}), | ||||
| 		).Return([]model.Block{block}, nil) | ||||
| 		th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		err := th.App.UndeleteBlock(container, "block-id", "user-id-1") | ||||
| 		th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(nil) | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		th.Store.EXPECT().GetBoard(boardID).Return(board, nil) | ||||
| 		th.Store.EXPECT().GetMembersForBoard(boardID).Return([]*model.BoardMember{}, nil) | ||||
| 		err := th.App.UndeleteBlock("block-id", "user-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| @@ -142,13 +124,12 @@ func TestUndeleteBlock(t *testing.T) { | ||||
| 			ID: "block-id", | ||||
| 		} | ||||
| 		th.Store.EXPECT().GetBlockHistory( | ||||
| 			gomock.Eq(container), | ||||
| 			gomock.Eq("block-id"), | ||||
| 			gomock.Eq(model.QueryBlockHistoryOptions{Limit: 1, Descending: true}), | ||||
| 		).Return([]model.Block{block}, nil) | ||||
| 		th.Store.EXPECT().UndeleteBlock(gomock.Eq(container), gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq(container), gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		err := th.App.UndeleteBlock(container, "block-id", "user-id-1") | ||||
| 		th.Store.EXPECT().UndeleteBlock(gomock.Eq("block-id"), gomock.Eq("user-id-1")).Return(blockError{"error"}) | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Eq("block-id")).Return(&block, nil) | ||||
| 		err := th.App.UndeleteBlock("block-id", "user-id-1") | ||||
| 		require.Error(t, err, "error") | ||||
| 	}) | ||||
| } | ||||
|   | ||||
							
								
								
									
										254
									
								
								server/app/boards.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								server/app/boards.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,254 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrBoardMemberIsLastAdmin = errors.New("cannot leave a board with no admins") | ||||
| 	ErrNewBoardCannotHaveID   = errors.New("new board cannot have an ID") | ||||
| ) | ||||
|  | ||||
| func (a *App) GetBoard(boardID string) (*model.Board, error) { | ||||
| 	board, err := a.store.GetBoard(boardID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return board, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*model.BoardsAndBlocks, []*model.BoardMember, error) { | ||||
| 	bab, members, err := a.store.DuplicateBoard(boardID, userID, toTeam, asTemplate) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	go func() { | ||||
| 		teamID := "" | ||||
| 		for _, board := range bab.Boards { | ||||
| 			teamID = board.TeamID | ||||
| 			a.wsAdapter.BroadcastBoardChange(teamID, board) | ||||
| 		} | ||||
| 		for _, block := range bab.Blocks { | ||||
| 			a.wsAdapter.BroadcastBlockChange(teamID, block) | ||||
| 		} | ||||
| 		for _, member := range members { | ||||
| 			a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member) | ||||
| 		} | ||||
| 	}() | ||||
| 	return bab, members, err | ||||
| } | ||||
|  | ||||
| func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) { | ||||
| 	return a.store.GetBoardsForUserAndTeam(userID, teamID) | ||||
| } | ||||
|  | ||||
| func (a *App) GetTemplateBoards(teamID string) ([]*model.Board, error) { | ||||
| 	return a.store.GetTemplateBoards(teamID) | ||||
| } | ||||
|  | ||||
| func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*model.Board, error) { | ||||
| 	if board.ID != "" { | ||||
| 		return nil, ErrNewBoardCannotHaveID | ||||
| 	} | ||||
| 	board.ID = utils.NewID(utils.IDTypeBoard) | ||||
|  | ||||
| 	var newBoard *model.Board | ||||
| 	var member *model.BoardMember | ||||
| 	var err error | ||||
| 	if addMember { | ||||
| 		newBoard, member, err = a.store.InsertBoardWithAdmin(board, userID) | ||||
| 	} else { | ||||
| 		newBoard, err = a.store.InsertBoard(board, userID) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastBoardChange(newBoard.TeamID, newBoard) | ||||
|  | ||||
| 		if addMember { | ||||
| 			a.wsAdapter.BroadcastMemberChange(newBoard.TeamID, newBoard.ID, member) | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	return newBoard, nil | ||||
| } | ||||
|  | ||||
| func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) { | ||||
| 	updatedBoard, err := a.store.PatchBoard(boardID, patch, userID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard) | ||||
| 	}() | ||||
|  | ||||
| 	return updatedBoard, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DeleteBoard(boardID, userID string) error { | ||||
| 	board, err := a.store.GetBoard(boardID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := a.store.DeleteBoard(boardID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastBoardDelete(board.TeamID, boardID) | ||||
| 	}() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetMembersForBoard(boardID string) ([]*model.BoardMember, error) { | ||||
| 	return a.store.GetMembersForBoard(boardID) | ||||
| } | ||||
|  | ||||
| func (a *App) GetMembersForUser(userID string) ([]*model.BoardMember, error) { | ||||
| 	return a.store.GetMembersForUser(userID) | ||||
| } | ||||
|  | ||||
| func (a *App) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, error) { | ||||
| 	board, err := a.store.GetBoard(member.BoardID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	existingMembership, err := a.store.GetMemberForBoard(member.BoardID, member.UserID) | ||||
| 	if err != nil && !errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if existingMembership != nil { | ||||
| 		return existingMembership, nil | ||||
| 	} | ||||
|  | ||||
| 	newMember, err := a.store.SaveMember(member) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member) | ||||
| 	}() | ||||
|  | ||||
| 	return newMember, nil | ||||
| } | ||||
|  | ||||
| func (a *App) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, error) { | ||||
| 	board, bErr := a.store.GetBoard(member.BoardID) | ||||
| 	if errors.Is(bErr, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if bErr != nil { | ||||
| 		return nil, bErr | ||||
| 	} | ||||
|  | ||||
| 	oldMember, err := a.store.GetMemberForBoard(member.BoardID, member.UserID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// if we're updating an admin, we need to check that there is at | ||||
| 	// least still another admin on the board | ||||
| 	if oldMember.SchemeAdmin && !member.SchemeAdmin { | ||||
| 		isLastAdmin, err2 := a.isLastAdmin(member.UserID, member.BoardID) | ||||
| 		if err2 != nil { | ||||
| 			return nil, err2 | ||||
| 		} | ||||
| 		if isLastAdmin { | ||||
| 			return nil, ErrBoardMemberIsLastAdmin | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	newMember, err := a.store.SaveMember(member) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastMemberChange(board.TeamID, member.BoardID, member) | ||||
| 	}() | ||||
|  | ||||
| 	return newMember, nil | ||||
| } | ||||
|  | ||||
| func (a *App) isLastAdmin(userID, boardID string) (bool, error) { | ||||
| 	members, err := a.store.GetMembersForBoard(boardID) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	for _, m := range members { | ||||
| 		if m.SchemeAdmin && m.UserID != userID { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 	} | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DeleteBoardMember(boardID, userID string) error { | ||||
| 	board, bErr := a.store.GetBoard(boardID) | ||||
| 	if errors.Is(bErr, sql.ErrNoRows) { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if bErr != nil { | ||||
| 		return bErr | ||||
| 	} | ||||
|  | ||||
| 	oldMember, err := a.store.GetMemberForBoard(boardID, userID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// if we're removing an admin, we need to check that there is at | ||||
| 	// least still another admin on the board | ||||
| 	if oldMember.SchemeAdmin { | ||||
| 		isLastAdmin, err := a.isLastAdmin(userID, boardID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if isLastAdmin { | ||||
| 			return ErrBoardMemberIsLastAdmin | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err := a.store.DeleteMember(boardID, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastMemberDelete(board.TeamID, boardID, userID) | ||||
| 	}() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (a *App) SearchBoardsForUserAndTeam(term, userID, teamID string) ([]*model.Board, error) { | ||||
| 	return a.store.SearchBoardsForUserAndTeam(term, userID, teamID) | ||||
| } | ||||
							
								
								
									
										126
									
								
								server/app/boards_and_blocks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								server/app/boards_and_blocks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *App) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks, userID string, addMember bool) (*model.BoardsAndBlocks, error) { | ||||
| 	var newBab *model.BoardsAndBlocks | ||||
| 	var members []*model.BoardMember | ||||
| 	var err error | ||||
|  | ||||
| 	if addMember { | ||||
| 		newBab, members, err = a.store.CreateBoardsAndBlocksWithAdmin(bab, userID) | ||||
| 	} else { | ||||
| 		newBab, err = a.store.CreateBoardsAndBlocks(bab, userID) | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// all new boards should belong to the same team | ||||
| 	teamID := newBab.Boards[0].TeamID | ||||
|  | ||||
| 	// This can be synchronous because this action is not common | ||||
| 	for _, board := range newBab.Boards { | ||||
| 		a.wsAdapter.BroadcastBoardChange(teamID, board) | ||||
| 	} | ||||
|  | ||||
| 	for _, block := range newBab.Blocks { | ||||
| 		b := block | ||||
| 		a.wsAdapter.BroadcastBlockChange(teamID, b) | ||||
| 		a.metrics.IncrementBlocksInserted(1) | ||||
| 		a.webhook.NotifyUpdate(b) | ||||
| 		a.notifyBlockChanged(notify.Add, &b, nil, userID) | ||||
| 	} | ||||
|  | ||||
| 	if addMember { | ||||
| 		for _, member := range members { | ||||
| 			a.wsAdapter.BroadcastMemberChange(teamID, member.BoardID, member) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return newBab, nil | ||||
| } | ||||
|  | ||||
| func (a *App) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks, userID string) (*model.BoardsAndBlocks, error) { | ||||
| 	oldBlocksMap := map[string]*model.Block{} | ||||
| 	for _, blockID := range pbab.BlockIDs { | ||||
| 		block, err := a.store.GetBlock(blockID) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		oldBlocksMap[blockID] = block | ||||
| 	} | ||||
|  | ||||
| 	bab, err := a.store.PatchBoardsAndBlocks(pbab, userID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		teamID := bab.Boards[0].TeamID | ||||
|  | ||||
| 		for _, block := range bab.Blocks { | ||||
| 			oldBlock, ok := oldBlocksMap[block.ID] | ||||
| 			if !ok { | ||||
| 				a.logger.Error("Error notifying for block change on patch boards and blocks; cannot get old block", mlog.String("blockID", block.ID)) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			b := block | ||||
| 			a.metrics.IncrementBlocksPatched(1) | ||||
| 			a.wsAdapter.BroadcastBlockChange(teamID, b) | ||||
| 			a.webhook.NotifyUpdate(b) | ||||
| 			a.notifyBlockChanged(notify.Update, &b, oldBlock, userID) | ||||
| 		} | ||||
|  | ||||
| 		for _, board := range bab.Boards { | ||||
| 			a.wsAdapter.BroadcastBoardChange(board.TeamID, board) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	return bab, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks, userID string) error { | ||||
| 	firstBoard, err := a.store.GetBoard(dbab.Boards[0]) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// we need the block entity to notify of the block changes, so we | ||||
| 	// fetch and store the blocks first | ||||
| 	blocks := []*model.Block{} | ||||
| 	for _, blockID := range dbab.Blocks { | ||||
| 		block, err := a.store.GetBlock(blockID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		blocks = append(blocks, block) | ||||
| 	} | ||||
|  | ||||
| 	if err := a.store.DeleteBoardsAndBlocks(dbab, userID); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	a.blockChangeNotifier.Enqueue(func() error { | ||||
| 		for _, block := range blocks { | ||||
| 			a.wsAdapter.BroadcastBlockDelete(firstBoard.TeamID, block.ID, block.BoardID) | ||||
| 			a.metrics.IncrementBlocksDeleted(1) | ||||
| 			a.notifyBlockChanged(notify.Update, block, block, userID) | ||||
| 		} | ||||
|  | ||||
| 		for _, boardID := range dbab.Boards { | ||||
| 			a.wsAdapter.BroadcastBoardDelete(firstBoard.TeamID, boardID) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										103
									
								
								server/app/category.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								server/app/category.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	ErrorCategoryPermissionDenied = errors.New("category doesn't belong to user") | ||||
| 	ErrorCategoryDeleted          = errors.New("category is deleted") | ||||
| ) | ||||
|  | ||||
| func (a *App) CreateCategory(category *model.Category) (*model.Category, error) { | ||||
| 	category.Hydrate() | ||||
| 	if err := category.IsValid(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := a.store.CreateCategory(*category); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	createdCategory, err := a.store.GetCategory(category.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastCategoryChange(*createdCategory) | ||||
| 	}() | ||||
|  | ||||
| 	return createdCategory, nil | ||||
| } | ||||
|  | ||||
| func (a *App) UpdateCategory(category *model.Category) (*model.Category, error) { | ||||
| 	// verify if category belongs to the user | ||||
| 	existingCategory, err := a.store.GetCategory(category.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if existingCategory.DeleteAt != 0 { | ||||
| 		return nil, ErrorCategoryDeleted | ||||
| 	} | ||||
|  | ||||
| 	if existingCategory.UserID != category.UserID { | ||||
| 		return nil, ErrorCategoryPermissionDenied | ||||
| 	} | ||||
|  | ||||
| 	category.UpdateAt = utils.GetMillis() | ||||
| 	if err = category.IsValid(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err = a.store.UpdateCategory(*category); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	updatedCategory, err := a.store.GetCategory(category.ID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastCategoryChange(*updatedCategory) | ||||
| 	}() | ||||
|  | ||||
| 	return updatedCategory, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DeleteCategory(categoryID, userID, teamID string) (*model.Category, error) { | ||||
| 	existingCategory, err := a.store.GetCategory(categoryID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// category is already deleted. This avoids | ||||
| 	// overriding the original deleted at timestamp | ||||
| 	if existingCategory.DeleteAt != 0 { | ||||
| 		return existingCategory, nil | ||||
| 	} | ||||
|  | ||||
| 	// verify if category belongs to the user | ||||
| 	if existingCategory.UserID != userID { | ||||
| 		return nil, ErrorCategoryPermissionDenied | ||||
| 	} | ||||
|  | ||||
| 	if err = a.store.DeleteCategory(categoryID, userID, teamID); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	deletedCategory, err := a.store.GetCategory(categoryID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastCategoryChange(*deletedCategory) | ||||
| 	}() | ||||
|  | ||||
| 	return deletedCategory, nil | ||||
| } | ||||
							
								
								
									
										26
									
								
								server/app/category_blocks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/app/category_blocks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| package app | ||||
|  | ||||
| import "github.com/mattermost/focalboard/server/model" | ||||
|  | ||||
| func (a *App) GetUserCategoryBlocks(userID, teamID string) ([]model.CategoryBlocks, error) { | ||||
| 	return a.store.GetUserCategoryBlocks(userID, teamID) | ||||
| } | ||||
|  | ||||
| func (a *App) AddUpdateUserCategoryBlock(teamID, userID, categoryID, blockID string) error { | ||||
| 	err := a.store.AddUpdateCategoryBlock(userID, categoryID, blockID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		a.wsAdapter.BroadcastCategoryBlockChange( | ||||
| 			teamID, | ||||
| 			userID, | ||||
| 			model.BlockCategoryWebsocketData{ | ||||
| 				BlockID:    blockID, | ||||
| 				CategoryID: categoryID, | ||||
| 			}) | ||||
| 	}() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -7,7 +7,6 @@ import ( | ||||
| 	"io" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/wiggin77/merror" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| @@ -18,10 +17,7 @@ var ( | ||||
| ) | ||||
|  | ||||
| func (a *App) ExportArchive(w io.Writer, opt model.ExportArchiveOptions) (errs error) { | ||||
| 	container := store.Container{ | ||||
| 		WorkspaceID: opt.WorkspaceID, | ||||
| 	} | ||||
| 	boards, err := a.getBoardsForArchive(container, opt.BoardIDs) | ||||
| 	boards, err := a.getBoardsForArchive(opt.BoardIDs) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -71,7 +67,7 @@ func (a *App) writeArchiveVersion(zw *zip.Writer) error { | ||||
| } | ||||
|  | ||||
| // writeArchiveBoard writes a single board to the archive in a zip directory. | ||||
| func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.ExportArchiveOptions) error { | ||||
| func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.ExportArchiveOptions) error { | ||||
| 	// create a directory per board | ||||
| 	w, err := zw.Create(board.ID + "/board.jsonl") | ||||
| 	if err != nil { | ||||
| @@ -79,18 +75,14 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Block, opt model.Exp | ||||
| 	} | ||||
|  | ||||
| 	// write the board block first | ||||
| 	if err = a.writeArchiveBlockLine(w, board); err != nil { | ||||
| 	if err = a.writeArchiveBoardLine(w, board); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var files []string | ||||
| 	container := store.Container{ | ||||
| 		WorkspaceID: opt.WorkspaceID, | ||||
| 	} | ||||
|  | ||||
| 	// write the board's blocks | ||||
| 	// TODO: paginate this | ||||
| 	blocks, err := a.GetBlocksWithRootID(container, board.ID) | ||||
| 	blocks, err := a.GetBlocksWithBoardID(board.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -143,6 +135,32 @@ func (a *App) writeArchiveBlockLine(w io.Writer, block model.Block) error { | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // writeArchiveBlockLine writes a single block to the archive. | ||||
| func (a *App) writeArchiveBoardLine(w io.Writer, board model.Board) error { | ||||
| 	b, err := json.Marshal(&board) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	line := model.ArchiveLine{ | ||||
| 		Type: "board", | ||||
| 		Data: b, | ||||
| 	} | ||||
|  | ||||
| 	b, err = json.Marshal(&line) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, err = w.Write(b) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// jsonl files need a newline | ||||
| 	_, err = w.Write(newline) | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // writeArchiveFile writes a single file to the archive. | ||||
| func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, opt model.ExportArchiveOptions) error { | ||||
| 	dest, err := zw.Create(boardID + "/" + filename) | ||||
| @@ -150,12 +168,12 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	src, err := a.GetFileReader(opt.WorkspaceID, boardID, filename) | ||||
| 	src, err := a.GetFileReader(opt.TeamID, boardID, filename) | ||||
| 	if err != nil { | ||||
| 		// just log this; image file is missing but we'll still export an equivalent board | ||||
| 		a.logger.Error("image file missing for export", | ||||
| 			mlog.String("filename", filename), | ||||
| 			mlog.String("workspace_id", opt.WorkspaceID), | ||||
| 			mlog.String("team_id", opt.TeamID), | ||||
| 			mlog.String("board_id", boardID), | ||||
| 		) | ||||
| 		return nil | ||||
| @@ -168,27 +186,25 @@ func (a *App) writeArchiveFile(zw *zip.Writer, filename string, boardID string, | ||||
|  | ||||
| // getBoardsForArchive fetches all the specified boards, or all boards in the workspace/team | ||||
| // if `boardIDs` is empty. | ||||
| func (a *App) getBoardsForArchive(container store.Container, boardIDs []string) ([]model.Block, error) { | ||||
| func (a *App) getBoardsForArchive(boardIDs []string) ([]model.Board, error) { | ||||
| 	if len(boardIDs) == 0 { | ||||
| 		boards, err := a.GetBlocks(container, "", model.TypeBoard) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("could not fetch all boards: %w", err) | ||||
| 		} | ||||
| 		return boards, nil | ||||
| 		// TODO: implement this | ||||
| 		// boards, err := a.GetAllBoards("", "board") | ||||
| 		// if err != nil { | ||||
| 		// 	return nil, fmt.Errorf("could not fetch all boards: %w", err) | ||||
| 		// } | ||||
| 		// return boards, nil | ||||
| 		return []model.Board{}, nil | ||||
| 	} | ||||
|  | ||||
| 	boards := make([]model.Block, 0, len(boardIDs)) | ||||
| 	boards := make([]model.Board, 0, len(boardIDs)) | ||||
|  | ||||
| 	for _, id := range boardIDs { | ||||
| 		b, err := a.GetBlockByID(container, id) | ||||
| 		b, err := a.GetBoard(id) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("could not fetch board %s: %w", id, err) | ||||
| 		} | ||||
|  | ||||
| 		if b.Type != model.TypeBoard { | ||||
| 			return nil, fmt.Errorf("block %s is not a board: %w", b.ID, model.ErrInvalidBoardBlock) | ||||
| 		} | ||||
|  | ||||
| 		boards = append(boards, *b) | ||||
| 	} | ||||
| 	return boards, nil | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import ( | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore" | ||||
| ) | ||||
|  | ||||
| func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) (string, error) { | ||||
| func (a *App) SaveFile(reader io.Reader, teamID, rootID, filename string) (string, error) { | ||||
| 	// NOTE: File extension includes the dot | ||||
| 	fileExtension := strings.ToLower(filepath.Ext(filename)) | ||||
| 	if fileExtension == ".jpeg" { | ||||
| @@ -21,7 +21,7 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) ( | ||||
| 	} | ||||
|  | ||||
| 	createdFilename := fmt.Sprintf(`%s%s`, utils.NewID(utils.IDTypeNone), fileExtension) | ||||
| 	filePath := filepath.Join(workspaceID, rootID, createdFilename) | ||||
| 	filePath := filepath.Join(teamID, rootID, createdFilename) | ||||
|  | ||||
| 	_, appErr := a.filesBackend.WriteFile(reader, filePath) | ||||
| 	if appErr != nil { | ||||
| @@ -31,14 +31,14 @@ func (a *App) SaveFile(reader io.Reader, workspaceID, rootID, filename string) ( | ||||
| 	return createdFilename, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetFileReader(workspaceID, rootID, filename string) (filestore.ReadCloseSeeker, error) { | ||||
| 	filePath := filepath.Join(workspaceID, rootID, filename) | ||||
| func (a *App) GetFileReader(teamID, rootID, filename string) (filestore.ReadCloseSeeker, error) { | ||||
| 	filePath := filepath.Join(teamID, rootID, filename) | ||||
| 	exists, err := a.filesBackend.FileExists(filePath) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	// FIXUP: Check the deprecated old location | ||||
| 	if workspaceID == "0" && !exists { | ||||
| 	if teamID == "0" && !exists { | ||||
| 		oldExists, err2 := a.filesBackend.FileExists(filename) | ||||
| 		if err2 != nil { | ||||
| 			return nil, err2 | ||||
|   | ||||
| @@ -5,16 +5,17 @@ import ( | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/filestore/mocks" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	testFileName = "temp-file-name" | ||||
| 	testRootID   = "test-root-id" | ||||
| 	testFilePath = "1/test-root-id/temp-file-name" | ||||
| 	testBoardID  = "test-board-id" | ||||
| 	testFilePath = "1/test-board-id/temp-file-name" | ||||
| ) | ||||
|  | ||||
| type TestError struct{} | ||||
| @@ -45,7 +46,7 @@ func TestGetFileReader(t *testing.T) { | ||||
|  | ||||
| 		mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) | ||||
| 		mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) | ||||
| 		actual, _ := th.App.GetFileReader("1", testRootID, testFileName) | ||||
| 		actual, _ := th.App.GetFileReader("1", testBoardID, testFileName) | ||||
| 		assert.Equal(t, mockedReadCloseSeek, actual) | ||||
| 	}) | ||||
|  | ||||
| @@ -71,7 +72,7 @@ func TestGetFileReader(t *testing.T) { | ||||
|  | ||||
| 		mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) | ||||
| 		mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) | ||||
| 		actual, err := th.App.GetFileReader("1", testRootID, testFileName) | ||||
| 		actual, err := th.App.GetFileReader("1", testBoardID, testFileName) | ||||
| 		assert.Error(t, err, mockedError) | ||||
| 		assert.Nil(t, actual) | ||||
| 	}) | ||||
| @@ -98,13 +99,13 @@ func TestGetFileReader(t *testing.T) { | ||||
|  | ||||
| 		mockedFileBackend.On("Reader", testFilePath).Return(readerFunc, readerErrorFunc) | ||||
| 		mockedFileBackend.On("FileExists", testFilePath).Return(fileExistsFunc, fileExistsErrorFunc) | ||||
| 		actual, err := th.App.GetFileReader("1", testRootID, testFileName) | ||||
| 		actual, err := th.App.GetFileReader("1", testBoardID, testFileName) | ||||
| 		assert.Error(t, err, mockedError) | ||||
| 		assert.Nil(t, actual) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should move file from old filepath to new filepath, if file doesnot exists in new filepath and workspace id is 0", func(t *testing.T) { | ||||
| 		filePath := "0/test-root-id/temp-file-name" | ||||
| 		filePath := "0/test-board-id/temp-file-name" | ||||
| 		workspaceid := "0" | ||||
| 		mockedFileBackend := &mocks.FileBackend{} | ||||
| 		th.App.filesBackend = mockedFileBackend | ||||
| @@ -134,12 +135,12 @@ func TestGetFileReader(t *testing.T) { | ||||
| 		mockedFileBackend.On("MoveFile", testFileName, filePath).Return(moveFileFunc) | ||||
| 		mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc) | ||||
|  | ||||
| 		actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName) | ||||
| 		actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName) | ||||
| 		assert.Equal(t, mockedReadCloseSeek, actual) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should return file reader, if file doesnot exists in new filepath and old file path", func(t *testing.T) { | ||||
| 		filePath := "0/test-root-id/temp-file-name" | ||||
| 		filePath := "0/test-board-id/temp-file-name" | ||||
| 		fileName := testFileName | ||||
| 		workspaceid := "0" | ||||
| 		mockedFileBackend := &mocks.FileBackend{} | ||||
| @@ -170,7 +171,7 @@ func TestGetFileReader(t *testing.T) { | ||||
| 		mockedFileBackend.On("MoveFile", fileName, filePath).Return(moveFileFunc) | ||||
| 		mockedFileBackend.On("Reader", filePath).Return(readerFunc, readerErrorFunc) | ||||
|  | ||||
| 		actual, _ := th.App.GetFileReader(workspaceid, testRootID, testFileName) | ||||
| 		actual, _ := th.App.GetFileReader(workspaceid, testBoardID, testFileName) | ||||
| 		assert.Equal(t, mockedReadCloseSeek, actual) | ||||
| 	}) | ||||
| } | ||||
| @@ -186,7 +187,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||
| 			paths := strings.Split(path, "/") | ||||
| 			assert.Equal(t, "1", paths[0]) | ||||
| 			assert.Equal(t, testRootID, paths[1]) | ||||
| 			assert.Equal(t, testBoardID, paths[1]) | ||||
| 			fileName = paths[2] | ||||
| 			return int64(10) | ||||
| 		} | ||||
| @@ -196,7 +197,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		} | ||||
|  | ||||
| 		mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) | ||||
| 		actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testRootID, fileName) | ||||
| 		actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", testBoardID, fileName) | ||||
| 		assert.Equal(t, fileName, actual) | ||||
| 		assert.Nil(t, err) | ||||
| 	}) | ||||
| @@ -209,7 +210,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||
| 			paths := strings.Split(path, "/") | ||||
| 			assert.Equal(t, "1", paths[0]) | ||||
| 			assert.Equal(t, "test-root-id", paths[1]) | ||||
| 			assert.Equal(t, "test-board-id", paths[1]) | ||||
| 			assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) | ||||
| 			return int64(10) | ||||
| 		} | ||||
| @@ -219,7 +220,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		} | ||||
|  | ||||
| 		mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) | ||||
| 		actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName) | ||||
| 		actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName) | ||||
| 		assert.Nil(t, err) | ||||
| 		assert.NotNil(t, actual) | ||||
| 	}) | ||||
| @@ -233,7 +234,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		writeFileFunc := func(reader io.Reader, path string) int64 { | ||||
| 			paths := strings.Split(path, "/") | ||||
| 			assert.Equal(t, "1", paths[0]) | ||||
| 			assert.Equal(t, "test-root-id", paths[1]) | ||||
| 			assert.Equal(t, "test-board-id", paths[1]) | ||||
| 			assert.Equal(t, "jpg", strings.Split(paths[2], ".")[1]) | ||||
| 			return int64(10) | ||||
| 		} | ||||
| @@ -243,7 +244,7 @@ func TestSaveFile(t *testing.T) { | ||||
| 		} | ||||
|  | ||||
| 		mockedFileBackend.On("WriteFile", mockedReadCloseSeek, mock.Anything).Return(writeFileFunc, writeFileErrorFunc) | ||||
| 		actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-root-id", fileName) | ||||
| 		actual, err := th.App.SaveFile(mockedReadCloseSeek, "1", "test-board-id", fileName) | ||||
| 		assert.Equal(t, "", actual) | ||||
| 		assert.Equal(t, "unable to store the file in the files storage: Mocked File backend error", err.Error()) | ||||
| 	}) | ||||
|   | ||||
| @@ -29,11 +29,10 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { | ||||
| 	defer ctrl.Finish() | ||||
| 	cfg := config.Configuration{} | ||||
| 	store := mockstore.NewMockStore(ctrl) | ||||
|  | ||||
| 	auth := auth.New(&cfg, store) | ||||
| 	auth := auth.New(&cfg, store, nil) | ||||
| 	logger := mlog.CreateConsoleTestLogger(false, mlog.LvlDebug) | ||||
| 	sessionToken := "TESTTOKEN" | ||||
| 	wsserver := ws.NewServer(auth, sessionToken, false, logger) | ||||
| 	wsserver := ws.NewServer(auth, sessionToken, false, logger, store) | ||||
| 	webhook := webhook.NewClient(&cfg, logger) | ||||
| 	metricsService := metrics.NewMetrics(metrics.InstanceInfo{}) | ||||
|  | ||||
| @@ -49,6 +48,7 @@ func SetupTestHelper(t *testing.T) (*TestHelper, func()) { | ||||
| 	app2 := New(&cfg, wsserver, appServices) | ||||
|  | ||||
| 	tearDown := func() { | ||||
| 		app2.Shutdown() | ||||
| 		if logger != nil { | ||||
| 			_ = logger.Shutdown() | ||||
| 		} | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import ( | ||||
| 	"github.com/krolaw/zipstream" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| @@ -84,7 +83,7 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { | ||||
| 				continue | ||||
| 			} | ||||
| 			// save file with original filename so it matches name in image block. | ||||
| 			filePath := filepath.Join(opt.WorkspaceID, boardID, filename) | ||||
| 			filePath := filepath.Join(opt.TeamID, boardID, filename) | ||||
| 			_, err := a.filesBackend.WriteFile(zr, filePath) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("cannot import file %s for board %s: %w", filename, dir, err) | ||||
| @@ -103,7 +102,10 @@ func (a *App) ImportArchive(r io.Reader, opt model.ImportArchiveOptions) error { | ||||
| func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (string, error) { | ||||
| 	// TODO: Stream this once `model.GenerateBlockIDs` can take a stream of blocks. | ||||
| 	//       We don't want to load the whole file in memory, even though it's a single board. | ||||
| 	blocks := make([]model.Block, 0, 10) | ||||
| 	boardsAndBlocks := &model.BoardsAndBlocks{ | ||||
| 		Blocks: make([]model.Block, 0, 10), | ||||
| 		Boards: make([]*model.Board, 0, 10), | ||||
| 	} | ||||
| 	lineReader := bufio.NewReader(r) | ||||
|  | ||||
| 	userID := opt.ModifiedBy | ||||
| @@ -137,7 +139,16 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str | ||||
| 					} | ||||
| 					block.ModifiedBy = userID | ||||
| 					block.UpdateAt = now | ||||
| 					blocks = append(blocks, block) | ||||
| 					boardsAndBlocks.Blocks = append(boardsAndBlocks.Blocks, block) | ||||
| 				case "board": | ||||
| 					var board model.Board | ||||
| 					if err2 := json.Unmarshal(archiveLine.Data, &board); err2 != nil { | ||||
| 						return "", fmt.Errorf("invalid block in archive line %d: %w", lineNum, err2) | ||||
| 					} | ||||
| 					board.ModifiedBy = userID | ||||
| 					board.UpdateAt = now | ||||
| 					board.TeamID = opt.TeamID | ||||
| 					boardsAndBlocks.Boards = append(boardsAndBlocks.Boards, &board) | ||||
| 				default: | ||||
| 					return "", model.NewErrUnsupportedArchiveLineType(lineNum, archiveLine.Type) | ||||
| 				} | ||||
| @@ -154,36 +165,33 @@ func (a *App) ImportBoardJSONL(r io.Reader, opt model.ImportArchiveOptions) (str | ||||
| 	} | ||||
|  | ||||
| 	modInfoCache := make(map[string]interface{}) | ||||
| 	modBlocks := make([]model.Block, 0, len(blocks)) | ||||
| 	for _, block := range blocks { | ||||
| 		b := block | ||||
| 		if opt.BlockModifier != nil && !opt.BlockModifier(&b, modInfoCache) { | ||||
| 	modBoards := make([]*model.Board, 0, len(boardsAndBlocks.Boards)) | ||||
| 	for _, board := range boardsAndBlocks.Boards { | ||||
| 		b := *board | ||||
| 		if opt.BoardModifier != nil && !opt.BoardModifier(&b, modInfoCache) { | ||||
| 			a.logger.Debug("skipping insert block per block modifier", | ||||
| 				mlog.String("blockID", block.ID), | ||||
| 				mlog.String("block_type", block.Type.String()), | ||||
| 				mlog.String("blockID", board.ID), | ||||
| 			) | ||||
| 			continue | ||||
| 		} | ||||
| 		modBlocks = append(modBlocks, b) | ||||
| 	} | ||||
|  | ||||
| 	blocks = model.GenerateBlockIDs(modBlocks, a.logger) | ||||
|  | ||||
| 	container := store.Container{ | ||||
| 		WorkspaceID: opt.WorkspaceID, | ||||
| 		modBoards = append(modBoards, &b) | ||||
| 	} | ||||
| 	boardsAndBlocks.Boards = modBoards | ||||
|  | ||||
| 	var err error | ||||
| 	blocks, err = a.InsertBlocks(container, blocks, opt.ModifiedBy, false) | ||||
| 	boardsAndBlocks, err = model.GenerateBoardsAndBlocksIDs(boardsAndBlocks, a.logger) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error inserting archive blocks: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	boardsAndBlocks, err = a.CreateBoardsAndBlocks(boardsAndBlocks, opt.ModifiedBy, false) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("error inserting archive blocks: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	// find new board id | ||||
| 	for _, block := range blocks { | ||||
| 		if block.Type == model.TypeBoard { | ||||
| 			return block.ID, nil | ||||
| 		} | ||||
| 	for _, board := range boardsAndBlocks.Boards { | ||||
| 		return board.ID, nil | ||||
| 	} | ||||
| 	return "", fmt.Errorf("missing board in archive: %w", model.ErrInvalidBoardBlock) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| package app | ||||
|  | ||||
| import "github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| import ( | ||||
| 	"context" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| // initialize is called when the App is first created. | ||||
| func (a *App) initialize(skipTemplateInit bool) { | ||||
| @@ -10,3 +14,13 @@ func (a *App) initialize(skipTemplateInit bool) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (a *App) Shutdown() { | ||||
| 	if a.blockChangeNotifier != nil { | ||||
| 		ctx, cancel := context.WithTimeout(context.Background(), blockChangeNotifierShutdownTimeout) | ||||
| 		defer cancel() | ||||
| 		if !a.blockChangeNotifier.Shutdown(ctx) { | ||||
| 			a.logger.Warn("blockChangeNotifier shutdown timed out") | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -21,17 +20,12 @@ const ( | ||||
|  | ||||
| var ( | ||||
| 	errUnableToFindWelcomeBoard = errors.New("unable to find welcome board in newly created blocks") | ||||
| 	errCannotCreateBoard        = errors.New("new board wasn't created") | ||||
| ) | ||||
|  | ||||
| func (a *App) PrepareOnboardingTour(userID string) (string, string, error) { | ||||
| 	// create a private workspace for the user | ||||
| 	workspaceID, err := a.store.CreatePrivateWorkspace(userID) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| func (a *App) PrepareOnboardingTour(userID string, teamID string) (string, string, error) { | ||||
| 	// copy the welcome board into this workspace | ||||
| 	boardID, err := a.createWelcomeBoard(userID, workspaceID) | ||||
| 	boardID, err := a.createWelcomeBoard(userID, teamID) | ||||
| 	if err != nil { | ||||
| 		return "", "", err | ||||
| 	} | ||||
| @@ -48,18 +42,18 @@ func (a *App) PrepareOnboardingTour(userID string) (string, string, error) { | ||||
| 		return "", "", err | ||||
| 	} | ||||
|  | ||||
| 	return workspaceID, boardID, nil | ||||
| 	return teamID, boardID, nil | ||||
| } | ||||
|  | ||||
| func (a *App) getOnboardingBoardID() (string, error) { | ||||
| 	blocks, err := a.store.GetDefaultTemplateBlocks() | ||||
| 	boards, err := a.store.GetTemplateBoards(globalTeamID) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	var onboardingBoardID string | ||||
| 	for _, block := range blocks { | ||||
| 		if block.Type == model.TypeBoard && block.Title == WelcomeBoardTitle { | ||||
| 	for _, block := range boards { | ||||
| 		if block.Title == WelcomeBoardTitle { | ||||
| 			onboardingBoardID = block.ID | ||||
| 			break | ||||
| 		} | ||||
| @@ -72,42 +66,20 @@ func (a *App) getOnboardingBoardID() (string, error) { | ||||
| 	return onboardingBoardID, nil | ||||
| } | ||||
|  | ||||
| func (a *App) createWelcomeBoard(userID, workspaceID string) (string, error) { | ||||
| func (a *App) createWelcomeBoard(userID, teamID string) (string, error) { | ||||
| 	onboardingBoardID, err := a.getOnboardingBoardID() | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	blocks, err := a.GetSubTree(store.Container{WorkspaceID: "0"}, onboardingBoardID, 3) | ||||
| 	bab, _, err := a.DuplicateBoard(onboardingBoardID, userID, teamID, false) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	blocks = model.GenerateBlockIDs(blocks, a.logger) | ||||
|  | ||||
| 	if errUpdateFileIDs := a.CopyCardFiles(onboardingBoardID, workspaceID, blocks); errUpdateFileIDs != nil { | ||||
| 		return "", errUpdateFileIDs | ||||
| 	if len(bab.Boards) != 1 { | ||||
| 		return "", errCannotCreateBoard | ||||
| 	} | ||||
|  | ||||
| 	// we're copying from a global template, so we need to set the | ||||
| 	// `isTemplate` flag to false on the board | ||||
| 	var welcomeBoardID string | ||||
| 	for i := range blocks { | ||||
| 		if blocks[i].Type == model.TypeBoard { | ||||
| 			blocks[i].Fields["isTemplate"] = false | ||||
|  | ||||
| 			if blocks[i].Title == WelcomeBoardTitle { | ||||
| 				welcomeBoardID = blocks[i].ID | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	model.StampModificationMetadata(userID, blocks, nil) | ||||
| 	_, err = a.InsertBlocks(store.Container{WorkspaceID: workspaceID}, blocks, userID, false) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return welcomeBoardID, nil | ||||
| 	return bab.Boards[0].ID, nil | ||||
| } | ||||
|   | ||||
| @@ -3,44 +3,32 @@ package app | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	testTeamID = "team_id" | ||||
| ) | ||||
|  | ||||
| func TestPrepareOnboardingTour(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	t.Run("base case", func(t *testing.T) { | ||||
| 		welcomeBoard := model.Block{ | ||||
| 			ID:    "block_id_1", | ||||
| 			Type:  model.TypeBoard, | ||||
| 			Title: "Welcome to Boards!", | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"isTemplate": true, | ||||
| 			}, | ||||
| 		teamID := testTeamID | ||||
| 		userID := "user_id_1" | ||||
| 		welcomeBoard := model.Board{ | ||||
| 			ID:         "board_id_1", | ||||
| 			Title:      "Welcome to Boards!", | ||||
| 			TeamID:     "0", | ||||
| 			IsTemplate: true, | ||||
| 		} | ||||
|  | ||||
| 		blocks := []model.Block{welcomeBoard} | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetSubTree3( | ||||
| 			store.Container{WorkspaceID: "0"}, | ||||
| 			"block_id_1", | ||||
| 			gomock.Any(), | ||||
| 		).Return([]model.Block{welcomeBoard}, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().InsertBlock( | ||||
| 			store.Container{WorkspaceID: "workspace_id_1"}, | ||||
| 			gomock.Any(), | ||||
| 			"user_id_1", | ||||
| 		).Return(nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().CreatePrivateWorkspace("user_id_1").Return("workspace_id_1", nil) | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) | ||||
| 		th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false).Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, | ||||
| 			nil, nil) | ||||
| 		th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil) | ||||
|  | ||||
| 		userPropPatch := model.UserPropPatch{ | ||||
| 			UpdatedFields: map[string]string{ | ||||
| @@ -50,11 +38,11 @@ func TestPrepareOnboardingTour(t *testing.T) { | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		th.Store.EXPECT().PatchUserProps("user_id_1", userPropPatch).Return(nil) | ||||
| 		th.Store.EXPECT().PatchUserProps(userID, userPropPatch).Return(nil) | ||||
|  | ||||
| 		workspaceID, boardID, err := th.App.PrepareOnboardingTour("user_id_1") | ||||
| 		teamID, boardID, err := th.App.PrepareOnboardingTour(userID, teamID) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.Equal(t, "workspace_id_1", workspaceID) | ||||
| 		assert.Equal(t, testTeamID, teamID) | ||||
| 		assert.NotEmpty(t, boardID) | ||||
| 	}) | ||||
| } | ||||
| @@ -64,88 +52,41 @@ func TestCreateWelcomeBoard(t *testing.T) { | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	t.Run("base case", func(t *testing.T) { | ||||
| 		welcomeBoard := model.Block{ | ||||
| 			ID:    "block_id_1", | ||||
| 			Type:  model.TypeBoard, | ||||
| 			Title: "Welcome to Boards!", | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"isTemplate": true, | ||||
| 			}, | ||||
| 		teamID := testTeamID | ||||
| 		userID := "user_id_1" | ||||
| 		welcomeBoard := model.Board{ | ||||
| 			ID:         "board_id_1", | ||||
| 			Title:      "Welcome to Boards!", | ||||
| 			TeamID:     "0", | ||||
| 			IsTemplate: true, | ||||
| 		} | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) | ||||
| 		th.Store.EXPECT().DuplicateBoard(welcomeBoard.ID, userID, teamID, false). | ||||
| 			Return(&model.BoardsAndBlocks{Boards: []*model.Board{&welcomeBoard}}, nil, nil) | ||||
| 		th.Store.EXPECT().GetMembersForBoard(welcomeBoard.ID).Return([]*model.BoardMember{}, nil) | ||||
|  | ||||
| 		blocks := []model.Block{welcomeBoard} | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetSubTree3( | ||||
| 			store.Container{WorkspaceID: "0"}, | ||||
| 			"block_id_1", | ||||
| 			gomock.Any(), | ||||
| 		).Return([]model.Block{welcomeBoard}, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().InsertBlock( | ||||
| 			store.Container{WorkspaceID: "workspace_id_1"}, | ||||
| 			gomock.Any(), | ||||
| 			"user_id_1", | ||||
| 		).Return(nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetBlock(gomock.Any(), "block_id_1").Return(&welcomeBoard, nil) | ||||
|  | ||||
| 		boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") | ||||
| 		boardID, err := th.App.createWelcomeBoard(userID, teamID) | ||||
| 		assert.Nil(t, err) | ||||
| 		assert.NotEmpty(t, boardID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("template doesn't contain a board", func(t *testing.T) { | ||||
| 		welcomeBoard := model.Block{ | ||||
| 			ID:    "block_id_1", | ||||
| 			Type:  model.TypeComment, | ||||
| 			Title: "Welcome to Boards!", | ||||
| 		} | ||||
| 		blocks := []model.Block{welcomeBoard} | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetSubTree3( | ||||
| 			store.Container{WorkspaceID: "0"}, | ||||
| 			"buixxjic3xjfkieees4iafdrznc", | ||||
| 			gomock.Any(), | ||||
| 		).Return([]model.Block{welcomeBoard}, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().InsertBlock( | ||||
| 			store.Container{WorkspaceID: "workspace_id_1"}, | ||||
| 			gomock.Any(), | ||||
| 			"user_id_1", | ||||
| 		).Return(nil) | ||||
|  | ||||
| 		boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") | ||||
| 		teamID := testTeamID | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil) | ||||
| 		boardID, err := th.App.createWelcomeBoard("user_id_1", teamID) | ||||
| 		assert.Error(t, err) | ||||
| 		assert.Empty(t, boardID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("template doesn't contain the welcome board", func(t *testing.T) { | ||||
| 		welcomeBoard := model.Block{ | ||||
| 			ID:    "block_id_1", | ||||
| 			Type:  model.TypeBoard, | ||||
| 			Title: "Jean luc Picard", | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"isTemplate": true, | ||||
| 			}, | ||||
| 		teamID := testTeamID | ||||
| 		welcomeBoard := model.Board{ | ||||
| 			ID:         "board_id_1", | ||||
| 			Title:      "Other template", | ||||
| 			TeamID:     teamID, | ||||
| 			IsTemplate: true, | ||||
| 		} | ||||
|  | ||||
| 		blocks := []model.Block{welcomeBoard} | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetSubTree3( | ||||
| 			store.Container{WorkspaceID: "0"}, | ||||
| 			"buixxjic3xjfkieees4iafdrznc", | ||||
| 			gomock.Any(), | ||||
| 		).Return([]model.Block{welcomeBoard}, nil) | ||||
|  | ||||
| 		th.Store.EXPECT().InsertBlock( | ||||
| 			store.Container{WorkspaceID: "workspace_id_1"}, | ||||
| 			gomock.Any(), | ||||
| 			"user_id_1", | ||||
| 		).Return(nil) | ||||
|  | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) | ||||
| 		boardID, err := th.App.createWelcomeBoard("user_id_1", "workspace_id_1") | ||||
| 		assert.Error(t, err) | ||||
| 		assert.Empty(t, boardID) | ||||
| @@ -157,24 +98,13 @@ func TestGetOnboardingBoardID(t *testing.T) { | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	t.Run("base case", func(t *testing.T) { | ||||
| 		board := model.Block{ | ||||
| 			ID:    "board_id_1", | ||||
| 			Type:  model.TypeBoard, | ||||
| 			Title: "Welcome to Boards!", | ||||
| 		welcomeBoard := model.Board{ | ||||
| 			ID:         "board_id_1", | ||||
| 			Title:      "Welcome to Boards!", | ||||
| 			TeamID:     "0", | ||||
| 			IsTemplate: true, | ||||
| 		} | ||||
|  | ||||
| 		card := model.Block{ | ||||
| 			ID:       "card_id_1", | ||||
| 			Type:     model.TypeCard, | ||||
| 			ParentID: board.ID, | ||||
| 		} | ||||
|  | ||||
| 		blocks := []model.Block{ | ||||
| 			board, | ||||
| 			card, | ||||
| 		} | ||||
|  | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) | ||||
|  | ||||
| 		onboardingBoardID, err := th.App.getOnboardingBoardID() | ||||
| 		assert.NoError(t, err) | ||||
| @@ -182,9 +112,7 @@ func TestGetOnboardingBoardID(t *testing.T) { | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no blocks found", func(t *testing.T) { | ||||
| 		blocks := []model.Block{} | ||||
|  | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{}, nil) | ||||
|  | ||||
| 		onboardingBoardID, err := th.App.getOnboardingBoardID() | ||||
| 		assert.Error(t, err) | ||||
| @@ -192,24 +120,13 @@ func TestGetOnboardingBoardID(t *testing.T) { | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("onboarding board doesn't exists", func(t *testing.T) { | ||||
| 		board := model.Block{ | ||||
| 			ID:    "board_id_1", | ||||
| 			Type:  model.TypeBoard, | ||||
| 			Title: "Some board title", | ||||
| 		welcomeBoard := model.Board{ | ||||
| 			ID:         "board_id_1", | ||||
| 			Title:      "Other template", | ||||
| 			TeamID:     "0", | ||||
| 			IsTemplate: true, | ||||
| 		} | ||||
|  | ||||
| 		card := model.Block{ | ||||
| 			ID:       "card_id_1", | ||||
| 			Type:     model.TypeCard, | ||||
| 			ParentID: board.ID, | ||||
| 		} | ||||
|  | ||||
| 		blocks := []model.Block{ | ||||
| 			board, | ||||
| 			card, | ||||
| 		} | ||||
|  | ||||
| 		th.Store.EXPECT().GetDefaultTemplateBlocks().Return(blocks, nil) | ||||
| 		th.Store.EXPECT().GetTemplateBoards("0").Return([]*model.Board{&welcomeBoard}, nil) | ||||
|  | ||||
| 		onboardingBoardID, err := th.App.getOnboardingBoardID() | ||||
| 		assert.Error(t, err) | ||||
|   | ||||
| @@ -5,11 +5,10 @@ import ( | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| ) | ||||
|  | ||||
| func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, error) { | ||||
| 	sharing, err := a.store.GetSharing(c, rootID) | ||||
| func (a *App) GetSharing(boardID string) (*model.Sharing, error) { | ||||
| 	sharing, err := a.store.GetSharing(boardID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| @@ -19,6 +18,6 @@ func (a *App) GetSharing(c store.Container, rootID string) (*model.Sharing, erro | ||||
| 	return sharing, nil | ||||
| } | ||||
|  | ||||
| func (a *App) UpsertSharing(c store.Container, sharing model.Sharing) error { | ||||
| 	return a.store.UpsertSharing(c, sharing) | ||||
| func (a *App) UpsertSharing(sharing model.Sharing) error { | ||||
| 	return a.store.UpsertSharing(sharing) | ||||
| } | ||||
|   | ||||
| @@ -4,9 +4,7 @@ import ( | ||||
| 	"database/sql" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	st "github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| @@ -16,10 +14,6 @@ func TestGetSharing(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: utils.NewID(utils.IDTypeWorkspace), | ||||
| 	} | ||||
|  | ||||
| 	t.Run("should get a sharing successfully", func(t *testing.T) { | ||||
| 		want := &model.Sharing{ | ||||
| 			ID:         utils.NewID(utils.IDTypeBlock), | ||||
| @@ -28,9 +22,9 @@ func TestGetSharing(t *testing.T) { | ||||
| 			ModifiedBy: "otherid", | ||||
| 			UpdateAt:   utils.GetMillis(), | ||||
| 		} | ||||
| 		th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return(want, nil) | ||||
| 		th.Store.EXPECT().GetSharing("test-id").Return(want, nil) | ||||
|  | ||||
| 		result, err := th.App.GetSharing(container, "test-id") | ||||
| 		result, err := th.App.GetSharing("test-id") | ||||
| 		require.NoError(t, err) | ||||
|  | ||||
| 		require.Equal(t, result, want) | ||||
| @@ -38,11 +32,11 @@ func TestGetSharing(t *testing.T) { | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should fail to get a sharing", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return( | ||||
| 		th.Store.EXPECT().GetSharing("test-id").Return( | ||||
| 			nil, | ||||
| 			errors.New("sharing not found"), | ||||
| 		) | ||||
| 		result, err := th.App.GetSharing(container, "test-id") | ||||
| 		result, err := th.App.GetSharing("test-id") | ||||
|  | ||||
| 		require.Nil(t, result) | ||||
| 		require.Error(t, err) | ||||
| @@ -50,11 +44,11 @@ func TestGetSharing(t *testing.T) { | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should return a tuple of nil", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().GetSharing(gomock.Eq(container), gomock.Eq("test-id")).Return( | ||||
| 		th.Store.EXPECT().GetSharing("test-id").Return( | ||||
| 			nil, | ||||
| 			sql.ErrNoRows, | ||||
| 		) | ||||
| 		result, err := th.App.GetSharing(container, "test-id") | ||||
| 		result, err := th.App.GetSharing("test-id") | ||||
|  | ||||
| 		require.Nil(t, result) | ||||
| 		require.NoError(t, err) | ||||
| @@ -65,9 +59,6 @@ func TestUpsertSharing(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	container := st.Container{ | ||||
| 		WorkspaceID: utils.NewID(utils.IDTypeWorkspace), | ||||
| 	} | ||||
| 	sharing := model.Sharing{ | ||||
| 		ID:         utils.NewID(utils.IDTypeBlock), | ||||
| 		Enabled:    true, | ||||
| @@ -77,15 +68,15 @@ func TestUpsertSharing(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	t.Run("should success to upsert sharing", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(nil) | ||||
| 		err := th.App.UpsertSharing(container, sharing) | ||||
| 		th.Store.EXPECT().UpsertSharing(sharing).Return(nil) | ||||
| 		err := th.App.UpsertSharing(sharing) | ||||
|  | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("should fail to upsert a sharing", func(t *testing.T) { | ||||
| 		th.Store.EXPECT().UpsertSharing(gomock.Eq(container), gomock.Eq(sharing)).Return(errors.New("sharing not found")) | ||||
| 		err := th.App.UpsertSharing(container, sharing) | ||||
| 		th.Store.EXPECT().UpsertSharing(sharing).Return(errors.New("sharing not found")) | ||||
| 		err := th.App.UpsertSharing(sharing) | ||||
|  | ||||
| 		require.Error(t, err) | ||||
| 		require.Equal(t, "sharing not found", err.Error()) | ||||
|   | ||||
| @@ -2,41 +2,40 @@ package app | ||||
|  | ||||
| import ( | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| ) | ||||
|  | ||||
| func (a *App) CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) { | ||||
| 	sub, err := a.store.CreateSubscription(c, sub) | ||||
| func (a *App) CreateSubscription(sub *model.Subscription) (*model.Subscription, error) { | ||||
| 	sub, err := a.store.CreateSubscription(sub) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	a.notifySubscriptionChanged(c, sub) | ||||
| 	a.notifySubscriptionChanged(sub) | ||||
|  | ||||
| 	return sub, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DeleteSubscription(c store.Container, blockID string, subscriberID string) (*model.Subscription, error) { | ||||
| 	sub, err := a.store.GetSubscription(c, blockID, subscriberID) | ||||
| func (a *App) DeleteSubscription(blockID string, subscriberID string) (*model.Subscription, error) { | ||||
| 	sub, err := a.store.GetSubscription(blockID, subscriberID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := a.store.DeleteSubscription(c, blockID, subscriberID); err != nil { | ||||
| 	if err := a.store.DeleteSubscription(blockID, subscriberID); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	sub.DeleteAt = utils.GetMillis() | ||||
| 	a.notifySubscriptionChanged(c, sub) | ||||
| 	a.notifySubscriptionChanged(sub) | ||||
|  | ||||
| 	return sub, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetSubscriptions(c store.Container, subscriberID string) ([]*model.Subscription, error) { | ||||
| 	return a.store.GetSubscriptions(c, subscriberID) | ||||
| func (a *App) GetSubscriptions(subscriberID string) ([]*model.Subscription, error) { | ||||
| 	return a.store.GetSubscriptions(subscriberID) | ||||
| } | ||||
|  | ||||
| func (a *App) notifySubscriptionChanged(c store.Container, subscription *model.Subscription) { | ||||
| func (a *App) notifySubscriptionChanged(subscription *model.Subscription) { | ||||
| 	if a.notifications == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	a.notifications.BroadcastSubscriptionChange(c.WorkspaceID, subscription) | ||||
| 	a.notifications.BroadcastSubscriptionChange(subscription) | ||||
| } | ||||
|   | ||||
							
								
								
									
										68
									
								
								server/app/teams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								server/app/teams.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *App) GetRootTeam() (*model.Team, error) { | ||||
| 	teamID := "0" | ||||
| 	team, _ := a.store.GetTeam(teamID) | ||||
| 	if team == nil { | ||||
| 		team = &model.Team{ | ||||
| 			ID:          teamID, | ||||
| 			SignupToken: utils.NewID(utils.IDTypeToken), | ||||
| 		} | ||||
| 		err := a.store.UpsertTeamSignupToken(*team) | ||||
| 		if err != nil { | ||||
| 			a.logger.Error("Unable to initialize team", mlog.Err(err)) | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		team, err = a.store.GetTeam(teamID) | ||||
| 		if err != nil { | ||||
| 			a.logger.Error("Unable to get initialized team", mlog.Err(err)) | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		a.logger.Info("initialized team") | ||||
| 	} | ||||
|  | ||||
| 	return team, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetTeam(id string) (*model.Team, error) { | ||||
| 	team, err := a.store.GetTeam(id) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return team, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetTeamsForUser(userID string) ([]*model.Team, error) { | ||||
| 	return a.store.GetTeamsForUser(userID) | ||||
| } | ||||
|  | ||||
| func (a *App) DoesUserHaveTeamAccess(userID string, teamID string) bool { | ||||
| 	return a.auth.DoesUserHaveTeamAccess(userID, teamID) | ||||
| } | ||||
|  | ||||
| func (a *App) UpsertTeamSettings(team model.Team) error { | ||||
| 	return a.store.UpsertTeamSettings(team) | ||||
| } | ||||
|  | ||||
| func (a *App) UpsertTeamSignupToken(team model.Team) error { | ||||
| 	return a.store.UpsertTeamSignupToken(team) | ||||
| } | ||||
|  | ||||
| func (a *App) GetTeamCount() (int64, error) { | ||||
| 	return a.store.GetTeamCount() | ||||
| } | ||||
							
								
								
									
										152
									
								
								server/app/teams_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								server/app/teams_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| var errInvalidTeam = errors.New("invalid team id") | ||||
|  | ||||
| var mockTeam = &model.Team{ | ||||
| 	ID:    "mock-team-id", | ||||
| 	Title: "MockTeam", | ||||
| } | ||||
|  | ||||
| var errUpsertSignupToken = errors.New("upsert error") | ||||
|  | ||||
| func TestGetRootTeam(t *testing.T) { | ||||
| 	var newRootTeam = &model.Team{ | ||||
| 		ID:    "0", | ||||
| 		Title: "NewRootTeam", | ||||
| 	} | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		title                    string | ||||
| 		teamToReturnBeforeUpsert *model.Team | ||||
| 		teamToReturnAfterUpsert  *model.Team | ||||
| 		isError                  bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"Success, Return new root team, when root team returned by mockstore is nil", | ||||
| 			nil, | ||||
| 			newRootTeam, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Success, Return existing root team, when root team returned by mockstore is notnil", | ||||
| 			newRootTeam, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Fail, Return nil, when root team returned by mockstore is nil, and upsert new root team fails", | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.title, func(t *testing.T) { | ||||
| 			th, tearDown := SetupTestHelper(t) | ||||
| 			defer tearDown() | ||||
| 			th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnBeforeUpsert, nil) | ||||
| 			th.Store.EXPECT().UpsertTeamSignupToken(gomock.Any()).DoAndReturn( | ||||
| 				func(arg0 model.Team) error { | ||||
| 					if tc.isError { | ||||
| 						return errUpsertSignupToken | ||||
| 					} | ||||
| 					th.Store.EXPECT().GetTeam("0").Return(tc.teamToReturnAfterUpsert, nil) | ||||
| 					return nil | ||||
| 				}) | ||||
| 			rootTeam, err := th.App.GetRootTeam() | ||||
|  | ||||
| 			if tc.isError { | ||||
| 				require.Error(t, err) | ||||
| 			} else { | ||||
| 				assert.NotNil(t, rootTeam.ID) | ||||
| 				assert.NotNil(t, rootTeam.SignupToken) | ||||
| 				assert.Equal(t, "", rootTeam.ModifiedBy) | ||||
| 				assert.Equal(t, int64(0), rootTeam.UpdateAt) | ||||
| 				assert.Equal(t, "NewRootTeam", rootTeam.Title) | ||||
| 				require.NoError(t, err) | ||||
| 				require.NotNil(t, rootTeam) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetTeam(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		title   string | ||||
| 		teamID  string | ||||
| 		isError bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"Success, Return new root team, when team returned by mockstore is not nil", | ||||
| 			"mock-team-id", | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Success, Return nil, when get team returns an sql error", | ||||
| 			"team-not-available-id", | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Fail, Return nil, when get team by mockstore returns an error", | ||||
| 			"invalid-team-id", | ||||
| 			true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	th.Store.EXPECT().GetTeam("mock-team-id").Return(mockTeam, nil) | ||||
| 	th.Store.EXPECT().GetTeam("invalid-team-id").Return(nil, errInvalidTeam) | ||||
| 	th.Store.EXPECT().GetTeam("team-not-available-id").Return(nil, sql.ErrNoRows) | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.title, func(t *testing.T) { | ||||
| 			t.Log(tc.title) | ||||
| 			team, err := th.App.GetTeam(tc.teamID) | ||||
|  | ||||
| 			if tc.isError { | ||||
| 				require.Error(t, err) | ||||
| 			} else if tc.teamID != "team-not-available-id" { | ||||
| 				assert.NotNil(t, team.ID) | ||||
| 				assert.NotNil(t, team.SignupToken) | ||||
| 				assert.Equal(t, "mock-team-id", team.ID) | ||||
| 				assert.Equal(t, "", team.ModifiedBy) | ||||
| 				assert.Equal(t, int64(0), team.UpdateAt) | ||||
| 				assert.Equal(t, "MockTeam", team.Title) | ||||
| 				require.NoError(t, err) | ||||
| 				require.NotNil(t, team) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestTeamOperations(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	th.Store.EXPECT().UpsertTeamSettings(*mockTeam).Return(nil) | ||||
| 	th.Store.EXPECT().UpsertTeamSignupToken(*mockTeam).Return(nil) | ||||
| 	th.Store.EXPECT().GetTeamCount().Return(int64(10), nil) | ||||
|  | ||||
| 	errUpsertTeamSettings := th.App.UpsertTeamSettings(*mockTeam) | ||||
| 	assert.NoError(t, errUpsertTeamSettings) | ||||
|  | ||||
| 	errUpsertTeamSignupToken := th.App.UpsertTeamSignupToken(*mockTeam) | ||||
| 	assert.NoError(t, errUpsertTeamSignupToken) | ||||
|  | ||||
| 	count, errGetTeamCount := th.App.GetTeamCount() | ||||
| 	assert.NoError(t, errGetTeamCount) | ||||
| 	assert.Equal(t, int64(10), count) | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| @@ -14,21 +14,26 @@ import ( | ||||
|  | ||||
| const ( | ||||
| 	defaultTemplateVersion = 2 | ||||
| 	globalTeamID           = "0" | ||||
| ) | ||||
|  | ||||
| //go:embed templates.boardarchive | ||||
| var defTemplates []byte | ||||
|  | ||||
| // initializeTemplates imports default templates if the blocks table is empty. | ||||
| func (a *App) InitTemplates() error { | ||||
| 	return a.initializeTemplates() | ||||
| } | ||||
|  | ||||
| // initializeTemplates imports default templates if the boards table is empty. | ||||
| func (a *App) initializeTemplates() error { | ||||
| 	blocks, err := a.store.GetDefaultTemplateBlocks() | ||||
| 	boards, err := a.store.GetTemplateBoards(globalTeamID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("cannot initialize templates: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	a.logger.Debug("Fetched template blocks", mlog.Int("count", len(blocks))) | ||||
| 	a.logger.Debug("Fetched template boards", mlog.Int("count", len(boards))) | ||||
|  | ||||
| 	isNeeded, reason := a.isInitializationNeeded(blocks) | ||||
| 	isNeeded, reason := a.isInitializationNeeded(boards) | ||||
| 	if !isNeeded { | ||||
| 		a.logger.Debug("Template import not needed, skipping") | ||||
| 		return nil | ||||
| @@ -36,67 +41,55 @@ func (a *App) initializeTemplates() error { | ||||
|  | ||||
| 	a.logger.Debug("Importing new default templates", mlog.String("reason", reason)) | ||||
|  | ||||
| 	if err := a.store.RemoveDefaultTemplates(blocks); err != nil { | ||||
| 		return fmt.Errorf("cannot remove old templates: %w", err) | ||||
| 	// Remove in case of newer Templates | ||||
| 	if err = a.store.RemoveDefaultTemplates(boards); err != nil { | ||||
| 		return fmt.Errorf("cannot remove old template boards: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	r := bytes.NewReader(defTemplates) | ||||
|  | ||||
| 	opt := model.ImportArchiveOptions{ | ||||
| 		WorkspaceID:   "0", | ||||
| 		TeamID:        globalTeamID, | ||||
| 		ModifiedBy:    "system", | ||||
| 		BlockModifier: fixTemplateBlock, | ||||
| 		BoardModifier: fixTemplateBoard, | ||||
| 	} | ||||
|  | ||||
| 	return a.ImportArchive(r, opt) | ||||
| 	if err = a.ImportArchive(r, opt); err != nil { | ||||
| 		return fmt.Errorf("cannot initialize global templates for team %s: %w", globalTeamID, err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // isInitializationNeeded returns true if the blocks table contains no default templates, | ||||
| // or contains at least one default template with an old version number. | ||||
| func (a *App) isInitializationNeeded(blocks []model.Block) (bool, string) { | ||||
| 	if len(blocks) == 0 { | ||||
| func (a *App) isInitializationNeeded(boards []*model.Board) (bool, string) { | ||||
| 	if len(boards) == 0 { | ||||
| 		return true, "no default templates found" | ||||
| 	} | ||||
|  | ||||
| 	// look for any template blocks with the wrong version number (or no version #). | ||||
| 	for _, block := range blocks { | ||||
| 		v, ok := block.Fields["templateVer"] | ||||
| 		if !ok { | ||||
| 			return true, "block missing templateVer" | ||||
| 	// look for any built-in template boards with the wrong version number (or no version #). | ||||
| 	for _, board := range boards { | ||||
| 		// if not built-in board...skip | ||||
| 		if board.CreatedBy != "system" { | ||||
| 			continue | ||||
| 		} | ||||
| 		version, ok := v.(float64) | ||||
| 		if !ok { | ||||
| 			return true, "templateVer NaN" | ||||
| 		} | ||||
| 		if version < defaultTemplateVersion { | ||||
| 			return true, "templateVer too old" | ||||
| 		if board.TemplateVersion < defaultTemplateVersion { | ||||
| 			return true, "template_version too old" | ||||
| 		} | ||||
| 	} | ||||
| 	return false, "" | ||||
| } | ||||
|  | ||||
| // fixTemplateBlock fixes a block to be inserted as part of a template. | ||||
| func fixTemplateBlock(block *model.Block, cache map[string]interface{}) bool { | ||||
| 	// cache contains ids of skipped blocks. Ensure their children are skipped as well. | ||||
| 	if _, ok := cache[block.ParentID]; ok { | ||||
| 		cache[block.ID] = struct{}{} | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| // fixTemplateBoard fixes a board to be inserted as part of a template. | ||||
| func fixTemplateBoard(board *model.Board, cache map[string]interface{}) bool { | ||||
| 	// filter out template blocks; we only want the non-template | ||||
| 	// blocks which we will turn into default template blocks. | ||||
| 	if b, ok := block.Fields["isTemplate"]; ok { | ||||
| 		if val, ok := b.(bool); ok && val { | ||||
| 			cache[block.ID] = struct{}{} | ||||
| 			return false | ||||
| 		} | ||||
| 	if board.IsTemplate { | ||||
| 		cache[board.ID] = struct{}{} | ||||
| 	} | ||||
|  | ||||
| 	// remove '(NEW)' from title & force template flag | ||||
| 	if block.Type == model.TypeBoard { | ||||
| 		block.Title = strings.ReplaceAll(block.Title, "(NEW)", "") | ||||
| 		block.Fields["isTemplate"] = true | ||||
| 		block.Fields["templateVer"] = defaultTemplateVersion | ||||
| 	} | ||||
| 	board.Title = strings.ReplaceAll(board.Title, "(NEW)", "") | ||||
| 	board.IsTemplate = true | ||||
| 	board.TemplateVersion = defaultTemplateVersion | ||||
| 	return true | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,12 @@ package app | ||||
|  | ||||
| import "github.com/mattermost/focalboard/server/model" | ||||
|  | ||||
| func (a *App) GetWorkspaceUsers(workspaceID string) ([]*model.User, error) { | ||||
| 	return a.store.GetUsersByWorkspace(workspaceID) | ||||
| func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) { | ||||
| 	return a.store.GetUsersByTeam(teamID) | ||||
| } | ||||
|  | ||||
| func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) { | ||||
| 	return a.store.SearchUsersByTeam(teamID, searchQuery) | ||||
| } | ||||
|  | ||||
| func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) { | ||||
|   | ||||
| @@ -1,67 +0,0 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func (a *App) GetRootWorkspace() (*model.Workspace, error) { | ||||
| 	workspaceID := "0" | ||||
| 	workspace, _ := a.store.GetWorkspace(workspaceID) | ||||
| 	if workspace == nil { | ||||
| 		workspace = &model.Workspace{ | ||||
| 			ID:          workspaceID, | ||||
| 			SignupToken: utils.NewID(utils.IDTypeToken), | ||||
| 		} | ||||
| 		err := a.store.UpsertWorkspaceSignupToken(*workspace) | ||||
| 		if err != nil { | ||||
| 			a.logger.Error("Unable to initialize workspace", mlog.Err(err)) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		workspace, err = a.store.GetWorkspace(workspaceID) | ||||
| 		if err != nil { | ||||
| 			a.logger.Error("Unable to get initialized workspace", mlog.Err(err)) | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		a.logger.Info("initialized workspace") | ||||
| 	} | ||||
|  | ||||
| 	return workspace, nil | ||||
| } | ||||
|  | ||||
| func (a *App) GetWorkspace(id string) (*model.Workspace, error) { | ||||
| 	workspace, err := a.store.GetWorkspace(id) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return workspace, nil | ||||
| } | ||||
|  | ||||
| func (a *App) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool { | ||||
| 	return a.auth.DoesUserHaveWorkspaceAccess(userID, workspaceID) | ||||
| } | ||||
|  | ||||
| func (a *App) UpsertWorkspaceSettings(workspace model.Workspace) error { | ||||
| 	return a.store.UpsertWorkspaceSettings(workspace) | ||||
| } | ||||
|  | ||||
| func (a *App) UpsertWorkspaceSignupToken(workspace model.Workspace) error { | ||||
| 	return a.store.UpsertWorkspaceSignupToken(workspace) | ||||
| } | ||||
|  | ||||
| func (a *App) GetWorkspaceCount() (int64, error) { | ||||
| 	return a.store.GetWorkspaceCount() | ||||
| } | ||||
|  | ||||
| func (a *App) GetUserWorkspaces(userID string) ([]model.UserWorkspace, error) { | ||||
| 	return a.store.GetUserWorkspaces(userID) | ||||
| } | ||||
| @@ -1,165 +0,0 @@ | ||||
| package app | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| var errInvalidWorkspace = errors.New("invalid workspace id") | ||||
|  | ||||
| var mockWorkspace = &model.Workspace{ | ||||
| 	ID:    "mock-workspace-id", | ||||
| 	Title: "MockWorkspace", | ||||
| } | ||||
|  | ||||
| var mockUserWorkspaces = []model.UserWorkspace{ | ||||
| 	{ | ||||
| 		ID:    "mock-user-workspace-id", | ||||
| 		Title: "MockUserWorkspace", | ||||
| 	}, | ||||
| } | ||||
|  | ||||
| var errUpsertSignupToken = errors.New("upsert error") | ||||
|  | ||||
| func TestGetRootWorkspace(t *testing.T) { | ||||
| 	var newRootWorkspace = &model.Workspace{ | ||||
| 		ID:    "0", | ||||
| 		Title: "NewRootWorkspace", | ||||
| 	} | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		title                         string | ||||
| 		workSpaceToReturnBeforeUpsert *model.Workspace | ||||
| 		workSpaceToReturnAfterUpsert  *model.Workspace | ||||
| 		isError                       bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"Success, Return new root workspace, when root workspace returned by mockstore is nil", | ||||
| 			nil, | ||||
| 			newRootWorkspace, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Success, Return existing root workspace, when root workspace returned by mockstore is notnil", | ||||
| 			newRootWorkspace, | ||||
| 			nil, | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Fail, Return nil, when root workspace returned by mockstore is nil, and upsert new root workspace fails", | ||||
| 			nil, | ||||
| 			nil, | ||||
| 			true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, eachTestacase := range testCases { | ||||
| 		t.Run(eachTestacase.title, func(t *testing.T) { | ||||
| 			th, tearDown := SetupTestHelper(t) | ||||
| 			defer tearDown() | ||||
| 			t.Log(eachTestacase.title) | ||||
| 			th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnBeforeUpsert, nil) | ||||
| 			th.Store.EXPECT().UpsertWorkspaceSignupToken(gomock.Any()).DoAndReturn( | ||||
| 				func(arg0 model.Workspace) error { | ||||
| 					if eachTestacase.isError { | ||||
| 						return errUpsertSignupToken | ||||
| 					} | ||||
| 					th.Store.EXPECT().GetWorkspace("0").Return(eachTestacase.workSpaceToReturnAfterUpsert, nil) | ||||
| 					return nil | ||||
| 				}) | ||||
| 			rootWorkSpace, err := th.App.GetRootWorkspace() | ||||
|  | ||||
| 			if eachTestacase.isError { | ||||
| 				require.Error(t, err) | ||||
| 			} else { | ||||
| 				assert.NotNil(t, rootWorkSpace.ID) | ||||
| 				assert.NotNil(t, rootWorkSpace.SignupToken) | ||||
| 				assert.Equal(t, "", rootWorkSpace.ModifiedBy) | ||||
| 				assert.Equal(t, int64(0), rootWorkSpace.UpdateAt) | ||||
| 				assert.Equal(t, "NewRootWorkspace", rootWorkSpace.Title) | ||||
| 				require.NoError(t, err) | ||||
| 				require.NotNil(t, rootWorkSpace) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGetWorkspace(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		title       string | ||||
| 		workspaceID string | ||||
| 		isError     bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"Success, Return new root workspace, when workspace returned by mockstore is not nil", | ||||
| 			"mock-workspace-id", | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Success, Return nil, when get workspace returns an sql error", | ||||
| 			"workspace-not-available-id", | ||||
| 			false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"Fail, Return nil, when get workspace by mockstore retruns an error", | ||||
| 			"invalid-workspace-id", | ||||
| 			true, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	th.Store.EXPECT().GetWorkspace("mock-workspace-id").Return(mockWorkspace, nil) | ||||
| 	th.Store.EXPECT().GetWorkspace("invalid-workspace-id").Return(nil, errInvalidWorkspace) | ||||
| 	th.Store.EXPECT().GetWorkspace("workspace-not-available-id").Return(nil, sql.ErrNoRows) | ||||
| 	for _, eachTestacase := range testCases { | ||||
| 		t.Run(eachTestacase.title, func(t *testing.T) { | ||||
| 			t.Log(eachTestacase.title) | ||||
| 			workSpace, err := th.App.GetWorkspace(eachTestacase.workspaceID) | ||||
|  | ||||
| 			if eachTestacase.isError { | ||||
| 				require.Error(t, err) | ||||
| 			} else if eachTestacase.workspaceID != "workspace-not-available-id" { | ||||
| 				assert.NotNil(t, workSpace.ID) | ||||
| 				assert.NotNil(t, workSpace.SignupToken) | ||||
| 				assert.Equal(t, "mock-workspace-id", workSpace.ID) | ||||
| 				assert.Equal(t, "", workSpace.ModifiedBy) | ||||
| 				assert.Equal(t, int64(0), workSpace.UpdateAt) | ||||
| 				assert.Equal(t, "MockWorkspace", workSpace.Title) | ||||
| 				require.NoError(t, err) | ||||
| 				require.NotNil(t, workSpace) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestWorkspaceOperations(t *testing.T) { | ||||
| 	th, tearDown := SetupTestHelper(t) | ||||
| 	defer tearDown() | ||||
|  | ||||
| 	th.Store.EXPECT().UpsertWorkspaceSettings(*mockWorkspace).Return(nil) | ||||
| 	th.Store.EXPECT().UpsertWorkspaceSignupToken(*mockWorkspace).Return(nil) | ||||
| 	th.Store.EXPECT().GetWorkspaceCount().Return(int64(10), nil) | ||||
| 	th.Store.EXPECT().GetUserWorkspaces("mock-user-id").Return(mockUserWorkspaces, nil) | ||||
|  | ||||
| 	errUpsertWorkspaceSettings := th.App.UpsertWorkspaceSettings(*mockWorkspace) | ||||
| 	assert.NoError(t, errUpsertWorkspaceSettings) | ||||
|  | ||||
| 	errUpsertWorkspaceSignupToken := th.App.UpsertWorkspaceSignupToken(*mockWorkspace) | ||||
| 	assert.NoError(t, errUpsertWorkspaceSignupToken) | ||||
|  | ||||
| 	count, errGetWorkspaceCount := th.App.GetWorkspaceCount() | ||||
| 	assert.NoError(t, errGetWorkspaceCount) | ||||
| 	assert.Equal(t, int64(10), count) | ||||
|  | ||||
| 	userWorkSpace, errGetUserWorkSpace := th.App.GetUserWorkspaces("mock-user-id") | ||||
| 	assert.NoError(t, errGetUserWorkSpace) | ||||
| 	assert.NotNil(t, userWorkSpace) | ||||
| } | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/pkg/errors" | ||||
| @@ -13,19 +14,20 @@ import ( | ||||
|  | ||||
| type AuthInterface interface { | ||||
| 	GetSession(token string) (*model.Session, error) | ||||
| 	IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) | ||||
| 	DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool | ||||
| 	IsValidReadToken(boardID string, readToken string) (bool, error) | ||||
| 	DoesUserHaveTeamAccess(userID string, teamID string) bool | ||||
| } | ||||
|  | ||||
| // Auth authenticates sessions. | ||||
| type Auth struct { | ||||
| 	config *config.Configuration | ||||
| 	store  store.Store | ||||
| 	config      *config.Configuration | ||||
| 	store       store.Store | ||||
| 	permissions permissions.PermissionsService | ||||
| } | ||||
|  | ||||
| // New returns a new Auth. | ||||
| func New(config *config.Configuration, store store.Store) *Auth { | ||||
| 	return &Auth{config: config, store: store} | ||||
| func New(config *config.Configuration, store store.Store, permissions permissions.PermissionsService) *Auth { | ||||
| 	return &Auth{config: config, store: store, permissions: permissions} | ||||
| } | ||||
|  | ||||
| // GetSession Get a user active session and refresh the session if needed. | ||||
| @@ -44,14 +46,9 @@ func (a *Auth) GetSession(token string) (*model.Session, error) { | ||||
| 	return session, nil | ||||
| } | ||||
|  | ||||
| // IsValidReadToken validates the read token for a block. | ||||
| func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken string) (bool, error) { | ||||
| 	rootID, err := a.store.GetRootID(c, blockID) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	sharing, err := a.store.GetSharing(c, rootID) | ||||
| // IsValidReadToken validates the read token for a board. | ||||
| func (a *Auth) IsValidReadToken(boardID string, readToken string) (bool, error) { | ||||
| 	sharing, err := a.store.GetSharing(boardID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return false, nil | ||||
| 	} | ||||
| @@ -59,17 +56,13 @@ func (a *Auth) IsValidReadToken(c store.Container, blockID string, readToken str | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	if sharing != nil && (sharing.ID == rootID && sharing.Enabled && sharing.Token == readToken) { | ||||
| 	if sharing != nil && (sharing.ID == boardID && sharing.Enabled && sharing.Token == readToken) { | ||||
| 		return true, nil | ||||
| 	} | ||||
|  | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| func (a *Auth) DoesUserHaveWorkspaceAccess(userID string, workspaceID string) bool { | ||||
| 	hasAccess, err := a.store.HasWorkspaceAccess(userID, workspaceID) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return hasAccess | ||||
| func (a *Auth) DoesUserHaveTeamAccess(userID string, teamID string) bool { | ||||
| 	return a.permissions.HasPermissionToTeam(userID, teamID, model.PermissionViewTeam) | ||||
| } | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| package auth | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions/localpermissions" | ||||
| 	mockpermissions "github.com/mattermost/focalboard/server/services/permissions/mocks" | ||||
| 	"github.com/mattermost/focalboard/server/services/store/mockstore" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| 	"github.com/pkg/errors" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| @@ -31,14 +32,19 @@ var mockSession = &model.Session{ | ||||
| func setupTestHelper(t *testing.T) *TestHelper { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	defer ctrl.Finish() | ||||
| 	ctrlPermissions := gomock.NewController(t) | ||||
| 	defer ctrlPermissions.Finish() | ||||
| 	cfg := config.Configuration{} | ||||
| 	mockStore := mockstore.NewMockStore(ctrl) | ||||
| 	newAuth := New(&cfg, mockStore) | ||||
| 	mockPermissions := mockpermissions.NewMockStore(ctrlPermissions) | ||||
| 	logger, err := mlog.NewLogger() | ||||
| 	require.NoError(t, err) | ||||
| 	newAuth := New(&cfg, mockStore, localpermissions.New(mockPermissions, logger)) | ||||
|  | ||||
| 	// called during default template setup for every test | ||||
| 	mockStore.EXPECT().GetDefaultTemplateBlocks().AnyTimes() | ||||
| 	mockStore.EXPECT().GetTemplateBoards(gomock.Any()).AnyTimes() | ||||
| 	mockStore.EXPECT().RemoveDefaultTemplates(gomock.Any()).AnyTimes() | ||||
| 	mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() | ||||
| 	mockStore.EXPECT().InsertBlock(gomock.Any(), gomock.Any()).AnyTimes() | ||||
|  | ||||
| 	return &TestHelper{ | ||||
| 		Auth:    newAuth, | ||||
| @@ -83,55 +89,57 @@ func TestGetSession(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestIsValidReadToken(t *testing.T) { | ||||
| 	th := setupTestHelper(t) | ||||
| 	// ToDo: reimplement | ||||
|  | ||||
| 	validBlockID := "testBlockID" | ||||
| 	mockContainer := store.Container{ | ||||
| 		WorkspaceID: "testWorkspaceID", | ||||
| 	} | ||||
| 	validReadToken := "testReadToken" | ||||
| 	mockSharing := model.Sharing{ | ||||
| 		ID:      "testRootID", | ||||
| 		Enabled: true, | ||||
| 		Token:   validReadToken, | ||||
| 	} | ||||
| 	// th := setupTestHelper(t) | ||||
|  | ||||
| 	testcases := []struct { | ||||
| 		title     string | ||||
| 		container store.Container | ||||
| 		blockID   string | ||||
| 		readToken string | ||||
| 		isError   bool | ||||
| 		isSuccess bool | ||||
| 	}{ | ||||
| 		{"fail, error GetRootID", mockContainer, "badBlock", "", true, false}, | ||||
| 		{"fail, rootID not found", mockContainer, "goodBlockID", "", false, false}, | ||||
| 		{"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false}, | ||||
| 		{"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false}, | ||||
| 		{"success", mockContainer, validBlockID, validReadToken, false, true}, | ||||
| 	} | ||||
| 	// validBlockID := "testBlockID" | ||||
| 	// mockContainer := store.Container{ | ||||
| 	// 	TeamID: "testTeamID", | ||||
| 	// } | ||||
| 	// validReadToken := "testReadToken" | ||||
| 	// mockSharing := model.Sharing{ | ||||
| 	// 	ID:      "testRootID", | ||||
| 	// 	Enabled: true, | ||||
| 	// 	Token:   validReadToken, | ||||
| 	// } | ||||
|  | ||||
| 	th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block")) | ||||
| 	th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil) | ||||
| 	th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil) | ||||
| 	th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2) | ||||
| 	th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows) | ||||
| 	th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error")) | ||||
| 	th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2) | ||||
| 	// testcases := []struct { | ||||
| 	// 	title     string | ||||
| 	// 	container store.Container | ||||
| 	// 	blockID   string | ||||
| 	// 	readToken string | ||||
| 	// 	isError   bool | ||||
| 	// 	isSuccess bool | ||||
| 	// }{ | ||||
| 	// 	{"fail, error GetRootID", mockContainer, "badBlock", "", true, false}, | ||||
| 	// 	{"fail, rootID not found", mockContainer, "goodBlockID", "", false, false}, | ||||
| 	// 	{"fail, sharing throws error", mockContainer, "goodBlockID2", "", true, false}, | ||||
| 	// 	{"fail, bad readToken", mockContainer, validBlockID, "invalidReadToken", false, false}, | ||||
| 	// 	{"success", mockContainer, validBlockID, validReadToken, false, true}, | ||||
| 	// } | ||||
|  | ||||
| 	for _, test := range testcases { | ||||
| 		t.Run(test.title, func(t *testing.T) { | ||||
| 			success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken) | ||||
| 			if test.isError { | ||||
| 				require.Error(t, err) | ||||
| 			} else { | ||||
| 				require.NoError(t, err) | ||||
| 			} | ||||
| 			if test.isSuccess { | ||||
| 				require.True(t, success) | ||||
| 			} else { | ||||
| 				require.False(t, success) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| 	// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "badBlock").Return("", errors.New("invalid block")) | ||||
| 	// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID").Return("rootNotFound", nil) | ||||
| 	// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), "goodBlockID2").Return("rootError", nil) | ||||
| 	// th.Store.EXPECT().GetRootID(gomock.Eq(mockContainer), validBlockID).Return("testRootID", nil).Times(2) | ||||
| 	// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootNotFound").Return(nil, sql.ErrNoRows) | ||||
| 	// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "rootError").Return(nil, errors.New("another error")) | ||||
| 	// th.Store.EXPECT().GetSharing(gomock.Eq(mockContainer), "testRootID").Return(&mockSharing, nil).Times(2) | ||||
|  | ||||
| 	// for _, test := range testcases { | ||||
| 	// 	t.Run(test.title, func(t *testing.T) { | ||||
| 	// 		success, err := th.Auth.IsValidReadToken(test.container, test.blockID, test.readToken) | ||||
| 	// 		if test.isError { | ||||
| 	// 			require.Error(t, err) | ||||
| 	// 		} else { | ||||
| 	// 			require.NoError(t, err) | ||||
| 	// 		} | ||||
| 	// 		if test.isSuccess { | ||||
| 	// 			require.True(t, success) | ||||
| 	// 		} else { | ||||
| 	// 			require.False(t, success) | ||||
| 	// 		} | ||||
| 	// 	}) | ||||
| 	// } | ||||
| } | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import ( | ||||
|  | ||||
| 	gomock "github.com/golang/mock/gomock" | ||||
| 	model "github.com/mattermost/focalboard/server/model" | ||||
| 	store "github.com/mattermost/focalboard/server/services/store" | ||||
| ) | ||||
|  | ||||
| // MockAuthInterface is a mock of AuthInterface interface. | ||||
| @@ -35,18 +34,18 @@ func (m *MockAuthInterface) EXPECT() *MockAuthInterfaceMockRecorder { | ||||
| 	return m.recorder | ||||
| } | ||||
|  | ||||
| // DoesUserHaveWorkspaceAccess mocks base method. | ||||
| func (m *MockAuthInterface) DoesUserHaveWorkspaceAccess(arg0, arg1 string) bool { | ||||
| // DoesUserHaveTeamAccess mocks base method. | ||||
| func (m *MockAuthInterface) DoesUserHaveTeamAccess(arg0, arg1 string) bool { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "DoesUserHaveWorkspaceAccess", arg0, arg1) | ||||
| 	ret := m.ctrl.Call(m, "DoesUserHaveTeamAccess", arg0, arg1) | ||||
| 	ret0, _ := ret[0].(bool) | ||||
| 	return ret0 | ||||
| } | ||||
|  | ||||
| // DoesUserHaveWorkspaceAccess indicates an expected call of DoesUserHaveWorkspaceAccess. | ||||
| func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveWorkspaceAccess(arg0, arg1 interface{}) *gomock.Call { | ||||
| // DoesUserHaveTeamAccess indicates an expected call of DoesUserHaveTeamAccess. | ||||
| func (mr *MockAuthInterfaceMockRecorder) DoesUserHaveTeamAccess(arg0, arg1 interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveWorkspaceAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveWorkspaceAccess), arg0, arg1) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoesUserHaveTeamAccess", reflect.TypeOf((*MockAuthInterface)(nil).DoesUserHaveTeamAccess), arg0, arg1) | ||||
| } | ||||
|  | ||||
| // GetSession mocks base method. | ||||
| @@ -65,16 +64,16 @@ func (mr *MockAuthInterfaceMockRecorder) GetSession(arg0 interface{}) *gomock.Ca | ||||
| } | ||||
|  | ||||
| // IsValidReadToken mocks base method. | ||||
| func (m *MockAuthInterface) IsValidReadToken(arg0 store.Container, arg1, arg2 string) (bool, error) { | ||||
| func (m *MockAuthInterface) IsValidReadToken(arg0, arg1 string) (bool, error) { | ||||
| 	m.ctrl.T.Helper() | ||||
| 	ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1, arg2) | ||||
| 	ret := m.ctrl.Call(m, "IsValidReadToken", arg0, arg1) | ||||
| 	ret0, _ := ret[0].(bool) | ||||
| 	ret1, _ := ret[1].(error) | ||||
| 	return ret0, ret1 | ||||
| } | ||||
|  | ||||
| // IsValidReadToken indicates an expected call of IsValidReadToken. | ||||
| func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1, arg2 interface{}) *gomock.Call { | ||||
| func (mr *MockAuthInterfaceMockRecorder) IsValidReadToken(arg0, arg1 interface{}) *gomock.Call { | ||||
| 	mr.mock.ctrl.T.Helper() | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1, arg2) | ||||
| 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsValidReadToken", reflect.TypeOf((*MockAuthInterface)(nil).IsValidReadToken), arg0, arg1) | ||||
| } | ||||
|   | ||||
| @@ -101,8 +101,8 @@ func (c *Client) DoAPIPut(url, data string) (*http.Response, error) { | ||||
| 	return c.DoAPIRequest(http.MethodPut, c.APIURL+url, data, "") | ||||
| } | ||||
|  | ||||
| func (c *Client) DoAPIDelete(url string) (*http.Response, error) { | ||||
| 	return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, "", "") | ||||
| func (c *Client) DoAPIDelete(url string, data string) (*http.Response, error) { | ||||
| 	return c.DoAPIRequest(http.MethodDelete, c.APIURL+url, data, "") | ||||
| } | ||||
|  | ||||
| func (c *Client) DoAPIRequest(method, url, data, etag string) (*http.Response, error) { | ||||
| @@ -152,20 +152,50 @@ func (c *Client) doAPIRequestReader(method, url string, data io.Reader, _ /* eta | ||||
| 	return rp, nil | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBlocksRoute() string { | ||||
| 	return "/workspaces/0/blocks" | ||||
| func (c *Client) GetTeamRoute(teamID string) string { | ||||
| 	return fmt.Sprintf("%s/%s", c.GetTeamsRoute(), teamID) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBlockRoute(id string) string { | ||||
| 	return fmt.Sprintf("%s/%s", c.GetBlocksRoute(), id) | ||||
| func (c *Client) GetTeamsRoute() string { | ||||
| 	return "/teams" | ||||
| } | ||||
|  | ||||
| func (c *Client) GetSubtreeRoute(id string) string { | ||||
| 	return fmt.Sprintf("%s/subtree", c.GetBlockRoute(id)) | ||||
| func (c *Client) GetBlockRoute(boardID, blockID string) string { | ||||
| 	return fmt.Sprintf("%s/%s", c.GetBlocksRoute(boardID), blockID) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBlocks() ([]model.Block, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetBlocksRoute(), "") | ||||
| func (c *Client) GetSubtreeRoute(boardID, blockID string) string { | ||||
| 	return fmt.Sprintf("%s/subtree", c.GetBlockRoute(boardID, blockID)) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBoardsRoute() string { | ||||
| 	return "/boards" | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBoardRoute(boardID string) string { | ||||
| 	return fmt.Sprintf("%s/%s", c.GetBoardsRoute(), boardID) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBlocksRoute(boardID string) string { | ||||
| 	return fmt.Sprintf("%s/blocks", c.GetBoardRoute(boardID)) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBoardsAndBlocksRoute() string { | ||||
| 	return "/boards-and-blocks" | ||||
| } | ||||
|  | ||||
| func (c *Client) GetTeam(teamID string) (*model.Team, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetTeamRoute(teamID), "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.TeamFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBlocksForBoard(boardID string) ([]model.Block, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetBlocksRoute(boardID), "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -174,8 +204,8 @@ func (c *Client) GetBlocks() ([]model.Block, *Response) { | ||||
| 	return model.BlocksFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPatch(c.GetBlockRoute(blockID), toJSON(blockPatch)) | ||||
| func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID), toJSON(blockPatch)) | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -184,8 +214,46 @@ func (c *Client) PatchBlock(blockID string, blockPatch *model.BlockPatch) (bool, | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBlocksRoute(), toJSON(blocks)) | ||||
| func (c *Client) DuplicateBoard(boardID string, asTemplate bool, teamID string) (bool, *Response) { | ||||
| 	queryParams := "?asTemplate=false&" | ||||
| 	if asTemplate { | ||||
| 		queryParams = "?asTemplate=true" | ||||
| 	} | ||||
| 	r, err := c.DoAPIPost(c.GetBoardRoute(boardID)+"/duplicate"+queryParams, "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) DuplicateBlock(boardID, blockID string, asTemplate bool) (bool, *Response) { | ||||
| 	queryParams := "?asTemplate=false" | ||||
| 	if asTemplate { | ||||
| 		queryParams = "?asTemplate=true" | ||||
| 	} | ||||
| 	r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/duplicate"+queryParams, "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) UndeleteBlock(boardID, blockID string) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBlockRoute(boardID, blockID)+"/undelete", "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) InsertBlocks(boardID string, blocks []model.Block) ([]model.Block, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBlocksRoute(boardID), toJSON(blocks)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -194,8 +262,8 @@ func (c *Client) InsertBlocks(blocks []model.Block) ([]model.Block, *Response) { | ||||
| 	return model.BlocksFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) DeleteBlock(blockID string) (bool, *Response) { | ||||
| 	r, err := c.DoAPIDelete(c.GetBlockRoute(blockID)) | ||||
| func (c *Client) DeleteBlock(boardID, blockID string) (bool, *Response) { | ||||
| 	r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID), "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -204,18 +272,8 @@ func (c *Client) DeleteBlock(blockID string) (bool, *Response) { | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) UndeleteBlock(blockID string) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBlockRoute(blockID)+"/undelete", "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetSubtreeRoute(blockID), "") | ||||
| func (c *Client) GetSubtree(boardID, blockID string) ([]model.Block, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetSubtreeRoute(boardID, blockID), "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -224,14 +282,45 @@ func (c *Client) GetSubtree(blockID string) ([]model.Block, *Response) { | ||||
| 	return model.BlocksFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| // Boards and blocks. | ||||
| func (c *Client) CreateBoardsAndBlocks(bab *model.BoardsAndBlocks) (*model.BoardsAndBlocks, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBoardsAndBlocksRoute(), toJSON(bab)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) PatchBoardsAndBlocks(pbab *model.PatchBoardsAndBlocks) (*model.BoardsAndBlocks, *Response) { | ||||
| 	r, err := c.DoAPIPatch(c.GetBoardsAndBlocksRoute(), toJSON(pbab)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardsAndBlocksFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) DeleteBoardsAndBlocks(dbab *model.DeleteBoardsAndBlocks) (bool, *Response) { | ||||
| 	r, err := c.DoAPIDelete(c.GetBoardsAndBlocksRoute(), toJSON(dbab)) | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| // Sharing | ||||
|  | ||||
| func (c *Client) GetSharingRoute(rootID string) string { | ||||
| 	return fmt.Sprintf("/workspaces/0/sharing/%s", rootID) | ||||
| func (c *Client) GetSharingRoute(boardID string) string { | ||||
| 	return fmt.Sprintf("%s/sharing", c.GetBoardRoute(boardID)) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetSharingRoute(rootID), "") | ||||
| func (c *Client) GetSharing(boardID string) (*model.Sharing, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetSharingRoute(boardID), "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -241,7 +330,7 @@ func (c *Client) GetSharing(rootID string) (*model.Sharing, *Response) { | ||||
| 	return &sharing, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) PostSharing(sharing model.Sharing) (bool, *Response) { | ||||
| func (c *Client) PostSharing(sharing *model.Sharing) (bool, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetSharingRoute(sharing.ID), toJSON(sharing)) | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| @@ -338,11 +427,116 @@ func (c *Client) UserChangePassword(id string, data *api.ChangePasswordRequest) | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetWorkspaceUploadFileRoute(workspaceID, rootID string) string { | ||||
| 	return fmt.Sprintf("/workspaces/%s/%s/files", workspaceID, rootID) | ||||
| func (c *Client) CreateBoard(board *model.Board) (*model.Board, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBoardsRoute(), toJSON(board)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) (*api.FileUploadResponse, *Response) { | ||||
| func (c *Client) PatchBoard(boardID string, patch *model.BoardPatch) (*model.Board, *Response) { | ||||
| 	r, err := c.DoAPIPatch(c.GetBoardRoute(boardID), toJSON(patch)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) DeleteBoard(boardID string) (bool, *Response) { | ||||
| 	r, err := c.DoAPIDelete(c.GetBoardRoute(boardID), "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBoard(boardID, readToken string) (*model.Board, *Response) { | ||||
| 	url := c.GetBoardRoute(boardID) | ||||
| 	if readToken != "" { | ||||
| 		url += fmt.Sprintf("?read_token=%s", readToken) | ||||
| 	} | ||||
|  | ||||
| 	r, err := c.DoAPIGet(url, "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetBoardsForTeam(teamID string) ([]*model.Board, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards", "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardsFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) SearchBoardsForTeam(teamID, term string) ([]*model.Board, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetTeamRoute(teamID)+"/boards/search?q="+term, "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardsFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetMembersForBoard(boardID string) ([]*model.BoardMember, *Response) { | ||||
| 	r, err := c.DoAPIGet(c.GetBoardRoute(boardID)+"/members", "") | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardMembersFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) AddMemberToBoard(member *model.BoardMember) (*model.BoardMember, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetBoardRoute(member.BoardID)+"/members", toJSON(member)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardMemberFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) UpdateBoardMember(member *model.BoardMember) (*model.BoardMember, *Response) { | ||||
| 	r, err := c.DoAPIPut(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, toJSON(member)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return model.BoardMemberFromJSON(r.Body), BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) DeleteBoardMember(member *model.BoardMember) (bool, *Response) { | ||||
| 	r, err := c.DoAPIDelete(c.GetBoardRoute(member.BoardID)+"/members/"+member.UserID, "") | ||||
| 	if err != nil { | ||||
| 		return false, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| 	defer closeBody(r) | ||||
|  | ||||
| 	return true, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetTeamUploadFileRoute(teamID, boardID string) string { | ||||
| 	return fmt.Sprintf("%s/%s/files", c.GetTeamRoute(teamID), boardID) | ||||
| } | ||||
|  | ||||
| func (c *Client) TeamUploadFile(teamID, boardID string, data io.Reader) (*api.FileUploadResponse, *Response) { | ||||
| 	body := &bytes.Buffer{} | ||||
| 	writer := multipart.NewWriter(body) | ||||
| 	part, err := writer.CreateFormFile(api.UploadFormFileKey, "file") | ||||
| @@ -358,7 +552,7 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) | ||||
| 		r.Header.Add("Content-Type", writer.FormDataContentType()) | ||||
| 	} | ||||
|  | ||||
| 	r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetWorkspaceUploadFileRoute(workspaceID, rootID), body, "", opt) | ||||
| 	r, err := c.doAPIRequestReader(http.MethodPost, c.APIURL+c.GetTeamUploadFileRoute(teamID, boardID), body, "", opt) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -372,12 +566,12 @@ func (c *Client) WorkspaceUploadFile(workspaceID, rootID string, data io.Reader) | ||||
| 	return fileUploadResponse, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetSubscriptionsRoute(workspaceID string) string { | ||||
| 	return fmt.Sprintf("/workspaces/%s/subscriptions", workspaceID) | ||||
| func (c *Client) GetSubscriptionsRoute() string { | ||||
| 	return "/subscriptions" | ||||
| } | ||||
|  | ||||
| func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription) (*model.Subscription, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetSubscriptionsRoute(workspaceID), toJSON(&sub)) | ||||
| func (c *Client) CreateSubscription(sub *model.Subscription) (*model.Subscription, *Response) { | ||||
| 	r, err := c.DoAPIPost(c.GetSubscriptionsRoute(), toJSON(&sub)) | ||||
| 	if err != nil { | ||||
| 		return nil, BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -390,10 +584,10 @@ func (c *Client) CreateSubscription(workspaceID string, sub *model.Subscription) | ||||
| 	return subNew, BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscriberID string) *Response { | ||||
| 	url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(workspaceID), blockID, subscriberID) | ||||
| func (c *Client) DeleteSubscription(blockID string, subscriberID string) *Response { | ||||
| 	url := fmt.Sprintf("%s/%s/%s", c.GetSubscriptionsRoute(), blockID, subscriberID) | ||||
|  | ||||
| 	r, err := c.DoAPIDelete(url) | ||||
| 	r, err := c.DoAPIDelete(url, "") | ||||
| 	if err != nil { | ||||
| 		return BuildErrorResponse(r, err) | ||||
| 	} | ||||
| @@ -402,8 +596,8 @@ func (c *Client) DeleteSubscription(workspaceID string, blockID string, subscrib | ||||
| 	return BuildResponse(r) | ||||
| } | ||||
|  | ||||
| func (c *Client) GetSubscriptions(workspaceID string, subscriberID string) ([]*model.Subscription, *Response) { | ||||
| 	url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(workspaceID), subscriberID) | ||||
| func (c *Client) GetSubscriptions(subscriberID string) ([]*model.Subscription, *Response) { | ||||
| 	url := fmt.Sprintf("%s/%s", c.GetSubscriptionsRoute(), subscriberID) | ||||
|  | ||||
| 	r, err := c.DoAPIGet(url, "") | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -5,7 +5,6 @@ go 1.16 | ||||
| require ( | ||||
| 	github.com/Masterminds/squirrel v1.5.0 | ||||
| 	github.com/go-sql-driver/mysql v1.6.0 | ||||
| 	github.com/golang-migrate/migrate/v4 v4.14.1 | ||||
| 	github.com/golang/mock v1.5.0 | ||||
| 	github.com/gorilla/mux v1.8.0 | ||||
| 	github.com/gorilla/websocket v1.4.2 | ||||
| @@ -14,6 +13,7 @@ require ( | ||||
| 	github.com/magiconair/properties v1.8.5 // indirect | ||||
| 	github.com/mattermost/mattermost-plugin-api v0.0.21 | ||||
| 	github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0 | ||||
| 	github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 | ||||
| 	github.com/mattn/go-sqlite3 v2.0.3+incompatible | ||||
| 	github.com/mitchellh/mapstructure v1.4.1 // indirect | ||||
| 	github.com/oklog/run v1.1.0 | ||||
|   | ||||
							
								
								
									
										158
									
								
								server/go.sum
									
									
									
									
									
								
							
							
						
						
									
										158
									
								
								server/go.sum
									
									
									
									
									
								
							| @@ -47,7 +47,6 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy | ||||
| git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= | ||||
| github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= | ||||
| github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= | ||||
| github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= | ||||
| github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= | ||||
| github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| @@ -68,7 +67,6 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0 | ||||
| github.com/Masterminds/squirrel v1.5.0 h1:JukIZisrUXadA9pl3rMkjhiamxiB0cXiu+HGp/Y8cY8= | ||||
| github.com/Masterminds/squirrel v1.5.0/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= | ||||
| github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= | ||||
| github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= | ||||
| github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= | ||||
| github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= | ||||
| github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk= | ||||
| @@ -161,7 +159,6 @@ github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE | ||||
| github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= | ||||
| github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= | ||||
| github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||
| github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY= | ||||
| github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= | ||||
| github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= | ||||
| github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= | ||||
| @@ -185,6 +182,7 @@ github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ1 | ||||
| github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= | ||||
| github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= | ||||
| github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= | ||||
| github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| @@ -195,19 +193,14 @@ github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFau | ||||
| github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= | ||||
| github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= | ||||
| github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= | ||||
| github.com/dhui/dktest v0.3.3 h1:DBuH/9GFaWbDRa42qsut/hbQu+srAQ0rPWnUoiGX7CA= | ||||
| github.com/dhui/dktest v0.3.3/go.mod h1:EML9sP4sqJELHn4jV7B0TY8oF6077nk83/tz7M56jcQ= | ||||
| github.com/die-net/lrucache v0.0.0-20181227122439-19a39ef22a11/go.mod h1:ew0MSjCVDdtGMjF3kzLK9hwdgF5mOE8SbYVF3Rc7mkU= | ||||
| github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= | ||||
| github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= | ||||
| github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= | ||||
| github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= | ||||
| github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= | ||||
| github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible h1:iWPIG7pWIsCwT6ZtHnTUpoVMnete7O/pzd9HFE3+tn8= | ||||
| github.com/docker/docker v17.12.0-ce-rc1.0.20200618181300-9dc6525e6118+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= | ||||
| github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= | ||||
| github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= | ||||
| github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= | ||||
| github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= | ||||
| github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= | ||||
| github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= | ||||
| @@ -233,6 +226,7 @@ github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqL | ||||
| github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= | ||||
| github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= | ||||
| github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | ||||
| github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= | ||||
| github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= | ||||
| github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= | ||||
| github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= | ||||
| @@ -290,9 +284,7 @@ github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhD | ||||
| github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= | ||||
| github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= | ||||
| github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= | ||||
| github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= | ||||
| github.com/golang-migrate/migrate/v4 v4.14.1 h1:qmRd/rNGjM1r3Ve5gHd5ZplytrD02UcItYNxJ3iUHHE= | ||||
| github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1uLlJg54C/VvW7tvxSz0= | ||||
| github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= | ||||
| github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= | ||||
| @@ -346,6 +338,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ | ||||
| github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= | ||||
| @@ -366,8 +359,9 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 | ||||
| github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= | ||||
| github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= | ||||
| github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= | ||||
| github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= | ||||
| @@ -403,7 +397,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p | ||||
| github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= | ||||
| github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= | ||||
| github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= | ||||
| github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= | ||||
| github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= | ||||
| @@ -416,7 +409,6 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP | ||||
| github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= | ||||
| github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= | ||||
| github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= | ||||
| github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= | ||||
| github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= | ||||
| github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0= | ||||
| github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= | ||||
| @@ -513,6 +505,8 @@ github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYb | ||||
| github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE= | ||||
| github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro= | ||||
| github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8= | ||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= | ||||
| github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= | ||||
| github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= | ||||
| github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= | ||||
| github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= | ||||
| @@ -564,6 +558,7 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
| github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
| github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
| github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= | ||||
| github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= | ||||
| @@ -592,6 +587,8 @@ github.com/mattermost/mattermost-plugin-api v0.0.21/go.mod h1:qz19Y+5HLbjtzY2RZ6 | ||||
| github.com/mattermost/mattermost-server/v6 v6.0.0-20210901153517-42e75fad4dae/go.mod h1:kmxJuVgpX13Th+e5L1ZsBs4aq+ETmmDg9joo5r4cIw8= | ||||
| github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0 h1:A7TCgCGF9JmAHBQv9qGm5SfPYTAl8dOXy/u6lCSV8ow= | ||||
| github.com/mattermost/mattermost-server/v6 v6.0.0-20210913141218-bb659d03fde0/go.mod h1:TUSk5lYJmwfTKTJLXR0eAsjJNlKkWzS5aGZegXG0J08= | ||||
| github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131 h1:agJMxBP8LV0nyV90PZ/BHmmjNyvzTWqR20wLwiXHx14= | ||||
| github.com/mattermost/morph v0.0.0-20220222074146-cff3f12ff131/go.mod h1:jxM3g1bx+k2Thz7jofcHguBS8TZn5Pc+o5MGmORObhw= | ||||
| github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= | ||||
| github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= | ||||
| github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= | ||||
| @@ -616,6 +613,7 @@ github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh | ||||
| github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= | ||||
| github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= | ||||
| @@ -658,7 +656,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= | ||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= | ||||
| github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= | ||||
| github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= | ||||
| github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= | ||||
| @@ -705,9 +702,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y | ||||
| github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= | ||||
| github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= | ||||
| github.com/oov/psd v0.0.0-20210618170533-9fb823ddb631/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= | ||||
| github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= | ||||
| github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= | ||||
| github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= | ||||
| github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= | ||||
| github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= | ||||
| github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= | ||||
| @@ -782,6 +777,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn | ||||
| github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= | ||||
| github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| @@ -1063,6 +1059,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB | ||||
| golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= | ||||
| golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| @@ -1203,6 +1200,7 @@ golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7w | ||||
| golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| @@ -1213,8 +1211,10 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w | ||||
| golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= | ||||
| golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= | ||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| @@ -1287,9 +1287,11 @@ golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc | ||||
| golang.org/x/tools v0.0.0-20200818005847-188abfa75333/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= | ||||
| golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= | ||||
| golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= | ||||
| golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs= | ||||
| golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= | ||||
| golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| @@ -1454,17 +1456,139 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh | ||||
| honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= | ||||
| honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= | ||||
| honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= | ||||
| lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= | ||||
| lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= | ||||
| modernc.org/b v1.0.0/go.mod h1:uZWcZfRj1BpYzfN9JTerzlNUnnPsV9O2ZA8JsRcubNg= | ||||
| modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= | ||||
| modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= | ||||
| modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= | ||||
| modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= | ||||
| modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= | ||||
| modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= | ||||
| modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= | ||||
| modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= | ||||
| modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= | ||||
| modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= | ||||
| modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= | ||||
| modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= | ||||
| modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= | ||||
| modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= | ||||
| modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= | ||||
| modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= | ||||
| modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= | ||||
| modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= | ||||
| modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= | ||||
| modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= | ||||
| modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= | ||||
| modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= | ||||
| modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= | ||||
| modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= | ||||
| modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= | ||||
| modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= | ||||
| modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= | ||||
| modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= | ||||
| modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= | ||||
| modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= | ||||
| modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= | ||||
| modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= | ||||
| modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= | ||||
| modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= | ||||
| modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= | ||||
| modernc.org/ccgo/v3 v3.12.84/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= | ||||
| modernc.org/ccgo/v3 v3.12.86/go.mod h1:dN7S26DLTgVSni1PVA3KxxHTcykyDurf3OgUzNqTSrU= | ||||
| modernc.org/ccgo/v3 v3.12.88/go.mod h1:0MFzUHIuSIthpVZyMWiFYMwjiFnhrN5MkvBrUwON+ZM= | ||||
| modernc.org/ccgo/v3 v3.12.90/go.mod h1:obhSc3CdivCRpYZmrvO88TXlW0NvoSVvdh/ccRjJYko= | ||||
| modernc.org/ccgo/v3 v3.12.92/go.mod h1:5yDdN7ti9KWPi5bRVWPl8UNhpEAtCjuEE7ayQnzzqHA= | ||||
| modernc.org/ccgo/v3 v3.12.95 h1:Ym2JG2G3P4IyZqjTTojHTl7qO0RysXeGSYPSoKPSBxc= | ||||
| modernc.org/ccgo/v3 v3.12.95/go.mod h1:ZcLyvtocXYi8uF+9Ebm3G8EF8HNY5hGomBqthDp4eC8= | ||||
| modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= | ||||
| modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= | ||||
| modernc.org/db v1.0.0/go.mod h1:kYD/cO29L/29RM0hXYl4i3+Q5VojL31kTUVpVJDw0s8= | ||||
| modernc.org/file v1.0.0/go.mod h1:uqEokAEn1u6e+J45e54dsEA/pw4o7zLrA2GwyntZzjw= | ||||
| modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= | ||||
| modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= | ||||
| modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= | ||||
| modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= | ||||
| modernc.org/internal v1.0.0/go.mod h1:VUD/+JAkhCpvkUitlEOnhpVxCgsBI90oTzSCRcqQVSM= | ||||
| modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= | ||||
| modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= | ||||
| modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= | ||||
| modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= | ||||
| modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= | ||||
| modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= | ||||
| modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= | ||||
| modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= | ||||
| modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= | ||||
| modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= | ||||
| modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= | ||||
| modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= | ||||
| modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= | ||||
| modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= | ||||
| modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= | ||||
| modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= | ||||
| modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= | ||||
| modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= | ||||
| modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= | ||||
| modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= | ||||
| modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= | ||||
| modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= | ||||
| modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= | ||||
| modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= | ||||
| modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= | ||||
| modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= | ||||
| modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= | ||||
| modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= | ||||
| modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= | ||||
| modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= | ||||
| modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= | ||||
| modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= | ||||
| modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= | ||||
| modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= | ||||
| modernc.org/libc v1.11.88/go.mod h1:h3oIVe8dxmTcchcFuCcJ4nAWaoiwzKCdv82MM0oiIdQ= | ||||
| modernc.org/libc v1.11.90/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= | ||||
| modernc.org/libc v1.11.98/go.mod h1:ynK5sbjsU77AP+nn61+k+wxUGRx9rOFcIqWYYMaDZ4c= | ||||
| modernc.org/libc v1.11.99/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= | ||||
| modernc.org/libc v1.11.101/go.mod h1:wLLYgEiY2D17NbBOEp+mIJJJBGSiy7fLL4ZrGGZ+8jI= | ||||
| modernc.org/libc v1.11.104 h1:gxoa5b3HPo7OzD4tKZjgnwXk/w//u1oovvjSMP3Q96Q= | ||||
| modernc.org/libc v1.11.104/go.mod h1:2MH3DaF/gCU8i/UBiVE1VFRos4o523M7zipmwH8SIgQ= | ||||
| modernc.org/lldb v1.0.0/go.mod h1:jcRvJGWfCGodDZz8BPwiKMJxGJngQ/5DrRapkQnLob8= | ||||
| modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= | ||||
| modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= | ||||
| modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= | ||||
| modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= | ||||
| modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= | ||||
| modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= | ||||
| modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= | ||||
| modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= | ||||
| modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY= | ||||
| modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k= | ||||
| modernc.org/sqlite v1.14.3 h1:psrTwgpEujgWEP3FNdsC9yNh5tSeA77U0GeWhHH4XmQ= | ||||
| modernc.org/sqlite v1.14.3/go.mod h1:xMpicS1i2MJ4C8+Ap0vYBqTwYfpFvdnPE6brbFOtV2Y= | ||||
| modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= | ||||
| modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= | ||||
| modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= | ||||
| modernc.org/tcl v1.9.2 h1:YA87dFLOsR2KqMka371a2Xgr+YsyUwo7OmHVSv/kztw= | ||||
| modernc.org/tcl v1.9.2/go.mod h1:aw7OnlIoiuJgu1gwbTZtrKnGpDqH9wyH++jZcxdqNsg= | ||||
| modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= | ||||
| modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= | ||||
| modernc.org/z v1.2.20 h1:DyboxM1sJR2NB803j2StnbnL6jcQXz273OhHDGu8dGk= | ||||
| modernc.org/z v1.2.20/go.mod h1:zU9FiF4PbHdOTUxw+IF8j7ArBMRPsHgq10uVPt6xTzo= | ||||
| modernc.org/zappy v1.0.0/go.mod h1:hHe+oGahLVII/aTTyWK/b53VDHMAGCBYYeZ9sn83HC4= | ||||
| rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= | ||||
| rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= | ||||
|   | ||||
| @@ -11,40 +11,38 @@ import ( | ||||
| ) | ||||
|  | ||||
| func TestGetBlocks(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelperWithToken(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocks() | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	initialCount := len(blocks) | ||||
| 	board := th.CreateBoard("team-id", model.BoardTypeOpen) | ||||
|  | ||||
| 	initialID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 	initialID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 	newBlocks := []model.Block{ | ||||
| 		{ | ||||
| 			ID:       initialID1, | ||||
| 			RootID:   initialID1, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 1, | ||||
| 			Type:     model.TypeBoard, | ||||
| 			Type:     model.TypeCard, | ||||
| 		}, | ||||
| 		{ | ||||
| 			ID:       initialID2, | ||||
| 			RootID:   initialID2, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 1, | ||||
| 			Type:     model.TypeBoard, | ||||
| 			Type:     model.TypeCard, | ||||
| 		}, | ||||
| 	} | ||||
| 	newBlocks, resp = th.Client.InsertBlocks(newBlocks) | ||||
| 	newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks) | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	require.Len(t, newBlocks, 2) | ||||
| 	blockID1 := newBlocks[0].ID | ||||
| 	blockID2 := newBlocks[1].ID | ||||
|  | ||||
| 	blocks, resp = th.Client.GetBlocks() | ||||
| 	blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	require.Len(t, blocks, initialCount+2) | ||||
| 	require.Len(t, blocks, 2) | ||||
|  | ||||
| 	blockIDs := make([]string, len(blocks)) | ||||
| 	for i, b := range blocks { | ||||
| @@ -55,12 +53,10 @@ func TestGetBlocks(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestPostBlock(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelperWithToken(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocks() | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	initialCount := len(blocks) | ||||
| 	board := th.CreateBoard("team-id", model.BoardTypeOpen) | ||||
|  | ||||
| 	var blockID1 string | ||||
| 	var blockID2 string | ||||
| @@ -70,21 +66,21 @@ func TestPostBlock(t *testing.T) { | ||||
| 		initialID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block := model.Block{ | ||||
| 			ID:       initialID1, | ||||
| 			RootID:   initialID1, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 1, | ||||
| 			Type:     model.TypeBoard, | ||||
| 			Type:     model.TypeCard, | ||||
| 			Title:    "New title", | ||||
| 		} | ||||
|  | ||||
| 		newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) | ||||
| 		newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, newBlocks, 1) | ||||
| 		blockID1 = newBlocks[0].ID | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+1) | ||||
| 		require.Len(t, blocks, 1) | ||||
|  | ||||
| 		blockIDs := make([]string, len(blocks)) | ||||
| 		for i, b := range blocks { | ||||
| @@ -99,21 +95,21 @@ func TestPostBlock(t *testing.T) { | ||||
| 		newBlocks := []model.Block{ | ||||
| 			{ | ||||
| 				ID:       initialID2, | ||||
| 				RootID:   initialID2, | ||||
| 				BoardID:  board.ID, | ||||
| 				CreateAt: 1, | ||||
| 				UpdateAt: 1, | ||||
| 				Type:     model.TypeBoard, | ||||
| 				Type:     model.TypeCard, | ||||
| 			}, | ||||
| 			{ | ||||
| 				ID:       initialID3, | ||||
| 				RootID:   initialID3, | ||||
| 				BoardID:  board.ID, | ||||
| 				CreateAt: 1, | ||||
| 				UpdateAt: 1, | ||||
| 				Type:     model.TypeBoard, | ||||
| 				Type:     model.TypeCard, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		newBlocks, resp := th.Client.InsertBlocks(newBlocks) | ||||
| 		newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, newBlocks, 2) | ||||
| 		blockID2 = newBlocks[0].ID | ||||
| @@ -121,9 +117,9 @@ func TestPostBlock(t *testing.T) { | ||||
| 		require.NotEqual(t, initialID2, blockID2) | ||||
| 		require.NotEqual(t, initialID3, blockID3) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+3) | ||||
| 		require.Len(t, blocks, 3) | ||||
|  | ||||
| 		blockIDs := make([]string, len(blocks)) | ||||
| 		for i, b := range blocks { | ||||
| @@ -137,22 +133,22 @@ func TestPostBlock(t *testing.T) { | ||||
| 	t.Run("Update a block should not be possible through the insert endpoint", func(t *testing.T) { | ||||
| 		block := model.Block{ | ||||
| 			ID:       blockID1, | ||||
| 			RootID:   blockID1, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 20, | ||||
| 			Type:     model.TypeBoard, | ||||
| 			Type:     model.TypeCard, | ||||
| 			Title:    "Updated title", | ||||
| 		} | ||||
|  | ||||
| 		newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) | ||||
| 		newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, newBlocks, 1) | ||||
| 		blockID4 := newBlocks[0].ID | ||||
| 		require.NotEqual(t, blockID1, blockID4) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+4) | ||||
| 		require.Len(t, blocks, 4) | ||||
|  | ||||
| 		var block4 model.Block | ||||
| 		for _, b := range blocks { | ||||
| @@ -166,42 +162,41 @@ func TestPostBlock(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestPatchBlock(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelperWithToken(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	initialID := utils.NewID(utils.IDTypeBlock) | ||||
|  | ||||
| 	board := th.CreateBoard("team-id", model.BoardTypeOpen) | ||||
| 	time.Sleep(10 * time.Millisecond) | ||||
|  | ||||
| 	block := model.Block{ | ||||
| 		ID:       initialID, | ||||
| 		RootID:   initialID, | ||||
| 		BoardID:  board.ID, | ||||
| 		CreateAt: 1, | ||||
| 		UpdateAt: 1, | ||||
| 		Type:     model.TypeBoard, | ||||
| 		Type:     model.TypeCard, | ||||
| 		Title:    "New title", | ||||
| 		Fields:   map[string]interface{}{"test": "test value", "test2": "test value 2"}, | ||||
| 	} | ||||
|  | ||||
| 	newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) | ||||
| 	th.CheckOK(resp) | ||||
| 	require.Len(t, newBlocks, 1) | ||||
| 	blockID := newBlocks[0].ID | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocks() | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	initialCount := len(blocks) | ||||
|  | ||||
| 	t.Run("Patch a block basic field", func(t *testing.T) { | ||||
| 		newTitle := "Updated title" | ||||
| 		blockPatch := &model.BlockPatch{ | ||||
| 			Title: &newTitle, | ||||
| 		} | ||||
|  | ||||
| 		_, resp := th.Client.PatchBlock(blockID, blockPatch) | ||||
| 		_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount) | ||||
| 		require.Len(t, blocks, 1) | ||||
|  | ||||
| 		var updatedBlock model.Block | ||||
| 		for _, b := range blocks { | ||||
| @@ -221,12 +216,12 @@ func TestPatchBlock(t *testing.T) { | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		_, resp := th.Client.PatchBlock(blockID, blockPatch) | ||||
| 		_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount) | ||||
| 		require.Len(t, blocks, 1) | ||||
|  | ||||
| 		var updatedBlock model.Block | ||||
| 		for _, b := range blocks { | ||||
| @@ -244,12 +239,12 @@ func TestPatchBlock(t *testing.T) { | ||||
| 			DeletedFields: []string{"test", "test3", "test100"}, | ||||
| 		} | ||||
|  | ||||
| 		_, resp := th.Client.PatchBlock(blockID, blockPatch) | ||||
| 		_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount) | ||||
| 		require.Len(t, blocks, 1) | ||||
|  | ||||
| 		var updatedBlock model.Block | ||||
| 		for _, b := range blocks { | ||||
| @@ -265,35 +260,34 @@ func TestPatchBlock(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestDeleteBlock(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelperWithToken(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocks() | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	initialCount := len(blocks) | ||||
| 	board := th.CreateBoard("team-id", model.BoardTypeOpen) | ||||
| 	time.Sleep(10 * time.Millisecond) | ||||
|  | ||||
| 	var blockID string | ||||
| 	t.Run("Create a block", func(t *testing.T) { | ||||
| 		initialID := utils.NewID(utils.IDTypeBlock) | ||||
| 		block := model.Block{ | ||||
| 			ID:       initialID, | ||||
| 			RootID:   initialID, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 1, | ||||
| 			Type:     model.TypeBoard, | ||||
| 			Type:     model.TypeCard, | ||||
| 			Title:    "New title", | ||||
| 		} | ||||
|  | ||||
| 		newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) | ||||
| 		newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, newBlocks, 1) | ||||
| 		require.NotZero(t, newBlocks[0].ID) | ||||
| 		require.NotEqual(t, initialID, newBlocks[0].ID) | ||||
| 		blockID = newBlocks[0].ID | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+1) | ||||
| 		require.Len(t, blocks, 1) | ||||
|  | ||||
| 		blockIDs := make([]string, len(blocks)) | ||||
| 		for i, b := range blocks { | ||||
| @@ -307,43 +301,45 @@ func TestDeleteBlock(t *testing.T) { | ||||
| 		// id,insert_at on block history | ||||
| 		time.Sleep(10 * time.Millisecond) | ||||
|  | ||||
| 		_, resp := th.Client.DeleteBlock(blockID) | ||||
| 		_, resp := th.Client.DeleteBlock(board.ID, blockID) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount) | ||||
| 		require.Empty(t, blocks) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestUndeleteBlock(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelper(t).InitBasic() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocks() | ||||
| 	board := th.CreateBoard("team-id", model.BoardTypeOpen) | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	initialCount := len(blocks) | ||||
|  | ||||
| 	var blockID string | ||||
| 	t.Run("Create a block", func(t *testing.T) { | ||||
| 		initialID := utils.NewID(utils.IDTypeBlock) | ||||
| 		initialID := utils.NewID(utils.IDTypeBoard) | ||||
| 		block := model.Block{ | ||||
| 			ID:       initialID, | ||||
| 			RootID:   initialID, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 1, | ||||
| 			Type:     model.TypeBoard, | ||||
| 			Title:    "New title", | ||||
| 		} | ||||
|  | ||||
| 		newBlocks, resp := th.Client.InsertBlocks([]model.Block{block}) | ||||
| 		newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, newBlocks, 1) | ||||
| 		require.NotZero(t, newBlocks[0].ID) | ||||
| 		require.NotEqual(t, initialID, newBlocks[0].ID) | ||||
| 		blockID = newBlocks[0].ID | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+1) | ||||
|  | ||||
| @@ -359,10 +355,10 @@ func TestUndeleteBlock(t *testing.T) { | ||||
| 		// id,insert_at on block history | ||||
| 		time.Sleep(10 * time.Millisecond) | ||||
|  | ||||
| 		_, resp := th.Client.DeleteBlock(blockID) | ||||
| 		_, resp := th.Client.DeleteBlock(board.ID, blockID) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount) | ||||
| 	}) | ||||
| @@ -372,10 +368,10 @@ func TestUndeleteBlock(t *testing.T) { | ||||
| 		// id,insert_at on block history | ||||
| 		time.Sleep(10 * time.Millisecond) | ||||
|  | ||||
| 		_, resp := th.Client.UndeleteBlock(blockID) | ||||
| 		_, resp := th.Client.UndeleteBlock(board.ID, blockID) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+1) | ||||
| 	}) | ||||
| @@ -384,12 +380,10 @@ func TestUndeleteBlock(t *testing.T) { | ||||
| func TestGetSubtree(t *testing.T) { | ||||
| 	t.Skip("TODO: fix flaky test") | ||||
|  | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelperWithToken(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	blocks, resp := th.Client.GetBlocks() | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	initialCount := len(blocks) | ||||
| 	board := th.CreateBoard("team-id", model.BoardTypeOpen) | ||||
|  | ||||
| 	parentBlockID := utils.NewID(utils.IDTypeBlock) | ||||
| 	childBlockID1 := utils.NewID(utils.IDTypeBlock) | ||||
| @@ -399,14 +393,14 @@ func TestGetSubtree(t *testing.T) { | ||||
| 		newBlocks := []model.Block{ | ||||
| 			{ | ||||
| 				ID:       parentBlockID, | ||||
| 				RootID:   parentBlockID, | ||||
| 				BoardID:  board.ID, | ||||
| 				CreateAt: 1, | ||||
| 				UpdateAt: 1, | ||||
| 				Type:     model.TypeBoard, | ||||
| 				Type:     model.TypeCard, | ||||
| 			}, | ||||
| 			{ | ||||
| 				ID:       childBlockID1, | ||||
| 				RootID:   parentBlockID, | ||||
| 				BoardID:  board.ID, | ||||
| 				ParentID: parentBlockID, | ||||
| 				CreateAt: 2, | ||||
| 				UpdateAt: 2, | ||||
| @@ -414,7 +408,7 @@ func TestGetSubtree(t *testing.T) { | ||||
| 			}, | ||||
| 			{ | ||||
| 				ID:       childBlockID2, | ||||
| 				RootID:   parentBlockID, | ||||
| 				BoardID:  board.ID, | ||||
| 				ParentID: parentBlockID, | ||||
| 				CreateAt: 2, | ||||
| 				UpdateAt: 2, | ||||
| @@ -422,12 +416,12 @@ func TestGetSubtree(t *testing.T) { | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		_, resp := th.Client.InsertBlocks(newBlocks) | ||||
| 		_, resp := th.Client.InsertBlocks(board.ID, newBlocks) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		blocks, resp := th.Client.GetBlocks() | ||||
| 		blocks, resp := th.Client.GetBlocksForBoard(board.ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, initialCount+1) // GetBlocks returns root blocks (null ParentID) | ||||
| 		require.Len(t, blocks, 1) // GetBlocks returns root blocks (null ParentID) | ||||
|  | ||||
| 		blockIDs := make([]string, len(blocks)) | ||||
| 		for i, b := range blocks { | ||||
| @@ -437,7 +431,7 @@ func TestGetSubtree(t *testing.T) { | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Get subtree for parent ID", func(t *testing.T) { | ||||
| 		blocks, resp := th.Client.GetSubtree(parentBlockID) | ||||
| 		blocks, resp := th.Client.GetSubtree(board.ID, parentBlockID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, blocks, 3) | ||||
|  | ||||
|   | ||||
							
								
								
									
										1270
									
								
								server/integrationtests/board_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1270
									
								
								server/integrationtests/board_test.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										816
									
								
								server/integrationtests/boards_and_blocks_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										816
									
								
								server/integrationtests/boards_and_blocks_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,816 @@ | ||||
| package integrationtests | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestCreateBoardsAndBlocks(t *testing.T) { | ||||
| 	teamID := testTeamID | ||||
|  | ||||
| 	t.Run("a non authenticated user should be rejected", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).Start() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		newBab := &model.BoardsAndBlocks{ | ||||
| 			Boards: []*model.Board{}, | ||||
| 			Blocks: []model.Block{}, | ||||
| 		} | ||||
|  | ||||
| 		bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 		th.CheckUnauthorized(resp) | ||||
| 		require.Nil(t, bab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("invalid boards and blocks", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		t.Run("no boards", func(t *testing.T) { | ||||
| 			newBab := &model.BoardsAndBlocks{ | ||||
| 				Boards: []*model.Board{}, | ||||
| 				Blocks: []model.Block{ | ||||
| 					{ID: "block-id", BoardID: "board-id", Type: model.TypeCard}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("no blocks", func(t *testing.T) { | ||||
| 			newBab := &model.BoardsAndBlocks{ | ||||
| 				Boards: []*model.Board{ | ||||
| 					{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, | ||||
| 				}, | ||||
| 				Blocks: []model.Block{}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("blocks from nonexistent boards", func(t *testing.T) { | ||||
| 			newBab := &model.BoardsAndBlocks{ | ||||
| 				Boards: []*model.Board{ | ||||
| 					{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, | ||||
| 				}, | ||||
| 				Blocks: []model.Block{ | ||||
| 					{ID: "block-id", BoardID: "nonexistent-board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("boards with no IDs", func(t *testing.T) { | ||||
| 			newBab := &model.BoardsAndBlocks{ | ||||
| 				Boards: []*model.Board{ | ||||
| 					{ID: "board-id", TeamID: teamID, Type: model.BoardTypePrivate}, | ||||
| 					{TeamID: teamID, Type: model.BoardTypePrivate}, | ||||
| 				}, | ||||
| 				Blocks: []model.Block{ | ||||
| 					{ID: "block-id", BoardID: "board-id", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("boards from different teams", func(t *testing.T) { | ||||
| 			newBab := &model.BoardsAndBlocks{ | ||||
| 				Boards: []*model.Board{ | ||||
| 					{ID: "board-id-1", TeamID: "team-id-1", Type: model.BoardTypePrivate}, | ||||
| 					{ID: "board-id-2", TeamID: "team-id-2", Type: model.BoardTypePrivate}, | ||||
| 				}, | ||||
| 				Blocks: []model.Block{ | ||||
| 					{ID: "block-id", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("creating boards and blocks", func(t *testing.T) { | ||||
| 			newBab := &model.BoardsAndBlocks{ | ||||
| 				Boards: []*model.Board{ | ||||
| 					{ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen}, | ||||
| 					{ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate}, | ||||
| 				}, | ||||
| 				Blocks: []model.Block{ | ||||
| 					{ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 					{ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.CreateBoardsAndBlocks(newBab) | ||||
| 			th.CheckOK(resp) | ||||
| 			require.NotNil(t, bab) | ||||
|  | ||||
| 			require.Len(t, bab.Boards, 2) | ||||
| 			require.Len(t, bab.Blocks, 2) | ||||
|  | ||||
| 			// board 1 should have been created with a new ID, and its | ||||
| 			// block should be there too | ||||
| 			boardsTermPublic, resp := th.Client.SearchBoardsForTeam(teamID, "public") | ||||
| 			th.CheckOK(resp) | ||||
| 			require.Len(t, boardsTermPublic, 1) | ||||
| 			board1 := boardsTermPublic[0] | ||||
| 			require.Equal(t, "public board", board1.Title) | ||||
| 			require.Equal(t, model.BoardTypeOpen, board1.Type) | ||||
| 			require.NotEqual(t, "board-id-1", board1.ID) | ||||
| 			blocks1, err := th.Server.App().GetBlocksForBoard(board1.ID) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Len(t, blocks1, 1) | ||||
| 			require.Equal(t, "block 1", blocks1[0].Title) | ||||
|  | ||||
| 			// board 1 should have been created with a new ID, and its | ||||
| 			// block should be there too | ||||
| 			boardsTermPrivate, resp := th.Client.SearchBoardsForTeam(teamID, "private") | ||||
| 			th.CheckOK(resp) | ||||
| 			require.Len(t, boardsTermPrivate, 1) | ||||
| 			board2 := boardsTermPrivate[0] | ||||
| 			require.Equal(t, "private board", board2.Title) | ||||
| 			require.Equal(t, model.BoardTypePrivate, board2.Type) | ||||
| 			require.NotEqual(t, "board-id-2", board2.ID) | ||||
| 			blocks2, err := th.Server.App().GetBlocksForBoard(board2.ID) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Len(t, blocks2, 1) | ||||
| 			require.Equal(t, "block 2", blocks2[0].Title) | ||||
|  | ||||
| 			// user should be an admin of both newly created boards | ||||
| 			user1 := th.GetUser1() | ||||
| 			members1, err := th.Server.App().GetMembersForBoard(board1.ID) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Len(t, members1, 1) | ||||
| 			require.Equal(t, user1.ID, members1[0].UserID) | ||||
| 			members2, err := th.Server.App().GetMembersForBoard(board2.ID) | ||||
| 			require.NoError(t, err) | ||||
| 			require.Len(t, members2, 1) | ||||
| 			require.Equal(t, user1.ID, members2[0].UserID) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestPatchBoardsAndBlocks(t *testing.T) { | ||||
| 	teamID := "team-id" | ||||
|  | ||||
| 	t.Run("a non authenticated user should be rejected", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).Start() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		pbab := &model.PatchBoardsAndBlocks{} | ||||
|  | ||||
| 		bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 		th.CheckUnauthorized(resp) | ||||
| 		require.Nil(t, bab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("invalid patch boards and blocks", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		userID := th.GetUser1().ID | ||||
| 		initialTitle := "initial title 1" | ||||
| 		newTitle := "new title 1" | ||||
|  | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		newBlock1 := model.Block{ | ||||
| 			ID:      "block-id-1", | ||||
| 			BoardID: board1.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		newBlock2 := model.Block{ | ||||
| 			ID:      "block-id-2", | ||||
| 			BoardID: board2.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		t.Run("no board IDs", func(t *testing.T) { | ||||
| 			pbab := &model.PatchBoardsAndBlocks{ | ||||
| 				BoardIDs: []string{}, | ||||
| 				BoardPatches: []*model.BoardPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 				BlockIDs: []string{block1.ID, block2.ID}, | ||||
| 				BlockPatches: []*model.BlockPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("missmatch board IDs and patches", func(t *testing.T) { | ||||
| 			pbab := &model.PatchBoardsAndBlocks{ | ||||
| 				BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 				BoardPatches: []*model.BoardPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 				BlockIDs: []string{block1.ID, block2.ID}, | ||||
| 				BlockPatches: []*model.BlockPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("no block IDs", func(t *testing.T) { | ||||
| 			pbab := &model.PatchBoardsAndBlocks{ | ||||
| 				BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 				BoardPatches: []*model.BoardPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 				BlockIDs: []string{}, | ||||
| 				BlockPatches: []*model.BlockPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("missmatch block IDs and patches", func(t *testing.T) { | ||||
| 			pbab := &model.PatchBoardsAndBlocks{ | ||||
| 				BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 				BoardPatches: []*model.BoardPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 				BlockIDs: []string{block1.ID, block2.ID}, | ||||
| 				BlockPatches: []*model.BlockPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("block that doesn't belong to any board", func(t *testing.T) { | ||||
| 			pbab := &model.PatchBoardsAndBlocks{ | ||||
| 				BoardIDs: []string{board1.ID}, | ||||
| 				BoardPatches: []*model.BoardPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 				BlockIDs: []string{block1.ID, "board-id-2"}, | ||||
| 				BlockPatches: []*model.BlockPatch{ | ||||
| 					{Title: &newTitle}, | ||||
| 					{Title: &newTitle}, | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.Nil(t, bab) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("if the user doesn't have permissions for one of the boards, nothing should be updated", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		userID := th.GetUser1().ID | ||||
| 		initialTitle := "initial title 2" | ||||
| 		newTitle := "new title 2" | ||||
|  | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, userID, false) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		newBlock1 := model.Block{ | ||||
| 			ID:      "block-id-1", | ||||
| 			BoardID: board1.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		newBlock2 := model.Block{ | ||||
| 			ID:      "block-id-2", | ||||
| 			BoardID: board2.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		pbab := &model.PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 			BoardPatches: []*model.BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{block1.ID, block2.ID}, | ||||
| 			BlockPatches: []*model.BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 		th.CheckForbidden(resp) | ||||
| 		require.Nil(t, bab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("boards belonging to different teams should be rejected", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		userID := th.GetUser1().ID | ||||
| 		initialTitle := "initial title 3" | ||||
| 		newTitle := "new title 3" | ||||
|  | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: "different-team-id", | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		newBlock1 := model.Block{ | ||||
| 			ID:      "block-id-1", | ||||
| 			BoardID: board1.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		newBlock2 := model.Block{ | ||||
| 			ID:      "block-id-2", | ||||
| 			BoardID: board2.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		pbab := &model.PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 			BoardPatches: []*model.BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{block1.ID, "board-id-2"}, | ||||
| 			BlockPatches: []*model.BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 		th.CheckBadRequest(resp) | ||||
| 		require.Nil(t, bab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("patches should be rejected if one is invalid", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		userID := th.GetUser1().ID | ||||
| 		initialTitle := "initial title 4" | ||||
| 		newTitle := "new title 4" | ||||
|  | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, userID, false) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		newBlock1 := model.Block{ | ||||
| 			ID:      "block-id-1", | ||||
| 			BoardID: board1.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		newBlock2 := model.Block{ | ||||
| 			ID:      "block-id-2", | ||||
| 			BoardID: board2.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		var invalidPatchType model.BoardType = "invalid" | ||||
| 		invalidPatch := &model.BoardPatch{Type: &invalidPatchType} | ||||
|  | ||||
| 		pbab := &model.PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 			BoardPatches: []*model.BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				invalidPatch, | ||||
| 			}, | ||||
| 			BlockIDs: []string{block1.ID, "board-id-2"}, | ||||
| 			BlockPatches: []*model.BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 		th.CheckBadRequest(resp) | ||||
| 		require.Nil(t, bab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("patches should be rejected if there is a block that doesn't belong to the boards being patched", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		userID := th.GetUser1().ID | ||||
| 		initialTitle := "initial title" | ||||
| 		newTitle := "new title" | ||||
|  | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		newBlock1 := model.Block{ | ||||
| 			ID:      "block-id-1", | ||||
| 			BoardID: board1.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		newBlock2 := model.Block{ | ||||
| 			ID:      "block-id-2", | ||||
| 			BoardID: board2.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		pbab := &model.PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{board1.ID}, | ||||
| 			BoardPatches: []*model.BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{block1.ID, block2.ID}, | ||||
| 			BlockPatches: []*model.BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 		th.CheckBadRequest(resp) | ||||
| 		require.Nil(t, bab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("patches should be applied if they're valid and they're related", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		userID := th.GetUser1().ID | ||||
| 		initialTitle := "initial title" | ||||
| 		newTitle := "new title" | ||||
|  | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Title:  initialTitle, | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, userID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		newBlock1 := model.Block{ | ||||
| 			ID:      "block-id-1", | ||||
| 			BoardID: board1.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock1, userID)) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		newBlock2 := model.Block{ | ||||
| 			ID:      "block-id-2", | ||||
| 			BoardID: board2.ID, | ||||
| 			Title:   initialTitle, | ||||
| 		} | ||||
| 		require.NoError(t, th.Server.App().InsertBlock(newBlock2, userID)) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		pbab := &model.PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{board1.ID, board2.ID}, | ||||
| 			BoardPatches: []*model.BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{block1.ID, block2.ID}, | ||||
| 			BlockPatches: []*model.BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		bab, resp := th.Client.PatchBoardsAndBlocks(pbab) | ||||
| 		th.CheckOK(resp) | ||||
| 		require.NotNil(t, bab) | ||||
| 		require.Len(t, bab.Boards, 2) | ||||
| 		require.Len(t, bab.Blocks, 2) | ||||
|  | ||||
| 		// ensure that the entities have been updated | ||||
| 		rBoard1, err := th.Server.App().GetBoard(board1.ID) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, newTitle, rBoard1.Title) | ||||
| 		rBlock1, err := th.Server.App().GetBlockByID(block1.ID) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, newTitle, rBlock1.Title) | ||||
|  | ||||
| 		rBoard2, err := th.Server.App().GetBoard(board2.ID) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, newTitle, rBoard2.Title) | ||||
| 		rBlock2, err := th.Server.App().GetBlockByID(block2.ID) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Equal(t, newTitle, rBlock2.Title) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestDeleteBoardsAndBlocks(t *testing.T) { | ||||
| 	teamID := "team-id" | ||||
|  | ||||
| 	t.Run("a non authenticated user should be rejected", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).Start() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		dbab := &model.DeleteBoardsAndBlocks{} | ||||
|  | ||||
| 		success, resp := th.Client.DeleteBoardsAndBlocks(dbab) | ||||
| 		th.CheckUnauthorized(resp) | ||||
| 		require.False(t, success) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("invalid delete boards and blocks", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		// a board is required for the permission checks | ||||
| 		newBoard := &model.Board{ | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board) | ||||
|  | ||||
| 		t.Run("no boards", func(t *testing.T) { | ||||
| 			dbab := &model.DeleteBoardsAndBlocks{ | ||||
| 				Blocks: []string{"block-id-1"}, | ||||
| 			} | ||||
|  | ||||
| 			success, resp := th.Client.DeleteBoardsAndBlocks(dbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.False(t, success) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("no blocks", func(t *testing.T) { | ||||
| 			dbab := &model.DeleteBoardsAndBlocks{ | ||||
| 				Boards: []string{board.ID}, | ||||
| 			} | ||||
|  | ||||
| 			success, resp := th.Client.DeleteBoardsAndBlocks(dbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.False(t, success) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("boards from different teams", func(t *testing.T) { | ||||
| 			newOtherTeamsBoard := &model.Board{ | ||||
| 				TeamID: "another-team-id", | ||||
| 				Type:   model.BoardTypeOpen, | ||||
| 			} | ||||
| 			otherTeamsBoard, err := th.Server.App().CreateBoard(newOtherTeamsBoard, th.GetUser1().ID, true) | ||||
| 			require.NoError(t, err) | ||||
| 			require.NotNil(t, board) | ||||
|  | ||||
| 			dbab := &model.DeleteBoardsAndBlocks{ | ||||
| 				Boards: []string{board.ID, otherTeamsBoard.ID}, | ||||
| 				Blocks: []string{"block-id-1"}, | ||||
| 			} | ||||
|  | ||||
| 			success, resp := th.Client.DeleteBoardsAndBlocks(dbab) | ||||
| 			th.CheckBadRequest(resp) | ||||
| 			require.False(t, success) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("if the user has no permissions to one of the boards, nothing should be deleted", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		// the user is an admin of the first board | ||||
| 		newBoard1 := &model.Board{ | ||||
| 			Type: model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board1, err := th.Server.App().CreateBoard(newBoard1, th.GetUser1().ID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
|  | ||||
| 		// but not of the second | ||||
| 		newBoard2 := &model.Board{ | ||||
| 			Type: model.BoardTypeOpen, | ||||
| 		} | ||||
| 		board2, err := th.Server.App().CreateBoard(newBoard2, th.GetUser1().ID, false) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
|  | ||||
| 		dbab := &model.DeleteBoardsAndBlocks{ | ||||
| 			Boards: []string{board1.ID, board2.ID}, | ||||
| 			Blocks: []string{"block-id-1"}, | ||||
| 		} | ||||
|  | ||||
| 		success, resp := th.Client.DeleteBoardsAndBlocks(dbab) | ||||
| 		th.CheckForbidden(resp) | ||||
| 		require.False(t, success) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("all boards and blocks should be deleted if the request is correct", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		newBab := &model.BoardsAndBlocks{ | ||||
| 			Boards: []*model.Board{ | ||||
| 				{ID: "board-id-1", Title: "public board", TeamID: teamID, Type: model.BoardTypeOpen}, | ||||
| 				{ID: "board-id-2", Title: "private board", TeamID: teamID, Type: model.BoardTypePrivate}, | ||||
| 			}, | ||||
| 			Blocks: []model.Block{ | ||||
| 				{ID: "block-id-1", Title: "block 1", BoardID: "board-id-1", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 				{ID: "block-id-2", Title: "block 2", BoardID: "board-id-2", Type: model.TypeCard, CreateAt: 1, UpdateAt: 1}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		bab, err := th.Server.App().CreateBoardsAndBlocks(newBab, th.GetUser1().ID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Len(t, bab.Boards, 2) | ||||
| 		require.Len(t, bab.Blocks, 2) | ||||
|  | ||||
| 		// ensure that the entities have been successfully created | ||||
| 		board1, err := th.Server.App().GetBoard("board-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board1) | ||||
| 		block1, err := th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block1) | ||||
|  | ||||
| 		board2, err := th.Server.App().GetBoard("board-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board2) | ||||
| 		block2, err := th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, block2) | ||||
|  | ||||
| 		// call the API to delete boards and blocks | ||||
| 		dbab := &model.DeleteBoardsAndBlocks{ | ||||
| 			Boards: []string{"board-id-1", "board-id-2"}, | ||||
| 			Blocks: []string{"block-id-1", "block-id-2"}, | ||||
| 		} | ||||
|  | ||||
| 		success, resp := th.Client.DeleteBoardsAndBlocks(dbab) | ||||
| 		th.CheckOK(resp) | ||||
| 		require.True(t, success) | ||||
|  | ||||
| 		// ensure that the entities have been successfully deleted | ||||
| 		board1, err = th.Server.App().GetBoard("board-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Nil(t, board1) | ||||
| 		block1, err = th.Server.App().GetBlockByID("block-id-1") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Nil(t, block1) | ||||
|  | ||||
| 		board2, err = th.Server.App().GetBoard("board-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Nil(t, board2) | ||||
| 		block2, err = th.Server.App().GetBlockByID("block-id-2") | ||||
| 		require.NoError(t, err) | ||||
| 		require.Nil(t, block2) | ||||
| 	}) | ||||
| } | ||||
| @@ -4,19 +4,30 @@ import ( | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/api" | ||||
| 	"github.com/mattermost/focalboard/server/client" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/server" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions/localpermissions" | ||||
| 	"github.com/mattermost/focalboard/server/services/store/sqlstore" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	user1Username = "user1" | ||||
| 	user2Username = "user2" | ||||
| 	password      = "Pa$$word" | ||||
| ) | ||||
|  | ||||
| type TestHelper struct { | ||||
| 	T       *testing.T | ||||
| 	Server  *server.Server | ||||
| 	Client  *client.Client | ||||
| 	Client2 *client.Client | ||||
| @@ -80,11 +91,14 @@ func newTestServer(singleUserToken string) *server.Server { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	permissionsService := localpermissions.New(db, logger) | ||||
|  | ||||
| 	params := server.Params{ | ||||
| 		Cfg:             cfg, | ||||
| 		SingleUserToken: singleUserToken, | ||||
| 		DBStore:         db, | ||||
| 		Logger:          logger, | ||||
| 		Cfg:                cfg, | ||||
| 		SingleUserToken:    singleUserToken, | ||||
| 		DBStore:            db, | ||||
| 		Logger:             logger, | ||||
| 		PermissionsService: permissionsService, | ||||
| 	} | ||||
|  | ||||
| 	srv, err := server.New(params) | ||||
| @@ -95,23 +109,26 @@ func newTestServer(singleUserToken string) *server.Server { | ||||
| 	return srv | ||||
| } | ||||
|  | ||||
| func SetupTestHelper() *TestHelper { | ||||
| func SetupTestHelperWithToken(t *testing.T) *TestHelper { | ||||
| 	sessionToken := "TESTTOKEN" | ||||
| 	th := &TestHelper{} | ||||
| 	th := &TestHelper{T: t} | ||||
| 	th.Server = newTestServer(sessionToken) | ||||
| 	th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken) | ||||
| 	th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken) | ||||
| 	return th | ||||
| } | ||||
|  | ||||
| func SetupTestHelperWithoutToken() *TestHelper { | ||||
| 	th := &TestHelper{} | ||||
| func SetupTestHelper(t *testing.T) *TestHelper { | ||||
| 	th := &TestHelper{T: t} | ||||
| 	th.Server = newTestServer("") | ||||
| 	th.Client = client.NewClient(th.Server.Config().ServerRoot, "") | ||||
| 	th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "") | ||||
| 	return th | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) InitBasic() *TestHelper { | ||||
| // Start starts the test server and ensures that it's correctly | ||||
| // responding to requests before returning. | ||||
| func (th *TestHelper) Start() *TestHelper { | ||||
| 	go func() { | ||||
| 		if err := th.Server.Start(); err != nil { | ||||
| 			panic(err) | ||||
| @@ -144,51 +161,28 @@ func (th *TestHelper) InitBasic() *TestHelper { | ||||
| 	return th | ||||
| } | ||||
|  | ||||
| var ErrRegisterFail = errors.New("register failed") | ||||
| // InitBasic starts the test server and initializes the clients of the | ||||
| // helper, registering them and logging them into the system. | ||||
| func (th *TestHelper) InitBasic() *TestHelper { | ||||
| 	th.Start() | ||||
|  | ||||
| func (th *TestHelper) InitUsers(username1 string, username2 string) error { | ||||
| 	workspace, err := th.Server.App().GetRootWorkspace() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	// user1 | ||||
| 	th.RegisterAndLogin(th.Client, user1Username, "user1@sample.com", password, "") | ||||
|  | ||||
| 	clients := []*client.Client{th.Client, th.Client2} | ||||
| 	usernames := []string{username1, username2} | ||||
| 	// get token | ||||
| 	team, resp := th.Client.GetTeam("0") | ||||
| 	th.CheckOK(resp) | ||||
| 	require.NotNil(th.T, team) | ||||
| 	require.NotNil(th.T, team.SignupToken) | ||||
|  | ||||
| 	for i, client := range clients { | ||||
| 		// register a new user | ||||
| 		password := utils.NewID(utils.IDTypeNone) | ||||
| 		registerRequest := &api.RegisterRequest{ | ||||
| 			Username: usernames[i], | ||||
| 			Email:    usernames[i] + "@example.com", | ||||
| 			Password: password, | ||||
| 			Token:    workspace.SignupToken, | ||||
| 		} | ||||
| 		success, resp := client.Register(registerRequest) | ||||
| 		if resp.Error != nil { | ||||
| 			return resp.Error | ||||
| 		} | ||||
| 		if !success { | ||||
| 			return ErrRegisterFail | ||||
| 		} | ||||
| 	// user2 | ||||
| 	th.RegisterAndLogin(th.Client2, user2Username, "user2@sample.com", password, team.SignupToken) | ||||
|  | ||||
| 		// login | ||||
| 		loginRequest := &api.LoginRequest{ | ||||
| 			Type:     "normal", | ||||
| 			Username: registerRequest.Username, | ||||
| 			Email:    registerRequest.Email, | ||||
| 			Password: registerRequest.Password, | ||||
| 		} | ||||
| 		data, resp := client.Login(loginRequest) | ||||
| 		if resp.Error != nil { | ||||
| 			return resp.Error | ||||
| 		} | ||||
|  | ||||
| 		client.Token = data.Token | ||||
| 	} | ||||
| 	return nil | ||||
| 	return th | ||||
| } | ||||
|  | ||||
| var ErrRegisterFail = errors.New("register failed") | ||||
|  | ||||
| func (th *TestHelper) TearDown() { | ||||
| 	defer func() { _ = th.Server.Logger().Shutdown() }() | ||||
|  | ||||
| @@ -198,4 +192,96 @@ func (th *TestHelper) TearDown() { | ||||
| 	} | ||||
|  | ||||
| 	os.RemoveAll(th.Server.Config().FilesPath) | ||||
|  | ||||
| 	if err := os.Remove(th.Server.Config().DBConfigString); err == nil { | ||||
| 		th.Server.Logger().Debug("Removed test database", mlog.String("file", th.Server.Config().DBConfigString)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) RegisterAndLogin(client *client.Client, username, email, password, token string) { | ||||
| 	req := &api.RegisterRequest{ | ||||
| 		Username: username, | ||||
| 		Email:    email, | ||||
| 		Password: password, | ||||
| 		Token:    token, | ||||
| 	} | ||||
|  | ||||
| 	success, resp := th.Client.Register(req) | ||||
| 	th.CheckOK(resp) | ||||
| 	require.True(th.T, success) | ||||
|  | ||||
| 	th.Login(client, username, password) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) Login(client *client.Client, username, password string) { | ||||
| 	req := &api.LoginRequest{ | ||||
| 		Type:     "normal", | ||||
| 		Username: username, | ||||
| 		Password: password, | ||||
| 	} | ||||
| 	data, resp := client.Login(req) | ||||
| 	th.CheckOK(resp) | ||||
| 	require.NotNil(th.T, data) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) Login1() { | ||||
| 	th.Login(th.Client, user1Username, password) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) Login2() { | ||||
| 	th.Login(th.Client2, user2Username, password) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) Logout(client *client.Client) { | ||||
| 	client.Token = "" | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) Me(client *client.Client) *model.User { | ||||
| 	user, resp := client.GetMe() | ||||
| 	th.CheckOK(resp) | ||||
| 	require.NotNil(th.T, user) | ||||
| 	return user | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) CreateBoard(teamID string, boardType model.BoardType) *model.Board { | ||||
| 	newBoard := &model.Board{ | ||||
| 		TeamID: teamID, | ||||
| 		Type:   boardType, | ||||
| 	} | ||||
| 	board, resp := th.Client.CreateBoard(newBoard) | ||||
| 	th.CheckOK(resp) | ||||
| 	return board | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) GetUser1() *model.User { | ||||
| 	return th.Me(th.Client) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) GetUser2() *model.User { | ||||
| 	return th.Me(th.Client2) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) CheckOK(r *client.Response) { | ||||
| 	require.Equal(th.T, http.StatusOK, r.StatusCode) | ||||
| 	require.NoError(th.T, r.Error) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) CheckBadRequest(r *client.Response) { | ||||
| 	require.Equal(th.T, http.StatusBadRequest, r.StatusCode) | ||||
| 	require.Error(th.T, r.Error) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) CheckNotFound(r *client.Response) { | ||||
| 	require.Equal(th.T, http.StatusNotFound, r.StatusCode) | ||||
| 	require.Error(th.T, r.Error) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) CheckUnauthorized(r *client.Response) { | ||||
| 	require.Equal(th.T, http.StatusUnauthorized, r.StatusCode) | ||||
| 	require.Error(th.T, r.Error) | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) CheckForbidden(r *client.Response) { | ||||
| 	require.Equal(th.T, http.StatusForbidden, r.StatusCode) | ||||
| 	require.Error(th.T, r.Error) | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| package integrationtests | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| @@ -9,60 +10,84 @@ import ( | ||||
| ) | ||||
|  | ||||
| func TestSharing(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelper(t).InitBasic() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	rootID := utils.NewID(utils.IDTypeBlock) | ||||
| 	var boardID string | ||||
| 	token := utils.NewID(utils.IDTypeToken) | ||||
|  | ||||
| 	t.Run("an unauthenticated client should not be able to get a sharing", func(t *testing.T) { | ||||
| 		th.Logout(th.Client) | ||||
|  | ||||
| 		sharing, resp := th.Client.GetSharing("board-id") | ||||
| 		th.CheckUnauthorized(resp) | ||||
| 		require.Nil(t, sharing) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Check no initial sharing", func(t *testing.T) { | ||||
| 		sharing, resp := th.Client.GetSharing(rootID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Empty(t, sharing.ID) | ||||
| 		require.False(t, sharing.Enabled) | ||||
| 		th.Login1() | ||||
|  | ||||
| 		teamID := "0" | ||||
| 		newBoard := &model.Board{ | ||||
| 			TeamID: teamID, | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 		} | ||||
|  | ||||
| 		board, err := th.Server.App().CreateBoard(newBoard, th.GetUser1().ID, true) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, board) | ||||
| 		boardID = board.ID | ||||
|  | ||||
| 		s, err := th.Server.App().GetSharing(boardID) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Nil(t, s) | ||||
|  | ||||
| 		sharing, resp := th.Client.GetSharing(boardID) | ||||
| 		th.CheckNotFound(resp) | ||||
| 		require.Nil(t, sharing) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("POST sharing, config = false", func(t *testing.T) { | ||||
| 		sharing := model.Sharing{ | ||||
| 			ID:       rootID, | ||||
| 			ID:       boardID, | ||||
| 			Token:    token, | ||||
| 			Enabled:  true, | ||||
| 			UpdateAt: 1, | ||||
| 		} | ||||
|  | ||||
| 		// it will fail with default config | ||||
| 		success, resp := th.Client.PostSharing(sharing) | ||||
| 		success, resp := th.Client.PostSharing(&sharing) | ||||
| 		require.False(t, success) | ||||
| 		require.Error(t, resp.Error) | ||||
|  | ||||
| 		t.Run("GET sharing", func(t *testing.T) { | ||||
| 			sharing, resp := th.Client.GetSharing(rootID) | ||||
| 			// Expect no error, but no Id returned | ||||
| 			require.NoError(t, resp.Error) | ||||
| 			require.NotNil(t, sharing) | ||||
| 			require.Equal(t, "", sharing.ID) | ||||
| 			sharing, resp := th.Client.GetSharing(boardID) | ||||
| 			// Expect not found error | ||||
| 			require.Error(t, resp.Error) | ||||
| 			require.Equal(t, resp.StatusCode, http.StatusNotFound) | ||||
| 			require.Nil(t, sharing) | ||||
| 		}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("POST sharing, config = true", func(t *testing.T) { | ||||
| 		th.Server.Config().EnablePublicSharedBoards = true | ||||
| 		sharing := model.Sharing{ | ||||
| 			ID:       rootID, | ||||
| 			ID:       boardID, | ||||
| 			Token:    token, | ||||
| 			Enabled:  true, | ||||
| 			UpdateAt: 1, | ||||
| 		} | ||||
|  | ||||
| 		// it will succeed with updated config | ||||
| 		success, resp := th.Client.PostSharing(sharing) | ||||
| 		success, resp := th.Client.PostSharing(&sharing) | ||||
| 		require.True(t, success) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		t.Run("GET sharing", func(t *testing.T) { | ||||
| 			sharing, resp := th.Client.GetSharing(rootID) | ||||
| 			sharing, resp := th.Client.GetSharing(boardID) | ||||
| 			require.NoError(t, resp.Error) | ||||
| 			require.NotNil(t, sharing) | ||||
| 			require.Equal(t, sharing.ID, rootID) | ||||
| 			require.Equal(t, sharing.ID, boardID) | ||||
| 			require.True(t, sharing.Enabled) | ||||
| 			require.Equal(t, sharing.Token, token) | ||||
| 		}) | ||||
|   | ||||
| @@ -6,13 +6,12 @@ import ( | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/client" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func createTestSubscriptions(client *client.Client, num int, workspaceID string) ([]*model.Subscription, string, error) { | ||||
| func createTestSubscriptions(client *client.Client, num int) ([]*model.Subscription, string, error) { | ||||
| 	newSubs := make([]*model.Subscription, 0, num) | ||||
|  | ||||
| 	user, resp := client.GetMe() | ||||
| @@ -20,29 +19,27 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string) | ||||
| 		return nil, "", fmt.Errorf("cannot get current user: %w", resp.Error) | ||||
| 	} | ||||
|  | ||||
| 	board := model.Block{ | ||||
| 		ID:       utils.NewID(utils.IDTypeBoard), | ||||
| 		RootID:   workspaceID, | ||||
| 	board := &model.Board{ | ||||
| 		TeamID:   "0", | ||||
| 		Type:     model.BoardTypeOpen, | ||||
| 		CreateAt: 1, | ||||
| 		UpdateAt: 1, | ||||
| 		Type:     model.TypeBoard, | ||||
| 	} | ||||
| 	boards, resp := client.InsertBlocks([]model.Block{board}) | ||||
| 	board, resp = client.CreateBoard(board) | ||||
| 	if resp.Error != nil { | ||||
| 		return nil, "", fmt.Errorf("cannot insert test board block: %w", resp.Error) | ||||
| 	} | ||||
| 	board = boards[0] | ||||
|  | ||||
| 	for n := 0; n < num; n++ { | ||||
| 		newBlock := model.Block{ | ||||
| 			ID:       utils.NewID(utils.IDTypeCard), | ||||
| 			RootID:   board.ID, | ||||
| 			BoardID:  board.ID, | ||||
| 			CreateAt: 1, | ||||
| 			UpdateAt: 1, | ||||
| 			Type:     model.TypeCard, | ||||
| 		} | ||||
|  | ||||
| 		newBlocks, resp := client.InsertBlocks([]model.Block{newBlock}) | ||||
| 		newBlocks, resp := client.InsertBlocks(board.ID, []model.Block{newBlock}) | ||||
| 		if resp.Error != nil { | ||||
| 			return nil, "", fmt.Errorf("cannot insert test card block: %w", resp.Error) | ||||
| 		} | ||||
| @@ -51,12 +48,11 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string) | ||||
| 		sub := &model.Subscription{ | ||||
| 			BlockType:      newBlock.Type, | ||||
| 			BlockID:        newBlock.ID, | ||||
| 			WorkspaceID:    workspaceID, | ||||
| 			SubscriberType: model.SubTypeUser, | ||||
| 			SubscriberID:   user.ID, | ||||
| 		} | ||||
|  | ||||
| 		subNew, resp := client.CreateSubscription(workspaceID, sub) | ||||
| 		subNew, resp := client.CreateSubscription(sub) | ||||
| 		if resp.Error != nil { | ||||
| 			return nil, "", resp.Error | ||||
| 		} | ||||
| @@ -66,20 +62,16 @@ func createTestSubscriptions(client *client.Client, num int, workspaceID string) | ||||
| } | ||||
|  | ||||
| func TestCreateSubscription(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelper(t).InitBasic() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	container := store.Container{ | ||||
| 		WorkspaceID: utils.NewID(utils.IDTypeWorkspace), | ||||
| 	} | ||||
|  | ||||
| 	t.Run("Create valid subscription", func(t *testing.T) { | ||||
| 		subs, userID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID) | ||||
| 		subs, userID, err := createTestSubscriptions(th.Client, 5) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Len(t, subs, 5) | ||||
|  | ||||
| 		// fetch the newly created subscriptions and compare | ||||
| 		subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID) | ||||
| 		subsFound, resp := th.Client.GetSubscriptions(userID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, subsFound, 5) | ||||
| 		assert.ElementsMatch(t, subs, subsFound) | ||||
| @@ -90,47 +82,38 @@ func TestCreateSubscription(t *testing.T) { | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		sub := &model.Subscription{ | ||||
| 			WorkspaceID:  container.WorkspaceID, | ||||
| 			SubscriberID: user.ID, | ||||
| 		} | ||||
| 		_, resp = th.Client.CreateSubscription(container.WorkspaceID, sub) | ||||
| 		_, resp = th.Client.CreateSubscription(sub) | ||||
| 		require.Error(t, resp.Error) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Create subscription for another user", func(t *testing.T) { | ||||
| 		sub := &model.Subscription{ | ||||
| 			WorkspaceID:  container.WorkspaceID, | ||||
| 			SubscriberID: utils.NewID(utils.IDTypeUser), | ||||
| 		} | ||||
| 		_, resp := th.Client.CreateSubscription(container.WorkspaceID, sub) | ||||
| 		_, resp := th.Client.CreateSubscription(sub) | ||||
| 		require.Error(t, resp.Error) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGetSubscriptions(t *testing.T) { | ||||
| 	th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 	th := SetupTestHelper(t).InitBasic() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	err := th.InitUsers("user1", "user2") | ||||
| 	require.NoError(t, err, "failed to init users") | ||||
|  | ||||
| 	container := store.Container{ | ||||
| 		WorkspaceID: utils.NewID(utils.IDTypeWorkspace), | ||||
| 	} | ||||
|  | ||||
| 	t.Run("Get subscriptions for user", func(t *testing.T) { | ||||
| 		mySubs, user1ID, err := createTestSubscriptions(th.Client, 5, container.WorkspaceID) | ||||
| 		mySubs, user1ID, err := createTestSubscriptions(th.Client, 5) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Len(t, mySubs, 5) | ||||
|  | ||||
| 		// create more subscriptions with different user | ||||
| 		otherSubs, _, err := createTestSubscriptions(th.Client2, 10, container.WorkspaceID) | ||||
| 		otherSubs, _, err := createTestSubscriptions(th.Client2, 10) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Len(t, otherSubs, 10) | ||||
|  | ||||
| 		// fetch the newly created subscriptions for current user, making sure only | ||||
| 		// the ones created for the current user are returned. | ||||
| 		subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, user1ID) | ||||
| 		subsFound, resp := th.Client.GetSubscriptions(user1ID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, subsFound, 5) | ||||
| 		assert.ElementsMatch(t, mySubs, subsFound) | ||||
| @@ -138,23 +121,19 @@ func TestGetSubscriptions(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestDeleteSubscription(t *testing.T) { | ||||
| 	th := SetupTestHelper().InitBasic() | ||||
| 	th := SetupTestHelper(t).InitBasic() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	container := store.Container{ | ||||
| 		WorkspaceID: utils.NewID(utils.IDTypeWorkspace), | ||||
| 	} | ||||
|  | ||||
| 	t.Run("Delete valid subscription", func(t *testing.T) { | ||||
| 		subs, userID, err := createTestSubscriptions(th.Client, 3, container.WorkspaceID) | ||||
| 		subs, userID, err := createTestSubscriptions(th.Client, 3) | ||||
| 		require.NoError(t, err) | ||||
| 		require.Len(t, subs, 3) | ||||
|  | ||||
| 		resp := th.Client.DeleteSubscription(container.WorkspaceID, subs[1].BlockID, userID) | ||||
| 		resp := th.Client.DeleteSubscription(subs[1].BlockID, userID) | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		// fetch the subscriptions and ensure the list is correct | ||||
| 		subsFound, resp := th.Client.GetSubscriptions(container.WorkspaceID, userID) | ||||
| 		subsFound, resp := th.Client.GetSubscriptions(userID) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		require.Len(t, subsFound, 2) | ||||
|  | ||||
| @@ -167,7 +146,7 @@ func TestDeleteSubscription(t *testing.T) { | ||||
| 		user, resp := th.Client.GetMe() | ||||
| 		require.NoError(t, resp.Error) | ||||
|  | ||||
| 		resp = th.Client.DeleteSubscription(container.WorkspaceID, "bogus", user.ID) | ||||
| 		resp = th.Client.DeleteSubscription("bogus", user.ID) | ||||
| 		require.Error(t, resp.Error) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/api" | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| @@ -16,7 +17,7 @@ const ( | ||||
| ) | ||||
|  | ||||
| func TestUserRegister(t *testing.T) { | ||||
| 	th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 	th := SetupTestHelper(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	// register | ||||
| @@ -29,14 +30,14 @@ func TestUserRegister(t *testing.T) { | ||||
| 	require.NoError(t, resp.Error) | ||||
| 	require.True(t, success) | ||||
|  | ||||
| 	// register again will failed | ||||
| 	// register again will fail | ||||
| 	success, resp = th.Client.Register(registerRequest) | ||||
| 	require.Error(t, resp.Error) | ||||
| 	require.False(t, success) | ||||
| } | ||||
|  | ||||
| func TestUserLogin(t *testing.T) { | ||||
| 	th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 	th := SetupTestHelper(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	t.Run("with nonexist user", func(t *testing.T) { | ||||
| @@ -78,7 +79,7 @@ func TestUserLogin(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestGetMe(t *testing.T) { | ||||
| 	th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 	th := SetupTestHelper(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	t.Run("not login yet", func(t *testing.T) { | ||||
| @@ -120,7 +121,7 @@ func TestGetMe(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestGetUser(t *testing.T) { | ||||
| 	th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 	th := SetupTestHelper(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	// register | ||||
| @@ -165,7 +166,7 @@ func TestGetUser(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestUserChangePassword(t *testing.T) { | ||||
| 	th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 	th := SetupTestHelper(t).Start() | ||||
| 	defer th.TearDown() | ||||
|  | ||||
| 	// register | ||||
| @@ -210,30 +211,58 @@ func randomBytes(t *testing.T, n int) []byte { | ||||
| 	return bb | ||||
| } | ||||
|  | ||||
| func TestWorkspaceUploadFile(t *testing.T) { | ||||
| func TestTeamUploadFile(t *testing.T) { | ||||
| 	t.Run("no permission", func(t *testing.T) { // native auth, but not login | ||||
| 		th := SetupTestHelperWithoutToken().InitBasic() | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		workspaceID := "0" | ||||
| 		rootID := utils.NewID(utils.IDTypeBlock) | ||||
| 		teamID := "0" | ||||
| 		boardID := utils.NewID(utils.IDTypeBoard) | ||||
| 		data := randomBytes(t, 1024) | ||||
| 		result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data)) | ||||
| 		result, resp := th.Client.TeamUploadFile(teamID, boardID, bytes.NewReader(data)) | ||||
| 		require.Error(t, resp.Error) | ||||
| 		require.Nil(t, result) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("success", func(t *testing.T) { // single token auth | ||||
| 		th := SetupTestHelper().InitBasic() | ||||
| 	t.Run("a board admin should be able to update a file", func(t *testing.T) { // single token auth | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		workspaceID := "0" | ||||
| 		rootID := utils.NewID(utils.IDTypeBlock) | ||||
| 		teamID := "0" | ||||
| 		newBoard := &model.Board{ | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 			TeamID: teamID, | ||||
| 		} | ||||
| 		board, resp := th.Client.CreateBoard(newBoard) | ||||
| 		th.CheckOK(resp) | ||||
| 		require.NotNil(t, board) | ||||
|  | ||||
| 		data := randomBytes(t, 1024) | ||||
| 		result, resp := th.Client.WorkspaceUploadFile(workspaceID, rootID, bytes.NewReader(data)) | ||||
| 		require.NoError(t, resp.Error) | ||||
| 		result, resp := th.Client.TeamUploadFile(teamID, board.ID, bytes.NewReader(data)) | ||||
| 		th.CheckOK(resp) | ||||
| 		require.NotNil(t, result) | ||||
| 		require.NotEmpty(t, result.FileID) | ||||
| 		// TODO get the uploaded file | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("user that doesn't belong to the board should not be able to upload a file", func(t *testing.T) { | ||||
| 		th := SetupTestHelper(t).InitBasic() | ||||
| 		defer th.TearDown() | ||||
|  | ||||
| 		teamID := "0" | ||||
| 		newBoard := &model.Board{ | ||||
| 			Type:   model.BoardTypeOpen, | ||||
| 			TeamID: teamID, | ||||
| 		} | ||||
| 		board, resp := th.Client.CreateBoard(newBoard) | ||||
| 		th.CheckOK(resp) | ||||
| 		require.NotNil(t, board) | ||||
|  | ||||
| 		data := randomBytes(t, 1024) | ||||
|  | ||||
| 		// a user that doesn't belong to the board tries to upload the file | ||||
| 		result, resp := th.Client2.TeamUploadFile(teamID, board.ID, bytes.NewReader(data)) | ||||
| 		th.CheckForbidden(resp) | ||||
| 		require.Nil(t, result) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import ( | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/server" | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions/localpermissions" | ||||
| ) | ||||
| import ( | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| @@ -145,11 +146,14 @@ func main() { | ||||
| 		logger.Fatal("server.NewStore ERROR", mlog.Err(err)) | ||||
| 	} | ||||
|  | ||||
| 	permissionsService := localpermissions.New(db, logger) | ||||
|  | ||||
| 	params := server.Params{ | ||||
| 		Cfg:             config, | ||||
| 		SingleUserToken: singleUserToken, | ||||
| 		DBStore:         db, | ||||
| 		Logger:          logger, | ||||
| 		Cfg:                config, | ||||
| 		SingleUserToken:    singleUserToken, | ||||
| 		DBStore:            db, | ||||
| 		Logger:             logger, | ||||
| 		PermissionsService: permissionsService, | ||||
| 	} | ||||
|  | ||||
| 	server, err := server.New(params) | ||||
| @@ -233,11 +237,14 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db | ||||
| 		logger.Fatal("server.NewStore ERROR", mlog.Err(err)) | ||||
| 	} | ||||
|  | ||||
| 	permissionsService := localpermissions.New(db, logger) | ||||
|  | ||||
| 	params := server.Params{ | ||||
| 		Cfg:             config, | ||||
| 		SingleUserToken: singleUserToken, | ||||
| 		DBStore:         db, | ||||
| 		Logger:          logger, | ||||
| 		Cfg:                config, | ||||
| 		SingleUserToken:    singleUserToken, | ||||
| 		DBStore:            db, | ||||
| 		Logger:             logger, | ||||
| 		PermissionsService: permissionsService, | ||||
| 	} | ||||
|  | ||||
| 	pServer, err = server.New(params) | ||||
|   | ||||
| @@ -19,10 +19,6 @@ type Block struct { | ||||
| 	// required: false | ||||
| 	ParentID string `json:"parentId"` | ||||
|  | ||||
| 	// The id for this block's root block | ||||
| 	// required: true | ||||
| 	RootID string `json:"rootId"` | ||||
|  | ||||
| 	// The id for user who created this block | ||||
| 	// required: true | ||||
| 	CreatedBy string `json:"createdBy"` | ||||
| @@ -59,9 +55,13 @@ type Block struct { | ||||
| 	// required: false | ||||
| 	DeleteAt int64 `json:"deleteAt"` | ||||
|  | ||||
| 	// The workspace id that the block belongs to | ||||
| 	// Deprecated. The workspace id that the block belongs to | ||||
| 	// required: false | ||||
| 	WorkspaceID string `json:"-"` | ||||
|  | ||||
| 	// The board id that the block belongs to | ||||
| 	// required: true | ||||
| 	WorkspaceID string `json:"workspaceId"` | ||||
| 	BoardID string `json:"boardId"` | ||||
| } | ||||
|  | ||||
| // BlockPatch is a patch for modify blocks | ||||
| @@ -71,10 +71,6 @@ type BlockPatch struct { | ||||
| 	// required: false | ||||
| 	ParentID *string `json:"parentId"` | ||||
|  | ||||
| 	// The id for this block's root block | ||||
| 	// required: false | ||||
| 	RootID *string `json:"rootId"` | ||||
|  | ||||
| 	// The schema version of this block | ||||
| 	// required: false | ||||
| 	Schema *int64 `json:"schema"` | ||||
| @@ -94,6 +90,10 @@ type BlockPatch struct { | ||||
| 	// The block removed fields | ||||
| 	// required: false | ||||
| 	DeletedFields []string `json:"deletedFields"` | ||||
|  | ||||
| 	// The board id that the block belongs to | ||||
| 	// required: false | ||||
| 	BoardID *string `json:"boardId"` | ||||
| } | ||||
|  | ||||
| // BlockPatchBatch is a batch of IDs and patches for modify blocks | ||||
| @@ -106,11 +106,11 @@ type BlockPatchBatch struct { | ||||
| 	BlockPatches []BlockPatch `json:"block_patches"` | ||||
| } | ||||
|  | ||||
| // BlockModifier is a callback that can modify each block during an import. | ||||
| // BoardModifier is a callback that can modify each board during an import. | ||||
| // A cache of arbitrary data will be passed for each call and any changes | ||||
| // to the cache will be preserved for the next call. | ||||
| // Return true to import the block or false to skip import. | ||||
| type BlockModifier func(block *Block, cache map[string]interface{}) bool | ||||
| type BoardModifier func(board *Board, cache map[string]interface{}) bool | ||||
|  | ||||
| func BlocksFromJSON(data io.Reader) []Block { | ||||
| 	var blocks []Block | ||||
| @@ -123,12 +123,12 @@ func (b Block) LogClone() interface{} { | ||||
| 	return struct { | ||||
| 		ID       string | ||||
| 		ParentID string | ||||
| 		RootID   string | ||||
| 		BoardID  string | ||||
| 		Type     BlockType | ||||
| 	}{ | ||||
| 		ID:       b.ID, | ||||
| 		ParentID: b.ParentID, | ||||
| 		RootID:   b.RootID, | ||||
| 		BoardID:  b.BoardID, | ||||
| 		Type:     b.Type, | ||||
| 	} | ||||
| } | ||||
| @@ -139,8 +139,8 @@ func (p *BlockPatch) Patch(block *Block) *Block { | ||||
| 		block.ParentID = *p.ParentID | ||||
| 	} | ||||
|  | ||||
| 	if p.RootID != nil { | ||||
| 		block.RootID = *p.RootID | ||||
| 	if p.BoardID != nil { | ||||
| 		block.BoardID = *p.BoardID | ||||
| 	} | ||||
|  | ||||
| 	if p.Schema != nil { | ||||
|   | ||||
| @@ -20,61 +20,61 @@ func TestGenerateBlockIDs(t *testing.T) { | ||||
| 		blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) | ||||
|  | ||||
| 		require.NotEqual(t, blockID, blocks[0].ID) | ||||
| 		require.Zero(t, blocks[0].RootID) | ||||
| 		require.Zero(t, blocks[0].BoardID) | ||||
| 		require.Zero(t, blocks[0].ParentID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Should generate a new ID for a single block with references", func(t *testing.T) { | ||||
| 		blockID := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID := utils.NewID(utils.IDTypeBlock) | ||||
| 		blocks := []Block{{ID: blockID, RootID: rootID, ParentID: parentID}} | ||||
| 		blocks := []Block{{ID: blockID, BoardID: boardID, ParentID: parentID}} | ||||
|  | ||||
| 		blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) | ||||
|  | ||||
| 		require.NotEqual(t, blockID, blocks[0].ID) | ||||
| 		require.Equal(t, rootID, blocks[0].RootID) | ||||
| 		require.Equal(t, boardID, blocks[0].BoardID) | ||||
| 		require.Equal(t, parentID, blocks[0].ParentID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Should generate IDs and link multiple blocks with existing references", func(t *testing.T) { | ||||
| 		blockID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1} | ||||
| 		block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} | ||||
|  | ||||
| 		blockID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID2 := blockID1 | ||||
| 		boardID2 := blockID1 | ||||
| 		parentID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2} | ||||
| 		block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} | ||||
|  | ||||
| 		blocks := []Block{block1, block2} | ||||
|  | ||||
| 		blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) | ||||
|  | ||||
| 		require.NotEqual(t, blockID1, blocks[0].ID) | ||||
| 		require.Equal(t, rootID1, blocks[0].RootID) | ||||
| 		require.Equal(t, boardID1, blocks[0].BoardID) | ||||
| 		require.Equal(t, parentID1, blocks[0].ParentID) | ||||
|  | ||||
| 		require.NotEqual(t, blockID2, blocks[1].ID) | ||||
| 		require.NotEqual(t, rootID2, blocks[1].RootID) | ||||
| 		require.NotEqual(t, boardID2, blocks[1].BoardID) | ||||
| 		require.Equal(t, parentID2, blocks[1].ParentID) | ||||
|  | ||||
| 		// blockID1 was referenced, so it should still be after the ID | ||||
| 		// changes | ||||
| 		require.Equal(t, blocks[0].ID, blocks[1].RootID) | ||||
| 		require.Equal(t, blocks[0].ID, blocks[1].BoardID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Should generate new IDs but not modify nonexisting references", func(t *testing.T) { | ||||
| 		blockID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID1 := "" | ||||
| 		boardID1 := "" | ||||
| 		parentID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1} | ||||
| 		block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} | ||||
|  | ||||
| 		blockID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID2 := "" | ||||
| 		block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2} | ||||
| 		block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} | ||||
|  | ||||
| 		blocks := []Block{block1, block2} | ||||
|  | ||||
| @@ -82,37 +82,37 @@ func TestGenerateBlockIDs(t *testing.T) { | ||||
|  | ||||
| 		// only the IDs should have changed | ||||
| 		require.NotEqual(t, blockID1, blocks[0].ID) | ||||
| 		require.Zero(t, blocks[0].RootID) | ||||
| 		require.Zero(t, blocks[0].BoardID) | ||||
| 		require.Equal(t, parentID1, blocks[0].ParentID) | ||||
|  | ||||
| 		require.NotEqual(t, blockID2, blocks[1].ID) | ||||
| 		require.Equal(t, rootID2, blocks[1].RootID) | ||||
| 		require.Equal(t, boardID2, blocks[1].BoardID) | ||||
| 		require.Zero(t, blocks[1].ParentID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Should modify correctly multiple blocks with existing and nonexisting references", func(t *testing.T) { | ||||
| 		blockID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block1 := Block{ID: blockID1, RootID: rootID1, ParentID: parentID1} | ||||
| 		block1 := Block{ID: blockID1, BoardID: boardID1, ParentID: parentID1} | ||||
|  | ||||
| 		// linked to 1 | ||||
| 		blockID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID2 := blockID1 | ||||
| 		boardID2 := blockID1 | ||||
| 		parentID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block2 := Block{ID: blockID2, RootID: rootID2, ParentID: parentID2} | ||||
| 		block2 := Block{ID: blockID2, BoardID: boardID2, ParentID: parentID2} | ||||
|  | ||||
| 		// linked to 2 | ||||
| 		blockID3 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID3 := blockID2 | ||||
| 		boardID3 := blockID2 | ||||
| 		parentID3 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block3 := Block{ID: blockID3, RootID: rootID3, ParentID: parentID3} | ||||
| 		block3 := Block{ID: blockID3, BoardID: boardID3, ParentID: parentID3} | ||||
|  | ||||
| 		// linked to 1 | ||||
| 		blockID4 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID4 := blockID1 | ||||
| 		boardID4 := blockID1 | ||||
| 		parentID4 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block4 := Block{ID: blockID4, RootID: rootID4, ParentID: parentID4} | ||||
| 		block4 := Block{ID: blockID4, BoardID: boardID4, ParentID: parentID4} | ||||
|  | ||||
| 		// blocks are shuffled | ||||
| 		blocks := []Block{block4, block2, block1, block3} | ||||
| @@ -121,44 +121,44 @@ func TestGenerateBlockIDs(t *testing.T) { | ||||
|  | ||||
| 		// block 1 | ||||
| 		require.NotEqual(t, blockID1, blocks[2].ID) | ||||
| 		require.Equal(t, rootID1, blocks[2].RootID) | ||||
| 		require.Equal(t, boardID1, blocks[2].BoardID) | ||||
| 		require.Equal(t, parentID1, blocks[2].ParentID) | ||||
|  | ||||
| 		// block 2 | ||||
| 		require.NotEqual(t, blockID2, blocks[1].ID) | ||||
| 		require.NotEqual(t, rootID2, blocks[1].RootID) | ||||
| 		require.Equal(t, blocks[2].ID, blocks[1].RootID) // link to 1 | ||||
| 		require.NotEqual(t, boardID2, blocks[1].BoardID) | ||||
| 		require.Equal(t, blocks[2].ID, blocks[1].BoardID) // link to 1 | ||||
| 		require.Equal(t, parentID2, blocks[1].ParentID) | ||||
|  | ||||
| 		// block 3 | ||||
| 		require.NotEqual(t, blockID3, blocks[3].ID) | ||||
| 		require.NotEqual(t, rootID3, blocks[3].RootID) | ||||
| 		require.Equal(t, blocks[1].ID, blocks[3].RootID) // link to 2 | ||||
| 		require.NotEqual(t, boardID3, blocks[3].BoardID) | ||||
| 		require.Equal(t, blocks[1].ID, blocks[3].BoardID) // link to 2 | ||||
| 		require.Equal(t, parentID3, blocks[3].ParentID) | ||||
|  | ||||
| 		// block 4 | ||||
| 		require.NotEqual(t, blockID4, blocks[0].ID) | ||||
| 		require.NotEqual(t, rootID4, blocks[0].RootID) | ||||
| 		require.Equal(t, blocks[2].ID, blocks[0].RootID) // link to 1 | ||||
| 		require.NotEqual(t, boardID4, blocks[0].BoardID) | ||||
| 		require.Equal(t, blocks[2].ID, blocks[0].BoardID) // link to 1 | ||||
| 		require.Equal(t, parentID4, blocks[0].ParentID) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("Should update content order", func(t *testing.T) { | ||||
| 		blockID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block1 := Block{ | ||||
| 			ID:       blockID1, | ||||
| 			RootID:   rootID1, | ||||
| 			BoardID:  boardID1, | ||||
| 			ParentID: parentID1, | ||||
| 		} | ||||
|  | ||||
| 		blockID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block2 := Block{ | ||||
| 			ID:       blockID2, | ||||
| 			RootID:   rootID2, | ||||
| 			BoardID:  boardID2, | ||||
| 			ParentID: parentID2, | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"contentOrder": []interface{}{ | ||||
| @@ -172,11 +172,11 @@ func TestGenerateBlockIDs(t *testing.T) { | ||||
| 		blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) | ||||
|  | ||||
| 		require.NotEqual(t, blockID1, blocks[0].ID) | ||||
| 		require.Equal(t, rootID1, blocks[0].RootID) | ||||
| 		require.Equal(t, boardID1, blocks[0].BoardID) | ||||
| 		require.Equal(t, parentID1, blocks[0].ParentID) | ||||
|  | ||||
| 		require.NotEqual(t, blockID2, blocks[1].ID) | ||||
| 		require.Equal(t, rootID2, blocks[1].RootID) | ||||
| 		require.Equal(t, boardID2, blocks[1].BoardID) | ||||
| 		require.Equal(t, parentID2, blocks[1].ParentID) | ||||
|  | ||||
| 		// since block 1 was referenced in block 2, | ||||
| @@ -189,35 +189,35 @@ func TestGenerateBlockIDs(t *testing.T) { | ||||
|  | ||||
| 	t.Run("Should update content order when it contain slices", func(t *testing.T) { | ||||
| 		blockID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID1 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block1 := Block{ | ||||
| 			ID:       blockID1, | ||||
| 			RootID:   rootID1, | ||||
| 			BoardID:  boardID1, | ||||
| 			ParentID: parentID1, | ||||
| 		} | ||||
|  | ||||
| 		blockID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block2 := Block{ | ||||
| 			ID:       blockID2, | ||||
| 			RootID:   rootID1, | ||||
| 			BoardID:  boardID1, | ||||
| 			ParentID: parentID1, | ||||
| 		} | ||||
|  | ||||
| 		blockID3 := utils.NewID(utils.IDTypeBlock) | ||||
| 		block3 := Block{ | ||||
| 			ID:       blockID3, | ||||
| 			RootID:   rootID1, | ||||
| 			BoardID:  boardID1, | ||||
| 			ParentID: parentID1, | ||||
| 		} | ||||
|  | ||||
| 		blockID4 := utils.NewID(utils.IDTypeBlock) | ||||
| 		rootID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		boardID2 := utils.NewID(utils.IDTypeBlock) | ||||
| 		parentID2 := utils.NewID(utils.IDTypeBlock) | ||||
|  | ||||
| 		block4 := Block{ | ||||
| 			ID:       blockID4, | ||||
| 			RootID:   rootID2, | ||||
| 			BoardID:  boardID2, | ||||
| 			ParentID: parentID2, | ||||
| 			Fields: map[string]interface{}{ | ||||
| 				"contentOrder": []interface{}{ | ||||
| @@ -235,11 +235,11 @@ func TestGenerateBlockIDs(t *testing.T) { | ||||
| 		blocks = GenerateBlockIDs(blocks, &mlog.Logger{}) | ||||
|  | ||||
| 		require.NotEqual(t, blockID1, blocks[0].ID) | ||||
| 		require.Equal(t, rootID1, blocks[0].RootID) | ||||
| 		require.Equal(t, boardID1, blocks[0].BoardID) | ||||
| 		require.Equal(t, parentID1, blocks[0].ParentID) | ||||
|  | ||||
| 		require.NotEqual(t, blockID4, blocks[3].ID) | ||||
| 		require.Equal(t, rootID2, blocks[3].RootID) | ||||
| 		require.Equal(t, boardID2, blocks[3].BoardID) | ||||
| 		require.Equal(t, parentID2, blocks[3].ParentID) | ||||
|  | ||||
| 		// since block 1 was referenced in block 2, | ||||
|   | ||||
| @@ -20,8 +20,8 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block { | ||||
| 			blockIDs[block.ID] = block.Type | ||||
| 		} | ||||
|  | ||||
| 		if _, ok := referenceIDs[block.RootID]; !ok { | ||||
| 			referenceIDs[block.RootID] = true | ||||
| 		if _, ok := referenceIDs[block.BoardID]; !ok { | ||||
| 			referenceIDs[block.BoardID] = true | ||||
| 		} | ||||
| 		if _, ok := referenceIDs[block.ParentID]; !ok { | ||||
| 			referenceIDs[block.ParentID] = true | ||||
| @@ -81,7 +81,7 @@ func GenerateBlockIDs(blocks []Block, logger *mlog.Logger) []Block { | ||||
| 	newBlocks := make([]Block, len(blocks)) | ||||
| 	for i, block := range blocks { | ||||
| 		block.ID = getExistingOrNewID(block.ID) | ||||
| 		block.RootID = getExistingOrOldID(block.RootID) | ||||
| 		block.BoardID = getExistingOrOldID(block.BoardID) | ||||
| 		block.ParentID = getExistingOrOldID(block.ParentID) | ||||
|  | ||||
| 		blockMod := block | ||||
|   | ||||
							
								
								
									
										304
									
								
								server/model/board.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								server/model/board.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,304 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| type BoardType string | ||||
|  | ||||
| const ( | ||||
| 	BoardTypeOpen    BoardType = "O" | ||||
| 	BoardTypePrivate BoardType = "P" | ||||
| ) | ||||
|  | ||||
| // Board groups a set of blocks and its layout | ||||
| // swagger:model | ||||
| type Board struct { | ||||
| 	// The ID for the board | ||||
| 	// required: true | ||||
| 	ID string `json:"id"` | ||||
|  | ||||
| 	// The ID of the team that the board belongs to | ||||
| 	// required: true | ||||
| 	TeamID string `json:"teamId"` | ||||
|  | ||||
| 	// The ID of the channel that the board was created from | ||||
| 	// required: false | ||||
| 	ChannelID string `json:"channelId"` | ||||
|  | ||||
| 	// The ID of the user that created the board | ||||
| 	// required: true | ||||
| 	CreatedBy string `json:"createdBy"` | ||||
|  | ||||
| 	// The ID of the last user that updated the board | ||||
| 	// required: true | ||||
| 	ModifiedBy string `json:"modifiedBy"` | ||||
|  | ||||
| 	// The type of the board | ||||
| 	// required: true | ||||
| 	Type BoardType `json:"type"` | ||||
|  | ||||
| 	// The title of the board | ||||
| 	// required: false | ||||
| 	Title string `json:"title"` | ||||
|  | ||||
| 	// The description of the board | ||||
| 	// required: false | ||||
| 	Description string `json:"description"` | ||||
|  | ||||
| 	// The icon of the board | ||||
| 	// required: false | ||||
| 	Icon string `json:"icon"` | ||||
|  | ||||
| 	// Indicates if the board shows the description on the interface | ||||
| 	// required: false | ||||
| 	ShowDescription bool `json:"showDescription"` | ||||
|  | ||||
| 	// Marks the template boards | ||||
| 	// required: false | ||||
| 	IsTemplate bool `json:"isTemplate"` | ||||
|  | ||||
| 	// Marks the template boards | ||||
| 	// required: false | ||||
| 	TemplateVersion int `json:"templateVersion"` | ||||
|  | ||||
| 	// The properties of the board | ||||
| 	// required: false | ||||
| 	Properties map[string]interface{} `json:"properties"` | ||||
|  | ||||
| 	// The properties of the board cards | ||||
| 	// required: false | ||||
| 	CardProperties []map[string]interface{} `json:"cardProperties"` | ||||
|  | ||||
| 	// The calculations on the board's cards | ||||
| 	// required: false | ||||
| 	ColumnCalculations map[string]interface{} `json:"columnCalculations"` | ||||
|  | ||||
| 	// The creation time | ||||
| 	// required: true | ||||
| 	CreateAt int64 `json:"createAt"` | ||||
|  | ||||
| 	// The last modified time | ||||
| 	// required: true | ||||
| 	UpdateAt int64 `json:"updateAt"` | ||||
|  | ||||
| 	// The deleted time. Set to indicate this block is deleted | ||||
| 	// required: false | ||||
| 	DeleteAt int64 `json:"deleteAt"` | ||||
| } | ||||
|  | ||||
| // BoardPatch is a patch for modify boards | ||||
| // swagger:model | ||||
| type BoardPatch struct { | ||||
| 	// The type of the board | ||||
| 	// required: false | ||||
| 	Type *BoardType `json:"type"` | ||||
|  | ||||
| 	// The title of the board | ||||
| 	// required: false | ||||
| 	Title *string `json:"title"` | ||||
|  | ||||
| 	// The description of the board | ||||
| 	// required: false | ||||
| 	Description *string `json:"description"` | ||||
|  | ||||
| 	// The icon of the board | ||||
| 	// required: false | ||||
| 	Icon *string `json:"icon"` | ||||
|  | ||||
| 	// Indicates if the board shows the description on the interface | ||||
| 	// required: false | ||||
| 	ShowDescription *bool `json:"showDescription"` | ||||
|  | ||||
| 	// The board updated properties | ||||
| 	// required: false | ||||
| 	UpdatedProperties map[string]interface{} `json:"updatedProperties"` | ||||
|  | ||||
| 	// The board removed properties | ||||
| 	// required: false | ||||
| 	DeletedProperties []string `json:"deletedProperties"` | ||||
|  | ||||
| 	// The board updated card properties | ||||
| 	// required: false | ||||
| 	UpdatedCardProperties []map[string]interface{} `json:"updatedCardProperties"` | ||||
|  | ||||
| 	// The board removed card properties | ||||
| 	// required: false | ||||
| 	DeletedCardProperties []string `json:"deletedCardProperties"` | ||||
|  | ||||
| 	// The board updated column calculations | ||||
| 	// required: false | ||||
| 	UpdatedColumnCalculations map[string]interface{} `json:"updatedColumnCalculations"` | ||||
|  | ||||
| 	// The board deleted column calculations | ||||
| 	// required: false | ||||
| 	DeletedColumnCalculations []string `json:"deletedColumnCalculations"` | ||||
| } | ||||
|  | ||||
| // BoardMember stores the information of the membership of a user on a board | ||||
| // swagger:model | ||||
| type BoardMember struct { | ||||
| 	// The ID of the board | ||||
| 	// required: true | ||||
| 	BoardID string `json:"boardId"` | ||||
|  | ||||
| 	// The ID of the user | ||||
| 	// required: true | ||||
| 	UserID string `json:"userId"` | ||||
|  | ||||
| 	// The independent roles of the user on the board | ||||
| 	// required: false | ||||
| 	Roles string `json:"roles"` | ||||
|  | ||||
| 	// Marks the user as an admin of the board | ||||
| 	// required: true | ||||
| 	SchemeAdmin bool `json:"schemeAdmin"` | ||||
|  | ||||
| 	// Marks the user as an editor of the board | ||||
| 	// required: true | ||||
| 	SchemeEditor bool `json:"schemeEditor"` | ||||
|  | ||||
| 	// Marks the user as an commenter of the board | ||||
| 	// required: true | ||||
| 	SchemeCommenter bool `json:"schemeCommenter"` | ||||
|  | ||||
| 	// Marks the user as an viewer of the board | ||||
| 	// required: true | ||||
| 	SchemeViewer bool `json:"schemeViewer"` | ||||
| } | ||||
|  | ||||
| func BoardFromJSON(data io.Reader) *Board { | ||||
| 	var board *Board | ||||
| 	_ = json.NewDecoder(data).Decode(&board) | ||||
| 	return board | ||||
| } | ||||
|  | ||||
| func BoardsFromJSON(data io.Reader) []*Board { | ||||
| 	var boards []*Board | ||||
| 	_ = json.NewDecoder(data).Decode(&boards) | ||||
| 	return boards | ||||
| } | ||||
|  | ||||
| func BoardMemberFromJSON(data io.Reader) *BoardMember { | ||||
| 	var boardMember *BoardMember | ||||
| 	_ = json.NewDecoder(data).Decode(&boardMember) | ||||
| 	return boardMember | ||||
| } | ||||
|  | ||||
| func BoardMembersFromJSON(data io.Reader) []*BoardMember { | ||||
| 	var boardMembers []*BoardMember | ||||
| 	_ = json.NewDecoder(data).Decode(&boardMembers) | ||||
| 	return boardMembers | ||||
| } | ||||
|  | ||||
| // Patch returns an updated version of the board. | ||||
| func (p *BoardPatch) Patch(board *Board) *Board { | ||||
| 	if p.Type != nil { | ||||
| 		board.Type = *p.Type | ||||
| 	} | ||||
|  | ||||
| 	if p.Title != nil { | ||||
| 		board.Title = *p.Title | ||||
| 	} | ||||
|  | ||||
| 	if p.Description != nil { | ||||
| 		board.Description = *p.Description | ||||
| 	} | ||||
|  | ||||
| 	if p.Icon != nil { | ||||
| 		board.Icon = *p.Icon | ||||
| 	} | ||||
|  | ||||
| 	if p.ShowDescription != nil { | ||||
| 		board.ShowDescription = *p.ShowDescription | ||||
| 	} | ||||
|  | ||||
| 	for key, property := range p.UpdatedProperties { | ||||
| 		board.Properties[key] = property | ||||
| 	} | ||||
|  | ||||
| 	for _, key := range p.DeletedProperties { | ||||
| 		delete(board.Properties, key) | ||||
| 	} | ||||
|  | ||||
| 	if len(p.UpdatedCardProperties) != 0 || len(p.DeletedCardProperties) != 0 { | ||||
| 		// first we accumulate all properties indexed by ID | ||||
| 		cardPropertyMap := map[string]map[string]interface{}{} | ||||
| 		for _, prop := range board.CardProperties { | ||||
| 			id, ok := prop["id"].(string) | ||||
| 			if !ok { | ||||
| 				// bad property, skipping | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			cardPropertyMap[id] = prop | ||||
| 		} | ||||
|  | ||||
| 		// if there are properties marked for removal, we delete them | ||||
| 		for _, propertyID := range p.DeletedCardProperties { | ||||
| 			delete(cardPropertyMap, propertyID) | ||||
| 		} | ||||
|  | ||||
| 		// if there are properties marked for update, we replace the | ||||
| 		// existing ones or add them | ||||
| 		for _, newprop := range p.UpdatedCardProperties { | ||||
| 			id, ok := newprop["id"].(string) | ||||
| 			if !ok { | ||||
| 				// bad new property, skipping | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			cardPropertyMap[id] = newprop | ||||
| 		} | ||||
|  | ||||
| 		// and finally we flatten and save the updated properties | ||||
| 		newCardProperties := []map[string]interface{}{} | ||||
| 		for _, p := range cardPropertyMap { | ||||
| 			newCardProperties = append(newCardProperties, p) | ||||
| 		} | ||||
|  | ||||
| 		board.CardProperties = newCardProperties | ||||
| 	} | ||||
|  | ||||
| 	for key, columnCalculation := range p.UpdatedColumnCalculations { | ||||
| 		board.ColumnCalculations[key] = columnCalculation | ||||
| 	} | ||||
|  | ||||
| 	for _, key := range p.DeletedColumnCalculations { | ||||
| 		delete(board.ColumnCalculations, key) | ||||
| 	} | ||||
|  | ||||
| 	return board | ||||
| } | ||||
|  | ||||
| func IsBoardTypeValid(t BoardType) bool { | ||||
| 	return t == BoardTypeOpen || t == BoardTypePrivate | ||||
| } | ||||
|  | ||||
| func (p *BoardPatch) IsValid() error { | ||||
| 	if p.Type != nil && !IsBoardTypeValid(*p.Type) { | ||||
| 		return InvalidBoardErr{"invalid-board-type"} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type InvalidBoardErr struct { | ||||
| 	msg string | ||||
| } | ||||
|  | ||||
| func (ibe InvalidBoardErr) Error() string { | ||||
| 	return ibe.msg | ||||
| } | ||||
|  | ||||
| func (b *Board) IsValid() error { | ||||
| 	if b.TeamID == "" { | ||||
| 		return InvalidBoardErr{"empty-team-id"} | ||||
| 	} | ||||
|  | ||||
| 	if !IsBoardTypeValid(b.Type) { | ||||
| 		return InvalidBoardErr{"invalid-board-type"} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										164
									
								
								server/model/boards_and_blocks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								server/model/boards_and_blocks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| var ErrNoBoardsInBoardsAndBlocks = errors.New("at least one board is required") | ||||
| var ErrNoBlocksInBoardsAndBlocks = errors.New("at least one block is required") | ||||
| var ErrNoTeamInBoardsAndBlocks = errors.New("team ID cannot be empty") | ||||
| var ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("board ids and patches need to match") | ||||
| var ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks = errors.New("block ids and patches need to match") | ||||
|  | ||||
| type BlockDoesntBelongToAnyBoardErr struct { | ||||
| 	blockID string | ||||
| } | ||||
|  | ||||
| func (e BlockDoesntBelongToAnyBoardErr) Error() string { | ||||
| 	return fmt.Sprintf("block %s doesn't belong to any board", e.blockID) | ||||
| } | ||||
|  | ||||
| // BoardsAndBlocks is used to operate over boards and blocks at the | ||||
| // same time | ||||
| // swagger:model | ||||
| type BoardsAndBlocks struct { | ||||
| 	// The boards | ||||
| 	// required: false | ||||
| 	Boards []*Board `json:"boards"` | ||||
|  | ||||
| 	// The blocks | ||||
| 	// required: false | ||||
| 	Blocks []Block `json:"blocks"` | ||||
| } | ||||
|  | ||||
| func (bab *BoardsAndBlocks) IsValid() error { | ||||
| 	if len(bab.Boards) == 0 { | ||||
| 		return ErrNoBoardsInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	if len(bab.Blocks) == 0 { | ||||
| 		return ErrNoBlocksInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	boardsMap := map[string]bool{} | ||||
| 	for _, board := range bab.Boards { | ||||
| 		boardsMap[board.ID] = true | ||||
| 	} | ||||
|  | ||||
| 	for _, block := range bab.Blocks { | ||||
| 		if _, ok := boardsMap[block.BoardID]; !ok { | ||||
| 			return BlockDoesntBelongToAnyBoardErr{block.ID} | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // DeleteBoardsAndBlocks is used to list the boards and blocks to | ||||
| // delete on a request | ||||
| // swagger:model | ||||
| type DeleteBoardsAndBlocks struct { | ||||
| 	// The boards | ||||
| 	// required: true | ||||
| 	Boards []string `json:"boards"` | ||||
|  | ||||
| 	// The blocks | ||||
| 	// required: true | ||||
| 	Blocks []string `json:"blocks"` | ||||
| } | ||||
|  | ||||
| func (dbab *DeleteBoardsAndBlocks) IsValid() error { | ||||
| 	if len(dbab.Boards) == 0 { | ||||
| 		return ErrNoBoardsInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	if len(dbab.Blocks) == 0 { | ||||
| 		return ErrNoBlocksInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // PatchBoardsAndBlocks is used to patch multiple boards and blocks on | ||||
| // a single request | ||||
| // swagger:model | ||||
| type PatchBoardsAndBlocks struct { | ||||
| 	// The board IDs to patch | ||||
| 	// required: true | ||||
| 	BoardIDs []string `json:"boardIDs"` | ||||
|  | ||||
| 	// The board patches | ||||
| 	// required: true | ||||
| 	BoardPatches []*BoardPatch `json:"boardPatches"` | ||||
|  | ||||
| 	// The block IDs to patch | ||||
| 	// required: true | ||||
| 	BlockIDs []string `json:"blockIDs"` | ||||
|  | ||||
| 	// The block patches | ||||
| 	// required: true | ||||
| 	BlockPatches []*BlockPatch `json:"blockPatches"` | ||||
| } | ||||
|  | ||||
| func (dbab *PatchBoardsAndBlocks) IsValid() error { | ||||
| 	if len(dbab.BoardIDs) == 0 { | ||||
| 		return ErrNoBoardsInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	if len(dbab.BoardIDs) != len(dbab.BoardPatches) { | ||||
| 		return ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	if len(dbab.BlockIDs) == 0 { | ||||
| 		return ErrNoBlocksInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	if len(dbab.BlockIDs) != len(dbab.BlockPatches) { | ||||
| 		return ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func GenerateBoardsAndBlocksIDs(bab *BoardsAndBlocks, logger *mlog.Logger) (*BoardsAndBlocks, error) { | ||||
| 	if err := bab.IsValid(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	blocksByBoard := map[string][]Block{} | ||||
| 	for _, block := range bab.Blocks { | ||||
| 		blocksByBoard[block.BoardID] = append(blocksByBoard[block.BoardID], block) | ||||
| 	} | ||||
|  | ||||
| 	boards := []*Board{} | ||||
| 	blocks := []Block{} | ||||
| 	for _, board := range bab.Boards { | ||||
| 		newID := utils.NewID(utils.IDTypeBoard) | ||||
| 		for _, block := range blocksByBoard[board.ID] { | ||||
| 			block.BoardID = newID | ||||
| 			blocks = append(blocks, block) | ||||
| 		} | ||||
|  | ||||
| 		board.ID = newID | ||||
| 		boards = append(boards, board) | ||||
| 	} | ||||
|  | ||||
| 	newBab := &BoardsAndBlocks{ | ||||
| 		Boards: boards, | ||||
| 		Blocks: GenerateBlockIDs(blocks, logger), | ||||
| 	} | ||||
|  | ||||
| 	return newBab, nil | ||||
| } | ||||
|  | ||||
| func BoardsAndBlocksFromJSON(data io.Reader) *BoardsAndBlocks { | ||||
| 	var bab *BoardsAndBlocks | ||||
| 	_ = json.NewDecoder(data).Decode(&bab) | ||||
| 	return bab | ||||
| } | ||||
							
								
								
									
										264
									
								
								server/model/boards_and_blocks_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								server/model/boards_and_blocks_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/require" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| func TestIsValidBoardsAndBlocks(t *testing.T) { | ||||
| 	t.Run("no boards", func(t *testing.T) { | ||||
| 		bab := &BoardsAndBlocks{ | ||||
| 			Blocks: []Block{ | ||||
| 				{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, | ||||
| 				{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, bab.IsValid(), ErrNoBoardsInBoardsAndBlocks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no blocks", func(t *testing.T) { | ||||
| 		bab := &BoardsAndBlocks{ | ||||
| 			Boards: []*Board{ | ||||
| 				{ID: "board-id-1", Type: BoardTypeOpen}, | ||||
| 				{ID: "board-id-2", Type: BoardTypePrivate}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, bab.IsValid(), ErrNoBlocksInBoardsAndBlocks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("block that doesn't belong to the boards", func(t *testing.T) { | ||||
| 		bab := &BoardsAndBlocks{ | ||||
| 			Boards: []*Board{ | ||||
| 				{ID: "board-id-1", Type: BoardTypeOpen}, | ||||
| 				{ID: "board-id-2", Type: BoardTypePrivate}, | ||||
| 			}, | ||||
| 			Blocks: []Block{ | ||||
| 				{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, | ||||
| 				{ID: "block-id-3", BoardID: "board-id-3", Type: TypeCard}, | ||||
| 				{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, bab.IsValid(), BlockDoesntBelongToAnyBoardErr{"block-id-3"}) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("valid boards and blocks", func(t *testing.T) { | ||||
| 		bab := &BoardsAndBlocks{ | ||||
| 			Boards: []*Board{ | ||||
| 				{ID: "board-id-1", Type: BoardTypeOpen}, | ||||
| 				{ID: "board-id-2", Type: BoardTypePrivate}, | ||||
| 			}, | ||||
| 			Blocks: []Block{ | ||||
| 				{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, | ||||
| 				{ID: "block-id-3", BoardID: "board-id-2", Type: TypeCard}, | ||||
| 				{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.NoError(t, bab.IsValid()) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestGenerateBoardsAndBlocksIDs(t *testing.T) { | ||||
| 	logger, err := mlog.NewLogger() | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	getBlockByType := func(blocks []Block, blockType BlockType) Block { | ||||
| 		for _, b := range blocks { | ||||
| 			if b.Type == blockType { | ||||
| 				return b | ||||
| 			} | ||||
| 		} | ||||
| 		return Block{} | ||||
| 	} | ||||
|  | ||||
| 	getBoardByTitle := func(boards []*Board, title string) *Board { | ||||
| 		for _, b := range boards { | ||||
| 			if b.Title == title { | ||||
| 				return b | ||||
| 			} | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	t.Run("invalid boards and blocks", func(t *testing.T) { | ||||
| 		bab := &BoardsAndBlocks{ | ||||
| 			Blocks: []Block{ | ||||
| 				{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, | ||||
| 				{ID: "block-id-2", BoardID: "board-id-2", Type: TypeCard}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		rBab, err := GenerateBoardsAndBlocksIDs(bab, logger) | ||||
| 		require.Error(t, err) | ||||
| 		require.Nil(t, rBab) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("correctly generates IDs for all the boards and links the blocks to them, with new IDs too", func(t *testing.T) { | ||||
| 		bab := &BoardsAndBlocks{ | ||||
| 			Boards: []*Board{ | ||||
| 				{ID: "board-id-1", Type: BoardTypeOpen, Title: "board1"}, | ||||
| 				{ID: "board-id-2", Type: BoardTypePrivate, Title: "board2"}, | ||||
| 				{ID: "board-id-3", Type: BoardTypeOpen, Title: "board3"}, | ||||
| 			}, | ||||
| 			Blocks: []Block{ | ||||
| 				{ID: "block-id-1", BoardID: "board-id-1", Type: TypeCard}, | ||||
| 				{ID: "block-id-2", BoardID: "board-id-2", Type: TypeView}, | ||||
| 				{ID: "block-id-3", BoardID: "board-id-2", Type: TypeText}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		rBab, err := GenerateBoardsAndBlocksIDs(bab, logger) | ||||
| 		require.NoError(t, err) | ||||
| 		require.NotNil(t, rBab) | ||||
|  | ||||
| 		// all boards and blocks should have refreshed their IDs, and | ||||
| 		// blocks should be correctly linked to the new board IDs | ||||
| 		board1 := getBoardByTitle(rBab.Boards, "board1") | ||||
| 		require.NotNil(t, board1) | ||||
| 		require.NotEmpty(t, board1.ID) | ||||
| 		require.NotEqual(t, "board-id-1", board1.ID) | ||||
| 		board2 := getBoardByTitle(rBab.Boards, "board2") | ||||
| 		require.NotNil(t, board2) | ||||
| 		require.NotEmpty(t, board2.ID) | ||||
| 		require.NotEqual(t, "board-id-2", board2.ID) | ||||
| 		board3 := getBoardByTitle(rBab.Boards, "board3") | ||||
| 		require.NotNil(t, board3) | ||||
| 		require.NotEmpty(t, board3.ID) | ||||
| 		require.NotEqual(t, "board-id-3", board3.ID) | ||||
|  | ||||
| 		block1 := getBlockByType(rBab.Blocks, TypeCard) | ||||
| 		require.NotNil(t, block1) | ||||
| 		require.NotEmpty(t, block1.ID) | ||||
| 		require.NotEqual(t, "block-id-1", block1.ID) | ||||
| 		require.Equal(t, board1.ID, block1.BoardID) | ||||
| 		block2 := getBlockByType(rBab.Blocks, TypeView) | ||||
| 		require.NotNil(t, block2) | ||||
| 		require.NotEmpty(t, block2.ID) | ||||
| 		require.NotEqual(t, "block-id-2", block2.ID) | ||||
| 		require.Equal(t, board2.ID, block2.BoardID) | ||||
| 		block3 := getBlockByType(rBab.Blocks, TypeText) | ||||
| 		require.NotNil(t, block3) | ||||
| 		require.NotEmpty(t, block3.ID) | ||||
| 		require.NotEqual(t, "block-id-3", block3.ID) | ||||
| 		require.Equal(t, board2.ID, block3.BoardID) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestIsValidPatchBoardsAndBlocks(t *testing.T) { | ||||
| 	newTitle := "new title" | ||||
| 	newDescription := "new description" | ||||
| 	var schema int64 = 1 | ||||
|  | ||||
| 	t.Run("no board ids", func(t *testing.T) { | ||||
| 		pbab := &PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{}, | ||||
| 			BlockIDs: []string{"block-id-1"}, | ||||
| 			BlockPatches: []*BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Schema: &schema}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, pbab.IsValid(), ErrNoBoardsInBoardsAndBlocks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("missmatch board IDs and patches", func(t *testing.T) { | ||||
| 		pbab := &PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{"board-id-1", "board-id-2"}, | ||||
| 			BoardPatches: []*BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{"block-id-1"}, | ||||
| 			BlockPatches: []*BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, pbab.IsValid(), ErrBoardIDsAndPatchesMissmatchInBoardsAndBlocks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("no block ids", func(t *testing.T) { | ||||
| 		pbab := &PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{"board-id-1", "board-id-2"}, | ||||
| 			BoardPatches: []*BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Description: &newDescription}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, pbab.IsValid(), ErrNoBlocksInBoardsAndBlocks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("missmatch block IDs and patches", func(t *testing.T) { | ||||
| 		pbab := &PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{"board-id-1", "board-id-2"}, | ||||
| 			BoardPatches: []*BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Description: &newDescription}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{"block-id-1"}, | ||||
| 			BlockPatches: []*BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Schema: &schema}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.ErrorIs(t, pbab.IsValid(), ErrBlockIDsAndPatchesMissmatchInBoardsAndBlocks) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("valid", func(t *testing.T) { | ||||
| 		pbab := &PatchBoardsAndBlocks{ | ||||
| 			BoardIDs: []string{"board-id-1", "board-id-2"}, | ||||
| 			BoardPatches: []*BoardPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 				{Description: &newDescription}, | ||||
| 			}, | ||||
| 			BlockIDs: []string{"block-id-1"}, | ||||
| 			BlockPatches: []*BlockPatch{ | ||||
| 				{Title: &newTitle}, | ||||
| 			}, | ||||
| 		} | ||||
|  | ||||
| 		require.NoError(t, pbab.IsValid()) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestIsValidDeleteBoardsAndBlocks(t *testing.T) { | ||||
| 	/* | ||||
| 		TODO fix this | ||||
| 		t.Run("no board ids", func(t *testing.T) { | ||||
| 			dbab := &DeleteBoardsAndBlocks{ | ||||
| 				TeamID: "team-id", | ||||
| 				Blocks: []string{"block-id-1"}, | ||||
| 			} | ||||
|  | ||||
| 			require.ErrorIs(t, dbab.IsValid(), NoBoardsInBoardsAndBlocksErr) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("no block ids", func(t *testing.T) { | ||||
| 			dbab := &DeleteBoardsAndBlocks{ | ||||
| 				TeamID: "team-id", | ||||
| 				Boards: []string{"board-id-1", "board-id-2"}, | ||||
| 			} | ||||
|  | ||||
| 			require.ErrorIs(t, dbab.IsValid(), NoBlocksInBoardsAndBlocksErr) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("valid", func(t *testing.T) { | ||||
| 			dbab := &DeleteBoardsAndBlocks{ | ||||
| 				TeamID: "team-id", | ||||
| 				Boards: []string{"board-id-1", "board-id-2"}, | ||||
| 				Blocks: []string{"block-id-1"}, | ||||
| 			} | ||||
|  | ||||
| 			require.NoError(t, dbab.IsValid()) | ||||
| 		}) | ||||
| 	*/ | ||||
| } | ||||
							
								
								
									
										57
									
								
								server/model/category.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								server/model/category.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
| ) | ||||
|  | ||||
| type Category struct { | ||||
| 	ID       string `json:"id"` | ||||
| 	Name     string `json:"name"` | ||||
| 	UserID   string `json:"userID"` | ||||
| 	TeamID   string `json:"teamID"` | ||||
| 	CreateAt int64  `json:"createAt"` | ||||
| 	UpdateAt int64  `json:"updateAt"` | ||||
| 	DeleteAt int64  `json:"deleteAt"` | ||||
| } | ||||
|  | ||||
| func (c *Category) Hydrate() { | ||||
| 	c.ID = utils.NewID(utils.IDTypeNone) | ||||
| 	c.CreateAt = utils.GetMillis() | ||||
| 	c.UpdateAt = c.CreateAt | ||||
| } | ||||
|  | ||||
| func (c *Category) IsValid() error { | ||||
| 	if strings.TrimSpace(c.ID) == "" { | ||||
| 		return newErrInvalidCategory("category ID cannot be empty") | ||||
| 	} | ||||
|  | ||||
| 	if strings.TrimSpace(c.Name) == "" { | ||||
| 		return newErrInvalidCategory("category name cannot be empty") | ||||
| 	} | ||||
|  | ||||
| 	if strings.TrimSpace(c.UserID) == "" { | ||||
| 		return newErrInvalidCategory("category user ID cannot be empty") | ||||
| 	} | ||||
|  | ||||
| 	if strings.TrimSpace(c.TeamID) == "" { | ||||
| 		return newErrInvalidCategory("category team id ID cannot be empty") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| type ErrInvalidCategory struct { | ||||
| 	msg string | ||||
| } | ||||
|  | ||||
| func newErrInvalidCategory(msg string) *ErrInvalidCategory { | ||||
| 	return &ErrInvalidCategory{ | ||||
| 		msg: msg, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (e *ErrInvalidCategory) Error() string { | ||||
| 	return e.msg | ||||
| } | ||||
							
								
								
									
										11
									
								
								server/model/category_blocks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								server/model/category_blocks.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| package model | ||||
|  | ||||
| type CategoryBlocks struct { | ||||
| 	Category | ||||
| 	BlockIDs []string `json:"blockIDs"` | ||||
| } | ||||
|  | ||||
| type BlockCategoryWebsocketData struct { | ||||
| 	BlockID    string `json:"blockID"` | ||||
| 	CategoryID string `json:"categoryID"` | ||||
| } | ||||
							
								
								
									
										7
									
								
								server/model/database.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								server/model/database.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| package model | ||||
|  | ||||
| const ( | ||||
| 	SqliteDBType   = "sqlite3" | ||||
| 	PostgresDBType = "postgres" | ||||
| 	MysqlDBType    = "mysql" | ||||
| ) | ||||
| @@ -33,7 +33,7 @@ type ArchiveLine struct { | ||||
| // ExportArchiveOptions provides options when exporting one or more boards | ||||
| // to an archive. | ||||
| type ExportArchiveOptions struct { | ||||
| 	WorkspaceID string | ||||
| 	TeamID string | ||||
|  | ||||
| 	// BoardIDs is the list of boards to include in the archive. | ||||
| 	// Empty slice means export all boards from workspace/team. | ||||
| @@ -42,9 +42,9 @@ type ExportArchiveOptions struct { | ||||
|  | ||||
| // ImportArchiveOptions provides options when importing an archive. | ||||
| type ImportArchiveOptions struct { | ||||
| 	WorkspaceID   string | ||||
| 	TeamID        string | ||||
| 	ModifiedBy    string | ||||
| 	BlockModifier BlockModifier | ||||
| 	BoardModifier BoardModifier | ||||
| } | ||||
|  | ||||
| // ErrUnsupportedArchiveVersion is an error returned when trying to import an | ||||
|   | ||||
| @@ -18,10 +18,6 @@ type NotificationHint struct { | ||||
| 	// required: true | ||||
| 	BlockID string `json:"block_id"` | ||||
|  | ||||
| 	// WorkspaceID is id of workspace the block belongs to | ||||
| 	// required: true | ||||
| 	WorkspaceID string `json:"workspace_id"` | ||||
|  | ||||
| 	// ModifiedByID is the id of the user who made the block change | ||||
| 	ModifiedByID string `json:"modified_by_id"` | ||||
|  | ||||
| @@ -41,9 +37,6 @@ func (s *NotificationHint) IsValid() error { | ||||
| 	if s.BlockID == "" { | ||||
| 		return ErrInvalidNotificationHint{"missing block id"} | ||||
| 	} | ||||
| 	if s.WorkspaceID == "" { | ||||
| 		return ErrInvalidNotificationHint{"missing workspace id"} | ||||
| 	} | ||||
| 	if s.BlockType == "" { | ||||
| 		return ErrInvalidNotificationHint{"missing block type"} | ||||
| 	} | ||||
| @@ -57,7 +50,6 @@ func (s *NotificationHint) Copy() *NotificationHint { | ||||
| 	return &NotificationHint{ | ||||
| 		BlockType:    s.BlockType, | ||||
| 		BlockID:      s.BlockID, | ||||
| 		WorkspaceID:  s.WorkspaceID, | ||||
| 		ModifiedByID: s.ModifiedByID, | ||||
| 		CreateAt:     s.CreateAt, | ||||
| 		NotifyAt:     s.NotifyAt, | ||||
| @@ -68,14 +60,12 @@ func (s *NotificationHint) LogClone() interface{} { | ||||
| 	return struct { | ||||
| 		BlockType    BlockType `json:"block_type"` | ||||
| 		BlockID      string    `json:"block_id"` | ||||
| 		WorkspaceID  string    `json:"workspace_id"` | ||||
| 		ModifiedByID string    `json:"modified_by_id"` | ||||
| 		CreateAt     string    `json:"create_at"` | ||||
| 		NotifyAt     string    `json:"notify_at"` | ||||
| 	}{ | ||||
| 		BlockType:    s.BlockType, | ||||
| 		BlockID:      s.BlockID, | ||||
| 		WorkspaceID:  s.WorkspaceID, | ||||
| 		ModifiedByID: s.ModifiedByID, | ||||
| 		CreateAt:     utils.TimeFromMillis(s.CreateAt).Format(time.StampMilli), | ||||
| 		NotifyAt:     utils.TimeFromMillis(s.NotifyAt).Format(time.StampMilli), | ||||
|   | ||||
							
								
								
									
										19
									
								
								server/model/permission.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								server/model/permission.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	PermissionViewTeam              = mmModel.PermissionViewTeam | ||||
| 	PermissionViewMembers           = mmModel.PermissionViewMembers | ||||
| 	PermissionCreatePublicChannel   = mmModel.PermissionCreatePublicChannel | ||||
| 	PermissionCreatePrivateChannel  = mmModel.PermissionCreatePrivateChannel | ||||
| 	PermissionManageBoardType       = &mmModel.Permission{Id: "manage_board_type", Name: "", Description: "", Scope: ""} | ||||
| 	PermissionDeleteBoard           = &mmModel.Permission{Id: "delete_board", Name: "", Description: "", Scope: ""} | ||||
| 	PermissionViewBoard             = &mmModel.Permission{Id: "view_board", Name: "", Description: "", Scope: ""} | ||||
| 	PermissionManageBoardRoles      = &mmModel.Permission{Id: "manage_board_roles", Name: "", Description: "", Scope: ""} | ||||
| 	PermissionShareBoard            = &mmModel.Permission{Id: "share_board", Name: "", Description: "", Scope: ""} | ||||
| 	PermissionManageBoardCards      = &mmModel.Permission{Id: "manage_board_cards", Name: "", Description: "", Scope: ""} | ||||
| 	PermissionManageBoardProperties = &mmModel.Permission{Id: "manage_board_properties", Name: "", Description: "", Scope: ""} | ||||
| ) | ||||
| @@ -147,30 +147,10 @@ func (pd PropDef) ParseDate(s string) (string, error) { | ||||
| // schema for all cards within the board. | ||||
| // The result is provided as a map for quick lookup, and the original order is | ||||
| // preserved via the `Index` field. | ||||
| func ParsePropertySchema(board *Block) (PropSchema, error) { | ||||
| 	if board == nil || board.Type != TypeBoard { | ||||
| 		return nil, ErrInvalidBoardBlock | ||||
| 	} | ||||
|  | ||||
| func ParsePropertySchema(board *Board) (PropSchema, error) { | ||||
| 	schema := make(map[string]PropDef) | ||||
|  | ||||
| 	// cardProperties contains a slice of maps (untyped at this point). | ||||
| 	cardPropsIface, ok := board.Fields["cardProperties"] | ||||
| 	if !ok { | ||||
| 		return schema, nil | ||||
| 	} | ||||
|  | ||||
| 	cardProps, ok := cardPropsIface.([]interface{}) | ||||
| 	if !ok || len(cardProps) == 0 { | ||||
| 		return schema, nil | ||||
| 	} | ||||
|  | ||||
| 	for i, cp := range cardProps { | ||||
| 		prop, ok := cp.(map[string]interface{}) | ||||
| 		if !ok { | ||||
| 			return nil, ErrInvalidPropSchema | ||||
| 		} | ||||
|  | ||||
| 	for i, prop := range board.CardProperties { | ||||
| 		pd := PropDef{ | ||||
| 			ID:      getMapString("id", prop), | ||||
| 			Index:   i, | ||||
|   | ||||
| @@ -13,14 +13,13 @@ import ( | ||||
| ) | ||||
|  | ||||
| func Test_parsePropertySchema(t *testing.T) { | ||||
| 	board := &Block{ | ||||
| 		ID:          utils.NewID(utils.IDTypeBoard), | ||||
| 		Type:        TypeBoard, | ||||
| 		Title:       "Test Board", | ||||
| 		WorkspaceID: utils.NewID(utils.IDTypeWorkspace), | ||||
| 	board := &Board{ | ||||
| 		ID:     utils.NewID(utils.IDTypeBoard), | ||||
| 		Title:  "Test Board", | ||||
| 		TeamID: utils.NewID(utils.IDTypeTeam), | ||||
| 	} | ||||
|  | ||||
| 	err := json.Unmarshal([]byte(fieldsExample), &board.Fields) | ||||
| 	err := json.Unmarshal([]byte(cardPropertiesExample), &board.CardProperties) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	t.Run("parse schema", func(t *testing.T) { | ||||
| @@ -46,93 +45,74 @@ func Test_parsePropertySchema(t *testing.T) { | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	fieldsExample = ` | ||||
| 	{ | ||||
| 		"cardProperties":[ | ||||
| 		   { | ||||
| 			  "id":"7c212e78-9345-4c60-81b5-0b0e37ce463f", | ||||
| 			  "name":"Type", | ||||
| 			  "options":[ | ||||
| 				 { | ||||
| 					"color":"propColorYellow", | ||||
| 					"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23", | ||||
| 					"value":"Ad Hoc" | ||||
| 				 }, | ||||
| 				 { | ||||
| 					"color":"propColorBlue", | ||||
| 					"id":"def6317c-ec11-410d-8a6b-ea461320f392", | ||||
| 					"value":"Standup" | ||||
| 				 }, | ||||
| 				 { | ||||
| 					"color":"propColorPurple", | ||||
| 					"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7", | ||||
| 					"value":"Weekly Sync" | ||||
| 				 } | ||||
| 			  ], | ||||
| 			  "type":"select" | ||||
| 		   }, | ||||
| 		   { | ||||
| 			  "id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4", | ||||
| 			  "name":"Summary", | ||||
| 			  "options":[ | ||||
| 				  | ||||
| 			  ], | ||||
| 			  "type":"text" | ||||
| 		   }, | ||||
| 		   { | ||||
| 			  "id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c", | ||||
| 			  "name":"Color", | ||||
| 			  "options":[ | ||||
| 				 { | ||||
| 					"color":"propColorDefault", | ||||
| 					"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1", | ||||
| 					"value":"RED" | ||||
| 				 }, | ||||
| 				 { | ||||
| 					"color":"propColorDefault", | ||||
| 					"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311", | ||||
| 					"value":"GREEN" | ||||
| 				 }, | ||||
| 				 { | ||||
| 					"color":"propColorDefault", | ||||
| 					"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be", | ||||
| 					"value":"BLUE" | ||||
| 				 } | ||||
| 			  ], | ||||
| 			  "type":"select" | ||||
| 		   }, | ||||
| 		   { | ||||
| 			  "id":"aawg1s8rxq8o1bbksxmsmpsdd3r", | ||||
| 			  "name":"MyTextProp", | ||||
| 			  "options":[ | ||||
| 				  | ||||
| 			  ], | ||||
| 			  "type":"text" | ||||
| 		   }, | ||||
| 		   { | ||||
| 			  "id":"awdwfigo4kse63bdfp56mzhip6w", | ||||
| 			  "name":"MyCheckBox", | ||||
| 			  "options":[ | ||||
| 				  | ||||
| 			  ], | ||||
| 			  "type":"checkbox" | ||||
| 		   }, | ||||
| 		   { | ||||
| 			  "id":"a8spou7if43eo1rqzb9qeq488so", | ||||
| 			  "name":"MyDate", | ||||
| 			  "options":[ | ||||
| 				  | ||||
| 			  ], | ||||
| 			  "type":"date" | ||||
| 		   } | ||||
| 		], | ||||
| 		"columnCalculations":[ | ||||
| 		    | ||||
| 		], | ||||
| 		"description":"", | ||||
| 		"icon":"🗒️", | ||||
| 		"isTemplate":false, | ||||
| 		"showDescription":false | ||||
| 	 }	 | ||||
| 	` | ||||
| 	cardPropertiesExample = `[ | ||||
| 	   { | ||||
| 		  "id":"7c212e78-9345-4c60-81b5-0b0e37ce463f", | ||||
| 		  "name":"Type", | ||||
| 		  "options":[ | ||||
| 			 { | ||||
| 				"color":"propColorYellow", | ||||
| 				"id":"31da50ca-f1a9-4d21-8636-17dc387c1a23", | ||||
| 				"value":"Ad Hoc" | ||||
| 			 }, | ||||
| 			 { | ||||
| 				"color":"propColorBlue", | ||||
| 				"id":"def6317c-ec11-410d-8a6b-ea461320f392", | ||||
| 				"value":"Standup" | ||||
| 			 }, | ||||
| 			 { | ||||
| 				"color":"propColorPurple", | ||||
| 				"id":"700f83f8-6a41-46cd-87e2-53e0d0b12cc7", | ||||
| 				"value":"Weekly Sync" | ||||
| 			 } | ||||
| 		  ], | ||||
| 		  "type":"select" | ||||
| 	   }, | ||||
| 	   { | ||||
| 		  "id":"13d2394a-eb5e-4f22-8c22-6515ec41c4a4", | ||||
| 		  "name":"Summary", | ||||
| 		  "options":[], | ||||
| 		  "type":"text" | ||||
| 	   }, | ||||
| 	   { | ||||
| 		  "id":"566cd860-bbae-4bcd-86a8-7df4db2ba15c", | ||||
| 		  "name":"Color", | ||||
| 		  "options":[ | ||||
| 			 { | ||||
| 				"color":"propColorDefault", | ||||
| 				"id":"efb0c783-f9ea-4938-8b86-9cf425296cd1", | ||||
| 				"value":"RED" | ||||
| 			 }, | ||||
| 			 { | ||||
| 				"color":"propColorDefault", | ||||
| 				"id":"2f100e13-e7c4-4ab6-81c9-a17baf98b311", | ||||
| 				"value":"GREEN" | ||||
| 			 }, | ||||
| 			 { | ||||
| 				"color":"propColorDefault", | ||||
| 				"id":"a05bdc80-bd90-45b0-8805-a7e77a4884be", | ||||
| 				"value":"BLUE" | ||||
| 			 } | ||||
| 		  ], | ||||
| 		  "type":"select" | ||||
| 	   }, | ||||
| 	   { | ||||
| 		  "id":"aawg1s8rxq8o1bbksxmsmpsdd3r", | ||||
| 		  "name":"MyTextProp", | ||||
| 		  "options":[], | ||||
| 		  "type":"text" | ||||
| 	   }, | ||||
| 	   { | ||||
| 		  "id":"awdwfigo4kse63bdfp56mzhip6w", | ||||
| 		  "name":"MyCheckBox", | ||||
| 		  "options":[], | ||||
| 		  "type":"checkbox" | ||||
| 	   }, | ||||
| 	   { | ||||
| 		  "id":"a8spou7if43eo1rqzb9qeq488so", | ||||
| 		  "name":"MyDate", | ||||
| 		  "options":[], | ||||
| 		  "type":"date" | ||||
| 	   } | ||||
| 	]` | ||||
| ) | ||||
|   | ||||
| @@ -31,10 +31,6 @@ type Subscription struct { | ||||
| 	// required: true | ||||
| 	BlockID string `json:"blockId"` | ||||
|  | ||||
| 	// WorkspaceID is id of the workspace the block belongs to | ||||
| 	// required: true | ||||
| 	WorkspaceID string `json:"workspaceId"` | ||||
|  | ||||
| 	// SubscriberType is the type of the entity (e.g. user, channel) that is subscribing | ||||
| 	// required: true | ||||
| 	SubscriberType SubscriberType `json:"subscriberType"` | ||||
| @@ -63,9 +59,6 @@ func (s *Subscription) IsValid() error { | ||||
| 	if s.BlockID == "" { | ||||
| 		return ErrInvalidSubscription{"missing block id"} | ||||
| 	} | ||||
| 	if s.WorkspaceID == "" { | ||||
| 		return ErrInvalidSubscription{"missing workspace id"} | ||||
| 	} | ||||
| 	if s.BlockType == "" { | ||||
| 		return ErrInvalidSubscription{"missing block type"} | ||||
| 	} | ||||
|   | ||||
							
								
								
									
										46
									
								
								server/model/team.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								server/model/team.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| package model | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| ) | ||||
|  | ||||
| // Team is information global to a team | ||||
| // swagger:model | ||||
| type Team struct { | ||||
| 	// ID of the team | ||||
| 	// required: true | ||||
| 	ID string `json:"id"` | ||||
|  | ||||
| 	// Title of the team | ||||
| 	// required: false | ||||
| 	Title string `json:"title"` | ||||
|  | ||||
| 	// Token required to register new users | ||||
| 	// required: true | ||||
| 	SignupToken string `json:"signupToken"` | ||||
|  | ||||
| 	// Team settings | ||||
| 	// required: false | ||||
| 	Settings map[string]interface{} `json:"settings"` | ||||
|  | ||||
| 	// ID of user who last modified this | ||||
| 	// required: true | ||||
| 	ModifiedBy string `json:"modifiedBy"` | ||||
|  | ||||
| 	// Updated time | ||||
| 	// required: true | ||||
| 	UpdateAt int64 `json:"updateAt"` | ||||
| } | ||||
|  | ||||
| func TeamFromJSON(data io.Reader) *Team { | ||||
| 	var team *Team | ||||
| 	_ = json.NewDecoder(data).Decode(&team) | ||||
| 	return team | ||||
| } | ||||
|  | ||||
| func TeamsFromJSON(data io.Reader) []*Team { | ||||
| 	var teams []*Team | ||||
| 	_ = json.NewDecoder(data).Decode(&teams) | ||||
| 	return teams | ||||
| } | ||||
| @@ -57,6 +57,8 @@ type User struct { | ||||
| 	IsBot bool `json:"is_bot"` | ||||
| } | ||||
|  | ||||
| // UserPropPatch is a user property patch | ||||
| // swagger:model | ||||
| type UserPropPatch struct { | ||||
| 	// The user prop updated fields | ||||
| 	// required: false | ||||
|   | ||||
| @@ -1,45 +0,0 @@ | ||||
| package model | ||||
|  | ||||
| // Workspace is information global to a workspace | ||||
| // swagger:model | ||||
| type Workspace struct { | ||||
| 	// ID of the workspace | ||||
| 	// required: true | ||||
| 	ID string `json:"id"` | ||||
|  | ||||
| 	// Title of the workspace | ||||
| 	// required: false | ||||
| 	Title string `json:"title"` | ||||
|  | ||||
| 	// Token required to register new users | ||||
| 	// required: true | ||||
| 	SignupToken string `json:"signupToken"` | ||||
|  | ||||
| 	// Workspace settings | ||||
| 	// required: false | ||||
| 	Settings map[string]interface{} `json:"settings"` | ||||
|  | ||||
| 	// ID of user who last modified this | ||||
| 	// required: true | ||||
| 	ModifiedBy string `json:"modifiedBy"` | ||||
|  | ||||
| 	// Updated time | ||||
| 	// required: true | ||||
| 	UpdateAt int64 `json:"updateAt"` | ||||
| } | ||||
|  | ||||
| // UserWorkspace is a summary of a single association between | ||||
| // a user and a workspace | ||||
| // swagger:model | ||||
| type UserWorkspace struct { | ||||
| 	// ID of the workspace | ||||
| 	// required: true | ||||
| 	ID string `json:"id"` | ||||
|  | ||||
| 	// Title of the workspace | ||||
| 	// required: false | ||||
| 	Title string `json:"title"` | ||||
|  | ||||
| 	// Number of boards in the workspace | ||||
| 	BoardCount int `json:"boardCount"` | ||||
| } | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/services/config" | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/ws" | ||||
|  | ||||
| @@ -12,13 +13,14 @@ import ( | ||||
| ) | ||||
|  | ||||
| type Params struct { | ||||
| 	Cfg             *config.Configuration | ||||
| 	SingleUserToken string | ||||
| 	DBStore         store.Store | ||||
| 	Logger          *mlog.Logger | ||||
| 	ServerID        string | ||||
| 	WSAdapter       ws.Adapter | ||||
| 	NotifyBackends  []notify.Backend | ||||
| 	Cfg                *config.Configuration | ||||
| 	SingleUserToken    string | ||||
| 	DBStore            store.Store | ||||
| 	Logger             *mlog.Logger | ||||
| 	ServerID           string | ||||
| 	WSAdapter          ws.Adapter | ||||
| 	NotifyBackends     []notify.Backend | ||||
| 	PermissionsService permissions.PermissionsService | ||||
| } | ||||
|  | ||||
| func (p Params) CheckValid() error { | ||||
| @@ -33,6 +35,10 @@ func (p Params) CheckValid() error { | ||||
| 	if p.Logger == nil { | ||||
| 		return ErrServerParam{name: "Logger", issue: "cannot be nil"} | ||||
| 	} | ||||
|  | ||||
| 	if p.PermissionsService == nil { | ||||
| 		return ErrServerParam{name: "Permissions", issue: "cannot be nil"} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -74,12 +74,12 @@ func New(params Params) (*Server, error) { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	authenticator := auth.New(params.Cfg, params.DBStore) | ||||
| 	authenticator := auth.New(params.Cfg, params.DBStore, params.PermissionsService) | ||||
|  | ||||
| 	// if no ws adapter is provided, we spin up a websocket server | ||||
| 	wsAdapter := params.WSAdapter | ||||
| 	if wsAdapter == nil { | ||||
| 		wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger) | ||||
| 		wsAdapter = ws.NewServer(authenticator, params.SingleUserToken, params.Cfg.AuthMode == MattermostAuthMod, params.Logger, params.DBStore) | ||||
| 	} | ||||
|  | ||||
| 	filesBackendSettings := filestore.FileBackendSettings{} | ||||
| @@ -137,18 +137,19 @@ func New(params Params) (*Server, error) { | ||||
| 		Metrics:       metricsService, | ||||
| 		Notifications: notificationService, | ||||
| 		Logger:        params.Logger, | ||||
| 		Permissions:   params.PermissionsService, | ||||
| 	} | ||||
| 	app := app.New(params.Cfg, wsAdapter, appServices) | ||||
|  | ||||
| 	focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.Logger, auditService) | ||||
| 	focalboardAPI := api.NewAPI(app, params.SingleUserToken, params.Cfg.AuthMode, params.PermissionsService, params.Logger, auditService) | ||||
|  | ||||
| 	// Local router for admin APIs | ||||
| 	localRouter := mux.NewRouter() | ||||
| 	focalboardAPI.RegisterAdminRoutes(localRouter) | ||||
|  | ||||
| 	// Init workspace | ||||
| 	if _, err := app.GetRootWorkspace(); err != nil { | ||||
| 		params.Logger.Error("Unable to get root workspace", mlog.Err(err)) | ||||
| 	// Init team | ||||
| 	if _, err := app.GetRootTeam(); err != nil { | ||||
| 		params.Logger.Error("Unable to get root team", mlog.Err(err)) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -202,6 +203,11 @@ func New(params Params) (*Server, error) { | ||||
|  | ||||
| 	server.initHandlers() | ||||
|  | ||||
| 	if err := app.InitTemplates(); err != nil { | ||||
| 		params.Logger.Error("Unable initialize team templates", mlog.Err(err)) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &server, nil | ||||
| } | ||||
|  | ||||
| @@ -272,13 +278,13 @@ func (s *Server) Start() error { | ||||
| 		for blockType, count := range blockCounts { | ||||
| 			s.metricsService.ObserveBlockCount(blockType, count) | ||||
| 		} | ||||
| 		workspaceCount, err := s.store.GetWorkspaceCount() | ||||
| 		teamCount, err := s.store.GetTeamCount() | ||||
| 		if err != nil { | ||||
| 			s.logger.Error("Error updating metrics", mlog.String("group", "workspaces"), mlog.Err(err)) | ||||
| 			s.logger.Error("Error updating metrics", mlog.String("group", "teams"), mlog.Err(err)) | ||||
| 			return | ||||
| 		} | ||||
| 		s.logger.Log(mlog.LvlFBMetrics, "Workspace metrics collected", mlog.Int64("workspace_count", workspaceCount)) | ||||
| 		s.metricsService.ObserveWorkspaceCount(workspaceCount) | ||||
| 		s.logger.Log(mlog.LvlFBMetrics, "Team metrics collected", mlog.Int64("team_count", teamCount)) | ||||
| 		s.metricsService.ObserveTeamCount(teamCount) | ||||
| 	} | ||||
| 	// metricsUpdater()   Calling this immediately causes integration unit tests to fail. | ||||
| 	s.metricsUpdaterTask = scheduler.CreateRecurringTask("updateMetrics", metricsUpdater, updateMetricsTaskFrequency) | ||||
| @@ -336,6 +342,8 @@ func (s *Server) Shutdown() error { | ||||
| 		s.logger.Warn("Error occurred when shutting down notification service", mlog.Err(err)) | ||||
| 	} | ||||
|  | ||||
| 	s.app.Shutdown() | ||||
|  | ||||
| 	defer s.logger.Info("Server.Shutdown") | ||||
|  | ||||
| 	return s.store.Shutdown() | ||||
| @@ -472,13 +480,13 @@ func initTelemetry(opts telemetryOptions) *telemetry.Service { | ||||
| 		} | ||||
| 		return m, nil | ||||
| 	}) | ||||
| 	telemetryService.RegisterTracker("workspaces", func() (telemetry.Tracker, error) { | ||||
| 		count, err := opts.app.GetWorkspaceCount() | ||||
| 	telemetryService.RegisterTracker("teams", func() (telemetry.Tracker, error) { | ||||
| 		count, err := opts.app.GetTeamCount() | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		m := map[string]interface{}{ | ||||
| 			"workspaces": count, | ||||
| 			"teams": count, | ||||
| 		} | ||||
| 		return m, nil | ||||
| 	}) | ||||
|   | ||||
| @@ -7,15 +7,15 @@ import ( | ||||
| const ( | ||||
| 	DefMaxQueueSize = 1000 | ||||
|  | ||||
| 	KeyAPIPath     = "api_path" | ||||
| 	KeyEvent       = "event" | ||||
| 	KeyStatus      = "status" | ||||
| 	KeyUserID      = "user_id" | ||||
| 	KeySessionID   = "session_id" | ||||
| 	KeyClient      = "client" | ||||
| 	KeyIPAddress   = "ip_address" | ||||
| 	KeyClusterID   = "cluster_id" | ||||
| 	KeyWorkspaceID = "workspace_id" | ||||
| 	KeyAPIPath   = "api_path" | ||||
| 	KeyEvent     = "event" | ||||
| 	KeyStatus    = "status" | ||||
| 	KeyUserID    = "user_id" | ||||
| 	KeySessionID = "session_id" | ||||
| 	KeyClient    = "client" | ||||
| 	KeyIPAddress = "ip_address" | ||||
| 	KeyClusterID = "cluster_id" | ||||
| 	KeyTeamID    = "team_id" | ||||
|  | ||||
| 	Success = "success" | ||||
| 	Attempt = "attempt" | ||||
|   | ||||
| @@ -2,7 +2,6 @@ package auth | ||||
|  | ||||
| import "regexp" | ||||
|  | ||||
| //nolint:lll | ||||
| var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") | ||||
|  | ||||
| // IsEmailValid checks if the email provided passes the required structure and length. | ||||
|   | ||||
| @@ -8,10 +8,10 @@ import ( | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	MetricsNamespace           = "focalboard" | ||||
| 	MetricsSubsystemBlocks     = "blocks" | ||||
| 	MetricsSubsystemWorkspaces = "workspaces" | ||||
| 	MetricsSubsystemSystem     = "system" | ||||
| 	MetricsNamespace       = "focalboard" | ||||
| 	MetricsSubsystemBlocks = "blocks" | ||||
| 	MetricsSubsystemTeams  = "teams" | ||||
| 	MetricsSubsystemSystem = "system" | ||||
|  | ||||
| 	MetricsCloudInstallationLabel = "installationId" | ||||
| ) | ||||
| @@ -38,8 +38,8 @@ type Metrics struct { | ||||
| 	blocksPatchedCount  prometheus.Counter | ||||
| 	blocksDeletedCount  prometheus.Counter | ||||
|  | ||||
| 	blockCount     *prometheus.GaugeVec | ||||
| 	workspaceCount prometheus.Gauge | ||||
| 	blockCount *prometheus.GaugeVec | ||||
| 	teamCount  prometheus.Gauge | ||||
|  | ||||
| 	blockLastActivity prometheus.Gauge | ||||
| } | ||||
| @@ -143,14 +143,14 @@ func NewMetrics(info InstanceInfo) *Metrics { | ||||
| 	}, []string{"BlockType"}) | ||||
| 	m.registry.MustRegister(m.blockCount) | ||||
|  | ||||
| 	m.workspaceCount = prometheus.NewGauge(prometheus.GaugeOpts{ | ||||
| 	m.teamCount = prometheus.NewGauge(prometheus.GaugeOpts{ | ||||
| 		Namespace:   MetricsNamespace, | ||||
| 		Subsystem:   MetricsSubsystemWorkspaces, | ||||
| 		Name:        "workspaces_total", | ||||
| 		Help:        "Total number of workspaces.", | ||||
| 		Subsystem:   MetricsSubsystemTeams, | ||||
| 		Name:        "teams_total", | ||||
| 		Help:        "Total number of teams.", | ||||
| 		ConstLabels: additionalLabels, | ||||
| 	}) | ||||
| 	m.registry.MustRegister(m.workspaceCount) | ||||
| 	m.registry.MustRegister(m.teamCount) | ||||
|  | ||||
| 	m.blockLastActivity = prometheus.NewGauge(prometheus.GaugeOpts{ | ||||
| 		Namespace:   MetricsNamespace, | ||||
| @@ -209,8 +209,8 @@ func (m *Metrics) ObserveBlockCount(blockType string, count int64) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *Metrics) ObserveWorkspaceCount(count int64) { | ||||
| func (m *Metrics) ObserveTeamCount(count int64) { | ||||
| 	if m != nil { | ||||
| 		m.workspaceCount.Set(float64(count)) | ||||
| 		m.teamCount.Set(float64(count)) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,6 @@ import ( | ||||
| // SubscriptionDelivery provides an interface for delivering subscription notifications to other systems, such as | ||||
| // channels server via plugin API. | ||||
| type SubscriptionDelivery interface { | ||||
| 	SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriberType model.SubscriberType, | ||||
| 	SubscriptionDeliverSlackAttachments(subscriberID string, subscriberType model.SubscriberType, | ||||
| 		attachments []*mm_model.SlackAttachment) error | ||||
| } | ||||
|   | ||||
| @@ -8,14 +8,13 @@ import ( | ||||
| 	"sort" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
|  | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| // Diff represents a difference between two versions of a block. | ||||
| type Diff struct { | ||||
| 	Board   *model.Block | ||||
| 	Board   *model.Board | ||||
| 	Card    *model.Block | ||||
| 	Authors StringMap | ||||
|  | ||||
| @@ -40,16 +39,15 @@ type PropDiff struct { | ||||
| } | ||||
|  | ||||
| type SchemaDiff struct { | ||||
| 	Board *model.Block | ||||
| 	Board *model.Board | ||||
|  | ||||
| 	OldPropDef *model.PropDef | ||||
| 	NewPropDef *model.PropDef | ||||
| } | ||||
|  | ||||
| type diffGenerator struct { | ||||
| 	container store.Container | ||||
| 	board     *model.Block | ||||
| 	card      *model.Block | ||||
| 	board *model.Board | ||||
| 	card  *model.Block | ||||
|  | ||||
| 	store        Store | ||||
| 	hint         *model.NotificationHint | ||||
| @@ -63,7 +61,7 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { | ||||
| 		Limit:      1, | ||||
| 		Descending: true, | ||||
| 	} | ||||
| 	blocks, err := dg.store.GetBlockHistory(dg.container, dg.hint.BlockID, opts) | ||||
| 	blocks, err := dg.store.GetBlockHistory(dg.hint.BlockID, opts) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not get block for notification: %w", err) | ||||
| 	} | ||||
| @@ -84,7 +82,10 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { | ||||
|  | ||||
| 	switch block.Type { | ||||
| 	case model.TypeBoard: | ||||
| 		return dg.generateDiffsForBoard(block, schema) | ||||
| 		dg.logger.Warn("generateDiffs for board skipped", mlog.String("block_id", block.ID)) | ||||
| 		// TODO: Fix this | ||||
| 		// return dg.generateDiffsForBoard(block, schema) | ||||
| 		return nil, nil | ||||
| 	case model.TypeCard: | ||||
| 		diff, err := dg.generateDiffsForCard(block, schema) | ||||
| 		if err != nil || diff == nil { | ||||
| @@ -100,27 +101,29 @@ func (dg *diffGenerator) generateDiffs() ([]*Diff, error) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model.PropSchema) ([]*Diff, error) { | ||||
| // TODO: fix this | ||||
| /* | ||||
| func (dg *diffGenerator) generateDiffsForBoard(board *model.Board, schema model.PropSchema) ([]*Diff, error) { | ||||
| 	opts := model.QuerySubtreeOptions{ | ||||
| 		AfterUpdateAt: dg.lastNotifyAt, | ||||
| 	} | ||||
|  | ||||
| 	// find all child blocks of the board that updated since last notify. | ||||
| 	blocks, err := dg.store.GetSubTree2(dg.container, board.ID, opts) | ||||
| 	find all child blocks of the board that updated since last notify. | ||||
| 	blocks, err := dg.store.GetSubTree2(board.ID, board.ID, opts) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not get subtree for board %s: %w", board.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	var diffs []*Diff | ||||
|  | ||||
| 	// generate diff for board title change or description | ||||
| 	generate diff for board title change or description | ||||
| 	boardDiff, err := dg.generateDiffForBlock(board, schema) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not generate diff for board %s: %w", board.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	if boardDiff != nil { | ||||
| 		// TODO: phase 2 feature (generate schema diffs and add to board diff) goes here. | ||||
| 		TODO: phase 2 feature (generate schema diffs and add to board diff) goes here. | ||||
| 		diffs = append(diffs, boardDiff) | ||||
| 	} | ||||
|  | ||||
| @@ -136,6 +139,7 @@ func (dg *diffGenerator) generateDiffsForBoard(board *model.Block, schema model. | ||||
| 	} | ||||
| 	return diffs, nil | ||||
| } | ||||
| */ | ||||
|  | ||||
| func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.PropSchema) (*Diff, error) { | ||||
| 	// generate diff for card title change and properties. | ||||
| @@ -148,7 +152,7 @@ func (dg *diffGenerator) generateDiffsForCard(card *model.Block, schema model.Pr | ||||
| 	opts := model.QuerySubtreeOptions{ | ||||
| 		AfterUpdateAt: dg.lastNotifyAt, | ||||
| 	} | ||||
| 	blocks, err := dg.store.GetSubTree2(dg.container, card.ID, opts) | ||||
| 	blocks, err := dg.store.GetSubTree2(card.BoardID, card.ID, opts) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not get subtree for card %s: %w", card.ID, err) | ||||
| 	} | ||||
| @@ -214,7 +218,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode | ||||
| 		Limit:          1, | ||||
| 		Descending:     true, | ||||
| 	} | ||||
| 	history, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts) | ||||
| 	history, err := dg.store.GetBlockHistory(newBlock.ID, opts) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("could not get block history for block %s: %w", newBlock.ID, err) | ||||
| 	} | ||||
| @@ -237,7 +241,7 @@ func (dg *diffGenerator) generateDiffForBlock(newBlock *model.Block, schema mode | ||||
| 		AfterUpdateAt: dg.lastNotifyAt, | ||||
| 		Descending:    true, | ||||
| 	} | ||||
| 	chgBlocks, err := dg.store.GetBlockHistory(dg.container, newBlock.ID, opts) | ||||
| 	chgBlocks, err := dg.store.GetBlockHistory(newBlock.ID, opts) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error getting block history for block %s: %w", newBlock.ID, err) | ||||
| 	} | ||||
|   | ||||
| @@ -34,7 +34,7 @@ var ( | ||||
| // DiffConvOpts provides options when converting diffs to slack attachments. | ||||
| type DiffConvOpts struct { | ||||
| 	Language     string | ||||
| 	MakeCardLink func(block *model.Block, board *model.Block, card *model.Block) string | ||||
| 	MakeCardLink func(block *model.Block, board *model.Board, card *model.Block) string | ||||
| 	Logger       *mlog.Logger | ||||
| } | ||||
|  | ||||
| @@ -49,7 +49,7 @@ func getTemplate(name string, opts DiffConvOpts, def string) (*template.Template | ||||
| 		t = template.New(key) | ||||
|  | ||||
| 		if opts.MakeCardLink == nil { | ||||
| 			opts.MakeCardLink = func(block *model.Block, _ *model.Block, _ *model.Block) string { | ||||
| 			opts.MakeCardLink = func(block *model.Block, _ *model.Board, _ *model.Block) string { | ||||
| 				return fmt.Sprintf("`%s`", block.Title) | ||||
| 			} | ||||
| 		} | ||||
| @@ -160,6 +160,7 @@ func cardDiff2SlackAttachment(cardDiff *Diff, opts DiffConvOpts) (*mm_model.Slac | ||||
| 		mlog.String("card_id", cardDiff.Card.ID), | ||||
| 		mlog.String("new_block_id", cardDiff.NewBlock.ID), | ||||
| 		mlog.String("old_block_id", cardDiff.OldBlock.ID), | ||||
| 		mlog.Int("childDiffs", len(cardDiff.Diffs)), | ||||
| 	) | ||||
|  | ||||
| 	buf.Reset() | ||||
|   | ||||
| @@ -144,12 +144,8 @@ func (n *notifier) notify() { | ||||
| } | ||||
|  | ||||
| func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { | ||||
| 	c := store.Container{ | ||||
| 		WorkspaceID: hint.WorkspaceID, | ||||
| 	} | ||||
|  | ||||
| 	// 	get the subscriber list | ||||
| 	subs, err := n.store.GetSubscribersForBlock(c, hint.BlockID) | ||||
| 	subs, err := n.store.GetSubscribersForBlock(hint.BlockID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -162,7 +158,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { | ||||
| 	oldestNotifiedAt := subs[0].NotifiedAt | ||||
|  | ||||
| 	// need the block's board and card. | ||||
| 	board, card, err := n.store.GetBoardAndCardByID(c, hint.BlockID) | ||||
| 	board, card, err := n.store.GetBoardAndCardByID(hint.BlockID) | ||||
| 	if err != nil || board == nil || card == nil { | ||||
| 		return fmt.Errorf("could not get board & card for block %s: %w", hint.BlockID, err) | ||||
| 	} | ||||
| @@ -175,7 +171,6 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { | ||||
| 	) | ||||
|  | ||||
| 	dg := &diffGenerator{ | ||||
| 		container:    c, | ||||
| 		board:        board, | ||||
| 		card:         card, | ||||
| 		store:        n.store, | ||||
| @@ -204,8 +199,8 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { | ||||
|  | ||||
| 	opts := DiffConvOpts{ | ||||
| 		Language: "en", // TODO: use correct language with i18n available on server. | ||||
| 		MakeCardLink: func(block *model.Block, board *model.Block, card *model.Block) string { | ||||
| 			return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.WorkspaceID, board.ID, card.ID)) | ||||
| 		MakeCardLink: func(block *model.Block, board *model.Board, card *model.Block) string { | ||||
| 			return fmt.Sprintf("[%s](%s)", block.Title, utils.MakeCardLink(n.serverRoot, board.TeamID, board.ID, card.ID)) | ||||
| 		}, | ||||
| 		Logger: n.logger, | ||||
| 	} | ||||
| @@ -236,7 +231,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { | ||||
| 				mlog.String("subscriber_type", string(sub.SubscriberType)), | ||||
| 			) | ||||
|  | ||||
| 			if err = n.delivery.SubscriptionDeliverSlackAttachments(hint.WorkspaceID, sub.SubscriberID, sub.SubscriberType, attachments); err != nil { | ||||
| 			if err = n.delivery.SubscriptionDeliverSlackAttachments(sub.SubscriberID, sub.SubscriberType, attachments); err != nil { | ||||
| 				merr.Append(fmt.Errorf("cannot deliver notification to subscriber %s [%s]: %w", | ||||
| 					sub.SubscriberID, sub.SubscriberType, err)) | ||||
| 			} | ||||
| @@ -262,7 +257,7 @@ func (n *notifier) notifySubscribers(hint *model.NotificationHint) error { | ||||
| 	} | ||||
|  | ||||
| 	// update the last notified_at for all subscribers since we at least attempted to notify all of them. | ||||
| 	err = dg.store.UpdateSubscribersNotifiedAt(dg.container, dg.hint.BlockID, notifiedAt) | ||||
| 	err = dg.store.UpdateSubscribersNotifiedAt(dg.hint.BlockID, notifiedAt) | ||||
| 	if err != nil { | ||||
| 		merr.Append(fmt.Errorf("could not update subscribers notified_at for block %s: %w", dg.hint.BlockID, err)) | ||||
| 	} | ||||
|   | ||||
| @@ -7,21 +7,20 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| ) | ||||
|  | ||||
| type Store interface { | ||||
| 	GetBlock(c store.Container, blockID string) (*model.Block, error) | ||||
| 	GetBlockHistory(c store.Container, blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) | ||||
| 	GetSubTree2(c store.Container, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) | ||||
| 	GetBoardAndCardByID(c store.Container, blockID string) (board *model.Block, card *model.Block, err error) | ||||
| 	GetBlock(blockID string) (*model.Block, error) | ||||
| 	GetBlockHistory(blockID string, opts model.QueryBlockHistoryOptions) ([]model.Block, error) | ||||
| 	GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error) | ||||
| 	GetBoardAndCardByID(blockID string) (board *model.Board, card *model.Block, err error) | ||||
|  | ||||
| 	GetUserByID(userID string) (*model.User, error) | ||||
|  | ||||
| 	CreateSubscription(c store.Container, sub *model.Subscription) (*model.Subscription, error) | ||||
| 	GetSubscribersForBlock(c store.Container, blockID string) ([]*model.Subscriber, error) | ||||
| 	GetSubscribersCountForBlock(c store.Container, blockID string) (int, error) | ||||
| 	UpdateSubscribersNotifiedAt(c store.Container, blockID string, notifyAt int64) error | ||||
| 	CreateSubscription(sub *model.Subscription) (*model.Subscription, error) | ||||
| 	GetSubscribersForBlock(blockID string) ([]*model.Subscriber, error) | ||||
| 	GetSubscribersCountForBlock(blockID string) (int, error) | ||||
| 	UpdateSubscribersNotifiedAt(blockID string, notifyAt int64) error | ||||
|  | ||||
| 	UpsertNotificationHint(hint *model.NotificationHint, notificationFreq time.Duration) (*model.NotificationHint, error) | ||||
| 	GetNextNotificationHint(remove bool) (*model.NotificationHint, error) | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import ( | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
| 	"github.com/mattermost/focalboard/server/services/store" | ||||
| 	"github.com/mattermost/focalboard/server/ws" | ||||
| 	"github.com/wiggin77/merror" | ||||
|  | ||||
| @@ -77,8 +76,6 @@ func (b *Backend) getBlockUpdateFreq(blockType model.BlockType) time.Duration { | ||||
| 	switch blockType { | ||||
| 	case model.TypeCard: | ||||
| 		return time.Second * time.Duration(b.notifyFreqCardSeconds) | ||||
| 	case model.TypeBoard: | ||||
| 		return time.Second * time.Duration(b.notifyFreqBoardSeconds) | ||||
| 	default: | ||||
| 		return defBlockNotificationFreq | ||||
| 	} | ||||
| @@ -95,35 +92,30 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { | ||||
| 	merr := merror.New() | ||||
| 	var err error | ||||
|  | ||||
| 	c := store.Container{ | ||||
| 		WorkspaceID: evt.Workspace, | ||||
| 	} | ||||
|  | ||||
| 	// if new card added, automatically subscribe the author. | ||||
| 	if evt.Action == notify.Add && evt.BlockChanged.Type == model.TypeCard { | ||||
| 		sub := &model.Subscription{ | ||||
| 			BlockType:      model.TypeCard, | ||||
| 			BlockID:        evt.BlockChanged.ID, | ||||
| 			WorkspaceID:    evt.Workspace, | ||||
| 			SubscriberType: model.SubTypeUser, | ||||
| 			SubscriberID:   evt.ModifiedByID, | ||||
| 		} | ||||
|  | ||||
| 		if sub, err = b.store.CreateSubscription(c, sub); err != nil { | ||||
| 		if _, err = b.store.CreateSubscription(sub); err != nil { | ||||
| 			b.logger.Warn("Cannot subscribe card author to card", | ||||
| 				mlog.String("card_id", evt.BlockChanged.ID), | ||||
| 				mlog.Err(err), | ||||
| 			) | ||||
| 		} | ||||
| 		b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub) | ||||
| 		b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub) | ||||
| 	} | ||||
|  | ||||
| 	// notify board subscribers | ||||
| 	subs, err := b.store.GetSubscribersForBlock(c, evt.Board.ID) | ||||
| 	subs, err := b.store.GetSubscribersForBlock(evt.Board.ID) | ||||
| 	if err != nil { | ||||
| 		merr.Append(fmt.Errorf("cannot fetch subscribers for board %s: %w", evt.Board.ID, err)) | ||||
| 	} | ||||
| 	if err = b.notifySubscribers(subs, evt.Board, evt.ModifiedByID); err != nil { | ||||
| 	if err = b.notifySubscribers(subs, evt.Board.ID, model.TypeBoard, evt.ModifiedByID); err != nil { | ||||
| 		merr.Append(fmt.Errorf("cannot notify board subscribers for board %s: %w", evt.Board.ID, err)) | ||||
| 	} | ||||
|  | ||||
| @@ -132,21 +124,21 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { | ||||
| 	} | ||||
|  | ||||
| 	// notify card subscribers | ||||
| 	subs, err = b.store.GetSubscribersForBlock(c, evt.Card.ID) | ||||
| 	subs, err = b.store.GetSubscribersForBlock(evt.Card.ID) | ||||
| 	if err != nil { | ||||
| 		merr.Append(fmt.Errorf("cannot fetch subscribers for card %s: %w", evt.Card.ID, err)) | ||||
| 	} | ||||
| 	if err = b.notifySubscribers(subs, evt.Card, evt.ModifiedByID); err != nil { | ||||
| 	if err = b.notifySubscribers(subs, evt.Card.ID, model.TypeCard, evt.ModifiedByID); err != nil { | ||||
| 		merr.Append(fmt.Errorf("cannot notify card subscribers for card %s: %w", evt.Card.ID, err)) | ||||
| 	} | ||||
|  | ||||
| 	// notify block subscribers (if/when other types can be subscribed to) | ||||
| 	if evt.Board.ID != evt.BlockChanged.ID && evt.Card.ID != evt.BlockChanged.ID { | ||||
| 		subs, err := b.store.GetSubscribersForBlock(c, evt.BlockChanged.ID) | ||||
| 		subs, err := b.store.GetSubscribersForBlock(evt.BlockChanged.ID) | ||||
| 		if err != nil { | ||||
| 			merr.Append(fmt.Errorf("cannot fetch subscribers for block %s: %w", evt.BlockChanged.ID, err)) | ||||
| 		} | ||||
| 		if err := b.notifySubscribers(subs, evt.BlockChanged, evt.ModifiedByID); err != nil { | ||||
| 		if err := b.notifySubscribers(subs, evt.BlockChanged.ID, evt.BlockChanged.Type, evt.ModifiedByID); err != nil { | ||||
| 			merr.Append(fmt.Errorf("cannot notify block subscribers for block %s: %w", evt.BlockChanged.ID, err)) | ||||
| 		} | ||||
| 	} | ||||
| @@ -154,24 +146,26 @@ func (b *Backend) BlockChanged(evt notify.BlockChangeEvent) error { | ||||
| } | ||||
|  | ||||
| // notifySubscribers triggers a change notification for subscribers by writing a notification hint to the database. | ||||
| func (b *Backend) notifySubscribers(subs []*model.Subscriber, block *model.Block, modifiedByID string) error { | ||||
| func (b *Backend) notifySubscribers(subs []*model.Subscriber, blockID string, idType model.BlockType, modifiedByID string) error { | ||||
| 	if len(subs) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	hint := &model.NotificationHint{ | ||||
| 		BlockType:    block.Type, | ||||
| 		BlockID:      block.ID, | ||||
| 		WorkspaceID:  block.WorkspaceID, | ||||
| 		BlockType:    idType, | ||||
| 		BlockID:      blockID, | ||||
| 		ModifiedByID: modifiedByID, | ||||
| 	} | ||||
|  | ||||
| 	hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(block.Type)) | ||||
| 	hint, err := b.store.UpsertNotificationHint(hint, b.getBlockUpdateFreq(idType)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("cannot upsert notification hint: %w", err) | ||||
| 	} | ||||
| 	if err := b.notifier.onNotifyHint(hint); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return b.notifier.onNotifyHint(hint) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // OnMention satisfies the `MentionListener` interface and is called whenever a @mention notification | ||||
| @@ -188,17 +182,13 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) { | ||||
| 	sub := &model.Subscription{ | ||||
| 		BlockType:      model.TypeCard, | ||||
| 		BlockID:        evt.Card.ID, | ||||
| 		WorkspaceID:    evt.Workspace, | ||||
| 		SubscriberType: model.SubTypeUser, | ||||
| 		SubscriberID:   userID, | ||||
| 	} | ||||
|  | ||||
| 	c := store.Container{ | ||||
| 		WorkspaceID: evt.Workspace, | ||||
| 	} | ||||
| 	var err error | ||||
|  | ||||
| 	if sub, err = b.store.CreateSubscription(c, sub); err != nil { | ||||
| 	if sub, err = b.store.CreateSubscription(sub); err != nil { | ||||
| 		b.logger.Warn("Cannot subscribe mentioned user to card", | ||||
| 			mlog.String("user_id", userID), | ||||
| 			mlog.String("card_id", evt.Card.ID), | ||||
| @@ -206,7 +196,7 @@ func (b *Backend) OnMention(userID string, evt notify.BlockChangeEvent) { | ||||
| 		) | ||||
| 		return | ||||
| 	} | ||||
| 	b.wsAdapter.BroadcastSubscriptionChange(c.WorkspaceID, sub) | ||||
| 	b.wsAdapter.BroadcastSubscriptionChange(evt.TeamID, sub) | ||||
|  | ||||
| 	b.logger.Debug("Subscribed mentioned user to card", | ||||
| 		mlog.String("user_id", userID), | ||||
|   | ||||
| @@ -14,13 +14,7 @@ import ( | ||||
|  | ||||
| // MentionDeliver notifies a user they have been mentioned in a block. | ||||
| func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) (string, error) { | ||||
| 	// determine which team the workspace is associated with | ||||
| 	teamID, err := pd.getTeamID(evt) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("cannot determine teamID for block change notification: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	member, err := teamMemberFromUsername(pd.api, mentionUsername, teamID) | ||||
| 	member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID) | ||||
| 	if err != nil { | ||||
| 		if isErrNotFound(err) { | ||||
| 			// not really an error; could just be someone typed "@sometext" | ||||
| @@ -30,16 +24,6 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// check that user is a member of the channel | ||||
| 	_, err = pd.api.GetChannelMember(evt.Workspace, member.UserId) | ||||
| 	if err != nil { | ||||
| 		if pd.api.IsErrNotFound(err) { | ||||
| 			// mentioned user is not a member of the channel; fail silently. | ||||
| 			return "", nil | ||||
| 		} | ||||
| 		return "", fmt.Errorf("cannot fetch channel member for user %s: %w", member.UserId, err) | ||||
| 	} | ||||
|  | ||||
| 	author, err := pd.api.GetUserByID(evt.ModifiedByID) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("cannot find user: %w", err) | ||||
| @@ -49,7 +33,7 @@ func (pd *PluginDelivery) MentionDeliver(mentionUsername string, extract string, | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("cannot get direct channel: %w", err) | ||||
| 	} | ||||
| 	link := utils.MakeCardLink(pd.serverRoot, evt.Workspace, evt.Board.ID, evt.Card.ID) | ||||
| 	link := utils.MakeCardLink(pd.serverRoot, evt.Board.TeamID, evt.Board.ID, evt.Card.ID) | ||||
|  | ||||
| 	post := &model.Post{ | ||||
| 		UserId:    pd.botID, | ||||
|   | ||||
| @@ -4,7 +4,10 @@ | ||||
| package plugindelivery | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/services/notify" | ||||
| 	"github.com/mattermost/focalboard/server/utils" | ||||
|  | ||||
| 	mm_model "github.com/mattermost/mattermost-server/v6/model" | ||||
| ) | ||||
| @@ -52,11 +55,32 @@ func New(botID string, serverRoot string, api PluginAPI) *PluginDelivery { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (pd *PluginDelivery) getTeamID(evt notify.BlockChangeEvent) (string, error) { | ||||
| 	// for now, the workspace ID is also the channel ID | ||||
| 	channel, err := pd.api.GetChannelByID(evt.Workspace) | ||||
| func (pd *PluginDelivery) Deliver(mentionUsername string, extract string, evt notify.BlockChangeEvent) error { | ||||
| 	member, err := teamMemberFromUsername(pd.api, mentionUsername, evt.TeamID) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 		if isErrNotFound(err) { | ||||
| 			// not really an error; could just be someone typed "@sometext" | ||||
| 			return nil | ||||
| 		} else { | ||||
| 			return fmt.Errorf("cannot lookup mentioned user: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 	return channel.TeamId, nil | ||||
|  | ||||
| 	author, err := pd.api.GetUserByID(evt.ModifiedByID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("cannot find user: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	channel, err := pd.api.GetDirectChannel(member.UserId, pd.botID) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("cannot get direct channel: %w", err) | ||||
| 	} | ||||
| 	link := utils.MakeCardLink(pd.serverRoot, evt.TeamID, evt.Board.ID, evt.Card.ID) | ||||
|  | ||||
| 	post := &mm_model.Post{ | ||||
| 		UserId:    pd.botID, | ||||
| 		ChannelId: channel.Id, | ||||
| 		Message:   formatMessage(author.Username, extract, evt.Card.Title, link, evt.BlockChanged), | ||||
| 	} | ||||
| 	return pd.api.CreatePost(post) | ||||
| } | ||||
|   | ||||
| @@ -17,10 +17,10 @@ var ( | ||||
| ) | ||||
|  | ||||
| // SubscriptionDeliverSlashAttachments notifies a user that changes were made to a block they are subscribed to. | ||||
| func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(workspaceID string, subscriberID string, subscriptionType model.SubscriberType, | ||||
| func (pd *PluginDelivery) SubscriptionDeliverSlackAttachments(subscriberID string, subscriptionType model.SubscriberType, | ||||
| 	attachments []*mm_model.SlackAttachment) error { | ||||
| 	// check subscriber is member of channel | ||||
| 	_, err := pd.api.GetChannelMember(workspaceID, subscriberID) | ||||
| 	_, err := pd.api.GetUserByID(subscriberID) | ||||
| 	if err != nil { | ||||
| 		if pd.api.IsErrNotFound(err) { | ||||
| 			// subscriber is not a member of the channel; fail silently. | ||||
|   | ||||
| @@ -22,8 +22,8 @@ const ( | ||||
|  | ||||
| type BlockChangeEvent struct { | ||||
| 	Action       Action | ||||
| 	Workspace    string | ||||
| 	Board        *model.Block | ||||
| 	TeamID       string | ||||
| 	Board        *model.Board | ||||
| 	Card         *model.Block | ||||
| 	BlockChanged *model.Block | ||||
| 	BlockOld     *model.Block | ||||
| @@ -31,7 +31,7 @@ type BlockChangeEvent struct { | ||||
| } | ||||
|  | ||||
| type SubscriptionChangeNotifier interface { | ||||
| 	BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) | ||||
| 	BroadcastSubscriptionChange(subscription *model.Subscription) | ||||
| } | ||||
|  | ||||
| // Backend provides an interface for sending notifications. | ||||
| @@ -113,7 +113,7 @@ func (s *Service) BlockChanged(evt BlockChangeEvent) { | ||||
|  | ||||
| // BroadcastSubscriptionChange sends a websocket message with details of the changed subscription to all | ||||
| // connected users in the workspace. | ||||
| func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription *model.Subscription) { | ||||
| func (s *Service) BroadcastSubscriptionChange(subscription *model.Subscription) { | ||||
| 	s.mux.RLock() | ||||
| 	backends := make([]Backend, len(s.backends)) | ||||
| 	copy(backends, s.backends) | ||||
| @@ -122,11 +122,10 @@ func (s *Service) BroadcastSubscriptionChange(workspaceID string, subscription * | ||||
| 	for _, backend := range backends { | ||||
| 		if scn, ok := backend.(SubscriptionChangeNotifier); ok { | ||||
| 			s.logger.Debug("Delivering subscription change notification", | ||||
| 				mlog.String("workspace_id", workspaceID), | ||||
| 				mlog.String("block_id", subscription.BlockID), | ||||
| 				mlog.String("subscriber_id", subscription.SubscriberID), | ||||
| 			) | ||||
| 			scn.BroadcastSubscriptionChange(workspaceID, subscription) | ||||
| 			scn.BroadcastSubscriptionChange(subscription) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										61
									
								
								server/services/permissions/localpermissions/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								server/services/permissions/localpermissions/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| package localpermissions | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type TestHelper struct { | ||||
| 	t           *testing.T | ||||
| 	ctrl        *gomock.Controller | ||||
| 	store       *permissionsMocks.MockStore | ||||
| 	permissions *Service | ||||
| } | ||||
|  | ||||
| func SetupTestHelper(t *testing.T) *TestHelper { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	mockStore := permissionsMocks.NewMockStore(ctrl) | ||||
|  | ||||
| 	return &TestHelper{ | ||||
| 		t:           t, | ||||
| 		ctrl:        ctrl, | ||||
| 		store:       mockStore, | ||||
| 		permissions: New(mockStore, nil), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) { | ||||
| 	for _, p := range hasPermissionTo { | ||||
| 		th.t.Run(roleName+" "+p.Id, func(t *testing.T) { | ||||
| 			th.store.EXPECT(). | ||||
| 				GetMemberForBoard(member.BoardID, member.UserID). | ||||
| 				Return(member, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) | ||||
| 			assert.True(t, hasPermission) | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	for _, p := range hasNotPermissionTo { | ||||
| 		th.t.Run(roleName+" "+p.Id, func(t *testing.T) { | ||||
| 			th.store.EXPECT(). | ||||
| 				GetMemberForBoard(member.BoardID, member.UserID). | ||||
| 				Return(member, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) | ||||
| 			assert.False(t, hasPermission) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,64 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| package localpermissions | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	"github.com/mattermost/focalboard/server/services/permissions" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
| 	"github.com/mattermost/mattermost-server/v6/shared/mlog" | ||||
| ) | ||||
|  | ||||
| type Service struct { | ||||
| 	store  permissions.Store | ||||
| 	logger *mlog.Logger | ||||
| } | ||||
|  | ||||
| func New(store permissions.Store, logger *mlog.Logger) *Service { | ||||
| 	return &Service{ | ||||
| 		store:  store, | ||||
| 		logger: logger, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (s *Service) HasPermissionToTeam(userID, teamID string, permission *mmModel.Permission) bool { | ||||
| 	if userID == "" || teamID == "" || permission == nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmModel.Permission) bool { | ||||
| 	if userID == "" || boardID == "" || permission == nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	member, err := s.store.GetMemberForBoard(boardID, userID) | ||||
| 	if errors.Is(err, sql.ErrNoRows) { | ||||
| 		return false | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		s.logger.Error("error getting member for board", | ||||
| 			mlog.String("boardID", boardID), | ||||
| 			mlog.String("userID", userID), | ||||
| 			mlog.Err(err), | ||||
| 		) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	switch permission { | ||||
| 	case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard: | ||||
| 		return member.SchemeAdmin | ||||
| 	case model.PermissionManageBoardCards, model.PermissionManageBoardProperties: | ||||
| 		return member.SchemeAdmin || member.SchemeEditor | ||||
| 	case model.PermissionViewBoard: | ||||
| 		return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer | ||||
| 	default: | ||||
| 		return false | ||||
| 	} | ||||
| } | ||||
| @@ -0,0 +1,144 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| package localpermissions | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestHasPermissionToTeam(t *testing.T) { | ||||
| 	th := SetupTestHelper(t) | ||||
|  | ||||
| 	t.Run("empty input should always unauthorize", func(t *testing.T) { | ||||
| 		assert.False(t, th.permissions.HasPermissionToTeam("", "team-id", model.PermissionManageBoardCards)) | ||||
| 		assert.False(t, th.permissions.HasPermissionToTeam("user-id", "", model.PermissionManageBoardCards)) | ||||
| 		assert.False(t, th.permissions.HasPermissionToTeam("user-id", "team-id", nil)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("all users have all permissions on teams", func(t *testing.T) { | ||||
| 		hasPermission := th.permissions.HasPermissionToTeam("user-id", "team-id", model.PermissionManageBoardCards) | ||||
| 		assert.True(t, hasPermission) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestHasPermissionToBoard(t *testing.T) { | ||||
| 	th := SetupTestHelper(t) | ||||
|  | ||||
| 	t.Run("empty input should always unauthorize", func(t *testing.T) { | ||||
| 		assert.False(t, th.permissions.HasPermissionToBoard("", "board-id", model.PermissionManageBoardCards)) | ||||
| 		assert.False(t, th.permissions.HasPermissionToBoard("user-id", "", model.PermissionManageBoardCards)) | ||||
| 		assert.False(t, th.permissions.HasPermissionToBoard("user-id", "board-id", nil)) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("nonexistent user", func(t *testing.T) { | ||||
| 		userID := "user-id" | ||||
| 		boardID := "board-id" | ||||
|  | ||||
| 		th.store.EXPECT(). | ||||
| 			GetMemberForBoard(boardID, userID). | ||||
| 			Return(nil, sql.ErrNoRows). | ||||
| 			Times(1) | ||||
|  | ||||
| 		hasPermission := th.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) | ||||
| 		assert.False(t, hasPermission) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("board admin", func(t *testing.T) { | ||||
| 		member := &model.BoardMember{ | ||||
| 			UserID:      "user-id", | ||||
| 			BoardID:     "board-id", | ||||
| 			SchemeAdmin: true, | ||||
| 		} | ||||
|  | ||||
| 		hasPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardType, | ||||
| 			model.PermissionDeleteBoard, | ||||
| 			model.PermissionManageBoardRoles, | ||||
| 			model.PermissionShareBoard, | ||||
| 			model.PermissionManageBoardCards, | ||||
| 			model.PermissionViewBoard, | ||||
| 			model.PermissionManageBoardProperties, | ||||
| 		} | ||||
|  | ||||
| 		hasNotPermissionTo := []*mmModel.Permission{} | ||||
|  | ||||
| 		th.checkBoardPermissions("admin", member, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("board editor", func(t *testing.T) { | ||||
| 		member := &model.BoardMember{ | ||||
| 			UserID:       "user-id", | ||||
| 			BoardID:      "board-id", | ||||
| 			SchemeEditor: true, | ||||
| 		} | ||||
|  | ||||
| 		hasPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardCards, | ||||
| 			model.PermissionViewBoard, | ||||
| 			model.PermissionManageBoardProperties, | ||||
| 		} | ||||
|  | ||||
| 		hasNotPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardType, | ||||
| 			model.PermissionDeleteBoard, | ||||
| 			model.PermissionManageBoardRoles, | ||||
| 			model.PermissionShareBoard, | ||||
| 		} | ||||
|  | ||||
| 		th.checkBoardPermissions("editor", member, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("board commenter", func(t *testing.T) { | ||||
| 		member := &model.BoardMember{ | ||||
| 			UserID:          "user-id", | ||||
| 			BoardID:         "board-id", | ||||
| 			SchemeCommenter: true, | ||||
| 		} | ||||
|  | ||||
| 		hasPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionViewBoard, | ||||
| 		} | ||||
|  | ||||
| 		hasNotPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardType, | ||||
| 			model.PermissionDeleteBoard, | ||||
| 			model.PermissionManageBoardRoles, | ||||
| 			model.PermissionShareBoard, | ||||
| 			model.PermissionManageBoardCards, | ||||
| 			model.PermissionManageBoardProperties, | ||||
| 		} | ||||
|  | ||||
| 		th.checkBoardPermissions("commenter", member, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("board viewer", func(t *testing.T) { | ||||
| 		member := &model.BoardMember{ | ||||
| 			UserID:       "user-id", | ||||
| 			BoardID:      "board-id", | ||||
| 			SchemeViewer: true, | ||||
| 		} | ||||
|  | ||||
| 		hasPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionViewBoard, | ||||
| 		} | ||||
|  | ||||
| 		hasNotPermissionTo := []*mmModel.Permission{ | ||||
| 			model.PermissionManageBoardType, | ||||
| 			model.PermissionDeleteBoard, | ||||
| 			model.PermissionManageBoardRoles, | ||||
| 			model.PermissionShareBoard, | ||||
| 			model.PermissionManageBoardCards, | ||||
| 			model.PermissionManageBoardProperties, | ||||
| 		} | ||||
|  | ||||
| 		th.checkBoardPermissions("viewer", member, hasPermissionTo, hasNotPermissionTo) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										86
									
								
								server/services/permissions/mmpermissions/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								server/services/permissions/mmpermissions/helpers_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. | ||||
| // See LICENSE.txt for license information. | ||||
|  | ||||
| package mmpermissions | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/mattermost/focalboard/server/model" | ||||
| 	mmpermissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mmpermissions/mocks" | ||||
| 	permissionsMocks "github.com/mattermost/focalboard/server/services/permissions/mocks" | ||||
|  | ||||
| 	mmModel "github.com/mattermost/mattermost-server/v6/model" | ||||
|  | ||||
| 	"github.com/golang/mock/gomock" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| type TestHelper struct { | ||||
| 	t           *testing.T | ||||
| 	ctrl        *gomock.Controller | ||||
| 	store       *permissionsMocks.MockStore | ||||
| 	api         *mmpermissionsMocks.MockAPI | ||||
| 	permissions *Service | ||||
| } | ||||
|  | ||||
| func SetupTestHelper(t *testing.T) *TestHelper { | ||||
| 	ctrl := gomock.NewController(t) | ||||
| 	mockStore := permissionsMocks.NewMockStore(ctrl) | ||||
| 	mockAPI := mmpermissionsMocks.NewMockAPI(ctrl) | ||||
|  | ||||
| 	return &TestHelper{ | ||||
| 		t:           t, | ||||
| 		ctrl:        ctrl, | ||||
| 		store:       mockStore, | ||||
| 		api:         mockAPI, | ||||
| 		permissions: New(mockStore, mockAPI), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (th *TestHelper) checkBoardPermissions(roleName string, member *model.BoardMember, teamID string, | ||||
| 	hasPermissionTo, hasNotPermissionTo []*mmModel.Permission) { | ||||
| 	for _, p := range hasPermissionTo { | ||||
| 		th.t.Run(roleName+" "+p.Id, func(t *testing.T) { | ||||
| 			th.store.EXPECT(). | ||||
| 				GetBoard(member.BoardID). | ||||
| 				Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			th.api.EXPECT(). | ||||
| 				HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam). | ||||
| 				Return(true). | ||||
| 				Times(1) | ||||
|  | ||||
| 			th.store.EXPECT(). | ||||
| 				GetMemberForBoard(member.BoardID, member.UserID). | ||||
| 				Return(member, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) | ||||
| 			assert.True(t, hasPermission) | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	for _, p := range hasNotPermissionTo { | ||||
| 		th.t.Run(roleName+" "+p.Id, func(t *testing.T) { | ||||
| 			th.store.EXPECT(). | ||||
| 				GetBoard(member.BoardID). | ||||
| 				Return(&model.Board{ID: member.BoardID, TeamID: teamID}, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			th.api.EXPECT(). | ||||
| 				HasPermissionToTeam(member.UserID, teamID, model.PermissionViewTeam). | ||||
| 				Return(true). | ||||
| 				Times(1) | ||||
|  | ||||
| 			th.store.EXPECT(). | ||||
| 				GetMemberForBoard(member.BoardID, member.UserID). | ||||
| 				Return(member, nil). | ||||
| 				Times(1) | ||||
|  | ||||
| 			hasPermission := th.permissions.HasPermissionToBoard(member.UserID, member.BoardID, p) | ||||
| 			assert.False(t, hasPermission) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user