Skip to content
This repository was archived by the owner on May 18, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion server/api/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,15 @@ func (a *API) handleArchiveExportBoard(w http.ResponseWriter, r *http.Request) {
boardID := vars["boardID"]
userID := getUserID(r)

// Check if user is admin
isAdmin, err := a.isHardcodedAdmin(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}

// check user has permission to board
if !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
if !isAdmin && !a.permissions.HasPermissionToBoard(userID, boardID, model.PermissionViewBoard) {
// if this user has `manage_system` permission and there is a license with the compliance
// feature enabled, then we will allow the export.
license := a.app.GetLicense()
Expand Down
90 changes: 77 additions & 13 deletions server/api/boards.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

func (a *API) registerBoardsRoutes(r *mux.Router) {
r.HandleFunc("/teams/{teamID}/boards", a.sessionRequired(a.handleGetBoards)).Methods("GET")
r.HandleFunc("/teams/{teamID}/boards/admin", a.sessionRequired(a.handleGetAllBoardsForTeamAdmin)).Methods("GET")
r.HandleFunc("/boards", a.sessionRequired(a.handleCreateBoard)).Methods("POST")
r.HandleFunc("/boards/{boardID}", a.attachSession(a.handleGetBoard, false)).Methods("GET")
r.HandleFunc("/boards/{boardID}", a.sessionRequired(a.handlePatchBoard)).Methods("PATCH")
Expand Down Expand Up @@ -63,24 +64,13 @@ func (a *API) handleGetBoards(w http.ResponseWriter, r *http.Request) {
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)

isAdmin, err := a.isHardcodedAdmin(userID)
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}

var boards []*model.Board
if isAdmin {
// Admin sees all boards for the team
boards, err = a.app.GetAllBoardsForTeam(teamID)
} else {
isGuest, err := a.userIsGuest(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}
boards, err = a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)
}
boards, err := a.app.GetBoardsForUserAndTeam(userID, teamID, !isGuest)

if err != nil {
a.errorResponse(w, r, err)
Expand Down Expand Up @@ -684,3 +674,77 @@ func (a *API) handleGetBoardMetadata(w http.ResponseWriter, r *http.Request) {

auditRec.Success()
}

func (a *API) handleGetAllBoardsForTeamAdmin(w http.ResponseWriter, r *http.Request) {
// swagger:operation GET /teams/{teamID}/boards/admin getAllBoardsAdmin
//
// Returns all team boards for admin
//
// ---
// produces:
// - application/json
// parameters:
// - name: teamID
// in: path
// description: Team ID
// required: true
// type: string
// security:
// - BearerAuth: []
// responses:
// '200':
// description: success
// schema:
// type: array
// items:
// "$ref": "#/definitions/Board"
// '403':
// description: access denied
// default:
// description: internal error
// schema:
// "$ref": "#/definitions/ErrorResponse"

teamID := mux.Vars(r)["teamID"]
userID := getUserID(r)

// Check if user is admin
isAdmin, err := a.isHardcodedAdmin(userID)
if err != nil {
a.errorResponse(w, r, err)
return
}

if !isAdmin {
a.errorResponse(w, r, model.NewErrPermission("access denied - admin only"))
return
}

auditRec := a.makeAuditRecord(r, "getAllBoardsAdmin", audit.Fail)
defer a.audit.LogRecord(audit.LevelRead, auditRec)
auditRec.AddMeta("teamID", teamID)

// Get all boards for team
boards, err := a.app.GetAllBoardsForTeam(teamID)
if err != nil {
a.errorResponse(w, r, err)
return
}

a.logger.Debug("GetAllBoardsAdmin",
mlog.String("teamID", teamID),
mlog.String("adminUserID", userID),
mlog.Int("boardsCount", len(boards)),
)

data, err := json.Marshal(boards)
if err != nil {
a.errorResponse(w, r, err)
return
}

jsonBytesResponse(w, http.StatusOK, data)

auditRec.AddMeta("boardsCount", len(boards))
auditRec.Success()
}
50 changes: 32 additions & 18 deletions webapp/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {FormattedMessage, useIntl} from 'react-intl'
import React, {useCallback, useEffect, useMemo, useState} from 'react'
import {FormattedMessage} from 'react-intl'
import {DragDropContext, Droppable, DropResult} from 'react-beautiful-dnd'

import {getActiveThemeName, loadTheme} from '../../theme'
import IconButton from '../../widgets/buttons/iconButton'
import HamburgerIcon from '../../widgets/icons/hamburger'
import HideSidebarIcon from '../../widgets/icons/hideSidebar'
import ShowSidebarIcon from '../../widgets/icons/showSidebar'
import {getAllSortedBoards, getCurrentBoard, getMySortedBoards} from '../../store/boards'
import {getCurrentBoard, getMySortedBoards} from '../../store/boards'
import {useAppDispatch, useAppSelector} from '../../store/hooks'
import {Utils} from '../../utils'
import {IUser} from '../../user'
Expand Down Expand Up @@ -72,22 +72,10 @@ const Sidebar = (props: Props) => {
const [isHidden, setHidden] = useState(false)
const [userHidden, setUserHidden] = useState(false)
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions())
const isUserAdmin = useAppSelector<boolean>(isAdmin);
const myBoards = useAppSelector(getMySortedBoards)
const allBoards = useAppSelector(getAllSortedBoards)
// For regular category display, always use myBoards (even for admin)
const boards = myBoards

// Admin-only boards = allBoards minus myBoards
const adminOnlyBoards = useMemo((): Board[] => {
if (!isUserAdmin) {
return []
}

const myBoardIDs = new Set(myBoards.map(b => b.id))
return allBoards.filter((board) => !myBoardIDs.has(board.id))
}, [isUserAdmin, myBoards, allBoards])
const [adminOnlyBoards, setAdminOnlyBoards] = useState<Board[]>([])

const isUserAdmin = useAppSelector<boolean>(isAdmin);
const boards = useAppSelector(getMySortedBoards)
const dispatch = useAppDispatch()
const sidebarCategories = useAppSelector<CategoryBoards[]>(getSidebarCategories)
const me = useAppSelector<IUser|null>(getMe)
Expand All @@ -96,6 +84,32 @@ const Sidebar = (props: Props) => {
const teamId = useAppSelector(getCurrentTeamId)
const team = useAppSelector(getCurrentTeam)

// Fetch admin-only boards separately
useEffect(() => {
const fetchAdminBoards = async () => {
if (!isUserAdmin || !team) {
setAdminOnlyBoards([])
return
}

try {
// Fetch all boards for team (admin endpoint)
const allBoards = await octoClient.getAllBoardsAdmin(team.id)

// Filter out boards the admin is already a member of
const myBoardIDs = new Set(boards.map(b => b.id))
const otherBoards = allBoards.filter((board: Board) => !myBoardIDs.has(board.id))

setAdminOnlyBoards(otherBoards)
} catch (err) {
console.error('Failed to fetch admin boards:', err)
setAdminOnlyBoards([])
}
}

fetchAdminBoards()
}, [isUserAdmin, team?.id, boards])

useEffect(() => {
const categoryOnChangeHandler = (_: WSClient, categories: Category[]) => {
dispatch(updateCategories(categories))
Expand Down
24 changes: 24 additions & 0 deletions webapp/src/octoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,18 @@ class OctoClient {
return boards
}

async getAllBoardsForTeamAdmin(teamId: string): Promise<Board[]> {
const path = `/api/v2/teams/${teamId}/boards/admin`
const response = await fetch(this.getBaseURL() + path, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return []
}
return (await response.json()) as Board[]
}

private async getBoardMembersWithPath(path: string): Promise<BoardMember[]> {
const response = await fetch(this.getBaseURL() + path, {headers: this.headers()})
if (response.status !== 200) {
Expand Down Expand Up @@ -789,6 +801,18 @@ class OctoClient {
return this.getBoardsWithPath(path)
}

async getAllBoardsAdmin(teamId: string): Promise<Board[]> {
const path = `/api/v2/teams/${teamId}/boards/admin`
const response = await fetch(this.getBaseURL() + path, {
method: 'GET',
headers: this.headers(),
})
if (response.status !== 200) {
return []
}
return (await response.json()) as Board[]
}

async getBoard(boardID: string): Promise<Board | undefined> {
let path = `/api/v2/boards/${boardID}`
const readToken = Utils.getReadToken()
Expand Down
7 changes: 0 additions & 7 deletions webapp/src/store/boards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,6 @@ export const getMySortedBoards = createSelector(
},
)

export const getAllSortedBoards = createSelector(
getBoards,
(boards) => {
return Object.values(boards).sort((a, b) => a.title.localeCompare(b.title))
},
)

export const getTemplates = (state: RootState): {[key: string]: Board} => state.boards.templates

export const getSortedTemplates = createSelector(
Expand Down
Loading