1
0
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:
Harrison Healey 2022-08-30 16:47:29 -04:00
commit 9c16d94854
295 changed files with 10690 additions and 4179 deletions

View File

@ -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 ./...

View File

@ -1,6 +1,8 @@
run:
timeout: 5m
modules-download-mode: readonly
skip-files:
- product/boards_product.go
linters-settings:
gofmt:

View File

@ -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; \

View File

@ -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{}

View File

@ -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)

View File

@ -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{}

View File

@ -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

View File

@ -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()
}

View File

@ -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"
>

View File

@ -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

View File

@ -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"

View File

@ -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;

View File

@ -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> => {

View File

@ -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.

View File

@ -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;

View File

@ -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'

View File

@ -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>

View File

@ -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 {

View File

@ -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(

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()
}

View File

@ -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
})

View File

@ -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)
})
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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")
}

View File

@ -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"})

View File

@ -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"

View File

@ -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) {

View File

@ -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 (

View File

@ -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)
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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: ""}
)

View File

@ -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
}

View File

@ -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

View File

@ -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),
)
}

View File

@ -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:

View File

@ -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:

View File

@ -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 (

View File

@ -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()

View File

@ -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.

View File

@ -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
}

View File

@ -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.

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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() }()

View File

@ -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)
}

View File

@ -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)
})
}
}

View File

@ -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}}

View File

@ -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);

View File

@ -0,0 +1 @@
DROP TABLE {{.prefix}}preferences;

View File

@ -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);

View File

@ -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}}

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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);

View File

@ -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);

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -25,6 +25,7 @@ type Params struct {
IsSingleUser bool
NewMutexFn MutexFactory
ServicesAPI servicesAPI
SkipMigrations bool
}
func (p Params) CheckValid() error {

View File

@ -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)
}

View File

@ -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) {

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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{}

View File

@ -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)

View File

@ -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))

View File

@ -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)
}
}
}

View File

@ -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:

View File

@ -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 (

View File

@ -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()

View File

@ -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 (

View File

@ -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