Add board membership read endpoint (#2799)

* Add board membership read endpoint

* Paginate board membership read endpoint
This commit is contained in:
Rob Zolkos
2026-04-09 12:54:25 -04:00
committed by GitHub
parent e70047ca0f
commit 69daae79f1
5 changed files with 178 additions and 0 deletions
@@ -0,0 +1,14 @@
class Boards::AccessesController < ApplicationController
include BoardScoped
def index
set_page_and_extract_portion_from @board.account.users.active.alphabetically.includes(:identity)
end
private
def involvement_by_user
@involvement_by_user ||= @board.accesses.where(user_id: @page.records.map(&:id)).pluck(:user_id, :involvement).to_h
end
helper_method :involvement_by_user
end
@@ -0,0 +1,8 @@
json.board_id @board.id
json.all_access @board.all_access?
json.users @page.records do |user|
json.partial! "users/user", user: user
json.has_access involvement_by_user.key?(user.id)
json.involvement involvement_by_user[user.id]
end
+1
View File
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
resources :boards do
scope module: :boards do
resources :accesses, only: :index
resource :subscriptions
resource :involvement
resource :publication
+50
View File
@@ -136,6 +136,56 @@ __Response:__
Returns `204 No Content` on success.
## Board Accesses
Board accesses let you see who has access to a board and their involvement level (watching or access only). Any board member can view this information.
### `GET /:account_slug/boards/:board_id/accesses`
Returns a paginated list of active account users with their access status for the specified board.
__Response:__
```json
{
"board_id": "03f5v9zkft4hj9qq0lsn9ohcm",
"all_access": false,
"users": [
{
"id": "03f5v9zjw7pz8717a4no1h8a7",
"name": "David Heinemeier Hansson",
"role": "owner",
"active": true,
"email_address": "david@example.com",
"created_at": "2025-12-05T19:36:35.401Z",
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7",
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7/avatar",
"has_access": true,
"involvement": "watching"
},
{
"id": "03f5v9zppzlksuj4mxba2nbzn",
"name": "Kevin Clark",
"role": "admin",
"active": true,
"email_address": "kevin@example.com",
"created_at": "2025-12-05T19:36:35.401Z",
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zppzlksuj4mxba2nbzn",
"avatar_url": "http://fizzy.localhost:3006/897362094/users/03f5v9zppzlksuj4mxba2nbzn/avatar",
"has_access": false,
"involvement": null
}
]
}
```
- `has_access` indicates whether the user has access to the board
- `involvement` is `"watching"`, `"access_only"`, or `null` (when the user does not have access)
- When `all_access` is `true`, all active account users have access to the board
- The `users` array contains the current page of results; if there are more users, follow the `Link` response header with `rel="next"`
To change who has access, use `PUT /:account_slug/boards/:board_id` with the `user_ids` parameter.
## Board Publications
Publishing a board makes it publicly accessible via a shareable link, without requiring authentication. Only board administrators can publish or unpublish a board.
@@ -0,0 +1,105 @@
require "test_helper"
class Boards::AccessesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
end
test "index returns active account users with access status" do
board = boards(:writebook)
get board_accesses_path(board), as: :json
assert_response :success
body = @response.parsed_body
assert_equal board.id, body["board_id"]
assert body["all_access"]
user_ids = body["users"].map { |u| u["id"] }
accounts("37s").users.active.each do |user|
assert_includes user_ids, user.id
end
end
test "index includes has_access and involvement for users with access" do
board = boards(:writebook)
board.access_for(users(:kevin)).update!(involvement: :watching)
get board_accesses_path(board), as: :json
kevin_entry = @response.parsed_body["users"].find { |u| u["id"] == users(:kevin).id }
assert kevin_entry["has_access"]
assert_equal "watching", kevin_entry["involvement"]
end
test "index shows has_access false and nil involvement for users without access" do
board = boards(:private)
get board_accesses_path(board), as: :json
assert_response :success
david_entry = @response.parsed_body["users"].find { |u| u["id"] == users(:david).id }
assert_not david_entry["has_access"]
assert_nil david_entry["involvement"]
end
test "index includes standard user fields" do
get board_accesses_path(boards(:writebook)), as: :json
user_entry = @response.parsed_body["users"].first
assert user_entry.key?("id")
assert user_entry.key?("name")
assert user_entry.key?("role")
assert user_entry.key?("email_address")
assert user_entry.key?("avatar_url")
end
test "index requires board access" do
logout_and_sign_in_as :david
board = boards(:private)
get board_accesses_path(board), as: :json
assert_response :not_found
end
test "index is accessible to non-admin board members" do
logout_and_sign_in_as :jz
board = boards(:writebook)
get board_accesses_path(board), as: :json
assert_response :success
end
test "index paginates account users" do
account = accounts("37s")
board = boards(:private)
200.times do |index|
identity = Identity.create!(email_address: "board-membership-#{index}@example.com")
account.users.create!(identity: identity, name: "Board Membership User #{index}", role: :member)
end
expected_ids = account.users.active.alphabetically.pluck(:id)
actual_ids = []
next_page = board_accesses_path(board, format: :json)
page_count = 0
while next_page
get next_page, as: :json
assert_response :success
page_count += 1
actual_ids.concat(@response.parsed_body["users"].map { |user| user["id"] })
next_page = next_page_from_link_header(@response.headers["Link"])
end
assert_equal expected_ids, actual_ids
assert_operator page_count, :>, 1
end
private
def next_page_from_link_header(link_header)
url = link_header&.match(/<([^>]+)>;\s*rel="next"/)&.captures&.first
URI.parse(url).request_uri if url
end
end