mirror of
https://github.com/mattermost/focalboard.git
synced 2025-03-26 20:53:55 +02:00
Merge branch 'main' into MM-46274_module-federation-poc
This commit is contained in:
commit
9c16d94854
4
Makefile
4
Makefile
@ -100,12 +100,12 @@ server-linux-package-docker:
|
||||
rm -rf package
|
||||
|
||||
generate: ## Install and run code generators.
|
||||
cd server; go get -modfile=go.tools.mod github.com/golang/mock/mockgen
|
||||
cd server; go get github.com/golang/mock/mockgen
|
||||
cd server; go generate ./...
|
||||
|
||||
server-lint: templates-archive ## Run linters on server code.
|
||||
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
|
||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
|
||||
exit 1; \
|
||||
fi;
|
||||
cd server; golangci-lint run ./...
|
||||
|
@ -1,6 +1,8 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
modules-download-mode: readonly
|
||||
skip-files:
|
||||
- product/boards_product.go
|
||||
|
||||
linters-settings:
|
||||
gofmt:
|
||||
|
@ -75,7 +75,7 @@ endif
|
||||
|
||||
ifneq ($(HAS_SERVER),)
|
||||
@if ! [ -x "$$(command -v golangci-lint)" ]; then \
|
||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install for installation instructions."; \
|
||||
echo "golangci-lint is not installed. Please see https://github.com/golangci/golangci-lint#install-golangci-lint for installation instructions."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
|
||||
|
@ -214,5 +214,23 @@ func (a *serviceAPIAdapter) RegisterRouter(sub *mux.Router) {
|
||||
a.api.routerService.RegisterRouter(boardsProductName, sub)
|
||||
}
|
||||
|
||||
//
|
||||
// Preferences service.
|
||||
//
|
||||
func (a *serviceAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||
p, appErr := a.api.preferencesService.GetPreferencesForUser(userID)
|
||||
return p, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.preferencesService.UpdatePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *serviceAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.preferencesService.DeletePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
// Ensure the adapter implements ServicesAPI.
|
||||
var _ model.ServicesAPI = &serviceAPIAdapter{}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mattermost/focalboard/mattermost-plugin/server/boards"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/app"
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
@ -43,6 +44,7 @@ func init() {
|
||||
app.KVStoreKey: {},
|
||||
app.StoreKey: {},
|
||||
app.SystemKey: {},
|
||||
app.PreferencesKey: {},
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -65,11 +67,11 @@ type boardsProduct struct {
|
||||
kvStoreService product.KVStoreService
|
||||
storeService product.StoreService
|
||||
systemService product.SystemService
|
||||
preferencesService product.PreferencesService
|
||||
|
||||
boardsApp *boards.BoardsApp
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (app.Product, error) {
|
||||
boards := &boardsProduct{}
|
||||
|
||||
@ -177,6 +179,12 @@ func newBoardsProduct(_ *app.Server, services map[app.ServiceKey]interface{}) (a
|
||||
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boards.systemService = systemService
|
||||
case app.PreferencesKey:
|
||||
preferencesService, ok := service.(product.PreferencesService)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid service key '%s': %w", key, errServiceTypeAssert)
|
||||
}
|
||||
boards.preferencesService = preferencesService
|
||||
case app.HooksKey: // not needed
|
||||
}
|
||||
}
|
||||
@ -197,6 +205,8 @@ func (bp *boardsProduct) Start() error {
|
||||
return fmt.Errorf("failed to create Boards service: %w", err)
|
||||
}
|
||||
|
||||
model.LogServerInfo(bp.logger)
|
||||
|
||||
bp.boardsApp = boardsApp
|
||||
if err := bp.boardsApp.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start Boards service: %w", err)
|
||||
|
@ -216,5 +216,37 @@ func (a *pluginAPIAdapter) RegisterRouter(sub *mux.Router) {
|
||||
// NOOP for plugin
|
||||
}
|
||||
|
||||
//
|
||||
// Preferences service.
|
||||
//
|
||||
func (a *pluginAPIAdapter) GetPreferencesForUser(userID string) (mm_model.Preferences, error) {
|
||||
preferences, appErr := a.api.GetPreferencesForUser(userID)
|
||||
if appErr != nil {
|
||||
return nil, normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
boardsPreferences := mm_model.Preferences{}
|
||||
|
||||
// Mattermost API gives us all preferences.
|
||||
// We want just the Focalboard ones.
|
||||
for _, preference := range preferences {
|
||||
if preference.Category == model.PreferencesCategoryFocalboard {
|
||||
boardsPreferences = append(boardsPreferences, preference)
|
||||
}
|
||||
}
|
||||
|
||||
return boardsPreferences, nil
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.UpdatePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
func (a *pluginAPIAdapter) DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error {
|
||||
appErr := a.api.DeletePreferencesForUser(userID, preferences)
|
||||
return normalizeAppErr(appErr)
|
||||
}
|
||||
|
||||
// Ensure the adapter implements ServicesAPI.
|
||||
var _ model.ServicesAPI = &pluginAPIAdapter{}
|
||||
|
@ -12,17 +12,9 @@ import (
|
||||
"github.com/mattermost/focalboard/server/services/permissions"
|
||||
"github.com/mattermost/focalboard/server/services/store"
|
||||
|
||||
mm_model "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
botUsername = "boards"
|
||||
botDisplayname = "Boards"
|
||||
botDescription = "Created by Boards plugin."
|
||||
)
|
||||
|
||||
type notifyBackendParams struct {
|
||||
cfg *config.Configuration
|
||||
servicesAPI model.ServicesAPI
|
||||
@ -71,15 +63,11 @@ func createSubscriptionsNotifyBackend(params notifyBackendParams) (*notifysubscr
|
||||
}
|
||||
|
||||
func createDelivery(servicesAPI model.ServicesAPI, serverRoot string) (*plugindelivery.PluginDelivery, error) {
|
||||
bot := &mm_model.Bot{
|
||||
Username: botUsername,
|
||||
DisplayName: botDisplayname,
|
||||
Description: botDescription,
|
||||
OwnerId: model.SystemUserID,
|
||||
}
|
||||
bot := model.FocalboardBot
|
||||
|
||||
botID, err := servicesAPI.EnsureBot(bot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure %s bot: %w", botDisplayname, err)
|
||||
return nil, fmt.Errorf("failed to ensure %s bot: %w", bot.DisplayName, err)
|
||||
}
|
||||
|
||||
return plugindelivery.New(botID, serverRoot, servicesAPI), nil
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/focalboard/mattermost-plugin/server/boards"
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
|
||||
pluginapi "github.com/mattermost/mattermost-plugin-api"
|
||||
|
||||
@ -52,6 +53,8 @@ func (p *Plugin) OnActivate() error {
|
||||
return fmt.Errorf("cannot activate plugin: %w", err)
|
||||
}
|
||||
|
||||
model.LogServerInfo(logger)
|
||||
|
||||
p.boardsApp = boardsApp
|
||||
return p.boardsApp.Start()
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ exports[`components/boardSelector renders with no results 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton size--medium"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
@ -126,7 +126,7 @@ exports[`components/boardSelector renders with some results 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton size--medium"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
@ -188,7 +188,7 @@ exports[`components/boardSelector renders with some results 1`] = `
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards undefined"
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
@ -227,7 +227,7 @@ exports[`components/boardSelector renders with some results 1`] = `
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards undefined"
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
@ -266,7 +266,7 @@ exports[`components/boardSelector renders with some results 1`] = `
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards undefined"
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
@ -327,7 +327,7 @@ exports[`components/boardSelector renders without start searching 1`] = `
|
||||
>
|
||||
<button
|
||||
aria-label="Close dialog"
|
||||
class="IconButton size--medium"
|
||||
class="IconButton dialog__close size--medium"
|
||||
title="Close dialog"
|
||||
type="button"
|
||||
>
|
||||
|
@ -12,7 +12,7 @@ exports[`components/boardSelectorItem renders board without title 1`] = `
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards undefined"
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
@ -56,7 +56,7 @@ exports[`components/boardSelectorItem renders linked board 1`] = `
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards undefined"
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
@ -100,7 +100,7 @@ exports[`components/boardSelectorItem renders not linked board 1`] = `
|
||||
class="icon"
|
||||
>
|
||||
<i
|
||||
class="CompassIcon icon-product-boards undefined"
|
||||
class="CompassIcon icon-product-boards"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
|
@ -69,7 +69,7 @@ exports[`components/rhsChannelBoardItem render board with menu open 1`] = `
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="Menu noselect left fixed"
|
||||
class="Menu noselect left "
|
||||
>
|
||||
<div
|
||||
class="menu-contents"
|
||||
@ -91,15 +91,20 @@ exports[`components/rhsChannelBoardItem render board with menu open 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-name"
|
||||
class="menu-option__content"
|
||||
>
|
||||
Unlink board
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Unlink board
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="menu-spacer hideOnWidescreen"
|
||||
@ -120,9 +125,13 @@ exports[`components/rhsChannelBoardItem render board with menu open 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="menu-name"
|
||||
class="menu-option__content"
|
||||
>
|
||||
Cancel
|
||||
<div
|
||||
class="menu-name"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="noicon"
|
||||
|
@ -23,15 +23,8 @@
|
||||
position: relative;
|
||||
width: 600px;
|
||||
height: 450px;
|
||||
|
||||
.toolbar {
|
||||
flex-direction: row-reverse;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmation-dialog-box {
|
||||
.dialog {
|
||||
position: fixed;
|
||||
|
@ -100,6 +100,10 @@ const BoardSelector = () => {
|
||||
const newBoard = createBoard({...board, channelId: currentChannel})
|
||||
await mutator.updateBoard(newBoard, board, 'linked channel')
|
||||
setShowLinkBoardConfirmation(null)
|
||||
dispatch(setLinkToChannel(''))
|
||||
setResults([])
|
||||
setIsSearching(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
|
||||
const unlinkBoard = async (board: Board): Promise<void> => {
|
||||
|
@ -8,7 +8,7 @@ const PostTypeCloudUpgradeNudge = (props: {post: Post}): JSX.Element => {
|
||||
const ctaHandler = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const windowAny = (window as any)
|
||||
windowAny?.openPricingModal()()
|
||||
windowAny?.openPricingModal()({trackingLocation: 'boards > click_view_upgrade_options_nudge'})
|
||||
}
|
||||
|
||||
// custom post type doesn't support styling via CSS stylesheet.
|
||||
|
@ -1,13 +1,15 @@
|
||||
.RHSChannelBoardItem {
|
||||
padding: 15px;
|
||||
text-align: left;
|
||||
border: 1px solid #cccccc;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
box-shadow: var(--elevation-1);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
color: rgb(var(--center-channel-color-rgb));
|
||||
|
||||
.date {
|
||||
color: #cccccc;
|
||||
font-size: 12px;
|
||||
opacity: 0.64;
|
||||
}
|
||||
|
||||
.board-info {
|
||||
@ -19,6 +21,7 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
white-space: nowrap;
|
||||
@ -28,6 +31,9 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 4px 0;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -14,6 +14,7 @@ import RHSChannelBoardItem from './rhsChannelBoardItem'
|
||||
|
||||
describe('components/rhsChannelBoardItem', () => {
|
||||
it('render board', async () => {
|
||||
const board = createBoard()
|
||||
const state = {
|
||||
teams: {
|
||||
current: {
|
||||
@ -22,8 +23,12 @@ describe('components/rhsChannelBoardItem', () => {
|
||||
display_name: 'Team name',
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
const board = createBoard()
|
||||
board.updateAt = 1657311058157
|
||||
board.title = 'Test board'
|
||||
|
||||
@ -37,6 +42,7 @@ describe('components/rhsChannelBoardItem', () => {
|
||||
})
|
||||
|
||||
it('render board with menu open', async () => {
|
||||
const board = createBoard()
|
||||
const state = {
|
||||
teams: {
|
||||
current: {
|
||||
@ -45,8 +51,12 @@ describe('components/rhsChannelBoardItem', () => {
|
||||
display_name: 'Team name',
|
||||
},
|
||||
},
|
||||
boards: {
|
||||
myBoardMemberships: {
|
||||
[board.id]: {userId: 'user_id_1', schemeAdmin: true},
|
||||
},
|
||||
}
|
||||
}
|
||||
const board = createBoard()
|
||||
board.updateAt = 1657311058157
|
||||
board.title = 'Test board'
|
||||
|
||||
|
@ -15,7 +15,10 @@ import Menu from '../../../../webapp/src/widgets/menu'
|
||||
import MenuWrapper from '../../../../webapp/src/widgets/menuWrapper'
|
||||
import {SuiteWindow} from '../../../../webapp/src/types/index'
|
||||
|
||||
import {Permission} from '../../../../webapp/src/constants'
|
||||
|
||||
import './rhsChannelBoardItem.scss'
|
||||
import BoardPermissionGate from '../../../../webapp/src/components/permissions/boardPermissionGate'
|
||||
|
||||
const windowAny = (window as SuiteWindow)
|
||||
|
||||
@ -55,18 +58,41 @@ const RHSChannelBoardItem = (props: Props) => {
|
||||
<MenuWrapper stopPropagationOnToggle={true}>
|
||||
<IconButton icon={<OptionsIcon/>}/>
|
||||
<Menu
|
||||
fixed={true}
|
||||
position='left'
|
||||
>
|
||||
<Menu.Text
|
||||
key={`unlinkBoard-${board.id}`}
|
||||
id='unlinkBoard'
|
||||
name={intl.formatMessage({id: 'rhs-boards.unlink-board', defaultMessage: 'Unlink board'})}
|
||||
icon={<DeleteIcon/>}
|
||||
onClick={() => {
|
||||
onUnlinkBoard(board)
|
||||
}}
|
||||
/>
|
||||
<BoardPermissionGate
|
||||
boardId={board.id}
|
||||
teamId={team.id}
|
||||
permissions={[Permission.ManageBoardRoles]}
|
||||
>
|
||||
<Menu.Text
|
||||
key={`unlinkBoard-${board.id}`}
|
||||
id='unlinkBoard'
|
||||
name={intl.formatMessage({id: 'rhs-boards.unlink-board', defaultMessage: 'Unlink board'})}
|
||||
icon={<DeleteIcon/>}
|
||||
onClick={() => {
|
||||
onUnlinkBoard(board)
|
||||
}}
|
||||
/>
|
||||
</BoardPermissionGate>
|
||||
<BoardPermissionGate
|
||||
boardId={board.id}
|
||||
teamId={team.id}
|
||||
permissions={[Permission.ManageBoardRoles]}
|
||||
invert={true}
|
||||
>
|
||||
<Menu.Text
|
||||
key={`unlinkBoard-${board.id}`}
|
||||
id='unlinkBoard'
|
||||
disabled={true}
|
||||
name={intl.formatMessage({id: 'rhs-boards.unlink-board1', defaultMessage: 'Unlink board Hello'})}
|
||||
icon={<DeleteIcon/>}
|
||||
onClick={() => {
|
||||
onUnlinkBoard(board)
|
||||
}}
|
||||
subText={intl.formatMessage({id: 'rhs-board-non-admin-msg', defaultMessage:'You are not an admin of the board'})}
|
||||
/>
|
||||
</BoardPermissionGate>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
.RHSChannelBoards {
|
||||
padding: 20px;
|
||||
padding: 16px 24px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 16px;
|
||||
|
||||
&.empty {
|
||||
display: flex;
|
||||
@ -45,7 +45,8 @@
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 16px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.Button {
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useEffect} from 'react'
|
||||
import {createIntl, createIntlCache} from 'react-intl'
|
||||
import {Store, Action} from 'redux'
|
||||
import {Provider as ReduxProvider} from 'react-redux'
|
||||
import {createBrowserHistory, History} from 'history'
|
||||
@ -13,6 +14,7 @@ import {selectTeam} from 'mattermost-redux/actions/teams'
|
||||
|
||||
import {SuiteWindow} from '../../../webapp/src/types/index'
|
||||
import {UserSettings} from '../../../webapp/src/userSettings'
|
||||
import {getMessages, getCurrentLanguage} from '../../../webapp/src/i18n'
|
||||
|
||||
const windowAny = (window as SuiteWindow)
|
||||
windowAny.baseURL = process.env.TARGET_IS_PRODUCT ? '/plugins/boards' : '/plugins/focalboard'
|
||||
@ -182,6 +184,13 @@ export default class Plugin {
|
||||
windowAny.frontendBaseURL = subpath + windowAny.frontendBaseURL
|
||||
windowAny.baseURL = subpath + windowAny.baseURL
|
||||
browserHistory = customHistory()
|
||||
const cache = createIntlCache()
|
||||
const intl = createIntl({
|
||||
// modeled after <IntlProvider> in webapp/src/app.tsx
|
||||
locale: getCurrentLanguage(),
|
||||
messages: getMessages(getCurrentLanguage())
|
||||
}, cache)
|
||||
|
||||
|
||||
this.registry = registry
|
||||
|
||||
@ -310,7 +319,7 @@ export default class Plugin {
|
||||
|
||||
if (this.registry.registerAppBarComponent) {
|
||||
const appBarIconURL = windowAny.baseURL + '/public/app-bar-icon.png'
|
||||
this.registry.registerAppBarComponent(appBarIconURL, () => mmStore.dispatch(toggleRHSPlugin), 'Boards')
|
||||
this.registry.registerAppBarComponent(appBarIconURL, () => mmStore.dispatch(toggleRHSPlugin), intl.formatMessage({id: 'AppBar.Tooltip', defaultMessage: 'Toggle Linked Boards'}))
|
||||
}
|
||||
|
||||
this.registry.registerPostWillRenderEmbedComponent(
|
||||
|
@ -165,6 +165,13 @@ func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool {
|
||||
return isValid
|
||||
}
|
||||
|
||||
func (a *API) userIsGuest(userID string) (bool, error) {
|
||||
if a.singleUserToken != "" {
|
||||
return false, nil
|
||||
}
|
||||
return a.app.UserIsGuest(userID)
|
||||
}
|
||||
|
||||
// Response helpers
|
||||
|
||||
func (a *API) errorResponse(w http.ResponseWriter, api string, code int, message string, sourceError error) {
|
||||
|
@ -134,6 +134,16 @@ func (a *API) handleArchiveImport(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
file, handle, err := r.FormFile(UploadFormFileKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(w, "%v", err)
|
||||
@ -206,7 +216,13 @@ func (a *API) handleArchiveExportTeam(w http.ResponseWriter, r *http.Request) {
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("TeamID", teamID)
|
||||
|
||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
|
@ -381,17 +381,6 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request)
|
||||
UpdateAt: now,
|
||||
}
|
||||
|
||||
user, err := a.app.GetUser(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
if user.IsGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusUnauthorized, "guests not supported", nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), sessionContextKey, session)
|
||||
handler(w, r.WithContext(ctx))
|
||||
return
|
||||
|
@ -104,6 +104,19 @@ func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if board.IsTemplate {
|
||||
var isGuest bool
|
||||
isGuest, err = a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guest are not allowed to get board templates"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getBlocks", audit.Fail)
|
||||
@ -190,7 +203,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
// type: string
|
||||
// - name: disable_notify
|
||||
// in: query
|
||||
// description: Disables notifications (for bulk data inserting)
|
||||
// description: Disables notifications (for bulk inserting)
|
||||
// required: false
|
||||
// type: bool
|
||||
// - name: Body
|
||||
@ -221,13 +234,6 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
// in phase 1 we use "manage_board_cards", but we would have to
|
||||
// check on specific actions for phase 2
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
}
|
||||
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -242,6 +248,8 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
hasComments := false
|
||||
hasContents := false
|
||||
for _, block := range blocks {
|
||||
// Error checking
|
||||
if len(block.Type) < 1 {
|
||||
@ -250,6 +258,12 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if block.Type == model.TypeComment {
|
||||
hasComments = true
|
||||
} else {
|
||||
hasContents = true
|
||||
}
|
||||
|
||||
if block.CreateAt < 1 {
|
||||
message := fmt.Sprintf("invalid createAt for block id %s", block.ID)
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, message, nil)
|
||||
@ -269,6 +283,19 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if hasContents {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if hasComments {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to post card comments"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
blocks = model.GenerateBlockIDs(blocks, a.logger)
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "postBlocks", audit.Fail)
|
||||
@ -289,7 +316,7 @@ func (a *API) handlePostBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
newBlocks, err := a.app.InsertBlocks(blocks, session.UserID, !disableNotify)
|
||||
newBlocks, err := a.app.InsertBlocksAndNotify(blocks, session.UserID, disableNotify)
|
||||
if err != nil {
|
||||
if errors.Is(err, app.ErrViewsLimitReached) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
|
||||
@ -336,6 +363,11 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// description: ID of block to delete
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: disable_notify
|
||||
// in: query
|
||||
// description: Disables notifications (for bulk deletion)
|
||||
// required: false
|
||||
// type: bool
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
@ -353,6 +385,9 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
boardID := vars["boardID"]
|
||||
blockID := vars["blockID"]
|
||||
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
@ -373,7 +408,7 @@ func (a *API) handleDeleteBlock(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
auditRec.AddMeta("blockID", blockID)
|
||||
|
||||
err = a.app.DeleteBlock(blockID, userID)
|
||||
err = a.app.DeleteBlockAndNotify(blockID, userID, disableNotify)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -497,6 +532,11 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
||||
// description: ID of block to patch
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: disable_notify
|
||||
// in: query
|
||||
// description: Disables notifications (for bulk patching)
|
||||
// required: false
|
||||
// type: bool
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: block patch to apply
|
||||
@ -520,6 +560,9 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
||||
boardID := vars["boardID"]
|
||||
blockID := vars["blockID"]
|
||||
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to make board changes"})
|
||||
return
|
||||
@ -553,7 +596,7 @@ func (a *API) handlePatchBlock(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
auditRec.AddMeta("blockID", blockID)
|
||||
|
||||
err = a.app.PatchBlock(blockID, patch, userID)
|
||||
err = a.app.PatchBlockAndNotify(blockID, patch, userID, disableNotify)
|
||||
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
return
|
||||
@ -583,6 +626,11 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
// description: Workspace ID
|
||||
// required: true
|
||||
// type: string
|
||||
// - name: disable_notify
|
||||
// in: query
|
||||
// description: Disables notifications (for bulk patching)
|
||||
// required: false
|
||||
// type: bool
|
||||
// - name: Body
|
||||
// in: body
|
||||
// description: block Ids and block patches to apply
|
||||
@ -606,6 +654,9 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
teamID := vars["teamID"]
|
||||
|
||||
val := r.URL.Query().Get("disable_notify")
|
||||
disableNotify := val == True
|
||||
|
||||
requestBody, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -638,7 +689,7 @@ func (a *API) handlePatchBlocks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
err = a.app.PatchBlocks(teamID, patches, userID)
|
||||
err = a.app.PatchBlocksAndNotify(teamID, patches, userID, disableNotify)
|
||||
if errors.Is(err, app.ErrPatchUpdatesLimitedCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", err)
|
||||
return
|
||||
@ -724,9 +775,16 @@ func (a *API) handleDuplicateBlock(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
if block.Type == model.TypeComment {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionCommentBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to comment on board cards"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardCards) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board cards"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "duplicateBlock", audit.Fail)
|
||||
|
@ -65,8 +65,14 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("teamID", teamID)
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve boards list
|
||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID)
|
||||
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -143,6 +149,16 @@ func (a *API) handleCreateBoard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
if err = newBoard.IsValid(); err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusBadRequest, err.Error(), err)
|
||||
return
|
||||
@ -233,6 +249,19 @@ func (a *API) handleGetBoard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var isGuest bool
|
||||
isGuest, err = a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToTeam(userID, board.TeamID, model.PermissionViewTeam) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to board"})
|
||||
return
|
||||
@ -502,6 +531,16 @@ func (a *API) handleDuplicateBoard(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "duplicateBoard", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("boardID", boardID)
|
||||
|
@ -98,6 +98,16 @@ func (a *API) handleCreateBoardsAndBlocks(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, block := range newBab.Blocks {
|
||||
// Error checking
|
||||
if len(block.Type) < 1 {
|
||||
|
@ -246,6 +246,16 @@ func (a *API) handleJoinBoard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"guests not allowed to join boards"})
|
||||
return
|
||||
}
|
||||
|
||||
newBoardMember := &model.BoardMember{
|
||||
UserID: userID,
|
||||
BoardID: boardID,
|
||||
@ -421,6 +431,16 @@ func (a *API) handleUpdateMember(w http.ResponseWriter, r *http.Request) {
|
||||
SchemeViewer: reqBoardMember.SchemeViewer,
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(paramsUserID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
if isGuest {
|
||||
newBoardMember.SchemeAdmin = false
|
||||
}
|
||||
|
||||
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionManageBoardRoles) {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to modify board members"})
|
||||
return
|
||||
|
@ -53,6 +53,16 @@ func (a *API) handleOnboard(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to create board"})
|
||||
return
|
||||
}
|
||||
|
||||
teamID, boardID, err := a.app.PrepareOnboardingTour(userID, teamID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
|
@ -146,8 +146,14 @@ func (a *API) handleSearchBoards(w http.ResponseWriter, r *http.Request) {
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("teamID", teamID)
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve boards list
|
||||
boards, err := a.app.SearchBoardsForUserInTeam(teamID, term, userID)
|
||||
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
@ -299,8 +305,14 @@ func (a *API) handleSearchAllBoards(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := a.makeAuditRecord(r, "searchAllBoards", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve boards list
|
||||
boards, err := a.app.SearchBoardsForUser(term, userID)
|
||||
boards, err := a.app.SearchBoardsForUser(term, userID, !isGuest)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
|
@ -226,7 +226,17 @@ func (a *API) handleGetTeamUsers(w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := a.makeAuditRecord(r, "getUsers", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
|
||||
users, err := a.app.SearchTeamUsers(teamID, searchQuery)
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
asGuestUser := ""
|
||||
if isGuest {
|
||||
asGuestUser = userID
|
||||
}
|
||||
|
||||
users, err := a.app.SearchTeamUsers(teamID, searchQuery, asGuestUser)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "searchQuery="+searchQuery, err)
|
||||
return
|
||||
|
@ -51,6 +51,16 @@ func (a *API) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
isGuest, err := a.userIsGuest(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if isGuest {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusForbidden, "", PermissionError{"access denied to templates"})
|
||||
return
|
||||
}
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getTemplates", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
auditRec.AddMeta("teamID", teamID)
|
||||
|
@ -18,6 +18,7 @@ func (a *API) registerUsersRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/users/me/memberships", a.sessionRequired(a.handleGetMyMemberships)).Methods("GET")
|
||||
r.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET")
|
||||
r.HandleFunc("/users/{userID}/config", a.sessionRequired(a.handleUpdateUserConfig)).Methods(http.MethodPut)
|
||||
r.HandleFunc("/users/me/config", a.sessionRequired(a.handleGetUserPreferences)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func (a *API) handleGetUsersList(w http.ResponseWriter, r *http.Request) {
|
||||
@ -219,6 +220,19 @@ func (a *API) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
session := ctx.Value(sessionContextKey).(*model.Session)
|
||||
|
||||
canSeeUser, err := a.app.CanSeeUser(session.UserID, userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
if !canSeeUser {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusNotFound, "", nil)
|
||||
return
|
||||
}
|
||||
|
||||
userData, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
@ -302,3 +316,44 @@ func (a *API) handleUpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
auditRec.Success()
|
||||
}
|
||||
|
||||
func (a *API) handleGetUserPreferences(w http.ResponseWriter, r *http.Request) {
|
||||
// swagger:operation GET /users/me/config getUserConfig
|
||||
//
|
||||
// Returns an array of user preferences
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// security:
|
||||
// - BearerAuth: []
|
||||
// responses:
|
||||
// '200':
|
||||
// description: success
|
||||
// schema:
|
||||
// "$ref": "#/definitions/Preferences"
|
||||
// default:
|
||||
// description: internal error
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ErrorResponse"
|
||||
|
||||
userID := getUserID(r)
|
||||
|
||||
auditRec := a.makeAuditRecord(r, "getUserConfig", audit.Fail)
|
||||
defer a.audit.LogRecord(audit.LevelRead, auditRec)
|
||||
|
||||
preferences, err := a.app.GetUserPreferences(userID)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(preferences)
|
||||
if err != nil {
|
||||
a.errorResponse(w, r.URL.Path, http.StatusInternalServerError, "", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytesResponse(w, http.StatusOK, data)
|
||||
auditRec.Success()
|
||||
}
|
||||
|
@ -65,11 +65,11 @@ func (a *App) DuplicateBlock(boardID string, blockID string, userID string, asTe
|
||||
return blocks, err
|
||||
}
|
||||
|
||||
func (a *App) GetBlocksWithBoardID(boardID string) ([]model.Block, error) {
|
||||
return a.store.GetBlocksWithBoardID(boardID)
|
||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||
return a.PatchBlockAndNotify(blockID, blockPatch, modifiedByID, false)
|
||||
}
|
||||
|
||||
func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedByID string) error {
|
||||
func (a *App) PatchBlockAndNotify(blockID string, blockPatch *model.BlockPatch, modifiedByID string, disableNotify bool) error {
|
||||
oldBlock, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -108,13 +108,19 @@ func (a *App) PatchBlock(blockID string, blockPatch *model.BlockPatch, modifiedB
|
||||
a.webhook.NotifyUpdate(*block)
|
||||
|
||||
// send notifications
|
||||
a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID)
|
||||
if !disableNotify {
|
||||
a.notifyBlockChanged(notify.Update, block, oldBlock, modifiedByID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string) error {
|
||||
return a.PatchBlocksAndNotify(teamID, blockPatches, modifiedByID, false)
|
||||
}
|
||||
|
||||
func (a *App) PatchBlocksAndNotify(teamID string, blockPatches *model.BlockPatchBatch, modifiedByID string, disableNotify bool) error {
|
||||
oldBlocks, err := a.store.GetBlocksByIDs(blockPatches.BlockIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -143,7 +149,9 @@ func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, mo
|
||||
}
|
||||
a.wsAdapter.BroadcastBlockChange(teamID, *newBlock)
|
||||
a.webhook.NotifyUpdate(*newBlock)
|
||||
a.notifyBlockChanged(notify.Update, newBlock, &oldBlocks[i], modifiedByID)
|
||||
if !disableNotify {
|
||||
a.notifyBlockChanged(notify.Update, newBlock, &oldBlocks[i], modifiedByID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@ -151,6 +159,10 @@ func (a *App) PatchBlocks(teamID string, blockPatches *model.BlockPatchBatch, mo
|
||||
}
|
||||
|
||||
func (a *App) InsertBlock(block model.Block, modifiedByID string) error {
|
||||
return a.InsertBlockAndNotify(block, modifiedByID, false)
|
||||
}
|
||||
|
||||
func (a *App) InsertBlockAndNotify(block model.Block, modifiedByID string, disableNotify bool) error {
|
||||
board, bErr := a.store.GetBoard(block.BoardID)
|
||||
if bErr != nil {
|
||||
return bErr
|
||||
@ -162,8 +174,9 @@ func (a *App) InsertBlock(block model.Block, modifiedByID string) error {
|
||||
a.wsAdapter.BroadcastBlockChange(board.TeamID, block)
|
||||
a.metrics.IncrementBlocksInserted(1)
|
||||
a.webhook.NotifyUpdate(block)
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
|
||||
if !disableNotify {
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@ -202,7 +215,11 @@ func (a *App) isWithinViewsLimit(boardID string, block model.Block) (bool, error
|
||||
return len(views) < limits.Views, nil
|
||||
}
|
||||
|
||||
func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotifications bool) ([]model.Block, error) {
|
||||
func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string) ([]model.Block, error) {
|
||||
return a.InsertBlocksAndNotify(blocks, modifiedByID, false)
|
||||
}
|
||||
|
||||
func (a *App) InsertBlocksAndNotify(blocks []model.Block, modifiedByID string, disableNotify bool) ([]model.Block, error) {
|
||||
if len(blocks) == 0 {
|
||||
return []model.Block{}, nil
|
||||
}
|
||||
@ -250,11 +267,10 @@ func (a *App) InsertBlocks(blocks []model.Block, modifiedByID string, allowNotif
|
||||
for _, b := range needsNotify {
|
||||
block := b
|
||||
a.webhook.NotifyUpdate(block)
|
||||
if allowNotifications {
|
||||
if !disableNotify {
|
||||
a.notifyBlockChanged(notify.Add, &block, nil, modifiedByID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -335,6 +351,10 @@ func (a *App) GetBlockByID(blockID string) (*model.Block, error) {
|
||||
}
|
||||
|
||||
func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
||||
return a.DeleteBlockAndNotify(blockID, modifiedBy, false)
|
||||
}
|
||||
|
||||
func (a *App) DeleteBlockAndNotify(blockID string, modifiedBy string, disableNotify bool) error {
|
||||
block, err := a.store.GetBlock(blockID)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -372,8 +392,9 @@ func (a *App) DeleteBlock(blockID string, modifiedBy string) error {
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
a.wsAdapter.BroadcastBlockDelete(board.TeamID, blockID, block.BoardID)
|
||||
a.metrics.IncrementBlocksDeleted(1)
|
||||
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
|
||||
|
||||
if !disableNotify {
|
||||
a.notifyBlockChanged(notify.Delete, block, block, modifiedBy)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
@ -287,7 +287,7 @@ func TestInsertBlocks(t *testing.T) {
|
||||
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.InsertBlocks([]model.Block{block}, "user-id-1", false)
|
||||
_, err := th.App.InsertBlocks([]model.Block{block}, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@ -297,7 +297,7 @@ func TestInsertBlocks(t *testing.T) {
|
||||
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.InsertBlocks([]model.Block{block}, "user-id-1", false)
|
||||
_, err := th.App.InsertBlocks([]model.Block{block}, "user-id-1")
|
||||
require.Error(t, err, "error")
|
||||
})
|
||||
|
||||
@ -329,7 +329,7 @@ func TestInsertBlocks(t *testing.T) {
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
|
||||
th.Store.EXPECT().GetBlocksWithParentAndType("test-board-id", "parent_id", "view").Return([]model.Block{{}}, nil)
|
||||
|
||||
_, err := th.App.InsertBlocks([]model.Block{block}, "user-id-1", false)
|
||||
_, err := th.App.InsertBlocks([]model.Block{block}, "user-id-1")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
@ -359,7 +359,7 @@ func TestInsertBlocks(t *testing.T) {
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil)
|
||||
th.Store.EXPECT().GetBlocksWithParentAndType("test-board-id", "parent_id", "view").Return([]model.Block{{}, {}}, nil)
|
||||
|
||||
_, err := th.App.InsertBlocks([]model.Block{block}, "user-id-1", false)
|
||||
_, err := th.App.InsertBlocks([]model.Block{block}, "user-id-1")
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
@ -400,7 +400,7 @@ func TestInsertBlocks(t *testing.T) {
|
||||
th.Store.EXPECT().GetCardLimitTimestamp().Return(int64(1), nil).Times(2)
|
||||
th.Store.EXPECT().GetBlocksWithParentAndType("test-board-id", "parent_id", "view").Return([]model.Block{{}}, nil).Times(2)
|
||||
|
||||
_, err := th.App.InsertBlocks([]model.Block{view1, view2}, "user-id-1", false)
|
||||
_, err := th.App.InsertBlocks([]model.Block{view1, view2}, "user-id-1")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ var (
|
||||
ErrInsufficientLicense = errors.New("appropriate license required")
|
||||
)
|
||||
|
||||
const linkBoardMessage = "@%s linked Board [%s](%s) with this channel"
|
||||
const unlinkBoardMessage = "@%s unlinked Board [%s](%s) with this channel"
|
||||
|
||||
func (a *App) GetBoard(boardID string) (*model.Board, error) {
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if model.IsErrNotFound(err) {
|
||||
@ -253,8 +256,8 @@ func (a *App) DuplicateBoard(boardID, userID, toTeam string, asTemplate bool) (*
|
||||
return bab, members, err
|
||||
}
|
||||
|
||||
func (a *App) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
|
||||
return a.store.GetBoardsForUserAndTeam(userID, teamID)
|
||||
func (a *App) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
return a.store.GetBoardsForUserAndTeam(userID, teamID, includePublicBoards)
|
||||
}
|
||||
|
||||
func (a *App) GetTemplateBoards(teamID, userID string) ([]*model.Board, error) {
|
||||
@ -302,18 +305,54 @@ func (a *App) CreateBoard(board *model.Board, userID string, addMember bool) (*m
|
||||
|
||||
func (a *App) PatchBoard(patch *model.BoardPatch, boardID, userID string) (*model.Board, error) {
|
||||
var oldMembers []*model.BoardMember
|
||||
var oldChannelID string
|
||||
if patch.ChannelID != nil && *patch.ChannelID == "" {
|
||||
var err error
|
||||
oldMembers, err = a.GetMembersForBoard(boardID)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to get the board members", mlog.Err(err))
|
||||
}
|
||||
board, err := a.store.GetBoard(boardID)
|
||||
if model.IsErrNotFound(err) {
|
||||
return nil, model.NewErrNotFound(boardID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldChannelID = board.ChannelID
|
||||
}
|
||||
updatedBoard, err := a.store.PatchBoard(boardID, patch, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if patch.ChannelID != nil {
|
||||
var username string
|
||||
user, err := a.store.GetUserByID(userID)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to get the board updater", mlog.Err(err))
|
||||
username = "unknown"
|
||||
} else {
|
||||
username = user.Username
|
||||
}
|
||||
|
||||
boardLink := utils.MakeBoardLink(a.config.ServerRoot, updatedBoard.TeamID, updatedBoard.ID)
|
||||
if *patch.ChannelID != "" {
|
||||
// TODO: this needs translated when available on the server
|
||||
message := fmt.Sprintf(linkBoardMessage, username, updatedBoard.Title, boardLink)
|
||||
err := a.store.PostMessage(message, "", *patch.ChannelID)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to post the link message to channel", mlog.Err(err))
|
||||
}
|
||||
} else if *patch.ChannelID == "" {
|
||||
message := fmt.Sprintf(unlinkBoardMessage, username, updatedBoard.Title, boardLink)
|
||||
err := a.store.PostMessage(message, "", oldChannelID)
|
||||
if err != nil {
|
||||
a.logger.Error("Unable to post the link message to channel", mlog.Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.blockChangeNotifier.Enqueue(func() error {
|
||||
a.wsAdapter.BroadcastBoardChange(updatedBoard.TeamID, updatedBoard)
|
||||
if patch.ChannelID != nil && *patch.ChannelID != "" {
|
||||
@ -513,8 +552,8 @@ func (a *App) DeleteBoardMember(boardID, userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) SearchBoardsForUser(term, userID string) ([]*model.Board, error) {
|
||||
return a.store.SearchBoardsForUser(term, userID)
|
||||
func (a *App) SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
return a.store.SearchBoardsForUser(term, userID, includePublicBoards)
|
||||
}
|
||||
|
||||
func (a *App) SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error) {
|
||||
|
@ -82,7 +82,7 @@ func (a *App) writeArchiveBoard(zw *zip.Writer, board model.Board, opt model.Exp
|
||||
var files []string
|
||||
// write the board's blocks
|
||||
// TODO: paginate this
|
||||
blocks, err := a.GetBlocksWithBoardID(board.ID)
|
||||
blocks, err := a.GetBlocksForBoard(board.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ func (a *App) GetUserTimezone(userID string) (string, error) {
|
||||
|
||||
func getUserBoards(userID string, teamID string, a *App) ([]string, error) {
|
||||
// get boards accessible by user and filter boardIDs
|
||||
boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID)
|
||||
boards, err := a.store.GetBoardsForUserAndTeam(userID, teamID, true)
|
||||
if err != nil {
|
||||
return nil, errors.New("error getting boards for user")
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
|
||||
IsGuest: false,
|
||||
}
|
||||
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id").Return(mockInsightsBoards, nil).AnyTimes()
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
|
||||
th.Store.EXPECT().
|
||||
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
|
||||
Return(mockTeamInsightsList, nil)
|
||||
@ -72,7 +72,7 @@ func TestGetTeamAndUserBoardsInsights(t *testing.T) {
|
||||
IsGuest: false,
|
||||
}
|
||||
th.Store.EXPECT().GetUserByID("user-id").Return(fakeUser, nil).AnyTimes()
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id").Return(mockInsightsBoards, nil).AnyTimes()
|
||||
th.Store.EXPECT().GetBoardsForUserAndTeam("user-id", "team-id", true).Return(mockInsightsBoards, nil).AnyTimes()
|
||||
th.Store.EXPECT().
|
||||
GetTeamBoardsInsights("team-id", "user-id", int64(0), 0, 10, []string{"mock-user-workspace-id"}).
|
||||
Return(nil, insightError{"board-insight-error"})
|
||||
|
@ -7,10 +7,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
KeyPrefix = "focalboard_" // use key prefix to namespace focalboard props
|
||||
KeyOnboardingTourStarted = KeyPrefix + "onboardingTourStarted"
|
||||
KeyOnboardingTourCategory = KeyPrefix + "tourCategory"
|
||||
KeyOnboardingTourStep = KeyPrefix + "onboardingTourStep"
|
||||
KeyOnboardingTourStarted = "onboardingTourStarted"
|
||||
KeyOnboardingTourCategory = "tourCategory"
|
||||
KeyOnboardingTourStep = "onboardingTourStep"
|
||||
|
||||
ValueOnboardingFirstStep = "0"
|
||||
ValueTourCategoryOnboarding = "onboarding"
|
||||
|
@ -5,25 +5,52 @@ import (
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func (a *App) GetTeamUsers(teamID string) ([]*model.User, error) {
|
||||
return a.store.GetUsersByTeam(teamID)
|
||||
func (a *App) GetTeamUsers(teamID string, asGuestID string) ([]*model.User, error) {
|
||||
return a.store.GetUsersByTeam(teamID, asGuestID)
|
||||
}
|
||||
|
||||
func (a *App) SearchTeamUsers(teamID string, searchQuery string) ([]*model.User, error) {
|
||||
return a.store.SearchUsersByTeam(teamID, searchQuery)
|
||||
func (a *App) SearchTeamUsers(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||
return a.store.SearchUsersByTeam(teamID, searchQuery, asGuestID)
|
||||
}
|
||||
|
||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) (map[string]interface{}, error) {
|
||||
func (a *App) UpdateUserConfig(userID string, patch model.UserPropPatch) ([]mmModel.Preference, error) {
|
||||
if err := a.store.PatchUserProps(userID, patch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := a.store.GetUserByID(userID)
|
||||
updatedPreferences, err := a.store.GetUserPreferences(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user.Props, nil
|
||||
return updatedPreferences, nil
|
||||
}
|
||||
|
||||
func (a *App) GetUserPreferences(userID string) ([]mmModel.Preference, error) {
|
||||
return a.store.GetUserPreferences(userID)
|
||||
}
|
||||
|
||||
func (a *App) UserIsGuest(userID string) (bool, error) {
|
||||
user, err := a.store.GetUserByID(userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return user.IsGuest, nil
|
||||
}
|
||||
|
||||
func (a *App) CanSeeUser(seerUser string, seenUser string) (bool, error) {
|
||||
isGuest, err := a.UserIsGuest(seerUser)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if isGuest {
|
||||
hasSharedChannels, err := a.store.CanSeeUser(seerUser, seenUser)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return hasSharedChannels, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (a *App) SearchUserChannels(teamID string, userID string, query string) ([]*mmModel.Channel, error) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
|
||||
//go:generate mockgen -destination=mocks/mockauth_interface.go -package mocks . AuthInterface
|
||||
package auth
|
||||
|
||||
import (
|
||||
|
@ -256,8 +256,14 @@ func (c *Client) GetAllBlocksForBoard(boardID string) ([]model.Block, *Response)
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch) (bool, *Response) {
|
||||
r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID), toJSON(blockPatch))
|
||||
const disableNotifyQueryParam = "disable_notify=true"
|
||||
|
||||
func (c *Client) PatchBlock(boardID, blockID string, blockPatch *model.BlockPatch, disableNotify bool) (bool, *Response) {
|
||||
var queryParams string
|
||||
if disableNotify {
|
||||
queryParams = "?" + disableNotifyQueryParam
|
||||
}
|
||||
r, err := c.DoAPIPatch(c.GetBlockRoute(boardID, blockID)+queryParams, toJSON(blockPatch))
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -307,8 +313,12 @@ func (c *Client) UndeleteBlock(boardID, blockID string) (bool, *Response) {
|
||||
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))
|
||||
func (c *Client) InsertBlocks(boardID string, blocks []model.Block, disableNotify bool) ([]model.Block, *Response) {
|
||||
var queryParams string
|
||||
if disableNotify {
|
||||
queryParams = "?" + disableNotifyQueryParam
|
||||
}
|
||||
r, err := c.DoAPIPost(c.GetBlocksRoute(boardID)+queryParams, toJSON(blocks))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
}
|
||||
@ -317,18 +327,12 @@ func (c *Client) InsertBlocks(boardID string, blocks []model.Block) ([]model.Blo
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) InsertBlocksDisableNotify(boardID string, blocks []model.Block) ([]model.Block, *Response) {
|
||||
r, err := c.DoAPIPost(c.GetBlocksRoute(boardID)+"?disable_notify=true", toJSON(blocks))
|
||||
if err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
func (c *Client) DeleteBlock(boardID, blockID string, disableNotify bool) (bool, *Response) {
|
||||
var queryParams string
|
||||
if disableNotify {
|
||||
queryParams = "?" + disableNotifyQueryParam
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
return model.BlocksFromJSON(r.Body), BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteBlock(boardID, blockID string) (bool, *Response) {
|
||||
r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID), "")
|
||||
r, err := c.DoAPIDelete(c.GetBlockRoute(boardID, blockID)+queryParams, "")
|
||||
if err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.2
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
@ -14,13 +13,14 @@ require (
|
||||
github.com/mattermost/mattermost-server/v6 v6.0.0-20220802151854-f07c31c5d933
|
||||
github.com/mattermost/morph v0.0.0-20220401091636-39f834798da8
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
github.com/mgdelacroix/foundation v0.0.0-20220812143423-0bfc18f73538
|
||||
github.com/oklog/run v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/rudderlabs/analytics-go v3.3.2+incompatible
|
||||
github.com/sergi/go-diff v1.2.0
|
||||
github.com/spf13/viper v1.10.1
|
||||
github.com/stretchr/testify v1.7.2
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/wiggin77/merror v1.0.3
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||
)
|
||||
@ -37,6 +37,8 @@ require (
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/golang-migrate/migrate/v4 v4.15.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/graph-gophers/graphql-go v1.4.0 // indirect
|
||||
@ -44,6 +46,7 @@ require (
|
||||
github.com/hashicorp/go-plugin v1.4.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.15.6 // indirect
|
||||
@ -54,6 +57,7 @@ require (
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
|
||||
github.com/mattermost/logr/v2 v2.0.15 // indirect
|
||||
github.com/mattermost/squirrel v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
@ -93,6 +97,7 @@ require (
|
||||
github.com/yuin/goldmark v1.4.12 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 // indirect
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f // indirect
|
||||
golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
|
1131
server/go.sum
1131
server/go.sum
File diff suppressed because it is too large
Load Diff
@ -34,7 +34,7 @@ func TestGetBlocks(t *testing.T) {
|
||||
Type: model.TypeCard,
|
||||
},
|
||||
}
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 2)
|
||||
blockID1 := newBlocks[0].ID
|
||||
@ -73,7 +73,7 @@ func TestPostBlock(t *testing.T) {
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
blockID1 = newBlocks[0].ID
|
||||
@ -109,7 +109,7 @@ func TestPostBlock(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 2)
|
||||
blockID2 = newBlocks[0].ID
|
||||
@ -140,7 +140,7 @@ func TestPostBlock(t *testing.T) {
|
||||
Title: "Updated title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
blockID4 := newBlocks[0].ID
|
||||
@ -180,7 +180,7 @@ func TestPatchBlock(t *testing.T) {
|
||||
Fields: map[string]interface{}{"test": "test value", "test2": "test value 2"},
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}, false)
|
||||
th.CheckOK(resp)
|
||||
require.Len(t, newBlocks, 1)
|
||||
blockID := newBlocks[0].ID
|
||||
@ -191,7 +191,7 @@ func TestPatchBlock(t *testing.T) {
|
||||
Title: &newTitle,
|
||||
}
|
||||
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch, false)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
@ -216,7 +216,7 @@ func TestPatchBlock(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch, false)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
@ -239,7 +239,7 @@ func TestPatchBlock(t *testing.T) {
|
||||
DeletedFields: []string{"test", "test3", "test100"},
|
||||
}
|
||||
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch)
|
||||
_, resp := th.Client.PatchBlock(board.ID, blockID, blockPatch, false)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
@ -278,7 +278,7 @@ func TestDeleteBlock(t *testing.T) {
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
require.NotZero(t, newBlocks[0].ID)
|
||||
@ -301,7 +301,7 @@ func TestDeleteBlock(t *testing.T) {
|
||||
// id,insert_at on block history
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID)
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID, false)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
@ -332,7 +332,7 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
Title: "New title",
|
||||
}
|
||||
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block})
|
||||
newBlocks, resp := th.Client.InsertBlocks(board.ID, []model.Block{block}, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
require.NotZero(t, newBlocks[0].ID)
|
||||
@ -355,7 +355,7 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
// id,insert_at on block history
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID)
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID, false)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
blocks, resp := th.Client.GetBlocksForBoard(board.ID)
|
||||
@ -381,7 +381,7 @@ func TestUndeleteBlock(t *testing.T) {
|
||||
// id,insert_at on block history
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID)
|
||||
_, resp := th.Client.DeleteBlock(board.ID, blockID, false)
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
_, resp = th.Client2.UndeleteBlock(board.ID, blockID)
|
||||
|
@ -263,7 +263,7 @@ func TestCreateBoard(t *testing.T) {
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, board)
|
||||
|
||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
|
||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, boards)
|
||||
})
|
||||
@ -277,7 +277,7 @@ func TestCreateBoard(t *testing.T) {
|
||||
th.CheckBadRequest(resp)
|
||||
require.Nil(t, board)
|
||||
|
||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
|
||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, boards)
|
||||
})
|
||||
@ -292,7 +292,7 @@ func TestCreateBoard(t *testing.T) {
|
||||
th.CheckForbidden(resp)
|
||||
require.Nil(t, board)
|
||||
|
||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID)
|
||||
boards, err := th.Server.App().GetBoardsForUserAndTeam(user1.ID, teamID, true)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, boards)
|
||||
})
|
||||
@ -450,7 +450,7 @@ func TestGetAllBlocksForBoard(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
insertedBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
insertedBlocks, resp := th.Client.InsertBlocks(board.ID, newBlocks, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, insertedBlocks, len(newBlocks))
|
||||
|
||||
@ -530,7 +530,7 @@ func TestSearchBoards(t *testing.T) {
|
||||
Type: model.BoardTypePrivate,
|
||||
TeamID: "other-team-id",
|
||||
}
|
||||
_, err = th.Server.App().CreateBoard(board5, user1.ID, true)
|
||||
rBoard5, err := th.Server.App().CreateBoard(board5, user1.ID, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
@ -543,13 +543,13 @@ func TestSearchBoards(t *testing.T) {
|
||||
Name: "should return all boards where user1 is member or that are public",
|
||||
Client: th.Client,
|
||||
Term: "board",
|
||||
ExpectedIDs: []string{rBoard1.ID, rBoard2.ID, rBoard3.ID},
|
||||
ExpectedIDs: []string{rBoard1.ID, rBoard2.ID, rBoard3.ID, rBoard5.ID},
|
||||
},
|
||||
{
|
||||
Name: "matching a full word",
|
||||
Client: th.Client,
|
||||
Term: "admin",
|
||||
ExpectedIDs: []string{rBoard1.ID, rBoard3.ID},
|
||||
ExpectedIDs: []string{rBoard1.ID, rBoard3.ID, rBoard5.ID},
|
||||
},
|
||||
{
|
||||
Name: "matching part of the word",
|
||||
@ -1595,6 +1595,52 @@ func TestUpdateMember(t *testing.T) {
|
||||
require.Len(t, members, 1)
|
||||
require.True(t, members[0].SchemeAdmin)
|
||||
})
|
||||
|
||||
t.Run("should always disable the admin role on update member if the user is a guest", func(t *testing.T) {
|
||||
th := SetupTestHelperPluginMode(t)
|
||||
defer th.TearDown()
|
||||
clients := setupClients(th)
|
||||
|
||||
newBoard := &model.Board{
|
||||
Title: "title",
|
||||
Type: model.BoardTypeOpen,
|
||||
TeamID: teamID,
|
||||
}
|
||||
board, err := th.Server.App().CreateBoard(newBoard, userAdmin, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
newGuestMember := &model.BoardMember{
|
||||
UserID: userGuest,
|
||||
BoardID: board.ID,
|
||||
SchemeViewer: true,
|
||||
SchemeCommenter: true,
|
||||
SchemeEditor: true,
|
||||
SchemeAdmin: false,
|
||||
}
|
||||
guestMember, err := th.Server.App().AddMemberToBoard(newGuestMember)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, guestMember)
|
||||
require.True(t, guestMember.SchemeViewer)
|
||||
require.True(t, guestMember.SchemeCommenter)
|
||||
require.True(t, guestMember.SchemeEditor)
|
||||
require.False(t, guestMember.SchemeAdmin)
|
||||
|
||||
memberUpdate := &model.BoardMember{
|
||||
UserID: userGuest,
|
||||
BoardID: board.ID,
|
||||
SchemeAdmin: true,
|
||||
SchemeViewer: true,
|
||||
SchemeCommenter: true,
|
||||
SchemeEditor: true,
|
||||
}
|
||||
|
||||
updatedGuestMember, resp := clients.Admin.UpdateBoardMember(memberUpdate)
|
||||
th.CheckOK(resp)
|
||||
require.True(t, updatedGuestMember.SchemeViewer)
|
||||
require.True(t, updatedGuestMember.SchemeCommenter)
|
||||
require.True(t, updatedGuestMember.SchemeEditor)
|
||||
require.False(t, updatedGuestMember.SchemeAdmin)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteMember(t *testing.T) {
|
||||
@ -1905,7 +1951,7 @@ func TestDuplicateBoard(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
|
||||
@ -1990,7 +2036,7 @@ func TestDuplicateBoard(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks)
|
||||
newBlocks, resp = th.Client.InsertBlocks(board.ID, newBlocks, false)
|
||||
require.NoError(t, resp.Error)
|
||||
require.Len(t, newBlocks, 1)
|
||||
|
||||
|
@ -37,6 +37,7 @@ const (
|
||||
userCommenter string = "commenter"
|
||||
userEditor string = "editor"
|
||||
userAdmin string = "admin"
|
||||
userGuest string = "guest"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -47,6 +48,7 @@ var (
|
||||
userCommenterID = userCommenter
|
||||
userEditorID = userEditor
|
||||
userAdminID = userAdmin
|
||||
userGuestID = userGuest
|
||||
)
|
||||
|
||||
type LicenseType int
|
||||
@ -62,6 +64,8 @@ type TestHelper struct {
|
||||
Server *server.Server
|
||||
Client *client.Client
|
||||
Client2 *client.Client
|
||||
|
||||
origEnvUnitTesting string
|
||||
}
|
||||
|
||||
type FakePermissionPluginAPI struct{}
|
||||
@ -248,8 +252,16 @@ func newTestServerLocalMode() *server.Server {
|
||||
}
|
||||
|
||||
func SetupTestHelperWithToken(t *testing.T) *TestHelper {
|
||||
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||
|
||||
sessionToken := "TESTTOKEN"
|
||||
th := &TestHelper{T: t}
|
||||
|
||||
th := &TestHelper{
|
||||
T: t,
|
||||
origEnvUnitTesting: origUnitTesting,
|
||||
}
|
||||
|
||||
th.Server = newTestServer(sessionToken)
|
||||
th.Client = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
||||
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, sessionToken)
|
||||
@ -261,21 +273,42 @@ func SetupTestHelper(t *testing.T) *TestHelper {
|
||||
}
|
||||
|
||||
func SetupTestHelperPluginMode(t *testing.T) *TestHelper {
|
||||
th := &TestHelper{T: t}
|
||||
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||
|
||||
th := &TestHelper{
|
||||
T: t,
|
||||
origEnvUnitTesting: origUnitTesting,
|
||||
}
|
||||
|
||||
th.Server = NewTestServerPluginMode()
|
||||
th.Start()
|
||||
return th
|
||||
}
|
||||
|
||||
func SetupTestHelperLocalMode(t *testing.T) *TestHelper {
|
||||
th := &TestHelper{T: t}
|
||||
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||
|
||||
th := &TestHelper{
|
||||
T: t,
|
||||
origEnvUnitTesting: origUnitTesting,
|
||||
}
|
||||
|
||||
th.Server = newTestServerLocalMode()
|
||||
th.Start()
|
||||
return th
|
||||
}
|
||||
|
||||
func SetupTestHelperWithLicense(t *testing.T, licenseType LicenseType) *TestHelper {
|
||||
th := &TestHelper{T: t}
|
||||
origUnitTesting := os.Getenv("FOCALBOARD_UNIT_TESTING")
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", "1")
|
||||
|
||||
th := &TestHelper{
|
||||
T: t,
|
||||
origEnvUnitTesting: origUnitTesting,
|
||||
}
|
||||
|
||||
th.Server = newTestServerWithLicense("", licenseType)
|
||||
th.Client = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||
th.Client2 = client.NewClient(th.Server.Config().ServerRoot, "")
|
||||
@ -343,6 +376,8 @@ func (th *TestHelper) InitBasic() *TestHelper {
|
||||
var ErrRegisterFail = errors.New("register failed")
|
||||
|
||||
func (th *TestHelper) TearDown() {
|
||||
os.Setenv("FOCALBOARD_UNIT_TESTING", th.origEnvUnitTesting)
|
||||
|
||||
logger := th.Server.Logger()
|
||||
|
||||
if l, ok := logger.(*mlog.Logger); ok {
|
||||
|
@ -54,7 +54,7 @@ func TestExportBoard(t *testing.T) {
|
||||
require.NoError(t, resp.Error)
|
||||
|
||||
// check for test card
|
||||
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID)
|
||||
boardsImported, err := th.Server.App().GetBoardsForUserAndTeam(th.GetUser1().ID, model.GlobalTeamID, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, boardsImported, 1)
|
||||
boardImported := boardsImported[0]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -73,6 +73,15 @@ func NewPluginTestStore(innerStore store.Store) *PluginTestStore {
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
},
|
||||
"guest": {
|
||||
ID: "guest",
|
||||
Props: map[string]interface{}{},
|
||||
Username: "guest",
|
||||
Email: "guest@sample.com",
|
||||
CreateAt: model.GetMillis(),
|
||||
UpdateAt: model.GetMillis(),
|
||||
IsGuest: true,
|
||||
},
|
||||
},
|
||||
testTeam: &model.Team{ID: "test-team", Title: "Test Team"},
|
||||
otherTeam: &model.Team{ID: "other-team", Title: "Other Team"},
|
||||
@ -109,6 +118,8 @@ func (s *PluginTestStore) GetTeamsForUser(userID string) ([]*model.Team, error)
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
case "admin":
|
||||
return []*model.Team{s.testTeam, s.otherTeam}, nil
|
||||
case "guest":
|
||||
return []*model.Team{s.testTeam}, nil
|
||||
}
|
||||
return nil, errTestStore
|
||||
}
|
||||
@ -160,7 +171,30 @@ func (s *PluginTestStore) PatchUserProps(userID string, patch model.UserPropPatc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||
func (s *PluginTestStore) GetUserPreferences(userID string) (mmModel.Preferences, error) {
|
||||
if userID == userTeamMember {
|
||||
return mmModel.Preferences{{
|
||||
UserId: userTeamMember,
|
||||
Category: "focalboard",
|
||||
Name: "test",
|
||||
Value: "test",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
|
||||
if asGuestID == "guest" {
|
||||
return []*model.User{
|
||||
s.users["viewer"],
|
||||
s.users["commenter"],
|
||||
s.users["editor"],
|
||||
s.users["admin"],
|
||||
s.users["guest"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case teamID == s.testTeam.ID:
|
||||
return []*model.User{
|
||||
@ -169,6 +203,7 @@ func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||
s.users["commenter"],
|
||||
s.users["editor"],
|
||||
s.users["admin"],
|
||||
s.users["guest"],
|
||||
}, nil
|
||||
case teamID == s.otherTeam.ID:
|
||||
return []*model.User{
|
||||
@ -184,9 +219,9 @@ func (s *PluginTestStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
||||
func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||
users := []*model.User{}
|
||||
teamUsers, err := s.GetUsersByTeam(teamID)
|
||||
teamUsers, err := s.GetUsersByTeam(teamID, asGuestID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -199,6 +234,32 @@ func (s *PluginTestStore) SearchUsersByTeam(teamID string, searchQuery string) (
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) CanSeeUser(seerID string, seenID string) (bool, error) {
|
||||
user, err := s.GetUserByID(seerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !user.IsGuest {
|
||||
return true, nil
|
||||
}
|
||||
seerMembers, err := s.GetMembersForUser(seerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
seenMembers, err := s.GetMembersForUser(seenID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, seerMember := range seerMembers {
|
||||
for _, seenMember := range seenMembers {
|
||||
if seerMember.BoardID == seenMember.BoardID {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) SearchUserChannels(teamID, userID, query string) ([]*mmModel.Channel, error) {
|
||||
return []*mmModel.Channel{
|
||||
{
|
||||
@ -235,8 +296,8 @@ func (s *PluginTestStore) GetChannel(teamID, channel string) (*mmModel.Channel,
|
||||
return nil, errTestStore
|
||||
}
|
||||
|
||||
func (s *PluginTestStore) SearchBoardsForUser(term string, userID string) ([]*model.Board, error) {
|
||||
boards, err := s.Store.SearchBoardsForUser(term, userID)
|
||||
func (s *PluginTestStore) SearchBoardsForUser(term string, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
boards, err := s.Store.SearchBoardsForUser(term, userID, includePublicBoards)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func createTestSubscriptions(client *client.Client, num int) ([]*model.Subscript
|
||||
Type: model.TypeCard,
|
||||
}
|
||||
|
||||
newBlocks, resp := client.InsertBlocks(board.ID, []model.Block{newBlock})
|
||||
newBlocks, resp := client.InsertBlocks(board.ID, []model.Block{newBlock}, false)
|
||||
if resp.Error != nil {
|
||||
return nil, "", fmt.Errorf("cannot insert test card block: %w", resp.Error)
|
||||
}
|
||||
|
@ -53,16 +53,6 @@ func monitorPid(pid int, logger *mlog.Logger) {
|
||||
}()
|
||||
}
|
||||
|
||||
func logInfo(logger *mlog.Logger) {
|
||||
logger.Info("FocalBoard Server",
|
||||
mlog.String("version", model.CurrentVersion),
|
||||
mlog.String("edition", model.Edition),
|
||||
mlog.String("build_number", model.BuildNumber),
|
||||
mlog.String("build_date", model.BuildDate),
|
||||
mlog.String("build_hash", model.BuildHash),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Command line args
|
||||
pMonitorPid := flag.Int("monitorpid", -1, "a process ID")
|
||||
@ -101,7 +91,7 @@ func main() {
|
||||
defer restore()
|
||||
}
|
||||
|
||||
logInfo(logger)
|
||||
model.LogServerInfo(logger)
|
||||
|
||||
singleUser := false
|
||||
if pSingleUser != nil {
|
||||
@ -214,7 +204,7 @@ func startServer(webPath string, filesPath string, port int, singleUserToken, db
|
||||
return
|
||||
}
|
||||
|
||||
logInfo(logger)
|
||||
model.LogServerInfo(logger)
|
||||
|
||||
if len(filesPath) > 0 {
|
||||
config.FilesPath = filesPath
|
||||
|
@ -94,10 +94,6 @@ 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
|
||||
@ -149,10 +145,6 @@ func (p *BlockPatch) Patch(block *Block) *Block {
|
||||
block.ParentID = *p.ParentID
|
||||
}
|
||||
|
||||
if p.BoardID != nil {
|
||||
block.BoardID = *p.BoardID
|
||||
}
|
||||
|
||||
if p.Schema != nil {
|
||||
block.Schema = *p.Schema
|
||||
}
|
||||
@ -176,6 +168,14 @@ func (p *BlockPatch) Patch(block *Block) *Block {
|
||||
return block
|
||||
}
|
||||
|
||||
type QueryBlocksOptions struct {
|
||||
BoardID string // if not empty then filter for blocks belonging to specified board
|
||||
ParentID string // if not empty then filter for blocks belonging to specified parent
|
||||
BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type
|
||||
Page int // page number to select when paginating
|
||||
PerPage int // number of blocks per page (default=-1, meaning unlimited)
|
||||
}
|
||||
|
||||
// QuerySubtreeOptions are query options that can be passed to GetSubTree methods.
|
||||
type QuerySubtreeOptions struct {
|
||||
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
|
||||
|
@ -67,6 +67,20 @@ func (mr *MockServicesAPIMockRecorder) CreatePost(arg0 interface{}) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePost", reflect.TypeOf((*MockServicesAPI)(nil).CreatePost), arg0)
|
||||
}
|
||||
|
||||
// DeletePreferencesForUser mocks base method.
|
||||
func (m *MockServicesAPI) DeletePreferencesForUser(arg0 string, arg1 model.Preferences) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeletePreferencesForUser", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeletePreferencesForUser indicates an expected call of DeletePreferencesForUser.
|
||||
func (mr *MockServicesAPIMockRecorder) DeletePreferencesForUser(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).DeletePreferencesForUser), arg0, arg1)
|
||||
}
|
||||
|
||||
// EnsureBot mocks base method.
|
||||
func (m *MockServicesAPI) EnsureBot(arg0 *model.Bot) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -243,6 +257,21 @@ func (mr *MockServicesAPIMockRecorder) GetMasterDB() *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMasterDB", reflect.TypeOf((*MockServicesAPI)(nil).GetMasterDB))
|
||||
}
|
||||
|
||||
// GetPreferencesForUser mocks base method.
|
||||
func (m *MockServicesAPI) GetPreferencesForUser(arg0 string) (model.Preferences, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetPreferencesForUser", arg0)
|
||||
ret0, _ := ret[0].(model.Preferences)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetPreferencesForUser indicates an expected call of GetPreferencesForUser.
|
||||
func (mr *MockServicesAPIMockRecorder) GetPreferencesForUser(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).GetPreferencesForUser), arg0)
|
||||
}
|
||||
|
||||
// GetTeamMember mocks base method.
|
||||
func (m *MockServicesAPI) GetTeamMember(arg0, arg1 string) (*model.TeamMember, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -399,6 +428,20 @@ func (mr *MockServicesAPIMockRecorder) RegisterRouter(arg0 interface{}) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterRouter", reflect.TypeOf((*MockServicesAPI)(nil).RegisterRouter), arg0)
|
||||
}
|
||||
|
||||
// UpdatePreferencesForUser mocks base method.
|
||||
func (m *MockServicesAPI) UpdatePreferencesForUser(arg0 string, arg1 model.Preferences) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdatePreferencesForUser", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdatePreferencesForUser indicates an expected call of UpdatePreferencesForUser.
|
||||
func (mr *MockServicesAPIMockRecorder) UpdatePreferencesForUser(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePreferencesForUser", reflect.TypeOf((*MockServicesAPI)(nil).UpdatePreferencesForUser), arg0, arg1)
|
||||
}
|
||||
|
||||
// UpdateUser mocks base method.
|
||||
func (m *MockServicesAPI) UpdateUser(arg0 *model.User) (*model.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -17,4 +17,6 @@ var (
|
||||
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: ""}
|
||||
PermissionCommentBoardCards = &mmModel.Permission{Id: "comment_board_cards", Name: "", Description: "", Scope: ""}
|
||||
PermissionDeleteOthersComments = &mmModel.Permission{Id: "delete_others_comments", Name: "", Description: "", Scope: ""}
|
||||
)
|
||||
|
@ -14,6 +14,19 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
botUsername = "boards"
|
||||
botDisplayname = "Boards"
|
||||
botDescription = "Created by Boards plugin."
|
||||
)
|
||||
|
||||
var FocalboardBot = &mm_model.Bot{
|
||||
Username: botUsername,
|
||||
DisplayName: botDisplayname,
|
||||
Description: botDescription,
|
||||
OwnerId: SystemUserID,
|
||||
}
|
||||
|
||||
type ServicesAPI interface {
|
||||
// Channels service
|
||||
GetDirectChannel(userID1, userID2 string) (*mm_model.Channel, error)
|
||||
@ -72,4 +85,9 @@ type ServicesAPI interface {
|
||||
|
||||
// Router service
|
||||
RegisterRouter(sub *mux.Router)
|
||||
|
||||
// Preferences services
|
||||
GetPreferencesForUser(userID string) (mm_model.Preferences, error)
|
||||
UpdatePreferencesForUser(userID string, preferences mm_model.Preferences) error
|
||||
DeletePreferencesForUser(userID string, preferences mm_model.Preferences) error
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SingleUser = "single-user"
|
||||
GlobalTeamID = "0"
|
||||
SystemUserID = "system"
|
||||
SingleUser = "single-user"
|
||||
GlobalTeamID = "0"
|
||||
SystemUserID = "system"
|
||||
PreferencesCategoryFocalboard = "focalboard"
|
||||
)
|
||||
|
||||
// User is a user
|
||||
|
@ -1,5 +1,9 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
// This is a list of all the current versions including any patches.
|
||||
// It should be maintained in chronological order with most current
|
||||
// release at the front of the list.
|
||||
@ -41,3 +45,14 @@ var (
|
||||
BuildHash string
|
||||
Edition string
|
||||
)
|
||||
|
||||
// LogServerInfo logs information about the server instance.
|
||||
func LogServerInfo(logger mlog.LoggerIFace) {
|
||||
logger.Info("FocalBoard Server",
|
||||
mlog.String("version", CurrentVersion),
|
||||
mlog.String("edition", Edition),
|
||||
mlog.String("build_number", BuildNumber),
|
||||
mlog.String("build_date", BuildDate),
|
||||
mlog.String("build_hash", BuildHash),
|
||||
)
|
||||
}
|
||||
|
@ -67,10 +67,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||
return member.SchemeAdmin
|
||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||
return member.SchemeAdmin || member.SchemeEditor
|
||||
case model.PermissionCommentBoardCards:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
|
||||
case model.PermissionViewBoard:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||
default:
|
||||
|
@ -99,10 +99,12 @@ func (s *Service) HasPermissionToBoard(userID, boardID string, permission *mmMod
|
||||
}
|
||||
|
||||
switch permission {
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard:
|
||||
case model.PermissionManageBoardType, model.PermissionDeleteBoard, model.PermissionManageBoardRoles, model.PermissionShareBoard, model.PermissionDeleteOthersComments:
|
||||
return member.SchemeAdmin
|
||||
case model.PermissionManageBoardCards, model.PermissionManageBoardProperties:
|
||||
return member.SchemeAdmin || member.SchemeEditor
|
||||
case model.PermissionCommentBoardCards:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter
|
||||
case model.PermissionViewBoard:
|
||||
return member.SchemeAdmin || member.SchemeEditor || member.SchemeCommenter || member.SchemeViewer
|
||||
default:
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||
//go:generate mockgen -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||
package mmpermissions
|
||||
|
||||
import (
|
||||
|
@ -457,6 +457,21 @@ func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnablePlugin", reflect.TypeOf((*MockAPI)(nil).EnablePlugin), arg0)
|
||||
}
|
||||
|
||||
// EnsureBotUser mocks base method.
|
||||
func (m *MockAPI) EnsureBotUser(arg0 *model.Bot) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "EnsureBotUser", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// EnsureBotUser indicates an expected call of EnsureBotUser.
|
||||
func (mr *MockAPIMockRecorder) EnsureBotUser(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBotUser", reflect.TypeOf((*MockAPI)(nil).EnsureBotUser), arg0)
|
||||
}
|
||||
|
||||
// ExecuteSlashCommand mocks base method.
|
||||
func (m *MockAPI) ExecuteSlashCommand(arg0 *model.CommandArgs) (*model.CommandResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -681,6 +696,21 @@ func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interf
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// GetCloudLimits mocks base method.
|
||||
func (m *MockAPI) GetCloudLimits() (*model.ProductLimits, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCloudLimits")
|
||||
ret0, _ := ret[0].(*model.ProductLimits)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCloudLimits indicates an expected call of GetCloudLimits.
|
||||
func (mr *MockAPIMockRecorder) GetCloudLimits() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockAPI)(nil).GetCloudLimits))
|
||||
}
|
||||
|
||||
// GetCommand mocks base method.
|
||||
func (m *MockAPI) GetCommand(arg0 string) (*model.Command, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store
|
||||
//go:generate mockgen -destination=mocks/mockstore.go -package mocks . Store
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
|
@ -18,10 +18,7 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
var systemsBot = &mmModel.Bot{
|
||||
Username: mmModel.BotSystemBotUsername,
|
||||
DisplayName: "System",
|
||||
}
|
||||
var boardsBotID string
|
||||
|
||||
// servicesAPI is the interface required my the MattermostAuthLayer to interact with
|
||||
// the mattermost-server. You can use plugin-api or product-api adapter implementations.
|
||||
@ -39,6 +36,9 @@ type servicesAPI interface {
|
||||
GetCloudLimits() (*mmModel.ProductLimits, error)
|
||||
EnsureBot(bot *mmModel.Bot) (string, error)
|
||||
CreatePost(post *mmModel.Post) (*mmModel.Post, error)
|
||||
GetPreferencesForUser(userID string) (mmModel.Preferences, error)
|
||||
DeletePreferencesForUser(userID string, preferences mmModel.Preferences) error
|
||||
UpdatePreferencesForUser(userID string, preferences mmModel.Preferences) error
|
||||
}
|
||||
|
||||
// Store represents the abstraction of the data storage.
|
||||
@ -74,8 +74,7 @@ func (s *MattermostAuthLayer) GetRegisteredUserCount() (int, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("count(*)").
|
||||
From("Users").
|
||||
Where(sq.Eq{"deleteAt": 0}).
|
||||
Where(sq.NotEq{"roles": "system_guest"})
|
||||
Where(sq.Eq{"deleteAt": 0})
|
||||
row := query.QueryRow()
|
||||
|
||||
var count int
|
||||
@ -131,32 +130,50 @@ func (s *MattermostAuthLayer) UpdateUserPasswordByID(userID, password string) er
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) PatchUserProps(userID string, patch model.UserPropPatch) error {
|
||||
user, err := s.servicesAPI.GetUserByID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to fetch user", mlog.String("userID", userID), mlog.Err(err))
|
||||
return err
|
||||
if len(patch.UpdatedFields) > 0 {
|
||||
updatedPreferences := mmModel.Preferences{}
|
||||
for key, value := range patch.UpdatedFields {
|
||||
preference := mmModel.Preference{
|
||||
UserId: userID,
|
||||
Category: model.PreferencesCategoryFocalboard,
|
||||
Name: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
updatedPreferences = append(updatedPreferences, preference)
|
||||
}
|
||||
|
||||
if err := s.servicesAPI.UpdatePreferencesForUser(userID, updatedPreferences); err != nil {
|
||||
s.logger.Error("failed to update user preferences", mlog.String("user_id", userID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
props := user.Props
|
||||
if len(patch.DeletedFields) > 0 {
|
||||
deletedPreferences := mmModel.Preferences{}
|
||||
for _, key := range patch.DeletedFields {
|
||||
preference := mmModel.Preference{
|
||||
UserId: userID,
|
||||
Category: model.PreferencesCategoryFocalboard,
|
||||
Name: key,
|
||||
}
|
||||
|
||||
for _, key := range patch.DeletedFields {
|
||||
delete(props, key)
|
||||
}
|
||||
deletedPreferences = append(deletedPreferences, preference)
|
||||
}
|
||||
|
||||
for key, value := range patch.UpdatedFields {
|
||||
props[key] = value
|
||||
}
|
||||
|
||||
user.Props = props
|
||||
|
||||
if _, err := s.servicesAPI.UpdateUser(user); err != nil {
|
||||
s.logger.Error("failed to update user", mlog.String("userID", userID), mlog.Err(err))
|
||||
return err
|
||||
if err := s.servicesAPI.DeletePreferencesForUser(userID, deletedPreferences); err != nil {
|
||||
s.logger.Error("failed to delete user preferences", mlog.String("user_id", userID), mlog.Err(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetUserPreferences(userID string) (mmModel.Preferences, error) {
|
||||
return s.servicesAPI.GetPreferencesForUser(userID)
|
||||
}
|
||||
|
||||
// GetActiveUserCount returns the number of users with active sessions within N seconds ago.
|
||||
func (s *MattermostAuthLayer) GetActiveUserCount(updatedSecondsAgo int64) (int, error) {
|
||||
query := s.getQueryBuilder().
|
||||
@ -270,16 +287,32 @@ func (s *MattermostAuthLayer) getQueryBuilder() sq.StatementBuilderType {
|
||||
return builder.RunWith(s.mmDB)
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||
func (s *MattermostAuthLayer) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
|
||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
|
||||
From("Users as u").
|
||||
Join("TeamMembers as tm ON tm.UserID = u.id").
|
||||
LeftJoin("Bots b ON ( b.UserID = u.id )").
|
||||
Where(sq.Eq{"u.deleteAt": 0}).
|
||||
Where(sq.NotEq{"u.roles": "system_guest"}).
|
||||
Where(sq.Eq{"tm.TeamId": teamID})
|
||||
Where(sq.Eq{"u.deleteAt": 0})
|
||||
|
||||
if asGuestID == "" {
|
||||
query = query.
|
||||
Join("TeamMembers as tm ON tm.UserID = u.id").
|
||||
Where(sq.Eq{"tm.TeamId": teamID})
|
||||
} else {
|
||||
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boardsIDs := []string{}
|
||||
for _, board := range boards {
|
||||
boardsIDs = append(boardsIDs, board.ID)
|
||||
}
|
||||
query = query.
|
||||
Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID").
|
||||
Where(sq.Eq{"bm.BoardId": boardsIDs})
|
||||
}
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
@ -298,7 +331,7 @@ func (s *MattermostAuthLayer) GetUsersByTeam(teamID string) ([]*model.User, erro
|
||||
func (s *MattermostAuthLayer) GetUsersList(userIDs []string) ([]*model.User, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
|
||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
|
||||
From("Users as u").
|
||||
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||
Where(sq.Eq{"u.id": userIDs})
|
||||
@ -317,12 +350,11 @@ func (s *MattermostAuthLayer) GetUsersList(userIDs []string) ([]*model.User, err
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
||||
func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select("u.id", "u.username", "u.email", "u.nickname", "u.firstname", "u.lastname", "u.props", "u.CreateAt as create_at", "u.UpdateAt as update_at",
|
||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot").
|
||||
"u.DeleteAt as delete_at", "b.UserId IS NOT NULL AS is_bot, u.roles = 'system_guest' as is_guest").
|
||||
From("Users as u").
|
||||
Join("TeamMembers as tm ON tm.UserID = u.id").
|
||||
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||
Where(sq.Eq{"u.deleteAt": 0}).
|
||||
Where(sq.Or{
|
||||
@ -331,11 +363,27 @@ func (s *MattermostAuthLayer) SearchUsersByTeam(teamID string, searchQuery strin
|
||||
sq.Like{"u.firstname": "%" + searchQuery + "%"},
|
||||
sq.Like{"u.lastname": "%" + searchQuery + "%"},
|
||||
}).
|
||||
Where(sq.Eq{"tm.TeamId": teamID}).
|
||||
Where(sq.NotEq{"u.roles": "system_guest"}).
|
||||
OrderBy("u.username").
|
||||
Limit(10)
|
||||
|
||||
if asGuestID == "" {
|
||||
query = query.
|
||||
Join("TeamMembers as tm ON tm.UserID = u.id").
|
||||
Where(sq.Eq{"tm.TeamId": teamID})
|
||||
} else {
|
||||
boards, err := s.GetBoardsForUserAndTeam(asGuestID, teamID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
boardsIDs := []string{}
|
||||
for _, board := range boards {
|
||||
boardsIDs = append(boardsIDs, board.ID)
|
||||
}
|
||||
query = query.
|
||||
Join(s.tablePrefix + "board_members as bm ON bm.UserID = u.ID").
|
||||
Where(sq.Eq{"bm.BoardId": boardsIDs})
|
||||
}
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -369,6 +417,7 @@ func (s *MattermostAuthLayer) usersFromRows(rows *sql.Rows) ([]*model.User, erro
|
||||
&user.UpdateAt,
|
||||
&user.DeleteAt,
|
||||
&user.IsBot,
|
||||
&user.IsGuest,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -561,7 +610,7 @@ func boardFields(prefix string) []string {
|
||||
// term that are either private and which the user is a member of, or
|
||||
// they're open, regardless of the user membership.
|
||||
// Search is case-insensitive.
|
||||
func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string) ([]*model.Board, error) {
|
||||
func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
query := s.getQueryBuilder().
|
||||
Select(boardFields("b.")...).
|
||||
Distinct().
|
||||
@ -571,17 +620,20 @@ func (s *MattermostAuthLayer) SearchBoardsForUser(term, userID string) ([]*model
|
||||
LeftJoin("ChannelMembers as cm on cm.channelId=b.channel_id").
|
||||
Where(sq.Eq{"b.is_template": false}).
|
||||
Where(sq.Eq{"tm.userID": userID}).
|
||||
Where(sq.Eq{"tm.deleteAt": 0}).
|
||||
Where(sq.Or{
|
||||
Where(sq.Eq{"tm.deleteAt": 0})
|
||||
|
||||
if includePublicBoards {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||
sq.And{
|
||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
||||
sq.Or{
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
sq.Eq{"cm.userId": userID},
|
||||
},
|
||||
},
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
sq.Eq{"cm.userId": userID},
|
||||
})
|
||||
} else {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
sq.Eq{"cm.userId": userID},
|
||||
})
|
||||
}
|
||||
|
||||
if term != "" {
|
||||
// break search query into space separated words
|
||||
@ -802,12 +854,14 @@ func (s *MattermostAuthLayer) GetMembersForBoard(boardID string) ([]*model.Board
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error) {
|
||||
func (s *MattermostAuthLayer) GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
members, err := s.GetMembersForUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Handle the includePublicBoards
|
||||
|
||||
boardIDs := []string{}
|
||||
for _, m := range members {
|
||||
boardIDs = append(boardIDs, m.BoardID)
|
||||
@ -852,18 +906,18 @@ func (s *MattermostAuthLayer) GetChannel(teamID, channelID string) (*mmModel.Cha
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) getSystemBotID() (string, error) {
|
||||
botID, err := s.servicesAPI.EnsureBot(systemsBot)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to ensure system bot", mlog.String("username", systemsBot.Username), mlog.Err(err))
|
||||
func (s *MattermostAuthLayer) getBoardsBotID() (string, error) {
|
||||
if boardsBotID == "" {
|
||||
var err error
|
||||
boardsBotID, err = s.servicesAPI.EnsureBot(model.FocalboardBot)
|
||||
s.logger.Error("failed to ensure boards bot", mlog.Err(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return botID, nil
|
||||
return boardsBotID, nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) SendMessage(message, postType string, receipts []string) error {
|
||||
botID, err := s.getSystemBotID()
|
||||
botID, err := s.getBoardsBotID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -880,14 +934,7 @@ func (s *MattermostAuthLayer) SendMessage(message, postType string, receipts []s
|
||||
continue
|
||||
}
|
||||
|
||||
post := &mmModel.Post{
|
||||
Message: message,
|
||||
UserId: botID,
|
||||
ChannelId: channel.Id,
|
||||
Type: postType,
|
||||
}
|
||||
|
||||
if _, err := s.servicesAPI.CreatePost(post); err != nil {
|
||||
if err := s.PostMessage(message, postType, channel.Id); err != nil {
|
||||
s.logger.Error(
|
||||
"failed to send message to receipt from SendMessage",
|
||||
mlog.String("receipt", receipt),
|
||||
@ -896,7 +943,28 @@ func (s *MattermostAuthLayer) SendMessage(message, postType string, receipts []s
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) PostMessage(message, postType, channelID string) error {
|
||||
botID, err := s.getBoardsBotID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post := &mmModel.Post{
|
||||
Message: message,
|
||||
UserId: botID,
|
||||
ChannelId: channelID,
|
||||
Type: postType,
|
||||
}
|
||||
|
||||
if _, err := s.servicesAPI.CreatePost(post); err != nil {
|
||||
s.logger.Error(
|
||||
"failed to send message to receipt from PostMessage",
|
||||
mlog.Err(err),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -908,3 +976,67 @@ func (s *MattermostAuthLayer) GetUserTimezone(userID string) (string, error) {
|
||||
timezone := user.Timezone
|
||||
return mmModel.GetPreferredTimezone(timezone), nil
|
||||
}
|
||||
|
||||
func (s *MattermostAuthLayer) CanSeeUser(seerID string, seenID string) (bool, error) {
|
||||
mmuser, appErr := s.servicesAPI.GetUserByID(seerID)
|
||||
if appErr != nil {
|
||||
return false, appErr
|
||||
}
|
||||
if !mmuser.IsGuest() {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("1").
|
||||
From(s.tablePrefix + "board_members AS BM1").
|
||||
Join(s.tablePrefix + "board_members AS BM2 ON BM1.BoardID=BM2.BoardID").
|
||||
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||
Where(sq.Or{
|
||||
sq.And{
|
||||
sq.Eq{"BM1.UserID": seerID},
|
||||
sq.Eq{"BM2.UserID": seenID},
|
||||
},
|
||||
sq.And{
|
||||
sq.Eq{"BM1.UserID": seenID},
|
||||
sq.Eq{"BM2.UserID": seerID},
|
||||
},
|
||||
}).Limit(1)
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
for rows.Next() {
|
||||
return true, err
|
||||
}
|
||||
|
||||
query = s.getQueryBuilder().
|
||||
Select("1").
|
||||
From("ChannelMembers AS CM1").
|
||||
Join("ChannelMembers AS CM2 ON CM1.BoardID=CM2.BoardID").
|
||||
LeftJoin("Bots b ON ( b.UserId = u.id )").
|
||||
Where(sq.Or{
|
||||
sq.And{
|
||||
sq.Eq{"CM1.UserID": seerID},
|
||||
sq.Eq{"CM2.UserID": seenID},
|
||||
},
|
||||
sq.And{
|
||||
sq.Eq{"CM1.UserID": seenID},
|
||||
sq.Eq{"CM2.UserID": seerID},
|
||||
},
|
||||
}).Limit(1)
|
||||
|
||||
rows, err = query.Query()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
for rows.Next() {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
@ -50,6 +50,21 @@ func (mr *MockStoreMockRecorder) AddUpdateCategoryBoard(arg0, arg1, arg2 interfa
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUpdateCategoryBoard", reflect.TypeOf((*MockStore)(nil).AddUpdateCategoryBoard), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// CanSeeUser mocks base method.
|
||||
func (m *MockStore) CanSeeUser(arg0, arg1 string) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CanSeeUser", arg0, arg1)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CanSeeUser indicates an expected call of CanSeeUser.
|
||||
func (mr *MockStoreMockRecorder) CanSeeUser(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanSeeUser", reflect.TypeOf((*MockStore)(nil).CanSeeUser), arg0, arg1)
|
||||
}
|
||||
|
||||
// CleanUpSessions mocks base method.
|
||||
func (m *MockStore) CleanUpSessions(arg0 int64) error {
|
||||
m.ctrl.T.Helper()
|
||||
@ -399,6 +414,21 @@ func (mr *MockStoreMockRecorder) GetBlockHistoryDescendants(arg0, arg1 interface
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockHistoryDescendants", reflect.TypeOf((*MockStore)(nil).GetBlockHistoryDescendants), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetBlocks mocks base method.
|
||||
func (m *MockStore) GetBlocks(arg0 model.QueryBlocksOptions) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBlocks", arg0)
|
||||
ret0, _ := ret[0].([]model.Block)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBlocks indicates an expected call of GetBlocks.
|
||||
func (mr *MockStoreMockRecorder) GetBlocks(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocks", reflect.TypeOf((*MockStore)(nil).GetBlocks), arg0)
|
||||
}
|
||||
|
||||
// GetBlocksByIDs mocks base method.
|
||||
func (m *MockStore) GetBlocksByIDs(arg0 []string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -429,21 +459,6 @@ func (mr *MockStoreMockRecorder) GetBlocksForBoard(arg0 interface{}) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksForBoard", reflect.TypeOf((*MockStore)(nil).GetBlocksForBoard), arg0)
|
||||
}
|
||||
|
||||
// GetBlocksWithBoardID mocks base method.
|
||||
func (m *MockStore) GetBlocksWithBoardID(arg0 string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBlocksWithBoardID", arg0)
|
||||
ret0, _ := ret[0].([]model.Block)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBlocksWithBoardID indicates an expected call of GetBlocksWithBoardID.
|
||||
func (mr *MockStoreMockRecorder) GetBlocksWithBoardID(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksWithBoardID", reflect.TypeOf((*MockStore)(nil).GetBlocksWithBoardID), arg0)
|
||||
}
|
||||
|
||||
// GetBlocksWithParent mocks base method.
|
||||
func (m *MockStore) GetBlocksWithParent(arg0, arg1 string) ([]model.Block, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -567,18 +582,18 @@ func (mr *MockStoreMockRecorder) GetBoardMemberHistory(arg0, arg1, arg2 interfac
|
||||
}
|
||||
|
||||
// GetBoardsForUserAndTeam mocks base method.
|
||||
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string) ([]*model.Board, error) {
|
||||
func (m *MockStore) GetBoardsForUserAndTeam(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "GetBoardsForUserAndTeam", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]*model.Board)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetBoardsForUserAndTeam indicates an expected call of GetBoardsForUserAndTeam.
|
||||
func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockStoreMockRecorder) GetBoardsForUserAndTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBoardsForUserAndTeam", reflect.TypeOf((*MockStore)(nil).GetBoardsForUserAndTeam), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// GetBoardsInTeamByIds mocks base method.
|
||||
@ -1075,6 +1090,21 @@ func (mr *MockStoreMockRecorder) GetUserCategoryBoards(arg0, arg1 interface{}) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserCategoryBoards", reflect.TypeOf((*MockStore)(nil).GetUserCategoryBoards), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUserPreferences mocks base method.
|
||||
func (m *MockStore) GetUserPreferences(arg0 string) (model0.Preferences, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserPreferences", arg0)
|
||||
ret0, _ := ret[0].(model0.Preferences)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserPreferences indicates an expected call of GetUserPreferences.
|
||||
func (mr *MockStoreMockRecorder) GetUserPreferences(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserPreferences", reflect.TypeOf((*MockStore)(nil).GetUserPreferences), arg0)
|
||||
}
|
||||
|
||||
// GetUserTimezone mocks base method.
|
||||
func (m *MockStore) GetUserTimezone(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -1091,18 +1121,18 @@ func (mr *MockStoreMockRecorder) GetUserTimezone(arg0 interface{}) *gomock.Call
|
||||
}
|
||||
|
||||
// GetUsersByTeam mocks base method.
|
||||
func (m *MockStore) GetUsersByTeam(arg0 string) ([]*model.User, error) {
|
||||
func (m *MockStore) GetUsersByTeam(arg0, arg1 string) ([]*model.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUsersByTeam", arg0)
|
||||
ret := m.ctrl.Call(m, "GetUsersByTeam", arg0, arg1)
|
||||
ret0, _ := ret[0].([]*model.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUsersByTeam indicates an expected call of GetUsersByTeam.
|
||||
func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0 interface{}) *gomock.Call {
|
||||
func (mr *MockStoreMockRecorder) GetUsersByTeam(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUsersByTeam", reflect.TypeOf((*MockStore)(nil).GetUsersByTeam), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUsersList mocks base method.
|
||||
@ -1251,6 +1281,20 @@ func (mr *MockStoreMockRecorder) PatchUserProps(arg0, arg1 interface{}) *gomock.
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchUserProps", reflect.TypeOf((*MockStore)(nil).PatchUserProps), arg0, arg1)
|
||||
}
|
||||
|
||||
// PostMessage mocks base method.
|
||||
func (m *MockStore) PostMessage(arg0, arg1, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PostMessage", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PostMessage indicates an expected call of PostMessage.
|
||||
func (mr *MockStoreMockRecorder) PostMessage(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMessage", reflect.TypeOf((*MockStore)(nil).PostMessage), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// RefreshSession mocks base method.
|
||||
func (m *MockStore) RefreshSession(arg0 *model.Session) error {
|
||||
m.ctrl.T.Helper()
|
||||
@ -1324,18 +1368,18 @@ func (mr *MockStoreMockRecorder) SaveMember(arg0 interface{}) *gomock.Call {
|
||||
}
|
||||
|
||||
// SearchBoardsForUser mocks base method.
|
||||
func (m *MockStore) SearchBoardsForUser(arg0, arg1 string) ([]*model.Board, error) {
|
||||
func (m *MockStore) SearchBoardsForUser(arg0, arg1 string, arg2 bool) ([]*model.Board, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "SearchBoardsForUser", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]*model.Board)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SearchBoardsForUser indicates an expected call of SearchBoardsForUser.
|
||||
func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockStoreMockRecorder) SearchBoardsForUser(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBoardsForUser", reflect.TypeOf((*MockStore)(nil).SearchBoardsForUser), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SearchBoardsForUserInTeam mocks base method.
|
||||
@ -1369,18 +1413,18 @@ func (mr *MockStoreMockRecorder) SearchUserChannels(arg0, arg1, arg2 interface{}
|
||||
}
|
||||
|
||||
// SearchUsersByTeam mocks base method.
|
||||
func (m *MockStore) SearchUsersByTeam(arg0, arg1 string) ([]*model.User, error) {
|
||||
func (m *MockStore) SearchUsersByTeam(arg0, arg1, arg2 string) ([]*model.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1)
|
||||
ret := m.ctrl.Call(m, "SearchUsersByTeam", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].([]*model.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// SearchUsersByTeam indicates an expected call of SearchUsersByTeam.
|
||||
func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1 interface{}) *gomock.Call {
|
||||
func (mr *MockStoreMockRecorder) SearchUsersByTeam(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsersByTeam", reflect.TypeOf((*MockStore)(nil).SearchUsersByTeam), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SendMessage mocks base method.
|
||||
|
@ -63,17 +63,34 @@ func (s *SQLStore) blockFields() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]model.Block, error) {
|
||||
func (s *SQLStore) getBlocks(db sq.BaseRunner, opts model.QueryBlocksOptions) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"board_id": boardID}).
|
||||
Where(sq.Eq{"parent_id": parentID}).
|
||||
Where(sq.Eq{"type": blockType})
|
||||
From(s.tablePrefix + "blocks")
|
||||
|
||||
if opts.BoardID != "" {
|
||||
query = query.Where(sq.Eq{"board_id": opts.BoardID})
|
||||
}
|
||||
|
||||
if opts.ParentID != "" {
|
||||
query = query.Where(sq.Eq{"parent_id": opts.ParentID})
|
||||
}
|
||||
|
||||
if opts.BlockType != "" && opts.BlockType != model.TypeUnknown {
|
||||
query = query.Where(sq.Eq{"type": opts.BlockType})
|
||||
}
|
||||
|
||||
if opts.Page != 0 {
|
||||
query = query.Offset(uint64(opts.Page))
|
||||
}
|
||||
|
||||
if opts.PerPage > 0 {
|
||||
query = query.Limit(uint64(opts.PerPage))
|
||||
}
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`getBlocksWithParentAndType ERROR`, mlog.Err(err))
|
||||
s.logger.Error(`getBlocks ERROR`, mlog.Err(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
@ -82,22 +99,21 @@ func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentI
|
||||
return s.blocksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"parent_id": parentID}).
|
||||
Where(sq.Eq{"board_id": boardID})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`getBlocksWithParent ERROR`, mlog.Err(err))
|
||||
|
||||
return nil, err
|
||||
func (s *SQLStore) getBlocksWithParentAndType(db sq.BaseRunner, boardID, parentID string, blockType string) ([]model.Block, error) {
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: boardID,
|
||||
ParentID: parentID,
|
||||
BlockType: model.BlockType(blockType),
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
return s.getBlocks(db, opts)
|
||||
}
|
||||
|
||||
return s.blocksFromRows(rows)
|
||||
func (s *SQLStore) getBlocksWithParent(db sq.BaseRunner, boardID, parentID string) ([]model.Block, error) {
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: boardID,
|
||||
ParentID: parentID,
|
||||
}
|
||||
return s.getBlocks(db, opts)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block, error) {
|
||||
@ -126,39 +142,12 @@ func (s *SQLStore) getBlocksByIDs(db sq.BaseRunner, ids []string) ([]model.Block
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksWithBoardID(db sq.BaseRunner, boardID string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"board_id": boardID})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`GetBlocksWithBoardID ERROR`, mlog.Err(err))
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
return s.blocksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksWithType(db sq.BaseRunner, boardID, blockType string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"type": blockType}).
|
||||
Where(sq.Eq{"board_id": boardID})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`getBlocksWithParentAndType ERROR`, mlog.Err(err))
|
||||
|
||||
return nil, err
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: boardID,
|
||||
BlockType: model.BlockType(blockType),
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
return s.blocksFromRows(rows)
|
||||
return s.getBlocks(db, opts)
|
||||
}
|
||||
|
||||
// getSubTree2 returns blocks within 2 levels of the given blockID.
|
||||
@ -194,19 +183,10 @@ func (s *SQLStore) getSubTree2(db sq.BaseRunner, boardID string, blockID string,
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBlocksForBoard(db sq.BaseRunner, boardID string) ([]model.Block, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(s.blockFields()...).
|
||||
From(s.tablePrefix + "blocks").
|
||||
Where(sq.Eq{"board_id": boardID})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error(`getAllBlocksForBoard ERROR`, mlog.Err(err))
|
||||
return nil, err
|
||||
opts := model.QueryBlocksOptions{
|
||||
BoardID: boardID,
|
||||
}
|
||||
defer s.CloseRows(rows)
|
||||
|
||||
return s.blocksFromRows(rows)
|
||||
return s.getBlocks(db, opts)
|
||||
}
|
||||
|
||||
func (s *SQLStore) blocksFromRows(rows *sql.Rows) ([]model.Block, error) {
|
||||
|
@ -252,21 +252,25 @@ func (s *SQLStore) getBoard(db sq.BaseRunner, boardID string) (*model.Board, err
|
||||
return s.getBoardByCondition(db, sq.Eq{"id": boardID})
|
||||
}
|
||||
|
||||
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string) ([]*model.Board, error) {
|
||||
func (s *SQLStore) getBoardsForUserAndTeam(db sq.BaseRunner, userID, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(boardFields("b.")...).
|
||||
Distinct().
|
||||
From(s.tablePrefix + "boards as b").
|
||||
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
||||
Where(sq.Eq{"b.team_id": teamID}).
|
||||
Where(sq.Eq{"b.is_template": false}).
|
||||
Where(sq.Or{
|
||||
Where(sq.Eq{"b.is_template": false})
|
||||
|
||||
if includePublicBoards {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||
sq.And{
|
||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
},
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
})
|
||||
} else {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
})
|
||||
}
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
@ -643,20 +647,24 @@ func (s *SQLStore) getMembersForBoard(db sq.BaseRunner, boardID string) ([]*mode
|
||||
// term that are either private and which the user is a member of, or
|
||||
// they're open, regardless of the user membership.
|
||||
// Search is case-insensitive.
|
||||
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string) ([]*model.Board, error) {
|
||||
func (s *SQLStore) searchBoardsForUser(db sq.BaseRunner, term, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select(boardFields("b.")...).
|
||||
Distinct().
|
||||
From(s.tablePrefix + "boards as b").
|
||||
LeftJoin(s.tablePrefix + "board_members as bm on b.id=bm.board_id").
|
||||
Where(sq.Eq{"b.is_template": false}).
|
||||
Where(sq.Or{
|
||||
Where(sq.Eq{"b.is_template": false})
|
||||
|
||||
if includePublicBoards {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"b.type": model.BoardTypeOpen},
|
||||
sq.And{
|
||||
sq.Eq{"b.type": model.BoardTypePrivate},
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
},
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
})
|
||||
} else {
|
||||
query = query.Where(sq.Or{
|
||||
sq.Eq{"bm.user_id": userID},
|
||||
})
|
||||
}
|
||||
|
||||
if term != "" {
|
||||
// break search query into space separated words
|
||||
|
@ -161,7 +161,7 @@ func (s *SQLStore) duplicateBoard(db sq.BaseRunner, boardID string, userID strin
|
||||
}
|
||||
|
||||
bab.Boards = []*model.Board{board}
|
||||
blocks, err := s.getBlocksWithBoardID(db, boardID)
|
||||
blocks, err := s.getBlocksForBoard(db, boardID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ func (s *SQLStore) getBlocksWithSameID(db sq.BaseRunner) ([]model.Block, error)
|
||||
return s.blocksFromRows(rows)
|
||||
}
|
||||
|
||||
func (s *SQLStore) runUniqueIDsMigration() error {
|
||||
func (s *SQLStore) RunUniqueIDsMigration() error {
|
||||
setting, err := s.GetSystemSetting(UniqueIDsMigrationKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get migration state: %w", err)
|
||||
@ -128,11 +128,11 @@ func (s *SQLStore) runUniqueIDsMigration() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// runCategoryUUIDIDMigration takes care of deriving the categories
|
||||
// RunCategoryUUIDIDMigration takes care of deriving the categories
|
||||
// from the boards and its memberships. The name references UUID
|
||||
// because of the preexisting purpose of this migration, and has been
|
||||
// preserved for compatibility with already migrated instances.
|
||||
func (s *SQLStore) runCategoryUUIDIDMigration() error {
|
||||
func (s *SQLStore) RunCategoryUUIDIDMigration() error {
|
||||
setting, err := s.GetSystemSetting(CategoryUUIDIDMigrationKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get migration state: %w", err)
|
||||
@ -348,7 +348,7 @@ func (s *SQLStore) createCategoryBoards(db sq.BaseRunner) error {
|
||||
// We no longer support boards existing in DMs and private
|
||||
// group messages. This function migrates all boards
|
||||
// belonging to a DM to the best possible team.
|
||||
func (s *SQLStore) runTeamLessBoardsMigration() error {
|
||||
func (s *SQLStore) RunTeamLessBoardsMigration() error {
|
||||
if !s.isPlugin {
|
||||
return nil
|
||||
}
|
||||
@ -550,7 +550,7 @@ func (s *SQLStore) getBoardUserTeams(tx sq.BaseRunner, board *model.Board) (map[
|
||||
return userTeams, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) runDeletedMembershipBoardsMigration() error {
|
||||
func (s *SQLStore) RunDeletedMembershipBoardsMigration() error {
|
||||
if !s.isPlugin {
|
||||
return nil
|
||||
}
|
||||
|
@ -194,7 +194,7 @@ func TestRunUniqueIDsMigration(t *testing.T) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
err := sqlStore.runUniqueIDsMigration()
|
||||
err := sqlStore.RunUniqueIDsMigration()
|
||||
require.NoError(t, err)
|
||||
|
||||
// blocks from workspace 1 haven't changed, so we can simply fetch them
|
||||
|
@ -31,7 +31,7 @@ func SetupTests(t *testing.T) (store.Store, func()) {
|
||||
IsPlugin: false,
|
||||
}
|
||||
store, err := New(storeParams)
|
||||
require.Nil(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
tearDown := func() {
|
||||
defer func() { _ = logger.Shutdown() }()
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/mattermost/morph/models"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/store/sqlstore"
|
||||
|
||||
"github.com/mattermost/morph"
|
||||
drivers "github.com/mattermost/morph/drivers"
|
||||
@ -21,7 +22,6 @@ import (
|
||||
sqlite "github.com/mattermost/morph/drivers/sqlite"
|
||||
embedded "github.com/mattermost/morph/sources/embedded"
|
||||
|
||||
mysqldriver "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq" // postgres driver
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
@ -31,7 +31,7 @@ import (
|
||||
)
|
||||
|
||||
//go:embed migrations
|
||||
var assets embed.FS
|
||||
var Assets embed.FS
|
||||
|
||||
const (
|
||||
uniqueIDsMigrationRequiredVersion = 14
|
||||
@ -43,30 +43,6 @@ const (
|
||||
|
||||
var errChannelCreatorNotInTeam = errors.New("channel creator not found in user teams")
|
||||
|
||||
func appendMultipleStatementsFlag(connectionString string) (string, error) {
|
||||
config, err := mysqldriver.ParseDSN(connectionString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if config.Params == nil {
|
||||
config.Params = map[string]string{}
|
||||
}
|
||||
|
||||
config.Params["multiStatements"] = "true"
|
||||
return config.FormatDSN(), nil
|
||||
}
|
||||
|
||||
// resetReadTimeout removes the timeout contraint from the MySQL dsn.
|
||||
func resetReadTimeout(dataSource string) (string, error) {
|
||||
config, err := mysqldriver.ParseDSN(dataSource)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config.ReadTimeout = 0
|
||||
return config.FormatDSN(), nil
|
||||
}
|
||||
|
||||
// migrations in MySQL need to run with the multiStatements flag
|
||||
// enabled, so this method creates a new connection ensuring that it's
|
||||
// enabled.
|
||||
@ -74,12 +50,12 @@ func (s *SQLStore) getMigrationConnection() (*sql.DB, error) {
|
||||
connectionString := s.connectionString
|
||||
if s.dbType == model.MysqlDBType {
|
||||
var err error
|
||||
connectionString, err = resetReadTimeout(connectionString)
|
||||
connectionString, err = sqlstore.ResetReadTimeout(connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connectionString, err = appendMultipleStatementsFlag(connectionString)
|
||||
connectionString, err = sqlstore.AppendMultipleStatementsFlag(connectionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -141,7 +117,7 @@ func (s *SQLStore) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
assetsList, err := assets.ReadDir("migrations")
|
||||
assetsList, err := Assets.ReadDir("migrations")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -162,7 +138,7 @@ func (s *SQLStore) Migrate() error {
|
||||
migrationAssets := &embedded.AssetSource{
|
||||
Names: assetNamesForDriver,
|
||||
AssetFunc: func(name string) ([]byte, error) {
|
||||
asset, mErr := assets.ReadFile("migrations/" + name)
|
||||
asset, mErr := Assets.ReadFile("migrations/" + name)
|
||||
if mErr != nil {
|
||||
return nil, mErr
|
||||
}
|
||||
@ -229,7 +205,7 @@ func (s *SQLStore) Migrate() error {
|
||||
return mErr
|
||||
}
|
||||
|
||||
if mErr := s.runUniqueIDsMigration(); mErr != nil {
|
||||
if mErr := s.RunUniqueIDsMigration(); mErr != nil {
|
||||
return fmt.Errorf("error running unique IDs migration: %w", mErr)
|
||||
}
|
||||
|
||||
@ -237,11 +213,11 @@ func (s *SQLStore) Migrate() error {
|
||||
return mErr
|
||||
}
|
||||
|
||||
if mErr := s.runTeamLessBoardsMigration(); mErr != nil {
|
||||
if mErr := s.RunTeamLessBoardsMigration(); mErr != nil {
|
||||
return mErr
|
||||
}
|
||||
|
||||
if mErr := s.runDeletedMembershipBoardsMigration(); mErr != nil {
|
||||
if mErr := s.RunDeletedMembershipBoardsMigration(); mErr != nil {
|
||||
return mErr
|
||||
}
|
||||
|
||||
@ -249,7 +225,7 @@ func (s *SQLStore) Migrate() error {
|
||||
return mErr
|
||||
}
|
||||
|
||||
if mErr := s.runCategoryUUIDIDMigration(); mErr != nil {
|
||||
if mErr := s.RunCategoryUUIDIDMigration(); mErr != nil {
|
||||
return fmt.Errorf("error running categoryID migration: %w", mErr)
|
||||
}
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetMySQLMigrationConnection(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Scenario string
|
||||
DSN string
|
||||
ExpectedDSN string
|
||||
}{
|
||||
{
|
||||
"Should append multiStatements param to the DSN path with existing params",
|
||||
"user:rand?&ompasswith@character@unix(/var/run/mysqld/mysqld.sock)/focalboard?writeTimeout=30s",
|
||||
"user:rand?&ompasswith@character@unix(/var/run/mysqld/mysqld.sock)/focalboard?writeTimeout=30s&multiStatements=true",
|
||||
},
|
||||
{
|
||||
"Should append multiStatements param to the DSN path with no existing params",
|
||||
"user:rand?&ompasswith@character@unix(/var/run/mysqld/mysqld.sock)/focalboard",
|
||||
"user:rand?&ompasswith@character@unix(/var/run/mysqld/mysqld.sock)/focalboard?multiStatements=true",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Scenario, func(t *testing.T) {
|
||||
res, err := appendMultipleStatementsFlag(tc.DSN)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.ExpectedDSN, res)
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
DROP INDEX idx_subscriptions_subscriber_id ON {{.prefix}}subscriptions;
|
||||
DROP INDEX idx_blocks_board_id_parent_id ON {{.prefix}}blocks;
|
||||
|
||||
{{if .mysql}}
|
||||
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
|
||||
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (channel_id, id);
|
||||
{{end}}
|
||||
|
||||
{{if .postgres}}
|
||||
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
|
||||
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (channel_id, id);
|
||||
{{end}}
|
||||
|
||||
{{if .sqlite}}
|
||||
ALTER TABLE {{.prefix}}blocks RENAME TO {{.prefix}}blocks_tmp;
|
||||
|
||||
CREATE TABLE {{.prefix}}blocks (
|
||||
id VARCHAR(36),
|
||||
insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||
parent_id VARCHAR(36),
|
||||
schema BIGINT,
|
||||
type TEXT,
|
||||
title TEXT,
|
||||
fields TEXT,
|
||||
create_at BIGINT,
|
||||
update_at BIGINT,
|
||||
delete_at BIGINT,
|
||||
root_id VARCHAR(36),
|
||||
modified_by VARCHAR(36),
|
||||
channel_id VARCHAR(36),
|
||||
created_by VARCHAR(36),
|
||||
board_id VARCHAR(36),
|
||||
PRIMARY KEY (channel_id, id)
|
||||
);
|
||||
|
||||
INSERT INTO {{.prefix}}blocks SELECT * FROM {{.prefix}}blocks_tmp;
|
||||
|
||||
DROP TABLE {{.prefix}}blocks_tmp;
|
||||
{{end}}
|
@ -0,0 +1,44 @@
|
||||
{{- /* delete old blocks PK and add id as the new one */ -}}
|
||||
{{if .mysql}}
|
||||
ALTER TABLE {{.prefix}}blocks DROP PRIMARY KEY;
|
||||
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
|
||||
{{end}}
|
||||
|
||||
{{if .postgres}}
|
||||
ALTER TABLE {{.prefix}}blocks DROP CONSTRAINT {{.prefix}}blocks_pkey1;
|
||||
ALTER TABLE {{.prefix}}blocks ADD PRIMARY KEY (id);
|
||||
{{end}}
|
||||
|
||||
{{- /* there is no way for SQLite to update the PK or add a unique constraint */ -}}
|
||||
{{if .sqlite}}
|
||||
ALTER TABLE {{.prefix}}blocks RENAME TO {{.prefix}}blocks_tmp;
|
||||
|
||||
CREATE TABLE {{.prefix}}blocks (
|
||||
id VARCHAR(36),
|
||||
insert_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||
parent_id VARCHAR(36),
|
||||
schema BIGINT,
|
||||
type TEXT,
|
||||
title TEXT,
|
||||
fields TEXT,
|
||||
create_at BIGINT,
|
||||
update_at BIGINT,
|
||||
delete_at BIGINT,
|
||||
root_id VARCHAR(36),
|
||||
modified_by VARCHAR(36),
|
||||
channel_id VARCHAR(36),
|
||||
created_by VARCHAR(36),
|
||||
board_id VARCHAR(36),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
INSERT INTO {{.prefix}}blocks SELECT * FROM {{.prefix}}blocks_tmp;
|
||||
|
||||
DROP TABLE {{.prefix}}blocks_tmp;
|
||||
{{end}}
|
||||
|
||||
{{- /* most block searches use board_id or a combination of board and parent ids */ -}}
|
||||
CREATE INDEX idx_blocks_board_id_parent_id ON {{.prefix}}blocks (board_id, parent_id);
|
||||
|
||||
{{- /* get subscriptions is used once per board page load */ -}}
|
||||
CREATE INDEX idx_subscriptions_subscriber_id ON {{.prefix}}subscriptions (subscriber_id);
|
@ -0,0 +1 @@
|
||||
DROP TABLE {{.prefix}}preferences;
|
@ -0,0 +1,14 @@
|
||||
create table {{.prefix}}preferences
|
||||
(
|
||||
userid varchar(36) not null,
|
||||
category varchar(32) not null,
|
||||
name varchar(32) not null,
|
||||
value text null,
|
||||
primary key (userid, category, name)
|
||||
);
|
||||
|
||||
create index idx_{{.prefix}}preferences_category
|
||||
on {{.prefix}}preferences (category);
|
||||
|
||||
create index idx_{{.prefix}}preferences_name
|
||||
on {{.prefix}}preferences (name);
|
@ -0,0 +1 @@
|
||||
SELECT 1;
|
@ -0,0 +1,53 @@
|
||||
{{if .plugin}}
|
||||
{{- /* For plugin mode, we need to write into Mattermost's `Preferences` table, hence, no use of `prefix`. */ -}}
|
||||
|
||||
{{if .postgres}}
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'welcomePageViewed', replace((props->'focalboard_welcomePageViewed')::varchar, '"', '') FROM Users WHERE props->'focalboard_welcomePageViewed' IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace((props->'hiddenBoardIDs')::varchar, '"[', '['), ']"', ']'), '\"', '"') FROM Users WHERE props->'hiddenBoardIDs' IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'tourCategory', replace((props->'focalboard_tourCategory')::varchar, '"', '') FROM Users WHERE props->'focalboard_tourCategory' IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStep', replace((props->'focalboard_onboardingTourStep')::varchar, '"', '') FROM Users WHERE props->'focalboard_onboardingTourStep' IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStarted', replace((props->'focalboard_onboardingTourStarted')::varchar, '"', '') FROM Users WHERE props->'focalboard_onboardingTourStarted' IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'version72MessageCanceled', replace((props->'focalboard_version72MessageCanceled')::varchar, '"', '') FROM Users WHERE props->'focalboard_version72MessageCanceled' IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'lastWelcomeVersion', replace((props->'focalboard_lastWelcomeVersion')::varchar, '"', '') FROM Users WHERE props->'focalboard_lastWelcomeVersion' IS NOT NULL;
|
||||
|
||||
UPDATE Users SET props = (props - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion');
|
||||
|
||||
{{else}}
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'welcomePageViewed', replace(JSON_EXTRACT(props, '$.focalboard_welcomePageViewed'), '"', '') FROM Users WHERE JSON_EXTRACT(props, '$.focalboard_welcomePageViewed') IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace(JSON_EXTRACT(props, '$.hiddenBoardIDs'), '"[', '['), ']"', ']'), '\\"', '"') FROM Users WHERE JSON_EXTRACT(props, '$.hiddenBoardIDs') IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'tourCategory', replace(JSON_EXTRACT(props, '$.focalboard_tourCategory'), '"', '') FROM Users WHERE JSON_EXTRACT(props, '$.focalboard_tourCategory') IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStep', replace(JSON_EXTRACT(props, '$.focalboard_onboardingTourStep'), '"', '') FROM Users WHERE JSON_EXTRACT(props, '$.focalboard_onboardingTourStep') IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStarted', replace(JSON_EXTRACT(props, '$.focalboard_onboardingTourStarted'), '"', '') FROM Users WHERE JSON_EXTRACT(props, '$.focalboard_onboardingTourStarted') IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'version72MessageCanceled', replace(JSON_EXTRACT(props, '$.focalboard_version72MessageCanceled'), '"', '') FROM Users WHERE JSON_EXTRACT(props, '$.focalboard_version72MessageCanceled') IS NOT NULL;
|
||||
INSERT INTO Preferences (userid, category, name, value) SELECT id, 'focalboard', 'lastWelcomeVersion', replace(JSON_EXTRACT(props, 'focalboard_lastWelcomeVersion'), '"', '') FROM Users WHERE JSON_EXTRACT(props, '$.focalboard_lastWelcomeVersion') IS NOT NULL;
|
||||
|
||||
UPDATE Users SET props = JSON_REMOVE(props, '$.focalboard_welcomePageViewed', '$.hiddenBoardIDs', '$.focalboard_tourCategory', '$.focalboard_onboardingTourStep', '$.focalboard_onboardingTourStarted', '$.focalboard_version72MessageCanceled', '$.focalboard_lastWelcomeVersion');
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{- /* For personal server, we need to write to Focalboard's preferences table, hence the use of `prefix`. */ -}}
|
||||
|
||||
{{if .postgres}}
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'welcomePageViewed', replace((props->'focalboard_welcomePageViewed')::varchar, '"', '') from {{.prefix}}users WHERE props->'focalboard_welcomePageViewed' IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace((props->'hiddenBoardIDs')::varchar, '"[', '['), ']"', ']'), '\"', '"') from {{.prefix}}users WHERE props->'hiddenBoardIDs' IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'tourCategory', replace((props->'focalboard_tourCategory')::varchar, '"', '') from {{.prefix}}users WHERE props->'focalboard_tourCategory' IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStep', replace((props->'focalboard_onboardingTourStep')::varchar, '"', '') from {{.prefix}}users WHERE props->'focalboard_onboardingTourStep' IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStarted', replace((props->'focalboard_onboardingTourStarted')::varchar, '"', '') from {{.prefix}}users WHERE props->'focalboard_onboardingTourStarted' IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'version72MessageCanceled', replace((props->'focalboard_version72MessageCanceled')::varchar, '"', '') from {{.prefix}}users WHERE props->'focalboard_version72MessageCanceled' IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'lastWelcomeVersion', replace((props->'focalboard_lastWelcomeVersion')::varchar, '"', '') from {{.prefix}}users WHERE props->'focalboard_lastWelcomeVersion' IS NOT NULL;
|
||||
|
||||
UPDATE {{.prefix}}users SET props = (props::jsonb - 'focalboard_welcomePageViewed' - 'hiddenBoardIDs' - 'focalboard_tourCategory' - 'focalboard_onboardingTourStep' - 'focalboard_onboardingTourStarted' - 'focalboard_version72MessageCanceled' - 'focalboard_lastWelcomeVersion')::json;
|
||||
|
||||
{{else}}
|
||||
{{- /* Surprisingly SQLite and MySQL have same JSON functions and syntax! */ -}}
|
||||
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'welcomePageViewed', replace(JSON_EXTRACT(props, '$.focalboard_welcomePageViewed'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.focalboard_welcomePageViewed') IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'hiddenBoardIDs', replace(replace(replace(JSON_EXTRACT(props, '$.hiddenBoardIDs'), '"[', '['), ']"', ']'), '\\"', '"') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.hiddenBoardIDs') IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'tourCategory', replace(JSON_EXTRACT(props, '$.focalboard_tourCategory'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.focalboard_tourCategory') IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStep', replace(JSON_EXTRACT(props, '$.focalboard_onboardingTourStep'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.focalboard_onboardingTourStep') IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'onboardingTourStarted', replace(JSON_EXTRACT(props, '$.focalboard_onboardingTourStarted'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.focalboard_onboardingTourStarted') IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'version72MessageCanceled', replace(JSON_EXTRACT(props, '$.focalboard_version72MessageCanceled'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.focalboard_version72MessageCanceled') IS NOT NULL;
|
||||
INSERT INTO {{.prefix}}preferences (userid, category, name, value) SELECT id, 'focalboard', 'lastWelcomeVersion', replace(JSON_EXTRACT(props, 'focalboard_lastWelcomeVersion'), '"', '') from {{.prefix}}users WHERE JSON_EXTRACT(props, '$.focalboard_lastWelcomeVersion') IS NOT NULL;
|
||||
|
||||
UPDATE {{.prefix}}users SET props = JSON_REMOVE(props, '$.focalboard_welcomePageViewed', '$.hiddenBoardIDs', '$.focalboard_tourCategory', '$.focalboard_onboardingTourStep', '$.focalboard_onboardingTourStarted', '$.focalboard_version72MessageCanceled', '$.focalboard_lastWelcomeVersion');
|
||||
{{end}}
|
||||
{{end}}
|
@ -0,0 +1,266 @@
|
||||
package migrationstests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-api/cluster"
|
||||
"github.com/mattermost/morph"
|
||||
"github.com/mattermost/morph/drivers"
|
||||
"github.com/mattermost/morph/drivers/mysql"
|
||||
"github.com/mattermost/morph/drivers/postgres"
|
||||
embedded "github.com/mattermost/morph/sources/embedded"
|
||||
"github.com/mgdelacroix/foundation"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/db"
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
mmSqlStore "github.com/mattermost/mattermost-server/v6/store/sqlstore"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mattermost/focalboard/server/services/store/sqlstore"
|
||||
)
|
||||
|
||||
var tablePrefix = "focalboard_"
|
||||
|
||||
type BoardsMigrator struct {
|
||||
withMattermostMigrations bool
|
||||
connString string
|
||||
driverName string
|
||||
db *sql.DB
|
||||
store *sqlstore.SQLStore
|
||||
morphEngine *morph.Morph
|
||||
morphDriver drivers.Driver
|
||||
}
|
||||
|
||||
func NewBoardsMigrator(withMattermostMigrations bool) *BoardsMigrator {
|
||||
return &BoardsMigrator{
|
||||
withMattermostMigrations: withMattermostMigrations,
|
||||
}
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) runMattermostMigrations() error {
|
||||
assets := db.Assets()
|
||||
assetsList, err := assets.ReadDir(filepath.Join("migrations", bm.driverName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetNames := make([]string, len(assetsList))
|
||||
for i, entry := range assetsList {
|
||||
assetNames[i] = entry.Name()
|
||||
}
|
||||
|
||||
src, err := embedded.WithInstance(&embedded.AssetSource{
|
||||
Names: assetNames,
|
||||
AssetFunc: func(name string) ([]byte, error) {
|
||||
return assets.ReadFile(filepath.Join("migrations", bm.driverName, name))
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
driver, err := bm.getDriver("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
engine, err := morph.New(context.Background(), driver, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
return engine.ApplyAll()
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) getDriver(migrationsTable string) (drivers.Driver, error) {
|
||||
migrationConfig := drivers.Config{
|
||||
StatementTimeoutInSecs: 1000000,
|
||||
MigrationsTable: migrationsTable,
|
||||
}
|
||||
|
||||
var driver drivers.Driver
|
||||
var err error
|
||||
if bm.driverName == model.PostgresDBType {
|
||||
driver, err = postgres.WithInstance(bm.db, &postgres.Config{Config: migrationConfig})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if bm.driverName == model.MysqlDBType {
|
||||
driver, err = mysql.WithInstance(bm.db, &mysql.Config{Config: migrationConfig})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) getMorphConnection() (*morph.Morph, drivers.Driver, error) {
|
||||
driver, err := bm.getDriver(fmt.Sprintf("%sschema_migrations", tablePrefix))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
assetsList, err := sqlstore.Assets.ReadDir("migrations")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
assetNamesForDriver := make([]string, len(assetsList))
|
||||
for i, dirEntry := range assetsList {
|
||||
assetNamesForDriver[i] = dirEntry.Name()
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"prefix": tablePrefix,
|
||||
"postgres": bm.driverName == model.PostgresDBType,
|
||||
"sqlite": bm.driverName == model.SqliteDBType,
|
||||
"mysql": bm.driverName == model.MysqlDBType,
|
||||
"plugin": bm.withMattermostMigrations,
|
||||
"singleUser": false,
|
||||
}
|
||||
|
||||
migrationAssets := &embedded.AssetSource{
|
||||
Names: assetNamesForDriver,
|
||||
AssetFunc: func(name string) ([]byte, error) {
|
||||
asset, mErr := sqlstore.Assets.ReadFile("migrations/" + name)
|
||||
if mErr != nil {
|
||||
return nil, mErr
|
||||
}
|
||||
|
||||
tmpl, pErr := template.New("sql").Parse(string(asset))
|
||||
if pErr != nil {
|
||||
return nil, pErr
|
||||
}
|
||||
buffer := bytes.NewBufferString("")
|
||||
|
||||
err = tmpl.Execute(buffer, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
},
|
||||
}
|
||||
|
||||
src, err := embedded.WithInstance(migrationAssets)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
engine, err := morph.New(context.Background(), driver, src)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return engine, driver, nil
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) Setup() error {
|
||||
var err error
|
||||
bm.driverName, bm.connString, err = sqlstore.PrepareNewTestDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bm.driverName == model.MysqlDBType {
|
||||
bm.connString, err = mmSqlStore.ResetReadTimeout(bm.connString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bm.connString, err = mmSqlStore.AppendMultipleStatementsFlag(bm.connString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var dbErr error
|
||||
bm.db, dbErr = sql.Open(bm.driverName, bm.connString)
|
||||
if dbErr != nil {
|
||||
return dbErr
|
||||
}
|
||||
|
||||
if err := bm.db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bm.withMattermostMigrations {
|
||||
if err := bm.runMattermostMigrations(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
storeParams := sqlstore.Params{
|
||||
DBType: bm.driverName,
|
||||
ConnectionString: bm.connString,
|
||||
TablePrefix: tablePrefix,
|
||||
Logger: mlog.CreateConsoleTestLogger(false, mlog.LvlDebug),
|
||||
DB: bm.db,
|
||||
IsPlugin: true,
|
||||
NewMutexFn: func(name string) (*cluster.Mutex, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
},
|
||||
SkipMigrations: true,
|
||||
}
|
||||
bm.store, err = sqlstore.New(storeParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
morphEngine, morphDriver, err := bm.getMorphConnection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bm.morphEngine = morphEngine
|
||||
bm.morphDriver = morphDriver
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) MigrateToStep(step int) error {
|
||||
applied, err := bm.morphDriver.AppliedMigrations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentVersion := len(applied)
|
||||
|
||||
if _, err := bm.morphEngine.Apply(step - currentVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) Interceptors() map[int]foundation.Interceptor {
|
||||
return map[int]foundation.Interceptor{
|
||||
18: bm.store.RunDeletedMembershipBoardsMigration,
|
||||
}
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) TearDown() error {
|
||||
if err := bm.morphEngine.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bm.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) DriverName() string {
|
||||
return bm.driverName
|
||||
}
|
||||
|
||||
func (bm *BoardsMigrator) DB() *sql.DB {
|
||||
return bm.db
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package migrationstests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDeletedMembershipBoardsMigration(t *testing.T) {
|
||||
t.Run("should detect a board linked to a team in which the owner has a deleted membership and restore it", func(t *testing.T) {
|
||||
th, tearDown := SetupPluginTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.f.MigrateToStepSkippingLastInterceptor(18).
|
||||
ExecFile("./fixtures/deletedMembershipBoardsMigrationFixtures.sql")
|
||||
|
||||
boardGroupChannel := struct {
|
||||
Created_By string
|
||||
Team_ID string
|
||||
}{}
|
||||
boardDirectMessage := struct {
|
||||
Created_By string
|
||||
Team_ID string
|
||||
}{}
|
||||
|
||||
th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
|
||||
require.Equal(t, "user-one", boardGroupChannel.Created_By)
|
||||
require.Equal(t, "team-one", boardGroupChannel.Team_ID)
|
||||
|
||||
th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
|
||||
require.Equal(t, "user-one", boardDirectMessage.Created_By)
|
||||
require.Equal(t, "team-one", boardDirectMessage.Team_ID)
|
||||
|
||||
th.f.RunInterceptor(18)
|
||||
|
||||
th.f.DB().Get(&boardGroupChannel, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
|
||||
require.Equal(t, "user-one", boardGroupChannel.Created_By)
|
||||
require.Equal(t, "team-three", boardGroupChannel.Team_ID)
|
||||
|
||||
th.f.DB().Get(&boardDirectMessage, "SELECT created_by, team_id FROM focalboard_boards WHERE id = 'board-group-channel'")
|
||||
require.Equal(t, "user-one", boardDirectMessage.Created_By)
|
||||
require.Equal(t, "team-three", boardDirectMessage.Team_ID)
|
||||
})
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
INSERT INTO Teams
|
||||
(Id, Name, Type, DeleteAt)
|
||||
VALUES
|
||||
('team-one', 'team-one', 'O', 0),
|
||||
('team-two', 'team-two', 'O', 0),
|
||||
('team-three', 'team-three', 'O', 0);
|
||||
|
||||
INSERT INTO Channels
|
||||
(Id, DeleteAt, TeamId, Type, Name, CreatorId)
|
||||
VALUES
|
||||
('group-channel', 0, 'team-one', 'G', 'group-channel', 'user-one'),
|
||||
('direct-channel', 0, 'team-one', 'D', 'direct-channel', 'user-one');
|
||||
|
||||
INSERT INTO Users
|
||||
(Id, Username, Email)
|
||||
VALUES
|
||||
('user-one', 'john-doe', 'john-doe@sample.com'),
|
||||
('user-two', 'jane-doe', 'jane-doe@sample.com');
|
||||
|
||||
INSERT INTO focalboard_boards
|
||||
(id, team_id, channel_id, created_by, modified_by, type, title, description, icon, show_description, is_template, create_at, update_at, delete_at)
|
||||
VALUES
|
||||
('board-group-channel', 'team-one', 'group-channel', 'user-one', 'user-one', 'P', 'Group Channel Board', '', '', false, false, 123, 123, 0),
|
||||
('board-direct-channel', 'team-one', 'direct-channel', 'user-one', 'user-one', 'P', 'Direct Channel Board', '', '', false, false, 123, 123, 0);
|
||||
|
||||
INSERT INTO focalboard_board_members
|
||||
(board_id, user_id, scheme_admin)
|
||||
VALUES
|
||||
('board-group-channel', 'user-one', true),
|
||||
('board-direct-channel', 'user-one', true);
|
||||
|
||||
INSERT INTO TeamMembers
|
||||
(TeamId, UserId, DeleteAt, SchemeAdmin)
|
||||
VALUES
|
||||
('team-one', 'user-one', 123, true),
|
||||
('team-one', 'user-two', 123, true),
|
||||
('team-two', 'user-one', 123, true),
|
||||
('team-two', 'user-two', 123, true),
|
||||
('team-three', 'user-one', 0, true),
|
||||
('team-three', 'user-two', 0, true);
|
||||
|
||||
INSERT INTO ChannelMembers
|
||||
(ChannelId, UserId, SchemeUser, SchemeAdmin)
|
||||
VALUES
|
||||
('group-channel', 'user-one', true, true),
|
||||
('group-channel', 'two-one', true, false),
|
||||
('direct-channel', 'user-one', true, true);
|
@ -0,0 +1,7 @@
|
||||
INSERT INTO Channels (Id, CreateAt, UpdateAt, DeleteAt, TeamId, Type, Name, CreatorId) VALUES ('chan-id', 123, 123, 0, 'team-id', 'O', 'channel', 'user-id');
|
||||
|
||||
INSERT INTO focalboard_blocks
|
||||
(id, workspace_id, root_id, parent_id, created_by, modified_by, type, title, create_at, update_at, delete_at)
|
||||
VALUES
|
||||
('board-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'board', 'My Board', 123, 123, 0),
|
||||
('card-id', 'chan-id', 'board-id', 'board-id', 'user-id', 'user-id', 'card', 'A card', 123, 123, 0);
|
@ -0,0 +1,45 @@
|
||||
package migrationstests
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
"github.com/mgdelacroix/foundation"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
t *testing.T
|
||||
f *foundation.Foundation
|
||||
isPlugin bool
|
||||
}
|
||||
|
||||
func SetupPluginTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
dbType := strings.TrimSpace(os.Getenv("FOCALBOARD_STORE_TEST_DB_TYPE"))
|
||||
if dbType == "" || dbType == model.SqliteDBType {
|
||||
t.Skip("Skipping plugin mode test for SQLite")
|
||||
}
|
||||
|
||||
return setupTestHelper(t, true)
|
||||
}
|
||||
|
||||
func SetupTestHelper(t *testing.T) (*TestHelper, func()) {
|
||||
return setupTestHelper(t, false)
|
||||
}
|
||||
|
||||
func setupTestHelper(t *testing.T, isPlugin bool) (*TestHelper, func()) {
|
||||
f := foundation.New(t, NewBoardsMigrator(isPlugin))
|
||||
|
||||
th := &TestHelper{
|
||||
t: t,
|
||||
f: f,
|
||||
isPlugin: isPlugin,
|
||||
}
|
||||
|
||||
tearDown := func() {
|
||||
th.f.TearDown()
|
||||
}
|
||||
|
||||
return th, tearDown
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package migrationstests
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test18AddTeamsAndBoardsSQLMigration(t *testing.T) {
|
||||
t.Run("should migrate a block of type board to the boards table", func(t *testing.T) {
|
||||
th, tearDown := SetupPluginTestHelper(t)
|
||||
defer tearDown()
|
||||
|
||||
th.f.MigrateToStep(17).
|
||||
ExecFile("./fixtures/test18AddTeamsAndBoardsSQLMigrationFixtures.sql")
|
||||
|
||||
board := struct {
|
||||
ID string
|
||||
Title string
|
||||
Type string
|
||||
}{}
|
||||
|
||||
// we check first that the board is inside the blocks table as
|
||||
// a block of board type
|
||||
err := th.f.DB().Get(&board, "SELECT id, title, type FROM focalboard_blocks WHERE id = 'board-id'")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "My Board", board.Title)
|
||||
require.Equal(t, "board", board.Type)
|
||||
|
||||
// then we run the migration
|
||||
th.f.MigrateToStep(18)
|
||||
|
||||
// we assert that the board is now a block
|
||||
bErr := th.f.DB().Get(&board, "SELECT id, title, type FROM focalboard_boards WHERE id = 'board-id'")
|
||||
require.NoError(t, bErr)
|
||||
require.Equal(t, "My Board", board.Title)
|
||||
require.Equal(t, "O", board.Type)
|
||||
|
||||
card := struct {
|
||||
Title string
|
||||
Type string
|
||||
Parent_ID string
|
||||
Board_ID string
|
||||
}{}
|
||||
|
||||
// we fetch the card to ensure that the
|
||||
cErr := th.f.DB().Get(&card, "SELECT title, type, parent_id, board_id FROM focalboard_blocks WHERE id = 'card-id'")
|
||||
require.NoError(t, cErr)
|
||||
require.Equal(t, "A card", card.Title)
|
||||
require.Equal(t, "card", card.Type)
|
||||
require.Equal(t, board.ID, card.Parent_ID)
|
||||
require.Equal(t, board.ID, card.Board_ID)
|
||||
})
|
||||
}
|
@ -25,6 +25,7 @@ type Params struct {
|
||||
IsSingleUser bool
|
||||
NewMutexFn MutexFactory
|
||||
ServicesAPI servicesAPI
|
||||
SkipMigrations bool
|
||||
}
|
||||
|
||||
func (p Params) CheckValid() error {
|
||||
|
@ -46,6 +46,11 @@ func (s *SQLStore) AddUpdateCategoryBoard(userID string, categoryID string, bloc
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) CanSeeUser(seerID string, seenID string) (bool, error) {
|
||||
return s.canSeeUser(s.db, seerID, seenID)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) CleanUpSessions(expireTime int64) error {
|
||||
return s.cleanUpSessions(s.db, expireTime)
|
||||
|
||||
@ -294,6 +299,11 @@ func (s *SQLStore) GetBlockHistoryDescendants(boardID string, opts model.QueryBl
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocks(opts model.QueryBlocksOptions) ([]model.Block, error) {
|
||||
return s.getBlocks(s.db, opts)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksByIDs(ids []string) ([]model.Block, error) {
|
||||
return s.getBlocksByIDs(s.db, ids)
|
||||
|
||||
@ -304,11 +314,6 @@ func (s *SQLStore) GetBlocksForBoard(boardID string) ([]model.Block, error) {
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksWithBoardID(boardID string) ([]model.Block, error) {
|
||||
return s.getBlocksWithBoardID(s.db, boardID)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBlocksWithParent(boardID string, parentID string) ([]model.Block, error) {
|
||||
return s.getBlocksWithParent(s.db, boardID, parentID)
|
||||
|
||||
@ -349,8 +354,8 @@ func (s *SQLStore) GetBoardMemberHistory(boardID string, userID string, limit ui
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string) ([]*model.Board, error) {
|
||||
return s.getBoardsForUserAndTeam(s.db, userID, teamID)
|
||||
func (s *SQLStore) GetBoardsForUserAndTeam(userID string, teamID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
return s.getBoardsForUserAndTeam(s.db, userID, teamID, includePublicBoards)
|
||||
|
||||
}
|
||||
|
||||
@ -519,13 +524,18 @@ func (s *SQLStore) GetUserCategoryBoards(userID string, teamID string) ([]model.
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetUserPreferences(userID string) (mmModel.Preferences, error) {
|
||||
return s.getUserPreferences(s.db, userID)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetUserTimezone(userID string) (string, error) {
|
||||
return s.getUserTimezone(s.db, userID)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) GetUsersByTeam(teamID string) ([]*model.User, error) {
|
||||
return s.getUsersByTeam(s.db, teamID)
|
||||
func (s *SQLStore) GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error) {
|
||||
return s.getUsersByTeam(s.db, teamID, asGuestID)
|
||||
|
||||
}
|
||||
|
||||
@ -712,6 +722,11 @@ func (s *SQLStore) PatchUserProps(userID string, patch model.UserPropPatch) erro
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) PostMessage(message string, postType string, channelID string) error {
|
||||
return s.postMessage(s.db, message, postType, channelID)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) RefreshSession(session *model.Session) error {
|
||||
return s.refreshSession(s.db, session)
|
||||
|
||||
@ -756,8 +771,8 @@ func (s *SQLStore) SaveMember(bm *model.BoardMember) (*model.BoardMember, error)
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) SearchBoardsForUser(term string, userID string) ([]*model.Board, error) {
|
||||
return s.searchBoardsForUser(s.db, term, userID)
|
||||
func (s *SQLStore) SearchBoardsForUser(term string, userID string, includePublicBoards bool) ([]*model.Board, error) {
|
||||
return s.searchBoardsForUser(s.db, term, userID, includePublicBoards)
|
||||
|
||||
}
|
||||
|
||||
@ -771,8 +786,8 @@ func (s *SQLStore) SearchUserChannels(teamID string, userID string, query string
|
||||
|
||||
}
|
||||
|
||||
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error) {
|
||||
return s.searchUsersByTeam(s.db, teamID, searchQuery)
|
||||
func (s *SQLStore) SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error) {
|
||||
return s.searchUsersByTeam(s.db, teamID, searchQuery, asGuestID)
|
||||
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,10 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
@ -15,6 +17,9 @@ import (
|
||||
"github.com/mattermost/mattermost-server/v6/shared/mlog"
|
||||
)
|
||||
|
||||
//nolint:lll
|
||||
var ErrInvalidMariaDB = errors.New("MariaDB database is not supported, you can find more information at https://docs.mattermost.com/install/software-hardware-requirements.html#database-software")
|
||||
|
||||
// SQLStore is a SQL database.
|
||||
type SQLStore struct {
|
||||
db *sql.DB
|
||||
@ -53,6 +58,10 @@ func New(params Params) (*SQLStore, error) {
|
||||
servicesAPI: params.ServicesAPI,
|
||||
}
|
||||
|
||||
if store.IsMariaDB() {
|
||||
return nil, ErrInvalidMariaDB
|
||||
}
|
||||
|
||||
var err error
|
||||
store.isBinaryParam, err = store.computeBinaryParam()
|
||||
if err != nil {
|
||||
@ -60,15 +69,32 @@ func New(params Params) (*SQLStore, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = store.Migrate()
|
||||
if err != nil {
|
||||
params.Logger.Error(`Table creation / migration failed`, mlog.Err(err))
|
||||
if !params.SkipMigrations {
|
||||
if mErr := store.Migrate(); mErr != nil {
|
||||
params.Logger.Error(`Table creation / migration failed`, mlog.Err(mErr))
|
||||
|
||||
return nil, err
|
||||
return nil, mErr
|
||||
}
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) IsMariaDB() bool {
|
||||
if s.dbType != model.MysqlDBType {
|
||||
return false
|
||||
}
|
||||
|
||||
row := s.db.QueryRow("SELECT Version()")
|
||||
|
||||
var version string
|
||||
if err := row.Scan(&version); err != nil {
|
||||
s.logger.Error("error checking database version", mlog.Err(err))
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(strings.ToLower(version), "mariadb")
|
||||
}
|
||||
|
||||
// computeBinaryParam returns whether the data source uses binary_parameters
|
||||
// when using Postgres.
|
||||
func (s *SQLStore) computeBinaryParam() (bool, error) {
|
||||
|
@ -6,6 +6,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
mmModel "github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/store"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/mattermost/focalboard/server/model"
|
||||
@ -211,11 +214,11 @@ func (s *SQLStore) updateUserPasswordByID(db sq.BaseRunner, userID, password str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string) ([]*model.User, error) {
|
||||
func (s *SQLStore) getUsersByTeam(db sq.BaseRunner, _ string, _ string) ([]*model.User, error) {
|
||||
return s.getUsersByCondition(db, nil, 0)
|
||||
}
|
||||
|
||||
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string) ([]*model.User, error) {
|
||||
func (s *SQLStore) searchUsersByTeam(db sq.BaseRunner, _ string, searchQuery string, _ string) ([]*model.User, error) {
|
||||
return s.getUsersByCondition(db, &sq.Like{"username": "%" + searchQuery + "%"}, 10)
|
||||
}
|
||||
|
||||
@ -255,30 +258,137 @@ func (s *SQLStore) usersFromRows(rows *sql.Rows) ([]*model.User, error) {
|
||||
}
|
||||
|
||||
func (s *SQLStore) patchUserProps(db sq.BaseRunner, userID string, patch model.UserPropPatch) error {
|
||||
user, err := s.getUserByID(db, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
if len(patch.UpdatedFields) > 0 {
|
||||
for key, value := range patch.UpdatedFields {
|
||||
preference := mmModel.Preference{
|
||||
UserId: userID,
|
||||
Category: model.PreferencesCategoryFocalboard,
|
||||
Name: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
if err := s.updateUserProps(db, preference); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if user.Props == nil {
|
||||
user.Props = map[string]interface{}{}
|
||||
if len(patch.DeletedFields) > 0 {
|
||||
for _, key := range patch.DeletedFields {
|
||||
preference := mmModel.Preference{
|
||||
UserId: userID,
|
||||
Category: model.PreferencesCategoryFocalboard,
|
||||
Name: key,
|
||||
}
|
||||
|
||||
if err := s.deleteUserProps(db, preference); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range patch.DeletedFields {
|
||||
delete(user.Props, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) updateUserProps(db sq.BaseRunner, preference mmModel.Preference) error {
|
||||
query := s.getQueryBuilder(db).
|
||||
Insert(s.tablePrefix+"preferences").
|
||||
Columns("UserId", "Category", "Name", "Value").
|
||||
Values(preference.UserId, preference.Category, preference.Name, preference.Value)
|
||||
|
||||
switch s.dbType {
|
||||
case model.MysqlDBType:
|
||||
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE Value = ?", preference.Value))
|
||||
case model.PostgresDBType:
|
||||
query = query.SuffixExpr(sq.Expr("ON CONFLICT (userid, category, name) DO UPDATE SET Value = ?", preference.Value))
|
||||
case model.SqliteDBType:
|
||||
query = query.SuffixExpr(sq.Expr(" on conflict(userid, category, name) do update set value = excluded.value"))
|
||||
default:
|
||||
return store.NewErrNotImplemented("failed to update preference because of missing driver")
|
||||
}
|
||||
|
||||
for key, value := range patch.UpdatedFields {
|
||||
user.Props[key] = value
|
||||
if _, err := query.Exec(); err != nil {
|
||||
return fmt.Errorf("failed to upsert user preference in database: userID: %s name: %s value: %s error: %w", preference.UserId, preference.Name, preference.Value, err)
|
||||
}
|
||||
|
||||
return s.updateUser(db, user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) deleteUserProps(db sq.BaseRunner, preference mmModel.Preference) error {
|
||||
query := s.getQueryBuilder(db).
|
||||
Delete(s.tablePrefix + "preferences").
|
||||
Where(sq.Eq{"UserId": preference.UserId}).
|
||||
Where(sq.Eq{"Category": preference.Category}).
|
||||
Where(sq.Eq{"Name": preference.Name})
|
||||
|
||||
if _, err := query.Exec(); err != nil {
|
||||
return fmt.Errorf("failed to delete user preference from database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) canSeeUser(db sq.BaseRunner, seerID string, seenID string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) sendMessage(db sq.BaseRunner, message, postType string, receipts []string) error {
|
||||
return errUnsupportedOperation
|
||||
}
|
||||
|
||||
func (s *SQLStore) postMessage(db sq.BaseRunner, message, postType string, channel string) error {
|
||||
return errUnsupportedOperation
|
||||
}
|
||||
|
||||
func (s *SQLStore) getUserTimezone(_ sq.BaseRunner, _ string) (string, error) {
|
||||
return "", errUnsupportedOperation
|
||||
}
|
||||
|
||||
func (s *SQLStore) getUserPreferences(db sq.BaseRunner, userID string) (mmModel.Preferences, error) {
|
||||
query := s.getQueryBuilder(db).
|
||||
Select("userid", "category", "name", "value").
|
||||
From(s.tablePrefix + "preferences").
|
||||
Where(sq.Eq{
|
||||
"userid": userID,
|
||||
"category": model.PreferencesCategoryFocalboard,
|
||||
})
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
s.logger.Error("failed to fetch user preferences", mlog.String("user_id", userID), mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
preferences, err := s.preferencesFromRows(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return preferences, nil
|
||||
}
|
||||
|
||||
func (s *SQLStore) preferencesFromRows(rows *sql.Rows) ([]mmModel.Preference, error) {
|
||||
preferences := []mmModel.Preference{}
|
||||
|
||||
for rows.Next() {
|
||||
var preference mmModel.Preference
|
||||
|
||||
err := rows.Scan(
|
||||
&preference.UserId,
|
||||
&preference.Category,
|
||||
&preference.Name,
|
||||
&preference.Value,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("failed to scan row for user preference", mlog.Err(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preferences = append(preferences, preference)
|
||||
}
|
||||
|
||||
return preferences, nil
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:generate mockgen --build_flags=--mod=mod -destination=mockstore/mockstore.go -package mockstore . Store
|
||||
//go:generate mockgen -destination=mockstore/mockstore.go -package mockstore . Store
|
||||
//go:generate go run ./generators/main.go
|
||||
package store
|
||||
|
||||
@ -14,10 +14,10 @@ const CardLimitTimestampSystemKey = "card_limit_timestamp"
|
||||
|
||||
// Store represents the abstraction of the data storage.
|
||||
type Store interface {
|
||||
GetBlocks(opts model.QueryBlocksOptions) ([]model.Block, error)
|
||||
GetBlocksWithParentAndType(boardID, parentID string, blockType string) ([]model.Block, error)
|
||||
GetBlocksWithParent(boardID, parentID string) ([]model.Block, error)
|
||||
GetBlocksByIDs(ids []string) ([]model.Block, error)
|
||||
GetBlocksWithBoardID(boardID string) ([]model.Block, error)
|
||||
GetBlocksWithType(boardID, blockType string) ([]model.Block, error)
|
||||
GetSubTree2(boardID, blockID string, opts model.QuerySubtreeOptions) ([]model.Block, error)
|
||||
GetBlocksForBoard(boardID string) ([]model.Block, error)
|
||||
@ -62,9 +62,10 @@ type Store interface {
|
||||
UpdateUser(user *model.User) error
|
||||
UpdateUserPassword(username, password string) error
|
||||
UpdateUserPasswordByID(userID, password string) error
|
||||
GetUsersByTeam(teamID string) ([]*model.User, error)
|
||||
SearchUsersByTeam(teamID string, searchQuery string) ([]*model.User, error)
|
||||
GetUsersByTeam(teamID string, asGuestID string) ([]*model.User, error)
|
||||
SearchUsersByTeam(teamID string, searchQuery string, asGuestID string) ([]*model.User, error)
|
||||
PatchUserProps(userID string, patch model.UserPropPatch) error
|
||||
GetUserPreferences(userID string) (mmModel.Preferences, error)
|
||||
|
||||
GetActiveUserCount(updatedSecondsAgo int64) (int, error)
|
||||
GetSession(token string, expireTime int64) (*model.Session, error)
|
||||
@ -90,7 +91,7 @@ type Store interface {
|
||||
// @withTransaction
|
||||
PatchBoard(boardID string, boardPatch *model.BoardPatch, userID string) (*model.Board, error)
|
||||
GetBoard(id string) (*model.Board, error)
|
||||
GetBoardsForUserAndTeam(userID, teamID string) ([]*model.Board, error)
|
||||
GetBoardsForUserAndTeam(userID, teamID string, includePublicBoards bool) ([]*model.Board, error)
|
||||
GetBoardsInTeamByIds(boardIDs []string, teamID string) ([]*model.Board, error)
|
||||
// @withTransaction
|
||||
DeleteBoard(boardID, userID string) error
|
||||
@ -101,7 +102,8 @@ type Store interface {
|
||||
GetBoardMemberHistory(boardID, userID string, limit uint64) ([]*model.BoardMemberHistoryEntry, error)
|
||||
GetMembersForBoard(boardID string) ([]*model.BoardMember, error)
|
||||
GetMembersForUser(userID string) ([]*model.BoardMember, error)
|
||||
SearchBoardsForUser(term, userID string) ([]*model.Board, error)
|
||||
CanSeeUser(seerID string, seenID string) (bool, error)
|
||||
SearchBoardsForUser(term, userID string, includePublicBoards bool) ([]*model.Board, error)
|
||||
SearchBoardsForUserInTeam(teamID, term, userID string) ([]*model.Board, error)
|
||||
|
||||
// @withTransaction
|
||||
@ -155,6 +157,7 @@ type Store interface {
|
||||
GetCloudLimits() (*mmModel.ProductLimits, error)
|
||||
SearchUserChannels(teamID, userID, query string) ([]*mmModel.Channel, error)
|
||||
GetChannel(teamID, channelID string) (*mmModel.Channel, error)
|
||||
PostMessage(message, postType, channelID string) error
|
||||
SendMessage(message, postType string, receipts []string) error
|
||||
|
||||
// Insights
|
||||
|
@ -266,20 +266,6 @@ func testPatchBlock(t *testing.T, store store.Store) {
|
||||
require.Len(t, blocks, initialCount)
|
||||
})
|
||||
|
||||
t.Run("invalid rootid", func(t *testing.T) {
|
||||
wrongBoardID := ""
|
||||
blockPatch := model.BlockPatch{
|
||||
BoardID: &wrongBoardID,
|
||||
}
|
||||
|
||||
err := store.PatchBlock("id-test", &blockPatch, "user-id-1")
|
||||
require.Error(t, err)
|
||||
|
||||
blocks, err := store.GetBlocksForBoard(boardID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, initialCount)
|
||||
})
|
||||
|
||||
t.Run("invalid fields data", func(t *testing.T) {
|
||||
blockPatch := model.BlockPatch{
|
||||
UpdatedFields: map[string]interface{}{"no-serialiable-value": t.Run},
|
||||
@ -753,14 +739,14 @@ func testGetBlocks(t *testing.T, store store.Store) {
|
||||
|
||||
t.Run("not existing board", func(t *testing.T) {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
blocks, err = store.GetBlocksWithBoardID("not-exists")
|
||||
blocks, err = store.GetBlocksForBoard("not-exists")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 0)
|
||||
})
|
||||
|
||||
t.Run("all blocks of the a board", func(t *testing.T) {
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
blocks, err = store.GetBlocksWithBoardID(boardID)
|
||||
blocks, err = store.GetBlocksForBoard(boardID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 5)
|
||||
})
|
||||
@ -966,12 +952,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
||||
})
|
||||
|
||||
t.Run("get block history after updateAt", func(t *testing.T) {
|
||||
rBlocks, err2 := store.GetBlocksWithType(boardID, "test")
|
||||
rBlock, err2 := store.GetBlock("block3")
|
||||
require.NoError(t, err2)
|
||||
require.NotZero(t, rBlocks[2].UpdateAt)
|
||||
require.NotZero(t, rBlock.UpdateAt)
|
||||
|
||||
opts := model.QueryBlockHistoryOptions{
|
||||
AfterUpdateAt: rBlocks[2].UpdateAt,
|
||||
AfterUpdateAt: rBlock.UpdateAt,
|
||||
Descending: false,
|
||||
}
|
||||
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
||||
@ -984,12 +970,12 @@ func testGetBlockMetadata(t *testing.T, store store.Store) {
|
||||
})
|
||||
|
||||
t.Run("get block history before updateAt", func(t *testing.T) {
|
||||
rBlocks, err2 := store.GetBlocksWithType(boardID, "test")
|
||||
rBlock, err2 := store.GetBlock("block3")
|
||||
require.NoError(t, err2)
|
||||
require.NotZero(t, rBlocks[2].UpdateAt)
|
||||
require.NotZero(t, rBlock.UpdateAt)
|
||||
|
||||
opts := model.QueryBlockHistoryOptions{
|
||||
BeforeUpdateAt: rBlocks[2].UpdateAt,
|
||||
BeforeUpdateAt: rBlock.UpdateAt,
|
||||
Descending: true,
|
||||
}
|
||||
blocks, err = store.GetBlockHistoryDescendants(boardID, opts)
|
||||
|
@ -68,8 +68,8 @@ func getBoardsInsightsTest(t *testing.T, store store.Store) {
|
||||
|
||||
_, _ = store.SaveMember(bm)
|
||||
|
||||
boardsUser1, _ := store.GetBoardsForUserAndTeam(testUserID, testTeamID)
|
||||
boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID)
|
||||
boardsUser1, _ := store.GetBoardsForUserAndTeam(testUserID, testTeamID, true)
|
||||
boardsUser2, _ := store.GetBoardsForUserAndTeam(testInsightsUserID1, testTeamID, true)
|
||||
t.Run("team insights", func(t *testing.T) {
|
||||
boardIDs := []string{boardsUser1[0].ID, boardsUser1[1].ID, boardsUser1[2].ID}
|
||||
topTeamBoards, err := store.GetTeamBoardsInsights(testTeamID, testUserID,
|
||||
|
@ -168,7 +168,7 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("should only find the two boards that the user is a member of for team 1 plus the one open board", func(t *testing.T) {
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1)
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, true)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*model.Board{
|
||||
rBoard1,
|
||||
@ -177,8 +177,17 @@ func testGetBoardsForUserAndTeam(t *testing.T, store store.Store) {
|
||||
}, boards)
|
||||
})
|
||||
|
||||
t.Run("should only find the two boards that the user is a member of for team 1", func(t *testing.T) {
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID1, false)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []*model.Board{
|
||||
rBoard1,
|
||||
rBoard2,
|
||||
}, boards)
|
||||
})
|
||||
|
||||
t.Run("should only find the board that the user is a member of for team 2", func(t *testing.T) {
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID2)
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID2, true)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, boards, 1)
|
||||
require.Equal(t, board5.ID, boards[0].ID)
|
||||
@ -688,7 +697,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
||||
userID := "user-id-1"
|
||||
|
||||
t.Run("should return empty if user is not a member of any board and there are no public boards on the team", func(t *testing.T) {
|
||||
boards, err := store.SearchBoardsForUser("", userID)
|
||||
boards, err := store.SearchBoardsForUser("", userID, true)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, boards)
|
||||
})
|
||||
@ -743,6 +752,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
||||
TeamID string
|
||||
UserID string
|
||||
Term string
|
||||
IncludePublic bool
|
||||
ExpectedBoardIDs []string
|
||||
}{
|
||||
{
|
||||
@ -750,6 +760,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
||||
TeamID: teamID1,
|
||||
UserID: userID,
|
||||
Term: "",
|
||||
IncludePublic: true,
|
||||
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
|
||||
},
|
||||
{
|
||||
@ -757,13 +768,23 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
||||
TeamID: teamID1,
|
||||
UserID: userID,
|
||||
Term: "board",
|
||||
IncludePublic: true,
|
||||
ExpectedBoardIDs: []string{board1.ID, board2.ID, board3.ID, board5.ID},
|
||||
},
|
||||
{
|
||||
Name: "should find all with term board where the user is member of",
|
||||
TeamID: teamID1,
|
||||
UserID: userID,
|
||||
Term: "board",
|
||||
IncludePublic: false,
|
||||
ExpectedBoardIDs: []string{board1.ID, board3.ID, board5.ID},
|
||||
},
|
||||
{
|
||||
Name: "should find only public as per the term, wether user is a member or not",
|
||||
TeamID: teamID1,
|
||||
UserID: userID,
|
||||
Term: "public",
|
||||
IncludePublic: true,
|
||||
ExpectedBoardIDs: []string{board1.ID, board2.ID, board5.ID},
|
||||
},
|
||||
{
|
||||
@ -771,6 +792,7 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
||||
TeamID: teamID1,
|
||||
UserID: userID,
|
||||
Term: "priv",
|
||||
IncludePublic: true,
|
||||
ExpectedBoardIDs: []string{board3.ID},
|
||||
},
|
||||
{
|
||||
@ -778,13 +800,14 @@ func testSearchBoardsForUser(t *testing.T, store store.Store) {
|
||||
TeamID: teamID2,
|
||||
UserID: userID,
|
||||
Term: "non-matching-term",
|
||||
IncludePublic: true,
|
||||
ExpectedBoardIDs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
boards, err := store.SearchBoardsForUser(tc.Term, tc.UserID)
|
||||
boards, err := store.SearchBoardsForUser(tc.Term, tc.UserID, tc.IncludePublic)
|
||||
require.NoError(t, err)
|
||||
|
||||
boardIDs := []string{}
|
||||
|
@ -41,7 +41,7 @@ func testCreateBoardsAndBlocks(t *testing.T, store store.Store) {
|
||||
teamID := testTeamID
|
||||
userID := testUserID
|
||||
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID)
|
||||
boards, err := store.GetBoardsForUserAndTeam(userID, teamID, true)
|
||||
require.Nil(t, err)
|
||||
require.Empty(t, boards)
|
||||
|
||||
|
@ -100,7 +100,7 @@ func LoadData(t *testing.T, store store.Store) {
|
||||
func testRunDataRetention(t *testing.T, store store.Store, batchSize int) {
|
||||
LoadData(t, store)
|
||||
|
||||
blocks, err := store.GetBlocksWithBoardID(boardID)
|
||||
blocks, err := store.GetBlocksForBoard(boardID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, blocks, 4)
|
||||
initialCount := len(blocks)
|
||||
@ -117,7 +117,7 @@ func testRunDataRetention(t *testing.T, store store.Store, batchSize int) {
|
||||
require.True(t, deletions > int64(initialCount))
|
||||
|
||||
// expect all blocks to be deleted.
|
||||
blocks, errBlocks := store.GetBlocksWithBoardID(boardID)
|
||||
blocks, errBlocks := store.GetBlocksForBoard(boardID)
|
||||
require.NoError(t, errBlocks)
|
||||
require.Equal(t, 0, len(blocks))
|
||||
|
||||
|
@ -27,7 +27,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
|
||||
testCreateAndGetUser(t, store)
|
||||
})
|
||||
|
||||
t.Run("CreateAndUpateUser", func(t *testing.T) {
|
||||
t.Run("CreateAndUpdateUser", func(t *testing.T) {
|
||||
store, tearDown := setup(t)
|
||||
defer tearDown()
|
||||
testCreateAndUpdateUser(t, store)
|
||||
@ -47,7 +47,7 @@ func StoreTestUserStore(t *testing.T, setup func(t *testing.T) (store.Store, fun
|
||||
|
||||
func testGetTeamUsers(t *testing.T, store store.Store) {
|
||||
t.Run("GetTeamUSers", func(t *testing.T) {
|
||||
users, err := store.GetUsersByTeam("team_1")
|
||||
users, err := store.GetUsersByTeam("team_1", "")
|
||||
require.Equal(t, 0, len(users))
|
||||
require.True(t, model.IsErrNotFound(err), "Should be ErrNotFound compatible error")
|
||||
|
||||
@ -66,7 +66,7 @@ func testGetTeamUsers(t *testing.T, store store.Store) {
|
||||
})
|
||||
}()
|
||||
|
||||
users, err = store.GetUsersByTeam("team_1")
|
||||
users, err = store.GetUsersByTeam("team_1", "")
|
||||
require.Equal(t, 1, len(users))
|
||||
require.Equal(t, "darth.vader", users[0].Username)
|
||||
require.NoError(t, err)
|
||||
@ -176,53 +176,81 @@ func testPatchUserProps(t *testing.T, store store.Store) {
|
||||
err := store.CreateUser(user)
|
||||
require.NoError(t, err)
|
||||
|
||||
key1 := "new_key_1"
|
||||
key2 := "new_key_2"
|
||||
key3 := "new_key_3"
|
||||
|
||||
// Only update props
|
||||
patch := model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
"new_key_1": "new_value_1",
|
||||
"new_key_2": "new_value_2",
|
||||
"new_key_3": "new_value_3",
|
||||
key1: "new_value_1",
|
||||
key2: "new_value_2",
|
||||
key3: "new_value_3",
|
||||
},
|
||||
}
|
||||
|
||||
err = store.PatchUserProps(user.ID, patch)
|
||||
require.NoError(t, err)
|
||||
fetchedUser, err := store.GetUserByID(user.ID)
|
||||
userPreferences, err := store.GetUserPreferences(user.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fetchedUser.Props["new_key_1"], "new_value_1")
|
||||
require.Equal(t, fetchedUser.Props["new_key_2"], "new_value_2")
|
||||
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3")
|
||||
require.Equal(t, 3, len(userPreferences))
|
||||
|
||||
for _, preference := range userPreferences {
|
||||
switch preference.Name {
|
||||
case key1:
|
||||
require.Equal(t, "new_value_1", preference.Value)
|
||||
case key2:
|
||||
require.Equal(t, "new_value_2", preference.Value)
|
||||
case key3:
|
||||
require.Equal(t, "new_value_3", preference.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a prop
|
||||
patch = model.UserPropPatch{
|
||||
DeletedFields: []string{
|
||||
"new_key_1",
|
||||
key1,
|
||||
},
|
||||
}
|
||||
|
||||
err = store.PatchUserProps(user.ID, patch)
|
||||
require.NoError(t, err)
|
||||
fetchedUser, err = store.GetUserByID(user.ID)
|
||||
userPreferences, err = store.GetUserPreferences(user.ID)
|
||||
require.NoError(t, err)
|
||||
_, ok := fetchedUser.Props["new_key_1"]
|
||||
require.False(t, ok)
|
||||
require.Equal(t, fetchedUser.Props["new_key_2"], "new_value_2")
|
||||
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3")
|
||||
|
||||
for _, preference := range userPreferences {
|
||||
switch preference.Name {
|
||||
case key1:
|
||||
t.Errorf("new_key_1 shouldn't exist in user preference as we just deleted it")
|
||||
case key2:
|
||||
require.Equal(t, "new_value_2", preference.Value)
|
||||
case key3:
|
||||
require.Equal(t, "new_value_3", preference.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// update and delete together
|
||||
patch = model.UserPropPatch{
|
||||
UpdatedFields: map[string]string{
|
||||
"new_key_3": "new_value_3_new_again",
|
||||
key3: "new_value_3_new_again",
|
||||
},
|
||||
DeletedFields: []string{
|
||||
"new_key_2",
|
||||
key2,
|
||||
},
|
||||
}
|
||||
err = store.PatchUserProps(user.ID, patch)
|
||||
require.NoError(t, err)
|
||||
fetchedUser, err = store.GetUserByID(user.ID)
|
||||
userPreferences, err = store.GetUserPreferences(user.ID)
|
||||
require.NoError(t, err)
|
||||
_, ok = fetchedUser.Props["new_key_2"]
|
||||
require.False(t, ok)
|
||||
require.Equal(t, fetchedUser.Props["new_key_3"], "new_value_3_new_again")
|
||||
|
||||
for _, preference := range userPreferences {
|
||||
switch preference.Name {
|
||||
case key1:
|
||||
t.Errorf("new_key_1 shouldn't exist in user preference as we just deleted it")
|
||||
case key2:
|
||||
t.Errorf("new_key_2 shouldn't exist in user preference as we just deleted it")
|
||||
case key3:
|
||||
require.Equal(t, "new_value_3_new_again", preference.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
21
server/swagger/swagger.yml
generated
21
server/swagger/swagger.yml
generated
@ -557,6 +557,27 @@ definitions:
|
||||
x-go-name: ErrorCode
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
Preference:
|
||||
description: Preference represents a single user preference. A user can have multiple preferences.
|
||||
properties:
|
||||
user_id:
|
||||
description: The associated user's ID
|
||||
type: string
|
||||
x-go-name: UserId
|
||||
Category:
|
||||
description: The category of preference. Its always "Focalboard" for this project
|
||||
type: string
|
||||
x-go-name: Category
|
||||
Name:
|
||||
description: Preference's name
|
||||
type: string
|
||||
x-go-name: Name
|
||||
value:
|
||||
description: Preference's value
|
||||
type: string
|
||||
x-go-name: Value
|
||||
type: object
|
||||
x-go-package: github.com/mattermost/focalboard/server/model
|
||||
FileUploadResponse:
|
||||
description: FileUploadResponse is the response to a file upload
|
||||
properties:
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockstore.go -package mocks . Store
|
||||
//go:generate mockgen -destination=mocks/mockstore.go -package mocks . Store
|
||||
package ws
|
||||
|
||||
import (
|
||||
|
@ -457,6 +457,21 @@ func (mr *MockAPIMockRecorder) EnablePlugin(arg0 interface{}) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnablePlugin", reflect.TypeOf((*MockAPI)(nil).EnablePlugin), arg0)
|
||||
}
|
||||
|
||||
// EnsureBotUser mocks base method.
|
||||
func (m *MockAPI) EnsureBotUser(arg0 *model.Bot) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "EnsureBotUser", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// EnsureBotUser indicates an expected call of EnsureBotUser.
|
||||
func (mr *MockAPIMockRecorder) EnsureBotUser(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureBotUser", reflect.TypeOf((*MockAPI)(nil).EnsureBotUser), arg0)
|
||||
}
|
||||
|
||||
// ExecuteSlashCommand mocks base method.
|
||||
func (m *MockAPI) ExecuteSlashCommand(arg0 *model.CommandArgs) (*model.CommandResponse, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@ -681,6 +696,21 @@ func (mr *MockAPIMockRecorder) GetChannelsForTeamForUser(arg0, arg1, arg2 interf
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChannelsForTeamForUser", reflect.TypeOf((*MockAPI)(nil).GetChannelsForTeamForUser), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// GetCloudLimits mocks base method.
|
||||
func (m *MockAPI) GetCloudLimits() (*model.ProductLimits, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetCloudLimits")
|
||||
ret0, _ := ret[0].(*model.ProductLimits)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetCloudLimits indicates an expected call of GetCloudLimits.
|
||||
func (mr *MockAPIMockRecorder) GetCloudLimits() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCloudLimits", reflect.TypeOf((*MockAPI)(nil).GetCloudLimits))
|
||||
}
|
||||
|
||||
// GetCommand mocks base method.
|
||||
func (m *MockAPI) GetCommand(arg0 string) (*model.Command, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -1,4 +1,4 @@
|
||||
//go:generate mockgen --build_flags=--mod=mod -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||
//go:generate mockgen -destination=mocks/mockpluginapi.go -package mocks github.com/mattermost/mattermost-server/v6/plugin API
|
||||
package ws
|
||||
|
||||
import (
|
||||
|
@ -87,7 +87,7 @@ Cypress.Commands.add('apiResetBoards', () => {
|
||||
Cypress.Commands.add('apiSkipTour', (userID: string) => {
|
||||
const body: UserConfigPatch = {
|
||||
updatedFields: {
|
||||
focalboard_welcomePageViewed: '1',
|
||||
welcomePageViewed: '1',
|
||||
[versionProperty]: 'true',
|
||||
},
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user